diff --git a/offline-backend-20260512/backend/app/gateway/routers/parallel_agent_tasks.py b/offline-backend-20260512/backend/app/gateway/routers/parallel_agent_tasks.py
index e8c41080..e8c529a6 100644
--- a/offline-backend-20260512/backend/app/gateway/routers/parallel_agent_tasks.py
+++ b/offline-backend-20260512/backend/app/gateway/routers/parallel_agent_tasks.py
@@ -70,6 +70,14 @@ class TaskUpdateRequest(BaseModel):
messages: list[dict[str, Any]] | None = None
+class TaskEnsureRequest(BaseModel):
+ """get-or-create:宿主传的 taskId 不固定,新 id 直接**初始化**一条空记录返回(不 404)。"""
+ id: str = Field(min_length=1, max_length=64)
+ title: str = Field(default="", max_length=512)
+ brief: str | None = None
+ agents: list[TaskAgentSchema] = []
+
+
# ── helpers ──────────────────────────────────────────────────────────────
@@ -115,6 +123,30 @@ async def create_task(request: Request, body: TaskCreateRequest) -> TaskResponse
return TaskResponse(**row)
+@router.post("/ensure", response_model=TaskResponse)
+async def ensure_task(request: Request, body: TaskEnsureRequest) -> TaskResponse:
+ """存在则返回(含 messages 供回显);不存在则用该 id **初始化**一条空记录返回。
+
+ 宿主传的 taskId 不固定:新 id 不再 404,而是即时建好、可直接回显/保存。幂等。
+ """
+ store = _get_store(request)
+ user_id = _current_user_id(request)
+ existing = await store.get_task(body.id, user_id)
+ if existing is not None:
+ return TaskResponse(**existing)
+ row = await store.create_task(
+ user_id,
+ {
+ "id": body.id,
+ "title": body.title.strip(),
+ "brief": body.brief,
+ "agents": [a.model_dump() for a in body.agents],
+ "messages": [],
+ },
+ )
+ return TaskResponse(**row)
+
+
@router.get("/{task_id}", response_model=TaskResponse)
async def get_task(request: Request, task_id: str) -> TaskResponse:
store = _get_store(request)
diff --git a/offline-backend-20260512/backend/config.yaml b/offline-backend-20260512/backend/config.yaml
index 152ebfb2..d95031b0 100644
--- a/offline-backend-20260512/backend/config.yaml
+++ b/offline-backend-20260512/backend/config.yaml
@@ -14,7 +14,7 @@ models:
display_name: DeepSeek Chat
use: langchain_openai:ChatOpenAI
model: deepseek-chat
- api_key: sk-e1c22b7931dc446f8b67e57878edd105
+ api_key: sk-3fee562f834a4c08bb0d471ef44cb445
base_url: https://api.deepseek.com
request_timeout: 600.0
max_retries: 2
@@ -27,7 +27,7 @@ models:
display_name: DeepSeek V4 Flash
use: langchain_openai:ChatOpenAI
model: deepseek-v4-flash
- api_key: sk-e1c22b7931dc446f8b67e57878edd105
+ api_key: sk-3fee562f834a4c08bb0d471ef44cb445
base_url: https://api.deepseek.com
request_timeout: 600.0
max_retries: 2
diff --git a/parallel-agents-vue-offline-pack/.npmrc b/parallel-agents-vue-offline-pack/.npmrc
new file mode 100644
index 00000000..fc5528ab
--- /dev/null
+++ b/parallel-agents-vue-offline-pack/.npmrc
@@ -0,0 +1,2 @@
+node-linker=hoisted
+package-import-method=copy
diff --git a/parallel-agents-vue-offline-pack/OFFLINE-README.md b/parallel-agents-vue-offline-pack/OFFLINE-README.md
new file mode 100644
index 00000000..9f1b289d
--- /dev/null
+++ b/parallel-agents-vue-offline-pack/OFFLINE-README.md
@@ -0,0 +1,34 @@
+# 内网离线开发包说明
+
+本压缩包已包含 **完整 hoisted 模式 node_modules**(物理文件复制,无 pnpm 符号链接),解压后可直接开发,无需联网安装依赖。
+
+## 环境要求
+
+- Node.js 18+(建议 20 LTS)
+- 全局安装 pnpm:`npm install -g pnpm`(内网可离线安装 pnpm 的 tgz 包)
+
+## 使用步骤
+
+1. 解压到任意目录(路径尽量短,避免 Windows 路径过长问题)
+2. 进入项目目录
+3. 直接运行:
+
+```bash
+pnpm dev # 开发调试 http://localhost:5183
+pnpm build:lib # 构建组件库
+pnpm typecheck # 类型检查
+```
+
+## 若 node_modules 损坏,可离线重装
+
+本包已附带 `.npmrc`(hoisted + copy 模式)。若需重建依赖:
+
+1. 删除 `node_modules`
+2. 在有 pnpm 本地 store 的机器上执行:`pnpm install --offline --frozen-lockfile`
+
+> 若内网机器从未装过依赖,请直接使用本包自带的 node_modules,不要删除。
+
+## 与直接压缩 node_modules 的区别
+
+pnpm 默认 node_modules 含大量符号链接/硬链接,WinRAR 等工具压缩后解压会断链导致缺依赖。
+本包使用 `node-linker=hoisted` + `package-import-method=copy` 重新安装,所有文件均为真实拷贝。
diff --git a/parallel-agents-vue-offline-pack/README.md b/parallel-agents-vue-offline-pack/README.md
new file mode 100644
index 00000000..c4602f35
--- /dev/null
+++ b/parallel-agents-vue-offline-pack/README.md
@@ -0,0 +1,79 @@
+# parallel-agents-vue
+
+并行多智能体面板 —— Vue3 可嵌入组件。输入「任务名称 + 简介」,**并行调用多个智能体**
+(智能体清单由宿主传入,运行期间后端固定**禁止写文件**),消息列表照搬 DeerFlow 圆桌
+研讨第二步并行多席位的**深色样式与完整结构**(步骤条 / 并行双列 / 席位状态条),支持
+**保存聊天记录 + 回显**与**重新分析**。
+
+## 开发调试
+
+```bash
+pnpm install
+pnpm dev # http://localhost:5183 ,填后端 baseUrl / token / 智能体 id 即可联调
+```
+
+后端需要一个跑着的 DeerFlow 网关(默认 `http://localhost:8001`),它提供:
+
+- `POST /api/parallel-agents/run/stream` —— 并行扇出,合并 SSE(事件带 `agent_id`);
+- `/api/parallel-agent-tasks` —— 任务保存 / 回显 CRUD。
+
+## 构建为组件库
+
+```bash
+pnpm build:lib # 产出 dist/parallel-agents-vue.{js,umd.cjs} + dist/parallel-agents-vue.css
+```
+
+## 嵌入其它 Vue 项目
+
+```ts
+import { ParallelAgentsPanel } from "parallel-agents-vue";
+import "parallel-agents-vue/style.css";
+```
+
+```vue
+
+
+
+
+```
+
+### 接口地址 & 登录
+
+- **接口地址**写在配置文件 `src/config.ts` 的 `DEFAULT_BASE_URL`(dev 可用 `VITE_PA_BASE_URL` 覆盖);
+ 也可用 `:base-url` prop 覆盖。
+- **登录**:把宿主侧的**用户名**通过 `:username` 传进来即可,组件会自动走 DeerFlow 的
+ **免密「用户名登录」**端点(`POST /api/v1/auth/login/username`,公共 + CSRF 豁免)换取 token
+ 并缓存、自动续期(401 重登)。**不需要宿主自己管 token**。
+ - 若你已有 token,也可直接传 `:token` / `:get-token`(优先级高于 username)。
+ - 若后端开了口令门(`auth_login.username_login.require_password=true`),再传 `:login-password`。
+
+### Props
+
+| prop | 类型 | 说明 |
+| --------------- | -------------------------------------- | ----------------------------------------------------- |
+| `agents` | `{agent_id,name,avatarType?}[]`(必填)| 要并行调用的智能体 |
+| `username` | `string` | 免密自动登录用户名(推荐方式,宿主自己传) |
+| `baseUrl` | `string` | 后端地址;不传用 `config.ts` 默认 |
+| `token` | `string` | 已有 Bearer token(与 `username` 二选一,优先) |
+| `getToken` | `() => string \| Promise` | 动态取 token |
+| `loginPassword` | `string` | 仅当后端开启口令门时需要 |
+| `taskId` | `string` | 传入则挂载时自动加载并回显该已保存任务 |
+| `model` | `string` | 可选模型名(不传用 `config.ts` 默认 / 后端回退) |
+| `dark` | `boolean`(默认 `true`) | 深色面板(自带成色,不依赖宿主主题) |
+
+### 事件
+
+- `@saved="(task) => ..."` —— 保存成功(含新建/更新)后回传 `SavedTask`。
+- `@update:taskId="(id) => ..."` —— 当前任务 id 变化(保存 / 加载历史时)。
+
+> 样式作用域全部包在根类 `.parallel-agents` 下,且**不引入 Tailwind preflight**,不会污染宿主全局样式。
diff --git a/parallel-agents-vue-offline-pack/docs/00-测试说明.md b/parallel-agents-vue-offline-pack/docs/00-测试说明.md
new file mode 100644
index 00000000..426c5ef1
--- /dev/null
+++ b/parallel-agents-vue-offline-pack/docs/00-测试说明.md
@@ -0,0 +1,48 @@
+# 并行多智能体面板 · 测试用例总览
+
+本目录是 `parallel-agents-vue` 的**手工测试用例**(Markdown)。覆盖:并行调用与流式、文件写入禁用、
+保存与回显、重新分析、免密登录与鉴权、嵌入与深色样式。
+
+## 用例清单
+
+| 文件 | 主题 |
+| -------------------------------------------- | ------------------------------------- |
+| [01-并行调用与流式.md](./01-并行调用与流式.md) | 并行扇出、流式输出、步骤条、席位状态条、禁写文件 |
+| [02-保存与回显.md](./02-保存与回显.md) | 保存聊天记录、历史列表、加载回显、更新保存 |
+| [03-重新分析.md](./03-重新分析.md) | 重新分析(覆盖重跑)、停止、空输入校验 |
+| [04-免密登录与鉴权.md](./04-免密登录与鉴权.md) | username 免密登录、token 优先级、401 重登、口令门 |
+| [05-嵌入与深色样式.md](./05-嵌入与深色样式.md) | 配置文件接口地址、抽屉/弹窗嵌入、深色自适应、不污染宿主 |
+| [06-可用智能体清单.md](./06-可用智能体清单.md) | **测试用真实 agent_id 清单(可直接复制粘贴)** + 示例任务 |
+| [07-使用说明.md](./07-使用说明.md) | **在其它 Vue 项目中引入哪些组件、如何使用**(props/事件/嵌入) |
+| [08-开发文档.md](./08-开发文档.md) | **二次开发**:架构/数据流/事件协议/目录职责/后端契约/扩展指南 |
+
+## 测试环境
+
+| 组件 | 地址 / 命令 |
+| --------------- | ------------------------------------------------------ |
+| DeerFlow 后端 | `http://localhost:8001`(`make dev`,需含本功能的新路由)|
+| Vue dev demo | `http://localhost:5183`(`pnpm dev`) |
+| 接口地址配置 | `src/config.ts` 的 `DEFAULT_BASE_URL` |
+
+## 前置准备(所有用例通用)
+
+1. 后端已启动,且包含本功能:
+ - `POST /api/parallel-agents/run/stream`(并行扇出,合并 SSE)
+ - `GET/POST/PUT/DELETE /api/parallel-agent-tasks`(任务保存/回显)
+ - `POST /api/v1/auth/login/username`(免密登录,公共)
+ - 数据表 `parallel_agent_tasks` 已建(`alembic upgrade head` 或 `create_all`)。
+2. 至少准备 **2 个真实存在的 agent_id**(必须是后端注册过的真实 id,不能只写名字)。
+ 本环境可直接用:`roundtable-solution-design`(方案设计)、`roundtable-risk-review`(风险审查)等
+ —— 完整可复制清单见 [06-可用智能体清单.md](./06-可用智能体清单.md)。
+3. 打开 dev demo `http://localhost:5183`,填:
+ - 后端 baseUrl:`http://localhost:8001`
+ - 用户名:`demo`(免密自动登录)
+ - 智能体:每行 `agent_id, 显示名`
+ 点「打开面板(弹窗内)」。
+
+## 用例字段约定
+
+- **用例ID**:`PA-<模块>-<序号>`
+- **优先级**:P0(核心,必过)/ P1(重要)/ P2(边界)
+- **前置条件 → 操作步骤 → 预期结果**:逐条核对,全部命中才算通过。
+- **结果记录**:建议在「实际结果 / 通过?」列里回填。
diff --git a/parallel-agents-vue-offline-pack/docs/01-并行调用与流式.md b/parallel-agents-vue-offline-pack/docs/01-并行调用与流式.md
new file mode 100644
index 00000000..5a736d00
--- /dev/null
+++ b/parallel-agents-vue-offline-pack/docs/01-并行调用与流式.md
@@ -0,0 +1,105 @@
+# 测试用例 · 并行调用与流式
+
+覆盖:并行扇出多个智能体、流式逐字输出、步骤条(工具调用)、点头像切换显示(单选)、席位状态条、
+运行期间**禁止写文件**。
+
+---
+
+## PA-RUN-01 · 并行调用多个智能体(P0)
+
+**前置条件**
+- 面板已打开,填入任务名「竞品调研」、简介「对比 A、B 两款产品的核心功能与定价」。
+- 传入 2 个智能体(情报收集、风险审查)。
+
+**操作步骤**
+1. 点击「开始分析」。
+
+**预期结果**
+- 顶部出现**席位状态条**,显示「并行 2 席」,两个席位头像角标先后转为「进行中」(转圈)。
+- 两个智能体**同时**开始执行(并行,而非一个跑完再跑下一个)——可在状态条上看到都在「进行中」。
+- 会话区每次显示**一个**席位的消息列表(默认第 1 个),点头像可切换查看另一个(见 PA-RUN-06)。
+- 气泡头部显示:头像、智能体名、状态徽标「并行执行中」、时间。
+
+---
+
+## PA-RUN-02 · 流式逐字输出 + 光标(P0)
+
+**前置条件**:PA-RUN-01 正在运行中。
+
+**操作步骤**
+1. 观察当前显示席位的气泡正文(点头像可切换看其它席位)。
+
+**预期结果**
+- 正文**逐步增量**出现(流式),不是一次性整段刷出。
+- 正在输出的气泡末尾有**闪烁光标**(`.pa-cursor`)。
+- 内容还没来时,显示 loading 占位(转圈 + 跳动圆点 + 「并行执行中」文案)。
+- Markdown 能正确渲染(标题/列表/代码块/表格按 `.pa-md` 样式)。
+
+---
+
+## PA-RUN-03 · 步骤条(思维链 / 工具调用)(P1)
+
+**前置条件**:智能体会调用工具(如检索类技能)。
+
+**操作步骤**
+1. 运行一轮,观察气泡顶部的「执行步骤」折叠卡。
+
+**预期结果**
+- 出现「执行步骤(N)」折叠卡(`MessageStepsCard`),点击可展开/收起。
+- 每条步骤显示:工具标签(如「调用工具 xxx」)、参数摘要(如有 `path` 优先显示)、时间。
+- 同一工具调用**不重复**出条目(按 key 去重)。
+- 若智能体本轮不调工具,则不出现步骤卡(属正常)。
+
+---
+
+## PA-RUN-04 · 席位状态流转(P1)
+
+**操作步骤**
+1. 运行一轮,全程观察席位状态条各头像角标。
+
+**预期结果**
+- 角标状态依次:灰(pending)→ 蓝色转圈(running)→ 绿色对勾(done)。
+- 某智能体失败时其角标变**红色叉**(error),且对应气泡显示「该智能体执行出错:…」、状态徽标「出错」。
+- 单个智能体失败**不影响**其它智能体继续完成(互不拖累)。
+- 全部结束后,运行按钮从「停止」恢复为「重新分析」。
+
+---
+
+## PA-RUN-05 · 运行期间禁止写文件(P0)
+
+**目的**:核对后端 `seat_parallel` 策略生效——并行席位被禁用 `write_file` / `str_replace` / `bash` 等。
+
+**操作步骤**
+1. 简介里**故意要求写文件**,例如:「请把结果保存成一个 .md 文件」。
+2. 点击「开始分析」,等待完成。
+
+**预期结果**
+- 智能体**不会**真正落地文件,产出**直接呈现在气泡正文**里。
+- 步骤条里**不出现** `write_file` / `bash` / `str_replace` 工具调用(被策略排除)。
+- 可旁证:后端日志中该席位 `role=seat_parallel`。
+
+---
+
+## PA-RUN-06 · 点头像切换显示(单选)(P1)
+
+**前置条件**:传入 ≥2 个智能体并运行。
+
+**操作步骤**
+1. 点击席位状态条上的某个头像。
+
+**预期结果**
+- 主区**切换为只显示**该智能体的消息列表(**单选**,每次显示一个)。
+- 被选中的头像高亮(外发光描边);再点别的头像即切到那个。
+- 切换不影响后台并行执行:未显示的智能体仍在跑/已完成,其增量在切回时能看到。
+- 默认显示第 1 个席位。
+
+---
+
+## PA-RUN-07 · 后端不可达 / 智能体 id 非法(P2)
+
+**操作步骤**
+1. 把 baseUrl 改成一个打不通的地址,或传入不存在的 agent_id,点「开始分析」。
+
+**预期结果**
+- 顶部出现红色错误提示(请求失败信息),不白屏、不卡死。
+- 非法 agent_id 时,对应席位以 error 角标 + 出错气泡呈现,其它正常席位不受影响。
diff --git a/parallel-agents-vue-offline-pack/docs/02-保存与回显.md b/parallel-agents-vue-offline-pack/docs/02-保存与回显.md
new file mode 100644
index 00000000..cb3298fa
--- /dev/null
+++ b/parallel-agents-vue-offline-pack/docs/02-保存与回显.md
@@ -0,0 +1,94 @@
+# 测试用例 · 保存与回显
+
+覆盖:保存聊天记录(新建/更新)、历史列表、加载已存任务并回显、`taskId` 自动加载、跨刷新持久化。
+
+---
+
+## PA-SAVE-01 · 首次保存(新建)(P0)
+
+**前置条件**:已跑完一轮、会话区有结果。任务名「竞品调研」。
+
+**操作步骤**
+1. 点击「保存」。
+
+**预期结果**
+- 按钮短暂显示「保存中…」,成功后变为「更新保存」(说明已绑定 taskId)。
+- 触发 `@saved` 事件,回传 `SavedTask`(含 id、title、agents、messages)。
+- 「历史」下拉里出现这条记录(标题 + 更新时间)。
+- 后端 `parallel_agent_tasks` 表新增一行(owner = 当前登录用户)。
+
+---
+
+## PA-SAVE-02 · 再次保存(更新覆盖)(P0)
+
+**前置条件**:PA-SAVE-01 已保存、当前仍是同一任务。
+
+**操作步骤**
+1. 修改任务名 / 简介,或重新分析得到新结果。
+2. 点击「更新保存」。
+
+**预期结果**
+- **不新增**记录,而是更新原 taskId 那条(title / brief / agents / messages 被覆盖为最新)。
+- 历史列表里该条的更新时间刷新、排到最前。
+
+---
+
+## PA-SAVE-03 · 历史加载并回显(P0)
+
+**前置条件**:历史里至少有 1 条已保存任务。
+
+**操作步骤**
+1. 点击「历史 ▾」展开列表。
+2. 选择一条历史任务。
+
+**预期结果**
+- 任务名、简介**回填**为该任务保存时的值。
+- 会话区**完整回显**保存时的消息列表(各智能体气泡、正文、步骤条),且**均为非流式终态**(无光标、无 loading)。
+- 席位状态条据消息重建:正常席位显示 done(绿勾),出错的显示 error(红叉)。
+- 当前 taskId 切换为该任务(再点「保存」是「更新保存」该条)。
+
+---
+
+## PA-SAVE-04 · taskId 进入即自动回显(P1)
+
+**操作步骤**
+1. 以 `:task-id="<已存在任务id>"` 方式挂载面板(嵌入场景)。
+
+**预期结果**
+- 面板挂载后**自动加载**该任务并回显(等同于 PA-SAVE-03 的结果),无需手动点历史。
+- `@update:taskId` 随之触发。
+
+---
+
+## PA-SAVE-05 · 跨刷新持久化(P1)
+
+**操作步骤**
+1. 保存一条任务后,刷新浏览器(或重开宿主页面)。
+2. 用相同用户名进入,打开历史。
+
+**预期结果**
+- 历史里仍能看到之前保存的任务(数据存在后端、按用户隔离,非浏览器 localStorage)。
+- 加载后能正常回显。
+
+---
+
+## PA-SAVE-06 · 无结果时禁止保存(P2)
+
+**操作步骤**
+1. 新开面板、尚未运行任何分析。
+
+**预期结果**
+- 「保存」按钮**禁用**(无可保存内容)。
+- 运行中也不应因保存导致状态错乱(保存按钮在有结果后才可用)。
+
+---
+
+## PA-SAVE-07 · 用户隔离(P1)
+
+**操作步骤**
+1. 用户名 A 保存一条任务。
+2. 切换用户名为 B(重新进入面板),打开历史。
+
+**预期结果**
+- B 的历史里**看不到** A 的任务。
+- B 直接用 A 的 taskId 加载应失败(404 / 无内容),不报错崩溃。
diff --git a/parallel-agents-vue-offline-pack/docs/03-重新分析.md b/parallel-agents-vue-offline-pack/docs/03-重新分析.md
new file mode 100644
index 00000000..7a0ef853
--- /dev/null
+++ b/parallel-agents-vue-offline-pack/docs/03-重新分析.md
@@ -0,0 +1,77 @@
+# 测试用例 · 重新分析与运行控制
+
+覆盖:重新分析(覆盖重跑)、运行中停止、空输入校验、运行态按钮切换。
+
+---
+
+## PA-RE-01 · 重新分析(覆盖重跑)(P0)
+
+**前置条件**:已跑完一轮、会话区有结果(按钮显示「重新分析」)。
+
+**操作步骤**
+1. (可选)修改任务名 / 简介。
+2. 点击「重新分析」。
+
+**预期结果**
+- **清空**上一轮的所有气泡与席位状态,按当前输入**重新并行**调用各智能体。
+- 不会和上一轮结果混在一起(旧消息被替换,不是追加)。
+- 重跑过程与首跑一致(流式、单列消息 + 点头像切换、步骤条、席位状态流转)。
+
+---
+
+## PA-RE-02 · 运行中按钮为「停止」(P1)
+
+**操作步骤**
+1. 开始分析,运行未结束时观察操作区。
+
+**预期结果**
+- 运行期间,主按钮显示「停止」(不再是「开始/重新分析」)。
+- 「保存」按钮在运行中可点(保存当前已产出的内容)或按实现禁用——以不报错为准。
+
+---
+
+## PA-RE-03 · 停止当前运行(P0)
+
+**前置条件**:正在运行中。
+
+**操作步骤**
+1. 点击「停止」。
+
+**预期结果**
+- 进行中的请求被取消(AbortController),不再有新增量进来。
+- 仍在 streaming 的气泡被收尾为终态(去掉光标,徽标置「已完成」)。
+- 按钮恢复为「重新分析」,可立即再次发起。
+- 控制台不应出现未捕获的 AbortError 报错(已被吞掉)。
+
+---
+
+## PA-RE-04 · 空输入校验(P1)
+
+**操作步骤**
+1. 任务名与简介**都留空**,点击「开始分析」。
+
+**预期结果**
+- 按钮处于**禁用**态,点击无效(`canStart` 为假需任务名或简介至少其一 + 至少一个智能体)。
+- 只填任务名或只填简介,按钮可用。
+
+---
+
+## PA-RE-05 · 未传智能体(P2)
+
+**操作步骤**
+1. `:agents="[]"`(空数组)挂载面板。
+
+**预期结果**
+- 操作区显示提示「⚠️ 未传入任何智能体(agents 为空)」。
+- 「开始分析」禁用,不会发起空请求。
+
+---
+
+## PA-RE-06 · 连续多次重新分析(P2)
+
+**操作步骤**
+1. 连续点击「重新分析」若干次(每次等上一轮结束或中途停止再点)。
+
+**预期结果**
+- 每次都干净重置 + 重跑,无残留旧气泡、无席位状态错乱。
+- 不出现多份 SSE 叠加导致的内容串列 / 重复。
diff --git a/parallel-agents-vue-offline-pack/docs/04-免密登录与鉴权.md b/parallel-agents-vue-offline-pack/docs/04-免密登录与鉴权.md
new file mode 100644
index 00000000..662a6610
--- /dev/null
+++ b/parallel-agents-vue-offline-pack/docs/04-免密登录与鉴权.md
@@ -0,0 +1,106 @@
+# 测试用例 · 免密登录与鉴权
+
+覆盖:username 免密自动登录、token 优先级、token 缓存与 401 重登、口令门、CSRF 绕过。
+
+> 背景:面板优先用宿主传入的 token;没有 token 时,用宿主传入的 **username** 走 DeerFlow 公共免密端点
+> `POST /api/v1/auth/login/username` 换取 `access_token`,以 `Authorization: Bearer` 发起后续请求
+> (带 Bearer 会绕过 CSRF)。token 解析优先级:`token` > `getToken` > 缓存 > `username` 登录。
+
+---
+
+## PA-AUTH-01 · username 免密自动登录(P0)
+
+**前置条件**:后端 `auth_login.username_login.require_password = false`(默认)。
+
+**操作步骤**
+1. 只传 `:username="demo"`(不传 token),打开面板。
+2. 开始分析 / 打开历史。
+
+**预期结果**
+- 面板能正常拉历史、能发起并行流式——说明已用 username 换到 token 并附带在请求头。
+- 旁证(手工 curl):
+ ```bash
+ curl -s -X POST -H "Content-Type: application/json" \
+ -d '{"username":"demo"}' http://localhost:8001/api/v1/auth/login/username
+ # → 返回 {"access_token":"...","token_type":"bearer",...}
+ ```
+
+---
+
+## PA-AUTH-02 · token 优先级高于 username(P1)
+
+**操作步骤**
+1. 同时传 `:token="<有效token>"` 和 `:username="demo"`。
+2. 触发请求(开始分析 / 历史)。
+
+**预期结果**
+- 直接用传入的 `token`(不会再去调登录端点)。
+- `:get-token` 同理优先于 username(动态取 token)。
+
+---
+
+## PA-AUTH-03 · token 缓存(不重复登录)(P1)
+
+**操作步骤**
+1. 只传 username,先后触发多次请求(拉历史 → 开始分析 → 保存)。
+
+**预期结果**
+- 登录端点**只调用一次**(首个请求),后续请求复用缓存 token(按 `baseUrl::username` 缓存)。
+- 可在浏览器 Network 面板核对 `login/username` 只出现一次。
+
+---
+
+## PA-AUTH-04 · 401 自动重登重试(P1)
+
+**前置条件**:能制造 token 失效(例如等待过期,或后端清理)。
+
+**操作步骤**
+1. 用 username 登录并缓存 token 后,使其失效,再触发一次 CRUD 请求。
+
+**预期结果**
+- 首次请求收到 401 → 面板**清缓存并用 username 重登一次**,再自动重试该请求。
+- 用户无感知(不需手动重新登录),请求最终成功。
+
+---
+
+## PA-AUTH-05 · 口令门(require_password=true)(P2)
+
+**前置条件**:后端把 `auth_login.username_login.require_password` 设为 `true`、配置 `password`。
+
+**操作步骤**
+1. 只传 username、不传 `loginPassword` → 触发请求。
+2. 再传上正确的 `:login-password="<口令>"` → 触发请求。
+
+**预期结果**
+- 不传口令时:登录返回 401「口令错误」,面板顶部红色错误提示,不崩溃。
+- 传对口令时:登录成功,后续请求正常。
+
+---
+
+## PA-AUTH-06 · Bearer 绕过 CSRF(P1)
+
+**目的**:核对带 Bearer 的 POST 不被 CSRF 拦截。
+
+**操作步骤(手工 curl 旁证)**
+```bash
+TOKEN=... # 上面登录拿到的 access_token
+# 带 Bearer 的 POST:参数非法应为 400/422,而非 403(403=CSRF 拦截)
+curl -s -o /dev/null -w "%{http_code}\n" -X POST \
+ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
+ -d '{}' http://localhost:8001/api/parallel-agents/run/stream
+```
+
+**预期结果**
+- 返回 **400/422**(业务参数校验),**不是 403**——证明 Bearer 已绕过 CSRF。
+- 不带任何鉴权时该 POST 返回 401/403(被拦截),属正常。
+
+---
+
+## PA-AUTH-07 · 未传 token 且未传 username(P2)
+
+**操作步骤**
+1. 既不传 token / getToken,也不传 username,触发需要鉴权的请求。
+
+**预期结果**
+- 请求因缺少鉴权失败(401/403),面板给出可读的错误提示(不白屏)。
+- 控制台/提示能反映「缺少 token 且未提供 username,无法登录」之类信息。
diff --git a/parallel-agents-vue-offline-pack/docs/05-嵌入与深色样式.md b/parallel-agents-vue-offline-pack/docs/05-嵌入与深色样式.md
new file mode 100644
index 00000000..c6f22d13
--- /dev/null
+++ b/parallel-agents-vue-offline-pack/docs/05-嵌入与深色样式.md
@@ -0,0 +1,105 @@
+# 测试用例 · 嵌入与深色样式
+
+覆盖:接口地址走配置文件、组件库构建产物、嵌入其它 Vue 项目、放进抽屉/弹窗自适应、深色成色、
+不污染宿主全局样式。
+
+---
+
+## PA-EMBED-01 · 接口地址走配置文件(P0)
+
+**操作步骤**
+1. 不传 `:base-url`,只改 `src/config.ts` 的 `DEFAULT_BASE_URL` 为目标后端地址。
+2. 运行面板触发请求。
+
+**预期结果**
+- 请求打到配置文件里的地址。
+- 传 `:base-url` prop 时**优先**用 prop(覆盖配置默认)。
+- dev 下设 `VITE_PA_BASE_URL` 环境变量也能覆盖默认。
+
+---
+
+## PA-EMBED-02 · 组件库构建产物(P0)
+
+**操作步骤**
+```bash
+pnpm build:lib
+```
+
+**预期结果**
+- `dist/` 产出:`parallel-agents-vue.js`(ESM)、`parallel-agents-vue.umd.cjs`(UMD)、`parallel-agents-vue.css`。
+- `vue` 被 external(不打进库,由宿主提供)。
+- 构建无报错、无「named + default exports」警告。
+
+---
+
+## PA-EMBED-03 · 嵌入其它 Vue 项目(P0)
+
+**操作步骤**
+1. 在另一个 Vue3 项目里:
+ ```ts
+ import { ParallelAgentsPanel } from "parallel-agents-vue";
+ import "parallel-agents-vue/style.css";
+ ```
+2. 模板中放置:
+ ```vue
+
+ ```
+
+**预期结果**
+- 组件正常渲染、可交互(深色面板,与 demo 一致)。
+- 只需 `:username` + `:agents` 两个 prop 即可跑起来(接口地址走配置)。
+- 也支持 `app.use(ParallelAgentsVue)` 全局注册后用 ``。
+
+---
+
+## PA-EMBED-04 · 放进抽屉 / 弹窗自适应(P0)
+
+**操作步骤**
+1. 把面板放进宿主的抽屉 / 对话框,外层给一个固定高度容器:
+ ```vue
+
+
+
+ ```
+2. 改变容器尺寸 / 视口大小。
+
+**预期结果**
+- 面板**充满容器**(根元素 `h-full w-full flex flex-col min-h-0`),不溢出、不塌缩。
+- 会话区内部独立滚动,输入区与席位条固定不被挤掉。
+- 窄容器下消息列表正常显示、内部滚动,不溢出。
+
+---
+
+## PA-EMBED-05 · 深色成色自带、与宿主主题无关(P1)
+
+**操作步骤**
+1. 分别在**浅色**宿主页和**深色**宿主页里嵌入面板。
+
+**预期结果**
+- 面板始终是深蓝深色(自带 `--pa-*` token,作用域在 `.parallel-agents`),不随宿主明暗变化。
+- 文案、徽标、步骤卡、并行列底、滚动条等观感与圆桌 Step2 深色一致。
+- 传 `:dark="false"` 时按实现切换(如未实现浅色则保持深色,至少不报错)。
+
+---
+
+## PA-EMBED-06 · 不污染宿主全局样式(P0)
+
+**目的**:核对**未引入 Tailwind preflight**(不重置宿主全局标签样式)。
+
+**操作步骤**
+1. 在一个有自己样式的宿主页里嵌入面板(页面有 `h1/p/button/ul` 等默认样式)。
+
+**预期结果**
+- 嵌入后,宿主页面**自身**的 `h1/p/button/列表/边距` 等样式**不被改变**(没有被 Tailwind 的 reset 抹平)。
+- 面板内部样式正常(不依赖宿主提供 Tailwind)。
+- 类名都在 `.parallel-agents` 作用域下,不与宿主类名冲突。
+
+---
+
+## PA-EMBED-07 · 多实例并存(P2)
+
+**操作步骤**
+1. 同一页面放置 2 个面板实例(不同 agents / 不同 username)。
+
+**预期结果**
+- 两个实例各自独立运行、各自保存/回显,互不串流、互不串 token 缓存(缓存按 `baseUrl::username` 区分)。
diff --git a/parallel-agents-vue-offline-pack/docs/06-可用智能体清单.md b/parallel-agents-vue-offline-pack/docs/06-可用智能体清单.md
new file mode 100644
index 00000000..a5d5f6a1
--- /dev/null
+++ b/parallel-agents-vue-offline-pack/docs/06-可用智能体清单.md
@@ -0,0 +1,73 @@
+# 测试用 · 可用智能体清单(可直接复制粘贴)
+
+测试时面板需要传入**真实存在的 `agent_id`**(后端真正拿去跑的标识,不能只写名字)。
+下面是当前后端里**可直接用于测试**的内置智能体(已排除 intent / recommender / coordinator 这些系统角色)。
+
+> 注:`name` 只是显示用、可省略;`agent_id` 必须照抄。下列为本环境实测可用的内置智能体;
+> 你部署里的智能体可能不同,用文末方法自查。
+
+## 可用智能体
+
+| agent_id | 显示名 |
+| --------------------------- | -------- |
+| `roundtable-solution-design`| 方案设计 |
+| `roundtable-risk-review` | 风险审查 |
+| `roundtable-execution-plan` | 执行规划 |
+| `roundtable-report` | 方案总结 |
+| `demo-expert` | 演示专家 |
+
+## A. 粘贴进 dev demo 页「智能体」输入框(每行 `agent_id, 显示名`)
+
+推荐至少 2 个以便点头像切换查看不同智能体:
+
+```
+roundtable-solution-design, 方案设计
+roundtable-risk-review, 风险审查
+```
+
+想多看几列就多贴几行:
+
+```
+roundtable-solution-design, 方案设计
+roundtable-risk-review, 风险审查
+roundtable-execution-plan, 执行规划
+```
+
+## B. 粘贴进组件 `:agents` prop(嵌入场景)
+
+```ts
+const agents = [
+ { agent_id: "roundtable-solution-design", name: "方案设计" },
+ { agent_id: "roundtable-risk-review", name: "风险审查" },
+ { agent_id: "roundtable-execution-plan", name: "执行规划" },
+];
+```
+
+```vue
+
+```
+
+## C. 配一个示例任务(直接粘贴到面板)
+
+- **任务名称**:`新功能上线方案评估`
+- **任务简介**:
+ ```
+ 我们要给 App 上线「扫码点餐」功能。请从你的专业角度,评估方案可行性、列出关键风险、
+ 并给出落地步骤与里程碑。
+ ```
+
+## 自查:列出你部署里所有可用的 agent_id
+
+```bash
+# 1) 免密登录拿 token(用户名换成你的)
+TOKEN=$(curl -s -X POST -H "Content-Type: application/json" \
+ -d '{"username":"demo"}' http://localhost:8001/api/v1/auth/login/username \
+ | python -c "import sys,json;print(json.load(sys.stdin)['access_token'])")
+
+# 2) 列出 agent_id name
+curl -s -H "Authorization: Bearer $TOKEN" http://localhost:8001/api/agents -o agents.json
+python -c "import json,io;[print(a['id'],'\t',a['name']) for a in json.load(io.open('agents.json',encoding='utf-8')).get('agents',[])]"
+```
+
+第一列 `id` 才是要填的 `agent_id`。挑你需要的智能体即可(避开 `roundtable-intent` /
+`roundtable-recommender` / `roundtable-coordinator*` 这些系统角色——它们不是用来当并行席位的)。
diff --git a/parallel-agents-vue-offline-pack/docs/07-使用说明.md b/parallel-agents-vue-offline-pack/docs/07-使用说明.md
new file mode 100644
index 00000000..6a5c2e1d
--- /dev/null
+++ b/parallel-agents-vue-offline-pack/docs/07-使用说明.md
@@ -0,0 +1,345 @@
+# 使用说明 · 在其它 Vue 项目中引入
+
+本组件库只对外暴露**一个**组件 `ParallelAgentsPanel`(其余 `MessageBubble` / `MessageStepsCard` /
+`ParallelSeatStrip` / `Avatar` / `MarkdownContent` 都是它的内部子组件,**无需也不要单独引入**)。
+宿主只管把它放进自己的页面 / 抽屉 / 弹窗,并传几个 prop。
+
+---
+
+## 一、要引入的东西(就这两样)
+
+1. **组件**:`ParallelAgentsPanel`
+2. **样式**:`parallel-agents-vue/style.css`(一次性引入即可,样式作用域全在 `.parallel-agents` 下)
+
+> 依赖说明:`vue ^3.4`(宿主提供,库不打包 vue);`markdown-it` 已打进库内,无需宿主安装。
+
+---
+
+## 二、两种引入方式
+
+### 方式 A:构建成库后按包引入(推荐)
+
+1. 在本项目构建:
+ ```bash
+ pnpm install
+ pnpm build:lib # 产出 dist/parallel-agents-vue.{js,umd.cjs,css}
+ ```
+2. 宿主项目里安装(任选其一):
+ - 把本目录作为本地依赖:`"parallel-agents-vue": "file:../parallel-agents-vue"`;
+ - 或发到你们的私有 npm 后正常 `pnpm add parallel-agents-vue`。
+3. 代码里引入:
+ ```ts
+ import { ParallelAgentsPanel } from "parallel-agents-vue";
+ import "parallel-agents-vue/style.css";
+ ```
+
+### 方式 B:直接拷贝源码(无需发包)
+
+把本项目 `src/` 下这些拷进宿主项目(保持相对结构),按相对路径引入 `ParallelAgentsPanel.vue` +
+`styles/parallel-agents.css` 即可:
+
+```
+src/
+ components/ ParallelAgentsPanel.vue · MessageBubble.vue · MessageStepsCard.vue
+ ParallelSeatStrip.vue · Avatar.vue · MarkdownContent.vue
+ composables/ useParallelRun.ts
+ lib/ avatar-constants.ts
+ styles/ parallel-agents.css
+ api.ts · config.ts · types.ts
+```
+宿主需有 `markdown-it` 依赖(`pnpm add markdown-it`),并保证构建支持 `.vue`(Vite/@vitejs/plugin-vue)。
+
+---
+
+## 三、注册与使用
+
+### 局部引入(推荐)
+
+```vue
+
+
+
+
+
+```
+
+### 全局注册(可选)
+
+```ts
+import ParallelAgentsVue from "parallel-agents-vue";
+import "parallel-agents-vue/style.css";
+app.use(ParallelAgentsVue); // 之后任意模板可直接用
+```
+
+---
+
+## 四、放进抽屉 / 弹窗
+
+组件根元素是 `h-full w-full flex flex-col min-h-0`,会**充满父容器**。所以**父容器要给一个高度**
+(弹窗/抽屉本身有高度即可):
+
+```vue
+
+
+
+```
+
+---
+
+## 五、接口地址与登录
+
+- **接口地址**:默认读 `src/config.ts` 的 `DEFAULT_BASE_URL`(可改源码,或 dev 设 `VITE_PA_BASE_URL`);
+ 也可用 `:base-url` prop 临时覆盖。
+- **登录**:把宿主的**用户名**用 `: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` | | 动态取 token |
+| `loginPassword` | `string` | | 仅当后端开启口令门时需要 |
+| `taskId` | `string` | | 传入则挂载时自动加载并回显该任务 |
+| `model` | `string` | | 可选模型名(不传走默认 / 后端回退) |
+| `dark` | `boolean`(默认 `true`) | | 深色面板(自带成色,不依赖宿主主题) |
+
+`AgentInput`/`SavedTask`/`AgentResult` 等类型也从包里导出:
+`import type { AgentInput, SavedTask, AgentResult } from "parallel-agents-vue"`。
+
+---
+
+## 六·一、每个参数怎么传(逐个举例)
+
+### `agents`(必填)——「智能体参数」怎么传
+
+一个数组,每项 `{ agent_id, name?, avatarType? }`:
+
+```ts
+const agents = [
+ { agent_id: "roundtable-solution-design", name: "方案设计" }, // 最常用:id + 显示名
+ { agent_id: "roundtable-risk-review", name: "风险审查", avatarType: "amber" }, // 顺带指定头像配色
+ { agent_id: "demo-expert" }, // 只给 id(显示名回退成 id)
+];
+```
+```vue
+
+```
+- `agent_id`**必须是后端真实注册的 id**(只写名字无效);可用 id 见 [06-可用智能体清单.md](./06-可用智能体清单.md)。
+- `name`:席位/气泡显示名,可省(省了用 id)。
+- `avatarType`:可选 `blue|purple|emerald|amber|pink|human`;不传按顺序自动轮转配色。
+
+### `username`——免密登录(推荐)
+
+```vue
+
+```
+
+### `baseUrl`——后端地址(不传用 config 默认)
+
+```vue
+
+```
+
+### `token` / `getToken`——已有登录态时(优先于 username)
+
+```vue
+
+
+
+
+```
+
+### `model`——指定模型(不传走默认/后端回退;也可在面板右上角下拉切)
+
+```vue
+
+```
+
+### `dark`——深色(默认 true,自带成色)
+
+```vue
+
+```
+
+### `taskId`——存储主键 + 回显(见 §七·二)
+
+```vue
+ (taskId = id)"
+/>
+```
+
+### 全量示例(所有参数一起)
+
+```vue
+
+
+
+
+
(taskId = id)"
+ @completed="onCompleted"
+ />
+
+
+```
+
+## 七、事件
+
+| 事件 | 载荷 | 触发时机 |
+| ----------------- | ------------------ | --------------------------------------- |
+| `saved` | `SavedTask` | 保存成功(新建或更新)后 |
+| `update:taskId` | `string` | 当前任务 id 变化(保存 / 加载历史时) |
+| `completed` | `AgentResult[]` | 一轮并行分析结束时,回传各智能体返回内容 |
+
+```vue
+ console.log('saved', t.id)"
+ @update:taskId="(id) => (taskId = id)"
+ @completed="(results) => console.log('各智能体返回:', results)"
+/>
+```
+
+## 七·一、如何获取模型返回的内容
+
+每个智能体(模型)返回的正文是 `AgentResult.content`。两种方式,任选其一:
+
+**A. 事件**:监听 `@completed`,一轮分析结束时自动回传 `AgentResult[]`:
+
+```vue
+ {
+ // results: [{ agent_id, name, content, status }]
+ for (const r of results) console.log(r.name, '→', r.content);
+ }"
+/>
+```
+
+**B. 方法(模板 ref)**:随时主动取(不必等事件):
+
+```vue
+
+
+
+
+
+```
+
+`AgentResult = { agent_id, name, content, status }`,`content` 即该智能体模型返回的正文(Markdown)。
+`defineExpose` 还暴露了 `start()` / `cancel()`,可由宿主代替按钮触发开始 / 停止。
+
+## 七·二、如何保存 + 回显聊天记录
+
+**保存**:面板里点「保存」即把当前整段聊天记录(各智能体气泡 + 步骤条等)存到后端,按用户隔离。
+传 `:task-id` 时**以它为存储主键**做 upsert(已存在→更新;不存在→用该 id 新建);不传则首次保存由后端
+生成 id(通过 `@update:taskId` 回传,下次再保存即更新这条)。
+
+**回显(两种方式)**:
+
+```vue
+
+
+```
+
+```ts
+// 方式 1 变体:运行中改 taskId(双向绑定),面板会自动切到并回显那条任务
+const taskId = ref(""); // 留空=新任务
+// taskId = id" ... />
+taskId.value = "task-001"; // 改了之后面板自动回显 task-001
+```
+
+> 方式 2:面板**右上角内置「历史」下拉**——点开选一条历史任务即回显,**无需写任何代码**。
+
+要点:先**保存过**、再用**同一个 taskId**(或从历史里选)才会有内容回显;没存过的新 id 第一次进去是空白
+(后台会探测一次 `GET /…/{id}` 返回 404,属正常,面板静默处理)。
+
+---
+
+## 八、交互要点(与圆桌 Step2 一致)
+
+- 输入「任务名称 + 简介」→「开始分析」→ 各智能体**并行**输出。运行期间禁用**写文件**(`write_file`/
+ `str_replace`)与**派活/脚本**(`agent_orchestration`/`bash`,防子智能体模仿总控调度);检索、查看、
+ `ask_clarification` 等照常 —— 所以**步骤条 / 协作卡片正常显示**,每个智能体只做自己那份分析。
+- 主区每次只显示**一个**席位的消息列表;**点席位头像切换**要看哪个(单选)。
+- 顶部席位状态条显示每个智能体的进行中 / 完成 / 出错。
+- 操作区右侧有**模型切换**下拉(拉取后端 `/api/models`);选「默认」则用 `model` prop / 配置 / 后端回退。
+ 切换作用于**下一次**「开始分析 / 重新分析」,运行中禁用。
+- 「保存」存档;「历史」里选回**回显**;「重新分析」清空重跑。
+
+---
+
+## 九、注意事项
+
+- **不会污染宿主全局样式**:库未引入 Tailwind preflight,类名都在 `.parallel-agents` 作用域内。
+- **深色自带成色**:面板始终深蓝深色,不随宿主明暗切换(与圆桌 Step2 观感一致)。
+- **跨域**:若宿主与后端不同源,后端需允许该来源的 CORS + 带 `credentials`;登录端点公共且 CSRF 豁免,
+ 业务请求用 Bearer 头已绕过 CSRF。
+- **agent_id 必须真实**:见 [06-可用智能体清单.md](./06-可用智能体清单.md) 获取可用 id 与自查方法。
diff --git a/parallel-agents-vue-offline-pack/docs/08-开发文档.md b/parallel-agents-vue-offline-pack/docs/08-开发文档.md
new file mode 100644
index 00000000..0cf3cd45
--- /dev/null
+++ b/parallel-agents-vue-offline-pack/docs/08-开发文档.md
@@ -0,0 +1,226 @@
+# 开发文档 · parallel-agents-vue
+
+面向**维护 / 二次开发**这个前端的同学。讲清楚:它是什么、架构与数据流、目录与各文件职责、
+对接的后端契约、样式系统、以及常见扩展怎么改。使用 / 嵌入请看 [07-使用说明.md](./07-使用说明.md)。
+
+---
+
+## 1. 它是什么
+
+一个**独立、可嵌入**的 Vue3 组件:输入「任务名称 + 简介」→ **并行**调用宿主指定的多个智能体 →
+深色消息列表(**1:1 复刻圆桌 Step2**:步骤条 / 协作卡片 / 席位状态条 / 点头像切换单显)→
+支持**保存 / 回显 / 重新分析**。只对外暴露一个组件 `ParallelAgentsPanel`。
+
+- 后端是同仓库的 DeerFlow 网关(`offline-backend-20260512/backend`),本前端只消费它的几个 HTTP 接口。
+- 并行**编排在后端**:前端发一条请求,后端扇出 N 个智能体、合并成**一条 SSE**回来;前端只解析这一条流。
+
+---
+
+## 2. 技术栈
+
+| 方面 | 选型 |
+| -------- | ----------------------------------------------------------------- |
+| 框架 | Vue 3(`
+