diff --git a/docs/103-h5-rich-page-rollout-runbook-20260707.md b/docs/103-h5-rich-page-rollout-runbook-20260707.md new file mode 100644 index 0000000..18a8adb --- /dev/null +++ b/docs/103-h5-rich-page-rollout-runbook-20260707.md @@ -0,0 +1,243 @@ +# 103 H5 页面生成灰度与发布复盘(2026-07-07) + +> 适用场景:H5 / 服务号聊天中,长内容、结构化内容、适合读者阅读或转发的内容没有自动生成页面,或 103 开关/发布后需要做受控验证。 + +## 结论摘要 + +本次问题不是 `static-page-publish` 或 `sandbox-fs` 完全不可用,而是三层问题叠加: + +1. 103 起初未开启 H5 session 灰度开关,生产仍停在旧路径。 +2. Router 将“长新闻 + 请整理给服务号用户阅读”判成 `direct_chat`,导致只回复摘要,不进入页面生成链路。 +3. Portal runtime 发布后,旧 `goosed-prod-*` 容器仍握着旧 live 目录 inode 的 bind mount,导致容器内写入成功但 host 当前 `/Users/john/Project/Memind/MindSpace` 看不到。 + +最终上线版本: + +- Portal release: `20260707-154245-ea4c6b7` +- Git commit: `ea4c6b707c2a95c5c63eed4ed5dc02f8a6f786bc` +- Branch used for release: `release/rich-page-router-0707` +- Production host: `john@58.38.22.103` + +## 安全边界 + +- 不从脏工作区发包。 +- 不从功能分支、局部文件或单 commit 手工拷贝到 103。 +- 不直接 `rsync` 到 103/105。 +- Portal 发包必须走 `bash scripts/release-portal-runtime-prod.sh --yes`。 +- `.env` 灰度开关可以作为运行配置变更,但必须先备份、记录、重启、健康检查。 +- goosed 容器重建是运行态 remount 修复,不是源码变更;执行前后必须检查 18006-18014 健康。 + +## 本次启用的 103 H5 灰度开关 + +103 `.env` 当前启用: + +```bash +MEMIND_SESSION_BROKER_ENABLED=1 +MEMIND_ROUTER_NORMALIZED_DECISION=1 +MEMIND_SSE_EVENT_TAXONOMY=1 +MEMIND_RUN_STREAM_REPLAY=1 +MEMIND_H5_HTML_FINISH_GUARD=1 +MEMIND_SESSION_STREAM_REPLAY=1 +MEMIND_SESSION_BROKER_METRICS=1 +``` + +启用前备份: + +```bash +/Users/john/Project/Memind/.env.backup-h5-session-flags-20260707-151846 +``` + +启动日志应看到: + +```text +[Portal] Session Broker enabled (MEMIND_SESSION_BROKER_ENABLED=1) +[Portal] H5 session flags: MEMIND_SESSION_BROKER_ENABLED, ... +``` + +## 标准验证流程 + +### 1. 基础健康 + +103 本机: + +```bash +ssh john@58.38.22.103 'curl -fsS http://127.0.0.1:8081/api/status' +``` + +外部入口: + +```bash +curl -ksS https://m.tkmind.cn/api/status +``` + +Manifest: + +```bash +ssh john@58.38.22.103 'cat /Users/john/Project/Memind/.release-manifest.txt' +``` + +### 2. 账号级 smoke + +使用生产账号登录后,创建 session 并发一条短消息。期望: + +- `/api/agent/runs` 返回 `202` +- run 最终 `succeeded` +- 会话里 `message_count=2` +- 助手回复 `OK` + +### 3. 页面链路 smoke + +发明确页面生成请求。期望: + +- run events 有 `intent_routed` +- route 为 `agent_orchestration` +- `suggestedSkill=static-page-publish` +- goosed 原始 session 有 `load_skill` 和 `sandbox-fs__write_file` +- 最终 Markdown 链接能 `HTTP 200` + +示例验收 URL: + +```text +https://m.tkmind.cn/MindSpace/a6fb1e97-2b0f-447b-b138-4561d8e5c53e/public/world-news-20260707.html +``` + +### 4. 长内容/服务号场景 smoke + +复测用户原始形态: + +```text +以下是今天(2026年7月7日,星期二)国际上的主要热点新闻: +... +请整理给服务号用户阅读。 +``` + +期望 run events: + +```json +{ + "eventType": "intent_routed", + "data": { + "route": "agent_orchestration", + "reason": "结构化长内容适合生成页面承载", + "source": "rule", + "suggestedSkill": "static-page-publish" + } +} +``` + +## 本次代码调整 + +文件: + +- `chat-intent-router.mjs` +- `chat-intent-router.test.mjs` + +策略: + +- 保留短问答、普通摘要、故事/诗歌等 direct chat。 +- 对“服务号/公众号/H5/读者/用户/转发/分享/阅读”等场景中较长、结构化、多条目内容,优先走 `static-page-publish`。 +- 避免完全依赖 LLM router 判断,否则容易把“整理给服务号用户阅读”判为纯文字摘要。 + +本地验证: + +```bash +node --test chat-intent-router.test.mjs direct-chat-service.test.mjs chat-skills.test.mjs +npm run verify:h5-session-patches +``` + +发包脚本也会跑: + +```bash +npm test -- --test-name-pattern='publish|space|billing' +npm run verify:mindspace-publish-guards +npm run verify:mindspace-publish-guards:full +``` + +## 发版路径 + +本次没有从原始脏工作区发包,而是使用干净 worktree: + +```bash +git worktree add -b codex/rich-page-router /Users/john/Project/Memind-rich-page-router main +git worktree add -b release/rich-page-router-0707 /private/tmp/memind-release-rich-page main +``` + +发布前闸门: + +```bash +bash scripts/check-release-ready.sh --skip-fetch +``` + +正式发布: + +```bash +bash scripts/release-portal-runtime-prod.sh --yes +``` + +发布完成后推送 `main`,保证 manifest commit 可追溯: + +```bash +git push origin main +``` + +## 关键踩坑:goosed bind mount 旧 inode + +现象: + +- goosed 原始会话显示 `sandbox-fs__write_file public/today-news-20260707.html` 成功。 +- 容器内能看到文件。 +- host 当前 `/Users/john/Project/Memind/MindSpace/.../public` 看不到文件。 +- 公网 URL 404 或被 `MEMIND_H5_HTML_FINISH_GUARD` 拦截。 + +原因: + +Portal runtime 发布会移动旧 live 目录到 archive,再展开新 runtime live 目录。已经运行的 `goosed-prod-*` 容器仍握着旧目录 inode 的 bind mount。 + +确认命令: + +```bash +ssh john@58.38.22.103 ' + /opt/homebrew/bin/docker exec goosed-prod-1 sh -lc "ls -l /Users/john/Project/Memind/MindSpace//public/.html" + ls -l /Users/john/Project/Memind/MindSpace//public/.html +' +``` + +修复: + +```bash +ssh john@58.38.22.103 ' + cd /Users/john/Project/goosed-prod + /opt/homebrew/bin/docker compose -f docker-compose.prod.yml up -d --force-recreate +' +``` + +健康检查: + +```bash +ssh john@58.38.22.103 ' + /opt/homebrew/bin/docker ps --format "{{.Names}} {{.Status}}" | grep goosed-prod | sort -V + for p in $(seq 18006 18014); do curl -kfsS "https://127.0.0.1:$p/status" >/dev/null || exit 1; done + curl -fsS http://127.0.0.1:8081/api/status +' +``` + +经验:Portal runtime 发布后,如果本次涉及 MindSpace 页面写入,应该把 goosed remount 检查纳入发布后验收;必要时重建 goosed 容器。 + +## 下次 Checklist + +1. 确认目标:是本地、103、还是 105 入口问题。 +2. 先只读确认生产 manifest、flags、health。 +3. 用真实生产账号做短消息 smoke。 +4. 用明确页面生成请求做 `write_file + URL 200` smoke。 +5. 用用户原始表达做路由 smoke,检查 `intent_routed`。 +6. 若 `direct_chat`:修 router 规则或 prompt,不要先开通用 code/tool gateway。 +7. 若有 `write_file` 但 URL 404:检查 host/container 同路径可见性,重点怀疑 goosed bind mount 旧 inode。 +8. 只从干净 release worktree 发包。 +9. 发包后验证 manifest、外部入口、真实业务页面链接。 +10. 推送 `main`,确保生产 manifest commit 可追溯。 + +## 不建议的做法 + +- 不要因为页面没生成就直接打开 `MEMIND_AGENT_CODE_RUNS_ENABLED` 或 `MEMIND_TOOL_GATEWAY_ENABLED`。 +- 页面发布主要应走 goosed + `sandbox-fs`,不是通用 code executor。 +- 不要只看 run `succeeded`,必须看是否有 `write_file` 和公网 URL 200。 +- 不要把 guard 拦截当作失败本身;guard 是保护用户不收到假链接,真正问题在路由或落盘。