24 KiB
使用说明 · 在其它 Vue 项目中引入
本组件库只对外暴露一个组件 ParallelAgentsPanel(其余 MessageBubble / MessageStepsCard /
ParallelSeatStrip / Avatar / MarkdownContent 都是它的内部子组件,无需也不要单独引入)。
宿主只管把它放进自己的页面 / 抽屉 / 弹窗,并传几个 prop。
⚠️ 契约变更(纯展示版):面板不再自带顶部表单与按钮。
- 任务名称 / 简介 / 模型 全部由宿主通过 props 传入(
taskName/brief支持v-model);- 「重新分析 / 保存 / 停止」按钮由宿主自己渲染,通过模板 ref 调用面板
defineExpose出的reanalyze()/save()/cancel()等方法(见 §六·二);- 面板本体只展示「席位状态条 + 并行会话 + 错误提示」。
完整的宿主参考实现见仓库
src/App.vue(弹窗里宿主自渲染工具条 + 调用 ref 方法)。
一、要引入的东西(就这两样)
- 组件:
ParallelAgentsPanel - 样式:
parallel-agents-vue/style.css(一次性引入即可,样式作用域全在.parallel-agents下)
依赖说明:
vue ^3.4(宿主提供,库不打包 vue);markdown-it已打进库内,无需宿主安装。 无需 Tailwind 或任何 CSS 框架:布局样式已内置为parallel-agents.css末尾的「最小工具类子集」 (全部作用域在.parallel-agents下,不污染宿主)。宿主项目不用装 / 配 Tailwind。
二、两种引入方式
方式 A:构建成库后按包引入(推荐)
- 在本项目构建:
pnpm install pnpm build:lib # 产出 dist/parallel-agents-vue.{js,umd.cjs,css} - 宿主项目里安装(任选其一):
- 把本目录作为本地依赖:
"parallel-agents-vue": "file:../parallel-agents-vue"; - 或发到你们的私有 npm 后正常
pnpm add parallel-agents-vue。
- 把本目录作为本地依赖:
- 代码里引入:
import { ParallelAgentsPanel } from "parallel-agents-vue"; import "parallel-agents-vue/style.css";
方式 B:直接拷贝源码(无需发包,推荐给需要改源码的人)
所有实现已收进一个自包含文件夹 src/parallel-agents/。把整个文件夹拷进宿主项目的 src/
下即可,内部全是相对路径引用、不依赖任何构建别名:
src/parallel-agents/ ← 整个文件夹拷走即用
index.ts 入口:自动引样式 + 导出组件/类型
ParallelAgentsPanel.vue 主组件
ParallelSeatStrip.vue · MessageBubble.vue · MessageStepsCard.vue · StepIcon.vue
ClarificationCard.vue · MarkdownContent.vue · Avatar.vue (子组件)
useParallelRun.ts 并行运行编排
api.ts · types.ts · config.ts · avatar-constants.ts · step-display.ts
parallel-agents.css 作用域样式(深色 token + 语义类 + 工具类子集,无 Tailwind)
README.md 本文件夹的速用说明
引入(样式由 index.ts 自动引入,无需手动 import css):
import { ParallelAgentsPanel } from "@/parallel-agents";
// 或相对路径: import { ParallelAgentsPanel } from "./parallel-agents";
宿主需有 markdown-it 依赖(pnpm add markdown-it),并保证构建支持 .vue(Vite/@vitejs/plugin-vue)。
不需要 Tailwind。
三、注册与使用
局部引入(推荐)
宿主持有任务名 / 简介 / 模型,并自渲染按钮调用面板暴露的方法:
<script setup lang="ts">
import { ref } from "vue";
import { ParallelAgentsPanel } from "parallel-agents-vue";
import "parallel-agents-vue/style.css";
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="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。
全局注册(可选)
import ParallelAgentsVue from "parallel-agents-vue";
import "parallel-agents-vue/style.css";
app.use(ParallelAgentsVue); // 之后任意模板可直接用 <ParallelAgentsPanel/>
四、放进抽屉 / 弹窗
组件根元素是 h-full w-full flex flex-col min-h-0,会充满父容器。所以父容器要给一个高度
(弹窗/抽屉本身有高度即可):
<YourDrawer v-model="open" size="large">
<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>
五、接口地址与登录
- 接口地址:默认读
src/config.ts的DEFAULT_BASE_URL(可改源码,或 dev 设VITE_PA_BASE_URL); 也可用:base-urlprop 临时覆盖。 - 登录:把宿主的用户名用
:username传进来即可——组件自动走本面板专用的「绕过常规登录」 公共端点POST /api/parallel-agents/auth/token换 token、缓存、401 自动重登。宿主不用管 token。 若已有 token,可改传:token/:get-token(优先级更高)。- 为什么专用端点:内网部署对标准
/api/v1/auth/login/username有额外限制(口令门/网关裁剪等),拿不到 token;该端点传用户名直接签发(账号不存在自动注册),已在 auth/csrf 中间件白名单里放行。
- 为什么专用端点:内网部署对标准
- token 缓存按
baseUrl::username维度,多个实例/多用户互不串。
六、Props
| prop | 类型 | 必填 | 说明 |
|---|---|---|---|
agents |
{ agent_id, name, avatarType? }[] |
✔ | 要并行调用的智能体(agent_id 必须是真实 id) |
username |
string |
免密自动登录用户名(推荐方式) | |
baseUrl |
string |
后端地址;不传用 config.ts 默认 |
|
token |
string |
已有 Bearer token(优先于 username) | |
getToken |
() => string | Promise<string> |
动态取 token | |
loginPassword |
string |
仅当后端开启口令门时需要 | |
taskId |
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 |
最近一次错误(面板内也会展示一条) |
<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(必填)——「智能体参数」怎么传
一个数组,每项 { agent_id, name?, avatarType? }:
const agents = [
{ agent_id: "roundtable-solution-design", name: "方案设计" }, // 最常用:id + 显示名
{ agent_id: "roundtable-risk-review", name: "风险审查", avatarType: "amber" }, // 顺带指定头像配色
{ agent_id: "demo-expert" }, // 只给 id(显示名回退成 id)
];
<ParallelAgentsPanel :agents="agents" :username="'demo'" />
agent_id必须是后端真实注册的 id(只写名字无效);可用 id 见 06-可用智能体清单.md。name:席位/气泡显示名,可省(省了用 id)。avatarType:可选blue|purple|emerald|amber|pink|human;不传按顺序自动轮转配色。
username——免密登录(推荐)
<ParallelAgentsPanel :username="currentUser.name" :agents="agents" />
baseUrl——后端地址(不传用 config 默认)
<ParallelAgentsPanel :base-url="'http://10.0.0.5:8001'" :username="'demo'" :agents="agents" />
token / getToken——已有登录态时(优先于 username)
<!-- 直接给字符串 token -->
<ParallelAgentsPanel :token="hostToken" :agents="agents" />
<!-- 或给一个(异步)取 token 的函数 -->
<ParallelAgentsPanel :get-token="() => authStore.getAccessToken()" :agents="agents" />
model——指定模型(宿主持有;不传走默认/后端回退)
<ParallelAgentsPanel :model="model" :username="'demo'" :agents="agents" />
模型下拉已移到宿主侧:宿主自己渲染下拉(可调后端
/api/models),把选中的模型名用:model传进来。
taskName / brief——任务名 / 简介(宿主持有,支持 v-model)
<ParallelAgentsPanel
v-model:task-name="taskName"
v-model:brief="brief"
:username="'demo'"
:agents="agents"
/>
用
v-model是为了回显:加载已保存任务时面板会把存档标题 / 简介回传给宿主。
dark——深色(默认 true,自带成色)
<ParallelAgentsPanel :dark="true" :username="'demo'" :agents="agents" />
taskId——存储主键 + 回显(见 §七·二)
<ParallelAgentsPanel
:task-id="taskId"
:username="'demo'"
:agents="agents"
@update:taskId="(id) => (taskId = id)"
/>
全量示例(所有参数一起)
<script setup lang="ts">
import { ref } from "vue";
import { ParallelAgentsPanel } from "parallel-agents-vue";
import "parallel-agents-vue/style.css";
import type { AgentInput, AgentResult, SavedTask } from "parallel-agents-vue";
const agents: AgentInput[] = [
{ agent_id: "roundtable-solution-design", name: "方案设计" },
{ 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);
}
function onCompleted(results: AgentResult[]) {
console.log("各智能体返回内容", results);
}
</script>
<template>
<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"
v-model:task-name="taskName"
v-model:brief="brief"
:model="model"
:dark="true"
@saved="onSaved"
@update:taskId="(id) => (taskId = id)"
@completed="onCompleted"
/>
</div>
</template>
七、事件
| 事件 | 载荷 | 触发时机 |
|---|---|---|
saved |
SavedTask |
保存成功(新建或更新)后;含自动保存 |
update:taskId |
string |
当前任务 id 变化(保存 / 加载历史时) |
update:taskName |
string |
加载已保存任务回显标题(配合 v-model:task-name) |
update:brief |
string |
加载已保存任务回显简介(配合 v-model:brief) |
agent-completed |
AgentResult |
每个智能体一分析完就回传它单个的结果(各席位各触发一次) |
completed |
AgentResult[] |
一轮并行分析全部结束时,回传所有智能体返回内容 |
<ParallelAgentsPanel
:username="user"
:agents="agents"
:task-id="taskId"
@saved="(t) => console.log('saved', t.id)"
@update:taskId="(id) => (taskId = id)"
@completed="(results) => console.log('各智能体返回:', results)"
/>
七·一、如何获取模型返回的内容
每个智能体(模型)返回的正文是 AgentResult.content。组件会自动把结果推给宿主,无需轮询:
A·1. 单个完成就推:监听 @agent-completed,哪个智能体先分析完就先回传哪个(各触发一次):
<ParallelAgentsPanel
:username="'demo'"
:agents="agents"
@agent-completed="(r) => {
// r: { agent_id, name, content, status } —— 单个智能体
console.log('完成一个:', r.name, '→', r.content);
}"
/>
A·2. 全部完成再推一次:监听 @completed,一轮分析全部结束时回传 AgentResult[]:
<ParallelAgentsPanel
:username="'demo'"
:agents="agents"
@completed="(results) => {
// results: [{ agent_id, name, content, status }]
for (const r of results) console.log(r.name, '→', r.content);
}"
/>
两个事件可同时用:
agent-completed做「来一个渲染一个」的增量更新,completed做「全部到齐」的汇总。
B. 方法(模板 ref):随时主动取(不必等事件):
<script setup lang="ts">
import { ref } from "vue";
import { ParallelAgentsPanel } from "parallel-agents-vue";
import type { AgentResult } from "parallel-agents-vue";
const panelRef = ref<InstanceType<typeof ParallelAgentsPanel> | null>(null);
function readAll() {
const all: AgentResult[] = panelRef.value!.getResults();
// [{ agent_id, name, content, status }]
}
function readOne(id: string) {
const one = panelRef.value!.getResult(id); // AgentResult | null
}
</script>
<template>
<ParallelAgentsPanel ref="panelRef" :username="user" :agents="agents" />
</template>
AgentResult = { agent_id, name, content, status },content 即该智能体模型返回的正文(Markdown)。
status 取值:pending | running | done | clarify | error。clarify = 该智能体在等用户协助
(待确认),不是已完成——宿主判断「是否全部完成」时应排除 clarify(例如
results.every(r => r.status === "done")),不要把待协助当成 done。
defineExpose 还暴露了 reanalyze() / start() / save() / cancel() / loadTask(id) 等方法及
running / saving / hasResults / canStart / history 等状态,完整清单见 §六·二。
七·二、如何保存 + 回显聊天记录
保存:宿主调用 panel.save() 即把当前整段聊天记录(各智能体气泡 + 步骤条等)存到后端,按用户隔离。
传 :task-id 时以它为存储主键做 upsert(已存在→更新;不存在→用该 id 新建);不传则首次保存由后端
生成 id(通过 @update:taskId 回传,下次再保存即更新这条)。
自动保存:每轮分析(含单席位重跑)结束后会自动保存一次,宿主无需手动点保存也不丢记录; 自动保存同样走 upsert,并照常触发
saved/update:taskId事件(首轮会回传新生成的 id)。panel.save()仍保留,供宿主想随时手动存档时调用。
进入默认显示最新:组件挂载时若没传
taskId,会自动加载并回显该用户最新的一条聊天记录 (历史按更新时间倒序,取第一条);传了taskId则优先回显指定那条。
回显(两种方式):
<!-- 方式 1:挂载时传一个已存在的 taskId → 自动加载并回显该任务的聊天记录 -->
<ParallelAgentsPanel :task-id="'task-001'" :username="'demo'" :agents="agents" />
// 方式 1 变体:运行中改 taskId(双向绑定),面板会自动切到并回显那条任务
const taskId = ref<string>(""); // 留空=新任务
// <ParallelAgentsPanel :task-id="taskId" @update:taskId="id => taskId = id" ... />
taskId.value = "task-001"; // 改了之后面板自动回显 task-001
方式 2:历史下拉已移到宿主侧(面板不再内置)。宿主读
panel.history(SavedTask[])自建下拉, 选中某条时调panel.loadTask(id)回显;面板会通过update:taskName/update:brief把存档标题 / 简介 回传给宿主,配合v-model同步宿主自己的输入框。需要刷新列表调panel.refreshHistory()。
<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,属正常,面板静默处理)。
八、交互要点(与圆桌 Step2 一致)
- 宿主备好「任务名称 + 简介(+ 模型)」props → 调
panel.reanalyze()→ 各智能体并行输出。 运行期间禁用写文件(write_file/str_replace)与派活/脚本(agent_orchestration/bash, 防子智能体模仿总控调度);检索、查看、ask_clarification等照常 —— 所以步骤条 / 协作卡片正常 显示,每个智能体只做自己那份分析。 - 主区每次只显示一个席位的消息列表;点席位头像切换要看哪个(单选)。
- 顶部席位状态条显示每个智能体的进行中 / 完成 / 待协助 / 出错;待协助数量会单独标「N 待协助」。
- 用户协助(澄清):智能体要澄清时该席位变「待协助」(不是完成),气泡里弹协作卡片。多个智能体 同时待协助时可分别点协助、互不阻塞(协助 A 不会禁用 B/C 的卡片);提交回答后该席位自动带上 补充重跑。
- 任务名 / 简介 / 模型 / 按钮 / 历史下拉全部在宿主侧:模型空 =
modelprop / 配置 / 后端回退; 模型作用于下一次reanalyze()。 panel.save()存档;panel.loadTask(id)回显;panel.reanalyze()清空重跑;panel.cancel()停止。- 每轮分析结束自动保存一次;挂载时没传
taskId会默认回显最新一条记录。 - 结果自动推给宿主:
@agent-completed(来一个推一个)+@completed(全部到齐再推一次)。
九、注意事项
- 不会污染宿主全局样式:不依赖 Tailwind,布局类已内置为作用域工具类子集,所有规则都在
.parallel-agents下,绝不外泄到宿主页面。 - 深色自带成色:面板始终深蓝深色,不随宿主明暗切换(与圆桌 Step2 观感一致)。
- 跨域:若宿主与后端不同源,后端需允许该来源的 CORS + 带
credentials;登录端点公共且 CSRF 豁免, 业务请求用 Bearer 头已绕过 CSRF。 - agent_id 必须真实:见 06-可用智能体清单.md 获取可用 id 与自查方法。