# H5 Session 架构改造与开发设计方案 日期: 2026-07-06 状态: Phase 1 已完成;Phase 2 Patch 3/4a/4b/4c 已落地(分支 `0706-bug干净`,待全量 soak 后合并)。 > 旧路径 [h5-session-gouzao0706.md](./h5-session-gouzao0706.md) 保留重定向至本文件。 适用范围: - H5 前端聊天主链路。 - Memind Portal 内的 router、agent-run-gateway、tkmind-proxy、session ownership、SSE stream。 - goosed 作为 agent runtime 的接入边界。 非目标: - 不重写 H5 UI。 - 不重写 goosed。 - 不引入新的大型框架或新平台。 - 不一次性重做完整 replay/event sourcing。 - 不改变当前生产发布规则和回归守卫。 - 不重复实施 [Memind 2.0 Streaming Agent Runtime 计划](./architecture/memind-2-streaming-agent-runtime-plan.md) 中已落地的 worker/queue/SLO 能力。 ## 0. 结论 本次改造的本质不是重构系统,而是把当前 H5 + Portal + goosed 链路从: ```text socket/runtime 牵引的会话执行链路 ``` 收敛为: ```text Portal control plane + goosed execution runtime ``` 最重要的架构转折点: ```text session 不再 socket 化。 ``` Session 必须由 Portal 侧的 store/broker 管理,WebSocket/SSE/HTTP 都只能是 transport,不能成为 session state 的事实源。 当前系统已经具备较好的基础: - H5 已经主要通过 `POST /agent/runs` 提交聊天/agent 请求。 - 旧的 `POST /sessions/:id/reply` 已经被拦截为 `410 AGENT_RUNS_REQUIRED`。 - `h5_user_sessions` 已经保存 session ownership、origin、`goosed_node`、`goosed_target`。 - `h5_agent_runs` 和 `h5_agent_run_events` 已经承担 run lifecycle 和基础事件记录。 - H5 session event stream 已经走 SSE,并且前端已有 `Last-Event-ID` 传递。 因此第一阶段不需要“建一个新系统”,而是抽一层很薄的 Session Broker,把已有能力从 `user-auth.mjs`、`tkmind-proxy.mjs`、`server.mjs`、`agent-run-gateway.mjs` 及若干旁路入口的散落调用中收口。 ### 0.1 与相关文档的关系 | 文档 | 关系 | |---|---| | [Memind 2.0 Streaming Agent Runtime 计划](./architecture/memind-2-streaming-agent-runtime-plan.md) | 已落地的 streaming/runtime、worker queue、SLO、tool gateway 等;**本文在其之上做 session 控制面收口,不重复 P6–P8 内容** | | [Memind / MindSpace / Goose 解耦推进方案](./memind-control-execution-split-plan-20260702.md) | 更广的 Control/Execution + MindSpace 服务化;本文聚焦 H5 session ownership,不涉及 MindSpace 独立部署 | | [MindSpace 发布与聊天 Finish 回归守卫](./regression-guards/mindspace-publish-and-chat-finish.md) | Patch 4 及 stream finish 相关改动**必须遵守** | | [103 Runtime Topology](./103-runtime-topology.md) | 生产 goosed target 拓扑参考;broker 的 `goosed_target` pinning 与之对齐 | ## 1. 当前结构与主要问题 ### 1.1 当前实际链路 ```mermaid flowchart LR H5["H5 useTKMindChat"] --> API["src/api/client.ts"] API --> RUN["POST /agent/runs"] RUN --> GW["agent-run-gateway.mjs"] GW --> ROUTER["chat-intent-router.mjs"] GW --> DIRECT["direct-chat-service.mjs"] GW --> PROXY["tkmind-proxy.mjs"] PROXY --> GOOSE["goosed runtime"] H5 --> RUNSSE["GET /agent/runs/:runId/events"] H5 --> SESSSE["GET /sessions/:sessionId/events"] RUNSSE --> GW SESSSE --> PROXY PROXY --> GOOSE ``` 这个链路比早期状态更健康: H5 已经不是直接把普通聊天发到 goosed reply path。 但仍有三个核心问题: 1. session lifecycle、ownership、target mapping 仍散落在多个模块里(见 §5.6 调用点清单)。 2. gateway 同时处理 run lifecycle、router、direct chat、tool gateway、goosed session 创建,边界偏厚。 3. SSE 已经存在,但事件语义还没有统一成稳定协议,尤其是 run stream 与 session stream 的终态/恢复语义不完全一致(见 §1.3)。 ### 1.2 当前已有资产 当前不应重复建设的能力: - `user-auth.mjs` - `registerAgentSession(userId, agentSessionId, goosedTarget)` - `getSessionTarget(agentSessionId)` - `ownsSession(userId, agentSessionId)` - `unregisterAgentSession(userId, agentSessionId)` - `db.mjs` - 自动补齐 `h5_user_sessions.goosed_node` - 自动补齐 `h5_user_sessions.goosed_target` - 自动补齐 `h5_user_sessions.origin` - 注意: `schema.sql` 中 `h5_user_sessions` 基表可能滞后于 runtime migration;**以 `db.mjs` 启动迁移为准**,Patch 1 实施时建议同步更新 `schema.sql` 注释或列定义。 - `tkmind-proxy.mjs` - goosed target 选择与 session target 解析。 - `/sessions/:id/events` SSE proxy。 - stream started/ended metrics。 - `reconcileAgentSession` 调用(resume 路径)。 - `session-snapshot` 相关(`sessionSnapshotService`) - direct chat / portal snapshot 缓存。 - `GET /sessions/:id` 在 hint 匹配时的 DB snapshot 快路径。 - gateway 执行前 transcript persist。 - **不是** ownership 事实源;与 broker 并列存在(见 §5.6)。 - `agent-run-gateway.mjs` - `h5_agent_runs` row。 - `h5_agent_run_events` 基础事件。 - queued/running/retryable/succeeded/failed 状态。 - worker heartbeat 和 stale run recovery。 这些能力应该被收口和命名,不应被复制。 ### 1.3 H5 双 SSE 通道现状 H5(`useTKMindChat.ts` + `src/api/client.ts`)在 agent 路径上**同时**订阅两条 SSE/事件通道: | 通道 | 端点 | 职责 | 终态信号 | |---|---|---|---| | Run stream | `GET /agent/runs/:runId/events` | run 排队/执行状态、`sessionId` 回填、direct chat 触发 snapshot 轮询 | `h5_agent_runs.status` → `succeeded` / `failed` | | Session stream | `GET /sessions/:sessionId/events` | assistant token、tool call、finish 等 goosed 运行时事件 | goosed `finish` / `error` 等 terminal event | 典型时序: ```text POST /agent/runs → subscribeAgentRunEvents(runId) # 等待 run 终态、拿 sessionId → subscribeSessionEvents(sessionId) # 接收流式内容(非 direct chat) → finish 后 syncSessionMessages # merge,禁止盲覆盖 ``` 这解释了 §1.1 第三点: **run 终态与 session 终态不是同一事件**,direct chat 甚至主要依赖 run 成功 + snapshot poll,而非 session SSE。 Patch 4 必须明确: - **run stream**: 保持现有 DB 驱动 polling/SSE,第一阶段不做 replay。 - **session stream**: 固化 delta/control/terminal 语义、`Last-Event-ID`、finish 后 idle/sync。 - **禁止** 在 Patch 4 中合并两条通道或改变 H5 订阅顺序。 ## 2. 目标架构 ```mermaid flowchart LR H5["H5"] --> PORTAL["Portal Control Plane"] PORTAL --> ROUTER["Router\n decision only"] PORTAL --> BROKER["Session Broker\n ownership + mapping"] PORTAL --> GATEWAY["Agent Gateway\n run lifecycle"] PORTAL --> STREAM["SSE Stream Module\n logical, in tkmind-proxy"] PORTAL --> SNAP["Session Snapshot\n conversation cache"] GATEWAY --> PROXY["goosed Proxy Adapter"] STREAM --> PROXY PROXY --> GOOSE["goosed\n execution runtime"] BROKER --> DB["MySQL\n h5_user_sessions"] GATEWAY --> RUNDB["MySQL\n h5_agent_runs/events"] SNAP --> SNAPSTORE["Redis / in-memory\n not ownership"] ``` 说明: - **SSE Stream Module** 是逻辑模块,当前与 `tkmind-proxy.mjs`、`server.mjs` 路由同进程部署,**不是**独立服务。 - **Session Snapshot** 缓存 conversation,与 Broker 分工不同,不可被 broker 吸收。 边界原则: | 层 | 可以做 | 禁止做 | |---|---|---| | Router | 判断 `chat/agent`、`reuse/new`、`sse/ws` | 执行工具、写 memory、创建 session | | Session Broker | ownership、target mapping、origin/timestamps | message history、tool state、execution state、memory | | Session Snapshot | conversation 缓存、merge 输入、direct chat snapshot | ownership、goosed target、计费 | | Agent Gateway | run lifecycle、排队、heartbeat、调用 broker/proxy | 直接持有 session mapping、绕过 proxy 打 goosed | | Proxy Adapter | goosed HTTP/SSE transport、headers、abort、backpressure、`reconcileAgentSession` | 用户业务决策、router 决策 | | goosed | runtime session、tool execution、event emission | H5 用户归属、计费、memory policy、业务路由 | ## 3. 协议分层 ### 3.1 主链路 ```text H5 -> Portal POST /agent/runs H5 <- Portal GET /agent/runs/:runId/events GET /sessions/:sessionId/events Portal -> goosed POST /agent/start POST /sessions/:sessionId/reply GET /sessions/:sessionId/events GET /sessions/:sessionId ``` 说明: - H5 不直接调用 goosed。 - H5 普通聊天不走 WebSocket。 - WebSocket 只允许用于真正双向交互场景,例如 terminal、远程 shell、实时协作,不作为 H5 chat/session state 的事实源。 - SSE 是 H5 chat/agent stream 的主通道。 ### 3.2 SSE 事件分类 第一阶段只做协议语义固化,不做完整 event sourcing。 统一把事件归为三类: | 类别 | 含义 | 例子 | |---|---|---| | `delta` | 内容增量 | assistant token、message chunk | | `control` | 状态变化 | active request、tool call、tool confirmation、balance、heartbeat | | `terminal` | 结束态 | finish、failed、canceled、error | 注意: - `h5_agent_runs.status` 第一阶段不改枚举,仍保持 `queued/running/retryable/succeeded/failed`。 - `streaming/tool_call/completed/canceled` 先作为 event 语义,不直接写入 DB status。 - `Last-Event-ID` 在 session stream 上继续保留;run stream 的 replay 后续再做,不在第一刀完成。 - **v0 策略**: taxonomy 先在 Portal 侧文档化 + 单测断言映射关系,**不改变** goosed 原始 event name 的 wire format;H5 仍按现有 event type 解析,仅在注释/测试层标注 taxonomy。 ### 3.3 SSE event type → taxonomy 映射(v0 参考) 以下为 Patch 4 测试与文档用的**语义映射**,不要求 goosed 改协议: | 来源 | 典型 event / 信号 | taxonomy | 备注 | |---|---|---|---| | session SSE | message / content chunk | `delta` | assistant token 流 | | session SSE | tool call / confirmation / balance | `control` | 不改变 active request 终态 | | session SSE | finish | `terminal` | 必须触发前端 idle + `syncSessionMessages` merge | | session SSE | error | `terminal` | headers sent 后走 SSE frame,不走 JSON | | run SSE | status: running | `control` | DB 驱动 | | run SSE | status: succeeded / failed | `terminal` | 关闭 run 订阅;agent 路径再依赖 session finish | ## 4. Router 改造设计 ### 4.1 目标 Router 从单纯 classifier 收敛为轻量 decision output,但不升级成复杂 decision engine。 ### 4.2 输出结构 新增统一输出字段: ```json { "route": "chat", "mode": "sse", "session_hint": "reuse", "flags": [] } ``` 字段定义: | 字段 | 允许值 | 含义 | |---|---|---| | `route` | `chat` / `agent` | 走直接聊天还是 agent 执行 | | `mode` | `sse` / `ws` | 事件传输协议建议;H5 chat **固定消费 sse** | | `session_hint` | `reuse` / `new` | 倾向复用当前 session 还是新建 | | `flags` | string array | 受控扩展位,只放风险/能力标签,不放执行计划 | 兼容要求: - 旧值 `direct_chat` 等价于 `chat`。 - 旧值 `agent_orchestration` 等价于 `agent`。 - 旧字段 `suggestedSkill`、`agentBrief` 第一阶段可以继续保留给现有逻辑使用,但不能扩展成多步 plan。 Router 可以读取上下文和 memory summary 做判断,但禁止: - 创建 session。 - 写 memory。 - 执行 tool。 - 选择具体 goosed target。 - 输出多步执行计划。 ### 4.3 字段消费方与语义 | 字段 | 消费者 | 第一阶段行为 | |---|---|---| | `route` | `agent-run-gateway.mjs` | 与现有 `direct_chat` / `agent_orchestration` 等价映射 | | `mode` | 暂无 H5 消费 | H5 chat 始终 SSE;`ws` 仅预留给 terminal/shell 等非 chat 场景,router 对 H5 chat **默认输出 `sse`** | | `session_hint` | `agent-run-gateway.mjs` | **建议性** hint,不 override 硬规则(见下) | | `flags` | gateway / 日志 | 能力标签,不触发执行 | `session_hint` 与 gateway 硬规则(hint 不能 override): ```text session_id 为空 → 必须新建 session(无论 hint) session_id 无效/无 ownership → 403 或新建(沿用现有 gateway 行为) direct chat 升级 deep reasoning → 清空 sessionId,强制 goosed start(hint reuse 失效) force_deep_reasoning flag → 同上 tool gateway / code run → 可能无 goosed session(agent_session_id NULL),broker 不适用 isDirectChatSessionId → 不走 goosed session stream ``` 第一阶段 **H5 不读取** router normalized 字段;仅 gateway 内部使用,便于后续扩展。 ### 4.4 flags 约束 允许的 flags 示例: ```text memory_recall selected_skill force_deep_reasoning code_task long_running ``` 不允许的 flags: ```text run_tool_x write_file_y use_worker_2 bill_as_x inject_memory_y ``` ## 5. Session Broker 设计 ### 5.1 目标 Session Broker 是一个薄 facade,不是第二套 session store。 它只负责: - session ownership。 - session origin。 - session 与 goosed target 的 mapping。 - session lifecycle timestamp。 - session 删除/解绑。 它不负责: - message history。 - tool state。 - execution state。 - memory。 - SSE replay。 - billing。 - provider selection。 ### 5.2 与 Session Snapshot、session-reconcile 的边界 | 模块 | 职责 | 与 Broker 关系 | |---|---|---| | **Session Broker** | `h5_user_sessions` ownership + target | 事实源 | | **Session Snapshot** | conversation 缓存、direct chat snapshot | broker 不管内容;snapshot **不得**替代 ownership 校验 | | **session-reconcile.mjs** | goosed session resume 时 working_dir 等字段对齐 | 由 **proxy adapter** 在 transport 层调用;broker 不封装 reconcile 逻辑 | 回归约束: `GET /sessions/:id` snapshot 缓存仅在 `hint_mc` **且** `hint_ua` 均匹配时命中(见 regression guard)。 ### 5.3 建议文件 ```text session-broker.mjs session-broker.test.mjs ``` ### 5.4 API v0 ```js export function createSessionBroker({ userAuth, tkmindProxy = null }) { return { async validateOwnership(userId, sessionId) {}, async registerSession({ userId, sessionId, target, origin = 'h5' }) {}, async unregisterSession({ userId, sessionId }) {}, async resolveSession(sessionId) {}, async resolveSessionTarget(sessionId) {}, }; } ``` 第一阶段不建议让 broker 直接承接所有 goosed start 行为。更稳妥的顺序是: 1. v0 只包现有 `userAuth` session 函数。 2. `tkmind-proxy.mjs` 内部改为通过 broker 记录/读取 target。 3. `agent-run-gateway.mjs` 再通过 broker 做 ownership/session resolution。 4. 旁路入口(§5.6)逐步改走 broker。 ### 5.5 direct chat 兼容 当前 direct chat session 使用 `h5direct_` 前缀,并通过: ```text registerAgentSession(userId, activeSessionId, 'h5-direct') ``` 写入 session 表。 Broker 必须把 `h5-direct` 当成合法 target/origin 信息处理,不能强行要求所有 session 都对应 goosed URL。 ### 5.6 WeChat 兼容 `h5_user_sessions.origin` 已有 `h5/wechat` 语义。Broker v0 必须保留 origin,不要把 WeChat dedicated session 当成普通 H5 session 清理或旋转。 ### 5.7 Session ownership 调用点清单(Patch 2 必须覆盖) 以下模块当前直接调用 `userAuth.registerAgentSession` / `ownsSession` / `getSessionTarget`,Patch 2 需全部纳入 broker 收口计划: | 模块 | 典型调用 | Patch 批次 | |---|---|---| | `tkmind-proxy.mjs` | register / getSessionTarget / ownsSession | 2a | | `agent-run-routes.mjs` | ownsSession(POST /agent/runs) | 2a | | `agent-run-gateway.mjs` | 间接 via tkmindProxy start | 2b | | `direct-chat-service.mjs` | register `h5-direct` | 2b | | `server.mjs` | 多处 ownsSession(sessions CRUD、snapshot、delete 等) | 2c | | `wechat-mp.mjs` | registerAgentSession | 2c | | `mindspace-page-edit-session.mjs` | ownsSession / registerAgentSession | 2c | | `mindspace-agent-runner.mjs` | registerAgentSession | 2c | | `mindspace-conversation-package-routes.mjs` | ownsSession | 2c | **Patch 2 完成定义(量化)**: ```bash # 除 session-broker.mjs 外,不应再直接调用 userAuth.registerAgentSession rg 'userAuth\.registerAgentSession' --glob '*.mjs' | rg -v 'session-broker\.mjs|user-auth\.mjs|\.test\.mjs' # ownsSession 同理(测试 mock 除外) rg 'userAuth\.ownsSession' --glob '*.mjs' | rg -v 'session-broker\.mjs|user-auth\.mjs|\.test\.mjs' ``` broker 未覆盖 tool gateway / code run 的 **无 session** 路径: 这类 run 的 `agent_session_id` 可为 NULL,不经过 broker。 ## 6. Agent Gateway 改造设计 ### 6.1 当前边界 当前 `agent-run-gateway.mjs` 实际承担: - run 创建。 - request idempotency。 - router 调用。 - direct chat 分支。 - tool gateway 分支。 - goosed session 创建。 - submit reply。 - heartbeat。 - stale recovery。 因此第一阶段不能按理想终态一次性“去 orchestration 化”,否则改动会明显变大。 ### 6.2 第一阶段只抽 session 相关逻辑 第一阶段改造目标: - 保留 run queue、heartbeat、stale recovery。 - 保留 direct chat 和 tool gateway 分支。 - 把 session ownership、session target mapping、session register/unregister 的调用收口到 broker。 建议变化: ```text agent-run-gateway before: direct userAuth/tkmindProxy session calls after: sessionBroker + tkmindProxy adapter ``` 不要在第一阶段做: - 删除 direct chat 分支。 - 重写 retry policy。 - 重写 stale recovery。 - 重写 run status enum。 - 重写 SSE run event handler。 ### 6.3 Gateway 最终边界 长期目标: ```text Gateway = run lifecycle Broker = session lifecycle Proxy = runtime transport Router = decision only ``` 但实施必须分阶段完成。 ## 7. Proxy / goosed 边界设计 ### 7.1 硬边界(H5 chat 主链路) H5 chat / agent 主链路上,Portal 到 goosed 的调用必须通过 `tkmind-proxy.mjs` 或其拆分后的 proxy adapter。 禁止**新增**: ```text server.mjs -> direct fetch goosed # H5 chat 路径 agent-run-gateway.mjs -> direct fetch goosed chat-intent-router.mjs -> direct fetch goosed H5 -> direct goosed ``` ### 7.2 已知例外(MindSpace 专用路径) 以下模块当前通过 `apiTarget` 直连 goosed,**不在 H5 chat 主链路内**。Patch 5 静态检查须显式 allowlist,避免误报: | 模块 | 用途 | Patch 5 策略 | |---|---|---| | `mindspace-agent-runner.mjs` | MindSpace agent 任务 | 保留独立 adapter;长期可迁入 proxy,第一阶段 document + allowlist | | `mindspace-page-edit-session.mjs` | 页面编辑 session | 同上 | | `scripts/*` 运维脚本 | 本地调试/fix | 排除在 prod boundary check 外 | 原则: **新增** goosed 调用必须走 proxy;存量 MindSpace 路径记录在案,不阻塞 Patch 1–4。 ### 7.3 goosed 保留能力 goosed 继续负责: - `/agent/start` - `/sessions/:sessionId/reply` - `/sessions/:sessionId/events` - `/sessions/:sessionId` - tool execution - runtime event emission goosed 不新增负责: - H5 user ownership。 - 余额/计费。 - router 结果。 - memory policy。 - session broker 事实源。 ### 7.4 target mapping 当前 `goosed_target` 已经用于 session pinning。Broker 不应替代这个字段,而应封装它。 目标行为: ```text new session: proxy pick target goosed start returns session id broker records session_id -> target existing session: broker resolves session_id -> target proxy sends reply/events to same target ``` ## 8. SSE 设计 ### 8.1 已有能力 已有能力: - H5 session events 使用 fetch stream。 - 前端维护 `lastEventId` 并传 `Last-Event-ID`。 - Portal proxy 透传 `Last-Event-ID` 到 goosed。 - Portal proxy 已处理 keepalive、abort、backpressure、billing transform、finish 后 snapshot refresh。 这些不能被第一阶段改造破坏。 ### 8.2 第一阶段目标 只做语义固化: - 明确事件分类: `delta/control/terminal`(见 §3.3 映射表)。 - 明确 session stream terminal event 必须关闭当前 active request,并走 `syncSessionMessages` merge(`useTKMindChat.ts`)。 - 明确 run stream terminal(succeeded/failed)关闭 run 订阅,**不替代** session finish 同步。 - 错误统一为 SSE `event: error`,不要在 header sent 后再走 JSON error。 - `GET /agent/runs/:runId/events` 保持现有兼容,不强行改成完整 replay。 ### 8.3 后续目标 第二阶段再考虑: - run event id。 - run event replay。 - session-level replay cursor。 - reconnect 后从 DB 补 terminal state。 ## 9. Run State Model ### 9.1 现状 DB status 当前保持: ```text queued running retryable succeeded failed ``` 这些状态已经被 gateway、worker、status API、测试和前端使用。 ### 9.2 目标语义 逻辑状态可以扩展为: ```text created queued running streaming tool_call completed failed canceled ``` 但第一阶段不改 DB enum,只做映射: | 逻辑状态 | 第一阶段承载方式 | |---|---| | `created` | run row insert 前后 | | `queued` | `h5_agent_runs.status = queued` | | `running` | `h5_agent_runs.status = running` | | `streaming` | session SSE control event | | `tool_call` | session SSE control event | | `completed` | `succeeded` + terminal event | | `failed` | `failed` + terminal/error event | | `canceled` | 后续新增,不在第一阶段强推 | ### 9.3 Tool gateway / code run 说明 tool gateway 与 code run 路径可能: - 不创建 goosed session(`h5_agent_runs.agent_session_id` 为 NULL)。 - 由 external worker / Aider / OpenHands 执行。 这类 run **不经过 Session Broker** 的 target mapping;gateway 仍负责 run lifecycle。避免在 broker 设计中假设“每个 run 必有 agent session”。 ## 10. 5 个 Patch 的实施版本 ### Patch 1: Session Broker facade 目标: - 新增 `session-broker.mjs`。 - 单测覆盖 ownership、target mapping、direct chat target、legacy node fallback。 - 不改变行为。 改动文件: ```text session-broker.mjs session-broker.test.mjs ``` 验收: - 所有现有 session 读写仍通过同一张 `h5_user_sessions`。 - 不新增 session cache。 - 不新增 message/tool/memory 字段。 - `schema.sql` 与 `db.mjs` 列定义对齐(或文档注明以 migration 为准)。 ### Patch 2: Proxy、Gateway 与旁路入口接入 Broker 目标: - `tkmind-proxy.mjs` 中 session register/resolve 通过 broker facade。 - `agent-run-routes.mjs` 中 ownership 校验通过 broker facade。 - `agent-run-gateway.mjs` 只在 session 创建/复用处接 broker,不动 direct/tool/retry。 - `direct-chat-service.mjs`、`server.mjs`、`wechat-mp.mjs` 等 §5.7 清单模块改走 broker。 改动文件: ```text # 2a tkmind-proxy.mjs agent-run-routes.mjs # 2b agent-run-gateway.mjs direct-chat-service.mjs # 2c server.mjs wechat-mp.mjs mindspace-page-edit-session.mjs mindspace-agent-runner.mjs mindspace-conversation-package-routes.mjs ``` 验收: - `POST /agent/runs` 行为不变。 - `GET /sessions/:sessionId/events` 行为不变。 - direct chat session 仍能创建和读取 snapshot。 - goosed target pinning 仍正确。 - §5.7 量化 grep 检查通过(测试 mock 除外)。 Feature flag(建议): ```text MEMIND_SESSION_BROKER_ENABLED=1 # 默认 off → on;off 时 fallback 到 userAuth 直调 ``` ### Patch 3: Router 输出收敛 目标: - 在旧 router classification 上新增 normalized decision。 - 输出 `route/mode/session_hint/flags`。 - 旧字段继续兼容。 改动文件: ```text chat-intent-router.mjs chat-intent-router.test.mjs agent-run-gateway.mjs ``` 验收: - 旧 direct chat / agent orchestration 判断不变。 - Router 不执行 tool。 - Router 不创建 session。 - Router 不写 memory。 - H5 chat 场景 `mode` 默认 `sse`;`session_hint` 不 override gateway 硬规则(§4.3)。 Feature flag(建议): ```text MEMIND_ROUTER_NORMALIZED_DECISION=1 # shadow: 只打日志不写行为;on: gateway 读取新字段 ``` ### Patch 4: SSE event contract v0 目标: - 文档化并测试 delta/control/terminal 分类(§3.3)。 - 保持现有 stream wire format 兼容。 - 修正 header-sent 后的错误输出,继续走 SSE error frame。 改动文件: ```text agent-run-routes.mjs tkmind-proxy.mjs src/api/client.ts # 仅注释/常量;尽量不改解析逻辑 src/hooks/useTKMindChat.ts # 若动 finish/sync 逻辑需跑 regression guard 相关测试 ``` 验收: - session stream 断开重连不退化。 - `Last-Event-ID` 继续透传。 - session terminal event 后前端能稳定 idle/sync(merge,非盲覆盖)。 - run stream terminal 与 session finish 职责分离(§1.3)。 - 不要求第一阶段完成完整 replay。 ### Patch 5: goosed 入口硬边界 目标: - 明确 H5 chat 主链路上 Portal → goosed 的唯一入口是 proxy adapter。 - 增加测试或静态检查(`scripts/check-goosed-proxy-boundary.mjs` 或 eslint),防止新增绕过 proxy 的 goosed fetch。 - 对 §7.2 例外模块做 allowlist。 - 保留 goosed API,不改 goosed core。 改动文件: ```text tkmind-proxy.mjs server.mjs scripts/check-goosed-proxy-boundary.mjs # 新增 tests ``` 验收: - H5 不能直接访问 goosed。 - gateway/router 不直接 fetch goosed。 - 旧 reply path 继续返回 `410 AGENT_RUNS_REQUIRED`。 - MindSpace 例外模块在 allowlist 内,检查脚本通过。 ## 11. 上线顺序 ### Phase 1: 安全收口 1. Patch 1: Broker facade,无行为变化。 2. Patch 2a: proxy + agent-run-routes 改走 broker,`MEMIND_SESSION_BROKER_ENABLED` 可回退。 3. Patch 2b: gateway + direct-chat-service 接 broker。 4. Patch 2c: server / wechat / mindspace 旁路入口接 broker。 ### Phase 2: 协议与 router 收敛 5. Patch 3: router 输出新增字段,旧字段兼容;可先 shadow 日志。 6. Patch 4a: SSE event taxonomy 和 terminal 行为测试。 ### Phase 3: 边界加固 7. Patch 5: goosed proxy 唯一入口约束 + allowlist。 8. Patch 4b: run stream replay / event id 评估,不在第一阶段强行上线。 ## 12. 风险与控制 | 风险 | 等级 | 控制方式 | |---|---|---| | Broker 变成第二套 session store | 高 | 只包 `h5_user_sessions`,禁止 cache/message/tool/memory | | Gateway 一次性重构过大 | 高 | 第一阶段只抽 session,保留 direct/tool/retry | | Patch 2 漏改旁路入口 | 高 | §5.7 清单 + grep 量化验收 | | Direct Chat 被误认为 goosed session | 中 | 保留 `h5direct_` 和 `h5-direct` target | | WeChat dedicated session 被误清理 | 中 | Broker 保留 origin,不统一 rotate | | SSE 固化变成 event-sourcing 重构 | 高 | v0 只做 taxonomy/terminal,不改 wire format | | Run/session 双通道语义混淆 | 高 | §1.3 明确分工;Patch 4 禁止合并通道 | | goosed target list 扩缩导致 session 路由错乱 | 中 | 继续以 `goosed_target` 为优先,legacy node fallback | | MindSpace Finish 同步被移动错层 | 高 | Finish 后 public HTML/materialize 逻辑仍留在 stream finish 处理附近 | | 旧接口兼容破坏 | 中 | 保持 `/agent/runs` 返回结构、旧 reply path 410 | | Snapshot 缓存误命中导致对话清空 | 高 | 保留 `hint_mc` + `hint_ua` 双 hint 规则 | ## 13. 验收清单 ### 13.1 单测 建议新增/补充: ```text session-broker.test.mjs chat-intent-router.test.mjs agent-run-routes.test.mjs tkmind-proxy.test.mjs direct-chat-service.test.mjs ``` 重点断言: - ownership 校验失败返回 403。 - direct chat target `h5-direct` 可解析且不会走 goosed URL。 - `goosed_target` 优先于 legacy `goosed_node`。 - router 新字段默认值稳定;`session_hint` 不 override 空 session_id。 - `/sessions/:id/reply` 仍返回 410。 - session event error 在 headers sent 后走 SSE frame。 - run terminal 与 session finish 职责分离(§1.3)。 ### 13.2 回归守卫 如果修改触及以下路径,必须执行对应 guard: ```bash npm run verify:mindspace-publish-guards npm run verify:mindspace-publish-guards:full npm run verify:mindspace-page-sync-guards npm run verify:chat-finish-sync # Patch 4 触及 useTKMindChat / client.ts 时必跑 ``` 受保护路径包括: - `src/hooks/useTKMindChat.ts` - `chat-finish-sync.mjs` - `mindspace-public-finish-sync.mjs` - `conversation-display.mjs` - `server.mjs` - `tkmind-proxy.mjs` ### 13.3 发布前验证 发布前必须: ```bash bash scripts/check-release-ready.sh ``` 并按当前仓库发布规则执行完整 `main` 打包发布,不允许从脏工作区或单修复散包发布。 ### 13.4 可观测性(建议,非阻塞第一阶段) broker/proxy 接入后可补充 metrics 或 structured log: ```text session_broker.resolve_target.miss # goosed_target 空且 node fallback session_broker.ownership.denied # 403 计数 session_broker.register.duplicate # 重复 register goosed_proxy.stream.started / ended # 已有,保持 ``` 用于验证「session 不再 socket 化」后排障是否按 broker → proxy → goosed 分层定位。 ## 14. 回滚策略 ### Patch 1 回滚 删除 broker facade 和测试即可,无数据迁移。 ### Patch 2 回滚 设置 `MEMIND_SESSION_BROKER_ENABLED=0`,调用切回 `userAuth` 原函数。底层仍是同一张表,不需要 DB 回滚。 ### Patch 3 回滚 设置 `MEMIND_ROUTER_NORMALIZED_DECISION=0`,继续使用旧 `direct_chat/agent_orchestration`。 ### Patch 4 回滚 保留现有 SSE wire format;如果新 event taxonomy 影响前端,先关闭新解析逻辑,仍按旧 event type 处理。 ### Patch 5 回滚 恢复旧 proxy 调用路径或禁用 boundary check script,但必须保留旧 reply path 410,避免重新打开已修复的直接 reply 风险。 ## 15. 实施禁区 第一阶段明确禁止: - 新建第二套 session 表。 - 把 message history 放进 broker。 - 把 tool state 放进 broker。 - 把 memory 选择逻辑放进 broker。 - 把 session snapshot 合并进 broker。 - 把 router 改成多步 planner。 - 把 gateway 的 direct chat/tool gateway/retry 一次性拆掉。 - 合并 run stream 与 session stream 为单通道。 - 修改 goosed core。 - 修改 H5 UI(Patch 4 尽量避免改 `useTKMindChat.ts` 行为)。 - 修改生产拓扑或 goosed 容器部署方式。 ## 16. 预期解决的问题 执行后能解决: - session 不再依赖某条 socket/stream 连接。 - session ownership 和 runtime target 有统一入口。 - 多 goosed target 下 session pinning 更清晰。 - H5 / Portal / goosed 协议边界更稳定。 - router 不再向执行规划膨胀。 - gateway 的 run lifecycle 与 session lifecycle 逐步分离。 - 生产排障能按 router、broker、gateway、proxy、goosed、SSE 分层定位。 不会直接解决: - 模型回答质量。 - 单次推理速度。 - 工具本身执行失败。 - 已污染老 session 的自动修复。 - 完整 run/session replay。 - memory 内容质量。 这些属于后续 P2/P3 稳定性增强,不应塞进第一阶段。 ## 17. 最终判断标准 改造是否成功,看三点: 1. session 不再依赖 WebSocket/SSE 连接状态。 2. goosed 不再承担 H5 用户、计费、router、memory 的业务决策。 3. router 不输出执行步骤,只输出受控 decision。 补充量化验收: 4. §5.7 调用点清单全部经 broker,grep 检查通过。 5. H5 双 SSE 通道行为与 Patch 前一致(含 direct chat、finish merge、Last-Event-ID)。 只要以上成立,系统就从 socket-driven agent system 进入 event-driven control-plane system 的正确轨道。