Add MindSpace decoupling foundation
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
# MindSpace Service Contract
|
||||
|
||||
日期: 2026-07-02
|
||||
|
||||
状态: Draft for P1/P2
|
||||
|
||||
目标:
|
||||
|
||||
- 定义 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>/...`。
|
||||
- 旧 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` 兼容路径。
|
||||
Reference in New Issue
Block a user