feat: overhaul task board and cross-conversation workflows

This commit is contained in:
shiyue
2026-08-12 09:41:36 +08:00
parent e7d28935ef
commit 86231f0d97
55 changed files with 11628 additions and 280 deletions

View File

@@ -0,0 +1,6 @@
{"file":".trellis/spec/frontend/index.md","reason":"执行前端质量检查清单"}
{"file":".trellis/spec/frontend/component-guidelines.md","reason":"检查渲染结构、事件归属和可维护性"}
{"file":".trellis/spec/frontend/quality-guidelines.md","reason":"检查回归覆盖、错误处理和无障碍状态"}
{"file":".trellis/spec/frontend/state-management.md","reason":"检查状态流转和并发保护"}
{"file":".trellis/spec/frontend/type-safety.md","reason":"检查附件状态数据的边界处理"}
{"file":".trellis/tasks/08-11-failed-insert-card-actions/research/failed-insert-chain.md","reason":"逐项核对重试、删除、回滚和回归验收契约"}

View File

@@ -0,0 +1,6 @@
{"file":".trellis/spec/frontend/index.md","reason":"执行前端预开发清单与完成后质量检查"}
{"file":".trellis/spec/frontend/component-guidelines.md","reason":"保持失败卡片渲染和事件边界清晰"}
{"file":".trellis/spec/frontend/quality-guidelines.md","reason":"遵循项目测试、错误处理和代码质量要求"}
{"file":".trellis/spec/frontend/state-management.md","reason":"正确管理失败、重试中、成功和删除状态"}
{"file":".trellis/spec/frontend/type-safety.md","reason":"校验附件状态字段和事件数据边界"}
{"file":".trellis/tasks/08-11-failed-insert-card-actions/research/failed-insert-chain.md","reason":"提供已验证的调用链、状态契约、持久化风险和测试范围"}

View File

@@ -0,0 +1,53 @@
# 失败插入卡片重试与删除
## 背景
图片或附件插入失败后,消息内卡片目前只显示“插入失败”,用户既不能在原位重试,也不能清理该失败项,只能重新组织消息或容忍残留卡片。
## 目标
为失败插入卡片提供“重试”和“删除”两个就地操作,使失败可恢复、残留可清理,同时保持原消息文本和其他附件不变。
## 用户故事
- 作为发送图片/附件的用户,我可以点击“重试”,沿用原失败项再次执行插入,不必重新选择文件。
- 作为发送图片/附件的用户,我可以点击“删除”,只移除目标失败项,不影响同一消息中的文本和其他附件。
- 作为键盘或辅助技术用户,我可以识别按钮用途和重试中的忙碌状态。
## 功能要求
1. 仅状态为失败的插入卡片展示“重试”和“删除”操作。
2. 重试必须以失败项的稳定标识为目标,复用现有插入链路及原始附件数据。
3. 重试开始后卡片进入处理中状态;在请求完成前禁用重复重试和删除,并暴露可访问的忙碌状态。
4. 重试成功后,卡片按现有成功状态渲染,不再展示失败操作。
5. 重试再次失败后,卡片恢复失败操作,并显示既有或更明确的失败原因。
6. 删除只移除目标失败项;同名附件、其他附件、消息文本及其他消息不得受影响。
7. 操作必须遵循现有状态持久化边界:若失败项已持久化,刷新后仍能正确重试/删除;若现有设计只保存于客户端,则不得伪造跨刷新保证。
8. 事件处理必须防止重复绑定和并发提交。
## 视觉与交互
- 两个操作放在失败状态附近,文本分别为“重试”“删除”。
- “重试”为主要恢复操作,“删除”为克制的危险操作;不引入与当前卡片风格冲突的大面积按钮。
- 重试期间显示明确的处理中反馈,并设置 `disabled``aria-busy`(或等价可访问语义)。
- 按钮应支持键盘聚焦与激活,并具有明确的可访问名称。
## 非目标
- 不重构整个消息或附件系统。
- 不删除用户本地原始文件,也不清理与目标失败项无关的服务器资源。
- 不新增独立的附件管理面板。
- 不改变成功卡片和进行中卡片的既有操作语义。
## 验收标准
- 失败卡片可见两个操作,其他状态不可见。
- 连续快速点击重试只产生一次有效插入请求。
- 成功、再次失败、删除三个分支均有自动化测试。
- 删除一项后,同消息中的文字和其他附件保持不变。
- 定向测试、相关回归和语法/静态检查通过,单次测试最长 60 秒。
- 最终差异不覆盖工作区中已有的“独立任务看板”改动。
## 实施隔离
`.trellis/.current-task` 当前属于另一个活动会话。本任务不切换该全局指针;实施与检查代理必须显式读取本目录下的 `prd.md`、上下文清单和研究材料。

View File

@@ -0,0 +1,74 @@
# 失败插入卡片链路与实现契约
## 现状链路
```text
sendMessage
→ 创建本地用户卡片 + clientMessageId
→ WebSocket type=message
→ handleMessage
→ 活动 Codex App turn 时进入 handleCodexAppSteerMessage
→ codex_app_steer_status(pending / inserted / failed)
→ handleServerMessage
→ updateCodexAppSteerMessage
→ setCodexAppSteerStatusElement
```
关键位置:
- `public/app.js``sendMessage``setCodexAppSteerStatusElement``updateCodexAppSteerMessage``createMsgElement``clearUserMessageIndex`
- `public/style.css``.codex-steer-*` 状态样式。
- `server.js``handleMessage``handleCodexAppSteerMessage`
- `scripts/regression.js``runCodexAppRuntimeImageSteerRegression``runCodexAppStaleRunningRegression`
## 已确认事实
1. 本地失败卡片以 `clientMessageId` / `data-message-id` 稳定定位,不应按文件名或 DOM 顺序匹配。
2. 当前 `userMessageIndex` 只保存内容与时间;重试还需要保存附件、会话、模式和 agent。
3. 当前状态渲染只有文本,没有操作容器、忙碌语义和并发保护。
4. 当前服务端在调用 `turn/steer` 前持久化用户消息,最终失败时不回滚,会留下模型未接收的幽灵消息。
5. 同步前置失败不会持久化;两类失败必须统一为“失败卡片仅存在于当前客户端”的语义。
6. 本地临时卡片当前提前增加 `currentSessionMessageCount`,失败后会造成消息索引漂移。
## 推荐实现
### 前端
- 新增独立的运行中插入记录 Map`clientMessageId` 保存元素、文本、附件克隆、sessionId、mode、agent、pending/committed 状态。
- `clearUserMessageIndex` 同时清理该 Map防止切换会话后重试旧请求。
- `setCodexAppSteerStatusElement` 统一维护:
- `role=status``aria-live=polite`
- 卡片 `aria-busy`
- 失败态的“重试”“删除”按钮;
- 非失败态移除操作容器;
- 按钮使用 `type=button`、明确 `aria-label`,并阻止重复绑定。
- `retryCodexAppSteerMessage` 只在记录存在、仍属于当前会话、当前为 Codex App 运行中且 WebSocket 可用时发送;先切到 pending 并禁用操作,发送失败则恢复 failed。
- `deleteCodexAppSteerMessage` 只允许删除 failed 临时项,移除 DOM、两个索引记录并刷新轮廓/滚动条。
- 临时插入不提前递增 `currentSessionMessageCount`;收到 `inserted` 后才标记 session message 并递增,失败/删除不改变持久化索引。
### 服务端
- 给待持久化的 steer 用户消息写入稳定 `id`(优先使用 `clientMessageId`)。
- 最终 `turn/steer` 失败时,在发送 failed 状态前,从最新会话中按该 id 精确移除本次用户消息并保存。
- stale no-active-turn 自动恢复成功分支不得回滚;同步前置失败和 ready 超时本就没有持久化项。
- 异步错误消息补带 `clientMessageId`,便于前端和回归关联。
## 测试契约
- 前端独立单测或可执行契约测试:
- failed 创建两个按钮pending/inserted 不展示;
- 快速双击重试只发送一次,载荷复用原文本与附件;
- retry 时 `aria-busy=true` 且操作禁用/移除;
- failed 恢复操作;
- delete 只移除目标项及对应索引;
- turn 已结束或 WebSocket 断开时不发送并保留可恢复失败态。
- 服务端定向回归:
- 现有同步附件失败仍不持久化;
- mismatch 异步失败后同名用户消息为 0
- stale recovery / 成功 steer 用户消息仍恰好为 1。
- 静态契约按钮文本、CSS 作用域和 status handler 保持存在。
## 已知限制
- 附件已经从服务端过期时,单纯重试仍会失败;按钮主要恢复临时 turn/server 错误。再次失败后必须恢复可操作状态和明确错误提示。
- 失败项按现有持久化边界保持客户端临时态,刷新后会消失;不新增任意历史消息删除协议。

