feat(h5-session): Session Broker、run SSE replay 与 Finish 竞态修复
落地 H5 Session 架构 Patch 1–5(Broker 收口、Router decision、SSE taxonomy、goosed 边界检查), 并新增可选 MEMIND_RUN_STREAM_REPLAY run 事件回放与 H5 假交付 guard;修复 Finish 先于 agent-run gate 导致 UI 永久 loading 的竞态,接入 verify:h5-session-patches 回归脚本。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
# 待修复项索引
|
||||
|
||||
记录**已确认、暂缓实现**的问题,避免与进行中的大分支并行改同一批文件导致分叉。
|
||||
|
||||
| 文档 | 问题 | 状态 | 阻塞原因 |
|
||||
|------|------|------|----------|
|
||||
| [h5-finish-before-run-gate-20260706.md](./h5-finish-before-run-gate-20260706.md) | Finish 先于 agent-run gate 导致 UI 永久 loading | 已修复(分支内) | 随 H5 session 分支合并 |
|
||||
| [h5-public-html-fake-delivery-guard-20260706.md](./h5-public-html-fake-delivery-guard-20260706.md) | H5 Agent 宣称「页面已生成」但 `public/*.html` 未落盘(无 write_file) | 待修复 | 等 H5 session / broker / SSE 收口分支合并后,与 `server.mjs`、`tkmind-proxy.mjs` 统一改 |
|
||||
|
||||
开工前:读对应 md 全文 + [h5-session-architecture-20260706.md](../h5-session-architecture-20260706.md),确认主线已合并再动 Finish 路径。
|
||||
@@ -0,0 +1,35 @@
|
||||
# Finish 先于 agent-run gate 的 UI 竞态(已修复,禁止 merge main / 上 103)
|
||||
|
||||
> **状态:已修复 + 单测(2026-07-06)**
|
||||
|
||||
## 现象
|
||||
|
||||
用户发消息后:
|
||||
|
||||
- assistant 回复已显示
|
||||
- 底部三个点(`streaming`)一直转,无法输入下一条
|
||||
|
||||
## 根因
|
||||
|
||||
H5 双 SSE 通道时序竞态:
|
||||
|
||||
1. `subscribeSessionEvents` 收到 goosed `Finish` → `chatState = idle`
|
||||
2. `waitForAgentRun` 稍后才 `succeeded` 返回
|
||||
3. 旧逻辑 unconditionally `setChatState('streaming')` → `Finish` 已错过,永久卡住
|
||||
|
||||
## 修复
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `chat-agent-run-gate.mjs` | 纯函数 `resolvePostAgentRunChatState` / `shouldPromoteSessionIdToStreaming` |
|
||||
| `chat-agent-run-gate.test.mjs` | 回归单测 |
|
||||
| `src/hooks/useTKMindChat.ts` | run gate 完成后若已是 `idle` 则保持 idle |
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
node --test chat-agent-run-gate.test.mjs
|
||||
npm run verify:h5-session-patches
|
||||
```
|
||||
|
||||
手工:本地 `pnpm dev` 发 `hi`,确认回复完成后 loading 点消失。
|
||||
@@ -0,0 +1,81 @@
|
||||
# 待修复:H5 页面「假交付」检测与 write_file 自动重试
|
||||
|
||||
> **状态:已实现 guard 模块(2026-07-06),默认关闭 `MEMIND_H5_HTML_FINISH_GUARD=0`**
|
||||
> **原暂缓原因**:H5 session / broker 分支已在本分支落地,Finish guard 已接入 `server.mjs` onAfterFinish。
|
||||
|
||||
## 症状
|
||||
|
||||
- H5 Agent 回复含「页面已生成 / 生成完成 / 已保存至 public/…」及公网 Markdown 链接
|
||||
- 用户点击链接 **404**
|
||||
- 磁盘 `MindSpace/<userId>/public/*.html` **不存在**
|
||||
- Finish 后 `syncPublicHtmlAfterFinish` 无 tool call 可 materialize,文件不会凭空出现
|
||||
|
||||
**典型案例(103 生产,2026-07-06):**
|
||||
|
||||
- 目标:`public/kids-posture-business.html`
|
||||
- 第一次:Agent 在聊天里输出完整商业包内容 + 链接,但未调用 `write_file` → 404
|
||||
- 用户追问后第二次同会话重试:真正 `write_file` 落盘 → 200
|
||||
- **后续(同页 10:54)**:HTML 用 `edit_file` 加了 5 张 `<img src="assets/*.svg">`,Agent 宣称「SVG 已 write_file 落盘」,但 `public/assets/kids-posture-hero.svg` 等 **5 个文件均不存在** → 页面 200、插图全部 404(与 HTML 假交付同类问题,范围扩展到非 `.html` 资源)
|
||||
|
||||
## 根因(非意图识别)
|
||||
|
||||
| 层级 | 行为 | 第一次失败时 |
|
||||
|------|------|--------------|
|
||||
| 意图路由 | `chat-intent-router` / `buildAutoChatSkillPrefix` 命中「生成页面」 | ✅ |
|
||||
| 技能提示 | `static-page-publish` 要求「必须先 write_file」 | ✅ 已注入(软约束) |
|
||||
| 模型执行 | 应调用 `write_file` | ❌ 未调用 |
|
||||
| Finish materialize | 仅从 messages 中 tool call 写盘 | ❌ 无 tool call |
|
||||
| 回复校验 | 拦截「已生成」但磁盘无文件 | ❌ **H5 无此闸门** |
|
||||
|
||||
微信通道已有 `shouldRetryHtmlGenerationReply`(`wechat-mp.mjs`),检测到链接/宣称与磁盘不一致时会 **session 重试**。H5 SSE 路径(`server.mjs` → `tkmindProxy.proxySessionEvents` → `onAfterFinish`)**没有等价逻辑**。
|
||||
|
||||
## 拟议修复(合并其它分支后统一做)
|
||||
|
||||
### 目标行为
|
||||
|
||||
当 Assistant 回复同时满足:
|
||||
|
||||
1. 含「页面已生成 / 生成完成 / 已发布 / …」类成功宣称,**或**含 `MindSpace/.../public/*.html` 公网链接
|
||||
2. 磁盘上对应 `public/*.html` **不存在**(或仅为 stub)
|
||||
|
||||
则:
|
||||
|
||||
1. **自动重试**:向同 session 发送 follow-up(参考 `wechat-mp.mjs` 的 `executeSessionReply`),明确要求 `load_skill` → `static-page-publish` → `write_file` 写入缺失路径
|
||||
2. **继续检验**:重试后再次 `materializeMissingPublicHtmlWrites` + 磁盘校验;仍失败则打 `[MindSpace]` warn 日志(可选:前端提示「页面落盘失败,请重试」)
|
||||
3. **资源完整性**:解析已落盘 HTML 中的 `src`/`href`(含 `assets/*.svg|png|jpg|webp`),缺失则一并列入 retry 清单(`materializePublicHtmlWritesFromSessionEvent` 目前只处理 `.html` tool call,不能假设插图已落盘)
|
||||
3. **上限**:建议最多 1~2 次重试,避免无限 loop
|
||||
|
||||
### 建议落点(实现时再对齐当时主线)
|
||||
|
||||
| 模块 | 改动 |
|
||||
|------|------|
|
||||
| 新建 `mindspace-h5-html-finish-guard.mjs`(或并入 `mindspace-public-finish-sync.mjs`) | 检测假交付、提取缺失 `public/*.html`、构建 retry prompt |
|
||||
| `server.mjs` `onAfterFinish` | Finish 后 sync 仍失败 → 调用 guard 重试 → 再 sync + register artifacts |
|
||||
| 复用 | `wechat-mp.mjs` 中 `shouldRetryHtmlGenerationReply`、`createPublicHtmlLinkExists`、`resolveHtmlPublishArtifacts` 等(考虑抽到共享模块,避免 H5 直接 import 整个 wechat-mp) |
|
||||
| 单测 | `mindspace-h5-html-finish-guard.test.mjs`;接入 `npm run verify:mindspace-publish-guards` |
|
||||
| 文档 | 合并后更新 [mindspace-publish-and-chat-finish.md](../regression-guards/mindspace-publish-and-chat-finish.md) |
|
||||
|
||||
### 与进行中分支的协调点
|
||||
|
||||
合并前必须先读并对齐:
|
||||
|
||||
- [docs/h5-session-architecture-20260706.md](../h5-session-architecture-20260706.md)
|
||||
- `session-broker.mjs`、`agent-run-gateway.mjs`、`tkmind-proxy.mjs` 的 Finish / SSE 收口
|
||||
|
||||
**不要在 broker 分支未合并前单独改上述文件的 Finish 路径**,否则合并冲突与行为分叉。
|
||||
|
||||
## 实现 checklist(待开工时)
|
||||
|
||||
- [ ] 其它分支(H5 session / broker)已合并 `main` 且 CI 绿
|
||||
- [ ] 从 `wechat-mp.mjs` 抽取或复用 HTML 发布校验纯函数
|
||||
- [ ] H5 `onAfterFinish` 接入 guard + 有限次 retry
|
||||
- [ ] 单测覆盖:假链接 404、retry 后落盘、正常 Finish 不误触发
|
||||
- [ ] `npm run verify:mindspace-publish-guards` 全绿
|
||||
- [ ] 103 手工:生成页 → 故意 mock 无 write_file → 确认自动 repair 或明确失败提示
|
||||
|
||||
## 参考
|
||||
|
||||
- 回归守卫:[mindspace-publish-and-chat-finish.md](../regression-guards/mindspace-publish-and-chat-finish.md)
|
||||
- 微信 retry:`wechat-mp.mjs` — `shouldRetryHtmlGenerationReply`、`executeSessionReply`
|
||||
- 落盘:`mindspace-public-finish-sync.mjs` — `materializeMissingPublicHtmlWrites`、`syncPublicHtmlAfterFinish`
|
||||
- 技能前缀:`chat-skills.mjs` — `generate-page` prompt
|
||||
@@ -0,0 +1,61 @@
|
||||
# Patch 4b:Run SSE Replay(进行中,禁止合并 main / 上 103)
|
||||
|
||||
> **状态:代码 + 单测已完成(2026-07-06);Finish 竞态见 [h5-finish-before-run-gate-20260706.md](./h5-finish-before-run-gate-20260706.md)。禁止 merge main / 上 103。**
|
||||
|
||||
## 目标
|
||||
|
||||
解决 H5 run SSE 重连丢状态问题:
|
||||
|
||||
- 使用已有 `h5_agent_run_events.id` 作为 SSE `id:` cursor
|
||||
- 客户端 `Last-Event-ID` 重连后从 DB 补发 missed events
|
||||
- 状态变更时写入 `run_snapshot` 事件,保证 sessionId 回填可回放
|
||||
|
||||
## 开关
|
||||
|
||||
```bash
|
||||
# 服务端(默认 0 = 旧 poll-only 行为)
|
||||
MEMIND_RUN_STREAM_REPLAY=1
|
||||
|
||||
# 本地验证
|
||||
MEMIND_RUN_STREAM_REPLAY=1 pnpm dev
|
||||
```
|
||||
|
||||
客户端 `subscribeAgentRunEvents` 已支持 `Last-Event-ID` + 断线重连(与 session stream 一致)。
|
||||
|
||||
## 改动文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `agent-run-stream.mjs` | replay 纯函数、flag |
|
||||
| `agent-run-gateway.mjs` | `listRunEventsForUser`、`appendRunSnapshot` |
|
||||
| `agent-run-routes.mjs` | replay 模式 SSE handler |
|
||||
| `src/api/client.ts` | run SSE Last-Event-ID + 重连 |
|
||||
| `agent-run-stream.test.mjs` | 单测 |
|
||||
| `agent-run-routes.test.mjs` | replay SSE handler 用例 |
|
||||
| `agent-run-gateway.test.mjs` | replay cursor / cursorMiss / run_snapshot 用例 |
|
||||
|
||||
## 2026-07-06 补充
|
||||
|
||||
- 修复 `listRunEventsForUser` 误返回 `cursorMiss: false`( stale `Last-Event-ID` 无法触发全量补发)
|
||||
- gateway 新增 3 项 replay 单测(51 pass 合计)
|
||||
|
||||
## 本地测试清单(合并/发布前必跑)
|
||||
|
||||
```bash
|
||||
npm run verify:h5-session-patches
|
||||
# 或分项:
|
||||
node --test agent-run-stream.test.mjs agent-run-routes.test.mjs agent-run-gateway.test.mjs
|
||||
npm run verify:goosed-proxy-boundary
|
||||
```
|
||||
|
||||
手工:
|
||||
|
||||
1. `MEMIND_RUN_STREAM_REPLAY=1 pnpm dev`
|
||||
2. 发起 agent run,DevTools 观察 `/agent/runs/:id/events` 含 `id:` 行
|
||||
3. 模拟断网/刷新,确认重连后仍能收到 `sessionId` 与 terminal `succeeded`
|
||||
|
||||
## 与 Patch 4a 关系
|
||||
|
||||
- Patch 4a:taxonomy shadow(已完成)
|
||||
- Patch 4b:run replay(本文件)
|
||||
- Session stream replay 仍为后续项,不在 4b 范围
|
||||
Reference in New Issue
Block a user