14a00774d9
- 新增 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>
962 lines
31 KiB
Markdown
962 lines
31 KiB
Markdown
# 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 的正确轨道。
|