Files
memind/docs/regression-guards/mindspace-publish-and-chat-finish.md

314 lines
17 KiB
Markdown
Raw Permalink 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.
# MindSpace 发布与聊天 Finish 回归守卫
> **状态:已验证(2026-06-29)— 请勿在未更新测试/verify 的情况下删改下列行为。**
本页记录两类好不容易调通的行为,以及如何用自动化手段防止后续「优化」误覆盖。
## 1. 公开 HTML`edit_file` 必须落盘覆盖
### 症状
- Agent 用 `edit_file` 改已有 `public/*.html` 后,链接仍是旧内容
- 不是缓存,是磁盘文件未被覆盖
### 必须保留
| 位置 | 行为 |
|------|------|
| `mindspace-public-finish-sync.mjs` | `edit_file` 读取 baseline + `old_str`/`new_str` patch,再 `writeFileSync` 覆盖 |
| `mindspace-public-finish-service.mjs` | 在 MindSpace 内调用流式物化与最终 Finish sync,并生成 canonical URL |
| `server/portal-session-routes.mjs` | Finish / SSE 流式事件只调用 `publicFinishService`,等待流式写入完成后再标记交付 ready |
### 守卫
- 单测:`mindspace-public-finish-sync.test.mjs`
- 源码 + runtime`npm run verify:public-finish-sync-runtime`
---
## 1.1 Chat SaveHTML 修复与落盘权威必须在 MindSpace
### 必须保留
| 位置 | 行为 |
|------|------|
| `mindspace-chat-save-service.mjs` | 修复错误私有图片引用、物化私有图片并受限写回 `public/*.html`;预览缩略图生成与 sidecar 写入 |
| `mindspace-chat-save-service.mjs` | quick share 的私有资源内联、`public/shared/*.html` 写入和 public URL 生成 |
| `mindspace-server-adapter-contract.mjs` | `chatSaveService.materializeWorkspaceHtml``ensurePreviewThumbnail``renderPreviewThumbnailSvg``createSharedHtml``readWorkspaceHtml` 同时暴露给 local / remote adapter |
| `server.mjs` | 只调用 `mindSpaceChatSave.materializeWorkspaceHtml`,禁止重新读取 `storageRoot` 或直接 `writeFile` |
| `server/portal-mindspace-chat-save-routes.mjs` | 预览缩略图与分析/保存流程只调用 `getMindSpaceChatSave()`,禁止直接解析 `publishDir` 或调用 `ensureWorkspaceHtmlThumbnail` |
| `server/portal-mindspace-chat-share-routes.mjs` | quick share 只调用 `createSharedHtml`quick Plaza 的 workspace HTML 读取只调用 `readWorkspaceHtml`;禁止直接解析 publishDir、写文件、读取 storage root 或生成 public URL |
这条边界用于保证 `split-service` 下 Portal 不再成为 Chat Save
的物理文件写权威。修复与图片物化继续保持幂等和 best-effort
真正的工作区写失败仍须明确返回错误,禁止静默回退到 Portal 本地写入。
### 守卫
- 单测:`mindspace-chat-save-service.test.mjs`
- local / remote / RPC`mindspace-local-runtime-services.test.mjs`
`mindspace-remote-server-adapter.test.mjs`
`mindspace-service/mindspace-rpc-server.test.mjs`
- 源码门禁:`scripts/verify-mindspace-authority-boundary.mjs`
- 综合验证:`npm run verify:mindspace-publish-guards`
- runtime 双边一致性:构建 Portal 与 MindSpace service runtime 后执行
`npm run verify:mindspace-publish-guards:full`
---
## 1.2 Conversation Artifact:登记与 manifest 写入权威必须在 MindSpace
### 必须保留
| 位置 | 行为 |
|------|------|
| `mindspace-conversation-package-artifact-service.mjs` | 从逻辑 user/session/artifact ref 解析工作区文件,登记 public HTML、聊天 DOCX、发布长图和通用 workspace 写入,并刷新 package manifest |
| `mindspace-server-adapter-contract.mjs` | `conversationArtifactService` 的四类登记方法同时暴露给 local / remote adapter`conversationPackageRegistry` 的远程契约只保留读取方法 |
| `server/portal-session-routes.mjs` | Finish 只传 user/session/relativePath/messageId,禁止把 `publishDir` 传入 artifact 登记 |
| `server/portal-mindspace-chat-save-routes.mjs` | DOCX 生成后调用 `registerChatDocxArtifact`,禁止直接取得 conversation package registry |
| `server.mjs` | public HTML 与发布长图登记只调用 `mindSpaceConversationArtifacts`;禁止直接 `putObjectForSession``recordArtifact``writeManifestForSession` |
该边界保证 local 与 split-service 使用同一个产物登记入口。MindSpace
负责 backing file 校验、确定性 artifact ID、对象写入和 manifest
刷新;Portal 仍只负责鉴权、Finish 时序与生成结果转发。
Finish 中受第 1 节保护的 `edit_file` 落盘兼容逻辑已迁入
`publicFinishService`,但仍复用同一纯实现和回归用例;禁止在 Portal
增加本地写回 fallback,也不得破坏已验证的覆盖语义。
### 守卫
- 单测:`mindspace-conversation-package-artifact-service.test.mjs`
- local / remote / RPC`mindspace-local-runtime-services.test.mjs`
`mindspace-remote-server-adapter.test.mjs`
`mindspace-service/mindspace-rpc-server.test.mjs`
- Portal 路由:`server/portal-session-routes.test.mjs`
`server/portal-mindspace-chat-save-routes.test.mjs`
- 源码门禁:`scripts/verify-mindspace-authority-boundary.mjs`
- 综合验证:`npm run verify:mindspace-publish-guards`
---
## 1.3 Workspace 读交付:物理路径与 canonical URL 必须在 MindSpace
### 必须保留
| 位置 | 行为 |
|------|------|
| `mindspace-workspace-publication-delivery-service.mjs` | 解析 `/MindSpace` 请求、校验 backing file 与 delivery contract、读取 HTML/二进制、补私有资源、生成缩略图/长图、扫描最近 HTML,并验证 Agent Run deliverables |
| `mindspace-publications.mjs` | `resolvePublic` 返回 `workspacePublicUrl`canonical workspace URL 不由 Portal 拼接 |
| `mindspace-public-finish-service.mjs` | Finish 内完成 HTML 交付完整性检查、H5/微信 Page Data preparation、微信 HTML 链接交付与 fresh thumbnail 校验/修复;跨 RPC 返回的结果必须移除绝对路径与 HTML 原文 |
| `mindspace-local-runtime-services.mjs` | 构造 `workspacePublicationDeliveryService``workspacePageDeliveryService`local/remote 使用同一 adapter contract |
| `server/portal-workspace-publication-delivery.mjs` | 只把逻辑 request path 交给 MindSpace,并代理 HTML/Base64 body;禁止读取、校验或返回本地绝对路径 |
| `server/portal-publication-routes.mjs` | `/u/:slug/public` 调用 `readOwnerPublicAsset`publication redirect 使用 `workspacePublicUrl` |
| `server/portal-session-routes.mjs` | 先同步页面记录,再调用 `preparePageDataAfterFinish` 并消费逻辑结果;禁止解析 `publishDir``storageRoot``h5Root` |
| `server/portal-integration-services-bootstrap.mjs` + `wechat-mp.mjs` | 微信只接收 `prepareWechatPageDataDelivery``prepareWechatHtmlDelivery``ensureWechatFreshPageThumbnails` 逻辑能力;禁止注入或读取 MindSpace 的 pool/storage path / publishDir / backing file path |
| `server/portal-mindspace-asset-routes.mjs` | 下载和 from-asset 只消费 `readAssetContent` / `readPublicAssetContent` 的 Base64 body |
Portal 可以继续负责鉴权、viewer-specific HTML 注入、CSP/缓存响应头和
repair turn 的触发,但不能获得 backing file 路径,也不能在 remote
模式下回退读取 Portal 本机目录。
### 守卫
- 核心服务:`mindspace-workspace-publication-delivery-service.test.mjs`
- Portal edge`server/portal-workspace-publication-delivery.test.mjs`
`server/portal-publication-routes.test.mjs`
- local / remote / RPC`mindspace-local-runtime-services.test.mjs`
`mindspace-remote-server-adapter.test.mjs`
`mindspace-service/mindspace-rpc-server.test.mjs`
- 源码门禁:`scripts/verify-mindspace-authority-boundary.mjs`
- 综合验证:`npm run verify:mindspace-publish-guards`
- runtime 双边一致性:构建两侧 runtime 后执行
`npm run verify:mindspace-publish-guards:full`
---
## 1.4 Agent MCP:逻辑 workspace/package 与 scoped token 必须成套
### 必须保留
| 位置 | 行为 |
|------|------|
| `mindspace-workspace-tool-service.mjs` | 只接受逻辑 `workspaceRef`、当前 `sessionId/packageId` 和相对路径;阻止绝对路径、路径穿越、symlink escape 与浏览器持久存储 |
| `mindspace-mcp-scoped-token.mjs` | token 同时绑定 user、session、package、workspace 与工具 allowlist,并校验签名和有效期 |
| `mindspace-service/mindspace-rpc-server.mjs` | `/mindspace/v1/mcp/:tool` 从 token 注入作用域,拒绝请求体覆盖 user/session/package/workspace |
| `mindspace-sandbox-mcp.mjs` | scoped 模式下通用 workspace 工具和 `generate_long_image` 只经 MindSpace ServiceDOCX 仅在临时目录生成,再通过内部 `write_binary_file` 上传 |
| `capabilities.mjs` + `user-auth.mjs` | 取得真实 session 后才生成 scoped token;签名 secret 不得进入 Goose extension env |
| `mindspace-conversation-package-artifact-service.mjs` | 非公开 workspace 写入也必须直接登记 artifact 并刷新 manifest,不能把 asset sync 的结果当作唯一登记保证 |
| `mindspace-workspace-tool-service.mjs` | 二进制写入和长图目标写入必须落盘、登记 artifact、刷新 manifest,并且响应不得暴露绝对路径 |
`publish_page` 在 scoped endpoint/token/workspace/session/package 任一项
缺失时不得暴露。保留旧本地文件工具只是未配置 scoped split-service
时的兼容路径,不得让它成为 remote 模式的静默 fallback。
scoped 模式下 `generate_docx` / `generate_long_image` 也不得在 RPC
失败时回退写 Goose 本地目标路径。
### 守卫
- 单测:`mindspace-workspace-tool-service.test.mjs`
`mindspace-mcp-scoped-token.test.mjs`
`mindspace-sandbox-mcp.test.mjs``capabilities.test.mjs`
- RPC / policy`mindspace-service/mindspace-rpc-server.test.mjs`
`user-auth.test.mjs``tkmind-proxy.test.mjs`
- 源码门禁:`scripts/verify-mindspace-authority-boundary.mjs`
- 综合验证:`npm run verify:mindspace-publish-guards`
---
## 1.5 Split-service 契约:版本、构建与生产闸门必须成套
### 必须保留
| 位置 | 行为 |
|------|------|
| `mindspace-server-adapter-contract.mjs` | `MINDSPACE_SERVER_ADAPTER_CONTRACT_VERSION`、required capabilities 与 required bindings 是 remote/local/RPC 的共享契约 |
| `mindspace-remote-server-adapter.mjs` | `assertReady()` 必须拉取 `/mindspace/v1/contract` 并校验版本、capability 与关键 method,失败时 Portal 启动 fail-fast |
| `server/portal-domain-services-bootstrap.mjs` | bootstrap 必须 `await mindSpaceRuntimeAdapter.assertReady?.()`,禁止把 contract mismatch 延迟到用户交付链路 |
| `mindspace-service/server.mjs` + `scripts/build-mindspace-service-runtime.mjs` | standalone runtime 必须写入并读取 `build-info.json`,让 `/health``/contract` 暴露 `buildId/gitSha/builtAt` |
| `scripts/release-mindspace-service-prod.sh` | 生产启动后必须校验 `/mindspace/v1/contract` 的版本、capability、关键 method 与 runtime manifest 的 `git_head` |
| `mindspace-storage-adapter.mjs` + `mindspace-service.mjs` | storage adapter 必须先通过版本化接口契约,后续 NAS/S3 adapter 不得绕过 facade |
| `scripts/audit-conversation-packages.mjs` + `scripts/trace-mindspace-artifact.mjs` | package/artifact 诊断必须保留 read-only 默认行为;`--repair` 只能补 `public_html` artifact 记录,不得改物理文件 |
这条边界保证 split-service 不会出现“Portal 已升级、MindSpace service
还是旧 runtime”的隐性半成功状态。旧契约必须在启动时被拒绝;生产
runtime 也必须和发布 manifest 的 git commit 对齐。
### 守卫
- 单测:`mindspace-remote-server-adapter.test.mjs`
`mindspace-service/mindspace-rpc-server.test.mjs`
`mindspace-storage-adapter.test.mjs`
`mindspace-conversation-package-audit.test.mjs`
- split smoke`npm run smoke:mindspace-split-service`
- 源码 + runtime 门禁:`npm run verify:mindspace-authority-boundary`
`npm run verify:mindspace-authority-boundary:full`
- 综合验证:`npm run verify:mindspace-publish-guards`
---
## 2. 聊天 Finish:禁止清空对话 / 禁止暴露内部前缀
### 症状
- 流式过程正常,**Finish 瞬间**聊天框刷新,只剩 1 条 user 消息
- 该消息露出 `【TKMind 路由提示】``static-page-publish` skill 前缀等 **Agent 专用文案**
- 历史记录里仍有完整对话 → 说明是 **前端 Finish 同步** 问题,不是 Goose 丢数据
### 必须保留
| 位置 | 行为 |
|------|------|
| `src/hooks/useTKMindChat.ts` | `syncSessionMessages``mergeConversationSnapshot`,**禁止**盲覆盖;服务端条数不足时按 `FINISH_SYNC_RETRY_DELAYS_MS` 重试 |
| `server/portal-session-routes.mjs` | Session snapshot 缓存仅在 `hint_mc` **且** `hint_ua` 均提供且匹配时命中 |
| `server.mjs` | 必须通过 `attachPortalSessionRoutes` 组合 Session 路由并注入 snapshot / proxy 依赖 |
| `conversation-display.mjs` + `src/utils/message.ts` | 用户消息无 `displayText` 时,用 `deriveUserFacingText` 剥掉 routing / skill / 用户身份前缀 |
| `chat-finish-sync.mjs` | 纯函数 merge 逻辑(被 TS 与单测共用) |
### 守卫
- 单测:`chat-finish-sync.test.mjs``conversation-display.test.mjs`
- 源码静态检查:`npm run verify:chat-finish-sync`
- 文档:本文件
### 恢复会话快照:禁止用旧 DB 历史覆盖新回复
视觉上下文不兼容等故障会把 Agent Run 旋转到新 Goose session。新 session
可能只包含最后一条数据库已同步的用户消息,以及旋转后新生成的助手回复和后续追问;
此时数据库旧历史条数通常更多。
- `conversation-repair.mjs` 发现 Goose 与 DB 存在共同消息 ID 时,必须以该 ID
为锚点合并:共享历史采用 DB 内容,同时保留 Goose 独有的新回复、后续追问和修改结果
- 只有双方没有共同消息 ID,且 Goose 确实是空占位/残缺记录时,才允许用 DB 历史整体修复
- Portal 保存 session snapshot 前不得把“当前同步计数”与“旧 messages_json”组合成伪最新快照
守卫:`conversation-repair.test.mjs` 中的 session recovery fresh-tail 用例。
---
## 3. 页面需求澄清:禁止误报“未生成 public HTML”
### 症状
- 用户只说「帮我生成一个页面吧」,没有给出主题或内容
- 助手正常追问页面类型、主题或素材
- Finish 后却显示红色错误条:「页面任务未生成 public HTML 交付物,不能标记成功」
### 根因与必须保留
`agent-run-gateway.mjs` 的页面交付守卫曾把所有页面意图都视为本轮必须交付 HTML,
没有区分“信息足够、可执行的页面任务”和“只能先澄清的泛化请求”。
- 必须使用用户消息 `metadata.displayText` 判断请求本身,不能让内部 routing / skill 前缀影响判定
- 「帮我生成一个页面吧」这类没有主题的泛化请求允许以澄清回复正常结束
- 「帮我做一个秋夜诗 H5 页面」等已有主题的任务仍须 fail closed:没有本轮 `public/*.html` 就不能标记成功
- Page Data 任务的交付守卫不受此例外影响
### 守卫
- 单测:`chat-skills.test.mjs``agent-run-gateway.test.mjs`
- 综合验证:`npm run verify:h5-session-patches`
---
## 4. 页面已落盘:自动配图失败或多工作区不得吞掉链接
### 症状
- 用户要求「帮我写一首诗词,做个页面吧」
- Agent 已调用 `write_file``public/*.html` 也已真实落盘
- 自动生图失败后整个 Agent Run 仍被标记失败,前端显示红条
- Portal 代码目录与 session 工作目录不同时,链接过滤器只检查 `process.cwd()/MindSpace`
将另一个共享工作区中真实存在的链接改成「页面生成未完成」
### 必须保留
- 输入区为 `auto` 且用户没有明确要求图片时,视觉页面配图只能是 best effort;生图失败不得拖垮已成功的 HTML 主交付
- 用户明确要求图片,或输入区切到 `required` 时,仍保留位图证据 fail closed
- 页面链接存在性检查必须同时覆盖:
- 当前 Portal 工作目录下的 `MindSpace/<userId>`
- `H5_USERS_ROOT` 同级的共享 `MindSpace/<userId>`
- 显式 `MEMIND_SHARED_PUBLISH_ROOT` / `GOOSED_SANDBOX_PUBLISH_ROOT`
- 不得因为 Portal 运行在临时 worktree,而误删共享 session 工作区中的有效页面链接
- 历史消息若已被替换成「页面生成未完成」占位文案,后续读取发现同名 HTML 已存在时必须恢复为可点击链接
### 守卫
- 单测:`chat-intent-router.test.mjs``agent-run-gateway.test.mjs``tkmind-proxy.test.mjs`
- 综合验证:`npm run verify:h5-session-patches`
---
## 发版 / CI 必跑命令
```bash
# 专项回归(单测 + 源码守卫)
npm run verify:mindspace-publish-guards
# 发版 Portal runtime 后(含 bundle 检查)
npm run verify:mindspace-publish-guards:full
```
`scripts/release-portal-runtime-prod.sh``scripts/release-prod.sh` 在未 `--skip-tests` 时会调用上述 verify。
---
## 修改这些区域时的 checklist
- [ ] 是否仍 merge 本地与服务器消息(Finish / UpdateConversation)?
- [ ] 是否仍过滤/剥离用户消息中的 agent-only 前缀?
- [ ] `edit_file` 是否仍会 materialize 到 `MindSpace/<userId>/public/`
- [ ] Chat Save HTML 修复与落盘是否仍只经 `chatSaveService`
- [ ] 是否已跑 `npm run verify:mindspace-publish-guards`
- [ ] 是否更新了本页或相关单测?
---
## 相关 Cursor 规则
`.cursor/rules/mindspace-publish-chat-finish-guards.mdc` — Cursor 在编辑相关文件时自动提示上述约束。
## 其它 AI 工具(Codex / Cloud 等)
**不会**自动读取 `.cursor/rules/`。请改为阅读仓库根目录 **[AGENTS.md](../../AGENTS.md)** 与本目录文档;verify 与单测对所有工具同样有效。