Files
memind/docs/h5-session-architecture-20260706.md
T
john 14a00774d9 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>
2026-07-06 16:06:26 +08:00

962 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 的正确轨道。