View File

@@ -0,0 +1,32 @@
{
"id": "failed-insert-card-actions",
"name": "failed-insert-card-actions",
"title": "失败插入卡片重试与删除",
"description": "为 Codex App 插入失败卡片增加重试与删除,并修复失败消息持久化回滚。",
"status": "completed",
"dev_type": "bugfix",
"scope": "frontend,server,regression",
"package": null,
"priority": "P2",
"creator": "shiyue",
"assignee": "shiyue",
"createdAt": "2026-08-11",
"completedAt": "2026-08-11",
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [
"public/app.js",
"public/style.css",
"server.js",
"scripts/regression.js",
"scripts/failed-insert-card-unit.js"
],
"notes": "未切换 .trellis/.current-taskccweb 重启因其他运行中对话暂缓。",
"meta": {}
}

View File

@@ -0,0 +1,3 @@
{"file":".trellis/spec/backend/quality-guidelines.md","reason":"检查后端实现的兼容性、错误边界和可维护性"}
{"file":".trellis/spec/frontend/quality-guidelines.md","reason":"检查任务看板视图的可用性和回归风险"}
{"file":".trellis/tasks/08-11-task-board/research/integration-contract.md","reason":"验证跨模块契约、优先级与文件边界没有漂移"}

View File

@@ -0,0 +1,3 @@
{"file":".trellis/spec/backend/index.md","reason":"遵循项目后端模块、错误处理和质量约束入口"}
{"file":".trellis/spec/frontend/index.md","reason":"遵循项目现有前端结构与交互约束入口"}
{"file":".trellis/tasks/08-11-task-board/research/integration-contract.md","reason":"三个并行实现必须共享同一数据、状态、错误码和文件边界契约"}

View File

