代码提交
This commit is contained in:
1 parent
ace30af202
commit
64afa9a1ed
5 files changed
+409
-172
No files matched your search
@@ -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
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { ParallelAgentsPanel } from "parallel-agents-vue";
|
||||
import "parallel-agents-vue/style.css";
|
||||
|
||||
@@ -64,15 +75,43 @@ const agents = [
|
||||
{ agent_id: "roundtable-solution-design", name: "方案设计" },
|
||||
{ agent_id: "roundtable-risk-review", name: "风险审查" },
|
||||
];
|
||||
|
||||
// 宿主持有这三项(面板不再渲染输入框)
|
||||
const taskName = ref("");
|
||||
const brief = ref("");
|
||||
const model = ref("");
|
||||
|
||||
// 模板 ref:拿到面板暴露的方法 / 状态
|
||||
const panel = ref<InstanceType<typeof ParallelAgentsPanel> | null>(null);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="height: 80vh">
|
||||
<ParallelAgentsPanel :username="'demo'" :agents="agents" />
|
||||
<div style="display: flex; flex-direction: column; height: 80vh">
|
||||
<!-- 宿主自己的工具条 -->
|
||||
<input v-model="taskName" placeholder="任务名称" />
|
||||
<textarea v-model="brief" placeholder="任务简介" />
|
||||
<button :disabled="!panel?.canStart" @click="panel?.reanalyze()">
|
||||
{{ panel?.hasResults ? "重新分析" : "开始分析" }}
|
||||
</button>
|
||||
<button :disabled="panel?.saving || !panel?.hasResults" @click="panel?.save()">保存</button>
|
||||
|
||||
<!-- 面板:纯展示,需父容器给高度 -->
|
||||
<ParallelAgentsPanel
|
||||
ref="panel"
|
||||
style="flex: 1; min-height: 0"
|
||||
:username="'demo'"
|
||||
:agents="agents"
|
||||
v-model:task-name="taskName"
|
||||
v-model:brief="brief"
|
||||
:model="model"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
> 面板暴露出的 `running` / `saving` / `hasResults` / `canStart` 等是 ref,**在组件实例上会自动解包**,
|
||||
> 模板里直接 `panel?.canStart` 读,**无需 `.value`**。
|
||||
|
||||
### 全局注册(可选)
|
||||
|
||||
```ts
|
||||
@@ -90,8 +129,20 @@ app.use(ParallelAgentsVue); // 之后任意模板可直接用 <ParallelAgentsPan
|
||||
|
||||
```vue
|
||||
<YourDrawer v-model="open" size="large">
|
||||
<div style="height: 86vh">
|
||||
<ParallelAgentsPanel :username="currentUsername" :agents="agents" @saved="onSaved" />
|
||||
<div style="display: flex; flex-direction: column; height: 86vh">
|
||||
<!-- 宿主工具条:任务名/简介/模型 + 按钮,由你自己渲染 -->
|
||||
<MyToolbar v-model:task-name="taskName" v-model:brief="brief"
|
||||
@start="panel?.reanalyze()" @save="panel?.save()" />
|
||||
<ParallelAgentsPanel
|
||||
ref="panel"
|
||||
style="flex: 1; min-height: 0"
|
||||
:username="currentUsername"
|
||||
:agents="agents"
|
||||
v-model:task-name="taskName"
|
||||
v-model:brief="brief"
|
||||
:model="model"
|
||||
@saved="onSaved"
|
||||
/>
|
||||
</div>
|
||||
</YourDrawer>
|
||||
```
|
||||
@@ -122,14 +173,66 @@ app.use(ParallelAgentsVue); // 之后任意模板可直接用 <ParallelAgentsPan
|
||||
| `getToken` | `() => string \| Promise<string>` | | 动态取 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<InstanceType<typeof ParallelAgentsPanel> | null>(null)`,模板 `<ParallelAgentsPanel ref="panel" .../>`。
|
||||
|
||||
**方法**
|
||||
|
||||
| 方法 | 等价于 | 说明 |
|
||||
| --------------------- | ----------------- | -------------------------------------------- |
|
||||
| `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
|
||||
<script setup lang="ts">
|
||||
const panel = ref<InstanceType<typeof ParallelAgentsPanel> | null>(null);
|
||||
// JS 里读 ref 状态要带 .value:
|
||||
function onClickStart() {
|
||||
if (panel.value?.canStart) panel.value.reanalyze();
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<!-- 模板里自动解包,直接读 -->
|
||||
<button :disabled="!panel?.canStart" @click="panel?.reanalyze()">
|
||||
{{ panel?.hasResults ? "重新分析" : "开始分析" }}
|
||||
</button>
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六·一、每个参数怎么传(逐个举例)
|
||||
|
||||
### `agents`(必填)——「智能体参数」怎么传
|
||||
@@ -171,11 +274,24 @@ const agents = [
|
||||
<ParallelAgentsPanel :get-token="() => authStore.getAccessToken()" :agents="agents" />
|
||||
```
|
||||
|
||||
### `model`——指定模型(不传走默认/后端回退;也可在面板右上角下拉切)
|
||||
### `model`——指定模型(宿主持有;不传走默认/后端回退)
|
||||
|
||||
```vue
|
||||
<ParallelAgentsPanel :model="'deepseek-chat'" :username="'demo'" :agents="agents" />
|
||||
<ParallelAgentsPanel :model="model" :username="'demo'" :agents="agents" />
|
||||
```
|
||||
> 模型下拉已移到宿主侧:宿主自己渲染下拉(可调后端 `/api/models`),把选中的模型名用 `:model` 传进来。
|
||||
|
||||
### `taskName` / `brief`——任务名 / 简介(宿主持有,支持 v-model)
|
||||
|
||||
```vue
|
||||
<ParallelAgentsPanel
|
||||
v-model:task-name="taskName"
|
||||
v-model:brief="brief"
|
||||
:username="'demo'"
|
||||
:agents="agents"
|
||||
/>
|
||||
```
|
||||
> 用 `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<InstanceType<typeof ParallelAgentsPanel> | null>(null);
|
||||
|
||||
function onSaved(t: SavedTask) {
|
||||
console.log("已保存", t.id);
|
||||
}
|
||||
@@ -217,13 +338,26 @@ function onCompleted(results: AgentResult[]) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="height: 86vh">
|
||||
<div style="display: flex; flex-direction: column; height: 86vh">
|
||||
<!-- 宿主工具条 -->
|
||||
<input v-model="taskName" placeholder="任务名称" />
|
||||
<textarea v-model="brief" placeholder="任务简介" />
|
||||
<button v-if="!panel?.running" :disabled="!panel?.canStart" @click="panel?.reanalyze()">
|
||||
{{ panel?.hasResults ? "重新分析" : "开始分析" }}
|
||||
</button>
|
||||
<button v-else @click="panel?.cancel()">停止</button>
|
||||
<button :disabled="panel?.saving || !panel?.hasResults" @click="panel?.save()">保存</button>
|
||||
|
||||
<ParallelAgentsPanel
|
||||
ref="panel"
|
||||
style="flex: 1; min-height: 0"
|
||||
:base-url="'http://localhost:8001'"
|
||||
:username="'demo'"
|
||||
:agents="agents"
|
||||
:task-id="taskId"
|
||||
:model="'deepseek-chat'"
|
||||
v-model:task-name="taskName"
|
||||
v-model:brief="brief"
|
||||
:model="model"
|
||||
:dark="true"
|
||||
@saved="onSaved"
|
||||
@update:taskId="(id) => (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
|
||||
<ParallelAgentsPanel
|
||||
@@ -254,9 +391,22 @@ function onCompleted(results: AgentResult[]) {
|
||||
|
||||
## 七·一、如何获取模型返回的内容
|
||||
|
||||
每个智能体(模型)返回的正文是 `AgentResult.content`。两种方式,任选其一:
|
||||
每个智能体(模型)返回的正文是 `AgentResult.content`。组件会**自动把结果推给宿主**,无需轮询:
|
||||
|
||||
**A. 事件**:监听 `@completed`,一轮分析结束时自动回传 `AgentResult[]`:
|
||||
**A·1. 单个完成就推**:监听 `@agent-completed`,**哪个智能体先分析完就先回传哪个**(各触发一次):
|
||||
|
||||
```vue
|
||||
<ParallelAgentsPanel
|
||||
:username="'demo'"
|
||||
:agents="agents"
|
||||
@agent-completed="(r) => {
|
||||
// r: { agent_id, name, content, status } —— 单个智能体
|
||||
console.log('完成一个:', r.name, '→', r.content);
|
||||
}"
|
||||
/>
|
||||
```
|
||||
|
||||
**A·2. 全部完成再推一次**:监听 `@completed`,一轮分析**全部**结束时回传 `AgentResult[]`:
|
||||
|
||||
```vue
|
||||
<ParallelAgentsPanel
|
||||
@@ -269,6 +419,8 @@ function onCompleted(results: AgentResult[]) {
|
||||
/>
|
||||
```
|
||||
|
||||
> 两个事件可同时用:`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<string>(""); // 留空=新任务
|
||||
taskId.value = "task-001"; // 改了之后面板自动回显 task-001
|
||||
```
|
||||
|
||||
> 方式 2:面板**右上角内置「历史」下拉**——点开选一条历史任务即回显,**无需写任何代码**。
|
||||
> 方式 2:**历史下拉已移到宿主侧**(面板不再内置)。宿主读 `panel.history`(`SavedTask[]`)自建下拉,
|
||||
> 选中某条时调 `panel.loadTask(id)` 回显;面板会通过 `update:taskName` / `update:brief` 把存档标题 / 简介
|
||||
> 回传给宿主,配合 `v-model` 同步宿主自己的输入框。需要刷新列表调 `panel.refreshHistory()`。
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- 宿主自建历史下拉 -->
|
||||
<select @change="(e) => panel?.loadTask((e.target as HTMLSelectElement).value)">
|
||||
<option v-for="t in panel?.history || []" :key="t.id" :value="t.id">
|
||||
{{ t.title || "未命名任务" }}
|
||||
</option>
|
||||
</select>
|
||||
</template>
|
||||
```
|
||||
|
||||
要点:先**保存过**、再用**同一个 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`(全部到齐再推一次)。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
/** Dev demo 调试页:填后端地址 / token / 智能体 id,把面板塞进一个弹窗,验证嵌入与流式。 */
|
||||
/**
|
||||
* Dev demo 调试页:填后端地址 / token / 智能体 id,把面板塞进一个弹窗,验证嵌入与流式。
|
||||
*
|
||||
* 同时作为「新契约」的参考实现:任务名 / 简介 / 模型由宿主持有并通过 props 传入,
|
||||
* 「重新分析 / 保存 / 停止」等按钮由宿主渲染,通过模板 ref 调用面板 expose 出的方法。
|
||||
*/
|
||||
import { reactive, ref } from "vue";
|
||||
import ParallelAgentsPanel from "./components/ParallelAgentsPanel.vue";
|
||||
import type { AgentInput, AgentResult } from "./types";
|
||||
@@ -16,13 +21,24 @@ const cfg = reactive({
|
||||
const open = ref(false);
|
||||
const agents = ref<AgentInput[]>([]);
|
||||
|
||||
// 宿主持有:任务名 / 简介 / 模型(面板不再渲染这些输入)。
|
||||
const taskName = ref(localStorage.getItem("pa.taskName") || "");
|
||||
const brief = ref(localStorage.getItem("pa.brief") || "");
|
||||
const model = ref(localStorage.getItem("pa.model") || "");
|
||||
|
||||
// 模板 ref:拿到面板暴露的方法 / 状态。
|
||||
const panel = ref<InstanceType<typeof ParallelAgentsPanel> | null>(null);
|
||||
|
||||
// 面板保存 / 生成 id 时回填,方便复制下来下次填进去测「按 taskId 回显」。
|
||||
function onTaskId(id: string) {
|
||||
cfg.taskId = id;
|
||||
localStorage.setItem("pa.taskId", id);
|
||||
}
|
||||
function onAgentCompleted(result: AgentResult) {
|
||||
console.log("[demo] agent-completed → 某智能体分析完:", result.name, result.status, result.content);
|
||||
}
|
||||
function onCompleted(results: AgentResult[]) {
|
||||
console.log("[demo] completed → 各智能体返回内容:", results);
|
||||
console.log("[demo] completed → 全部分析完,各智能体返回内容:", results);
|
||||
}
|
||||
|
||||
function parseAgents(): AgentInput[] {
|
||||
@@ -68,21 +84,74 @@ function launch() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 模拟宿主弹窗:面板自适应容器 -->
|
||||
<!-- 模拟宿主弹窗:宿主自己渲染「任务名 / 简介 / 模型 + 按钮」,面板只展示会话 -->
|
||||
<div
|
||||
v-if="open"
|
||||
style="position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 50"
|
||||
@click.self="open = false"
|
||||
>
|
||||
<div style="width: 92vw; max-width: 1080px; height: 86vh; border-radius: 12px; overflow: hidden; box-shadow: 0 20px 60px rgba(0,0,0,0.5)">
|
||||
<ParallelAgentsPanel
|
||||
:base-url="cfg.baseUrl"
|
||||
:username="cfg.username || undefined"
|
||||
:task-id="cfg.taskId || undefined"
|
||||
:agents="agents"
|
||||
@update:taskId="onTaskId"
|
||||
@completed="onCompleted"
|
||||
/>
|
||||
<div style="width: 92vw; max-width: 1080px; height: 86vh; border-radius: 12px; overflow: hidden; box-shadow: 0 20px 60px rgba(0,0,0,0.5); display: flex; flex-direction: column; background: #0d1526">
|
||||
<!-- 宿主工具条:这部分原来在面板内,现在由宿主自己掌控 -->
|
||||
<div style="display: flex; flex-direction: column; gap: 8px; padding: 12px 16px; border-bottom: 1px solid #1e2d4d">
|
||||
<input
|
||||
v-model="taskName"
|
||||
placeholder="任务名称"
|
||||
style="padding: 6px 10px; background: #111c33; color: #e5edf7; border: 1px solid #2d4473; border-radius: 6px"
|
||||
/>
|
||||
<textarea
|
||||
v-model="brief"
|
||||
rows="2"
|
||||
placeholder="任务简介:把背景、目标、约束写清楚"
|
||||
style="padding: 6px 10px; background: #111c33; color: #e5edf7; border: 1px solid #2d4473; border-radius: 6px; resize: vertical"
|
||||
/>
|
||||
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 8px">
|
||||
<!-- expose 出的 ref 在组件实例上会自动解包,直接读,无需 .value -->
|
||||
<button
|
||||
v-if="!panel?.running"
|
||||
:disabled="!panel?.canStart"
|
||||
@click="panel?.reanalyze()"
|
||||
style="padding: 6px 14px; background: #1677ff; color: #fff; border: 0; border-radius: 6px; cursor: pointer"
|
||||
>
|
||||
{{ panel?.hasResults ? "重新分析" : "开始分析" }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
@click="panel?.cancel()"
|
||||
style="padding: 6px 14px; background: #2d4473; color: #e5edf7; border: 0; border-radius: 6px; cursor: pointer"
|
||||
>
|
||||
停止
|
||||
</button>
|
||||
<button
|
||||
:disabled="panel?.saving || !panel?.hasResults"
|
||||
@click="panel?.save()"
|
||||
style="padding: 6px 14px; background: #2d4473; color: #e5edf7; border: 0; border-radius: 6px; cursor: pointer"
|
||||
>
|
||||
{{ panel?.saving ? "保存中…" : cfg.taskId ? "更新保存" : "保存" }}
|
||||
</button>
|
||||
<input
|
||||
v-model="model"
|
||||
placeholder="模型(留空=默认)"
|
||||
style="margin-left: auto; width: 200px; padding: 6px 10px; background: #111c33; color: #e5edf7; border: 1px solid #2d4473; border-radius: 6px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 面板:自适应容器,纯展示会话 -->
|
||||
<div style="flex: 1; min-height: 0">
|
||||
<ParallelAgentsPanel
|
||||
ref="panel"
|
||||
:base-url="cfg.baseUrl"
|
||||
:username="cfg.username || undefined"
|
||||
:task-id="cfg.taskId || undefined"
|
||||
v-model:task-name="taskName"
|
||||
v-model:brief="brief"
|
||||
:model="model"
|
||||
:agents="agents"
|
||||
@update:taskId="onTaskId"
|
||||
@agent-completed="onAgentCompleted"
|
||||
@completed="onCompleted"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 并行多智能体面板(主组件,可嵌入)。
|
||||
* 并行多智能体面板(主组件,可嵌入)—— 纯展示 + 暴露方法版本。
|
||||
*
|
||||
* 输入「任务名称 + 简介」,并行调用宿主传入的多个智能体(后端固定禁写文件),消息列表
|
||||
* 照搬圆桌 Step2 并行多席位的深色样式与结构(步骤条 / 并行双列 / 席位状态条),支持保存
|
||||
* 聊天记录 + 回显 + 重新分析。
|
||||
* 「任务名称 / 简介 / 模型」全部由宿主通过 props 传入;本组件不再渲染顶部表单与按钮,
|
||||
* 只展示「席位状态条 + 并行会话」。宿主自行渲染「重新分析 / 保存」等按钮,并通过模板 ref
|
||||
* 调用本组件暴露的 `reanalyze()` / `save()` / `cancel()` 等方法。
|
||||
*
|
||||
* 并行调用宿主传入的多个智能体(后端固定禁写文件),消息列表照搬圆桌 Step2 并行多席位的
|
||||
* 深色样式与结构(步骤条 / 并行双列 / 席位状态条),支持保存聊天记录 + 回显 + 重新分析。
|
||||
*
|
||||
* 自适应容器:根元素 `h-full w-full flex flex-col min-h-0`,宿主直接塞进抽屉 / 弹窗即可。
|
||||
*/
|
||||
import { computed, onMounted, reactive, ref, watch } from "vue";
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import type { AgentInput, AgentResult, Dialogue, SavedTask, SeatView } from "@/types";
|
||||
import { useParallelRun } from "@/composables/useParallelRun";
|
||||
import {
|
||||
createTask,
|
||||
ensureTask,
|
||||
listModels,
|
||||
listTasks,
|
||||
updateTask,
|
||||
type ApiConfig,
|
||||
type ModelItem,
|
||||
} from "@/api";
|
||||
import { DEFAULT_BASE_URL, DEFAULT_MODEL } from "@/config";
|
||||
import { DEFAULT_BASE_URL } from "@/config";
|
||||
import ParallelSeatStrip from "./ParallelSeatStrip.vue";
|
||||
import MessageBubble from "./MessageBubble.vue";
|
||||
|
||||
@@ -37,15 +38,26 @@ const props = withDefaults(
|
||||
loginPassword?: string;
|
||||
agents: AgentInput[];
|
||||
taskId?: string;
|
||||
/** 任务名称:由宿主传入(可配合 update:taskName 做 v-model)。 */
|
||||
taskName?: string;
|
||||
/** 任务简介:由宿主传入(可配合 update:brief 做 v-model)。 */
|
||||
brief?: string;
|
||||
/** 本次并行分析使用的模型;空 = 后端回退默认。 */
|
||||
model?: string;
|
||||
dark?: boolean;
|
||||
}>(),
|
||||
{ dark: true },
|
||||
{ dark: true, taskName: "", brief: "", model: "" },
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "saved", task: SavedTask): void;
|
||||
(e: "update:taskId", id: string): void;
|
||||
/** 回显已保存任务时回传标题,宿主可据此同步自己的输入框。 */
|
||||
(e: "update:taskName", name: string): void;
|
||||
/** 回显已保存任务时回传简介。 */
|
||||
(e: "update:brief", brief: string): void;
|
||||
/** 单个智能体一分析完就回传它的结果(每个席位完成时各触发一次)。 */
|
||||
(e: "agent-completed", result: AgentResult): void;
|
||||
/** 一轮并行分析结束时回传各智能体的返回内容。 */
|
||||
(e: "completed", results: AgentResult[]): void;
|
||||
}>();
|
||||
@@ -58,21 +70,26 @@ const cfg = computed<ApiConfig>(() => ({
|
||||
loginPassword: props.loginPassword,
|
||||
}));
|
||||
|
||||
const run = useParallelRun();
|
||||
// 单个智能体完成即回传它的结果(一分析完就给宿主)。
|
||||
const run = useParallelRun({
|
||||
onAgentSettled: (agentId) => {
|
||||
const r = getResult(agentId);
|
||||
if (r) emit("agent-completed", r);
|
||||
},
|
||||
});
|
||||
const { dialogues, seats, running } = run;
|
||||
|
||||
const form = reactive({ taskName: "", brief: "" });
|
||||
// 任务名 / 简介 / 模型全部由宿主 props 提供(本组件不再持有可编辑表单)。
|
||||
const taskName = computed(() => props.taskName ?? "");
|
||||
const brief = computed(() => props.brief ?? "");
|
||||
const model = computed(() => (props.model || "").trim() || undefined);
|
||||
|
||||
const currentTaskId = ref<string | null>(props.taskId ?? null);
|
||||
const selected = ref<string[]>([]);
|
||||
const saving = ref(false);
|
||||
const history = ref<SavedTask[]>([]);
|
||||
const showHistory = ref(false);
|
||||
const errorMsg = ref<string | null>(null);
|
||||
|
||||
// 模型切换:拉后端模型列表,空字符串 = 用默认(prop/config/后端回退)。
|
||||
const models = ref<ModelItem[]>([]);
|
||||
const selectedModel = ref<string>(props.model ?? DEFAULT_MODEL ?? "");
|
||||
|
||||
const dialogueById = computed(() => {
|
||||
const m = new Map<number, Dialogue>();
|
||||
for (const d of dialogues.value) m.set(d.id, d);
|
||||
@@ -81,7 +98,7 @@ const dialogueById = computed(() => {
|
||||
|
||||
const hasResults = computed(() => dialogues.value.length > 0);
|
||||
const canStart = computed(
|
||||
() => !running.value && props.agents.length > 0 && (form.taskName.trim() || form.brief.trim()),
|
||||
() => !running.value && props.agents.length > 0 && !!(taskName.value.trim() || brief.value.trim()),
|
||||
);
|
||||
|
||||
// 单选:主区每次只显示**一个**席位的消息列表,点头像切换。
|
||||
@@ -137,21 +154,43 @@ function getResults(): AgentResult[] {
|
||||
function getResult(agentId: string): AgentResult | null {
|
||||
return getResults().find((r) => r.agent_id === agentId) ?? null;
|
||||
}
|
||||
// 模板 ref 用:`<ParallelAgentsPanel ref="panelRef" />` → panelRef.value.getResults()
|
||||
defineExpose({ getResults, getResult, start: () => onStart(), cancel: () => run.cancel() });
|
||||
// 模板 ref 用:`<ParallelAgentsPanel ref="panelRef" />`
|
||||
// 宿主自行渲染按钮,调用下面这些方法 / 读取状态:
|
||||
// panelRef.value.reanalyze() // 开始 / 重新分析
|
||||
// panelRef.value.save() // 保存 / 更新保存
|
||||
// panelRef.value.cancel() // 停止
|
||||
// panelRef.value.loadTask(id) // 回显某条历史
|
||||
// panelRef.value.running / saving / hasResults / canStart // 按钮态(ref)
|
||||
defineExpose({
|
||||
getResults,
|
||||
getResult,
|
||||
start: () => onStart(),
|
||||
reanalyze: () => onStart(),
|
||||
save: () => onSave(),
|
||||
cancel: () => run.cancel(),
|
||||
loadTask: (id: string) => loadTask(id),
|
||||
refreshHistory: () => refreshHistory(),
|
||||
history,
|
||||
running,
|
||||
saving,
|
||||
hasResults,
|
||||
canStart,
|
||||
errorMsg,
|
||||
});
|
||||
|
||||
/** 用户在某智能体的协作卡片里提交了回答 → 带上回答重跑该智能体。 */
|
||||
async function onClarify(agentId: string, answer: string) {
|
||||
errorMsg.value = null;
|
||||
const brief = `${form.brief}\n\n【用户补充】${answer}`.trim();
|
||||
const merged = `${brief.value}\n\n【用户补充】${answer}`.trim();
|
||||
try {
|
||||
await run.rerunAgent(cfg.value, {
|
||||
taskName: form.taskName,
|
||||
brief,
|
||||
taskName: taskName.value,
|
||||
brief: merged,
|
||||
agentId,
|
||||
model: selectedModel.value || undefined,
|
||||
model: model.value,
|
||||
});
|
||||
emit("completed", getResults());
|
||||
void autoSave(); // 重跑完也自动存一次
|
||||
} catch (err) {
|
||||
errorMsg.value = err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
@@ -162,26 +201,37 @@ async function onStart() {
|
||||
errorMsg.value = null;
|
||||
try {
|
||||
await run.start(cfg.value, {
|
||||
taskName: form.taskName,
|
||||
brief: form.brief,
|
||||
taskName: taskName.value,
|
||||
brief: brief.value,
|
||||
agents: props.agents,
|
||||
model: selectedModel.value || undefined,
|
||||
model: model.value,
|
||||
});
|
||||
// 一轮结束:回传各智能体返回内容(宿主也可用 ref 调 getResults())。
|
||||
emit("completed", getResults());
|
||||
void autoSave(); // 分析完自动保存一次
|
||||
} catch (err) {
|
||||
errorMsg.value = err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
}
|
||||
|
||||
/** 一轮分析结束后自动保存一次(有结果才存;失败静默,不打断主流程)。 */
|
||||
async function autoSave() {
|
||||
if (!hasResults.value || saving.value) return;
|
||||
try {
|
||||
await onSave();
|
||||
} catch {
|
||||
/* 自动保存失败不打扰用户,错误已在 onSave 内记录 */
|
||||
}
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
if (saving.value || !hasResults.value) return;
|
||||
saving.value = true;
|
||||
errorMsg.value = null;
|
||||
try {
|
||||
const title = form.taskName.trim() || "未命名任务";
|
||||
const title = taskName.value.trim() || "未命名任务";
|
||||
const agentsSnapshot: AgentInput[] = props.agents.map((a) => ({ ...a }));
|
||||
const payload = { title, brief: form.brief, agents: agentsSnapshot, messages: dialogues.value };
|
||||
const payload = { title, brief: brief.value, agents: agentsSnapshot, messages: dialogues.value };
|
||||
// 按宿主传入的 taskId **upsert**:有 id 先更新,不存在(404)再用该 id 创建;无 id 则新建。
|
||||
let saved: SavedTask | null;
|
||||
const id = currentTaskId.value;
|
||||
@@ -206,14 +256,17 @@ async function onSave() {
|
||||
|
||||
async function refreshHistory() {
|
||||
try {
|
||||
history.value = await listTasks(cfg.value);
|
||||
const list = await listTasks(cfg.value);
|
||||
// 按更新时间倒序,最新的在最前(供「进入默认显示最新一条」用)。
|
||||
history.value = [...list].sort((a, b) =>
|
||||
(b.updated_at || b.created_at || "").localeCompare(a.updated_at || a.created_at || ""),
|
||||
);
|
||||
} catch {
|
||||
/* 历史拉取失败不阻断主流程 */
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTask(id: string) {
|
||||
showHistory.value = false;
|
||||
errorMsg.value = null;
|
||||
try {
|
||||
// get-or-create:新 id 直接初始化空记录(不 404);已存在则回显其聊天记录。
|
||||
@@ -224,11 +277,12 @@ async function loadTask(id: string) {
|
||||
currentTaskId.value = task.id;
|
||||
selected.value = [];
|
||||
if (task.messages && task.messages.length) {
|
||||
form.taskName = task.title ?? "";
|
||||
form.brief = task.brief ?? "";
|
||||
// 标题 / 简介由宿主持有:回传让宿主同步自己的输入框(v-model)。
|
||||
emit("update:taskName", task.title ?? "");
|
||||
emit("update:brief", task.brief ?? "");
|
||||
run.hydrate(task.messages);
|
||||
} else {
|
||||
// 空记录(新初始化):清空当前会话,等用户填任务 / 开始分析。
|
||||
// 空记录(新初始化):清空当前会话,等宿主填任务 / 触发分析。
|
||||
run.reset();
|
||||
}
|
||||
emit("update:taskId", task.id);
|
||||
@@ -237,19 +291,14 @@ async function loadTask(id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshModels() {
|
||||
const list = await listModels(cfg.value);
|
||||
models.value = list;
|
||||
// 当前选择不在列表里(且非空)时,回退为默认(空 = 后端回退)。
|
||||
if (selectedModel.value && !list.some((m) => m.name === selectedModel.value)) {
|
||||
selectedModel.value = "";
|
||||
onMounted(async () => {
|
||||
await refreshHistory();
|
||||
if (props.taskId) {
|
||||
void loadTask(props.taskId);
|
||||
} else if (history.value.length) {
|
||||
// 没指定 taskId:默认回显最新一条聊天记录。
|
||||
void loadTask(history.value[0].id);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void refreshHistory();
|
||||
void refreshModels();
|
||||
if (props.taskId) void loadTask(props.taskId);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -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' : ''"
|
||||
>
|
||||
<!-- 顶部:任务名 + 操作 -->
|
||||
<div class="pa-border flex shrink-0 flex-col gap-2 border-b px-4 py-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
v-model="form.taskName"
|
||||
class="pa-input min-w-0 flex-1 px-3 py-1.5 text-sm"
|
||||
placeholder="任务名称"
|
||||
/>
|
||||
<div class="relative shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
class="pa-btn-ghost px-2.5 py-1.5 text-xs"
|
||||
@click="showHistory = !showHistory"
|
||||
>
|
||||
历史 ▾
|
||||
</button>
|
||||
<div
|
||||
v-if="showHistory"
|
||||
class="pa-card pa-border absolute right-0 z-20 mt-1 max-h-72 w-72 overflow-auto rounded-lg border p-1 shadow-lg"
|
||||
>
|
||||
<div v-if="!history.length" class="px-3 py-4 text-center text-xs pa-text-muted">
|
||||
暂无保存的任务
|
||||
</div>
|
||||
<button
|
||||
v-for="t in history"
|
||||
:key="t.id"
|
||||
type="button"
|
||||
class="pa-btn-ghost block w-full truncate px-2.5 py-1.5 text-left text-xs"
|
||||
style="border: 0"
|
||||
@click="loadTask(t.id)"
|
||||
>
|
||||
<span class="font-semibold">{{ t.title || "未命名任务" }}</span>
|
||||
<span class="pa-text-muted"> · {{ (t.updated_at || "").slice(0, 16).replace("T", " ") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="form.brief"
|
||||
rows="2"
|
||||
class="pa-input w-full resize-y px-3 py-1.5 text-sm"
|
||||
placeholder="任务简介:把背景、目标、约束写清楚,会并行下发给各智能体"
|
||||
/>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
v-if="!running"
|
||||
type="button"
|
||||
class="pa-btn-primary px-3.5 py-1.5 text-sm font-semibold"
|
||||
:disabled="!canStart"
|
||||
@click="onStart"
|
||||
>
|
||||
{{ hasResults ? "重新分析" : "开始分析" }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="pa-btn-ghost px-3.5 py-1.5 text-sm font-semibold"
|
||||
@click="run.cancel()"
|
||||
>
|
||||
停止
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pa-btn-ghost px-3.5 py-1.5 text-sm font-semibold"
|
||||
:disabled="saving || !hasResults"
|
||||
@click="onSave"
|
||||
>
|
||||
{{ saving ? "保存中…" : currentTaskId ? "更新保存" : "保存" }}
|
||||
</button>
|
||||
<!-- 顶部表单与按钮已移除:任务名 / 简介 / 模型由宿主 props 传入,
|
||||
「重新分析 / 保存 / 停止」由宿主调用本组件 expose 出的方法。 -->
|
||||
|
||||
<!-- 模型切换:作用于下一次「开始分析 / 重新分析」 -->
|
||||
<label class="ml-auto flex items-center gap-1.5 text-xs pa-text-muted">
|
||||
模型
|
||||
<select
|
||||
v-model="selectedModel"
|
||||
:disabled="running"
|
||||
class="pa-input px-2 py-1 text-xs"
|
||||
title="选择本次并行分析使用的模型"
|
||||
>
|
||||
<option value="">默认</option>
|
||||
<option v-for="m in models" :key="m.name" :value="m.name">
|
||||
{{ m.display_name || m.name }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<span v-if="props.agents.length === 0" class="text-xs pa-text-muted">
|
||||
⚠️ 未传入任何智能体(agents 为空)
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="errorMsg" class="text-xs" style="color: var(--pa-rose)">{{ errorMsg }}</p>
|
||||
</div>
|
||||
<!-- 错误提示(保留,便于宿主无需额外处理也能看到失败原因) -->
|
||||
<p
|
||||
v-if="errorMsg"
|
||||
class="pa-border shrink-0 border-b px-4 py-2 text-xs"
|
||||
style="color: var(--pa-rose)"
|
||||
>
|
||||
{{ errorMsg }}
|
||||
</p>
|
||||
|
||||
<!-- 席位状态条 -->
|
||||
<div v-if="seats.length" class="pa-border shrink-0 border-b px-4">
|
||||
@@ -383,7 +351,7 @@ onMounted(() => {
|
||||
v-else
|
||||
class="flex h-full flex-col items-center justify-center gap-2 text-center"
|
||||
>
|
||||
<p class="text-sm pa-text-muted">填写任务名称与简介,点「开始分析」并行调用各智能体。</p>
|
||||
<p class="text-sm pa-text-muted">等待宿主触发「开始 / 重新分析」,并行调用各智能体。</p>
|
||||
<p class="text-xs pa-text-muted">并行运行期间禁止写文件,产出直接呈现在正文。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<Dialogue[]>([]);
|
||||
const seats = ref<SeatView[]>([]);
|
||||
const running = ref(false);
|
||||
const abortRef = shallowRef<AbortController | null>(null);
|
||||
|
||||
// 本轮已「结算」(回调过 onAgentSettled)的 agentId,去重,避免重复回传。
|
||||
const settled = new Set<string>();
|
||||
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<string, string>(); // 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 = [];
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user