137 lines
8.2 KiB
Markdown
137 lines
8.2 KiB
Markdown
# AI 学习项目目录服务端协作契约
|
||
|
||
> 状态:Makelore 客户端与 Electron Main 已按本契约实现;运营后台、数据库、对象存储与生产数据不在本仓库内,需在 Works Square 侧实现并部署后联调。
|
||
|
||
## 1. 产品与权限边界
|
||
|
||
AI 学习是登录后可用的精选项目目录。V1 只包含项目列表、README 详情和 ZIP 下载,不包含课程生成、课程播放器、本地课程库、学习进度、Agent、ASR、评分或课堂 runtime,也不兼容旧课程接口。
|
||
|
||
沿用现有账号与 `module_access.learning` 策略:客户端入口策略在页面初始化前拦截无权限账号,Works Square 的列表、详情、媒体和下载接口仍必须独立校验登录态及 Learning 权限,不能依赖客户端置灰。
|
||
|
||
Renderer 的固定链路为 `Renderer -> Host API -> Electron Main -> Works Square`。Renderer 不得获得 Works Token、对象存储凭据、内部存储 key、任意下载 URL或本地保存路径。
|
||
|
||
## 2. 运营后台“学习项目管理”
|
||
|
||
运营后台新增一级菜单“学习项目管理”,至少支持新建、编辑、预览、发布、下架、排序和查看发布记录。项目字段如下:
|
||
|
||
| 字段 | 规则 |
|
||
| --- | --- |
|
||
| 项目名称 `name` | 必填,1–200 字符 |
|
||
| 摘要 `summary` | 必填,1–2,000 字符,用于卡片 |
|
||
| 封面 `cover` | 必填,PNG/JPEG/WebP,最大 10 MiB;服务端读取真实 MIME、尺寸并生成受控媒体 |
|
||
| 项目压缩包 `archive` | 必填 ZIP;运营上传可保留独立的 512 MiB 服务端限制,服务端流式记录字节数、计算 SHA-256 并验证 ZIP 签名;该上传限制不由客户端在下载时执行 |
|
||
| README `readme` | 必填 `.md`,UTF-8,最大 500 KiB;不接受可执行 HTML 作为发布内容 |
|
||
| 标签 `tags` | 0–16 个,每项 1–64 字符,去重 |
|
||
| 版本 `version` | 可空,最大 64 字符,仅作展示 |
|
||
| 排序 `sort_order` | 有界整数;列表默认按运营排序,再按发布时间稳定排序 |
|
||
| 状态 `status` | `draft`、`published`、`archived` |
|
||
|
||
上传中的对象不能直接进入公开目录。发布必须在一个事务/发布代际中冻结元数据、经过净化和 URL 校验的 README、封面及 ZIP 摘要;任一校验失败则整个发布失败,旧的已发布版本继续可读。下架后列表和详情立即不可见,但已有审计记录不能物理删除。
|
||
|
||
后台必须记录操作人、时间、发布代际、变更摘要、归档 SHA-256/字节数、README 警告和远程图片 URL 数量。客户端不提供任何运营上传或发布入口。
|
||
|
||
## 3. README 远程图片发布规则
|
||
|
||
README 中的远程图片在“发布”时由服务端解析 Markdown,并保留通过校验的原始 HTTPS URL;服务端不下载或处理图片字节。处理要求:
|
||
|
||
1. 只接受无用户名/密码的 HTTPS URL;拒绝 `http:`、`data:`、`file:`、本地路径和协议相对地址。
|
||
2. 只允许默认 HTTPS 端口且不允许 fragment;解析当前 DNS,任一结果属于 loopback、私网、链路本地、保留地址、云元数据或其他非公网地址时拒绝发布。
|
||
3. 限制 README 图片总量;不请求远端响应,因此不校验重定向、响应大小、MIME、像素、实际格式或内容。SVG 及其他 Electron 可渲染格式可直接显示。
|
||
4. 发布后的 Markdown 保留通过校验的 URL,不创建 README 图片 blob 或新 release-media 行;现有媒体路由继续用于封面和历史已镜像发布。
|
||
5. Markdown 原始 HTML 在客户端被禁用;服务端也从发布内容中移除 HTML 并返回 warning,避免运营误判展示效果。
|
||
6. 客户端请求会直接到第三方图片 origin;图片可用性、后续 DNS/重定向和格式支持由 origin 与 Electron 决定,origin 也会看到请求方网络信息。单图加载失败不得阻断 README 其余内容。
|
||
|
||
封面同样优先返回固定媒体路径。若返回 HTTPS CDN 地址,该地址必须无凭据、由 Works Square 控制且不包含用户隐私。
|
||
|
||
## 4. 客户端公开接口
|
||
|
||
| 方法 | Works Square 路径 | 用途 |
|
||
| --- | --- | --- |
|
||
| `GET` | `/api/learning/projects?cursor=&limit=` | 已发布项目分页列表 |
|
||
| `GET` | `/api/learning/projects/:id` | 项目详情和 README |
|
||
| `GET` | `/api/learning/projects/:id/media/:mediaId` | 受控封面/README raster 图片 |
|
||
| `GET` | `/api/learning/projects/:id/archive` | ZIP 字节流或同 Works origin 重定向 |
|
||
|
||
`limit` 为 1–48,默认 24;`cursor` 是不透明游标。空列表返回 `200`,不要用 `404`。列表不返回 `readmeMarkdown`、`archiveSha256`、`archiveFileName` 或任何存储字段。
|
||
|
||
### 列表响应
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"items": [
|
||
{
|
||
"id": "robot-arm",
|
||
"name": "桌面机械臂",
|
||
"summary": "从零搭建一个可以抓取积木的桌面机械臂。",
|
||
"cover": {
|
||
"url": "/api/learning/projects/robot-arm/media/cover",
|
||
"alt": "桌面机械臂成品",
|
||
"width": 1600,
|
||
"height": 900
|
||
},
|
||
"tags": ["机器人", "Python"],
|
||
"version": "1.2.0",
|
||
"archiveBytes": 12582912,
|
||
"publishedAt": "2026-08-01T00:00:00Z",
|
||
"updatedAt": "2026-08-18T00:00:00Z"
|
||
}
|
||
],
|
||
"nextCursor": null,
|
||
"total": 1
|
||
}
|
||
}
|
||
```
|
||
|
||
### 详情响应
|
||
|
||
详情复用全部列表字段,并增加:
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"data": {
|
||
"id": "robot-arm",
|
||
"name": "桌面机械臂",
|
||
"summary": "从零搭建一个可以抓取积木的桌面机械臂。",
|
||
"cover": {
|
||
"url": "/api/learning/projects/robot-arm/media/cover",
|
||
"alt": "桌面机械臂成品",
|
||
"width": 1600,
|
||
"height": 900
|
||
},
|
||
"tags": ["机器人", "Python"],
|
||
"version": "1.2.0",
|
||
"archiveBytes": 12582912,
|
||
"publishedAt": "2026-08-01T00:00:00Z",
|
||
"updatedAt": "2026-08-18T00:00:00Z",
|
||
"readmeMarkdown": "# 桌面机械臂\n\n",
|
||
"archiveFileName": "makelore-robot-arm-1.2.0.zip",
|
||
"archiveSha256": "64位小写十六进制SHA-256"
|
||
}
|
||
}
|
||
```
|
||
|
||
TypeScript 权威字段定义位于 [`shared/learning.ts`](../shared/learning.ts)。未列出的内部字段会被 Main 丢弃。
|
||
|
||
### 媒体与归档响应
|
||
|
||
- 媒体接口只用于封面和历史已镜像内容,返回受控 raster 内容并设置准确 `Content-Type` 与 `Content-Length`;Main 限制 10 MiB,并转换为 data URL 给 Renderer。新发布 README 的 HTTPS 图片不经过该接口。
|
||
- 归档接口返回 `application/zip`、`application/x-zip-compressed` 或 `application/octet-stream`;`Content-Length` 可省略,客户端也不使用它决定是否保存。如需重定向,只能跳转到与 Works API 相同 origin 的 HTTP(S) 地址,最多 5 跳;Main 不向重定向目标转发 Bearer。
|
||
- `archiveBytes` 是列表、详情和运营审计使用的展示元数据,不是客户端下载门禁。客户端不比较它、`Content-Length` 与实际流字节数,也不设置归档大小上限;客户端仍要求实际归档匹配 `archiveSha256` 和 ZIP 签名,验证失败时删除临时文件,不留下部分下载。
|
||
|
||
## 5. 错误、缓存与上线顺序
|
||
|
||
统一使用现有 `{ success, status, code, error }` 错误封装。至少支持:
|
||
|
||
- `LEARNING_AUTH_REQUIRED`(401);
|
||
- `LEARNING_FORBIDDEN`(403);
|
||
- `LEARNING_PROJECT_NOT_FOUND`(404);
|
||
- `LEARNING_CONFLICT`(409,发布代际变化);
|
||
- `LEARNING_UNAVAILABLE`(429/502/503)。
|
||
|
||
列表/详情可对发布代际生成 ETag;媒体和归档按内容摘要设置不可变缓存,但不得缓存带用户私有授权的响应到公共共享缓存。日志不得记录 Bearer、签名 URL、完整 README 图片 URL 或对象存储 key;可记录规范化主机、URL 摘要和图片数量。
|
||
|
||
上线顺序:先部署数据库/对象存储、运营后台、README HTTPS URL 校验和四个公开接口,再发布包含直连图片与无下载大小校验的新客户端;随后用真实账号完成发布/下架/远程图片(含 SVG、失效 origin 和隐私提示)/ZIP 联调,并覆盖缺失或不准确 `Content-Length` 的归档响应。旧 `/api/learning/courses`、generation/progress/runtime 接口不在新客户端兼容范围内,可按服务端消费者盘点结果独立退役。
|