@@ -0,0 +1,195 @@
# 技术设计与并行边界
## 共享模型
会话 JSON 新增可选字段:
```json
{
"taskTracking": {
"enabled": true,
"statusId": "in_progress",
"baseStatus": "in_progress",
"source": "user",
"reason": "",
"summary": "",
"progress": null,
"reportingStatus": "reported",
"version": 1,
"enabledAt": "ISO-8601",
"disabledAt": null,
"statusUpdatedAt": "ISO-8601",
"completedAt": null,
"archivedAt": null,
"archivedBy": null
}
}
```
状态定义:
```json
{
"id": "waiting_deploy",
"label": "等待部署",
"baseStatus": "waiting_user",
"description": "实现完成,等待部署环境或发布窗口",
"color": "#f59e0b",
"order": 40,
"system": false,
"enabled": true
}
```
## 服务契约
MCP 对话负责产出 `lib/task-board-service.js`,导出:
```text
createTaskBoardService(deps)
.getStatusDefinitions()
.upsertStatusDefinition(input, actor)
.removeStatusDefinition(id, migrateTo, actor)
.listTasks(filters)
.setTracking(sessionId, enabled, actor)
.updateStatus(sessionId, update, actor)
.setArchived(sessionId, archived, actor)
.recordLifecycleEvent(sessionId, event)
```
服务通过依赖注入读取/保存会话和状态配置,不直接 import `server.js`。所有修改使用版本号和统一校验。
## WebSocket 契约
客户端请求:
- `task_board_query`
- `task_tracking_set`
- `task_status_set`
- `task_status_definition_upsert`
- `task_status_definition_remove`
- `task_archive_set`
服务端响应/广播:
- `task_board_result`
- `task_board_event`
- `task_tracking_result`
- `task_status_result`
- `task_status_definitions_result`
- `task_archive_result`
所有请求携带 `requestId`;所有写响应返回规范化任务、状态定义或稳定错误码。
## MCP 契约
### ccweb_task_status_list
无业务参数;来源会话从 MCP 上下文解析。返回 `trackingEnabled`、当前状态和状态定义。
### ccweb_task_update
```json
{
"statusId": "in_progress",
"reason": "可选,最长 2000 字符",
"summary": "可选,最长 4000 字符",
"progress": 0
}
```
`progress` 范围 0-100。跟踪关闭返回 `task_tracking_disabled`,未知状态返回 `task_status_unknown`
## 生命周期契约
Hook 模块只接收事实事件,不自行读写文件:
```json
{
"type": "turn_completed",
"turnId": "可选",
"occurredAt": "ISO-8601",
"outcome": "completed",
"pendingUserInput": false,
"hadTaskStatusUpdate": false,
"error": null
}
```
规则:
- 未开启跟踪:忽略。
- 用户消息:若归档则取消归档;状态进入 `in_progress`
- turn started非终态进入 `in_progress`
- structured user input进入 `waiting_user`
- turn failed进入 `blocked` 并记录错误。
- turn completed 且本轮无 MCP保持状态设置 `reportingStatus=missing`
- 本轮已有 MCP不得被 Hook 覆盖。
## 并行文件所有权
### 前端对话(只允许修改)
- `public/task-board.js`
- `public/task-board.css`
- `scripts/task-board-frontend-unit.js`
不得修改:`public/index.html``public/app.js``public/style.css``server.js``scripts/regression.js`
### MCP/状态服务对话(只允许修改)
- `lib/task-board-service.js`
- `lib/task-board-mcp.js`
- `scripts/task-board-service-unit.js`
不得修改:`server.js``lib/ccweb-mcp-server.js``scripts/regression.js`、任何 `public/*` 现有文件。
### Hook 对话(只允许修改)
- `lib/task-board-lifecycle.js`
- `scripts/task-board-lifecycle-unit.js`
不得修改:`server.js``lib/codex-app-runtime.js``scripts/regression.js`、任何 `public/*` 文件。
### 集成对话(前三路完成后独占)
可以修改中心文件并调整前三路模块:
- `server.js`
- `lib/ccweb-mcp-server.js`
- `lib/codex-app-runtime.js`(仅必要时)
- `public/index.html`
- `public/app.js`
- `public/style.css`(优先不改,使用独立 CSS
- `scripts/regression.js`
- 前三路新模块
## 视觉方向
- 视觉论点:延续 cc-web 的安静暗色操作界面,以克制的青色主强调和少量状态色构建密集、清晰的横向看板。
- 内容计划:顶层视图入口与工具条;横向状态工作区;归档视图;状态编辑面板。
- 交互论点:视图切换轻淡入、卡片悬停与状态变化使用短促位移/颜色过渡、窄屏保持流畅横向滚动。
## 验证命令
```bash
node --check lib/task-board-service.js
node --check lib/task-board-mcp.js
node --check lib/task-board-lifecycle.js
node --check public/task-board.js
node scripts/task-board-service-unit.js
node scripts/task-board-lifecycle-unit.js
node scripts/task-board-frontend-unit.js
node --check server.js
timeout 60s npm run regression
```
视觉验收使用隔离端口和临时配置目录启动测试服务,不提前重启 pm2 生产服务。
## 重启门禁
1. 调用 `ccweb_list_conversations(status="running")`
2. 当前来源对话 ID 固定记录为 `56bb82c6-4fa3-4cb8-bb84-6969e6547d3a`;标题“设计任务看板状态机制”仅作人工交叉核验。
3. 三个实现对话和集成对话必须全部为 idle`ccweb_list_pending_replies` 无 waiting/delivering。
4. 任何其他 conversation 状态为 running 都暂停重启并报告。
5. 满足条件后执行 `pm2 restart ccweb --update-env`,随后检查 pm2 状态和 HTTP/WebSocket 健康。

View File

@@ -0,0 +1,79 @@
# 独立任务看板 PRD
## 目标
为 cc-web 增加一个与现有聊天功能解耦的任务看板。只有显式开启“加入任务看板”的会话才参与状态跟踪Agent 可通过 MCP 主动上报cc-web 生命周期 Hook 在缺少上报时提供保守兜底。完成状态与软归档分离。
## 用户场景
1. 用户从普通聊天页开启“加入任务看板”,该会话立即出现在看板。
2. 用户从任务看板创建会话,新会话默认开启任务跟踪。
3. 用户查看系统固定状态列,并新增、编辑、排序或停用自定义状态。
4. Agent 调用状态 MCP 上报处理中、等待用户、阻碍、完成或自定义状态。
5. Agent 没有上报时cc-web 根据 turn 生命周期和结构化事件更新基础状态或标记“未上报”。
6. 用户手动归档已完成任务;以后可取消归档。归档不删除会话。
## 功能要求
### 任务跟踪开关
- 普通新对话默认 `taskTracking.enabled=false`
- 从任务看板创建的会话默认 `true`
- 关闭开关后不进入看板、不运行自动分类;保留已有状态用于审计。
- Agent 不能通过状态 MCP 偷偷开启、关闭或归档任务。
### 状态
- 系统固定状态 ID`unassigned``in_progress``waiting_user``blocked``completed`
- 系统 ID 和语义不可删除;展示名称、颜色、顺序允许配置。
- 自定义状态必须映射到一个系统 `baseStatus`,并包含名称、说明、颜色、顺序和启用状态。
- 删除仍被任务引用的自定义状态前必须指定迁移目标。
- 状态优先级:用户操作 > MCP 明确上报 > 生命周期 Hook > 默认状态。
### MCP
- `ccweb_task_status_list`:读取当前会话是否启用跟踪及可用状态定义。
- `ccweb_task_update`:当前来源会话上报 `statusId``reason``summary``progress`
- 跟踪关闭时 `ccweb_task_update` 返回 `task_tracking_disabled`,不得自动加入看板。
- 不提供 Agent 归档 MCP。
### 生命周期 Hook
- 使用 cc-web 内部事件,不修改用户 `.codex/hooks.json`
- 处理 `turn_started``user_input_requested``turn_completed``turn_failed``user_message_received`
- `turn_started` 可进入处理中;结构化用户输入进入等待用户;失败进入阻碍。
- `turn_completed` 不等于任务完成。若本轮没有 MCP 上报,保留业务状态并标记 `reportingStatus=missing`
- 已归档会话收到新用户消息时自动取消归档并进入处理中。
### 看板
- 提供独立任务看板视图,不替换现有聊天页与会话列表。
- 支持搜索、会话/Agent 筛选、优先级筛选、横向状态列、归档视图和状态管理面板。
- 卡片点击打开原会话;运行态和任务态分开显示。
- 支持桌面横向看板和窄屏横向滚动,不破坏现有响应式布局。
### 归档
- 归档使用 `archivedAt`/`archivedBy` 软标记,不移动或删除会话文件。
- 用户可手动归档/取消归档。
- 自动归档策略保留扩展点,本期不默认启用定时归档。
## 非目标
- 不修改 Codex Hook 配置。
- 不把所有历史对话自动加入任务池。
- 不从自由文本强行推断任务完成。
- 不删除、移动或迁移现有会话文件。
- 不在三个并行实现对话中修改中心接线文件。
## 验收标准
1. 现有普通会话默认不出现在任务看板。
2. 开关启用后会话可进入看板,关闭后立即退出但状态保留。
3. 系统状态存在,自定义状态可创建、编辑、排序、停用并安全迁移。
4. 两个 MCP 工具可发现、可调用,并遵守来源会话和开关边界。
5. cc-web 生命周期事件正确驱动基础状态,且不会把 turn 完成误判为任务完成。
6. 归档不删除会话,重新发消息可恢复。
7. 前端视图与现有 cc-web 风格一致,桌面和窄屏可用。
8. `node --check`、新模块单元测试和 `timeout 60s npm run regression` 全部通过。
9. 隔离测试服务完成真实浏览器验收;生产服务仅在重启门禁通过后重启。

View File

@@ -0,0 +1,38 @@
# 并行实现共享契约
本文件是三个实现对话与最终集成对话的唯一共享协议摘要。实现前必须同时阅读 `prd.md``info.md`
## 核心不变量
1. 普通会话默认不跟踪;`taskTracking.enabled` 是进入任务池的唯一开关。
2. `archivedAt``statusId` 分离;归档绝不删除或移动会话。
3. 系统状态 ID 稳定;自定义状态必须映射到系统 `baseStatus`
4. 用户写入优先于 MCPMCP 优先于 HookHook 不覆盖同轮 MCP。
5. `turn_completed` 绝不自动等价为 `completed`
6. 三个并行对话不得修改中心接线文件;第四个对话统一接线。
## 系统状态
| ID | 默认标签 | 语义 |
|---|---|---|
| unassigned | 待认领 | 已加入看板但尚未开始 |
| in_progress | 处理中 | 任务仍在推进 |
| waiting_user | 等待确认 | 等待用户输入、验收或外部条件 |
| blocked | 遇到阻碍 | 发生错误或无法继续 |
| completed | 已完成 | 任务业务目标已经完成 |
## 错误码
- `task_tracking_disabled`
- `task_status_unknown`
- `task_status_invalid`
- `task_status_in_use`
- `task_version_conflict`
- `task_session_not_found`
## 集成原则
- 公共服务模块不依赖 `server.js`,由集成层注入 IO。
- 前端模块通过适配器接收 `send``openSession` 和初始数据,不直接依赖现有全局变量。
- 所有新增消息和工具使用 `task_` / `ccweb_task_` 前缀,避免覆盖现有协议。
- 单元测试可独立运行,不启动生产 cc-web。

View File

@@ -0,0 +1,26 @@
{
"id": "task-board",
"name": "task-board",
"title": "独立任务看板",
"description": "",
"status": "in_progress",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "shiyue",
"assignee": "shiyue",
"createdAt": "2026-08-11",
"completedAt": null,
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}

View File

@@ -0,0 +1,4 @@
{"file":".trellis/spec/backend/index.md","reason":"复核后端改动与项目质量入口"}
{"file":".trellis/spec/guides/cross-layer-thinking-guide.md","reason":"复核所有跨层边界和回传数据完整性"}
{"file":".trellis/spec/guides/code-reuse-thinking-guide.md","reason":"复核正式 MCP 与备用工具定义没有再次复制漂移"}
{"file":".trellis/tasks/08-11-unify-ccweb-message-reply/research/current-behavior.md","reason":"复核兼容行为和真实失败场景均有覆盖"}

View File

@@ -0,0 +1,4 @@
{"file":".trellis/spec/backend/index.md","reason":"后端规范入口与质量检查索引"}
{"file":".trellis/spec/guides/cross-layer-thinking-guide.md","reason":"跨工具 Schema、dispatcher、pending 与运行提示的数据流契约"}
{"file":".trellis/spec/guides/code-reuse-thinking-guide.md","reason":"正式 MCP 与备用定义必须复用单一来源"}
{"file":".trellis/tasks/08-11-unify-ccweb-message-reply/research/current-behavior.md","reason":"当前行为、真实误用和兼容边界"}

View File

@@ -0,0 +1,48 @@
# 技术设计:统一 ccweb 消息回传接口
## 数据流
```text
MCP 工具 Schema
↓ replyMode
内部 dispatcher
↓ one_way / expectReply
sendCrossConversationMessage
├─ one_way → 目标运行,结果留在目标
└─ return_and_continue
↓ pending(requestId + originalRequest)
目标完成 → ready → 来源空闲
写回展示消息 + 带关联信息的隐藏续跑消息
```
## 建议模块边界
- `lib/ccweb-mcp-server.js`
- 继续作为正式工具定义的单一来源。
- 导出 reply mode 常量send schema 复用这些常量。
- 不再公开 request-reply definition。
- `server.js`
- dynamic tools 从已导入的 `CCWEB_MCP_TOOLS` 按白名单筛选并添加 namespace。
- dispatcher 保留旧别名。
- 发送、pending、目标提示和来源续跑接入 reply mode。
## 兼容边界
- 新 Schema`replyMode` 必填。
-`send_message`:内部缺省归一为 `one_way`
-`request_reply`:内部固定归一为 `return_and_continue`
- 旧 MCP/dynamic 客户端:隐藏 allowlist 接受 `ccweb_request_reply`,但工具列表不公开。
- 旧 pending 数据:`originalRequest` 缺失时来源提示显示明确占位,不失败。
## 测试策略
先补失败断言,再实现:
1. 共享定义send schema 必填 replyMode、enum 正确、旧工具不公开。
2. `one_way`:目标提示标注不自动回传,不创建 requestId。
3. `return_and_continue`:创建 requestId目标提示禁止手工回传来源收到结果并自动续跑。
4. 来源续跑:包含 requestId、目标信息、原始请求、完整性判断指令。
5. 兼容别名:内部 `ccweb_request_reply` 仍建立回传。
6. 隐藏入口:正式 MCP tools/call 与旧 dynamic call 接受旧名tools/list 不包含旧名。
7. 双路径一致:正式 MCP 和 dynamic tools 的 send definition 深度一致namespace 除外)。

