feat: Enhance AI Agent and Channel Management

- Updated labels in the AI Agents dashboard for clarity, changing "流程状态" to "Playbook 状态" and "未发布流程" to "未发布 Playbook".
- Introduced AI Agent rollout percentage management in channel editing, allowing users to set and rollback rollout percentages.
- Added new API endpoints for rolling back AI Agent rollout and fetching agent run metrics.
- Implemented new UI components for displaying agent run details, including status, duration, and input/output tokens.
- Enhanced type definitions for AdminChannel and AIAgent to include rollout percentages and runtime modes.
- Updated navigation to include a section for agent runs.
- Added new translations for agent run features in both English and Chinese.
This commit is contained in:
mlogclub
2026-07-25 12:04:06 +08:00
parent 45741d4032
commit 34051a4631
101 changed files with 8377 additions and 340 deletions
+167
View File
@@ -0,0 +1,167 @@
"use client"
import { useEffect, useState } from "react"
import { AlertTriangleIcon, BotMessageSquareIcon, Clock3Icon, WorkflowIcon, WrenchIcon } from "lucide-react"
import { toast } from "sonner"
import { DashboardListPage } from "@/components/dashboard/list"
import { JsonTreeViewer } from "@/components/json-tree-viewer"
import { OptionCombobox } from "@/components/option-combobox"
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 { formatDateTime } from "@/lib/utils"
import { useI18n } from "@/i18n/provider"
import { WorkflowRunAuditGraph } from "../ai-workflow-runs/_components/workflow-run-audit-graph"
function statusVariant(status: string) {
if (status === "failed") return "destructive" as const
if (status === "interrupted") return "outline" as const
if (status === "completed") return "default" as const
return "secondary" as const
}
export default function DashboardAgentRunsPage() {
const t = useI18n()
const [open, setOpen] = useState(false)
const [loading, setLoading] = useState(false)
const [run, setRun] = useState<AgentRun | null>(null)
const [workflowAuditOpen, setWorkflowAuditOpen] = useState(false)
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) {
setOpen(true)
setLoading(true)
try {
setRun(await fetchAgentRun(id))
} catch (error) {
toast.error(error instanceof Error ? error.message : t("agentRun.loadDetailFailed"))
setOpen(false)
} finally {
setLoading(false)
}
}
async function openWorkflowAudit(id: number) {
if (id <= 0) return
setWorkflowAuditOpen(true)
setWorkflowAuditLoading(true)
try {
setWorkflowRun(await fetchAIWorkflowRun(id))
} catch (error) {
toast.error(error instanceof Error ? error.message : "加载 Workflow 节点审计失败")
setWorkflowAuditOpen(false)
} finally {
setWorkflowAuditLoading(false)
}
}
return (
<>
{metrics ? <div className="grid grid-cols-2 gap-px border-b bg-border sm:grid-cols-4 lg:grid-cols-5 xl:grid-cols-10">
<Metric label="运行完成率" value={`${Math.round(metrics.completionRate * 100)}%`} detail={`${metrics.completedRuns}/${metrics.totalRuns}`} />
<Metric label="解决率" value={metrics.reviewedRuns ? `${Math.round(metrics.resolutionRate * 100)}%` : "-"} detail={`${metrics.resolvedRuns}/${metrics.reviewedRuns} 已质检`} />
<Metric label="无依据率" value={metrics.reviewedRuns ? `${Math.round(metrics.unsupportedEvidenceRate * 100)}%` : "-"} detail={`${metrics.unsupportedEvidenceRuns}/${metrics.reviewedRuns} 已质检`} />
<Metric label="工具成功率" value={metrics.toolCalls ? `${Math.round(metrics.toolSuccessRate * 100)}%` : "-"} detail={`${metrics.toolCalls} 次调用`} />
<Metric label="平均步骤" value={metrics.averageSteps.toFixed(1)} detail={`${metrics.totalRuns} 次运行`} />
<Metric label="P95 时延" value={`${metrics.p95DurationMs} ms`} detail={`平均 ${metrics.averageDurationMs} ms`} />
<Metric label="Token" value={`${metrics.promptTokens + metrics.completionTokens}`} detail={`${metrics.promptTokens}/${metrics.completionTokens}`} />
<Metric label="转人工率" value={`${Math.round(metrics.handoffRate * 100)}%`} detail="已转人工会话" />
<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}
getItemId={(item) => item.id}
getRowClassName={() => "cursor-pointer"}
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> },
{ key: "duration", label: t("agentRun.duration"), className: "w-24 text-right", render: (item) => `${item.durationMs || 0} ms` },
{ key: "tokens", label: t("agentRun.tokens"), className: "w-28 text-right", render: (item) => `${item.promptTokens || 0}/${item.completionTokens || 0}` },
{ key: "error", label: t("agentRun.error"), className: "w-72 max-w-72", render: (item) => item.errorMessage ? <span className="block truncate text-xs text-destructive" title={item.errorMessage}>{item.errorMessage}</span> : "-" },
]}
labels={{ refresh: t("agentRun.refresh"), query: t("agentRun.query"), loading: t("agentRun.loading"), empty: t("agentRun.empty"), loadFailed: t("agentRun.loadFailed") }}
/>
<AgentRunDetailDialog open={open} loading={loading} run={run} onOpenWorkflowAudit={openWorkflowAudit} onQualityFeedbackSaved={(id) => void openDetail(id)} onOpenChange={(next) => { setOpen(next); if (!next) setRun(null) }} t={t} />
<WorkflowAuditDialog open={workflowAuditOpen} loading={workflowAuditLoading} run={workflowRun} onOpenChange={(next) => { setWorkflowAuditOpen(next); if (!next) setWorkflowRun(null) }} />
</>
)
}
function Metric({ label, value, detail }: { label: string; value: string; detail: string }) { return <div className="bg-background px-4 py-3"><div className="text-xs text-muted-foreground">{label}</div><div className="mt-1 text-lg font-semibold">{value}</div><div className="text-xs text-muted-foreground">{detail}</div></div> }
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}
<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} />
<section className="space-y-2"><h3 className="text-sm font-medium">{t("agentRun.steps")}</h3>{(run.steps ?? []).map((step) => <StepBlock key={step.id} step={step} t={t} />)}{!run.steps?.length ? <p className="text-sm text-muted-foreground">{t("agentRun.emptySteps")}</p> : null}</section>
<section className="space-y-2"><h3 className="text-sm font-medium">{t("agentRun.toolCalls")}</h3>{(run.toolCalls ?? []).map((call) => <ToolCallBlock key={call.id} call={call} t={t} />)}{!run.toolCalls?.length ? <p className="text-sm text-muted-foreground">{t("agentRun.emptyToolCalls")}</p> : null}</section>
</div> : <div className="py-10 text-sm text-muted-foreground">{t("agentRun.notFound")}</div>}
</ProjectDialog>
}
function QualityFeedbackPanel({ run, onSaved }: { run: AgentRun; onSaved: (agentRunId: number) => void }) {
const [resolutionStatus, setResolutionStatus] = useState<"unknown" | "resolved" | "unresolved">("unknown")
const [evidenceStatus, setEvidenceStatus] = useState<"unknown" | "supported" | "unsupported">("unknown")
const [comment, setComment] = useState("")
const [saving, setSaving] = useState(false)
useEffect(() => {
setResolutionStatus(run.qualityFeedback?.resolutionStatus ?? "unknown")
setEvidenceStatus(run.qualityFeedback?.evidenceStatus ?? "unknown")
setComment(run.qualityFeedback?.comment ?? "")
}, [run.id, run.qualityFeedback])
async function save() {
setSaving(true)
try {
await saveAgentRunQualityFeedback({ agentRunId: run.id, resolutionStatus, evidenceStatus, comment })
toast.success("质检结果已保存")
onSaved(run.id)
} catch (error) {
toast.error(error instanceof Error ? error.message : "保存质检结果失败")
} finally {
setSaving(false)
}
}
return <section className="space-y-3 border p-3"><div><h3 className="text-sm font-medium"></h3><p className="text-xs text-muted-foreground"></p></div><div className="grid gap-3 sm:grid-cols-2"><OptionCombobox value={resolutionStatus} placeholder="选择解决情况" options={[{ value: "unknown", label: "解决情况:未判断" }, { value: "resolved", label: "解决情况:已解决" }, { value: "unresolved", label: "解决情况:未解决" }]} onChange={(value) => setResolutionStatus(value === "resolved" || value === "unresolved" ? value : "unknown")} /><OptionCombobox value={evidenceStatus} placeholder="选择依据情况" options={[{ value: "unknown", label: "依据情况:未判断" }, { value: "supported", label: "依据情况:有依据" }, { value: "unsupported", label: "依据情况:无依据" }]} onChange={(value) => setEvidenceStatus(value === "supported" || value === "unsupported" ? value : "unknown")} /></div><Textarea rows={3} value={comment} onChange={(event) => setComment(event.target.value)} placeholder="质检备注" /><div className="flex items-center justify-between gap-3"><span className="text-xs text-muted-foreground">{run.qualityFeedback?.updatedAt ? `最近标注:${run.qualityFeedback.updatedAt}` : "尚未标注"}</span><Button type="button" size="sm" disabled={saving} onClick={save}></Button></div></section>
}
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>}>
{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>
}
function Meta({ label, value }: { label: string; value: string }) { return <span className="inline-flex items-center gap-1 rounded-md border bg-background px-2 py-1"><span className="text-muted-foreground">{label}</span><span className="font-medium">{value || "-"}</span></span> }
function StepBlock({ step, t }: { step: AgentStep; t: (key: string) => string }) { return <div className="rounded-md border p-3"><div className="flex flex-wrap items-center gap-2"><Clock3Icon className="size-4 text-muted-foreground" /><span className="font-medium">{step.stepCode || step.stepType}</span><Badge variant={statusVariant(step.status)}>{step.status}</Badge><span className="text-xs text-muted-foreground">{step.durationMs || 0} ms</span></div>{step.errorMessage ? <p className="mt-2 text-xs text-destructive">{step.errorMessage}</p> : null}<div className="mt-3 grid gap-3 lg:grid-cols-2"><Preview title={t("agentRun.input")} raw={step.inputPreview} /><Preview title={t("agentRun.output")} raw={step.outputPreview} /></div></div> }
function ToolCallBlock({ call, t }: { call: AgentToolCall; t: (key: string) => string }) { return <div className="rounded-md border p-3"><div className="flex flex-wrap items-center gap-2"><WrenchIcon className="size-4 text-muted-foreground" /><span className="font-medium">{call.toolCode}</span><Badge variant={statusVariant(call.status)}>{call.status}</Badge><span className="text-xs text-muted-foreground">{call.riskLevel}</span></div>{call.errorMessage ? <p className="mt-2 text-xs text-destructive">{call.errorMessage}</p> : null}<div className="mt-3 grid gap-3 lg:grid-cols-2"><Preview title={t("agentRun.arguments")} raw={call.argumentsPreview} /><Preview title={t("agentRun.result")} raw={call.resultPreview} /></div></div> }
function Preview({ title, raw }: { title: string; raw: string }) { const value = parseJSON(raw); return <div className="min-w-0"><div className="mb-1 text-xs text-muted-foreground">{title}</div>{value !== null ? <JsonTreeViewer value={value} collapsed={2} /> : raw?.trim() ? <pre className="max-h-52 overflow-auto rounded-md border bg-muted/20 p-2 text-xs whitespace-pre-wrap break-all">{raw}</pre> : <div className="rounded-md border bg-muted/20 px-2 py-1.5 text-xs text-muted-foreground">-</div>}</div> }
function parseJSON(raw: string): unknown | null { try { return raw?.trim() ? JSON.parse(raw) : null } catch { return null } }
@@ -6,6 +6,7 @@ import {
GitBranchIcon,
HistoryIcon,
PlugIcon,
RotateCcwIcon,
SaveIcon,
SettingsIcon,
Trash2Icon,
@@ -36,25 +37,34 @@ import { Textarea } from "@/components/ui/textarea"
import {
createAIAgent,
fetchAIAgent,
fetchAIAgentRevisions,
fetchAIAgentWorkflow,
fetchAIConfigsAll,
fetchKnowledgeBasesAll,
fetchAIWorkflowDefaultDefinition,
fetchAIWorkflowNodeSpecs,
fetchAIWorkflowTemplates,
fetchAIWorkflowVersions,
fetchAgentTeamsAll,
fetchMCPCatalog,
fetchSkillDefinitionsAll,
publishAIAgentWorkflow,
publishAIAgent,
rollbackAIAgent,
rollbackAIAgentRollout,
saveAIAgentWorkflow,
updateAIAgent,
validateAIWorkflow,
type AIAgent,
type AgentRevision,
type AIConfig,
type AIWorkflowDefinition,
type AIWorkflowNodeSpec,
type AIWorkflowTemplate,
type AIWorkflowVersion,
type AdminAgentTeam,
type CreateAIAgentPayload,
type KnowledgeBase,
type MCPToolCatalogItem,
type MCPToolSourceType,
type SkillDefinition,
@@ -128,6 +138,7 @@ export function AIAgentConfigWorkbench({
const [activeSection, setActiveSection] = useState<SectionKey>("basic")
const [agent, setAgent] = useState<AIAgent | null>(null)
const [workflowVersions, setWorkflowVersions] = useState<AIWorkflowVersion[]>([])
const [agentRevisions, setAgentRevisions] = useState<AgentRevision[]>([])
const [nodeSpecs, setNodeSpecs] = useState<AIWorkflowNodeSpec[]>([])
const [loading, setLoading] = useState(true)
const [savingAgent, setSavingAgent] = useState(false)
@@ -137,28 +148,36 @@ export function AIAgentConfigWorkbench({
const [name, setName] = useState("")
const [description, setDescription] = useState("")
const [aiConfigId, setAIConfigId] = useState("")
const [runtimeMode, setRuntimeMode] = useState<"workflow" | "autonomous" | "hybrid">("autonomous")
const [serviceMode, setServiceMode] = useState(String(IMConversationServiceMode.AIFirst))
const [systemPrompt, setSystemPrompt] = useState("")
const [welcomeMessage, setWelcomeMessage] = useState("")
const [replyTimeoutSeconds, setReplyTimeoutSeconds] = useState("180")
const [rolloutPercent, setRolloutPercent] = useState("5")
const [handoffMode, setHandoffMode] = useState(String(AIAgentHandoffMode.WaitPool))
const [fallbackMode, setFallbackMode] = useState(String(AIAgentFallbackMode.NoAnswer))
const [fallbackMessage, setFallbackMessage] = useState("")
const [selectedTeamIds, setSelectedTeamIds] = useState<number[]>([])
const [selectedSkillIds, setSelectedSkillIds] = useState<number[]>([])
const [selectedKnowledgeBaseIds, setSelectedKnowledgeBaseIds] = useState<number[]>([])
const [directTools, setDirectTools] = useState<DirectToolItem[]>([])
const [definition, setDefinition] = useState<AIWorkflowDefinition>(fallbackDefinition)
const [workflowRevision, setWorkflowRevision] = useState(0)
const [workflowTemplates, setWorkflowTemplates] = useState<AIWorkflowTemplate[]>([])
const [selectedWorkflowTemplate, setSelectedWorkflowTemplate] = useState("")
const [aiConfigs, setAIConfigs] = useState<AIConfig[]>([])
const [agentTeams, setAgentTeams] = useState<AdminAgentTeam[]>([])
const [skills, setSkills] = useState<SkillDefinition[]>([])
const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBase[]>([])
const [toolCatalog, setToolCatalog] = useState<MCPToolCatalogItem[]>([])
const [teamToAdd, setTeamToAdd] = useState("")
const [skillToAdd, setSkillToAdd] = useState("")
const [knowledgeBaseToAdd, setKnowledgeBaseToAdd] = useState("")
const [directToolGroupToAdd, setDirectToolGroupToAdd] = useState("")
const [directToolToAdd, setDirectToolToAdd] = useState("")
const previousRolloutPercent = agent?.previousRolloutPercent ?? 0
useEffect(() => {
setCurrentAgentId(agentId ?? null)
@@ -175,51 +194,63 @@ export function AIAgentConfigWorkbench({
const [
specs,
defaultDefinition,
templates,
configs,
teams,
skillList,
knowledgeBaseList,
catalog,
] = await Promise.all([
fetchAIWorkflowNodeSpecs(),
fetchAIWorkflowDefaultDefinition().catch(() => fallbackDefinition),
fetchAIWorkflowTemplates(),
fetchAIConfigsAll({ modelType: AIModelType.LLM }),
fetchAgentTeamsAll(),
fetchSkillDefinitionsAll({ status: Status.Ok }),
fetchKnowledgeBasesAll({ status: Status.Ok }),
fetchMCPCatalog(),
])
setNodeSpecs(specs ?? [])
setWorkflowTemplates(templates ?? [])
setAIConfigs(configs ?? [])
setAgentTeams(teams ?? [])
setSkills(skillList ?? [])
setKnowledgeBases(knowledgeBaseList ?? [])
setToolCatalog(catalog ?? [])
if (!currentAgentId || currentAgentId <= 0) {
setAgent(null)
setWorkflowVersions([])
setAgentRevisions([])
setName("")
setDescription("")
setAIConfigId("")
setAIConfigId("")
setRuntimeMode("autonomous")
setServiceMode(String(IMConversationServiceMode.AIFirst))
setSystemPrompt("")
setWelcomeMessage("")
setReplyTimeoutSeconds("180")
setRolloutPercent("5")
setHandoffMode(String(AIAgentHandoffMode.WaitPool))
setFallbackMode(String(AIAgentFallbackMode.NoAnswer))
setFallbackMessage("")
setSelectedTeamIds([])
setSelectedSkillIds([])
setSelectedKnowledgeBaseIds([])
setDirectTools([])
replaceWorkflowDefinition(defaultDefinition ?? fallbackDefinition)
return
}
const [agentDetail, workflowDetail] = await Promise.all([
const [agentDetail, workflowDetail, revisionList] = await Promise.all([
fetchAIAgent(currentAgentId),
fetchAIAgentWorkflow(currentAgentId),
fetchAIAgentRevisions(currentAgentId),
])
setAgent(agentDetail)
setAgentRevisions(revisionList ?? [])
if (workflowDetail?.id > 0) {
const versionPage = await fetchAIWorkflowVersions({ workflowId: workflowDetail.id, limit: 20 })
setWorkflowVersions(versionPage.results ?? [])
@@ -228,16 +259,19 @@ export function AIAgentConfigWorkbench({
}
setName(agentDetail.name)
setDescription(agentDetail.description || "")
setAIConfigId(toText(agentDetail.aiConfigId))
setAIConfigId(toText(agentDetail.aiConfigId))
setRuntimeMode(agentDetail.runtimeMode === "autonomous" || agentDetail.runtimeMode === "hybrid" ? agentDetail.runtimeMode : "workflow")
setServiceMode(String(agentDetail.serviceMode || IMConversationServiceMode.AIFirst))
setSystemPrompt(agentDetail.systemPrompt || "")
setWelcomeMessage(agentDetail.welcomeMessage || "")
setReplyTimeoutSeconds(String(agentDetail.replyTimeoutSeconds ?? 180))
setRolloutPercent(String(agentDetail.rolloutPercent || 100))
setHandoffMode(String(agentDetail.handoffMode || AIAgentHandoffMode.WaitPool))
setFallbackMode(String(agentDetail.fallbackMode || AIAgentFallbackMode.NoAnswer))
setFallbackMessage(agentDetail.fallbackMessage || "")
setSelectedTeamIds((agentDetail.teams ?? []).map((team) => team.id))
setSelectedSkillIds(agentDetail.skillIds ?? [])
setSelectedKnowledgeBaseIds(agentDetail.knowledgeBaseIds ?? [])
setDirectTools(agentDetail.directTools ?? [])
replaceWorkflowDefinition(workflowDetail.draftDefinition ?? defaultDefinition ?? fallbackDefinition)
} catch (error) {
@@ -259,6 +293,14 @@ export function AIAgentConfigWorkbench({
],
[]
)
const runtimeModeOptions = useMemo(
() => [
{ value: "autonomous", label: "自主接待" },
{ value: "hybrid", label: "自主接待 + 流程" },
{ value: "workflow", label: "高级编排 / Playbooks" },
],
[]
)
const handoffModeOptions = useMemo(
() => [
{ value: String(AIAgentHandoffMode.WaitPool), label: "进入待接入池" },
@@ -271,6 +313,7 @@ export function AIAgentConfigWorkbench({
() => [
{ value: String(AIAgentFallbackMode.NoAnswer), label: "直接说明知识不足" },
{ value: String(AIAgentFallbackMode.SuggestRetry), label: "引导用户补充信息" },
{ value: String(AIAgentFallbackMode.Handoff), label: "转人工客服" },
],
[]
)
@@ -286,10 +329,18 @@ export function AIAgentConfigWorkbench({
() => skills.map((item) => ({ value: String(item.id), label: item.name })),
[skills]
)
const knowledgeBaseOptions = useMemo(
() => knowledgeBases.map((item) => ({ value: String(item.id), label: item.name })),
[knowledgeBases]
)
const directToolOptions = useMemo<DirectToolOption[]>(
() =>
toolCatalog
.filter((tool) => !tool.autoInjected && tool.sourceType === "mcp")
.filter(
(tool) =>
!tool.autoInjected &&
(tool.sourceType === "mcp" || tool.toolCode === "builtin/conversation_context" || tool.toolCode === "graph/prepare_ticket_draft")
)
.map((tool) => ({
value: tool.toolCode,
label: `${tool.title || tool.toolName} · ${tool.toolCode}`,
@@ -356,14 +407,17 @@ export function AIAgentConfigWorkbench({
name: name.trim(),
description: description.trim(),
aiConfigId: Number(aiConfigId),
runtimeMode,
serviceMode: Number(serviceMode),
systemPrompt: systemPrompt.trim(),
welcomeMessage: welcomeMessage.trim(),
replyTimeoutSeconds: Number(replyTimeoutSeconds),
rolloutPercent: Number(rolloutPercent),
teamIds: uniqueNumbers(selectedTeamIds),
handoffMode: Number(handoffMode),
fallbackMode: Number(fallbackMode),
fallbackMessage: fallbackMessage.trim(),
knowledgeBaseIds: uniqueNumbers(selectedKnowledgeBaseIds),
skillIds: uniqueNumbers(selectedSkillIds),
directTools,
}
@@ -392,6 +446,20 @@ export function AIAgentConfigWorkbench({
}
}
async function publishAutonomousAgent() {
if (!agent || runtimeMode !== "autonomous") return
setSavingAgent(true)
try {
await publishAIAgent(agent.id)
await loadData()
toast.success("Autonomous Agent published")
} catch (error) {
toast.error(error instanceof Error ? error.message : "Failed to publish Autonomous Agent")
} finally {
setSavingAgent(false)
}
}
async function saveWorkflowDraft() {
if (!currentAgentId) return
setSavingWorkflow(true)
@@ -410,6 +478,36 @@ export function AIAgentConfigWorkbench({
}
}
async function rollbackAgentRevision(revisionId: number) {
if (!agent || revisionId <= 0 || revisionId === agent.publishedRevisionId) return
setSavingAgent(true)
try {
await rollbackAIAgent(agent.id, revisionId)
toast.success("已回滚到选中的 Agent 版本")
await loadData()
onAgentSaved?.()
} catch (error) {
toast.error(error instanceof Error ? error.message : "回滚 Agent 版本失败")
} finally {
setSavingAgent(false)
}
}
async function rollbackAgentRollout() {
if (!agent || agent.previousRolloutPercent < 1) return
setSavingAgent(true)
try {
await rollbackAIAgentRollout(agent.id)
toast.success("已恢复上一次灰度比例")
await loadData()
onAgentSaved?.()
} catch (error) {
toast.error(error instanceof Error ? error.message : "恢复灰度比例失败")
} finally {
setSavingAgent(false)
}
}
async function validateWorkflowDraft() {
setSavingWorkflow(true)
try {
@@ -438,6 +536,13 @@ export function AIAgentConfigWorkbench({
}
}
function applySelectedWorkflowTemplate() {
const template = workflowTemplates.find((item) => item.code === selectedWorkflowTemplate)
if (!template) return
replaceWorkflowDefinition(template.definition)
toast.success(`已应用 ${template.name} 模板,保存草稿或发布后生效`)
}
async function publishWorkflow() {
if (!currentAgentId) return
setSavingWorkflow(true)
@@ -475,13 +580,16 @@ export function AIAgentConfigWorkbench({
const sections: { key: SectionKey; title: string; icon: ReactNode }[] = [
{ key: "basic", title: "基础信息", icon: <SettingsIcon /> },
{ key: "capabilities", title: "能力来源", icon: <PlugIcon /> },
{ key: "workflow", title: "会话流程", icon: <GitBranchIcon /> },
{ key: "workflow", title: "高级编排 / Playbooks", icon: <GitBranchIcon /> },
]
const selectedTeamOptions = selectedOptions(selectedTeamIds, teamOptions)
const selectedSkillOptions = selectedOptions(selectedSkillIds, skillOptions)
const workflowPublished = isWorkflowPublished(agent)
const workflowStateText =
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 workflowStateText =
agent?.workflowStateText || (workflowPublished ? "已发布" : "未发布")
return (
@@ -494,8 +602,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={workflowPublished ? "default" : "outline"}>
{workflowStateText}
<Badge variant={runtimePublished ? "default" : "outline"}>
{runtimeMode === "autonomous" ? (autonomousPublished ? "已发布" : "未发布") : runtimeMode === "hybrid" ? (hybridPublished ? "已发布" : "未发布") : workflowStateText}
</Badge>
{workflowPublished ? (
<Badge variant="secondary"> #{agent?.workflowVersionId}</Badge>
@@ -507,6 +615,7 @@ export function AIAgentConfigWorkbench({
null
) : (
<>
{agent && runtimeMode === "autonomous" ? <Button type="button" variant="outline" disabled={savingAgent || loading} onClick={publishAutonomousAgent}> Agent</Button> : null}
<Button
type="button"
variant="outline"
@@ -522,9 +631,9 @@ export function AIAgentConfigWorkbench({
</div>
<div className="flex min-h-0 flex-1 flex-col bg-background">
{agent && !workflowPublished ? (
{agent && !runtimePublished ? (
<div className="shrink-0 border-b border-amber-200 bg-amber-50 px-5 py-2 text-sm text-amber-900">
AI
{runtimeMode === "autonomous" ? "未发布 Agent,AI 不会自动回复。保存配置后发布 Agent,再绑定渠道或启用自动回复。" : runtimeMode === "hybrid" ? "未发布 Hybrid Agent,AI 不会自动回复。保存配置后请进入“高级编排 / Playbooks”发布一个版本。" : "未发布 Playbook,AI 不会自动回复。保存配置后请进入“高级编排 / Playbooks”发布一个版本,再绑定渠道或启用自动回复。"}
</div>
) : null}
<div className="shrink-0 border-b bg-muted/30 px-4 py-2">
@@ -597,6 +706,9 @@ export function AIAgentConfigWorkbench({
onChange={setAIConfigId}
/>
</FieldBlock>
<FieldBlock label="运行模式">
<OptionCombobox value={runtimeMode} options={runtimeModeOptions} placeholder="选择运行模式" onChange={(value) => setRuntimeMode(value === "autonomous" || value === "hybrid" ? value : "workflow")} />
</FieldBlock>
<FieldBlock label="回复超时秒数">
<Input
type="number"
@@ -606,6 +718,17 @@ export function AIAgentConfigWorkbench({
onChange={(event) => setReplyTimeoutSeconds(event.target.value)}
/>
</FieldBlock>
<FieldBlock label="会话灰度比例(%">
<div className="flex items-center gap-2">
<Input type="number" min={1} max={100} step={1} value={rolloutPercent} onChange={(event) => setRolloutPercent(event.target.value)} />
{previousRolloutPercent > 0 ? (
<Button type="button" variant="outline" size="sm" disabled={savingAgent} onClick={rollbackAgentRollout}>
<RotateCcwIcon />
{previousRolloutPercent}%
</Button>
) : null}
</div>
</FieldBlock>
</div>
<FieldBlock label="系统提示词">
<ContentEditor
@@ -657,6 +780,23 @@ export function AIAgentConfigWorkbench({
{activeSection === "capabilities" ? (
<ConfigSection>
<div className="text-sm font-medium"></div>
<AddRow
value={knowledgeBaseToAdd}
options={knowledgeBaseOptions.filter((option) => !selectedKnowledgeBaseIds.includes(Number(option.value)))}
placeholder="选择知识库"
onValueChange={setKnowledgeBaseToAdd}
onAdd={() => {
addSelected(knowledgeBaseToAdd, selectedKnowledgeBaseIds, setSelectedKnowledgeBaseIds)
setKnowledgeBaseToAdd("")
}}
/>
<BadgeList empty="未配置知识库。" items={selectedOptions(selectedKnowledgeBaseIds, knowledgeBaseOptions)} onRemove={(id) => setSelectedKnowledgeBaseIds((current) => current.filter((item) => item !== id))} />
</ConfigSection>
) : null}
{activeSection === "capabilities" ? (
<ConfigSection>
<AddRow
value={skillToAdd}
options={skillOptions.filter((option) => !selectedSkillIds.includes(Number(option.value)))}
@@ -730,7 +870,19 @@ export function AIAgentConfigWorkbench({
) : null}
{activeSection === "workflow" ? (
<WorkflowEditor
<div className="flex min-h-0 flex-1 flex-col">
<div className="flex shrink-0 items-center gap-2 border-b px-4 py-2">
<OptionCombobox
value={selectedWorkflowTemplate}
options={workflowTemplates.map((item) => ({ value: item.code, label: item.name }))}
placeholder="选择 Playbook 模板"
onChange={setSelectedWorkflowTemplate}
/>
<Button type="button" variant="outline" size="sm" disabled={!selectedWorkflowTemplate || savingWorkflow || loading} onClick={applySelectedWorkflowTemplate}>
</Button>
</div>
<WorkflowEditor
key={workflowRevision}
definition={definition}
nodeSpecs={nodeSpecs}
@@ -756,7 +908,8 @@ export function AIAgentConfigWorkbench({
</Button>
}
/>
/>
</div>
) : null}
<Dialog open={versionDialogOpen} onOpenChange={setVersionDialogOpen}>
@@ -767,6 +920,9 @@ export function AIAgentConfigWorkbench({
<VersionRecordsTable
agent={agent}
workflowVersions={workflowVersions}
agentRevisions={agentRevisions}
onRollback={rollbackAgentRevision}
rollbackDisabled={savingAgent || loading}
/>
</DialogContent>
</Dialog>
@@ -828,12 +984,52 @@ function AddRow({
function VersionRecordsTable({
agent,
workflowVersions,
agentRevisions,
onRollback,
rollbackDisabled,
}: {
agent: AIAgent | null
workflowVersions: AIWorkflowVersion[]
agentRevisions: AgentRevision[]
onRollback: (revisionId: number) => void
rollbackDisabled: boolean
}) {
return (
<div className="max-h-[60vh] overflow-auto rounded-md border">
<div className="max-h-[60vh] space-y-4 overflow-auto">
<div className="rounded-md border">
<div className="border-b px-3 py-2 text-sm font-medium">Agent </div>
{agentRevisions.length > 0 ? (
<Table>
<TableHeader className="bg-muted/40">
<TableRow>
<TableHead className="w-28"></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="text-right"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{agentRevisions.map((revision) => {
const active = agent?.publishedRevisionId === revision.id
return (
<TableRow key={revision.id}>
<TableCell className="font-medium">r{revision.revision}{active ? <Badge variant="secondary" className="ml-2"></Badge> : null}</TableCell>
<TableCell className="text-muted-foreground">{revision.publishedAt || "-"}</TableCell>
<TableCell>{revision.publishedByName || "-"}</TableCell>
<TableCell>{revision.workflowVersionId > 0 ? `#${revision.workflowVersionId}` : "-"}</TableCell>
<TableCell className="text-right">
<Button type="button" variant="outline" size="sm" disabled={active || rollbackDisabled} onClick={() => onRollback(revision.id)}></Button>
</TableCell>
</TableRow>
)
})}
</TableBody>
</Table>
) : <div className="p-4 text-sm text-muted-foreground"> Agent </div>}
</div>
<div className="rounded-md border">
<div className="border-b px-3 py-2 text-sm font-medium"></div>
{workflowVersions.length > 0 ? (
<Table>
<TableHeader className="bg-muted/40">
@@ -875,6 +1071,7 @@ function VersionRecordsTable({
) : (
<div className="p-4 text-sm text-muted-foreground"></div>
)}
</div>
</div>
)
}
+2 -2
View File
@@ -122,7 +122,7 @@ export default function DashboardAIAgentsPage() {
},
{
key: "workflow",
label: "流程状态",
label: "Playbook 状态",
render: (item) => {
const published = isWorkflowPublished(item);
return (
@@ -139,7 +139,7 @@ export default function DashboardAIAgentsPage() {
</div>
{!published ? (
<div className="text-xs text-muted-foreground">
AI
PlaybookAI
</div>
) : (
<div className="text-xs text-muted-foreground">
+61 -11
View File
@@ -4,7 +4,7 @@ import { useEffect, useMemo, useState } from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { Controller, Resolver, useForm, useWatch } from "react-hook-form"
import { z } from "zod/v4"
import { CopyIcon, ExternalLinkIcon } from "lucide-react"
import { CopyIcon, ExternalLinkIcon, RotateCcwIcon } from "lucide-react"
import { toast } from "sonner"
import { getWidgetDemoPath } from "@/components/support-chat/demo-navigation"
@@ -28,6 +28,7 @@ import {
fetchAIAgentsAll,
fetchChannel,
fetchWxWorkKFAccounts,
rollbackChannelAIAgentRollout,
resetChannelUserTokenSecret,
} from "@/lib/api/admin"
import { useI18n } from "@/i18n/provider"
@@ -74,6 +75,7 @@ function createSchema(t: Translate) {
.object({
channelType: z.enum(["web", "wechat_mp", "wxwork_kf"], t("channel.typeRequired")),
aiAgentId: z.string().trim().regex(/^\d+$/, t("channel.agentRequired")),
aiAgentRolloutPercent: z.coerce.number().int().min(1).max(100),
name: z.string().trim().min(1, t("channel.nameRequired")),
openKfId: z.string().trim(),
widgetTitle: z.string().trim(),
@@ -98,6 +100,7 @@ function createSchema(t: Translate) {
type EditForm = {
channelType: "web" | "wechat_mp" | "wxwork_kf"
aiAgentId: string
aiAgentRolloutPercent: number
name: string
openKfId: string
widgetTitle: string
@@ -114,6 +117,7 @@ function createEmptyForm(t: Translate): EditForm {
return {
channelType: "web",
aiAgentId: "",
aiAgentRolloutPercent: 100,
name: "",
openKfId: "",
widgetTitle: defaultWebChannelConfig.title,
@@ -202,6 +206,7 @@ function buildForm(item: AdminChannel | null, t: Translate): EditForm {
? "wechat_mp"
: "web",
aiAgentId: item.aiAgentId > 0 ? String(item.aiAgentId) : "",
aiAgentRolloutPercent: item.aiAgentRolloutPercent || 100,
name: item.name,
openKfId: parseOpenKfId(item.configJson),
widgetTitle: wechatConfig?.title ?? webConfig.title,
@@ -240,6 +245,7 @@ function buildPayload(form: EditForm, status: number, t: Translate): CreateAdmin
return {
channelType,
aiAgentId: Number(form.aiAgentId),
aiAgentRolloutPercent: form.aiAgentRolloutPercent,
name: form.name.trim(),
configJson,
status,
@@ -247,8 +253,15 @@ function buildPayload(form: EditForm, status: number, t: Translate): CreateAdmin
}
}
function isAgentWorkflowPublished(agent: AIAgent | undefined) {
return Boolean(agent?.workflowPublished ?? (agent?.workflowVersionId ?? 0) > 0)
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)
}
type ChannelFormBodyProps = Omit<ChannelFormDialogProps, "open">
@@ -300,6 +313,7 @@ function ChannelFormBody({
const [wxWorkKFAccountsLoading, setWxWorkKFAccountsLoading] = useState(false)
const [wxWorkKFAccountsError, setWxWorkKFAccountsError] = useState("")
const [channelDetail, setChannelDetail] = useState<AdminChannel | null>(null)
const [rollingBackRollout, setRollingBackRollout] = useState(false)
const [currentStatus, setCurrentStatus] = useState(0)
const form = useForm<
z.input<typeof schema>,
@@ -321,6 +335,26 @@ function ChannelFormBody({
const aiAgentId = useWatch({ control, name: "aiAgentId" })
const openKfId = useWatch({ control, name: "openKfId" })
const userTokenSecret = useWatch({ control, name: "userTokenSecret" })
const previousRolloutPercent = channelDetail?.previousAiAgentRolloutPercent ?? 0
async function rollbackRolloutPercent() {
if (!channelDetail || previousRolloutPercent < 1) return
setRollingBackRollout(true)
try {
await rollbackChannelAIAgentRollout(channelDetail.id)
setValue("aiAgentRolloutPercent", previousRolloutPercent)
setChannelDetail({
...channelDetail,
aiAgentRolloutPercent: previousRolloutPercent,
previousAiAgentRolloutPercent: channelDetail.aiAgentRolloutPercent,
})
toast.success("已恢复上一次渠道灰度比例")
} catch (error) {
toast.error(error instanceof Error ? error.message : "恢复渠道灰度比例失败")
} finally {
setRollingBackRollout(false)
}
}
useEffect(() => {
async function loadAIAgents() {
@@ -392,11 +426,11 @@ function ChannelFormBody({
const selectedAIAgent = aiAgents.find((item) => String(item.id) === aiAgentId)
const availableAIAgents = aiAgents.filter(
(item) => isAgentWorkflowPublished(item) || String(item.id) === aiAgentId
(item) => isAgentChannelBindable(item) || String(item.id) === aiAgentId
)
const aiAgentOptions = availableAIAgents.map((item) => ({
value: String(item.id),
label: isAgentWorkflowPublished(item)
label: isAgentChannelBindable(item)
? `${item.name} · 当前生效 #${item.workflowVersionId}`
: `${item.name} · 未发布`,
}))
@@ -426,8 +460,8 @@ function ChannelFormBody({
async function onFormSubmit(values: EditForm) {
const selected = aiAgents.find((item) => String(item.id) === values.aiAgentId)
if (!isAgentWorkflowPublished(selected)) {
toast.error("该 Agent 尚未发布流程,不能绑定渠道")
if (!isAgentChannelBindable(selected)) {
toast.error("该 Agent 尚未完成发布,不能绑定渠道")
return
}
await onSubmit(buildPayload(values, currentStatus, t))
@@ -521,23 +555,39 @@ function ChannelFormBody({
/>
)}
/>
{selectedAIAgent && !isAgentWorkflowPublished(selectedAIAgent) ? (
{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
</div>
) : null}
{selectedAIAgent && isAgentWorkflowPublished(selectedAIAgent) ? (
{selectedAIAgent && isAgentChannelBindable(selectedAIAgent) ? (
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Badge variant="secondary">
{selectedAIAgent.workflowStateText || "已发布"}
{selectedAIAgent.runtimeMode === "autonomous" ? "已发布" : selectedAIAgent.workflowStateText || "已发布"}
</Badge>
<span> #{selectedAIAgent.workflowVersionId}</span>
<span>{selectedAIAgent.runtimeMode === "autonomous" ? `当前版本 #${selectedAIAgent.publishedRevisionId}` : `当前生效版本 #${selectedAIAgent.workflowVersionId}`}</span>
</div>
) : null}
<FieldError errors={[errors.aiAgentId]} />
</FieldContent>
</Field>
<Field data-invalid={!!errors.aiAgentRolloutPercent}>
<FieldLabel htmlFor="channel-ai-agent-rollout">AI %</FieldLabel>
<FieldContent>
<div className="flex items-center gap-2">
<Input id="channel-ai-agent-rollout" type="number" min={1} max={100} step={1} {...register("aiAgentRolloutPercent")} />
{previousRolloutPercent > 0 ? (
<Button type="button" variant="outline" size="sm" disabled={saving || rollingBackRollout} onClick={rollbackRolloutPercent}>
<RotateCcwIcon />
{previousRolloutPercent}%
</Button>
) : null}
</div>
<FieldError errors={[errors.aiAgentRolloutPercent]} />
</FieldContent>
</Field>
<Field data-invalid={!!errors.channelType}>
<FieldLabel>{t("channel.channelType")}</FieldLabel>
<FieldContent>
+230 -1
View File
@@ -177,6 +177,8 @@ export type AdminChannel = {
channelType: string
channelId: string
aiAgentId: number
aiAgentRolloutPercent: number
previousAiAgentRolloutPercent: number
aiAgentName?: string
name: string
configJson: string
@@ -194,6 +196,7 @@ export type WxWorkKFAccount = {
export type CreateAdminChannelPayload = {
channelType: string
aiAgentId: number
aiAgentRolloutPercent: number
name: string
configJson: string
status: number
@@ -216,17 +219,26 @@ export type AIAgent = {
statusName: string
aiConfigId: number
aiConfigName?: string
runtimeMode: "workflow" | "autonomous" | "hybrid"
runtimeModeName: string
maxSteps: number
contextWindow: number
toolPolicy: string
knowledgePolicy: string
serviceMode: number
serviceModeName: string
systemPrompt: string
welcomeMessage: string
replyTimeoutSeconds: number
rolloutPercent: number
previousRolloutPercent: number
teams: { id: number; name: string }[]
handoffMode: number
handoffModeName: string
fallbackMode: number
fallbackModeName: string
fallbackMessage: string
knowledgeBaseIds: number[]
skillIds: number[]
skills: { id: number; name: string }[]
directTools: {
@@ -238,6 +250,7 @@ export type AIAgent = {
arguments?: Record<string, string>
}[]
workflowVersionId: number
publishedRevisionId: number
workflowPublished: boolean
workflowState: string
workflowStateText: string
@@ -252,14 +265,21 @@ export type CreateAIAgentPayload = {
name: string
description: string
aiConfigId: number
runtimeMode?: "workflow" | "autonomous" | "hybrid"
maxSteps?: number
contextWindow?: number
toolPolicy?: string
knowledgePolicy?: string
serviceMode: number
systemPrompt: string
welcomeMessage: string
replyTimeoutSeconds: number
rolloutPercent: number
teamIds: number[]
handoffMode: number
fallbackMode: number
fallbackMessage: string
knowledgeBaseIds: number[]
skillIds: number[]
directTools: {
toolCode: string
@@ -275,6 +295,18 @@ export type UpdateAIAgentPayload = CreateAIAgentPayload & {
id: number
}
export type AgentRevision = {
id: number
agentId: number
revision: number
workflowVersionId: number
status: number
definitionHash: string
publishedAt: string
publishedById: number
publishedByName: string
}
export type AIWorkflowPosition = {
x: number
y: number
@@ -387,6 +419,13 @@ export type AIWorkflowNodeSpec = {
defaultInputs?: Record<string, AIWorkflowValue>
}
export type AIWorkflowTemplate = {
code: string
name: string
description: string
definition: AIWorkflowDefinition
}
export type AIWorkflowValidationResult = {
valid: boolean
errors: {
@@ -568,6 +607,123 @@ export type AIWorkflowRun = {
nodes?: AIWorkflowNodeRun[]
}
export type AgentRun = {
id: number
conversationId: number
aiAgentId: number
agentRevisionId: number
sourceMessageId: number
workflowRunId: number
engineCode: string
status: string
promptTokens: number
completionTokens: number
startedAt: string
endedAt: string
durationMs: number
errorMessage: string
traceData: string
createdAt: string
updatedAt: string
steps?: AgentStep[]
toolCalls?: AgentToolCall[]
qualityFeedback?: AgentRunQualityFeedback
}
export type AgentRunQualityFeedback = {
id: number
agentRunId: number
resolutionStatus: "unknown" | "resolved" | "unresolved"
evidenceStatus: "unknown" | "supported" | "unsupported"
comment: string
updateUserName: string
updatedAt: string
}
export type AgentRunMetrics = {
totalRuns: number
completedRuns: number
failedRuns: number
interruptedRuns: number
completionRate: number
toolCalls: number
toolSuccessRate: number
averageSteps: number
averageDurationMs: number
p95DurationMs: number
promptTokens: number
completionTokens: number
handoffRate: number
knowledgeFallbackRate: number
resumedInterrupts: number
resolvedInterrupts: number
interruptRecoveryRate: number
reviewedRuns: number
resolvedRuns: number
resolutionRate: number
unsupportedEvidenceRuns: number
unsupportedEvidenceRate: number
}
export type AgentRunEngineComparison = {
engineCode: string
metrics: AgentRunMetrics
}
export type AgentEvaluationCase = {
id: string
category?: string
message: string
history?: string[]
expect?: Record<string, unknown>
}
export type AgentEvaluationReport = {
engineCode: string
total: number
passed: number
results: {
caseId: string
category: string
engineCode: string
passed: boolean
replyText: string
interrupted: boolean
error?: string
finding?: string
}[]
csv: string
}
export type AgentStep = {
id: number
agentRunId: number
stepType: string
stepCode: string
status: string
inputPreview: string
outputPreview: string
errorMessage: string
startedAt: string
endedAt: string
durationMs: number
}
export type AgentToolCall = {
id: number
agentRunId: number
agentStepId: number
toolCode: string
riskLevel: string
requireConfirm: boolean
status: string
argumentsPreview: string
resultPreview: string
errorMessage: string
durationMs: number
createdAt: string
}
export type AdminAgentProfile = {
id: number
userId: number
@@ -744,6 +900,13 @@ export function updateChannel(payload: UpdateAdminChannelPayload) {
})
}
export function rollbackChannelAIAgentRollout(id: number) {
return request<void>("/api/dashboard/channel/rollback_ai_agent_rollout", {
method: "POST",
body: JSON.stringify({ id }),
})
}
export function updateChannelStatus(id: number, status: number) {
return request<void>("/api/dashboard/channel/update_status", {
method: "POST",
@@ -800,6 +963,31 @@ export function updateAIAgent(payload: UpdateAIAgentPayload) {
})
}
export function publishAIAgent(id: number) {
return request<void>("/api/dashboard/ai-agent/publish", {
method: "POST",
body: JSON.stringify({ id }),
})
}
export function fetchAIAgentRevisions(id: number) {
return request<AgentRevision[]>(`/api/dashboard/ai-agent/${id}/revision/list`)
}
export function rollbackAIAgent(id: number, revisionId: number) {
return request<void>("/api/dashboard/ai-agent/rollback", {
method: "POST",
body: JSON.stringify({ id, revisionId }),
})
}
export function rollbackAIAgentRollout(id: number) {
return request<void>("/api/dashboard/ai-agent/rollback_rollout", {
method: "POST",
body: JSON.stringify({ id }),
})
}
export function deleteAIAgent(id: number) {
return request<void>("/api/dashboard/ai-agent/delete", {
method: "POST",
@@ -837,7 +1025,11 @@ export function fetchAIWorkflowNodeSpecs() {
}
export function fetchAIWorkflowDefaultDefinition() {
return request<AIWorkflowDefinition>("/api/dashboard/ai-workflow/default-definition")
return request<AIWorkflowDefinition>("/api/dashboard/ai-workflow/default-definition")
}
export function fetchAIWorkflowTemplates() {
return request<AIWorkflowTemplate[]>("/api/dashboard/ai-workflow/template/list")
}
export function fetchAIWorkflowVersions(query?: Record<string, string | number | undefined>) {
@@ -1135,6 +1327,43 @@ export function fetchAIWorkflowRun(id: number) {
return request<AIWorkflowRun>(`/api/dashboard/ai-workflow/run/${id}`)
}
export function fetchAgentRuns(query?: Record<string, string | number | undefined>) {
return request<PageResult<AgentRun>>(
`/api/dashboard/agent-run/list${toQueryString(query)}`
)
}
export function fetchAgentRun(id: number) {
return request<AgentRun>(`/api/dashboard/agent-run/${id}`)
}
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[] }) {
return request<AgentEvaluationReport>("/api/dashboard/agent-run/evaluate", {
method: "POST",
body: JSON.stringify(payload),
})
}
export function saveAgentRunQualityFeedback(payload: {
agentRunId: number
resolutionStatus: AgentRunQualityFeedback["resolutionStatus"]
evidenceStatus: AgentRunQualityFeedback["evidenceStatus"]
comment: string
}) {
return request<void>("/api/dashboard/agent-run/quality_feedback", {
method: "POST",
body: JSON.stringify(payload),
})
}
export function updateSkillDefinitionStatus(id: number, status: number) {
return request<void>("/api/dashboard/skill-definition/update_status", {
method: "POST",
+13
View File
@@ -3,10 +3,12 @@
export enum AIAgentFallbackMode {
NoAnswer = 1,
SuggestRetry = 2,
Handoff = 3,
}
export const AIAgentFallbackModeLabels: Record<AIAgentFallbackMode, string> = {
[AIAgentFallbackMode.NoAnswer]: "直接说明知识不足",
[AIAgentFallbackMode.SuggestRetry]: "引导用户补充信息",
[AIAgentFallbackMode.Handoff]: "转人工客服",
}
export enum AIAgentHandoffMode {
@@ -20,6 +22,17 @@ export const AIAgentHandoffModeLabels: Record<AIAgentHandoffMode, string> = {
[AIAgentHandoffMode.AIHoldAndNotify]: "AI继续接待并提醒人工",
}
export enum AIAgentRuntimeMode {
Workflow = "workflow",
Autonomous = "autonomous",
Hybrid = "hybrid",
}
export const AIAgentRuntimeModeLabels: Record<AIAgentRuntimeMode, string> = {
[AIAgentRuntimeMode.Workflow]: "流程编排",
[AIAgentRuntimeMode.Autonomous]: "自主运行",
[AIAgentRuntimeMode.Hybrid]: "混合运行",
}
export enum AIModelType {
LLM = "llm",
Embedding = "embedding",
+6
View File
@@ -211,6 +211,12 @@ export const dashboardNavSections: DashboardNavSectionConfig[] = [
icon: <WorkflowIcon />,
requiredPermission: "aiAgent.view",
},
{
titleKey: "nav.agentRuns",
url: "/dashboard/agent-runs",
icon: <BotMessageSquareIcon />,
requiredPermission: "aiAgent.view",
},
],
},
{
+32
View File
@@ -2288,6 +2288,37 @@
"input": "Input",
"output": "Output"
},
"agentRun": {
"conversation": "Conversation",
"agent": "Agent",
"engine": "Engine",
"status": "Status",
"startedAt": "Started",
"duration": "Duration",
"tokens": "Input/Output Tokens",
"error": "Error",
"refresh": "Refresh",
"query": "Query",
"loading": "Loading agent runs",
"empty": "No agent runs",
"loadFailed": "Failed to load agent runs",
"loadDetailFailed": "Failed to load agent run detail",
"detailTitle": "Agent Run Detail",
"detailDescription": "View the unified run audit",
"loadingDetail": "Loading agent run detail",
"close": "Close",
"revision": "Revision",
"trace": "Trace",
"steps": "Steps",
"emptySteps": "No steps recorded",
"toolCalls": "Tool Calls",
"emptyToolCalls": "No tool calls recorded",
"input": "Input Preview",
"output": "Output Preview",
"arguments": "Arguments Preview",
"result": "Result Preview",
"notFound": "Agent run not found"
},
"nav": {
"overview": "Overview",
"receptionCenter": "Support Desk",
@@ -2308,6 +2339,7 @@
"aiAgents": "Agents",
"aiWorkflows": "AI Workflows",
"workflowRuns": "Workflow Audit",
"agentRuns": "Agent Audit",
"skillDefinition": "Skills",
"mcp": "MCP tools",
"system": "System",
+32
View File
@@ -2288,6 +2288,37 @@
"input": "输入",
"output": "输出"
},
"agentRun": {
"conversation": "会话",
"agent": "Agent",
"engine": "运行引擎",
"status": "状态",
"startedAt": "开始时间",
"duration": "耗时",
"tokens": "输入/输出 Token",
"error": "错误",
"refresh": "刷新",
"query": "查询",
"loading": "加载 Agent 运行记录中",
"empty": "暂无 Agent 运行记录",
"loadFailed": "加载 Agent 运行记录失败",
"loadDetailFailed": "加载 Agent 运行详情失败",
"detailTitle": "Agent 运行详情",
"detailDescription": "查看统一运行审计",
"loadingDetail": "加载 Agent 运行详情中",
"close": "关闭",
"revision": "版本快照",
"trace": "运行轨迹",
"steps": "执行步骤",
"emptySteps": "暂无步骤记录",
"toolCalls": "工具调用",
"emptyToolCalls": "暂无工具调用记录",
"input": "输入摘要",
"output": "输出摘要",
"arguments": "参数摘要",
"result": "结果摘要",
"notFound": "未找到 Agent 运行记录"
},
"nav": {
"overview": "总览",
"receptionCenter": "接待中心",
@@ -2308,6 +2339,7 @@
"aiAgents": "Agent",
"aiWorkflows": "AI流程",
"workflowRuns": "流程审计",
"agentRuns": "Agent 审计",
"skillDefinition": "Skills",
"mcp": "MCP tools",
"system": "系统管理",