CI / backend (push) Canceled after 0s
- README:富文本行改成「Web Vditor 编辑 / 安卓 Markwon 预览编辑」,e2e 15 项 - ROADMAP 第 5 节:勾掉表格与超过一两行的旧描述(表格是只读渲染, HTML 片段仍按文本显示);附件改为短期签名 raw_url;安卓从"未做"改成已完成 - 记下仍欠的三件事:保存后回列表而不是回详情、图片压缩/缩略图、离线缓存 验证:pytest 74 passed;richtext 15 / smoke 7 / tickets 8 / knowledge 7 全绿; tsc -b 0 错;vite build 通过;安卓 assembleDebug 与模拟器逐屏走通。
463 lines
19 KiB
Markdown
463 lines
19 KiB
Markdown
# 接口约定(M0–M3 已实现)
|
||
|
||
> 只写**已经在跑**的接口。写完没验证的东西不进这份文档。
|
||
> 前缀 `/api/v1`。所有响应都是同一个信封,**HTTP 200 不代表业务成功**。
|
||
|
||
## 0. 通用
|
||
|
||
### 0.1 信封
|
||
|
||
```json
|
||
{ "code": 0, "message": "ok", "data": { }, "request_id": "c4a8…" }
|
||
```
|
||
|
||
- `code == 0` 才是成功;非 0 时 `data` 里放结构化细节(例如冲突时的 `current_version`)。
|
||
- `request_id` 出现在响应头 `X-Request-Id` 与日志里,报障时带上它。
|
||
- 分页统一 `{items, next_cursor, has_more}`;`next_cursor` 为空即到底。
|
||
|
||
### 0.2 错误码
|
||
|
||
| 码 | HTTP | 含义 | 客户端该怎么处理 |
|
||
|---|---|---|---|
|
||
| 1001 | 400 | 参数不合法 | 提示并标红字段 |
|
||
| 1002 | 401 | 未认证 | 清 token → 登录页 |
|
||
| 1004 | 404 | 资源不存在 | — |
|
||
| 1009 | 409 | 唯一冲突(如 slug 已存在) | 换个名字 |
|
||
| 2001 | 401 | 缺少访问令牌 | 登录页 |
|
||
| 2002 | 401 | 令牌无效/过期 | 先试 refresh,失败再登录 |
|
||
| 2003 | 403 | 令牌类型不符(refresh 当 access 用) | 程序 bug |
|
||
| 2010 | 429 | 验证码请求太频繁 | 倒计时 60 秒 |
|
||
| 2011 | 400 | 验证码错误/已过期 | 重发 |
|
||
| 2012 | 403 | 账号被禁用 | 联系管理员 |
|
||
| 3001 | 404 | 工作区不存在 | — |
|
||
| 3003 | 403 | 不是该工作区成员 | **不要**提示"工作区不存在" |
|
||
| 3004 | 403 | 角色权限不足 | 隐藏对应按钮 |
|
||
| 3005 | 409 | 已经是成员 | 直接进工作区 |
|
||
| 3010 | 400 | 邀请码无效/过期 | 提示重新索取 |
|
||
| 4009 | 409 | 版本冲突(乐观锁) | 用 `data.current_version` 重试或刷新 |
|
||
| 5001 | 404 | 任务不存在 | 刷新列表 |
|
||
| 5002 | 409 | 非法状态流转 | 用 `data.allowed` 决定下一步给什么按钮 |
|
||
| 5003 | 404 | 标签不存在 | — |
|
||
|
||
> 错误码发布后**不许改号**,客户端靠它做分支。
|
||
|
||
### 0.3 认证
|
||
|
||
`Authorization: Bearer <access_token>`。`access` 30 分钟,`refresh` 14 天。
|
||
`refresh_token` 只能用于 `/auth/refresh`(服务端校验 `typ`)。
|
||
|
||
### 0.4 幂等
|
||
|
||
所有 `POST/PATCH/DELETE` 都可带 `Idempotency-Key: <uuid>`。
|
||
24 小时内同用户 + 同 key 返回首次结果,并带响应头 `X-Idempotent-Replay: 1`。
|
||
**只缓存 2xx/3xx**,失败不缓存,所以重试是安全的。
|
||
|
||
### 0.5 并发
|
||
|
||
改动类接口(`PATCH /items/{id}`、`POST /items/{id}/status`、`DELETE`)必须带 `version`。
|
||
拿不到 `version` 就别改——先 `GET` 一次。
|
||
|
||
---
|
||
|
||
## 1. 认证 `/auth`
|
||
|
||
### `POST /auth/sms/send`
|
||
|
||
```json
|
||
{ "phone": "13800000000" }
|
||
```
|
||
|
||
响应 `data: { dev_code?: "123456" }`。`ENV=dev` 时才回显验证码,生产没有这个字段。
|
||
60 秒内重复请求 → `2010`。
|
||
|
||
### `POST /auth/login`
|
||
|
||
```json
|
||
{ "phone": "13800000000", "code": "123456" }
|
||
```
|
||
|
||
```json
|
||
{ "user": { "id": "…", "phone": "138****0000", "nickname": "用户0000" },
|
||
"tokens": { "access_token": "…", "refresh_token": "…", "token_type": "bearer", "expires_in": 1800 } }
|
||
```
|
||
|
||
手机号**首次登录即注册**(昵称取"用户" + 后四位)。手机号回显永远是打码的。
|
||
|
||
### `POST /auth/refresh`
|
||
|
||
```json
|
||
{ "refresh_token": "…" }
|
||
```
|
||
|
||
返回同形状的 `tokens`。无状态 JWT,**服务端不维护黑名单**——token 泄露只能等过期,这是 M0 已知取舍。
|
||
|
||
### `GET /auth/me`
|
||
|
||
返回 `{ user, workspaces[] }`,`workspaces[].role` 是当前用户在该工作区的角色。
|
||
|
||
---
|
||
|
||
## 2. 工作区 `/workspaces`
|
||
|
||
| 方法 | 路径 | 权限 | 说明 |
|
||
|---|---|---|---|
|
||
| POST | `/workspaces` | 登录 | 建工作区,创建者自动成为 `owner` |
|
||
| GET | `/workspaces` | 登录 | 我加入的工作区 |
|
||
| GET | `/workspaces/{ws_id}` | 成员 | 详情 |
|
||
| GET | `/workspaces/{ws_id}/members` | 成员 | 游标分页 |
|
||
| POST | `/workspaces/{ws_id}/invitations` | admin+ | 生成邀请码,`{role}`;**只有 owner 能邀请 admin** |
|
||
| POST | `/workspaces/{ws_id}/join` | 登录 | `{code}` 凭码加入,邀请码一次性 |
|
||
|
||
角色等级:`owner(4) > admin(3) > member(2) > guest(1)`。
|
||
|
||
---
|
||
|
||
## 3. 任务 `/workspaces/{ws_id}/items`
|
||
|
||
所有接口都要求**是该工作区成员**;跨租户一律 `3003`,任务不存在一律 `5001`(不区分"没有"和"不是你的")。
|
||
|
||
### 3.1 `GET /items`
|
||
|
||
| 参数 | 取值 | 说明 |
|
||
|---|---|---|
|
||
| `view` | `inbox` `active` `today` `week` `done` `canceled` `all` `deleted` | 默认 `active`(todo+doing+blocked) |
|
||
| `status` | 可重复 | 与 `view` 叠加时取更严的 |
|
||
| `assignee` | `me` 或 uuid | |
|
||
| `tag` | 标签名 | |
|
||
| `q` | 关键词 | 标题 + 正文,`ILIKE %q%` |
|
||
| `sort` | `updated`(默认) `due` `created` | `due` 不分页 |
|
||
| `include_deleted` | 布尔 | 与 `view=all` 一起用 |
|
||
| `limit` | 1–100,默认 20 | |
|
||
| `cursor` | 上一页的 `next_cursor` | |
|
||
|
||
`today` / `week` 的边界按 `settings.default_timezone`(默认 `Asia/Shanghai`)算,
|
||
用**半开区间** `[今天 00:00, 明天 00:00)`,不用 UTC 切日。
|
||
|
||
### 3.2 `POST /items`
|
||
|
||
```json
|
||
{ "title": "今天要交的周报", "body_md": "", "due_at": "2026-10-28T18:00:00+08:00",
|
||
"priority": 1, "assignee_id": null, "status": "inbox", "tag_names": ["周报"] }
|
||
```
|
||
|
||
`status` 默认 `inbox`(进收件箱,先归类再排期)。返回完整 `item`(含 `tags[]`、`version`)。
|
||
|
||
### 3.3 `PATCH /items/{item_id}`
|
||
|
||
**必须带 `version`**。`null` 与"没传"含义不同:
|
||
|
||
- 传 `due_at: null` → 表示"清空"要用 `"clear_due_at": true`
|
||
- 同理 `clear_assignee`、`clear_tags`
|
||
- `tag_names` 一旦传就是**整体替换**
|
||
|
||
冲突回 `4009` 且 `data.current_version` 给最新版本号。
|
||
|
||
### 3.4 `POST /items/{item_id}/status`
|
||
|
||
```json
|
||
{ "status": "doing", "version": 3 }
|
||
```
|
||
|
||
状态机(`backend/app/domain/task.py`):
|
||
|
||
```
|
||
inbox → todo, canceled
|
||
todo → inbox, doing, done, canceled
|
||
doing → todo, done, blocked, canceled
|
||
blocked → doing, canceled
|
||
done → todo (重开)
|
||
canceled→ todo (恢复)
|
||
```
|
||
|
||
非法流转回 `5002`,`data = { from, to, allowed }`。
|
||
**前端可以据此直接渲染"下一步能点什么",不用自己维护一份规则。**
|
||
目标状态与当前相同:直接返回,版本号不变。
|
||
|
||
### 3.5 `POST /items/batch-status`(看板多选拖拽)
|
||
|
||
```json
|
||
{ "status": "done",
|
||
"items": [{ "id": "…", "version": 3 }, { "id": "…", "version": 7 }] }
|
||
```
|
||
|
||
```json
|
||
{ "updated": [{ "id": "…", "version": 4, "unchanged": false }],
|
||
"failed": [{ "id": "…", "code": 4009, "message": "…", "current_version": 8 }] }
|
||
```
|
||
|
||
- **不是全成功或全回滚**:拖 10 条里有 1 条被同事改过,其余 9 条照样生效。
|
||
全回滚会让用户白干一次,而且更难解释"为什么什么都没变"。
|
||
- 单次最多 50 条;`failed[].code` 只会是 `4009`(版本冲突)、`5002`(状态不允许)、`5001`(不存在)。
|
||
- 已经是目标状态的条目返回 `unchanged: true`,计入 `updated` 但不产生事件。
|
||
|
||
### 3.6 `GET /items/board`(看板一屏数据)
|
||
|
||
```json
|
||
{ "columns": { "todo": [...], "doing": [...], "blocked": [...], "done": [...] },
|
||
"order": ["todo", "doing", "blocked", "done"],
|
||
"labels": { "todo": "待办", "doing": "进行中", "blocked": "被阻塞", "done": "已完成" } }
|
||
```
|
||
|
||
- 每列默认 30 条(`per_column` 1–100),按"截止早的在前、无截止靠后",再按更新时间。
|
||
- **收件箱不在看板上**:没归类的东西不该占着"正在做的事"的位置。
|
||
- 一次请求拿全,避免拖拽时四列数据不同步。
|
||
|
||
### 3.7 `DELETE /items/{item_id}?version=3`
|
||
|
||
软删(写 `deleted_at`),只在 `view=deleted` / `include_deleted=true` 时可见。
|
||
|
||
### 3.8 评论
|
||
|
||
| 方法 | 路径 | 说明 |
|
||
|---|---|---|
|
||
| GET | `/items/{item_id}/comments` | 时间正序,游标分页 |
|
||
| POST | `/items/{item_id}/comments` | `{body_md}` |
|
||
|
||
### 3.9 标签 `/workspaces/{ws_id}/tags`
|
||
|
||
| 方法 | 路径 | 说明 |
|
||
|---|---|---|
|
||
| GET | `/tags` | 工作区内标签,`{id,name,color}` |
|
||
| POST | `/tags` | `{name, color?}`;同名直接返回已有的(幂等,不报错) |
|
||
|
||
---
|
||
|
||
## 4. 工单 `/workspaces/{ws_id}`
|
||
|
||
工单**不是**另一张表里的另一种东西,它是一条 `kind='ticket'` 的 item 加一行 `ticket_ext`。
|
||
所以详情仍然走 `GET /items/{item_id}`,响应里多一个 `ticket` 字段。
|
||
|
||
### 4.1 `POST /items/{item_id}/promote`(任务升级成工单)
|
||
|
||
```json
|
||
{ "assignee_id": "uuid", "title": "可选覆盖", "body_md": "可选补充",
|
||
"due_at": "2026-10-30T10:00:00+08:00", "priority": 2, "version": 3 }
|
||
```
|
||
|
||
返回 `{ "task": {...}, "ticket": {...} }`。
|
||
|
||
- 原任务**留在原地**变成 `blocked`,不搬数据——时间线完整保留。
|
||
- 一个任务只能升级一次,重复升级 → `6002`。
|
||
- `assignee_id` 必须是工作区内 `active` 成员,否则 `1001`。
|
||
- 传 `version` 走乐观锁,`4009` 表示任务刚被别人改过。
|
||
- 被指派人立刻收到 `ticket_assigned` 通知。
|
||
|
||
### 4.2 工单动作
|
||
|
||
| 方法 | 路径 | 谁可以 | 前置状态 → 结果 |
|
||
|---|---|---|---|
|
||
| POST | `/items/{item_id}/ticket/accept` | 办理人 / admin+ | open → accepted |
|
||
| POST | `/items/{item_id}/ticket/start` | 办理人 / admin+ | accepted → in_progress |
|
||
| POST | `/items/{item_id}/ticket/submit` | 办理人 / admin+ | in_progress → pending_verify |
|
||
| POST | `/items/{item_id}/ticket/verify` | **需求人** / admin+ | pending_verify → done / in_progress |
|
||
| POST | `/items/{item_id}/ticket/reassign` | 需求人 / admin+ | 任意未完成 → open(清空受理时间) |
|
||
| POST | `/items/{item_id}/ticket/cancel` | 需求人 / 办理人 / admin+ | 未完成 → canceled |
|
||
|
||
`verify` 的请求体:`{ "approve": true, "note": "可选说明", "resolution_md": "验收结论" }`。
|
||
|
||
- 驳回时 `approve=false`,回到 `in_progress`(不是 `open`——人还在做,不必重新认领),`reject_count` 加一。
|
||
- **办理人不能验收自己的工单**,否则 `6004`。这是唯一一条"就算 owner 也不通融"的规则之外的角色限制。
|
||
- `resolution_md` 是 M3 知识草稿的原料,尽量写。
|
||
- 前置状态不对 → `6003`,`details` 同时给 `allowed`(当前状态还能去哪)与 `allowed_from`(该动作要求的前置状态)。
|
||
|
||
### 4.3 工单列表 `GET /tickets`
|
||
|
||
| 参数 | 说明 |
|
||
|---|---|
|
||
| `role` | `assigned`(派给我的,默认)/ `requested`(我发起的)/ `all` |
|
||
| `status` | 可重复,如 `?status=open&status=accepted` |
|
||
| `limit` / `cursor` | 与任务列表一致 |
|
||
|
||
### 4.4 事项关系 `GET|POST /items/{item_id}/links`
|
||
|
||
```json
|
||
{ "to_item_id": "uuid", "link_type": "parent" }
|
||
```
|
||
|
||
`GET` 返回 `{ "children": [...], "parents": [...], "related": [...] }`。
|
||
`link_type` 只有 `parent` / `related`,**方向是 from 是 to 的子项**;自环 `5005`、重复挂 `5005`、对方不存在 `5004`、跨租户 `5001`。
|
||
|
||
### 4.5 通知 `/workspaces/{ws_id}/notifications`
|
||
|
||
| 方法 | 路径 | 说明 |
|
||
|---|---|---|
|
||
| GET | `/notifications?unread_only=true` | 我的通知,新的在前 |
|
||
| GET | `/notifications/unread-count` | `{ "unread": 3 }`,轮询用 |
|
||
| POST | `/notifications/{id}/read` | 标一条 |
|
||
| POST | `/notifications/read-all` | 全标已读 |
|
||
|
||
`kind`:`ticket_assigned` / `ticket_submitted` / `ticket_approved` / `ticket_rejected` / `task_unblocked`。
|
||
通知只发给当事人,同工作区里其他人看不到——这是"协同"与"广播"的区别。
|
||
|
||
### 4.6 工单的错误码
|
||
|
||
| 码 | HTTP | 含义 |
|
||
|---|---|---|
|
||
| 6001 | 404 | 工单不存在,或不是工单 |
|
||
| 6002 | 409 | 这个任务已经升级过了 |
|
||
| 6003 | 409 | 当前状态不允许这个动作 |
|
||
| 6004 | 403 | 你没有权限做这个动作(例如自验) |
|
||
| 6005 | 404 | 通知不存在,或不是你的 |
|
||
|
||
---
|
||
|
||
## 5. 知识 `/workspaces/{ws_id}`
|
||
|
||
### 5.1 `GET /knowledge`
|
||
|
||
| 参数 | 说明 |
|
||
|---|---|
|
||
| `status` | 可重复;`draft`/`in_review`/`published`/`archived`,不传为全部 |
|
||
| `doc_type` | `note`/`howto`/`faq`/`postmortem`/`decision` |
|
||
| `source_item_id` | 按来源事项筛 |
|
||
| `q` | 标题 / 正文 / 摘要子串 |
|
||
| `limit` | 1–100,默认 30 |
|
||
| `cursor` | 上一页的 `next_cursor` |
|
||
|
||
返回 `{items, next_cursor, has_more}`。每条是 `item` 字段 + `knowledge` + `source`:
|
||
|
||
```json
|
||
{
|
||
"id": "…", "kind": "knowledge", "title": "发票原件必须一起寄",
|
||
"body_md": "…", "status": "published", "tags": [],
|
||
"knowledge": {
|
||
"doc_type": "howto", "summary": "…", "status_hint": "published",
|
||
"source_kind": "task", "source_item_id": "…", "generated_by": "human",
|
||
"published_at": "2026-10-28T05:54:44Z", "archived_at": null,
|
||
"chunk_count": 0, "author_id": "…"
|
||
},
|
||
"source": {"id": "…", "kind": "task", "title": "发票原件必须一起寄", "status": "done"}
|
||
}
|
||
```
|
||
|
||
> `status` 与 `knowledge.status_hint` 是同一件事:前者在 `item` 上(复用事项表),
|
||
> 后者在 `knowledge_doc` 上(列表按知识口径筛时不必回连 `item`)。**两者必须同步**,
|
||
> 状态动作接口会一起写。
|
||
|
||
### 5.2 `GET /knowledge/{item_id}`
|
||
|
||
详情,附最近 50 条 `activities`。不存在或跨租户统一 `7001`。
|
||
|
||
### 5.3 `POST /knowledge` — 手记或指定来源
|
||
|
||
```json
|
||
{"title": "报销单要贴发票原件", "body_md": "…", "doc_type": "note",
|
||
"source_item_id": "可选的来源事项 id"}
|
||
```
|
||
|
||
- 不带 `source_item_id` → `source_kind=manual`(手记),允许没有来源。
|
||
- 带 → 来源必须是同工作区的 `task`/`ticket`,否则 `7005`。
|
||
- 新知识一律 `draft`,不会直接对外可见。
|
||
|
||
### 5.4 `PATCH /knowledge/{item_id}` — 修改内容
|
||
|
||
必须带 `version`(乐观锁,冲突 `4009`)。可改 `title`/`body_md`/`summary`/`doc_type`。
|
||
**已发布的不许直接改**(`7003`,先撤回审议或归档),避免"发布后被悄悄改写"。
|
||
|
||
### 5.5 `POST /knowledge/{item_id}/status` — 状态动作
|
||
|
||
| action | 允许的前置状态 | 结果 | 谁能做 |
|
||
|---|---|---|---|
|
||
| `submit_review` | draft | in_review | 成员 |
|
||
| `withdraw` | in_review | draft | 成员 |
|
||
| `publish` | draft / in_review | published | owner / admin |
|
||
| `archive` | draft / in_review / published | archived | 成员 |
|
||
| `restore` | archived | draft | 成员 |
|
||
|
||
非法动作返回 `7003`,`details.allowed_from` 是动作要求的前置状态,`details.allowed_actions`
|
||
是当前状态下还能做什么——前端据此渲染按钮,不用自己维护一份状态机。
|
||
|
||
**发布要 owner/admin 的理由**:发布等于团队为这条结论背书,不能谁都能推。
|
||
|
||
### 5.6 `POST /items/{item_id}/knowledge` — 把一件事沉淀成知识
|
||
|
||
Query:`doc_type`(默认 `postmortem`)。来源可以是任务或工单;工单会带出验收结论。
|
||
返回与 5.2 相同的知识对象;同一来源重复沉淀 → `7002`。
|
||
|
||
**工单验收通过时,后端会自动调用这条逻辑**(`generated_by=rule`,`doc_type=postmortem`,
|
||
正文开头固定一句"这份草稿由系统根据来源自动整理,请先核对再发布"),所以用户不需要记得点。
|
||
|
||
### 5.7 知识的错误码
|
||
|
||
| 码 | HTTP | 含义 |
|
||
|---|---|---|
|
||
| 7001 | 404 | 知识不存在,或不是知识 |
|
||
| 7002 | 409 | 这个来源已经有知识了 |
|
||
| 7003 | 409 | 当前状态不允许这个动作 |
|
||
| 7004 | 403 | 权限不足(如非管理员发布) |
|
||
| 7005 | 409 | 来源事项无效或缺失 |
|
||
|
||
---
|
||
|
||
## 6. 附件与富文本
|
||
|
||
### 6.1 存储格式:Markdown + 附件表
|
||
|
||
正文类字段(任务 `body_md`、工单说明、知识正文、评论、验收结论)**一律存 Markdown**,不存 HTML。
|
||
理由:能导出、能全文检索、能直接喂给 LLM;HTML 每次渲染都要重新清洗一遍,是隐患。
|
||
|
||
图片不是 base64 塞进正文,而是走附件接口拿地址。**库里存的是不带凭据的相对路径**:
|
||
|
||
```markdown
|
||

|
||
```
|
||
|
||
| 方法 | 路径 | 说明 |
|
||
|---|---|---|
|
||
| POST | `/workspaces/{ws_id}/items/{item_id}/attachments` | multipart,字段名 `file`;返回 201 + 附件对象 |
|
||
| GET | `/workspaces/{ws_id}/items/{item_id}/attachments` | `{items: [...]}`,每条含 `raw_url` |
|
||
| GET | `/workspaces/{ws_id}/attachments/{id}/download` | 下载(`Content-Disposition: attachment`) |
|
||
| GET | `/workspaces/{ws_id}/attachments/{id}/raw?exp=&sig=` | 直链,给 `<img src>` 用(签名见 6.2) |
|
||
| DELETE | `/workspaces/{ws_id}/attachments/{id}?confirm=true` | 上传者或管理员可删;不传 confirm 报 `8004` |
|
||
|
||
附件对象的字段:`id / item_id / filename / content_type / size / sha256 / uploader_id / created_at / raw_url / can_delete`。
|
||
`raw_url` 是**后端签发的短期直链**(默认 30 分钟),客户端直接用它渲染,不需要自己拼令牌。
|
||
事项与知识的列表接口会附带 `attachment_count`,详情接口附带 `attachments` 全量。
|
||
|
||
### 6.2 图片直链:短期签名,而不是把令牌写进正文
|
||
|
||
`<img>` 发不出 `Authorization` 头。两种解法:把图片读成 base64 内联(正文膨胀几十倍,破坏检索),
|
||
或者给一个短期签名地址。取后者:
|
||
|
||
- `raw_url` 形如 `…/raw?exp=1730000000&sig=<hmac>`,签名载荷是 `workspace_id:attachment_id:exp`,
|
||
密钥从 `JWT_SECRET` 域分离派生(`attachment-raw-v1`),不与登录令牌共用。
|
||
- 校验只看签名与过期时间,**不需要用户已登录**;签名里绑了 `workspace_id`,A 工作区签的链接
|
||
打到 B 工作区会被拒(`8003`)。
|
||
- 过期不宽限,刷新页面即可拿到新链接。
|
||
- 兼容旧路径:不带 `sig` 时仍可用 `?token=<access>` 走成员校验,供老客户端过渡。
|
||
|
||
为什么非要这样:令牌写进正文会落库,几天后过期就是满篇红叉;用户换设备后旧令牌失效,图片全挂。
|
||
把凭据放在"每次渲染时现签"的 URL 上,正文才能长期存活。
|
||
|
||
### 6.3 附件错误码
|
||
|
||
### 6.3 附件错误码
|
||
|
||
| 码 | HTTP | 含义 |
|
||
|---|---|---|
|
||
| 8001 | 404 | 附件不存在,或不属于这个工作区 |
|
||
| 8002 | 413 | 超过 `MAX_UPLOAD_MB`(默认 20) |
|
||
| 8003 | 403 | 不是上传者也不是管理员 |
|
||
| 8004 | 409 | 需要 `?confirm=true`(删除是破坏性动作) |
|
||
|
||
### 6.4 前端约定
|
||
|
||
- 编辑器写 Markdown,工具栏/快捷键 `Ctrl+B`、`Ctrl+I` 只管插入语法;预览用 `renderMarkdown()`
|
||
渲染(先转义再上标签,链接协议白名单 `http(s)/mailto//`)。
|
||
- 粘贴或拖入图片时:先上传 → 拿到 `id` → 在光标处插入 `)`。
|
||
- 上传接口自己带 `Idempotency-Key`,**不要**手动设 `Content-Type`(浏览器要自己带 boundary)。
|
||
|
||
---
|
||
|
||
## 7. 健康与版本
|
||
|
||
| 方法 | 路径 | 说明 |
|
||
|---|---|---|
|
||
| GET | `/health` | `{status, db}`,DB 探活用 `SELECT 1` |
|
||
| GET | `/version` | `{commit, build_time, db_role}` |
|
||
|
||
`db_role` 里的 `is_super / bypass_rls / read_all` 全为 `false` 才说明 RLS 真的生效——
|
||
这是部署后的第一道检查,比"能登录"重要得多。
|
||
|
||
部署校验:`curl /version` 的 `commit` 必须等于 `git rev-parse --short HEAD`。
|