View File

@@ -0,0 +1,98 @@
# PRD统一 ccweb 消息回传接口
## 背景
cc-web 当前同时公开 `ccweb_send_message``ccweb_request_reply`。两者输入完全
相同,后者在内部只是为发送函数开启 `expectReply`。真实会话已出现:任务文字明确
要求“完成后汇报/最终验收”,模型仍选择 `ccweb_send_message`,导致来源对话拿不到
自动回传。工具描述、Codex App 备用定义、目标运行提示和来源续跑提示也存在语义缺口。
## 目标
向新模型只公开一个 `ccweb_send_message`,用必填 `replyMode` 明确选择:
- `one_way`:单向投递,目标本轮输出只留在目标对话。
- `return_and_continue`:建立异步回传请求;目标完成后把结果写回来源,并触发来源续跑。
调用始终立即返回,任何模式都不表达同步阻塞等待。
## 功能要求
### 1. 公开工具契约
- `ccweb_send_message` 的输入必须包含:
- `targetConversationId: string`
- `content: string`
- `replyMode: "one_way" | "return_and_continue"`
- JSON Schema 必须把 `replyMode` 列入 `required`,且不得为新调用提供默认值。
- 工具描述必须包含强选择规则:
- 仅当来源不需要目标结果时使用 `one_way`
- 分析、实现、测试、验收、完成后汇报或来源后续依赖结果时,必须使用
`return_and_continue`
- `ccweb_request_reply` 不再出现在正式 MCP 工具列表或 Codex App 备用工具列表中。
### 2. 兼容策略
- 内部 dispatcher 继续接受旧工具名 `ccweb_request_reply`,语义固定映射为
`return_and_continue`,保护已经加载旧工具定义的存量线程。
- 正式 MCP 的 `tools/call` 与 Codex App 旧 dynamic call 必须继续接受该隐藏旧名;
它只从 `tools/list`/新 dynamic tools 中移除,不能在 dispatcher 前被 allowlist 拒绝。
- 内部 dispatcher 可继续把缺少 `replyMode` 的旧 `ccweb_send_message` 调用按
`one_way` 处理;新公开 Schema 仍必须强制该字段,以避免破坏存量线程。
- 现有返回字段保持兼容;自动回传模式继续返回 `requestId``status: waiting`
`replyDelivery``sourceAutoRun`
### 3. 单一定义来源
-`lib/ccweb-mcp-server.js` 导出的正式工具定义作为单一来源:
- 集中定义并导出 reply mode 常量;
- 在正式 `TOOLS` 中定义公开 `ccweb_send_message` 的 description 与 inputSchema。
- `server.js` 已导入 `CCWEB_MCP_TOOLS`Codex App 备用工具列表必须从该数组按通信
工具白名单筛选并添加 namespace不能继续复制文案/Schema。
- 筛选不得把 task-board、图片显示或用户表单等非备用通信工具意外加入 dynamic tools。
### 4. 目标对话运行提示
- `one_way` 模式必须明确:本轮输出不会自动回传来源。
- `return_and_continue` 模式必须明确:
- 系统会自动回传本轮最终输出;
- 目标应在真正完成或明确阻塞后给出完整交付;
- 不要为回复本请求而手工调用跨对话发送工具,避免重复。
- 目标提示继续包含来源标题、来源 ID 和原消息内容。
### 5. 来源续跑提示
- pending reply 状态需要保留原始请求文本,兼容旧状态缺少该字段。
- 自动续跑运行文本至少包含:
- `requestId`
- 目标对话标题/ID
- 原始请求
- 返回正文
- 提示必须要求来源先判断返回是否完整满足原始请求;不能把“已返回”直接当作“已完成”。
### 6. 文档
- README 只把 `ccweb_send_message` 列为公开发送工具,并说明两个 `replyMode`
- README 可说明旧 `ccweb_request_reply` 仅为内部兼容别名,但不得继续推荐新调用使用。
## 非目标
- 不实现同步阻塞等待。
- 不新增第三种回传模式。
- 不重构跨对话 pending reply 的整体持久化机制。
- 不在本任务中解决所有“turn 完成是否等于任务完成”的协议问题;只通过提示与来源完整性检查降低误判。
- 不修改任务看板、失败插入卡片等并发任务的功能。
- 不重启生产 `ccweb` 服务。
## 验收标准
1. 新工具清单中存在一个带必填 `replyMode``ccweb_send_message`,不存在公开
`ccweb_request_reply`
2. 正式 MCP 与 Codex App 备用定义引用同一个共享契约。
3. 两种新模式均有端到端回归;自动回传行为与现状兼容。
4.`ccweb_request_reply` 通过隐藏兼容入口MCP tools/call、旧 dynamic call、
内部 dispatcher仍成功但不出现在公开列表。
5. 目标提示和来源续跑提示具有上述模式与关联信息。
6. README 与实现一致。
7. 语法检查、相关专项测试、完整回归和 `git diff --check` 通过。
8. 实施不得覆盖任务开始前已有的并发改动。

