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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 不会自动回复
|
||||
未发布 Playbook,AI 不会自动回复
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "系统管理",
|
||||
|
||||
Reference in New Issue
Block a user