@@ -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` 。