View File

@@ -0,0 +1,25 @@
# 并发改动基线
## 当前并发任务
- 任务看板:占用全局 `.planning/.active_plan``.trellis/.current-task`,仍在运行。
- 失败插入卡片:有 `server.js`/前端/回归改动,当前对话显示运行中或刚完成。
- `ccweb_list_conversations` children scope已完成但改动仍未提交。
- 本任务:只允许精确修改跨对话 MCP、提示、README 与相关回归,不覆盖上述改动。
## 高冲突文件与安全范围
| 文件 | 本任务安全范围 | 必须保留 |
|---|---|---|
| `server.js` | 5733-5777 提示、5796-6103 跨对话发送/回传、10221-10410 dynamic tools、dispatcher 对应 case | task-board lifecycle、children scope、failed-insert rollback |
| `lib/ccweb-mcp-server.js` | send/request 工具定义与导出常量 | children scope schema、task-board spread、其他 MCP 工具 |
| `scripts/regression.js` | 6392-6524、7349 附近跨对话断言及工具列表断言 | task-board、失败插入和 children scope 回归 |
| `README.md` | MCP 工具表与跨对话说明 | children scope 文档与其他功能说明 |
## 实施纪律
1. 修改前重新查看目标函数当前文本,避免使用旧行号/旧上下文套补丁。
2. 不做全文件格式化或 import 重排。
3. 每个阶段检查 `git diff`,只确认本任务新增的 hunk。
4. 不重启生产服务;验证使用隔离测试进程。

View File

@@ -0,0 +1,25 @@
# 当前行为与缺口
## 当前实现
- `requestCrossConversationReply()``sendCrossConversationMessage()``expectReply`
包装,因此公开工具合并不需要新增通信机制。
- 正式 MCP 与 Codex App 备用 dynamic tools 各自复制 description 和 inputSchema。
- `server.js` 已经导入正式 `CCWEB_MCP_TOOLS`,可以直接按白名单派生备用定义,
无需增加新的契约模块。
- pending reply 已持久化 requestId、来源/目标、hopCount 和 replyText可向后兼容地新增
`originalRequest`
- 来源忙碌时 ready reply 会保留,空闲后由 flush 逻辑投递;该行为必须保持。
## 已观察失败
- 模型在明确要求“完成后汇报/最终验收”的任务上调用了单向发送工具。
- 同一模型此前使用过自动回传工具,说明仅靠两个工具名称仍不能稳定表达选择规则。
- 某次自动回传正文只是“完成定位、准备修改”,目标后来继续执行;来源续跑需要明确检查完整性。
## 设计结论
- 工具选择必须从“选两个近似工具”改为“调用一个工具时填写必填枚举”。
- 新公开契约不提供默认值;内部兼容层可宽容旧调用。
- 目标与来源的运行提示都必须携带通信模式和关联上下文。
- 提示优化无法完全替代任务完成协议;本任务不扩大到生命周期重构。

View File

