# Memind / MindSpace / Goose 解耦推进方案 日期: 2026-07-02 适用目标: - 把 MindSpace 从 Memind Portal 单体中拆出来,成为可以独立部署、独立扩容、独立迁移存储的服务。 - Memind 继续负责产品入口、用户会话、聊天体验、套餐/能力编排和业务路由。 - Goose 继续负责 agent 执行,但不直接拥有 MindSpace 状态,也不把某台机器的文件路径当作长期契约。 - 每次对话生成或上传的图片、文件、页面、公开链接等,都要形成一个可浏览、可迁移、可打包的 conversation package。 本文只描述架构目标、边界和推进计划,不要求一次性完成所有代码拆分。 ## 0. 结论 应该拆,但不要按“105 = Control Portal,103 = Execution Portal”的机器绑定方式拆。 正确目标是服务边界: ```text Memind App - H5 / 服务号 / 认证入口 / 用户会话 - 聊天 UI 和产品业务编排 - 套餐、能力、计费、请求整形 - 不拼接 MindSpace 物理路径 - 不假设 MindSpace 部署在哪台机器 MindSpace Service - space / asset / page / publication / conversation package - canonical URL 生成和 public backing file 校验 - 存储适配器: local fs / NAS / S3 / 其它对象存储 - 权限、审计、manifest、元数据和实际文件一致性 - 可以独立部署在任意位置 Goose Runtime - agent session / tool execution / long running job - 通过 MindSpace API 或 MindSpace MCP 读写文件 - 不直接拥有 MindSpace 元数据 - 不把本地 `MindSpace/` 路径作为产品级契约 ``` 103/105 只能作为当前生产部署拓扑的一个例子。103 是生产,不是本地开发依赖,也不应该成为 MindSpace 独立化后的架构前提。 硬性规则: 1. MindSpace 的真实存储位置必须可替换,本地开发、NAS、S3 都应该只是 storage adapter。 2. Memind 不得根据 userId、slug、fileName 自己拼接 MindSpace public URL。 3. Goose 不得直接写业务数据库里的 MindSpace 状态,只能通过 MindSpace API/MCP 能力写入。 4. MindSpace Service 是 asset/page/publication/package 元数据和 canonical URL 的唯一权威。 5. 每个用户对话产生的文件必须归属到一个 conversation package,不能只散落在物理目录里。 6. 物理目录结构不是外部契约;外部契约应该是 package id、asset id、page id、publication id 和 canonical URL。 ## 1. 当前现状 当前 `/Users/john/Project/Memind` 更接近一个运行态 Portal 单体: - `server.mjs` 同时承载 H5 API、用户认证、MindSpace API、文件/发布服务、agent session proxy、Agent job worker、workspace watcher、微信/Plaza 等。 - Goose 是执行后端,Portal 通过 `TKMIND_API_TARGETS` / `TKMIND_API_TARGET` 调它。 - MindSpace 有两类本地状态: - workspace/public 文件: `MindSpace//...` - asset/page/publication 存储: `data/mindspace/users/...` - MySQL/RDS 保存用户、资产、页面、发布、任务、会话、计费、Plaza/微信等业务状态。 - `h5_page_records` 已有 `source_session_id`、`source_message_id`,`h5_agent_jobs` 也有 `session_id`,说明页面和任务已经部分具备对话来源线索。 - 但 `h5_assets` 这类底层文件资产还没有完整的 conversation package 归属模型。 现在最危险的隐含假设不是 HTTP 能不能转发,而是代码里把“某台机器上的路径”和“产品里的 MindSpace 文件”混成了同一个概念。未来 MindSpace 可以在本机、NAS、S3 或独立服务中,Memind 和 Goose 都不能依赖这层物理细节。 ## 2. 目标架构 ```mermaid flowchart LR U["Browser / WeChat"] --> M["Memind App"] M --> MS["MindSpace Service"] M --> AG["Agent Gateway"] AG --> G["Goose Runtime"] G --> MCP["MindSpace MCP / API"] MCP --> MS MS --> DB["Metadata DB"] MS --> ST["Storage Adapter"] ST --> LFS["Local FS"] ST --> NAS["NAS"] ST --> S3["S3 / Object Storage"] ``` 核心变化: - Memind App 面向用户,不面向物理文件系统。 - MindSpace Service 面向文件、页面、发布、包和存储后端。 - Goose Runtime 面向执行,不面向 MindSpace 业务所有权。 - Storage Adapter 面向字节和对象,不面向用户、会话、页面这些业务概念。 ## 3. 服务边界 ### 3.1 Memind App Memind App 负责: - H5 静态入口和聊天体验。 - 服务号 webhook、菜单、客服入口。 - 用户登录、session、cookie/header 规范化。 - 套餐、能力、计费和业务策略。 - 把用户请求转成 MindSpace API、Agent Gateway API 调用。 - 展示 MindSpace 返回的 URL、JSON、SSE、HTML 状态。 Memind App 禁止: - 拼接 `MindSpace//public/*.html`。 - 直接读写 `MindSpace/` 或 `data/mindspace/`。 - 根据文件名猜测 public URL。 - 自己判断 public backing file 是否存在。 - 把具体机器路径传给前端当长期 API。 ### 3.2 MindSpace Service MindSpace Service 负责: - Space、category、asset、asset version、page、page version、publication。 - Conversation package 和 package manifest。 - Canonical public URL 生成。 - Public backing file 存在性校验。 - 文件上传、下载、预览、缩略图、长图、HTML 页面落盘。 - 存储适配器封装: local fs、NAS、S3、其它对象存储。 - 权限校验、审计、配额和一致性修复。 MindSpace Service 应该提供稳定 API: ```text /api/mindspace/v1/spaces /api/mindspace/v1/assets /api/mindspace/v1/pages /api/mindspace/v1/publications /api/mindspace/v1/conversation-packages /api/mindspace/v1/conversation-packages/:packageId/manifest /api/mindspace/v1/conversation-packages/:packageId/artifacts ``` ### 3.3 Goose Runtime Goose Runtime 负责: - Agent session start/reply/resume/events。 - Tool execution。 - Long running job。 - 通过 MindSpace MCP/API 读取输入文件、写入生成文件、创建页面、发布页面。 Goose Runtime 不应直接负责: - MindSpace 数据库表的业务写入。 - Public URL 拼接。 - 页面发布状态判断。 - Conversation package manifest 的最终一致性。 短期内,如果 local filesystem adapter 仍需要 `sandboxRoot`,可以继续保留本地路径注入。但它只能是某个 adapter 的内部实现,不应该是 Memind/MindSpace/Goose 之间的长期契约。 ## 4. Conversation Package 模型 用户要求的“每次对话里面的文件形成一个文件夹包”,建议抽象成 conversation package。 它在产品上像文件夹,在系统里是元数据包: ```text mindspace://users//conversations// manifest.json input/ images/ files/ pages/ public/ generated/ ``` 注意: 上面是逻辑视图,不要求物理存储必须长这样。local fs 可以映射成真实目录,NAS 可以映射成共享目录,S3 可以映射成 object prefix。 Package manifest 应包含: - `packageId` - `userId` - `sessionId` - `title` - `createdAt` - `updatedAt` - `storagePrefix` - `artifacts[]` - `pages[]` - `publications[]` - `messages[]` - `agentRuns[]` Artifact 应至少包含: - `artifactId` - `assetId` - `pageId` - `publicationId` - `messageId` - `agentRunId` - `kind`: `input_image` / `input_file` / `generated_image` / `generated_file` / `page` / `public_html` / `long_image` - `displayName` - `mimeType` - `sizeBytes` - `storageKey` - `canonicalUrl` - `createdAt` 推荐新增表: ```sql CREATE TABLE h5_conversation_packages ( id VARCHAR(64) PRIMARY KEY, user_id VARCHAR(64) NOT NULL, session_id VARCHAR(128) NOT NULL, title VARCHAR(255) DEFAULT NULL, status VARCHAR(32) NOT NULL DEFAULT 'active', storage_prefix VARCHAR(512) DEFAULT NULL, manifest_asset_id VARCHAR(64) DEFAULT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uniq_conversation_package_session (user_id, session_id) ); CREATE TABLE h5_conversation_artifacts ( id VARCHAR(64) PRIMARY KEY, package_id VARCHAR(64) NOT NULL, asset_id VARCHAR(64) DEFAULT NULL, page_id VARCHAR(64) DEFAULT NULL, publication_id VARCHAR(64) DEFAULT NULL, agent_run_id VARCHAR(128) DEFAULT NULL, message_id VARCHAR(128) DEFAULT NULL, role VARCHAR(32) DEFAULT NULL, artifact_kind VARCHAR(64) NOT NULL, sort_order INT NOT NULL DEFAULT 0, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_conversation_artifacts_package (package_id), KEY idx_conversation_artifacts_asset (asset_id), KEY idx_conversation_artifacts_page (page_id), KEY idx_conversation_artifacts_message (message_id) ); ``` 也可以先用轻量方案过渡: - 给 `h5_assets` 增加 `source_session_id`、`source_message_id`、`source_run_id`。 - 利用现有 `h5_page_records.source_session_id`、`h5_page_records.source_message_id` 回填页面来源。 - 利用现有 `h5_agent_jobs.session_id` 回填 agent 生成物来源。 - 先做 package view,再补完整 package tables。 ## 5. Storage Adapter MindSpace Service 内部需要统一存储接口: ```ts interface MindSpaceStorageAdapter { putObject(key, body, options) getObject(key) statObject(key) deleteObject(key) listObjects(prefix, options) copyObject(sourceKey, targetKey, options) createReadStream(key) createWriteStream(key, options) getSignedUrl(key, options) } ``` 推荐分层: ```text MindSpace Domain -> Asset/Page/Publication/Package services -> Storage adapter interface -> local fs / NAS / S3 implementations ``` 关键要求: - Memind 不知道 `storageKey` 如何映射到磁盘或 S3。 - Goose 不直接生成 public URL,只提交产物给 MindSpace。 - public URL 可以是 MindSpace Service 的 route,也可以是 CDN/object storage URL,但必须由 MindSpace Service 生成。 - 本地开发默认用 local fs adapter,不依赖 103。 - 生产可以继续先用现有机器路径,之后切 NAS/S3 时不改 Memind/Goose 业务层。 ## 6. Agent 执行契约调整 当前 agent policy 里的关键值包括: - `sandboxRoot` - `serverPath` - `nodeExecPath` - MCP envs 这些仍可作为短期兼容层,但长期应改成: ```json { "mindspace": { "workspaceRef": "mindspace://users/u_xxx/conversations/s_xxx", "packageId": "cp_xxx", "apiBaseUrl": "https://mindspace.example.com", "accessToken": "scoped-short-lived-token", "allowedTools": ["read_file", "write_file", "edit_file", "publish_page"] } } ``` 推进原则: - Local fs adapter 可以把 `workspaceRef` 映射到本机临时目录或真实目录。 - NAS adapter 可以把 `workspaceRef` 映射到共享挂载目录。 - S3 adapter 不应暴露 raw fs path,应通过 MCP/API 完成读写和预览。 - Agent 写入完成后,MindSpace Service 负责把产物登记到 package manifest。 ## 7. 推进阶段 ### P0: 盘点和冻结边界 目标: 先明确哪些代码还在直接使用路径、URL、数据库表。 输出: - 路径调用点清单: `MindSpace/`、`data/mindspace/`、`sandboxRoot`、`public/*.html`。 - URL 调用点清单: `/MindSpace/`、`canonicalUrl`、`publicationUrl`。 - DB 调用点清单: asset/page/publication/job/session 相关表。 - 当前本地开发拓扑和生产拓扑分开写清楚,避免继续把 103 当开发依赖。 ### P1: 在单体内抽出 MindSpace Service 边界 目标: 不改部署方式,先把边界从代码里立起来。 动作: - 新增或整理 `MindSpaceService` domain 层。 - 引入 `MindSpaceStorageAdapter` interface。 - local fs adapter 先包住现有 `MindSpace/` 和 `data/mindspace/`。 - 禁止新代码直接拼接 MindSpace public URL。 - 给 public URL 生成和 backing file 校验加集中入口。 验收: - 现有 H5、公开页、页面发布、文件下载行为不变。 - 代码搜索可以证明新增业务不再直接依赖物理路径。 ### P2: Conversation Package 首先落地 目标: 用户每次对话生成的图片、文件、页面、公开链接都能在一个包里看到。 动作: - 为每个 chat session 创建或懒创建 conversation package。 - 上传文件、图片描述、页面生成、长图、HTML 发布都写入 artifact 归属。 - 新增 package manifest API。 - H5 增加 package view: 图片、文件、页面、公开链接按对话聚合展示。 - 对历史数据做 best-effort 回填。 当前开发分支进展: - 已新增 conversation package 表/注册层/API/manifest 导出和 manifest 校验脚本。 - H5 已增加“对话包”查看入口,隐藏内部 message UUID,只展示用户可理解的产物信息。 - `public/*.html` 已接入 Finish 后登记和历史会话懒回填,可在 package 中看到公开页面。 - 上传图片/文件已支持 `sessionId/messageId` provenance;全新会话首条图片上传会在 agent run 返回真实 `sessionId` 后按 `messageId` 后置认领进 package。 - 公开页长图下载已登记为 `long_image` artifact,PNG 同步写入 conversation package 私有对象,canonical URL 保持公开页长图下载入口。 - Finish 后 `public` workspace 同步已携带同轮 `messageId`,让生成文件、图片、docx companion 进入 package 时具备消息级 provenance。 - 聊天 docx 下载已抽出 package 登记模块并加入测试,确保 docx 私有对象、artifact、manifest 刷新一致。 - 发布入口已覆盖 chat-sourced page 的 publication companion 流程,确认 `public_html`、同目录 `docx`、`long_image` 会在真实 `publish()` 调用中进入同一 package。 - H5 package view 已增加按产物类型筛选、计数、空包引导和加载失败重试提示。 - H5 package view 已增加按消息维度筛选,用户可从某一轮请求反查对应产物,且不展示内部 message UUID。 - Manifest 已包含 `messages[]` / `agentRuns[]` provenance 聚合,便于追溯某条消息或某次 agent run 产生了哪些 artifact。 - 已增加 package manifest 一致性校验脚本,覆盖 artifact URL / storage object 的可读性。 - 读取 package 前已增加历史会话 best-effort 扫描回填,覆盖 `h5_page_records`、`h5_publish_records`、`h5_upload_sessions` 中已有 `source_session_id` 的页面、发布页、companion 文件和已完成上传。 - Package 读取前的 backfill/hydrate 编排已收拢进 `MindSpaceService` facade,`server.mjs` 路由层只负责鉴权后调用 facade。 剩余开发项: - P2 当前分支实现已完成;进入提交前需保持 guard、build、全量测试和回归 smoke 通过。 验收: - 一次对话中上传图片、生成 HTML、发布页面后,package 页面能看到所有产物。 - package manifest 可以导出,并能指向真实可读的 backing file。 - 不再只能靠散落目录追查某次对话产生了什么。 ### P3: MindSpace Service 本地独立进程 目标: 在本机开发环境先拆进程,不碰生产 103。 动作: - MindSpace API 单独启动,例如 `localhost:`。 - Memind App 通过 `MINDSPACE_API_BASE_URL` 调它。 - Goose 仍可本地运行,但通过 MindSpace API/MCP 获取 workspace。 - 本地 storage adapter 继续使用 local fs。 验收: - 停掉 MindSpace Service 后,Memind H5 能明确报 MindSpace 不可用,而不是静默写本地路径。 - 换一个 local storage root 后,Memind 和 Goose 不需要改业务代码。 ### P4: MindSpace MCP Bridge 目标: Goose 不再把 raw filesystem path 当核心契约。 动作: - MCP tools 改为接受 `workspaceRef` / `packageId`。 - `read_file`、`write_file`、`edit_file`、`publish_page` 通过 MindSpace Service 落元数据。 - scoped token 限制到用户、session、package 和允许工具。 验收: - Agent 生成文件后,MindSpace package manifest 自动更新。 - Policy 泄漏时也只能访问当前 package 范围。 ### P5: NAS/S3 Adapter 目标: 存储后端可替换。 动作: - 实现 NAS adapter 或 S3 adapter。 - 增加 migration/backfill 工具,把现有文件迁移到新 storage prefix。 - 增加校验脚本: DB record、manifest、object stat、public URL 一致。 - 必要时先 shadow-read 或 dual-write,降低迁移风险。 验收: - local fs 切到 NAS/S3 后,Memind App 不改代码。 - Goose 不改业务契约。 - 公开页、下载、预览、长图、package manifest 都正常。 ### P6: 生产部署迁移 目标: 等本地和 staging 验证后,再考虑生产拓扑。 原则: - 103/105 只是部署位置,不是架构边界。 - 当前 103 可以继续承载旧 Portal 和 MindSpace,直到 MindSpace Service 独立进程稳定。 - 生产迁移前必须有回滚方案、manifest 校验、public URL 抽样验证。 - 不从本地直接依赖 103 做开发验证。 ## 8. 当前 103/105 如何理解 如果短期还要用 103/105 描述生产部署,可以这样表述: ```text 105 = public edge / Memind App entry 103 = current production host running legacy Portal + MindSpace + Goose ``` 但这只是当前生产状态,不是最终架构。 最终应该允许: ```text Memind App: 105 / k8s / 任意 Web 服务 MindSpace: 独立 VM / NAS 旁路服务 / S3-backed 服务 / k8s service Goose Runtime: 本机 / worker pool / 独立执行集群 Storage Backend: local fs / NAS / S3 / CDN-backed object storage ``` ## 9. 验收标准 拆分成功不以“服务能启动”为准,而以这些行为为准: 1. 一次对话上传的图片、文件、生成页面、公开 HTML、长图都能在同一个 package 中看到。 2. Package manifest 中的每个 artifact 都能追溯到 asset/page/publication/message/run。 3. Memind App 不直接拼接 MindSpace 物理路径或 public URL。 4. Goose 生成文件后,MindSpace 元数据和实际 backing file 一致。 5. 切换 local fs / NAS / S3 adapter 时,Memind App 和 Goose 的业务逻辑不需要改。 6. 公开 URL 由 MindSpace Service 生成并校验。 7. 本地开发不依赖 103,生产 103 也不是架构上的必须组件。 ## 10. 风险 | 风险 | 表现 | 控制方式 | | --- | --- | --- | | 路径假设太深 | 代码到处拼 `MindSpace/` | P0 做调用点清单,P1 加 service/adapter 边界 | | URL 权威混乱 | Memind、Goose、MindSpace 各自拼 URL | URL 只由 MindSpace Service 生成 | | Agent 直写绕过元数据 | 文件存在但 package 看不到 | MCP/API 写入后必须登记 artifact | | S3 非文件系统语义 | `edit_file`、stream、rename 行为不一致 | 先定义 adapter 能力,必要时用临时工作区 materialize | | 迁移丢历史关系 | 老页面找不到来源对话 | 利用现有 `source_session_id`、job session 做 best-effort 回填 | | 权限过宽 | agent token 访问其它用户文件 | scoped short-lived token,绑定 user/session/package | | package 过大 | 长对话产物太多 | manifest 分页、artifact 分类、归档策略 | | 生产切换风险 | public link 失效 | 迁移前跑 object stat + URL 抽样 + 回滚 | ## 11. 建议下一步 建议按这个顺序推进: 1. 保留本文档为总纲,另外新增 `docs/mindspace-service-contract.md` 写 API、storage adapter、package manifest 的详细契约。 2. 做 P0 audit: 搜索所有 `MindSpace/`、`data/mindspace`、`sandboxRoot`、`/MindSpace/`、`canonicalUrl` 调用点。 3. 先在单体内做 P1,不急着拆进程,避免一次同时改部署、存储、执行链路。 4. 优先实现 P2 conversation package,因为这是用户可见价值,也能倒逼所有产物建立 provenance。 5. 本地完成 P1/P2 smoke 后,再做 P3 独立 MindSpace Service。 6. 等 P3 稳定后再考虑 P4 MCP 契约和 P5 NAS/S3。 7. 生产 103/105 迁移放到最后,只作为部署迁移,不作为架构设计的出发点。 最小可交付版本: ```text 单体内 MindSpaceService 边界 + local fs storage adapter + conversation package tables/API + H5 package view + public URL 集中生成 + 现有发布/下载/预览回归通过 ``` 这个版本即使还没有真正拆进程,也已经把未来独立部署、NAS/S3 和 Goose 解耦的地基打好了。 ## 12. 启动门禁和回退策略 本轮推进只能在独立分支上开始,禁止直接提交 `main`。 当前建议分支: ```text codex/mindspace-decouple-20260702 ``` 开始任何代码改动前必须先确认: ```bash git branch --show-current git status --short --branch git merge-base --is-ancestor origin/main HEAD ``` 通过条件: - 当前分支不是 `main`。 - 当前分支基于最新 `origin/main` 创建。 - 工作区里只出现本轮 MindSpace 解耦相关文件。 - 不允许从本机开发流程依赖 103。 本地保护: - `.git/hooks/pre-commit` 应阻止在 `main` 上直接 commit。 - 如果需要新分支,必须使用 `bash scripts/new-branch.sh `,不要直接在脏工作区切分支。 - 每个阶段开始前先记录 `git status --short --branch`。 回退分三层: 1. 单文件回退: 如果某个文件改坏,只恢复该文件,不影响其它正在推进的内容。 2. 阶段回退: P1/P2/P3 每个阶段独立提交;如果阶段失败,只 revert 该阶段提交。 3. 分支回退: 如果方向整体不对,保留 `main` 不动,直接放弃 `codex/mindspace-decouple-20260702` 分支即可。 禁止事项: - 禁止在 `main` 上 commit。 - 禁止从脏工作区发布。 - 禁止把 103 当作本地开发依赖。 - 禁止在没有 package provenance 的情况下继续扩大文件生成链路。 - 禁止把 storage backend 细节暴露成 Memind 或 Goose 的长期 API。 第一批可执行任务: 1. P0 audit: 生成路径、URL、policy、DB 调用点清单。 2. 新增 `docs/mindspace-service-contract.md`,只写契约,不改运行逻辑。 3. 设计 conversation package schema migration,但先不执行生产迁移。 4. 在单体内抽 `MindSpaceStorageAdapter` 和 public URL 生成入口。 5. 给现有发布/下载/预览跑回归,再进入下一阶段。