"use client"; import { Boxes, Brain, Check, Copy, MessageSquareText, MoreHorizontal, Pencil, Plus, Rocket, Sparkles, Trash2, Workflow, ChevronLeft, ChevronUp, ChevronDown, Save, Terminal, Loader2, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { TurnConfigEditor } from "@/components/turn-config-editor"; import { Input } from "@/components/ui/input"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { DataList } from "@/components/ui/data-list"; import { PageHeader } from "@/components/ui/page-header"; import { FilterPills } from "@/components/ui/filter-pills"; import { SearchInput } from "@/components/ui/search-input"; import { ListToolbar } from "@/components/ui/list-toolbar"; import { useCallback, useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { assistantsApi, knowledgeBasesApi, modelResourcesApi, toolsApi, type Assistant, type AssistantType as ApiAssistantType, type AssistantUpsert, type KnowledgeBase, type KnowledgeRetrievalConfig, type ModelResource, type Tool, type TurnConfig, } from "@/lib/api"; import type { WorkflowSettings } from "@/components/workflow/types"; import { SectionCard } from "@/components/editor/section-card"; import { defaultGraph, workflowUsesVision, type WorkflowGraph, } from "@/components/workflow/specs"; import { defaultTurnConfig, normalizeTurnConfig, } from "@/lib/turn-config"; import { activeDynamicVariableDefinitions, activeWorkflowDynamicVariableDefinitions, } from "@/components/assistant-editor/dynamic-variables"; import { useWorkflowEditorState } from "@/components/assistant-editor/use-workflow-editor-state"; import { DebugDrawer } from "@/components/assistant-editor/debug-preview"; import { AssistantIdentity, EditableTitle, EditorBackButton, ResourceSelectField, TextAreaField, ToggleRow, } from "@/components/assistant-editor/editor-controls"; import type { AssistantForm } from "@/components/assistant-editor/types"; import { PromptEditor } from "@/components/assistant-editor/prompt-editor"; import { WorkflowPage } from "@/components/assistant-editor/workflow-page"; type AgentPlatform = "dify" | "fastgpt"; type AgentPlatformForm = { name: string; platform: AgentPlatform; agent: string; asr: string; voice: string; enableInterrupt: boolean; turnConfig: TurnConfig; }; type OpenCodeForm = { name: string; prompt: string; agent: string; model: string; asr: string; voice: string; enableInterrupt: boolean; turnConfig: TurnConfig; visionEnabled: boolean; visionModelResourceId: string; }; type AssistantType = "提示词" | "工作流" | "Dify" | "FastGPT" | "OpenCode"; type BuildMethod = "提示词" | "工作流" | "智能体平台" | "OpenCode"; const assistantTypes: AssistantType[] = [ "提示词", "工作流", "Dify", "FastGPT", "OpenCode", ]; function defaultKnowledgeRetrievalConfig(): KnowledgeRetrievalConfig { return { mode: "automatic", topN: 5, scoreThreshold: 0, }; } // 后端 type(英文) ↔ 列表展示标签(中文) const typeToLabel: Record = { prompt: "提示词", workflow: "工作流", dify: "Dify", fastgpt: "FastGPT", opencode: "OpenCode", }; const typeFromBuildMethod: Record< Exclude, ApiAssistantType > = { 提示词: "prompt", 工作流: "workflow", OpenCode: "opencode", }; // 后端 type → 编辑器视图(工作流暂为占位页) const typeToView = { prompt: "create", dify: "create-agent-platform", fastgpt: "create-agent-platform", opencode: "create-opencode", workflow: "workflow", } as const; type View = "list" | "choose" | "loading" | (typeof typeToView)[ApiAssistantType]; // 路由驱动的页面模式: // /assistants → list | /assistants/new → choose(引导,确认即建库) | /assistants/[id] → edit export type AssistantPageProps = | { mode: "list" } | { mode: "choose" } | { mode: "edit"; assistantId: string }; // 各类型的空白表单模板(新建用) function blankPromptForm(name: string): AssistantForm { return { name, greeting: "", prompt: "", dynamicVariableDefinitions: {}, runtimeMode: "pipeline", realtimeModel: "", model: "", asr: "", voice: "", knowledgeBase: "", knowledgeRetrievalConfig: defaultKnowledgeRetrievalConfig(), enableInterrupt: true, turnConfig: defaultTurnConfig(), visionEnabled: false, visionModelResourceId: "", toolIds: [], }; } function blankAgentPlatformForm(name: string): AgentPlatformForm { return { name, platform: "dify", agent: "", asr: "", voice: "", enableInterrupt: true, turnConfig: defaultTurnConfig(), }; } function blankOpenCodeForm(name: string): OpenCodeForm { return { name, prompt: "", agent: "", model: "", asr: "", voice: "", enableInterrupt: true, turnConfig: defaultTurnConfig(), visionEnabled: false, visionModelResourceId: "", }; } function formatTimestamp(iso?: string | null): string { if (!iso) return ""; const d = new Date(iso); if (Number.isNaN(d.getTime())) return ""; const pad = (n: number) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad( d.getHours(), )}:${pad(d.getMinutes())}`; } type AssistantTypeOption = { type: BuildMethod; label: string; description: string; icon: React.ReactNode; /** 提示词、工作流、智能体平台已落地;OpenCode 暂时显示即将上线 */ available: boolean; }; const assistantTypeOptions: AssistantTypeOption[] = [ { type: "提示词", label: "使用提示词构建", description: "通过提示词、模型与语音快速搭建对话助手,适合大多数场景。", icon: , available: true, }, { type: "工作流", label: "使用工作流构建", description: "用可视化编排串联多个节点,适合多步骤、带分支的复杂流程。", icon: , available: true, }, { type: "智能体平台", label: "从智能体平台创建", description: "接入 Dify、FastGPT 等智能体平台,复用平台中的提示词、知识库和工作流配置。", icon: , available: true, }, { type: "OpenCode", label: "使用 OpenCode 构建", description: "对接 OpenCode 服务,通过提示词驱动代码助手并支持实时语音对话。", icon: , available: false, }, ]; type AssistantListItem = { id: string; name: string; type: AssistantType; updatedAt: string; /** 原始 ISO 时间戳,用于按时间排序(updatedAt 为展示用整形字符串) */ updatedAtRaw: string | null | undefined; }; type TypeFilter = "全部" | AssistantType; const typeFilters: TypeFilter[] = ["全部", ...assistantTypes]; // 列表按更新时间排序:newest=最近更新在前(倒叙,默认) / oldest=最早更新在前 type SortOrder = "newest" | "oldest"; export function AssistantPage(props: AssistantPageProps) { const router = useRouter(); // 编辑中的助手 id(来自路由) const editingId = props.mode === "edit" ? props.assistantId : null; const [form, setForm] = useState(() => blankPromptForm("")); const [agentPlatformForm, setAgentPlatformForm] = useState(() => blankAgentPlatformForm(""), ); const [openCodeForm, setOpenCodeForm] = useState(() => blankOpenCodeForm(""), ); const [assistants, setAssistants] = useState([]); const [listLoading, setListLoading] = useState(true); const [listError, setListError] = useState(null); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(null); // 编辑模式:加载指定 id 助手失败时的错误 const [loadError, setLoadError] = useState(null); // 编辑模式:后端返回的打码 API Key(用于编辑页展示"当前密钥") // 下拉数据源:模型资源 + 知识库 const [modelResources, setModelResources] = useState([]); const [knowledgeBases, setKnowledgeBases] = useState([]); const [tools, setTools] = useState([]); // 视图由路由模式决定;仅编辑模式需要先 loading,等拿到助手类型后切换 const [view, setView] = useState(() => { if (props.mode === "choose") return "choose"; if (props.mode === "edit") return "loading"; return "list"; }); const [searchQuery, setSearchQuery] = useState(""); const [typeFilter, setTypeFilter] = useState("全部"); const [sortOrder, setSortOrder] = useState("newest"); const [currentPage, setCurrentPage] = useState(1); // choose 步骤的草稿:名称与已选类型,确认后直接建库并进入编辑页 // (工作流占位页也用它展示名称与类型) const [draftName, setDraftName] = useState(""); const [draftType, setDraftType] = useState(null); const [draftAgentPlatform, setDraftAgentPlatform] = useState(null); // 引导页:创建请求进行中 / 创建失败 const [creating, setCreating] = useState(false); const [createError, setCreateError] = useState(null); // 已保存基线(当前类型表单的 JSON);与表单不一致时保存按钮才可点击 const [savedSnapshot, setSavedSnapshot] = useState(null); const { workflowName, setWorkflowName, workflowGraph, setWorkflowGraph, workflowSettings, setWorkflowSettings, workflowDynamicVariableDefinitions, setWorkflowDynamicVariableDefinitions, workflowDebugOpen, setWorkflowDebugOpen, workflowSettingsOpen, setWorkflowSettingsOpen, workflowEditingNodeId, setWorkflowEditingNodeId, workflowEditingEdgeId, setWorkflowEditingEdgeId, activeNodeId, setActiveNodeId, dynamicVariablesOpen, setDynamicVariablesOpen, } = useWorkflowEditorState(); const effectiveDynamicVariableDefinitions = activeDynamicVariableDefinitions( [form.prompt, form.greeting], form.dynamicVariableDefinitions, ); const effectiveWorkflowDynamicVariableDefinitions = activeWorkflowDynamicVariableDefinitions( workflowGraph, workflowDynamicVariableDefinitions, ); const loadAssistants = useCallback(async () => { setListLoading(true); setListError(null); try { setAssistants(await assistantsApi.list()); } catch (error) { setListError(error instanceof Error ? error.message : "加载失败"); } finally { setListLoading(false); } }, []); useEffect(() => { // 列表页挂载时拉取助手列表(与后端同步) if (props.mode !== "list") return; // eslint-disable-next-line react-hooks/set-state-in-effect void loadAssistants(); }, [props.mode, loadAssistants]); // 进入创建/编辑前加载下拉数据源(模型资源 + 知识库) const loadResources = useCallback(async () => { try { const [creds, kbs, toolRows] = await Promise.all([ modelResourcesApi.list(), knowledgeBasesApi.list(), toolsApi.list(), ]); setModelResources(creds); setKnowledgeBases(kbs); setTools(toolRows); } catch { // 拉取失败时下拉为空,不阻塞表单 } }, []); // 编辑页挂载时加载下拉数据源 useEffect(() => { if (props.mode !== "edit") return; // eslint-disable-next-line react-hooks/set-state-in-effect void loadResources(); }, [props.mode, loadResources]); // 按资源类型生成 {value:id, label:name} 选项 const credOptions = (type: ModelResource["capability"]) => modelResources .filter((c) => c.capability === type) .map((c) => ({ value: c.id, label: c.name })); const agentOptions = (interfaceType: "dify" | "fastgpt" | "opencode") => modelResources .filter( (resource) => resource.capability === "Agent" && resource.interfaceType === interfaceType, ) .map((resource) => ({ value: resource.id, label: resource.name })); const visionModelOptionsFor = (currentModelId: string) => modelResources .filter( (c) => c.capability === "LLM" && c.supportImageInput && c.id !== currentModelId, ) .map((c) => ({ value: c.id, label: c.name })); const kbOptions = knowledgeBases.map((k) => ({ value: k.id, label: k.name })); function handlePromptVisionEnabledChange(enabled: boolean) { updateForm("visionEnabled", enabled); if (!enabled) updateForm("visionModelResourceId", ""); } function handlePromptModelChange(value: string) { updateForm("model", value); if (form.visionModelResourceId === value) { updateForm("visionModelResourceId", ""); } } function handleOpenCodeVisionEnabledChange(enabled: boolean) { updateOpenCodeForm("visionEnabled", enabled); if (!enabled) updateOpenCodeForm("visionModelResourceId", ""); } function handleOpenCodeModelChange(value: string) { updateOpenCodeForm("model", value); if (openCodeForm.visionModelResourceId === value) { updateOpenCodeForm("visionModelResourceId", ""); } } function startCreate() { router.push("/assistants/new"); } // 把后端 Assistant 回填进提示词表单(model/asr/voice 等存模型资源 id) // 返回回填后的表单,供调用方记录"已保存基线" function fillPromptForm(a: Assistant): AssistantForm { const next: AssistantForm = { name: a.name, greeting: a.greeting, prompt: a.prompt, dynamicVariableDefinitions: a.dynamicVariableDefinitions ?? {}, runtimeMode: a.runtimeMode, realtimeModel: a.modelResourceIds.Realtime ?? "", model: a.modelResourceIds.LLM ?? "", asr: a.modelResourceIds.ASR ?? "", voice: a.modelResourceIds.TTS ?? "", knowledgeBase: a.knowledgeBaseId ?? "", knowledgeRetrievalConfig: a.knowledgeRetrievalConfig ?? defaultKnowledgeRetrievalConfig(), enableInterrupt: a.enableInterrupt, turnConfig: normalizeTurnConfig(a.turnConfig), visionEnabled: a.visionEnabled, visionModelResourceId: a.visionModelResourceId ?? "", toolIds: a.toolIds ?? [], }; setForm(next); return next; } // 编辑:跳转到 /assistants/[id],由编辑页按助手类型回填表单 function handleEdit(assistant: AssistantListItem) { router.push(`/assistants/${assistant.id}`); } // 引导页确认:直接创建到数据库拿到 id,再进入该助手的编辑页 async function confirmType() { if ( !draftName.trim() || !draftType || (draftType === "智能体平台" && !draftAgentPlatform) || creating ) { return; } setCreating(true); setCreateError(null); try { let assistantType: ApiAssistantType; if (draftType === "智能体平台") { if (!draftAgentPlatform) return; assistantType = draftAgentPlatform; } else { assistantType = typeFromBuildMethod[draftType]; } const created = await assistantsApi.create( baseUpsert({ name: draftName.trim(), type: assistantType, }), ); router.push(`/assistants/${created.id}`); } catch (error) { setCreateError(error instanceof Error ? error.message : "创建失败"); setCreating(false); } } // 复制助手:服务端整行复制(含真 key,密钥不经浏览器) async function handleDuplicate(assistant: AssistantListItem) { try { await assistantsApi.duplicate(assistant.id); await loadAssistants(); } catch (error) { setListError(error instanceof Error ? error.message : "复制失败"); } } // 删除助手 async function handleDelete(id: string) { try { await assistantsApi.remove(id); await loadAssistants(); } catch (error) { setListError(error instanceof Error ? error.message : "删除失败"); } } // 平铺字段的空白 upsert,各类型按需覆盖(后端会按 type 清掉无关字段) function baseUpsert(over: Partial): AssistantUpsert { return { name: "", type: "prompt", runtimeMode: "pipeline", greeting: "", enableInterrupt: true, turnConfig: defaultTurnConfig(), visionEnabled: false, visionModelResourceId: null, modelResourceIds: {}, knowledgeBaseId: null, knowledgeRetrievalConfig: defaultKnowledgeRetrievalConfig(), toolIds: [], prompt: "", dynamicVariableDefinitions: {}, apiUrl: "", apiKey: "", appId: "", graph: {}, ...over, }; } // 保存后停留在编辑页,并把当前表单记为已保存基线(按钮随之置灰)。 async function save(payload: AssistantUpsert) { if (!editingId) return; setSaving(true); setSaveError(null); try { await assistantsApi.update(editingId, payload); if (view === "create") { setSavedSnapshot(JSON.stringify(form)); } else if (view === "create-agent-platform") { const next = { ...agentPlatformForm }; setAgentPlatformForm(next); setSavedSnapshot(JSON.stringify(next)); } else if (view === "create-opencode") { const next = { ...openCodeForm }; setOpenCodeForm(next); setSavedSnapshot(JSON.stringify(next)); } else if (view === "workflow") { setSavedSnapshot( JSON.stringify({ name: workflowName, graph: workflowGraph, settings: workflowSettings, dynamicVariableDefinitions: effectiveWorkflowDynamicVariableDefinitions, }), ); } } catch (error) { setSaveError(error instanceof Error ? error.message : "保存失败"); } finally { setSaving(false); } } function handleSavePrompt() { void save( baseUpsert({ name: form.name.trim(), type: "prompt", runtimeMode: form.runtimeMode, greeting: form.greeting, enableInterrupt: form.enableInterrupt, turnConfig: form.turnConfig, visionEnabled: form.visionEnabled, visionModelResourceId: form.visionModelResourceId || null, modelResourceIds: { ...(form.model ? { LLM: form.model } : {}), ...(form.asr ? { ASR: form.asr } : {}), ...(form.voice ? { TTS: form.voice } : {}), ...(form.realtimeModel ? { Realtime: form.realtimeModel } : {}), }, knowledgeBaseId: form.runtimeMode === "pipeline" ? form.knowledgeBase || null : null, knowledgeRetrievalConfig: form.knowledgeRetrievalConfig, toolIds: form.toolIds, prompt: form.prompt, dynamicVariableDefinitions: effectiveDynamicVariableDefinitions, }), ); } // ---- 外部智能体平台(Dify / FastGPT)---- function fillAgentPlatformForm(a: Assistant): AgentPlatformForm { const next: AgentPlatformForm = { name: a.name, platform: a.type === "fastgpt" ? "fastgpt" : "dify", agent: a.modelResourceIds.Agent ?? "", asr: a.modelResourceIds.ASR ?? "", voice: a.modelResourceIds.TTS ?? "", enableInterrupt: a.enableInterrupt, turnConfig: normalizeTurnConfig(a.turnConfig), }; setAgentPlatformForm(next); return next; } function handleSaveAgentPlatform() { void save( baseUpsert({ name: agentPlatformForm.name.trim(), type: agentPlatformForm.platform, enableInterrupt: agentPlatformForm.enableInterrupt, turnConfig: agentPlatformForm.turnConfig, modelResourceIds: { ...(agentPlatformForm.agent ? { Agent: agentPlatformForm.agent } : {}), ...(agentPlatformForm.asr ? { ASR: agentPlatformForm.asr } : {}), ...(agentPlatformForm.voice ? { TTS: agentPlatformForm.voice } : {}), }, }), ); } // ---- OpenCode ---- function fillOpenCodeForm(a: Assistant): OpenCodeForm { const next: OpenCodeForm = { name: a.name, prompt: a.prompt, agent: a.modelResourceIds.Agent ?? "", model: a.modelResourceIds.LLM ?? "", asr: a.modelResourceIds.ASR ?? "", voice: a.modelResourceIds.TTS ?? "", enableInterrupt: a.enableInterrupt, turnConfig: normalizeTurnConfig(a.turnConfig), visionEnabled: a.visionEnabled, visionModelResourceId: a.visionModelResourceId ?? "", }; setOpenCodeForm(next); return next; } // 编辑模式:按路由中的 id 拉取助手,回填对应类型的表单后再切换视图 useEffect(() => { if (props.mode !== "edit" || !editingId) return; let cancelled = false; void (async () => { try { const assistant = await assistantsApi.get(editingId); if (cancelled) return; if (assistant.type === "prompt") { setSavedSnapshot(JSON.stringify(fillPromptForm(assistant))); } else if ( assistant.type === "fastgpt" || assistant.type === "dify" ) { setSavedSnapshot(JSON.stringify(fillAgentPlatformForm(assistant))); } else if (assistant.type === "opencode") { setSavedSnapshot(JSON.stringify(fillOpenCodeForm(assistant))); } else { // 工作流:回填名称与图(空图回落到默认 开始→智能体→结束) const graph = assistant.graph && Array.isArray((assistant.graph as WorkflowGraph).nodes) && (assistant.graph as WorkflowGraph).nodes.length > 0 ? (assistant.graph as WorkflowGraph) : defaultGraph(); const wfSettings: WorkflowSettings = { llm: graph.settings?.defaultLlmResourceId || assistant.modelResourceIds.LLM, asr: graph.settings?.defaultAsrResourceId || assistant.modelResourceIds.ASR, tts: graph.settings?.defaultTtsResourceId || assistant.modelResourceIds.TTS, visionEnabled: graph.settings?.visionEnabled ?? assistant.visionEnabled, visionModelResourceId: graph.settings?.visionModelResourceId ?? assistant.visionModelResourceId ?? "", toolIds: graph.settings?.toolIds ?? [], knowledgeBaseId: graph.settings?.knowledgeBaseId || assistant.knowledgeBaseId || "", knowledgeRetrievalConfig: { mode: graph.settings?.knowledgeMode || assistant.knowledgeRetrievalConfig.mode, topN: graph.settings?.knowledgeTopN ?? assistant.knowledgeRetrievalConfig.topN, scoreThreshold: graph.settings?.knowledgeScoreThreshold ?? assistant.knowledgeRetrievalConfig.scoreThreshold, }, globalPrompt: graph.settings?.globalPrompt ?? "", allowInterrupt: graph.settings?.enableInterrupt ?? assistant.enableInterrupt, turnConfig: normalizeTurnConfig( graph.settings?.turnConfig ?? assistant.turnConfig, ), }; setWorkflowName(assistant.name); setWorkflowGraph(graph); setWorkflowSettings(wfSettings); const dynamicVariableDefinitions = activeWorkflowDynamicVariableDefinitions( graph, assistant.dynamicVariableDefinitions ?? {}, ); setWorkflowDynamicVariableDefinitions( dynamicVariableDefinitions, ); setSavedSnapshot( JSON.stringify({ name: assistant.name, graph, settings: wfSettings, dynamicVariableDefinitions, }), ); } setView(typeToView[assistant.type]); } catch (error) { if (!cancelled) { setLoadError( error instanceof Error ? error.message : "加载助手失败", ); } } })(); return () => { cancelled = true; }; }, [ props.mode, editingId, setWorkflowDynamicVariableDefinitions, setWorkflowGraph, setWorkflowName, setWorkflowSettings, ]); function handleSaveOpenCode() { void save( baseUpsert({ name: openCodeForm.name.trim(), type: "opencode", enableInterrupt: openCodeForm.enableInterrupt, turnConfig: openCodeForm.turnConfig, visionEnabled: openCodeForm.visionEnabled, visionModelResourceId: openCodeForm.visionModelResourceId || null, modelResourceIds: { ...(openCodeForm.agent ? { Agent: openCodeForm.agent } : {}), ...(openCodeForm.model ? { LLM: openCodeForm.model } : {}), ...(openCodeForm.asr ? { ASR: openCodeForm.asr } : {}), ...(openCodeForm.voice ? { TTS: openCodeForm.voice } : {}), }, prompt: openCodeForm.prompt, }), ); } function handleSaveWorkflow() { void save( baseUpsert({ name: workflowName.trim(), type: "workflow", enableInterrupt: workflowSettings.allowInterrupt, turnConfig: workflowSettings.turnConfig, visionEnabled: workflowUsesVision(workflowGraph), visionModelResourceId: null, modelResourceIds: { ...(workflowSettings.llm ? { LLM: workflowSettings.llm } : {}), ...(workflowSettings.asr ? { ASR: workflowSettings.asr } : {}), ...(workflowSettings.tts ? { TTS: workflowSettings.tts } : {}), }, knowledgeBaseId: workflowSettings.knowledgeBaseId || null, knowledgeRetrievalConfig: workflowSettings.knowledgeRetrievalConfig, toolIds: workflowSettings.toolIds, graph: workflowGraph as unknown as Record, dynamicVariableDefinitions: effectiveWorkflowDynamicVariableDefinitions, }), ); } // 当前编辑器表单相对已保存基线是否有改动(决定保存按钮是否可点) const activeFormJson = view === "create" ? JSON.stringify(form) : view === "create-agent-platform" ? JSON.stringify(agentPlatformForm) : view === "create-opencode" ? JSON.stringify(openCodeForm) : view === "workflow" ? JSON.stringify({ name: workflowName, graph: workflowGraph, settings: workflowSettings, dynamicVariableDefinitions: effectiveWorkflowDynamicVariableDefinitions, }) : null; const dirty = activeFormJson !== null && savedSnapshot !== null && activeFormJson !== savedSnapshot; const listItems: AssistantListItem[] = assistants.map((a) => ({ id: a.id, name: a.name, type: typeToLabel[a.type], updatedAt: formatTimestamp(a.updatedAt), updatedAtRaw: a.updatedAt, })); const filteredAssistants = listItems.filter((assistant) => { if (typeFilter !== "全部" && assistant.type !== typeFilter) { return false; } const keyword = searchQuery.trim().toLowerCase(); if (!keyword) { return true; } return [assistant.name, assistant.id, assistant.type, assistant.updatedAt] .join(" ") .toLowerCase() .includes(keyword); }); // 按更新时间排序(以原始 ISO 时间戳为准);缺失时间戳视为最早,id 作为稳定的次级排序键 const timeValue = (iso: string | null | undefined) => { const t = iso ? new Date(iso).getTime() : NaN; return Number.isNaN(t) ? 0 : t; }; const sortedAssistants = [...filteredAssistants].sort((a, b) => { const diff = timeValue(b.updatedAtRaw) - timeValue(a.updatedAtRaw); if (diff !== 0) return sortOrder === "newest" ? diff : -diff; return a.id.localeCompare(b.id); }); const pageSize = 5; const totalPages = Math.max(1, Math.ceil(sortedAssistants.length / pageSize)); const safeCurrentPage = Math.min(currentPage, totalPages); const pageStart = (safeCurrentPage - 1) * pageSize; const pageEnd = pageStart + pageSize; const paginatedAssistants = sortedAssistants.slice(pageStart, pageEnd); function handleSearchChange(value: string) { setSearchQuery(value); setCurrentPage(1); } function handleFilterChange(filter: TypeFilter) { setTypeFilter(filter); setCurrentPage(1); } function handleSortChange(order: SortOrder) { setSortOrder(order); setCurrentPage(1); } function toggleSortOrder() { handleSortChange(sortOrder === "newest" ? "oldest" : "newest"); } function updateForm( key: K, value: AssistantForm[K], ) { setForm((prev) => ({ ...prev, [key]: value, })); } function updateAgentPlatformForm( key: K, value: AgentPlatformForm[K], ) { setAgentPlatformForm((prev) => ({ ...prev, [key]: value, })); } function handleAgentPlatformChange(platform: AgentPlatform) { setAgentPlatformForm((prev) => ({ ...prev, platform, agent: prev.platform === platform ? prev.agent : "", })); } function updateOpenCodeForm( key: K, value: OpenCodeForm[K], ) { setOpenCodeForm((prev) => ({ ...prev, [key]: value, })); } if (view === "loading") { return (
{loadError ? (
加载助手失败
{loadError}
) : (
正在加载助手…
)}
); } if (view === "list") { return (
创建助手 } />
} search={ } /> rows={paginatedAssistants} rowKey={(assistant) => assistant.id} loading={listLoading} loadingText="正在加载助手列表…" error={listError} onRetry={() => void loadAssistants()} empty={{ title: listItems.length === 0 ? "暂无助手" : "未找到匹配的助手", description: listItems.length === 0 ? "点击右上角「创建助手」开始。" : "请调整关键词或筛选条件后再试。", }} pagination={{ page: safeCurrentPage, totalPages, onPageChange: setCurrentPage, summary: filteredAssistants.length === 0 ? "没有数据" : `显示 ${pageStart + 1}-${Math.min(pageEnd, filteredAssistants.length)} / 共 ${filteredAssistants.length} 个助手`, }} columns={[ { key: "name", header: "助手名称", width: "md:w-[360px]", cell: (assistant) => ( <>
{assistant.name}
{assistant.id}
), }, { key: "type", header: "助手类型", width: "md:w-[128px]", cell: (assistant) => ( {assistant.type} ), }, { key: "updatedAt", width: "md:w-[176px]", header: ( ), cellClassName: "whitespace-nowrap tabular-nums text-muted-foreground", cell: (assistant) => assistant.updatedAt, }, { key: "actions", header: "操作", align: "right", cellClassName: "flex justify-end gap-2", cell: (assistant) => ( <> handleDuplicate(assistant)} > 复制 { event.preventDefault(); void handleDelete(assistant.id); }} > 删除 ), }, ]} />
); } if (view === "choose") { return (

创建助手

先为助手取个名字,再选择构建方式。确认后将立即创建助手并进入编辑页。

构建方式
{assistantTypeOptions.map((option) => { const selected = draftType === option.type; return ( ); })}
{draftType === "智能体平台" && (
选择智能体平台

平台类型用于匹配对应的应用资源和运行方式。

{(["dify", "fastgpt"] as const).map((platform) => { const selected = draftAgentPlatform === platform; const label = platform === "dify" ? "Dify" : "FastGPT"; return ( ); })}
)}
{createError && ( {createError} )}
); } if (view === "workflow") { return ( tool.status === "active") .map((tool) => ({ value: tool.id, label: tool.name }))} knowledgeOptions={kbOptions} onBack={() => router.push("/assistants")} onSave={() => void handleSaveWorkflow()} setWorkflowName={setWorkflowName} setWorkflowGraph={setWorkflowGraph} setWorkflowSettings={setWorkflowSettings} setWorkflowDynamicVariableDefinitions={ setWorkflowDynamicVariableDefinitions } setWorkflowDebugOpen={setWorkflowDebugOpen} setWorkflowSettingsOpen={setWorkflowSettingsOpen} setWorkflowEditingNodeId={setWorkflowEditingNodeId} setWorkflowEditingEdgeId={setWorkflowEditingEdgeId} setActiveNodeId={setActiveNodeId} setDynamicVariablesOpen={setDynamicVariablesOpen} /> ); } if (view === "create-agent-platform") { const platformLabel = agentPlatformForm.platform === "dify" ? "Dify" : "FastGPT"; return (
router.push("/assistants")} /> updateAgentPlatformForm("name", value)} />
{saveError && ( {saveError} )}
} title="智能体平台配置" description={`选择智能体平台及对应应用。开场白、知识库、提示词等对话编排请在 ${platformLabel} 平台配置,本页不重复设置。`} >
智能体平台
{(["dify", "fastgpt"] as const).map((platform) => { const selected = agentPlatformForm.platform === platform; const label = platform === "dify" ? "Dify" : "FastGPT"; return ( ); })}
updateAgentPlatformForm("agent", value)} options={agentOptions(agentPlatformForm.platform)} noneLabel="请选择" />
} title="语音配置" description={`从「模型资源」中选择语音识别与语音合成。大模型、知识库与开场白由 ${platformLabel} 应用提供,请前往 ${platformLabel} 平台配置。`} > updateAgentPlatformForm("asr", value)} options={credOptions("ASR")} noneLabel="无" /> updateAgentPlatformForm("voice", value)} options={credOptions("TTS")} noneLabel="无" /> } title="交互策略" description="设置实时视频对话时的交互体验" > updateAgentPlatformForm("enableInterrupt", checked) } onConfigChange={(config) => updateAgentPlatformForm("turnConfig", config) } />
); } if (view === "create-opencode") { return (
router.push("/assistants")} /> updateOpenCodeForm("name", value)} />
{saveError && ( {saveError} )}
} title="OpenCode 服务配置" description="从「模型资源」中选择 OpenCode 服务资源。" > updateOpenCodeForm("agent", value)} options={agentOptions("opencode")} noneLabel="请选择" /> } title="提示词" description="描述助手的角色、能力和回答要求" > updateOpenCodeForm("prompt", value)} placeholder="请输入提示词,描述助手的角色、能力和回答要求" rows={8} /> } title="模型与语音配置" description="配置 OpenCode 使用的大语言模型、语音识别与语音合成资源。" > {openCodeForm.visionEnabled && ( updateOpenCodeForm("visionModelResourceId", value) } options={visionModelOptionsFor(openCodeForm.model)} noneLabel="模型自己" /> )} updateOpenCodeForm("asr", value)} options={credOptions("ASR")} noneLabel="无" /> updateOpenCodeForm("voice", value)} options={credOptions("TTS")} noneLabel="无" /> } title="交互策略" description="设置实时视频对话时的交互体验" > updateOpenCodeForm("enableInterrupt", checked)} onConfigChange={(config) => updateOpenCodeForm("turnConfig", config)} />
); } return ( router.push("/assistants")} onSave={() => void handleSavePrompt()} setDynamicVariablesOpen={setDynamicVariablesOpen} updateForm={updateForm} handlePromptVisionEnabledChange={handlePromptVisionEnabledChange} handlePromptModelChange={handlePromptModelChange} /> ); }