@@ -0,0 +1,361 @@
# ccweb 消息回传统一影响面映射
## 结论
本任务的核心影响面集中在 3 条链路:
1. 公开工具契约:`lib/ccweb-mcp-server.js``TOOLS` 当前同时公开
`ccweb_send_message``ccweb_request_reply``server.js` 的 Codex App
fallback dynamic tools 又复制了一份定义。
2. 内部兼容分发:`server.js``callInternalMcpTool()` 已经把两个工具名分别
分发到 `sendCrossConversationMessage()``requestCrossConversationReply()`
旧别名兼容应保留在这里。
3. 自动回传状态机:`sendCrossConversationMessage()` 通过 `expectReply` 创建
pending reply目标完成后由 `completeCrossConversationReply()``deliverCrossConversationReply()`
写回来源并触发来源自动续跑。
`ccweb_request_reply` 不能简单从 `TOOLS` 删除后收工,因为当前正式 MCP 的
`tools/call` 白名单也复用 `TOOLS`。如果公开列表不再包含旧名,就必须把
“公开 tools/list” 与 “tools/call 内部兼容旧名” 分离。
## 代码索引状态
- `codebase-memory-mcp` 项目:`home-cc-web`
- 索引状态:`ready`
- 代码图节点/边:`5941` / `13063`
## 文件影响清单
| 文件 | 当前职责 | 本任务影响 |
|---|---|---|
| `lib/ccweb-mcp-server.js` | 正式 MCP stdio 服务器、正式 `TOOLS` 定义、`tools/list` 和 stdio `tools/call` 白名单 | 新增/调整共享契约导出;`ccweb_send_message` 必填 `replyMode`;公开列表移除 `ccweb_request_reply`stdio `tools/call` 仍需兼容旧名 |
| `server.js` | 导入 `CCWEB_MCP_TOOLS`、共享 HTTP MCP、内部 dispatcher、跨对话发送/回传、Codex App fallback dynamic tools、pending 持久化 | 复用共享定义;`ccweb_send_message``replyMode` 归一到 `expectReply`;旧 `ccweb_request_reply` 兼容;目标/来源提示增加模式和原始请求pending 状态新增 `originalRequest` |
| `README.md` | MCP 工具用户文档 | 公开工具表只推荐 `ccweb_send_message`,说明 `replyMode=one_way/return_and_continue`;旧 `ccweb_request_reply` 只标内部兼容别名 |
| `scripts/regression.js` | 端到端回归覆盖 MCP 发送、回传、busy source 队列、Codex App running target | 现有 `ccweb_request_reply` 测试改为兼容测试;新增 `replyMode` 双模式、公开列表、正式 MCP/dynamic tools 定义一致性、提示内容断言 |
| `public/app.js` | 跨对话 reply 展示、折叠状态、ready count 前端消费 | 本任务通常不改;但回归会间接受到 returned reply metadata 影响 |
## 正式 MCP 定义链路
### 当前位置
- `lib/ccweb-mcp-server.js:15` 定义 `const TOOLS = [...]`
- `lib/ccweb-mcp-server.js:110` 定义公开 `ccweb_send_message`
- `lib/ccweb-mcp-server.js:159` 定义公开 `ccweb_request_reply`
- `lib/ccweb-mcp-server.js:445` 在 stdio `tools/list` 返回 `{ tools: TOOLS }`
- `lib/ccweb-mcp-server.js:450` 在 stdio `tools/call``TOOLS.some(...)` 做白名单。
- `lib/ccweb-mcp-server.js:503` 导出 `{ TOOLS, prepareImagePayload, runStdioServer }`
### 需要调整
- 新建或拆出无副作用共享契约:
- reply mode 常量:`one_way``return_and_continue`
- 校验集合或归一函数。
- `ccweb_send_message` 的 description 与 inputSchema。
- `ccweb_send_message.inputSchema.required` 必须包含:
- `targetConversationId`
- `content`
- `replyMode`
- `replyMode` schema 必须是 enum
- `one_way`
- `return_and_continue`
- `ccweb_request_reply` 不再出现在正式 `tools/list`
- `tools/call` 兼容旧名时不要再依赖公开 `TOOLS` 作为唯一白名单。可选策略:
- `PUBLIC_TOOLS` 用于 `tools/list`
- `CALLABLE_TOOL_NAMES``isCallableMcpTool(name)` 用于 `tools/call`,包含旧
`ccweb_request_reply`
## Codex App fallback dynamic tools 链路
### 当前位置
- `server.js:15` 导入 `TOOLS: CCWEB_MCP_TOOLS`
- `server.js:10221` 定义 `codexAppCommunicationDynamicTools()`
- `server.js:10273` 复制定义 `ccweb_send_message`,当前无 `replyMode`
- `server.js:10358` 复制定义 `ccweb_request_reply`
- `server.js:10391` 定义 `handleCodexAppDynamicToolCall()`
- `server.js:10395-10402` Codex App dynamic tool 白名单仍包含 `ccweb_request_reply`
- `server.js:10407` 调用 `callInternalMcpTool()`
### 需要调整
- `codexAppCommunicationDynamicTools()` 不应再复制 send 的 description/schema。
- 建议从共享正式定义派生 Codex App fallback 定义:
- 保留 `namespace: 'ccweb'`
- 复用同一 `description``inputSchema`
- 不包含 `ccweb_request_reply`
- `handleCodexAppDynamicToolCall()` 要区分“公开 fallback tool”与“旧线程动态调用兼容”
- 新公开列表不含 `ccweb_request_reply`
- 已加载旧 dynamic tool 的线程调用 `ccweb_request_reply` 时,内部仍可接受并映射到
`return_and_continue`
## Composer MCP 候选链路
### 当前位置
- `server.js:2536` 定义 `listComposerMcpItems()`
- `server.js:2555` 遍历 `CCWEB_MCP_TOOLS` 生成 `/` 里的 `mcp:ccweb/<tool>` 候选。
### 需要调整
- 如果 `CCWEB_MCP_TOOLS` 改为公开 tools/list则 Composer 会自动不再展示
`mcp:ccweb/ccweb_request_reply`
- 必须确认 `/` 候选里 `mcp:ccweb/ccweb_send_message` 的描述与 schema 语义一致。
- 不要从内部兼容白名单反推 Composer 候选,否则会重新暴露旧工具。
## 内部 dispatcher 链路
### 当前位置
- `server.js:6084` 定义 `callInternalMcpTool(tool, args, sourceSessionId, sourceHopCount)`
- `server.js:6094-6095``ccweb_send_message` 直接调用
`sendCrossConversationMessage(args, sourceSessionId, sourceHopCount)`
- `server.js:6100-6101``ccweb_request_reply` 调用
`requestCrossConversationReply(args, sourceSessionId, sourceHopCount)`
- `server.js:5915-5917``requestCrossConversationReply()` 只是
`sendCrossConversationMessage(..., { expectReply: true })`
### 需要调整
- `callInternalMcpTool()` 是兼容旧工具名的最佳位置。
- 新逻辑建议:
- `ccweb_request_reply` 固定归一为 `replyMode=return_and_continue`
- `ccweb_send_message` 读取 `args.replyMode`
- 缺少 `replyMode` 的旧 `ccweb_send_message` 内部按 `one_way` 处理。
- 非法 `replyMode` 返回 `mcpToolError('invalid_reply_mode', ...)`
- 保持返回字段兼容:
- `one_way` 返回现有 `messageId``deliveryStatus` 等。
- `return_and_continue` 继续返回 `requestId``status: waiting`
`replyDelivery``sourceAutoRun`
## 正式 MCP HTTP 与 stdio 入口
### 共享 HTTP MCP
- `server.js:6145` 定义 `handleMcpJsonRpcMessage()`
- `server.js:6161``tools/list` 返回 `CCWEB_MCP_TOOLS`
- `server.js:6165``tools/call``CCWEB_MCP_TOOLS.some(...)` 做白名单。
- `server.js:6168-6174` 调用 `callInternalMcpTool()`
- `server.js:6188-6227` 定义 `handleSharedMcpHttpApi()`,从 URL query 读取
`sourceSessionId``sourceHopCount` 后调用 `handleMcpJsonRpcMessage()`
### 旧内部 HTTP API
- `server.js:6230` 定义 `handleInternalMcpApi()`
- `server.js:6243-6247` 从 body 读取 `tool/args/sourceSessionId/sourceHopCount`
并直接调用 `callInternalMcpTool()`
- `scripts/regression.js` 当前大量通过该内部 API 调用工具。
### stdio MCP 桥
- `lib/ccweb-mcp-server.js:327` 定义 `callCcweb(tool, args)`
- `lib/ccweb-mcp-server.js:352-357``tool/args/sourceSessionId/sourceHopCount`
POST 到内部 API。
- `lib/ccweb-mcp-server.js:425` 定义 stdio `handleRequest()`
- `lib/ccweb-mcp-server.js:445` 返回 `TOOLS`
- `lib/ccweb-mcp-server.js:450` 当前用 `TOOLS` 做 stdio call 白名单。
### 关键风险
正式 `tools/list` 删除旧工具后:
- `handleMcpJsonRpcMessage()``tools/call` 会拒绝旧 `ccweb_request_reply`
- stdio `handleRequest()``tools/call` 也会拒绝旧 `ccweb_request_reply`
因此兼容旧工具名必须覆盖两个 `tools/call` 白名单,而不是只改
`callInternalMcpTool()`
## 发送与回传状态机
### 单向发送 / 创建 pending
- `server.js:5808` 定义 `sendCrossConversationMessage()`
- `server.js:5814-5815``options.expectReply` 推导 `expectReply/sourceAutoRun`
- `server.js:5849` `expectReply` 为真时生成 `requestId`
- `server.js:5857-5874` 写入目标 user message 的 `crossConversation` metadata
并通过 `setPendingCrossConversationReply()` 创建 pending。
- `server.js:5883-5887` 调用 `handleMessage()`,目标 runtimeText 来自
`buildCrossConversationRuntimeText()`
- `server.js:5906-5911` 仅在 `requestId` 存在时返回 `status/replyDelivery/sourceAutoRun`
### pending 持久化
- `server.js:222` pending 文件是 `config/cross-conversation-replies.json`
- `server.js:4376-4402` `normalizeCrossConversationReplyState()` 归一状态,当前字段包括:
`requestId/messageId/sourceConversationId/sourceTitle/targetConversationId/targetTitle/status/createdAt/hopCount/sourceAutoRun/replyText/completedAt/returnedAt/replyMessageId/lastError`
- `server.js:4419-4422` `saveCrossConversationReplies()` 写入文件。
- `server.js:4430-4439` `loadCrossConversationReplies()` 启动加载未 returned 的 pending。
- `server.js:4447-4452` `setPendingCrossConversationReply()` 创建 pending。
- `server.js:4455-4468` `updatePendingCrossConversationReply()` 更新 pending。
- `server.js:4471-4475` `deletePendingCrossConversationReply()` 删除 pending。
### 需要新增字段
- PRD 要求 pending reply 状态保留原始请求文本,建议字段名使用
`originalRequest`
- 写入点:`sendCrossConversationMessage()` 创建 pending 的对象。
- 归一点:`normalizeCrossConversationReplyState()`,旧状态缺失时填 `''` 或明确占位。
- 输出点:
- `crossConversationReplySummary()`
- `getPendingCrossConversationReply()`
- `buildCrossConversationReplyAutoRunText()`
### 目标完成与来源写回
- `server.js:9257` 普通 Codex 运行 entry 记录 `crossConversationReplyRequestId`
- `server.js:6796-6799` 普通 Codex 完成后调用
`completeCrossConversationReply()`,再 flush 来源 pending。
- `server.js:11158` Codex App 运行 entry 记录 `crossConversationReplyRequestId`
- `server.js:11322-11326` Codex App 完成后调用
`completeCrossConversationReply()`,再 flush 来源 pending。
- `server.js:6060-6081` `completeCrossConversationReply()` 提取目标输出,更新
pending 为 `ready`,随后调用 `deliverCrossConversationReply()`
- `server.js:5969-6048` `deliverCrossConversationReply()`
- 来源不存在或目标不存在时改为 `failed`
- 来源仍 running 时保持 `ready`,稍后 flush。
- 已处理过则标记 returned 并删除 pending。
- 追加 `ccwebDisplayOnly` assistant 消息到来源。
- metadata 写入 `replyToRequestId/processed/autoRun`
- `sourceAutoRun` 为真时调用 `startCrossConversationReplyAutoRun()`
- `server.js:6050-6057` `flushPendingCrossConversationReplies()` 在来源空闲后投递 ready reply。
## 目标运行提示
### 当前位置
- `server.js:5733-5737` `buildCrossConversationRuntimeText(sourceSession, content)`
- 当前只包含来源标题、来源 ID、消息正文。
- `server.js:5886` `sendCrossConversationMessage()` 发送目标运行时使用该提示。
### 需要调整
`buildCrossConversationRuntimeText()` 需要知道模式,建议参数扩展为:
- `sourceSession`
- `content`
- `replyMode``{ replyMode, requestId }`
提示必须覆盖:
- `one_way`:明确本轮输出不会自动回传来源。
- `return_and_continue`
- 系统会自动回传本轮最终输出。
- 目标应在真正完成或明确阻塞后给出完整交付。
- 不要为回复本请求而手工调用跨对话发送工具,避免重复。
- 继续保留来源标题、来源 ID、原消息正文。
## 来源自动续跑提示
### 当前位置
- `server.js:5744-5748` `buildCrossConversationReplyAutoRunText(targetSession, replyText)`
- `server.js:5750-5777` `startCrossConversationReplyAutoRun()` 使用该 runtimeText 触发来源隐藏续跑。
- 当前提示只包含目标标题和返回正文。
### 需要调整
`buildCrossConversationReplyAutoRunText()` 需要额外输入 pending 或关联上下文,至少包含:
- `requestId`
- 目标对话标题/ID
- 原始请求 `originalRequest`
- 返回正文 `replyText`
- 明确要求来源先判断返回是否完整满足原始请求,不能把“已返回”直接当作“已完成”。
建议让 `deliverCrossConversationReply()``pending``{ requestId, originalRequest }`
传给 `startCrossConversationReplyAutoRun()`,再传给
`buildCrossConversationReplyAutoRunText()`
## `ccweb_create_conversation` 间接受影响
### 当前位置
- `server.js:5646` 定义 `createMcpConversation()`
- `server.js:5664` 兼容 `args.requestReply === true || args.waitForReply === true`
- `server.js:5694-5697` 创建后首条消息调用
`sendCrossConversationMessage(..., { expectReply: requestReply })`
- `server.js:5723-5728` 如果有 `requestId`,返回 `replyStatus/replyDelivery/sourceAutoRun`
- `lib/ccweb-mcp-server.js:101` 正式 `ccweb_create_conversation` schema 公开 `requestReply`
- `server.js:10349` Codex App fallback schema 也公开 `requestReply`
### 需要保持
PRD 不要求修改 `ccweb_create_conversation` 的参数语义。实现 `replyMode` 时必须确保:
- `requestReply=true` 仍创建 pending reply。
- 返回字段仍兼容现有断言。
- 目标提示也应按 `return_and_continue` 模式生成,因为底层仍是跨对话发送。
## 前端展示影响
前端不参与工具契约选择,但会消费返回消息 metadata
- `public/app.js:1481-1489` `getCrossConversationReplyCollapseKey()` 使用
`replyToRequestId/messageId` 作为折叠 key。
- `public/app.js:7135-7144` 收到 `session_message` 时,如果是
`replyToRequestId`,会减少 ready reply count。
- `public/app.js:7968-7974` `createMsgElement()``reply/replyToRequestId`
标记跨对话回复气泡。
本任务如果只新增 `originalRequest/replyMode` metadata前端无需改如果改变
`replyToRequestId/processed/ccwebDisplayOnly` 字段,则会影响展示和 ready count。
## 回归测试影响面
### 现有相关断言
- `scripts/regression.js:6348-6386` 覆盖 `ccweb_create_conversation` +
`requestReply=true` 自动回传。
- `scripts/regression.js:6392-6415` 覆盖 `ccweb_send_message` 单向发送与目标 runtime prompt。
- `scripts/regression.js:6417-6434` 覆盖跨对话 hop count。
- `scripts/regression.js:6440-6490` 覆盖旧 `ccweb_request_reply` 自动回传。
- `scripts/regression.js:6503-6578` 覆盖来源 busy 时 ready reply 排队、pending list/detail、
来源空闲后 flush 和 auto-run。
- `scripts/regression.js:7348-7357` 覆盖目标 Codex App running 时拒绝发送。
### 需要新增/调整断言
- `tools/list` 中:
- 存在 `ccweb_send_message`
- `ccweb_send_message.inputSchema.required` 包含 `replyMode`
- `replyMode.enum``['one_way', 'return_and_continue']`
- 不存在公开 `ccweb_request_reply`
- 正式 MCP 与 Codex App fallback
- send definition 共用同一 schema/description。
- Codex App fallback 不公开 `ccweb_request_reply`
- `ccweb_send_message(replyMode='one_way')`
- 不返回 `requestId`
- 不创建 pending。
- 目标 runtime prompt 明确不会自动回传。
- `ccweb_send_message(replyMode='return_and_continue')`
- 返回 `requestId/status/replyDelivery/sourceAutoRun`
- 目标消息 metadata 有 `expectsReply/replyRequestId`
- pending 文件有 `originalRequest`
- 目标 runtime prompt 明确会自动回传并禁止手工重复发送。
- 来源 auto-run prompt 包含 `requestId`、目标标题/ID、原始请求、返回正文、完整性判断指令。
- 兼容:
- 内部 `ccweb_request_reply` 仍成功并等价于 `return_and_continue`
-`ccweb_send_message` 缺少 `replyMode` 仍按 `one_way` 成功。
- stdio/shared HTTP `tools/call` 对旧 `ccweb_request_reply` 仍可执行,即使 `tools/list`
不再公开它。
## 建议实现顺序
1. 抽出共享契约,定义 reply modes 和公开 `ccweb_send_message` definition。
2. 让正式 MCP `tools/list` 和 Codex App fallback 复用共享 send definition。
3. 分离公开工具列表与内部可调用工具白名单,保证旧 `ccweb_request_reply` 可 call 不可 list。
4. 在 dispatcher 或发送函数中归一 `replyMode -> expectReply`
5. pending 增加 `originalRequest`,归一、保存、查询和旧数据兼容。
6. 改目标 runtime prompt 和来源 auto-run prompt。
7. 更新 README。
8. 更新回归断言,覆盖正式 MCP、dynamic fallback、旧兼容和 busy source 队列。
## 主要风险
- 如果只删除 `TOOLS` 中的 `ccweb_request_reply`,旧线程的正式 MCP 调用会被
`tools/call` 白名单挡住,达不到 PRD 的内部兼容要求。
- 如果只改 `lib/ccweb-mcp-server.js`Codex App fallback dynamic tools 仍会暴露旧工具。
- 如果 `replyMode` 在 schema 必填但内部没有兼容缺省,已加载旧 schema 的
`ccweb_send_message` 调用可能被拒绝。
- 如果来源 auto-run prompt 仍只包含“已返回”,模型可能把不完整返回误判为任务完成。
- 如果 pending 删除过早,`get_pending_reply` 对 returned 历史查询会依赖来源消息里的
`replyToRequestId`,因此不能破坏 `processed/replyToRequestId/ccwebDisplayOnly`

