diff --git a/frontend-web/.env.production b/frontend-web/.env.production index c655d856..d7dac2cd 100644 --- a/frontend-web/.env.production +++ b/frontend-web/.env.production @@ -5,6 +5,8 @@ VITE_BACKEND_BASE_URL=https://ch1.b.uat.4.cn/deerflow VITE_LANGGRAPH_BASE_URL=https://ch1.b.uat.4.cn/deerflow/api +# CMZS后端接口 +VITE_API_BASE_URL=https://ch1.b.uat.4.cn/zncm # 轻应用「新建笔记」系列接口 VITE_NOTE_API_BASE_URL=https://your-intranet-host/note-api # 佳来模型对话接口 diff --git a/parallel-agents-vue/docs/07-使用说明.md b/parallel-agents-vue/docs/07-使用说明.md index 6a5c2e1d..25d8acf4 100644 --- a/parallel-agents-vue/docs/07-使用说明.md +++ b/parallel-agents-vue/docs/07-使用说明.md @@ -4,6 +4,14 @@ `ParallelSeatStrip` / `Avatar` / `MarkdownContent` 都是它的内部子组件,**无需也不要单独引入**)。 宿主只管把它放进自己的页面 / 抽屉 / 弹窗,并传几个 prop。 +> **⚠️ 契约变更(纯展示版)**:面板**不再自带顶部表单与按钮**。 +> - **任务名称 / 简介 / 模型** 全部由宿主通过 props 传入(`taskName` / `brief` 支持 `v-model`); +> - **「重新分析 / 保存 / 停止」按钮由宿主自己渲染**,通过模板 ref 调用面板 `defineExpose` 出的 +> `reanalyze()` / `save()` / `cancel()` 等方法(见 [§六·二](#六二暴露的方法与状态模板-ref)); +> - 面板本体只展示「席位状态条 + 并行会话 + 错误提示」。 +> +> 完整的宿主参考实现见仓库 `src/App.vue`(弹窗里宿主自渲染工具条 + 调用 ref 方法)。 + --- ## 一、要引入的东西(就这两样) @@ -55,8 +63,11 @@ src/ ### 局部引入(推荐) +宿主持有任务名 / 简介 / 模型,并自渲染按钮调用面板暴露的方法: + ```vue - - + + + + + + {{ panel?.hasResults ? "重新分析" : "开始分析" }} + + 保存 + + + ``` +> 面板暴露出的 `running` / `saving` / `hasResults` / `canStart` 等是 ref,**在组件实例上会自动解包**, +> 模板里直接 `panel?.canStart` 读,**无需 `.value`**。 + ### 全局注册(可选) ```ts @@ -90,8 +129,20 @@ app.use(ParallelAgentsVue); // 之后任意模板可直接用 - - + + + + ``` @@ -122,14 +173,66 @@ app.use(ParallelAgentsVue); // 之后任意模板可直接用 string \| Promise` | | 动态取 token | | `loginPassword` | `string` | | 仅当后端开启口令门时需要 | | `taskId` | `string` | | 传入则挂载时自动加载并回显该任务 | -| `model` | `string` | | 可选模型名(不传走默认 / 后端回退) | +| `taskName` | `string` | | **任务名称(宿主持有)**,支持 `v-model:task-name` | +| `brief` | `string` | | **任务简介(宿主持有)**,支持 `v-model:brief` | +| `model` | `string` | | 模型名(宿主持有;空 = 默认 / 后端回退) | | `dark` | `boolean`(默认 `true`) | | 深色面板(自带成色,不依赖宿主主题) | +> `taskName` / `brief` 用 `v-model` 是为了**回显**:加载已保存任务时,面板会通过 `update:taskName` / +> `update:brief` 把存档里的标题 / 简介回传给宿主,宿主据此同步自己的输入框。 + `AgentInput`/`SavedTask`/`AgentResult` 等类型也从包里导出: `import type { AgentInput, SavedTask, AgentResult } from "parallel-agents-vue"`。 --- +## 六·二、暴露的方法与状态(模板 ref) + +面板通过 `defineExpose` 暴露下列**方法**与**状态**,供宿主用模板 ref 调用 / 读取。 +拿 ref:`const panel = ref | null>(null)`,模板 ``。 + +**方法** + +| 方法 | 等价于 | 说明 | +| --------------------- | ----------------- | -------------------------------------------- | +| `reanalyze()` | 旧「重新分析」按钮 | 用当前 `taskName`/`brief`/`model` 开始 / 重跑一轮 | +| `start()` | 同 `reanalyze()` | 别名,语义同上 | +| `save()` | 旧「保存」按钮 | 保存 / 更新当前聊天记录(按 `taskId` upsert)| +| `cancel()` | 旧「停止」按钮 | 中止本轮并行运行 | +| `loadTask(id)` | 旧「历史」选择 | 加载并回显某条任务(同时回传标题/简介) | +| `refreshHistory()` | — | 重新拉取历史列表(结果在 `history` 里) | +| `getResults()` | — | 取所有智能体返回内容 `AgentResult[]` | +| `getResult(id)` | — | 取某个智能体返回内容 `AgentResult \| null` | + +**状态(ref,组件实例上自动解包,模板里直接读无需 `.value`)** + +| 状态 | 类型 | 说明 | +| -------------- | ---------------- | ----------------------------------- | +| `running` | `boolean` | 是否正在并行运行(用于切「停止」按钮)| +| `saving` | `boolean` | 是否正在保存 | +| `hasResults` | `boolean` | 是否已有结果(「开始」/「重新」文案 & 保存可用)| +| `canStart` | `boolean` | 是否满足开始条件(有 agents + 任务名或简介)| +| `history` | `SavedTask[]` | 已保存任务列表(宿主可自建历史下拉)| +| `errorMsg` | `string \| null` | 最近一次错误(面板内也会展示一条) | + +```vue + + + + + {{ panel?.hasResults ? "重新分析" : "开始分析" }} + + +``` + +--- + ## 六·一、每个参数怎么传(逐个举例) ### `agents`(必填)——「智能体参数」怎么传 @@ -171,11 +274,24 @@ const agents = [ ``` -### `model`——指定模型(不传走默认/后端回退;也可在面板右上角下拉切) +### `model`——指定模型(宿主持有;不传走默认/后端回退) ```vue - + ``` +> 模型下拉已移到宿主侧:宿主自己渲染下拉(可调后端 `/api/models`),把选中的模型名用 `:model` 传进来。 + +### `taskName` / `brief`——任务名 / 简介(宿主持有,支持 v-model) + +```vue + +``` +> 用 `v-model` 是为了**回显**:加载已保存任务时面板会把存档标题 / 简介回传给宿主。 ### `dark`——深色(默认 true,自带成色) @@ -208,6 +324,11 @@ const agents: AgentInput[] = [ { agent_id: "roundtable-risk-review", name: "风险审查" }, ]; const taskId = ref("task-001"); +const taskName = ref(""); +const brief = ref(""); +const model = ref("deepseek-chat"); +const panel = ref | null>(null); + function onSaved(t: SavedTask) { console.log("已保存", t.id); } @@ -217,13 +338,26 @@ function onCompleted(results: AgentResult[]) { - + + + + + + {{ panel?.hasResults ? "重新分析" : "开始分析" }} + + 停止 + 保存 + (taskId = id)" @@ -237,9 +371,12 @@ function onCompleted(results: AgentResult[]) { | 事件 | 载荷 | 触发时机 | | ----------------- | ------------------ | --------------------------------------- | -| `saved` | `SavedTask` | 保存成功(新建或更新)后 | +| `saved` | `SavedTask` | 保存成功(新建或更新)后;**含自动保存** | | `update:taskId` | `string` | 当前任务 id 变化(保存 / 加载历史时) | -| `completed` | `AgentResult[]` | 一轮并行分析结束时,回传各智能体返回内容 | +| `update:taskName` | `string` | 加载已保存任务回显标题(配合 `v-model:task-name`)| +| `update:brief` | `string` | 加载已保存任务回显简介(配合 `v-model:brief`)| +| `agent-completed` | `AgentResult` | **每个**智能体一分析完就回传它单个的结果(各席位各触发一次)| +| `completed` | `AgentResult[]` | 一轮并行分析**全部**结束时,回传所有智能体返回内容 | ```vue { + // r: { agent_id, name, content, status } —— 单个智能体 + console.log('完成一个:', r.name, '→', r.content); + }" +/> +``` + +**A·2. 全部完成再推一次**:监听 `@completed`,一轮分析**全部**结束时回传 `AgentResult[]`: ```vue ``` +> 两个事件可同时用:`agent-completed` 做「来一个渲染一个」的增量更新,`completed` 做「全部到齐」的汇总。 + **B. 方法(模板 ref)**:随时主动取(不必等事件): ```vue @@ -294,14 +446,22 @@ function readOne(id: string) { ``` `AgentResult = { agent_id, name, content, status }`,`content` 即该智能体模型返回的正文(Markdown)。 -`defineExpose` 还暴露了 `start()` / `cancel()`,可由宿主代替按钮触发开始 / 停止。 +`defineExpose` 还暴露了 `reanalyze()` / `start()` / `save()` / `cancel()` / `loadTask(id)` 等方法及 +`running` / `saving` / `hasResults` / `canStart` / `history` 等状态,完整清单见 [§六·二](#六二暴露的方法与状态模板-ref)。 ## 七·二、如何保存 + 回显聊天记录 -**保存**:面板里点「保存」即把当前整段聊天记录(各智能体气泡 + 步骤条等)存到后端,按用户隔离。 +**保存**:宿主调用 `panel.save()` 即把当前整段聊天记录(各智能体气泡 + 步骤条等)存到后端,按用户隔离。 传 `:task-id` 时**以它为存储主键**做 upsert(已存在→更新;不存在→用该 id 新建);不传则首次保存由后端 生成 id(通过 `@update:taskId` 回传,下次再保存即更新这条)。 +> **自动保存**:每轮分析(含单席位重跑)**结束后会自动保存一次**,宿主无需手动点保存也不丢记录; +> 自动保存同样走 upsert,并照常触发 `saved` / `update:taskId` 事件(首轮会回传新生成的 id)。 +> `panel.save()` 仍保留,供宿主想随时手动存档时调用。 + +> **进入默认显示最新**:组件挂载时若**没传 `taskId`**,会自动加载并回显**该用户最新的一条**聊天记录 +> (历史按更新时间倒序,取第一条);传了 `taskId` 则优先回显指定那条。 + **回显(两种方式)**: ```vue @@ -316,7 +476,20 @@ const taskId = ref(""); // 留空=新任务 taskId.value = "task-001"; // 改了之后面板自动回显 task-001 ``` -> 方式 2:面板**右上角内置「历史」下拉**——点开选一条历史任务即回显,**无需写任何代码**。 +> 方式 2:**历史下拉已移到宿主侧**(面板不再内置)。宿主读 `panel.history`(`SavedTask[]`)自建下拉, +> 选中某条时调 `panel.loadTask(id)` 回显;面板会通过 `update:taskName` / `update:brief` 把存档标题 / 简介 +> 回传给宿主,配合 `v-model` 同步宿主自己的输入框。需要刷新列表调 `panel.refreshHistory()`。 + +```vue + + + panel?.loadTask((e.target as HTMLSelectElement).value)"> + + {{ t.title || "未命名任务" }} + + + +``` 要点:先**保存过**、再用**同一个 taskId**(或从历史里选)才会有内容回显;没存过的新 id 第一次进去是空白 (后台会探测一次 `GET /…/{id}` 返回 404,属正常,面板静默处理)。 @@ -325,14 +498,17 @@ taskId.value = "task-001"; // 改了之后面板自动回显 task- ## 八、交互要点(与圆桌 Step2 一致) -- 输入「任务名称 + 简介」→「开始分析」→ 各智能体**并行**输出。运行期间禁用**写文件**(`write_file`/ - `str_replace`)与**派活/脚本**(`agent_orchestration`/`bash`,防子智能体模仿总控调度);检索、查看、 - `ask_clarification` 等照常 —— 所以**步骤条 / 协作卡片正常显示**,每个智能体只做自己那份分析。 +- 宿主备好「任务名称 + 简介(+ 模型)」props → 调 `panel.reanalyze()` → 各智能体**并行**输出。 + 运行期间禁用**写文件**(`write_file`/`str_replace`)与**派活/脚本**(`agent_orchestration`/`bash`, + 防子智能体模仿总控调度);检索、查看、`ask_clarification` 等照常 —— 所以**步骤条 / 协作卡片正常 + 显示**,每个智能体只做自己那份分析。 - 主区每次只显示**一个**席位的消息列表;**点席位头像切换**要看哪个(单选)。 - 顶部席位状态条显示每个智能体的进行中 / 完成 / 出错。 -- 操作区右侧有**模型切换**下拉(拉取后端 `/api/models`);选「默认」则用 `model` prop / 配置 / 后端回退。 - 切换作用于**下一次**「开始分析 / 重新分析」,运行中禁用。 -- 「保存」存档;「历史」里选回**回显**;「重新分析」清空重跑。 +- **任务名 / 简介 / 模型 / 按钮 / 历史下拉全部在宿主侧**:模型空 = `model` prop / 配置 / 后端回退; + 模型作用于**下一次**`reanalyze()`。 +- `panel.save()` 存档;`panel.loadTask(id)` 回显;`panel.reanalyze()` 清空重跑;`panel.cancel()` 停止。 +- **每轮分析结束自动保存一次**;挂载时没传 `taskId` 会**默认回显最新一条**记录。 +- 结果自动推给宿主:`@agent-completed`(来一个推一个)+ `@completed`(全部到齐再推一次)。 --- diff --git a/parallel-agents-vue/src/App.vue b/parallel-agents-vue/src/App.vue index 6422be50..c680596e 100644 --- a/parallel-agents-vue/src/App.vue +++ b/parallel-agents-vue/src/App.vue @@ -1,5 +1,10 @@ @@ -258,98 +307,17 @@ onMounted(() => { class="parallel-agents pa-bg flex h-full w-full min-h-0 flex-col" :class="dark ? 'parallel-agents--dark' : ''" > - - - - - - - 历史 ▾ - - - - 暂无保存的任务 - - - {{ t.title || "未命名任务" }} - · {{ (t.updated_at || "").slice(0, 16).replace("T", " ") }} - - - - - - - - {{ hasResults ? "重新分析" : "开始分析" }} - - - 停止 - - - {{ saving ? "保存中…" : currentTaskId ? "更新保存" : "保存" }} - + - - - 模型 - - 默认 - - {{ m.display_name || m.name }} - - - - - - ⚠️ 未传入任何智能体(agents 为空) - - - {{ errorMsg }} - + + + {{ errorMsg }} + @@ -383,7 +351,7 @@ onMounted(() => { v-else class="flex h-full flex-col items-center justify-center gap-2 text-center" > - 填写任务名称与简介,点「开始分析」并行调用各智能体。 + 等待宿主触发「开始 / 重新分析」,并行调用各智能体。 并行运行期间禁止写文件,产出直接呈现在正文。 diff --git a/parallel-agents-vue/src/composables/useParallelRun.ts b/parallel-agents-vue/src/composables/useParallelRun.ts index 0167af05..b316a9f2 100644 --- a/parallel-agents-vue/src/composables/useParallelRun.ts +++ b/parallel-agents-vue/src/composables/useParallelRun.ts @@ -18,12 +18,25 @@ function nextId(): number { return _seq++; } -export function useParallelRun() { +export interface ParallelRunOptions { + /** 单个智能体进入终态(完成 / 待确认 / 出错)时回调一次,用于「一分析完就回传」。 */ + onAgentSettled?: (agentId: string, status: SeatView["status"]) => void; +} + +export function useParallelRun(opts: ParallelRunOptions = {}) { const dialogues = ref([]); const seats = ref([]); const running = ref(false); const abortRef = shallowRef(null); + // 本轮已「结算」(回调过 onAgentSettled)的 agentId,去重,避免重复回传。 + const settled = new Set(); + function settleAgent(agentId: string, status: SeatView["status"]) { + if (settled.has(agentId)) return; + settled.add(agentId); + opts.onAgentSettled?.(agentId, status); + } + // 增量节流:累积到 pending,rAF 合并刷新一次,避免逐 token reflow。 const pending = new Map(); // agentId -> 待追加文本 let rafScheduled = false; @@ -93,6 +106,7 @@ export function useParallelRun() { } setSeatStatus(ev.agent_id, "done"); dialogues.value = [...dialogues.value]; + settleAgent(ev.agent_id, "done"); break; } case "agent_clarification": { @@ -113,6 +127,7 @@ export function useParallelRun() { } setSeatStatus(ev.agent_id, "done"); dialogues.value = [...dialogues.value]; + settleAgent(ev.agent_id, "done"); break; } case "agent_error": { @@ -125,6 +140,7 @@ export function useParallelRun() { } setSeatStatus(ev.agent_id, "error"); dialogues.value = [...dialogues.value]; + settleAgent(ev.agent_id, "error"); break; } case "all_done": @@ -188,6 +204,8 @@ export function useParallelRun() { } } dialogues.value = [...dialogues.value]; + // 兜底:没收到终态事件就断流的席位也回调一次(按当前席位状态)。 + for (const s of seats.value) settleAgent(s.agentId, s.status === "error" ? "error" : "done"); running.value = false; abortRef.value = null; } @@ -218,6 +236,7 @@ export function useParallelRun() { setSeatStatus(args.agentId, "running"); dialogues.value = [...dialogues.value]; + settled.delete(args.agentId); // 该席位重跑,允许再次回传 running.value = true; const ac = new AbortController(); abortRef.value = ac; @@ -235,6 +254,8 @@ export function useParallelRun() { if (dd.confidence === "并行执行中") dd.confidence = "已完成"; } dialogues.value = [...dialogues.value]; + const st = seats.value.find((x) => x.agentId === args.agentId); + settleAgent(args.agentId, st?.status === "error" ? "error" : "done"); running.value = false; abortRef.value = null; } @@ -266,6 +287,7 @@ export function useParallelRun() { cancel(); pending.clear(); bubbleByAgent.clear(); + settled.clear(); dialogues.value = []; seats.value = []; }
{{ errorMsg }}
+ {{ errorMsg }} +
填写任务名称与简介,点「开始分析」并行调用各智能体。
等待宿主触发「开始 / 重新分析」,并行调用各智能体。
并行运行期间禁止写文件,产出直接呈现在正文。