# 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 Save:HTML 修复与落盘权威必须在 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 Service;DOCX 仅在临时目录生成,再通过内部 `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/` - `H5_USERS_ROOT` 同级的共享 `MindSpace/` - 显式 `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//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 与单测对所有工具同样有效。