17 KiB
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-publishskill 前缀等 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 工作目录下的
- 不得因为 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 必跑命令
# 专项回归(单测 + 源码守卫)
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 与本目录文档;verify 与单测对所有工具同样有效。