Files
memind/docs/memind-control-execution-split-plan-20260702.md

562 lines
22 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.
# Memind / MindSpace / Goose 解耦推进方案
日期: 2026-07-02
适用目标:
- 把 MindSpace 从 Memind Portal 单体中拆出来,成为可以独立部署、独立扩容、独立迁移存储的服务。
- Memind 继续负责产品入口、用户会话、聊天体验、套餐/能力编排和业务路由。
- Goose 继续负责 agent 执行,但不直接拥有 MindSpace 状态,也不把某台机器的文件路径当作长期契约。
- 每次对话生成或上传的图片、文件、页面、公开链接等,都要形成一个可浏览、可迁移、可打包的 conversation package。
本文只描述架构目标、边界和推进计划,不要求一次性完成所有代码拆分。
## 0. 结论
应该拆,但不要按“105 = Control Portal103 = 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/<userId>` 路径作为产品级契约
```
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/<userId>/...`
- 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/<userId>/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/<userId>/conversations/<sessionId>/
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` artifactPNG 同步写入 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:<mindspace-port>`
- 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/<userId>` | 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 <branch-name>`,不要直接在脏工作区切分支。
- 每个阶段开始前先记录 `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. 给现有发布/下载/预览跑回归,再进入下一阶段。