feat(h5): web 联网能力、实时查询路由与 session Finish 对齐

- 新增 web 能力并挂载 platform/web(web_search/fetch_url)
- 实时查询强制 web skill,router fallback 与 await session Finish
- Session Broker 覆盖率/指标、stream replay 与相关单测/E2E 脚本

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-07-06 16:06:26 +08:00
parent 08feae8bef
commit 14a00774d9
41 changed files with 3501 additions and 126 deletions
+961
View File
@@ -0,0 +1,961 @@
# 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 控制面收口,不重复 P6P8 内容** |
| [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 formatH5 仍按现有 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 starthint reuse 失效)
force_deep_reasoning flag → 同上
tool gateway / code run → 可能无 goosed sessionagent_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` | ownsSessionPOST /agent/runs | 2a |
| `agent-run-gateway.mjs` | 间接 via tkmindProxy start | 2b |
| `direct-chat-service.mjs` | register `h5-direct` | 2b |
| `server.mjs` | 多处 ownsSessionsessions 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 terminalsucceeded/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 mappinggateway 仍负责 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 → onoff 时 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/syncmerge,非盲覆盖)。
- 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 UIPatch 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 的正确轨道。