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:
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user