View File

@@ -0,0 +1,40 @@
# 测试影响图
## 最小覆盖层次
1. **公开契约**:正式 `TOOLS/tools/list` 与 Codex App 备用列表只公开一个
`ccweb_send_message``replyMode` 必填且 enum 精确。
2. **统一发送 E2E**`one_way` 不建 request`return_and_continue` 建 request、
自动回传、来源忙碌时排队。
3. **隐藏兼容**:旧 `ccweb_request_reply` 不在 tools/list但正式 MCP `tools/call`
Codex App 旧 dynamic call 和内部 dispatcher 仍接受。
4. **提示语义**:目标提示区分模式;来源续跑包含 requestId、目标、原请求和完整性检查。
## 现有可复用入口
- `scripts/regression.js` 已有 `TOOLS` 静态断言、server 源码片段断言和完整跨对话 E2E。
- `callInternalMcp``nextMessage``waitForJsonCondition` 可复用。
- 现有单向发送、request-reply、busy source 三段应改为:
- 公开单向:`ccweb_send_message(replyMode='one_way')`
- 公开回传:`ccweb_send_message(replyMode='return_and_continue')`
- 旧别名:单独保留一条隐藏兼容用例。
## 必须先失败的断言
- `TOOLS` 不包含 `ccweb_request_reply`
- send schema 的 required 包含 `replyMode`enum 为
`['one_way', 'return_and_continue']`
- send description 含“仅当来源不需要结果才 one_way / 依赖结果必须 return”。
- Codex App 备用列表与正式通信定义一致,且无旧公开工具。
- `return_and_continue` 返回 waiting requestId。
- 自动回传目标提示禁止手工重复回传。
- 来源 auto-run 文本包含 requestId、目标 ID、原始请求和完整性判断规则。
- hidden legacy allowlist 仍允许 `ccweb_request_reply`
## 关键风险
- `lib/ccweb-mcp-server.js` 的 JSON-RPC `tools/call` 当前使用公开 `TOOLS` allowlist。
直接删除定义会让已缓存旧工具 schema 的客户端在 dispatcher 前失败;必须增加隐藏兼容 allowlist。
- 只测 Schema 不能证明 dispatcher 使用 replyMode必须保留 E2E。
- 完整 regression 覆盖面很大TDD 循环应优先使用可聚焦的静态/跨对话目标,最终再跑完整回归。

View File

@@ -0,0 +1,26 @@
{
"id": "unify-ccweb-message-reply",
"name": "unify-ccweb-message-reply",
"title": "统一 ccweb 消息回传接口",
"description": "",
"status": "planning",
"dev_type": null,
"scope": null,
"package": null,
"priority": "P2",
"creator": "shiyue",
"assignee": "shiyue",
"createdAt": "2026-08-11",
"completedAt": null,
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "",
"meta": {}
}