Refactor AI Agent configuration and workflow handling

- Removed runtime mode handling from AIAgentConfigWorkbench and related components.
- Updated tests to reflect changes in AI Agent policy copy and configuration.
- Changed terminology from "workflow" to "revision" in various components and API responses.
- Simplified agent binding logic in channel editing.
- Cleaned up unused variables and types related to runtime modes.
- Updated localization files for consistency with new terminology.
This commit is contained in:
mlogclub
2026-07-27 23:29:02 +08:00
parent 241f274927
commit 847f688398
97 changed files with 1666 additions and 5941 deletions
@@ -138,9 +138,9 @@ export function DashboardHome() {
</div>
</div>
<div className="rounded-md border bg-background px-3 py-2.5">
<div className="text-sm text-muted-foreground">{t("dashboardHome.todaySkillRunFailCount")}</div>
<div className="text-sm text-muted-foreground">{t("dashboardHome.todayAgentRunFailCount")}</div>
<div className="mt-1 text-2xl font-semibold">
{data.aiStats.todaySkillRunFailCount}
{data.aiStats.todayAgentRunFailCount}
</div>
</div>
<div className="rounded-md border bg-background px-3 py-2.5">
+4 -9
View File
@@ -11,7 +11,7 @@ import { ProjectDialog } from "@/components/project-dialog"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Textarea } from "@/components/ui/textarea"
import { fetchAgentRun, fetchAgentRunMetrics, fetchAgentRuns, fetchAIWorkflowRun, fetchAgentRunEngineComparisons, saveAgentRunQualityFeedback, type AgentRun, type AgentRunEngineComparison, type AgentRunMetrics, type AgentStep, type AgentToolCall, type AIWorkflowRun } from "@/lib/api/admin"
import { fetchAgentRun, fetchAgentRunMetrics, fetchAgentRuns, fetchAIWorkflowRun, saveAgentRunQualityFeedback, type AgentRun, type AgentRunMetrics, type AgentStep, type AgentToolCall, type AIWorkflowRun } from "@/lib/api/admin"
import { formatDateTime } from "@/lib/utils"
import { useI18n } from "@/i18n/provider"
import { WorkflowRunAuditGraph } from "../ai-workflow-runs/_components/workflow-run-audit-graph"
@@ -32,11 +32,9 @@ export default function DashboardAgentRunsPage() {
const [workflowAuditLoading, setWorkflowAuditLoading] = useState(false)
const [workflowRun, setWorkflowRun] = useState<AIWorkflowRun | null>(null)
const [metrics, setMetrics] = useState<AgentRunMetrics | null>(null)
const [comparisons, setComparisons] = useState<AgentRunEngineComparison[]>([])
useEffect(() => {
void fetchAgentRunMetrics().then(setMetrics).catch(() => setMetrics(null))
void fetchAgentRunEngineComparisons().then(setComparisons).catch(() => setComparisons([]))
}, [])
async function openDetail(id: number) {
@@ -80,12 +78,10 @@ export default function DashboardAgentRunsPage() {
<Metric label="知识兜底率" value={`${Math.round(metrics.knowledgeFallbackRate * 100)}%`} detail="证据不足或检索失败" />
<Metric label="中断恢复率" value={metrics.resumedInterrupts ? `${Math.round(metrics.interruptRecoveryRate * 100)}%` : "-"} detail={`${metrics.resolvedInterrupts}/${metrics.resumedInterrupts}`} />
</div> : null}
{comparisons.length > 0 ? <section className="border-b"><div className="px-4 py-3 text-sm font-medium"></div><div className="overflow-x-auto"><table className="w-full min-w-[760px] text-sm"><thead className="border-y bg-muted/30 text-left text-xs text-muted-foreground"><tr><th className="px-4 py-2 font-medium"></th><th className="px-4 py-2 text-right font-medium"></th><th className="px-4 py-2 text-right font-medium"></th><th className="px-4 py-2 text-right font-medium"></th><th className="px-4 py-2 text-right font-medium"></th><th className="px-4 py-2 text-right font-medium"></th><th className="px-4 py-2 text-right font-medium">P95</th><th className="px-4 py-2 text-right font-medium">Token</th></tr></thead><tbody>{comparisons.map((item) => <tr key={item.engineCode} className="border-b last:border-0"><td className="px-4 py-2 font-medium">{item.engineCode}</td><td className="px-4 py-2 text-right">{item.metrics.totalRuns}</td><td className="px-4 py-2 text-right">{Math.round(item.metrics.completionRate * 100)}%</td><td className="px-4 py-2 text-right">{item.metrics.reviewedRuns ? `${Math.round(item.metrics.resolutionRate * 100)}%` : "-"}</td><td className="px-4 py-2 text-right">{item.metrics.reviewedRuns ? `${Math.round(item.metrics.unsupportedEvidenceRate * 100)}%` : "-"}</td><td className="px-4 py-2 text-right">{item.metrics.toolCalls ? `${Math.round(item.metrics.toolSuccessRate * 100)}%` : "-"}</td><td className="px-4 py-2 text-right">{item.metrics.p95DurationMs} ms</td><td className="px-4 py-2 text-right">{item.metrics.promptTokens + item.metrics.completionTokens}</td></tr>)}</tbody></table></div></section> : null}
<DashboardListPage<AgentRun>
filters={[
{ name: "conversationId", label: t("agentRun.conversation"), defaultValue: "", valueType: "number", className: "w-full sm:w-40" },
{ name: "aiAgentId", label: t("agentRun.agent"), defaultValue: "", valueType: "number", className: "w-full sm:w-40" },
{ name: "engineCode", label: t("agentRun.engine"), defaultValue: "", className: "w-full sm:w-40" },
{ name: "status", label: t("agentRun.status"), defaultValue: "", className: "w-full sm:w-40" },
]}
fetchList={fetchAgentRuns}
@@ -94,7 +90,6 @@ export default function DashboardAgentRunsPage() {
onRowClick={(item) => void openDetail(item.id)}
columns={[
{ key: "startedAt", label: t("agentRun.startedAt"), className: "w-42 text-xs text-muted-foreground", render: (item) => formatDateTime(item.startedAt || item.createdAt) },
{ key: "engine", label: t("agentRun.engine"), className: "w-32", render: (item) => item.engineCode || "-" },
{ key: "agent", label: t("agentRun.agent"), className: "w-28", render: (item) => `#${item.aiAgentId || "-"}` },
{ key: "conversation", label: t("agentRun.conversation"), className: "w-28", render: (item) => `#${item.conversationId || "-"}` },
{ key: "status", label: t("agentRun.status"), className: "w-30", render: (item) => <Badge variant={statusVariant(item.status)}>{item.status || "-"}</Badge> },
@@ -115,8 +110,8 @@ function Metric({ label, value, detail }: { label: string; value: string; detail
function AgentRunDetailDialog({ open, loading, run, onOpenChange, onOpenWorkflowAudit, onQualityFeedbackSaved, t }: { open: boolean; loading: boolean; run: AgentRun | null; onOpenChange: (open: boolean) => void; onOpenWorkflowAudit: (workflowRunId: number) => void; onQualityFeedbackSaved: (agentRunId: number) => void; t: (key: string) => string }) {
return <ProjectDialog open={open} onOpenChange={onOpenChange} size="xl" allowFullscreen defaultFullscreen title={<span className="flex items-center gap-2"><BotMessageSquareIcon className="size-4" />{t("agentRun.detailTitle")}</span>} description={run ? `Run #${run.id}` : t("agentRun.detailDescription")} footer={<Button variant="outline" onClick={() => onOpenChange(false)}>{t("agentRun.close")}</Button>}>
{loading ? <div className="py-10 text-sm text-muted-foreground">{t("agentRun.loadingDetail")}</div> : run ? <div className="space-y-4">
<div className="flex flex-wrap gap-2 rounded-md border bg-muted/20 px-3 py-2 text-xs"><Meta label={t("agentRun.engine")} value={run.engineCode} /><Meta label={t("agentRun.status")} value={run.status} /><Meta label={t("agentRun.agent")} value={`#${run.aiAgentId}`} /><Meta label={t("agentRun.revision")} value={`#${run.agentRevisionId || "-"}`} /><Meta label={t("agentRun.duration")} value={`${run.durationMs || 0} ms`} /><Meta label={t("agentRun.tokens")} value={`${run.promptTokens || 0}/${run.completionTokens || 0}`} /></div>
{run.workflowRunId > 0 ? <section className="flex items-center justify-between gap-3 border px-3 py-2"><div><div className="text-sm font-medium"> Playbook </div><div className="text-xs text-muted-foreground">Workflow Run #{run.workflowRunId} </div></div><Button type="button" variant="outline" size="sm" onClick={() => onOpenWorkflowAudit(run.workflowRunId)}><WorkflowIcon /></Button></section> : null}
<div className="flex flex-wrap gap-2 rounded-md border bg-muted/20 px-3 py-2 text-xs"><Meta label={t("agentRun.status")} value={run.status} /><Meta label={t("agentRun.agent")} value={`#${run.aiAgentId}`} /><Meta label={t("agentRun.revision")} value={`#${run.agentRevisionId || "-"}`} /><Meta label={t("agentRun.duration")} value={`${run.durationMs || 0} ms`} /><Meta label={t("agentRun.tokens")} value={`${run.promptTokens || 0}/${run.completionTokens || 0}`} /></div>
{run.workflowRunId > 0 ? <section className="flex items-center justify-between gap-3 border px-3 py-2"><div><div className="text-sm font-medium"> Workflow </div><div className="text-xs text-muted-foreground">Workflow Run #{run.workflowRunId} </div></div><Button type="button" variant="outline" size="sm" onClick={() => onOpenWorkflowAudit(run.workflowRunId)}><WorkflowIcon /></Button></section> : null}
<QualityFeedbackPanel run={run} onSaved={onQualityFeedbackSaved} />
{run.errorMessage ? <div className="flex gap-2 rounded-md border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive"><AlertTriangleIcon className="size-4 shrink-0" />{run.errorMessage}</div> : null}
<Preview title={t("agentRun.trace")} raw={run.traceData} />
@@ -155,7 +150,7 @@ function QualityFeedbackPanel({ run, onSaved }: { run: AgentRun; onSaved: (agent
}
function WorkflowAuditDialog({ open, loading, run, onOpenChange }: { open: boolean; loading: boolean; run: AIWorkflowRun | null; onOpenChange: (open: boolean) => void }) {
return <ProjectDialog open={open} onOpenChange={onOpenChange} size="xl" allowFullscreen defaultFullscreen title={<span className="flex items-center gap-2"><WorkflowIcon className="size-4" />Workflow </span>} description={run ? `Workflow Run #${run.id}` : "加载关联 Playbook 的节点审计"} footer={<Button variant="outline" onClick={() => onOpenChange(false)}></Button>}>
return <ProjectDialog open={open} onOpenChange={onOpenChange} size="xl" allowFullscreen defaultFullscreen title={<span className="flex items-center gap-2"><WorkflowIcon className="size-4" />Workflow </span>} description={run ? `Workflow Run #${run.id}` : "加载关联 Workflow 的节点审计"} footer={<Button variant="outline" onClick={() => onOpenChange(false)}></Button>}>
{loading ? <div className="py-10 text-sm text-muted-foreground">...</div> : run ? <div className="space-y-3"><div className="flex flex-wrap gap-2 rounded-md border bg-muted/20 px-3 py-2 text-xs"><Meta label="状态" value={run.statusName} /><Meta label="Workflow" value={run.workflowName || `#${run.workflowId}`} /><Meta label="版本" value={`v${run.workflowVersion || "-"}`} /><Meta label="时延" value={`${run.durationMs || 0} ms`} /></div>{run.errorMessage ? <div className="flex gap-2 rounded-md border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive"><AlertTriangleIcon className="size-4 shrink-0" />{run.errorMessage}</div> : null}<WorkflowRunAuditGraph run={run} /></div> : <div className="py-10 text-sm text-muted-foreground"> Workflow Run</div>}
</ProjectDialog>
}
@@ -7,7 +7,7 @@ const zhMessagesSource = await readFile(new URL("../../../../messages/zh-CN.json
const adminApiSource = await readFile(new URL("../../../../lib/api/admin.ts", import.meta.url), "utf8")
const zhMessages = JSON.parse(zhMessagesSource)
test("AI Agent workflow-era policy copy separates handoff execution from knowledge fallback", () => {
test("AI Agent policy copy separates handoff execution from knowledge fallback", () => {
const combinedSource = `${configWorkbenchSource}\n${zhMessagesSource}`
assert.match(combinedSource, /转人工执行方式/)
@@ -31,3 +31,11 @@ test("AI Agent config no longer exposes legacy graph tool routing knobs", () =>
assert.doesNotMatch(aiAgentMessages, /Graph Tool/)
assert.doesNotMatch(aiAgentMessages, /内置流程/)
})
test("AI Agent config uses one Agent Loop without a runtime mode selector", () => {
assert.doesNotMatch(configWorkbenchSource, /runtimeMode/)
assert.doesNotMatch(adminApiSource, /runtimeMode/)
assert.doesNotMatch(configWorkbenchSource, /运行方式/)
assert.match(configWorkbenchSource, /Workflow 是 Agent 的可选能力/)
assert.match(configWorkbenchSource, /写操作(需确认)/)
})
@@ -61,7 +61,6 @@ import {
} from "@/lib/generated/enums"
import { cn } from "@/lib/utils"
type RuntimeMode = "workflow" | "autonomous" | "hybrid"
type SectionKey = "setup" | "persona" | "capability" | "service"
type MCPToolItem = CreateAIAgentPayload["mcpTools"][number]
@@ -74,28 +73,6 @@ type MCPToolOption = {
meta: MCPToolItem
}
const runtimeModes: {
value: RuntimeMode
title: string
description: string
}[] = [
{
value: "autonomous",
title: "自主接待",
description: "自主选择知识和工具处理请求,工作流可选。",
},
{
value: "hybrid",
title: "自主接待 + 工作流",
description: "自主处理咨询,按需调用一个或多个工作流。",
},
{
value: "workflow",
title: "仅工作流",
description: "严格按一个已发布工作流处理会话。",
},
]
function toText(value: string | number | undefined | null) {
if (value === undefined || value === null || value === 0) return ""
return String(value)
@@ -105,10 +82,6 @@ function uniqueNumbers(input: number[]) {
return Array.from(new Set(input.filter((id) => Number.isFinite(id) && id > 0)))
}
function isWorkflowPublished(agent: AIAgent | null) {
return Boolean(agent?.workflowPublished ?? (agent?.workflowVersionId ?? 0) > 0)
}
export function AIAgentConfigWorkbench({
agentId,
onAgentSaved,
@@ -131,7 +104,6 @@ export function AIAgentConfigWorkbench({
const [name, setName] = useState("")
const [description, setDescription] = useState("")
const [aiConfigId, setAIConfigId] = useState("")
const [runtimeMode, setRuntimeMode] = useState<RuntimeMode>("autonomous")
const [serviceMode, setServiceMode] = useState(String(IMConversationServiceMode.AIFirst))
const [systemPrompt, setSystemPrompt] = useState("")
const [welcomeMessage, setWelcomeMessage] = useState("")
@@ -186,7 +158,6 @@ export function AIAgentConfigWorkbench({
setName("")
setDescription("")
setAIConfigId("")
setRuntimeMode("autonomous")
setServiceMode(String(IMConversationServiceMode.AIFirst))
setSystemPrompt("")
setWelcomeMessage("")
@@ -211,11 +182,6 @@ export function AIAgentConfigWorkbench({
setName(detail.name)
setDescription(detail.description || "")
setAIConfigId(toText(detail.aiConfigId))
setRuntimeMode(
detail.runtimeMode === "autonomous" || detail.runtimeMode === "hybrid"
? detail.runtimeMode
: "workflow",
)
setServiceMode(String(detail.serviceMode || IMConversationServiceMode.AIFirst))
setSystemPrompt(detail.systemPrompt || "")
setWelcomeMessage(detail.welcomeMessage || "")
@@ -315,6 +281,8 @@ export function AIAgentConfigWorkbench({
toolName: tool.toolName,
title: tool.title || tool.toolName,
description: tool.description || "",
riskLevel: "read",
requireConfirmation: false,
arguments: undefined,
},
})),
@@ -345,6 +313,8 @@ export function AIAgentConfigWorkbench({
function setMCPToolSelection(values: string[]) {
setMCPTools(
values.flatMap((value) => {
const current = mcpTools.find((item) => item.toolCode === value)
if (current) return [current]
const option = mcpToolOptions.find((item) => item.value === value)
return option ? [option.meta] : []
}),
@@ -352,12 +322,7 @@ export function AIAgentConfigWorkbench({
}
function setWorkflowSelection(values: string[]) {
let selectedVersionIds = values.map(Number).filter((value) => value > 0)
if (runtimeMode === "workflow" && selectedVersionIds.length > 1) {
const currentIds = workflowBindings.map((binding) => binding.workflowVersionId)
const newlySelected = selectedVersionIds.find((id) => !currentIds.includes(id))
selectedVersionIds = [newlySelected ?? selectedVersionIds.at(-1)!]
}
const selectedVersionIds = values.map(Number).filter((value) => value > 0)
setWorkflowBindings(
selectedVersionIds.flatMap((workflowVersionId, index) => {
const current = workflowBindings.find(
@@ -394,16 +359,6 @@ export function AIAgentConfigWorkbench({
toast.error("请选择 AI 配置")
return false
}
if (runtimeMode === "hybrid" && workflowBindings.length === 0) {
setActiveSection("capability")
toast.error("Hybrid 模式至少需要关联一个已发布工作流")
return false
}
if (runtimeMode === "workflow" && workflowBindings.length !== 1) {
setActiveSection("capability")
toast.error("仅工作流模式必须且只能关联一个已发布工作流")
return false
}
return true
}
@@ -412,7 +367,6 @@ export function AIAgentConfigWorkbench({
name: name.trim(),
description: description.trim(),
aiConfigId: Number(aiConfigId),
runtimeMode,
serviceMode: Number(serviceMode),
systemPrompt: systemPrompt.trim(),
welcomeMessage: welcomeMessage.trim(),
@@ -454,7 +408,7 @@ export function AIAgentConfigWorkbench({
}
async function publishAgent() {
if (!agent || runtimeMode === "workflow") return
if (!agent) return
if (!validateForm()) return
setSaving(true)
try {
@@ -484,19 +438,7 @@ export function AIAgentConfigWorkbench({
}
}
const workflowPublished = isWorkflowPublished(agent)
const autonomousPublished =
runtimeMode === "autonomous" && (agent?.publishedRevisionId ?? 0) > 0
const hybridPublished =
runtimeMode === "hybrid" &&
workflowPublished &&
(agent?.publishedRevisionId ?? 0) > 0
const runtimePublished =
runtimeMode === "workflow"
? workflowPublished
: runtimeMode === "hybrid"
? hybridPublished
: autonomousPublished
const agentPublished = (agent?.publishedRevisionId ?? 0) > 0
const sections: {
key: SectionKey
@@ -527,8 +469,8 @@ export function AIAgentConfigWorkbench({
<div className="flex min-w-0 items-center gap-2">
<h1 className="truncate text-base font-semibold">{agent?.name || "新建 AI Agent"}</h1>
{agent?.statusName ? <Badge variant="secondary">{agent.statusName}</Badge> : null}
<Badge variant={runtimePublished ? "default" : "outline"}>
{runtimePublished ? "已发布" : agent ? "未发布" : "尚未创建"}
<Badge variant={agentPublished ? "default" : "outline"}>
{agentPublished ? "已发布" : agent ? "未发布" : "尚未创建"}
</Badge>
</div>
{agent ? (
@@ -574,8 +516,8 @@ export function AIAgentConfigWorkbench({
</nav>
<div className="mt-auto flex items-center justify-between rounded-lg border bg-background p-3 text-xs">
<span className="text-muted-foreground"></span>
<span className={runtimePublished ? "font-medium text-emerald-600" : "font-medium text-amber-600"}>
{runtimePublished ? "已发布" : agent ? "未发布" : "尚未创建"}
<span className={agentPublished ? "font-medium text-emerald-600" : "font-medium text-amber-600"}>
{agentPublished ? "已发布" : agent ? "未发布" : "尚未创建"}
</span>
</div>
</aside>
@@ -610,39 +552,6 @@ export function AIAgentConfigWorkbench({
</div>
</FormSection>
<FormSection
title="运行方式"
description="决定 Agent 是否自主处理请求,以及工作流的关联要求。"
>
<div className="grid gap-3 md:grid-cols-3">
{runtimeModes.map((mode) => (
<button
key={mode.value}
type="button"
onClick={() => setRuntimeMode(mode.value)}
className={cn(
"relative rounded-xl border p-4 text-left transition-colors hover:border-primary/40 hover:bg-primary/[0.02]",
runtimeMode === mode.value &&
"border-primary bg-primary/5 ring-1 ring-primary",
)}
>
<span
className={cn(
"absolute top-4 right-4 size-4 rounded-full border",
runtimeMode === mode.value
? "border-[5px] border-primary"
: "border-muted-foreground/40",
)}
/>
<strong className="block pr-6 text-sm">{mode.title}</strong>
<span className="mt-2 block pr-5 text-xs leading-5 text-muted-foreground">
{mode.description}
</span>
</button>
))}
</div>
</FormSection>
<FormSection
title="模型与响应"
description="选择推理模型并设置单次回复的超时时间。"
@@ -750,11 +659,7 @@ export function AIAgentConfigWorkbench({
}
>
<div className="rounded-lg border border-blue-200 bg-blue-50 px-3.5 py-3 text-sm text-blue-900 dark:border-blue-900 dark:bg-blue-950/30 dark:text-blue-200">
{runtimeMode === "autonomous"
? "当前为自主接待模式,工作流是可选能力。"
: runtimeMode === "hybrid"
? "当前为 Hybrid 模式,至少关联一个已发布工作流后才能保存。"
: "当前为仅工作流模式,必须且只能关联一个已发布工作流。"}
Workflow Agent
</div>
<OptionCombobox
multiple
@@ -780,6 +685,67 @@ export function AIAgentConfigWorkbench({
emptyText="没有可用 MCP Tool"
onValuesChange={setMCPToolSelection}
/>
{mcpTools.length > 0 ? (
<div className="space-y-2">
{mcpTools.map((tool) => (
<div
key={tool.toolCode}
className="flex flex-wrap items-center justify-between gap-3 rounded-lg border px-3 py-2"
>
<div className="min-w-0">
<div className="truncate text-sm font-medium">
{tool.title || tool.toolCode}
</div>
<div className="truncate font-mono text-xs text-muted-foreground">
{tool.toolCode}
</div>
</div>
<div className="flex items-center gap-2">
<Button
type="button"
size="sm"
variant={tool.riskLevel === "read" ? "default" : "outline"}
onClick={() =>
setMCPTools((items) =>
items.map((item) =>
item.toolCode === tool.toolCode
? {
...item,
riskLevel: "read",
requireConfirmation: false,
}
: item,
),
)
}
>
</Button>
<Button
type="button"
size="sm"
variant={tool.riskLevel === "write" ? "destructive" : "outline"}
onClick={() =>
setMCPTools((items) =>
items.map((item) =>
item.toolCode === tool.toolCode
? {
...item,
riskLevel: "write",
requireConfirmation: true,
}
: item,
),
)
}
>
</Button>
</div>
</div>
))}
</div>
) : null}
</FormSection>
</div>
) : null}
@@ -868,10 +834,10 @@ export function AIAgentConfigWorkbench({
<span
className={cn(
"size-2 rounded-full",
runtimePublished ? "bg-emerald-500" : "bg-amber-500",
agentPublished ? "bg-emerald-500" : "bg-amber-500",
)}
/>
<span>{runtimePublished ? "当前配置已发布" : "保存配置后再发布 Agent"}</span>
<span>{agentPublished ? "当前配置已发布" : "保存配置后再发布 Agent"}</span>
</div>
<div className="flex items-center gap-2">
<Button type="button" variant="outline" onClick={onCancel}>
@@ -886,7 +852,7 @@ export function AIAgentConfigWorkbench({
<SaveIcon />
</Button>
{agent && runtimeMode !== "workflow" ? (
{agent ? (
<Button type="button" disabled={saving} onClick={publishAgent}>
Agent
</Button>
@@ -1012,7 +978,7 @@ function VersionRecordsTable({
<TableHead className="w-28"></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
@@ -1033,10 +999,8 @@ function VersionRecordsTable({
{revision.publishedAt || "-"}
</TableCell>
<TableCell>{revision.publishedByName || "-"}</TableCell>
<TableCell>
{revision.workflowVersionId > 0
? `#${revision.workflowVersionId}`
: "-"}
<TableCell className="font-mono text-xs text-muted-foreground">
{revision.definitionHash?.slice(0, 12) || "-"}
</TableCell>
<TableCell className="text-right">
<Button
+11 -18
View File
@@ -62,10 +62,6 @@ function getNextStatus(item: AIAgent) {
return item.status === Status.Ok ? Status.Disabled : Status.Ok;
}
function isWorkflowPublished(item: AIAgent) {
return Boolean(item.workflowPublished ?? item.workflowVersionId > 0);
}
export default function DashboardAIAgentsPage() {
const t = useI18n();
const statusOptions = useMemo(() => getStatusOptions(t), [t]);
@@ -126,31 +122,28 @@ export default function DashboardAIAgentsPage() {
render: (item) => getServiceModeLabel(item.serviceMode, t),
},
{
key: "workflow",
label: "工作流状态",
key: "publication",
label: "发布状态",
render: (item) => {
const published = isWorkflowPublished(item);
const published = item.publishedRevisionId > 0;
const workflowCount = item.workflowBindings?.length ?? 0;
return (
<div className="space-y-1">
<div className="flex flex-wrap items-center gap-1.5">
<Badge variant={published ? "default" : "outline"}>
{item.workflowStateText || (published ? "已发布" : "未发布")}
{published ? "已发布" : "未发布"}
</Badge>
{published ? (
<span className="font-mono text-xs text-muted-foreground">
#{item.workflowVersionId}
Revision #{item.publishedRevisionId}
</span>
) : null}
</div>
{!published ? (
<div className="text-xs text-muted-foreground">
AI
</div>
) : (
<div className="text-xs text-muted-foreground">
#{item.workflowVersionId}
</div>
)}
<div className="text-xs text-muted-foreground">
{workflowCount > 0
? `已配置 ${workflowCount} 个 Workflow`
: "由 Agent 自主判断并直接回复"}
</div>
</div>
);
},
@@ -254,14 +254,7 @@ function buildPayload(form: EditForm, status: number, t: Translate): CreateAdmin
}
function isAgentChannelBindable(agent: AIAgent | undefined) {
if (!agent) return false
if (agent.runtimeMode === "autonomous") {
return agent.publishedRevisionId > 0
}
if (agent.runtimeMode === "hybrid") {
return agent.publishedRevisionId > 0 && agent.workflowVersionId > 0
}
return Boolean(agent.workflowPublished ?? agent.workflowVersionId > 0)
return Boolean(agent && agent.publishedRevisionId > 0)
}
type ChannelFormBodyProps = Omit<ChannelFormDialogProps, "open">
@@ -431,7 +424,7 @@ function ChannelFormBody({
const aiAgentOptions = availableAIAgents.map((item) => ({
value: String(item.id),
label: isAgentChannelBindable(item)
? `${item.name} · 当前生效 #${item.workflowVersionId}`
? `${item.name} · Revision #${item.publishedRevisionId}`
: `${item.name} · 未发布`,
}))
const wxWorkKFAccountOptions = wxWorkKFAccounts.map((item) => ({
@@ -557,15 +550,15 @@ function ChannelFormBody({
/>
{selectedAIAgent && !isAgentChannelBindable(selectedAIAgent) ? (
<div className="rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-900">
Agent AI Agent
Agent AI Agent Revision
</div>
) : null}
{selectedAIAgent && isAgentChannelBindable(selectedAIAgent) ? (
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Badge variant="secondary">
{selectedAIAgent.runtimeMode === "autonomous" ? "已发布" : selectedAIAgent.workflowStateText || "已发布"}
</Badge>
<span>{selectedAIAgent.runtimeMode === "autonomous" ? `当前版本 #${selectedAIAgent.publishedRevisionId}` : `当前生效版本 #${selectedAIAgent.workflowVersionId}`}</span>
<span>Revision #{selectedAIAgent.publishedRevisionId}</span>
</div>
) : null}
<FieldError errors={[errors.aiAgentId]} />
@@ -360,9 +360,6 @@ function DebugDialogBody({
<CardContent className="space-y-3 text-sm">
<div className="flex flex-wrap gap-2">
<Badge variant="outline">{result?.skillName || skillName}</Badge>
{result?.graphToolCode ? (
<Badge variant="secondary">{result.graphToolCode}</Badge>
) : null}
{result?.interruptType ? (
<Badge variant="secondary">{result.interruptType}</Badge>
) : null}
@@ -376,12 +373,6 @@ function DebugDialogBody({
<div className="text-xs text-muted-foreground">{t("skillDefinition.skillName")}</div>
<div className="mt-1 font-medium">{result?.skillName || skillName}</div>
</div>
<div className="rounded-lg bg-muted/50 p-3">
<div className="text-xs text-muted-foreground">Plan Reason</div>
<div className="mt-1 whitespace-pre-wrap break-words">
{result?.planReason || t("skillDefinition.none")}
</div>
</div>
<div className="rounded-lg bg-muted/50 p-3">
<div className="text-xs text-muted-foreground">Reply</div>
<div className="mt-1 whitespace-pre-wrap break-words">
@@ -424,20 +415,6 @@ function DebugDialogBody({
)}
</div>
</div>
<div className="rounded-lg bg-muted/50 p-3">
<div className="text-xs text-muted-foreground">{t("skillDefinition.exposedTools")}</div>
<div className="mt-2 flex flex-wrap gap-2">
{(result?.exposedToolCodes ?? []).length > 0 ? (
result?.exposedToolCodes.map((toolCode) => (
<Badge key={toolCode} variant="outline">
{toolCode}
</Badge>
))
) : (
<span className="text-muted-foreground">{t("skillDefinition.none")}</span>
)}
</div>
</div>
<div className="rounded-lg bg-muted/50 p-3">
<div className="text-xs text-muted-foreground">{t("skillDefinition.invokedTools")}</div>
<div className="mt-2 flex flex-wrap gap-2">
@@ -457,9 +434,6 @@ function DebugDialogBody({
</div>
<div className="grid grid-cols-1 gap-4">
<ResultBlock title={t("skillDefinition.skillRouteTrace")} value={result?.skillRouteTrace} emptyText={t("skillDefinition.emptyData")} />
<ResultBlock title={t("skillDefinition.toolSearchTrace")} value={result?.toolSearchTrace} emptyText={t("skillDefinition.emptyData")} />
<ResultBlock title={t("skillDefinition.graphToolTrace")} value={result?.graphToolTrace} emptyText={t("skillDefinition.emptyData")} />
<ResultBlock title={t("skillDefinition.traceData")} value={result?.traceData} emptyText={t("skillDefinition.emptyData")} />
</div>
@@ -523,9 +497,6 @@ function DebugDialogBody({
<CardContent className="space-y-3 text-sm">
<div className="flex flex-wrap gap-2">
<Badge variant="outline">{resumeResult.skillName || skillName}</Badge>
{resumeResult.graphToolCode ? (
<Badge variant="secondary">{resumeResult.graphToolCode}</Badge>
) : null}
{resumeResult.interruptType ? (
<Badge variant="secondary">{resumeResult.interruptType}</Badge>
) : null}
@@ -547,16 +518,8 @@ function DebugDialogBody({
{resumeResult.replyText || t("skillDefinition.none")}
</div>
</div>
<div className="rounded-lg bg-muted/50 p-3">
<div className="text-xs text-muted-foreground">Resume Plan Reason</div>
<div className="mt-1 whitespace-pre-wrap break-words">
{resumeResult.planReason || t("skillDefinition.none")}
</div>
</div>
</CardContent>
</Card>
<ResultBlock title={t("skillDefinition.resumeToolSearchTrace")} value={resumeResult.toolSearchTrace} emptyText={t("skillDefinition.emptyData")} />
<ResultBlock title={t("skillDefinition.resumeGraphToolTrace")} value={resumeResult.graphToolTrace} emptyText={t("skillDefinition.emptyData")} />
<ResultBlock title={t("skillDefinition.resumeTraceData")} value={resumeResult.traceData} emptyText={t("skillDefinition.emptyData")} />
</div>
) : null}
+5 -27
View File
@@ -219,8 +219,6 @@ export type AIAgent = {
statusName: string
aiConfigId: number
aiConfigName?: string
runtimeMode: "workflow" | "autonomous" | "hybrid"
runtimeModeName: string
maxSteps: number
contextWindow: number
toolPolicy: string
@@ -247,14 +245,12 @@ export type AIAgent = {
toolName: string
title: string
description: string
riskLevel: "read" | "write"
requireConfirmation: boolean
arguments?: Record<string, string>
}[]
workflowBindings: AIAgentWorkflowBinding[]
workflowVersionId: number
publishedRevisionId: number
workflowPublished: boolean
workflowState: string
workflowStateText: string
sortNo: number
createdAt: string
updatedAt: string
@@ -266,7 +262,6 @@ export type CreateAIAgentPayload = {
name: string
description: string
aiConfigId: number
runtimeMode?: "workflow" | "autonomous" | "hybrid"
maxSteps?: number
contextWindow?: number
toolPolicy?: string
@@ -288,6 +283,8 @@ export type CreateAIAgentPayload = {
toolName: string
title: string
description: string
riskLevel: "read" | "write"
requireConfirmation: boolean
arguments?: Record<string, string>
}[]
workflowBindings: AIAgentWorkflowBindingInput[]
@@ -321,7 +318,6 @@ export type AgentRevision = {
id: number
agentId: number
revision: number
workflowVersionId: number
status: number
definitionHash: string
publishedAt: string
@@ -536,14 +532,8 @@ export type SkillDebugRunResult = {
skillDefinitionId: number
skillName: string
replyText: string
planReason: string
skillRouteTrace: string
toolWhitelist: string[]
exposedToolCodes: string[]
invokedToolCodes: string[]
toolSearchTrace: string
graphToolTrace: string
graphToolCode: string
interruptType: string
checkPointId: string
interrupted: boolean
@@ -650,7 +640,6 @@ export type AgentRun = {
agentRevisionId: number
sourceMessageId: number
workflowRunId: number
engineCode: string
status: string
promptTokens: number
completionTokens: number
@@ -701,11 +690,6 @@ export type AgentRunMetrics = {
unsupportedEvidenceRate: number
}
export type AgentRunEngineComparison = {
engineCode: string
metrics: AgentRunMetrics
}
export type AgentEvaluationCase = {
id: string
category?: string
@@ -715,13 +699,11 @@ export type AgentEvaluationCase = {
}
export type AgentEvaluationReport = {
engineCode: string
total: number
passed: number
results: {
caseId: string
category: string
engineCode: string
passed: boolean
replyText: string
interrupted: boolean
@@ -1394,11 +1376,7 @@ export function fetchAgentRunMetrics(aiAgentId?: number) {
return request<AgentRunMetrics>(`/api/dashboard/agent-run/metrics${toQueryString(aiAgentId ? { aiAgentId } : undefined)}`)
}
export function fetchAgentRunEngineComparisons(aiAgentId?: number) {
return request<AgentRunEngineComparison[]>(`/api/dashboard/agent-run/comparison${toQueryString(aiAgentId ? { aiAgentId } : undefined)}`)
}
export function runAgentEvaluation(payload: { aiAgentId: number; engineCode: string; cases: AgentEvaluationCase[] }) {
export function runAgentEvaluation(payload: { aiAgentId: number; cases: AgentEvaluationCase[] }) {
return request<AgentEvaluationReport>("/api/dashboard/agent-run/evaluate", {
method: "POST",
body: JSON.stringify(payload),
+1 -1
View File
@@ -69,7 +69,7 @@ export type DashboardOverview = {
todayKnowledgeRetrieves: number
todayKnowledgeRetrieveFailCount: number
todayKnowledgeRetrieveFailRate: number
todaySkillRunFailCount: number
todayAgentRunFailCount: number
todayAiHandoffCount: number
}
alerts: DashboardAlert[]
+2 -8
View File
@@ -419,7 +419,7 @@
"enabledChannels": "Active Channels",
"todayKnowledgeRetrieves": "Knowledge Searches Today",
"todayKnowledgeRetrieveFailRate": "Search Failure Rate Today",
"todaySkillRunFailCount": "Skill Failures Today",
"todayAgentRunFailCount": "Agent Run Failures Today",
"todayAiHandoffCount": "AI-to-Human Handoffs Today",
"empty": "No dashboard overview data yet.",
"summaryTodayNewConversations": "New Conversations Today",
@@ -1644,12 +1644,7 @@
"agentRequired": "Select an AI agent.",
"messageRequired": "Enter a user message.",
"emptyData": "No data",
"skillRouteTrace": "Skill Route Trace",
"toolSearchTrace": "Tool Search Trace",
"graphToolTrace": "Graph Tool Trace",
"traceData": "Trace Data",
"resumeToolSearchTrace": "Resume Tool Search Trace",
"resumeGraphToolTrace": "Resume Graph Tool Trace",
"resumeTraceData": "Resume Trace Data",
"confirm": "Confirm",
"reject": "Cancel",
@@ -1669,7 +1664,7 @@
"matchedAgent": "Matched Agent",
"noAgentSelected": "None selected",
"userMessage": "User Message",
"userMessagePlaceholder": "Enter a user message to debug this skill's routing, tools, and reply.",
"userMessagePlaceholder": "Enter a user message to debug this skill's capability calls and reply inside the Agent Loop.",
"debugSummary": "Debug Summary",
"interrupted": "Interrupted",
"notInterrupted": "Not interrupted",
@@ -1678,7 +1673,6 @@
"errorMessage": "Error Message",
"toolView": "Tool View",
"skillToolWhitelist": "Skill Tool Allowlist",
"exposedTools": "Tools Exposed This Run",
"invokedTools": "Tools Called This Run",
"resumeDebug": "Resume Debug",
"currentCheckpoint": "Current Checkpoint",
+2 -8
View File
@@ -419,7 +419,7 @@
"enabledChannels": "启用中的接入渠道",
"todayKnowledgeRetrieves": "今日知识检索次数",
"todayKnowledgeRetrieveFailRate": "今日检索失败率",
"todaySkillRunFailCount": "今日 Skill 失败次数",
"todayAgentRunFailCount": "今日 Agent 运行失败次数",
"todayAiHandoffCount": "今日 AI 转人工次数",
"empty": "暂无首页概览数据",
"summaryTodayNewConversations": "今日新增会话",
@@ -1645,12 +1645,7 @@
"agentRequired": "请选择 AI Agent",
"messageRequired": "请输入用户消息",
"emptyData": "暂无数据",
"skillRouteTrace": "Skill 路由追踪",
"toolSearchTrace": "工具搜索追踪",
"graphToolTrace": "Graph 工具追踪",
"traceData": "追踪数据",
"resumeToolSearchTrace": "恢复工具搜索追踪",
"resumeGraphToolTrace": "恢复 Graph 工具追踪",
"resumeTraceData": "恢复追踪数据",
"confirm": "确认",
"reject": "取消",
@@ -1670,7 +1665,7 @@
"matchedAgent": "命中 Agent",
"noAgentSelected": "未选择",
"userMessage": "用户消息",
"userMessagePlaceholder": "输入一段用户消息,调试当前 Skill 的路由、工具和回复。",
"userMessagePlaceholder": "输入一段用户消息,调试当前 Skill 在 Agent Loop 中的能力调用和回复。",
"debugSummary": "调试摘要",
"interrupted": "已中断",
"notInterrupted": "未中断",
@@ -1679,7 +1674,6 @@
"errorMessage": "错误信息",
"toolView": "工具视图",
"skillToolWhitelist": "技能工具白名单",
"exposedTools": "本轮实际暴露工具",
"invokedTools": "本轮实际调用工具",
"resumeDebug": "恢复调试",
"currentCheckpoint": "当前 Checkpoint",