270 lines
7.5 KiB
Markdown
270 lines
7.5 KiB
Markdown
# MindSpace Service Contract
|
||
|
||
日期: 2026-07-02
|
||
|
||
状态: Draft for P1/P2
|
||
|
||
生产拓扑更新(2026-07-03):
|
||
|
||
- 103 的 MindSpace Service 已独立部署在 `/Users/john/MindSpace`,服务为 `cn.tkmind.mindspace-service`,端口 `8082`。
|
||
- Portal live 目录仍是 `/Users/john/Project/Memind`,但它不再是 MindSpace Service 的根目录。
|
||
- `/Users/john/Project/Memind/MindSpace` 只能当旧链路兼容/存量目录处理;新排障和新配置必须优先看 [103 runtime topology](./103-runtime-topology.md)。
|
||
|
||
目标:
|
||
|
||
- 定义 MindSpace 从 Memind Portal 单体拆出前必须稳定下来的 API、存储、URL、权限和 package 契约。
|
||
- 允许先在单体内实现,再独立成进程。
|
||
- 保证 Memind App 和 Goose Runtime 不依赖 MindSpace 的物理部署位置。
|
||
|
||
## 1. 边界原则
|
||
|
||
MindSpace Service 是以下对象的唯一权威:
|
||
|
||
- Space
|
||
- Category
|
||
- Asset
|
||
- Asset Version
|
||
- Page
|
||
- Page Version
|
||
- Publication
|
||
- Conversation Package
|
||
- Package Manifest
|
||
- Canonical Public URL
|
||
- Storage Key 到 backing object 的校验
|
||
|
||
Memind App 只调用 API,不直接读写 `MindSpace/` 或 `data/mindspace/`。
|
||
|
||
Goose Runtime 只通过 MindSpace API/MCP 获得 scoped workspace capability,不直接拥有 MindSpace 业务状态。
|
||
|
||
## 2. API Surface
|
||
|
||
建议从当前 `/api/mindspace/v1/*` 延续,先在单体内实现兼容 facade:
|
||
|
||
```text
|
||
GET /api/mindspace/v1/spaces/current
|
||
GET /api/mindspace/v1/assets
|
||
POST /api/mindspace/v1/assets/uploads
|
||
GET /api/mindspace/v1/assets/:assetId/download
|
||
GET /api/mindspace/v1/pages
|
||
POST /api/mindspace/v1/pages
|
||
GET /api/mindspace/v1/pages/:pageId
|
||
POST /api/mindspace/v1/pages/:pageId/publish
|
||
GET /api/mindspace/v1/publications/:publicationId
|
||
GET /api/mindspace/v1/conversation-packages
|
||
POST /api/mindspace/v1/conversation-packages/ensure
|
||
GET /api/mindspace/v1/conversation-packages/:packageId
|
||
GET /api/mindspace/v1/conversation-packages/:packageId/manifest
|
||
POST /api/mindspace/v1/conversation-packages/:packageId/artifacts
|
||
```
|
||
|
||
所有响应必须包含稳定 ID,不把物理路径作为前端契约。
|
||
|
||
## 3. 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)
|
||
}
|
||
```
|
||
|
||
Adapter 要求:
|
||
|
||
- `key` 是相对 storage root 的逻辑 object key,禁止绝对路径。
|
||
- local fs adapter 可以把 key 映射到磁盘。
|
||
- NAS adapter 可以把 key 映射到共享挂载。
|
||
- S3 adapter 可以把 key 映射到 object prefix。
|
||
- 业务层不得依赖 adapter 的物理实现。
|
||
|
||
## 4. Canonical URL
|
||
|
||
URL 生成入口:
|
||
|
||
```ts
|
||
createPublicPageUrl({ userId, publicationId, pageId, slug })
|
||
createPublicAssetUrl({ userId, assetId, variant })
|
||
canonicalizeMindSpaceUrl(inputUrl)
|
||
validatePublicBackingObject({ publicationId })
|
||
```
|
||
|
||
规则:
|
||
|
||
- 新代码不得直接拼 `/MindSpace/<userId>/...`。
|
||
- 当前分支新增的 `mindspace-canonical-url.mjs` 是 P1 facade 起点;后续旧 helper 迁移到它后,再接入 backing object 校验。
|
||
- 当前分支新增的 `mindspace-service.mjs` 是组合门面起点;它先组合 package manifest、storage adapter、canonical URL,不接入现有运行链路。
|
||
- 旧 URL helper 暂时保留为 compatibility layer。
|
||
- public URL 必须能反查到 publication 或 asset。
|
||
- URL 返回前应校验 backing object 存在,或明确返回 pending 状态。
|
||
|
||
## 5. Conversation Package
|
||
|
||
Conversation package 是“每次对话文件夹包”的业务抽象。
|
||
|
||
逻辑 URI:
|
||
|
||
```text
|
||
mindspace://users/<userId>/conversations/<sessionId>
|
||
```
|
||
|
||
推荐 manifest:
|
||
|
||
```json
|
||
{
|
||
"schemaVersion": 1,
|
||
"packageId": "cp_xxx",
|
||
"userId": "u_xxx",
|
||
"sessionId": "s_xxx",
|
||
"title": "对话标题",
|
||
"storagePrefix": "users/u_xxx/conversations/s_xxx",
|
||
"artifacts": [
|
||
{
|
||
"artifactId": "ca_xxx",
|
||
"kind": "public_html",
|
||
"assetId": "asset_xxx",
|
||
"pageId": "page_xxx",
|
||
"publicationId": "pub_xxx",
|
||
"messageId": "msg_xxx",
|
||
"agentRunId": "run_xxx",
|
||
"displayName": "report.html",
|
||
"mimeType": "text/html",
|
||
"sizeBytes": 12345,
|
||
"storageKey": "users/u_xxx/conversations/s_xxx/public/report.html",
|
||
"canonicalUrl": "https://...",
|
||
"createdAt": 1783000000000
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
Artifact kind:
|
||
|
||
```text
|
||
input_image
|
||
input_file
|
||
generated_image
|
||
generated_file
|
||
page
|
||
public_html
|
||
long_image
|
||
thumbnail
|
||
docx
|
||
pdf
|
||
```
|
||
|
||
## 6. Metadata Tables
|
||
|
||
首选新增:
|
||
|
||
```sql
|
||
CREATE TABLE h5_conversation_packages (
|
||
id VARCHAR(64) PRIMARY KEY,
|
||
user_id CHAR(36) NOT NULL,
|
||
session_id VARCHAR(128) NOT NULL,
|
||
title VARCHAR(255) DEFAULT NULL,
|
||
status ENUM('active', 'archived', 'deleted') NOT NULL DEFAULT 'active',
|
||
storage_prefix VARCHAR(512) DEFAULT NULL,
|
||
manifest_asset_id CHAR(36) DEFAULT NULL,
|
||
created_at BIGINT NOT NULL,
|
||
updated_at BIGINT NOT NULL,
|
||
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 CHAR(36) DEFAULT NULL,
|
||
page_id CHAR(36) DEFAULT NULL,
|
||
publication_id CHAR(36) DEFAULT NULL,
|
||
agent_run_id CHAR(36) DEFAULT NULL,
|
||
message_id VARCHAR(128) DEFAULT NULL,
|
||
role VARCHAR(32) DEFAULT NULL,
|
||
artifact_kind VARCHAR(64) NOT NULL,
|
||
display_name VARCHAR(255) DEFAULT NULL,
|
||
storage_key VARCHAR(512) DEFAULT NULL,
|
||
canonical_url VARCHAR(512) DEFAULT NULL,
|
||
sort_order INT NOT NULL DEFAULT 0,
|
||
created_at BIGINT NOT NULL,
|
||
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)
|
||
);
|
||
```
|
||
|
||
迁移原则:
|
||
|
||
- 先新增表,不改旧表语义。
|
||
- 新写入链路双写 artifact 归属。
|
||
- 历史数据 best-effort 回填。
|
||
- 回填失败不影响旧页面访问。
|
||
|
||
## 7. Goose Capability
|
||
|
||
短期兼容:
|
||
|
||
```json
|
||
{
|
||
"sandboxRoot": "/absolute/local/MindSpace/<userId>",
|
||
"serverPath": "/absolute/local/mindspace-sandbox-mcp.mjs"
|
||
}
|
||
```
|
||
|
||
长期目标:
|
||
|
||
```json
|
||
{
|
||
"workspaceRef": "mindspace://users/<userId>/conversations/<sessionId>",
|
||
"packageId": "cp_xxx",
|
||
"apiBaseUrl": "https://mindspace.example.com",
|
||
"accessToken": "short-lived-scoped-token",
|
||
"allowedTools": ["read_file", "write_file", "edit_file", "publish_page"]
|
||
}
|
||
```
|
||
|
||
权限要求:
|
||
|
||
- Token 必须绑定 user、session、package。
|
||
- Token 必须有过期时间。
|
||
- Tool allowlist 必须由 Memind/MindSpace policy 共同决定。
|
||
- Goose 不得凭 token 访问其它用户 package。
|
||
|
||
## 8. Acceptance Tests
|
||
|
||
P1:
|
||
|
||
- local fs adapter 拒绝绝对 key 和 `..` traversal。
|
||
- canonical URL 只通过 facade 生成。
|
||
- 旧 `MindSpace/<userId>/public/*.html` 链路不变。
|
||
- `npm run verify:mindspace-publish-guards` 通过。
|
||
|
||
P2:
|
||
|
||
- 上传图片后 package manifest 出现 `input_image`。
|
||
- 生成 HTML 后 package manifest 出现 `public_html`。
|
||
- 发布页面后 artifact 关联 `pageId` 和 `publicationId`。
|
||
- 同一 session 多次生成按 `sortOrder` 或 `createdAt` 稳定排序。
|
||
|
||
P3:
|
||
|
||
- MindSpace Service 停止时,Memind App 明确返回服务不可用。
|
||
- 替换 local storage root 不需要改 Memind App / Goose 业务逻辑。
|
||
|
||
## 9. Non-Goals
|
||
|
||
当前阶段不做:
|
||
|
||
- 不迁移生产 103。
|
||
- 不切换 NAS/S3。
|
||
- 不删除旧 URL helper。
|
||
- 不改变现有 `/MindSpace/*` 公开访问行为。
|
||
- 不改变 Goose 当前可用的 `sandboxRoot` 兼容路径。
|