forked from bai/curriculum-project-hub
chore: 删除 .omo/ 与文件库-接口契约.md
.omo/ 下 12 个 run-continuation/ses_*.json 是 agent 会话续跑状态, 机器生成,本不该进版本库;文件库-开工计划.md 一并删除。 《文件库-接口契约.md》(C/D 编号)同时删除。两份文档的内容都可从 git 历史取回。 代码注释里的 C/D 编号(契约 8.1、C2、C4、D11–D19 等)因此不再有在库 文档可查,分布在 filelib 的 model / grantService / treeService / guards、prisma schema 与迁移、以及 ADR-0028。README 原先按路径引用 这两份文档,现改为说明出处与取回方式。
This commit is contained in:
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"sessionID": "ses_0705f8afbffePWIq7GrFfwfFwV",
|
|
||||||
"updatedAt": "2026-07-23T15:40:15.630Z",
|
|
||||||
"sources": {
|
|
||||||
"background-task": {
|
|
||||||
"state": "idle",
|
|
||||||
"updatedAt": "2026-07-23T15:40:15.630Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"sessionID": "ses_07252990dffeRFmXnvdyynty2z",
|
|
||||||
"updatedAt": "2026-07-23T06:33:33.086Z",
|
|
||||||
"sources": {
|
|
||||||
"background-task": {
|
|
||||||
"state": "idle",
|
|
||||||
"updatedAt": "2026-07-23T06:33:33.086Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"sessionID": "ses_075d214a0ffe6mU8VNgukUfH5L",
|
|
||||||
"updatedAt": "2026-07-22T14:15:01.741Z",
|
|
||||||
"sources": {
|
|
||||||
"background-task": {
|
|
||||||
"state": "idle",
|
|
||||||
"updatedAt": "2026-07-22T14:15:01.741Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"sessionID": "ses_075d3282bffejz0HFE00taOaGM",
|
|
||||||
"updatedAt": "2026-07-22T14:13:54.785Z",
|
|
||||||
"sources": {
|
|
||||||
"background-task": {
|
|
||||||
"state": "idle",
|
|
||||||
"updatedAt": "2026-07-22T14:13:54.785Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"sessionID": "ses_0781484caffeKJHXVKiD4BqEJw",
|
|
||||||
"updatedAt": "2026-07-22T06:17:18.238Z",
|
|
||||||
"sources": {
|
|
||||||
"background-task": {
|
|
||||||
"state": "idle",
|
|
||||||
"updatedAt": "2026-07-22T06:17:18.238Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"sessionID": "ses_07b163c13ffeudULxSfIfK986T",
|
|
||||||
"updatedAt": "2026-07-21T13:55:58.878Z",
|
|
||||||
"sources": {
|
|
||||||
"background-task": {
|
|
||||||
"state": "idle",
|
|
||||||
"updatedAt": "2026-07-21T13:55:58.878Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"sessionID": "ses_07b1e6c8effeq7bdmj6NgO0LqC",
|
|
||||||
"updatedAt": "2026-07-21T13:38:01.132Z",
|
|
||||||
"sources": {
|
|
||||||
"background-task": {
|
|
||||||
"state": "idle",
|
|
||||||
"updatedAt": "2026-07-21T13:38:01.132Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"sessionID": "ses_07b932dddffecSipFLwlJOvoJ9",
|
|
||||||
"updatedAt": "2026-07-21T11:27:10.420Z",
|
|
||||||
"sources": {
|
|
||||||
"background-task": {
|
|
||||||
"state": "idle",
|
|
||||||
"updatedAt": "2026-07-21T11:27:10.420Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"sessionID": "ses_07b9c4268ffeic4mMXCPUt225j",
|
|
||||||
"updatedAt": "2026-07-21T11:18:53.449Z",
|
|
||||||
"sources": {
|
|
||||||
"background-task": {
|
|
||||||
"state": "idle",
|
|
||||||
"updatedAt": "2026-07-21T11:18:53.449Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"sessionID": "ses_07b9c4346ffeVBdV5pf4h6s9PW",
|
|
||||||
"updatedAt": "2026-07-21T11:18:29.979Z",
|
|
||||||
"sources": {
|
|
||||||
"background-task": {
|
|
||||||
"state": "idle",
|
|
||||||
"updatedAt": "2026-07-21T11:18:29.979Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"sessionID": "ses_07b9c9a75ffeOyJ4ewp3DBvXhz",
|
|
||||||
"updatedAt": "2026-07-21T11:21:09.776Z",
|
|
||||||
"sources": {
|
|
||||||
"background-task": {
|
|
||||||
"state": "idle",
|
|
||||||
"updatedAt": "2026-07-21T11:21:09.776Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"sessionID": "ses_07cdbac0effeiBrdcHLUUUfwuF",
|
|
||||||
"updatedAt": "2026-07-23T15:37:59.918Z",
|
|
||||||
"sources": {
|
|
||||||
"background-task": {
|
|
||||||
"state": "idle",
|
|
||||||
"updatedAt": "2026-07-23T15:37:59.918Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
# 文件库 · 开工计划(v1.0)
|
|
||||||
|
|
||||||
> 配套文档:《文件库-接口契约.md》(v0.1,仓库根目录)。本计划已吸收规划顾问评审意见,包含开工前必须先冻结的**架构收口决策 D11–D19**(回写契约 v0.2 时并入)。
|
|
||||||
> 工期按 1 人全栈估算;前端从 Phase 2 后可并行。
|
|
||||||
|
|
||||||
## 开工判定
|
|
||||||
|
|
||||||
**可以开工。** 四个外部依赖(版本包 / Group / 审计 / 平台身份)全部有契约可依、可 mock 并行开发;无阻塞项。唯一前置:本文件的 D11–D19 决策在写第一行 schema 前过一遍(半天,自查即可,有异议再升级)。
|
|
||||||
|
|
||||||
## 架构收口决策(新增,先冻结再动工)
|
|
||||||
|
|
||||||
| 编号 | 决策 | 理由 |
|
|
||||||
|---|---|---|
|
|
||||||
| D11 | `creator` 是 Node 的**不可变列**,创建时同时落一条 Manage grant;独立权限开关关闭时,**creator 的 Manage 仍生效**,其余项目级 grant 冻结不参与计算 | 否则 creator 可能被自己关掉的开关锁死 |
|
|
||||||
| D12 | 移动节点授权 = **本节点 Manage + 目标父 Edit+**;移动在事务 + Postgres 咨询锁内完成,防并发成环 | 预检环检测在并发下不安全 |
|
|
||||||
| D13 | Group resolve 失败返回 **503**(不伪装 404/403);每次 HTTP 请求 fresh resolve,不跨请求缓存;单请求内多次校验合并为一次调用(此点与 Group 团队确认,OPEN-9) | 依赖故障 ≠ 无权限;契约 G4 要求实时 |
|
|
||||||
| D14 | 命名规则:NFC 归一化、trim、≤128 字符、禁 `/` 与控制字符;**活跃兄弟节点大小写不敏感唯一**;根节点全局唯一 | 缺命名规则必有脏数据 |
|
|
||||||
| D15 | 删除 = 只给被删节点打标,后代**不递归打标**,靠"任一祖先已删"过滤;所有查询链路强制此过滤(含递归 SQL,不只靠 Prisma middleware) | 递归打标的写放大与恢复复杂度不可控 |
|
|
||||||
| D16 | `baseVersion` 为**文件级版本**(GET file 返回的 version);无关文件的提交不产生冲突 | 与 C1 `head(dir, filePath)` 语义一致 |
|
|
||||||
| D17 | breadcrumb 只暴露用户有 View 的祖先名称;无权限祖先显示占位符 `…`,不暴露名字 | 404 不泄露语义(D8)延伸到面包屑 |
|
|
||||||
| D18 | v1 **单副本部署**(git 仓库在本地磁盘);多副本需共享存储或分片,后置 | 不解决不存在的扩展问题 |
|
|
||||||
| D19 | 网站管理员**不隐式读内容**;force_adjust 凭 node id 操作(id 从审计或用户上报获得) | C4 已定的最小权限原则 |
|
|
||||||
|
|
||||||
## 阶段计划
|
|
||||||
|
|
||||||
### Phase 0 · 骨架与身份先行(1–2 天)
|
|
||||||
|
|
||||||
- [ ] 过 D11–D19,回写契约 v0.2
|
|
||||||
- [ ] repo 骨架 `server/` + `web/`,Postgres docker-compose,CI(lint / typecheck / vitest / 集成测试用真实 Postgres service container)
|
|
||||||
- [ ] **JWT 中间件 + 本地 JWKS 测试服务 + 签名 token fixtures**(有效/过期/错 issuer/错 audience/错算法/未知 kid/轮换)——身份先行,不做"宽松 mock 身份"
|
|
||||||
- [ ] 4 个 port 接口定义 + mock:`VersionStore`、`GroupResolver`、`AuditSink`(直写 outbox 表的实现就是真的,mock 的是对端服务)、`ExportAdapter`
|
|
||||||
- [ ] fastify-swagger 接入,OpenAPI 骨架——**OpenAPI 随每个端点同步产出,不留到最后**
|
|
||||||
- **验收**:骨架可 `pnpm dev` 起服务;本地 JWKS 六类 token fixture(有效/过期/错 issuer/错 audience/错算法/未知 kid)中间件行为全对;CI 流水线绿
|
|
||||||
|
|
||||||
### Phase 1 · 数据模型与权限引擎(2–3 天)⚑ 心脏
|
|
||||||
|
|
||||||
- [ ] Prisma schema:`Node`(parentId **权威** + id 编码的 materialized path 派生列,name 不入 path)、`Grant`、`ProjectIndependentSettings`、`OutboxEvent`(含 attempts/nextAttemptAt/lease/lastError,relay 字段一次到位)、`ExportJob`
|
|
||||||
- [ ] 数据库约束:kind 枚举、项目无子节点、活跃兄弟名唯一(部分唯一索引,root 的 NULL parent 特殊处理)、活跃 grant 按 (nodeId, principalType, principalId) 唯一、独立设置仅项目
|
|
||||||
- [ ] **纯权限 reducer**:输入已解析的 grants + 祖先链 + 用户组集合,输出 role|null;不碰 DB/网络。fast-check 属性测试:max 单调、加 grant 只升不降、祖先继承、toggle 行为、顺序无关、软删过滤
|
|
||||||
- [ ] 数据获取层 + `effective(user, node)` 组装
|
|
||||||
- [ ] 树操作服务:create(creator 自动 Manage、root 仅管理员且必带 ≥1 Manage)、rename、move(D12 事务+咨询锁)、soft delete(D15)、breadcrumb(D17)
|
|
||||||
- **验收**:并发对移(A→B 与 B→A)、移动+并发建子、property tests、删除祖先过滤,真实 Postgres 全绿
|
|
||||||
|
|
||||||
### Phase 2 · 树与授权 API(1–2 天)
|
|
||||||
|
|
||||||
- [ ] nodes 端点组 + grants 端点组 + independent-permission 开关 + effective-permission 自查
|
|
||||||
- [ ] 授权矩阵校验(契约 8.1:creator-only 授 Manage、Manage 不可动 creator、force_adjust 独立通道)
|
|
||||||
- [ ] 全部写操作落 outbox 事件(C3 词汇表)
|
|
||||||
- **验收**:契约 8.1 矩阵逐格 API 测试;D8 的 404/403/503 三分语义测试
|
|
||||||
|
|
||||||
### Phase 3 · 仓库生命周期与文件 API(2 天)
|
|
||||||
|
|
||||||
- [ ] 项目 provisioning 状态机 `PROVISIONING → READY | FAILED`:先建 DB 行(PROVISIONING)→ 调 `VersionStore.init`(幂等)→ 置 READY;失败可重试;孤儿仓库对账 job
|
|
||||||
- [ ] 每项目写锁(Postgres 咨询锁,跨进程安全)——不假设版本包能跨进程串行
|
|
||||||
- [ ] 路径安全:canonical 后必须落在项目根内、拒绝 `.git`、禁 symlink 逃逸、路径长度上限
|
|
||||||
- [ ] 上传限额(单文件 10MB / 单项目配额,数值 OPEN-5)+ 流式接收
|
|
||||||
- [ ] files 端点组全量(list/read/upload/commits→409/diff/history/delete)+ 双客户端冲突 e2e
|
|
||||||
- **验收**:DB/git 故障注入(init 失败、提交后崩溃)→ 对账能收敛;路径穿越语料库测试全拒
|
|
||||||
|
|
||||||
### Phase 4 · 外部集成(1–2 天)
|
|
||||||
|
|
||||||
- [ ] `GroupResolver` HTTP client:超时/5xx/429/畸形 JSON/万级 group 的故障注入测试;失败 → 503(D13)
|
|
||||||
- [ ] Audit relay worker:租约领取、指数退避、eventId 幂等、重启恢复、积压告警
|
|
||||||
- [ ] 导出 job:状态机 + 轮询 + 下载权限校验;`ExportAdapter` 桩(参数 OPEN-6 未定前**不计入完成标准**)
|
|
||||||
- **验收**:relay 重启不丢不重(对端幂等);group 故障时写操作 503、读操作按 D13
|
|
||||||
|
|
||||||
### Phase 5 · 前端(3–5 天,Phase 2 后并行启动)
|
|
||||||
|
|
||||||
- [ ] 登录(平台 SSO 占位 + 本地 JWKS 直通开关)
|
|
||||||
- [ ] 树浏览(懒加载 + 分页)、breadcrumb(D17 占位符)、创建对话框(类型 + 批量授权选择器,Group search 接 C2)
|
|
||||||
- [ ] 权限管理面板(按 8.1 矩阵控制可选项,creator 标识)
|
|
||||||
- [ ] 文件页:查看/上传/下载/历史列表(编辑 UI 不归我方,留对接位)
|
|
||||||
- [ ] 导出按钮 + job 轮询
|
|
||||||
- **验收**:无权限节点全链路不可见;授权面板不会送出矩阵禁止的组合
|
|
||||||
|
|
||||||
### Phase 6 · 硬化与交付(1–2 天)
|
|
||||||
|
|
||||||
- [ ] 404/403/503 全端点核对;并发与重试幂等抽查
|
|
||||||
- [ ] 备份脚本(DB + git 仓库一致性快照)、健康检查(DB/JWKS/磁盘/Group)、磁盘水位告警
|
|
||||||
- [ ] OpenAPI 终稿 + 部署脚本 + 一页运维手册
|
|
||||||
|
|
||||||
## 里程碑
|
|
||||||
|
|
||||||
| 里程碑 | 内容 | 累计工期 |
|
|
||||||
|---|---|---|
|
|
||||||
| M1 | Phase 0–1:骨架 + 权限引擎 + 树操作可跑 | ~4 天 |
|
|
||||||
| M2 | Phase 2–3:后端 API 全量(含文件冲突流) | ~7 天 |
|
|
||||||
| M3 | Phase 4:外部集成(导出不计) | ~9 天 |
|
|
||||||
| M4 | Phase 5–6:前端 + 硬化,可交付 | ~14–18 天 |
|
|
||||||
|
|
||||||
## Mock 保真红线(mock 可以顶,但不许骗)
|
|
||||||
|
|
||||||
| Mock | 不许掩盖的事 | 真身到位后的契约测试 |
|
|
||||||
|---|---|---|
|
|
||||||
| VersionStore | 磁盘延迟、半初始化、锁残留、跨进程并发 | 真包跑临时仓库:并发 commit/init 重试/遍历与 symlink 语料 |
|
|
||||||
| GroupResolver | 超时、5xx、畸形响应、大规模组集 | 故障注入 HTTP 桩全过;我方绝不自己推祖先 |
|
|
||||||
| GroupSearch(前端选择器) | 与 resolve 是**两个独立端点**,不可用 resolve mock 顶替 | C2 `/groups/search` 真身到位后跑分页/空结果/超时 |
|
|
||||||
| Audit 对端 | 重复投递、乱序、长时间不可用 | relay 重启恢复 + 对端幂等 |
|
|
||||||
| JWT | issuer/audience/算法/轮换 | 本地 JWKS 全用例 |
|
|
||||||
| ExportAdapter | 参数、幂等、耗时 | 参数定了再写,之前不算完成 |
|
|
||||||
|
|
||||||
## Top 5 风险
|
|
||||||
|
|
||||||
1. **DB/git 分裂**(崩溃导致元数据、仓库、审计三边不一致)→ provisioning 状态机 + 对账 job,Phase 3 前置
|
|
||||||
2. **移动子树改变整树权限** → D12 事务+锁+源目标双授权,并发测试
|
|
||||||
3. **creator/独立权限开关交互** → D11 先冻结
|
|
||||||
4. **外部契约缺口**(导出参数、请求内 resolve 合并)→ OPEN 清单跟踪,mock 保真红线不许掩盖
|
|
||||||
5. **存储滥用/路径逃逸** → 路径安全 + 配额进 Phase 3 验收,不拖到硬化
|
|
||||||
|
|
||||||
## OPEN 清单(在契约文档基础上增补)
|
|
||||||
|
|
||||||
- OPEN-9:单请求内合并多次 resolve 是否符合 G4"实时"语义(找 Group 团队确认)
|
|
||||||
- OPEN-10:恶意软件扫描归属(我方/平台/不做)
|
|
||||||
- 其余 OPEN-1~8 见《文件库-接口契约.md》第 10 节
|
|
||||||
@@ -96,9 +96,11 @@ allowDevLoginBypass = (NODE_ENV !== "production") && HUB_DEV_LOGIN_BYPASS 为真
|
|||||||
|
|
||||||
## 文件库(filelib/)
|
## 文件库(filelib/)
|
||||||
|
|
||||||
独立文件库模块,语义由仓库根《文件库-接口契约.md》(C/D 编号)与
|
独立文件库模块。代码注释里的 C/D 编号(契约 8.1、C2、C4、D11–D19 等)
|
||||||
`.omo/文件库-开工计划.md`(D11–D19)锚定。**与 hub 自己的 Folder/Project
|
出自两份已删除的文档:《文件库-接口契约.md》与 `.omo/文件库-开工计划.md`,
|
||||||
(ADR-0021 explorer)是两套体系,不复用。**
|
内容可从 git 历史取回。其中 D19(网站管理员 = silo org OWNER/ADMIN)
|
||||||
|
另见 ADR-0028。**与 hub 自己的 Folder/Project(ADR-0021 explorer)是
|
||||||
|
两套体系,不复用。**
|
||||||
|
|
||||||
| 文件 | 职责 |
|
| 文件 | 职责 |
|
||||||
|------|------|
|
|------|------|
|
||||||
|
|||||||
-304
@@ -1,304 +0,0 @@
|
|||||||
# 文件库系统 · 接口契约与决策(v0.1 我方提案)
|
|
||||||
|
|
||||||
> **读者**:版本团队、Group 团队、审计团队、平台方、编辑团队、产品。
|
|
||||||
> **用法**:本文档是我方(文件库)提出的对齐基线。所有决策带 `D-x` 编号、所有契约带 `C-x` 编号,评审时请按编号引用("D5 我们不同意,建议……")。评审通过后本文件冻结,各方照此实现;任何变更走文档修订,不接受口头对齐。
|
|
||||||
> **状态**:v0.1 提案,未冻结。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 系统边界与分工
|
|
||||||
|
|
||||||
| 子系统 | 负责方 | 与我方关系 |
|
|
||||||
|---|---|---|
|
|
||||||
| 文件库(目录树 / 权限引擎 / 内容存储 / 对外 API / 前端) | **我方** | — |
|
|
||||||
| 版本能力(git 化、commit、diff、历史) | 版本团队 | 交付 **npm 工具包**,我方在自有存储上集成(见 C1) |
|
|
||||||
| Group 系统(全局嵌套组) | Group 团队 | 我方**实时消费**其查询接口(见 C2) |
|
|
||||||
| 审计系统(存储 / 查询 / 保留) | 审计团队 | 我方**上报事件**(见 C3);查询界面归审计方 |
|
|
||||||
| 平台身份(登录 / 角色) | 平台方 | 我方验签消费角色(见 C4) |
|
|
||||||
| 在线编辑与冲突合并 UI(需求 2.5) | 编辑团队 | **消费我方文件 API**(见第 9 节) |
|
|
||||||
| 导出工具 | 已有工具 | 我方按需传参调用(参数清单 OPEN-6) |
|
|
||||||
|
|
||||||
**内容归属**:文件内容(git 仓库)物理存储在文件库自己的服务器上,由我方管理;版本团队不持有任何数据,只交付代码。
|
|
||||||
|
|
||||||
## 2. 总体架构
|
|
||||||
|
|
||||||
```
|
|
||||||
编辑团队(2.5 UI) ──┐
|
|
||||||
其他消费方 ──┤ HTTP + JWT
|
|
||||||
▼
|
|
||||||
┌─────────────────────┐
|
|
||||||
│ 文件库服务(我方) │
|
|
||||||
│ 目录树 + 权限引擎 │──── 实时查询 ──▶ Group 系统(C2)
|
|
||||||
│ 文件 API + 授权 API │──── 验签 JWT ──▶ 平台身份(C4)
|
|
||||||
└─────────┬───────────┘
|
|
||||||
│ import(函数调用)
|
|
||||||
▼
|
|
||||||
┌─────────────────────┐ outbox 中继(at-least-once)
|
|
||||||
│ 版本工具包(C1,npm) │ ┌──────────────────────▶ 审计系统(C3)
|
|
||||||
│ 操作我方磁盘上的 │ │
|
|
||||||
│ git 仓库(内容归我方)│ ▼
|
|
||||||
└─────────────────────┘ 我方 DB:业务表 + outbox 表(同事务)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. 决策总表
|
|
||||||
|
|
||||||
| 编号 | 决策 | 一句话理由 | 影响方 |
|
|
||||||
|---|---|---|---|
|
|
||||||
| D1 | 文件库后端 TypeScript + Fastify + Prisma(PostgreSQL);前端 React | 与平台技术心智一致 | 我方 |
|
|
||||||
| D2 | 文件内容物理存储在文件库服务器,版本能力以 npm 包交付 | 需求 2.1"项目初始化为 Git 仓库",仓库是项目的存储 | 版本团队 |
|
|
||||||
| D3 | 版本包冲突用**返回值**表达,异常仅用于系统错误 | 冲突是正常业务流,不是故障 | 版本、编辑 |
|
|
||||||
| D4 | Group 查询**实时调用、不缓存** | 需求 3.3"成员变更实时生效" | Group |
|
|
||||||
| D5 | 审计走"本地 outbox + 后台中继"满足"同事务或可靠消息" | 跨服务做不到真同事务,outbox 是最简单的可靠方案 | 审计 |
|
|
||||||
| D6 | 平台身份用 JWT(RS256)验签,平台角色 `platform.admin` 映射网站管理员 | 无状态、无需每次回调平台 | 平台 |
|
|
||||||
| D7 | 权限管理规则按第 8 节补齐(2.4 空洞的我方版本) | 需求 2.4 为空,必须先有规则才能写码 | 产品确认 |
|
|
||||||
| D8 | 无 View 权限时 API 返回 **404**(不泄露存在性);有 View 但操作越权返回 403 | 防资源探测 | 所有消费方 |
|
|
||||||
| D9 | 软删除:删除即打标隐藏,所有 API 默认不可见 | 需求 2.6/3.3 | 我方 |
|
|
||||||
| D10 | 导出为**异步任务**(提交返回 job,轮询取结果) | 导出耗时不确定,同步会超时 | 编辑/前端 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. C1 · 版本工具包契约(给版本团队)
|
|
||||||
|
|
||||||
**交付形态**:npm 包(TypeScript,自带类型定义),运行在文件库后端进程内。实现技术(isomorphic-git / nodegit / 其他)由版本团队自选,**本契约只约束接口与语义**。
|
|
||||||
|
|
||||||
### 4.1 接口签名
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export type VersionId = string; // 不透明字符串,调用方不得解析(当前为 git commit hash)
|
|
||||||
|
|
||||||
export interface CommitRequest {
|
|
||||||
/** 编辑的起始版本;null 表示新建文件 */
|
|
||||||
baseVersion: VersionId | null;
|
|
||||||
content: string | Buffer; // 文本用 string(UTF-8),二进制用 Buffer
|
|
||||||
message?: string; // 缺省由包自动生成
|
|
||||||
author?: string; // 操作者标识,写入版本记录
|
|
||||||
}
|
|
||||||
|
|
||||||
export type CommitResult =
|
|
||||||
| { status: "ok"; version: VersionId }
|
|
||||||
| { status: "conflict"; currentVersion: VersionId };
|
|
||||||
|
|
||||||
export interface VersionInfo {
|
|
||||||
version: VersionId;
|
|
||||||
message: string;
|
|
||||||
author?: string;
|
|
||||||
committedAt: string; // ISO 8601
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FileEntry { path: string; size: number; }
|
|
||||||
|
|
||||||
export interface VersionStore {
|
|
||||||
init(projectDir: string): Promise<void>; // S7: 幂等
|
|
||||||
list(projectDir: string, prefix?: string): Promise<FileEntry[]>;
|
|
||||||
head(projectDir: string, filePath: string): Promise<VersionId>; // 文件不存在 → FileNotFoundError
|
|
||||||
read(projectDir: string, filePath: string, at?: VersionId): Promise<Buffer>; // 缺省读最新
|
|
||||||
commit(projectDir: string, filePath: string, req: CommitRequest): Promise<CommitResult>;
|
|
||||||
remove(projectDir: string, filePath: string, baseVersion: VersionId): Promise<CommitResult>;
|
|
||||||
diff(projectDir: string, filePath: string, from: VersionId, to: VersionId): Promise<string>; // unified diff
|
|
||||||
history(projectDir: string, filePath: string, limit?: number): Promise<VersionInfo[]>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 语义规则
|
|
||||||
|
|
||||||
| 编号 | 规则 |
|
|
||||||
|---|---|
|
|
||||||
| S1 | **冲突不是异常**。`baseVersion` 落后于当前版本时返回 `{status:"conflict"}`;异常仅用于 IO 故障、仓库损坏等系统错误 |
|
|
||||||
| S2 | `baseVersion: null` = 新建;路径已存在时返回 conflict |
|
|
||||||
| S3 | 二进制文件与文本同样版本化;`diff` 仅保证对文本有意义,二进制可返回占位说明 |
|
|
||||||
| S4 | 包必须保证**同一 projectDir 的写操作(commit/remove)串行化**,调用方可并发调用 |
|
|
||||||
| S5 | 存储布局不透明:我方不直接读写仓库目录,一切经包接口 |
|
|
||||||
| S6 | 在线编辑仅针对文本文件(需求 2.5);二进制材料走上传/下载 |
|
|
||||||
| S7 | `init` 幂等,重复调用不报错、不重建 |
|
|
||||||
| S8 | 规模假设:单项目文件数千级、单文件 10MB 以内,超出另行对齐 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. C2 · Group 查询契约(给 Group 团队)
|
|
||||||
|
|
||||||
**形态**:HTTP + JSON。我方只读,不写 Group 系统任何数据(需求 3.3)。
|
|
||||||
|
|
||||||
### 5.1 接口
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /groups/resolve-member-groups?userId={userId}
|
|
||||||
→ { "groupIds": ["g1","g2",...] }
|
|
||||||
# 权限计算专用:用户直接所属的全部 group + 这些 group 的所有祖先 group,去重。
|
|
||||||
# 方向是"向祖先"收集(需求 3.2:权限沿 group 树向下传递)。
|
|
||||||
|
|
||||||
GET /groups/search?q={keyword}&limit={n}
|
|
||||||
→ [{ "id":"g1","name":"物理教研组","breadcrumb":"总部 / 教研 / 物理" }]
|
|
||||||
# 前端授权选择器用。
|
|
||||||
|
|
||||||
GET /groups/{groupId}
|
|
||||||
→ { "id":"g1","name":"物理教研组","parentId":"g0","status":"active" }
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.2 语义规则
|
|
||||||
|
|
||||||
| 编号 | 规则 |
|
|
||||||
|---|---|
|
|
||||||
| G1 | `groupId` 为不透明、全局唯一字符串 |
|
|
||||||
| G2 | resolve 只含**祖先方向**,不含子孙;用户无所属时返回空数组 |
|
|
||||||
| G3 | 软删除的 group 不出现在任何接口结果中 |
|
|
||||||
| G4 | **实时性**:我方每次权限计算都实时调用 resolve,不缓存;Group 方成员变更须即刻在 resolve 结果中可见(需求 3.3) |
|
|
||||||
| G5 | 文件夹/项目的授权记录只存在文件库,Group 系统不感知(需求 3.3) |
|
|
||||||
| G6 | 可用性:resolve 位于我方**每一次受保护请求**的权限计算路径上,其可用性即文件库可用性 → 需要 Group 方给出延迟与可用性承诺(OPEN-1) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. C3 · 审计上报契约(给审计团队)
|
|
||||||
|
|
||||||
### 6.1 机制(满足需求 5.1"同事务或可靠消息")
|
|
||||||
|
|
||||||
1. 我方在每个关键写操作的**同一数据库事务**内,向本地 `audit_outbox` 表写入事件;
|
|
||||||
2. 后台中继进程读取 outbox,POST 到审计系统 `POST /audit/events`,**at-least-once**,失败重试;
|
|
||||||
3. 审计系统按 `eventId` **幂等去重**;
|
|
||||||
4. 中继只负责送达,业务操作不因审计系统不可用而失败(但事件绝不丢)。
|
|
||||||
|
|
||||||
### 6.2 事件信封(补齐需求 5.3 空缺的字段定义)
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"eventId": "01J…", // 我方生成的唯一 id,幂等键
|
|
||||||
"schemaVersion": 1,
|
|
||||||
"occurredAt": "2026-07-21T10:00:00.000Z",
|
|
||||||
"sourceService": "filelib",
|
|
||||||
"actor": { "userId": "u123", "platformRole": "teacher" },
|
|
||||||
"action": "permission.grant", // 见 6.3 词汇表
|
|
||||||
"object": { "type": "folder", "id": "n456", "path": "/物理/必修一" },
|
|
||||||
"result": "success", // success | failure
|
|
||||||
"errorCode": null,
|
|
||||||
"detail": { /* 按 action 而定的负载,如 grant 的 {principalType, principalId, role} */ }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.3 action 词汇表(文件库范围,对照需求 5.2)
|
|
||||||
|
|
||||||
| action | 需求 5.2 对应 |
|
|
||||||
|---|---|
|
|
||||||
| `folder.create` / `folder.rename` / `folder.move` / `folder.delete` | 创建/删除/移动/重命名文件夹 |
|
|
||||||
| `project.create` / `project.rename` / `project.move` / `project.delete` | 同上(项目) |
|
|
||||||
| `permission.grant` / `permission.update` / `permission.revoke` | 权限变更(detail 含个人/Group、Manage/Edit/View) |
|
|
||||||
| `project.independent_permission.enable` / `.disable` / `.change` | 项目独立权限开启/关闭/变更 |
|
|
||||||
| `file.upload` / `file.rename` / `file.delete` | 材料文件管理 |
|
|
||||||
| `file.commit` | 文件编辑提交(含冲突合并后提交;编辑经我方 API 落盘,故由我方上报) |
|
|
||||||
| `file.conflict_detected` | 冲突检测发生(detail 含起始版本与冲突版本) |
|
|
||||||
| `export.run` | 导出操作 |
|
|
||||||
| `admin.force_adjust` | 网站管理员强制权限调整(高危) |
|
|
||||||
|
|
||||||
Group 相关动作(创建/删除/成员变更/嵌套变更)由 **Group 系统自行上报**;归档类动作待归档功能定案后补充(OPEN-4)。
|
|
||||||
|
|
||||||
### 6.4 边界
|
|
||||||
|
|
||||||
- 审计的存储、防篡改、保留策略(需求 5.5 ≥180 天)、查询接口与查询界面,全部归审计系统;
|
|
||||||
- 文件库前端**不内嵌**审计查询页(若产品要求嵌入,OPEN-8 再议)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. C4 · 平台身份契约(给平台方)
|
|
||||||
|
|
||||||
1. 调用方在 `Authorization: Bearer <JWT>` 中携带平台签发的令牌(RS256);平台通过 JWKS endpoint 分发公钥,我方只做验签 + 过期检查,**不回调平台、不建用户表**;
|
|
||||||
2. claims 约定:`sub`(用户 id,全局唯一)、`roles`(平台角色数组)、`exp`、`iss`;
|
|
||||||
3. **角色映射**:`roles` 含 `platform.admin` → 文件库"网站管理员";其余合法令牌 → 普通用户;无令牌/验签失败/过期 → `401`;
|
|
||||||
4. 网站管理员权限范围:创建根目录、强制权限调整(必审计)、以及平台方赋予的其他高危操作;**不隐式穿透**各节点的业务权限(要管理某子树须被显式授权或走强制调整并留痕);
|
|
||||||
5. 前端登录跳转平台 SSO,具体流程由前端与平台方另行对齐。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. 权限规则 · 2.4 补齐版(D7,产品确认后冻结)
|
|
||||||
|
|
||||||
### 8.1 创建者与授权矩阵
|
|
||||||
|
|
||||||
| 角色 | 能授/改/收的级别 | 限制 |
|
|
||||||
|---|---|---|
|
|
||||||
| **节点创建者** | Manage / Edit / View | 自身 Manage 不可被收回(转让 OPEN-3) |
|
|
||||||
| **Manage 持有者** | Edit / View | **不可**授予 Manage;**不可**修改/收回创建者的任何权限 |
|
|
||||||
| **Edit / View** | 无授权能力 | — |
|
|
||||||
| **网站管理员** | 任意(走 `admin.force_adjust`,必审计) | 不属于日常授权路径 |
|
|
||||||
|
|
||||||
### 8.2 各级别能力清单
|
|
||||||
|
|
||||||
| 能力 | View | Edit | Manage |
|
|
||||||
|---|:---:|:---:|:---:|
|
|
||||||
| 浏览树 / 读文件 / 下载 | ✓ | ✓ | ✓ |
|
|
||||||
| 导出 | ✓ | ✓ | ✓ |
|
|
||||||
| 创建子文件夹/项目(需求:父级 Edit+) | | ✓ | ✓ |
|
|
||||||
| 编辑文本文件 / 上传 / 删改项目内材料 | | ✓ | ✓ |
|
|
||||||
| 重命名 / 移动 / 删除**本节点** | | | ✓ |
|
|
||||||
| 授权管理(按 8.1 矩阵) | | | ✓ |
|
|
||||||
| 项目独立权限开关 | | | ✓ |
|
|
||||||
|
|
||||||
### 8.3 其余规则
|
|
||||||
|
|
||||||
- **P3 根目录**:仅网站管理员可创建;创建时必须指定 ≥1 名 Manage 持有者(可以不是创建者本人),保证每棵子树都有权限链起点;
|
|
||||||
- **P4 空权限创建**:允许;此时仅创建者可见可用;
|
|
||||||
- **P5 项目独立权限**:默认关闭(仅继承父链);开启后项目级授权参与 max 计算;关闭时项目级授权**冻结不删除**,重新开启即恢复;
|
|
||||||
- **P6 计算规则**(与需求 2.3 一致的形式化):
|
|
||||||
|
|
||||||
`effective(user, R) = max { grant.role | grant ∈ grants(s, r), s ∈ {user} ∪ resolveGroups(user), r ∈ {R} ∪ ancestors(R) }`,无 grant → 无权限。个人低权限**不构成降权**(只取最高,不做减法);
|
|
||||||
- **P7 可见性**:对某节点无任何权限的用户,该节点对其不可见(API 行为见 D8)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. 文件库对外 API(消费方:编辑团队及其他)
|
|
||||||
|
|
||||||
**约定**:REST + JSON + JWT(C4);统一错误信封 `{ "error": { "code", "message", "details?" } }`;D8 可见性语义(无 View → 404;越权操作 → 403)。
|
|
||||||
|
|
||||||
### 9.1 树与节点
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /nodes?parentId={id} # 列子节点(缺省列根);仅返回有 View 的
|
|
||||||
POST /nodes # 创建文件夹/项目 {parentId, type, name, grants?[]}
|
|
||||||
# parentId=null 仅网站管理员;type=project 时自动调 C1.init
|
|
||||||
GET /nodes/{id} # 节点详情 + 我的 effective 权限 + breadcrumb
|
|
||||||
PATCH /nodes/{id} # 重命名/移动 {name?, parentId?} (需本节点 Manage)
|
|
||||||
DELETE /nodes/{id} # 软删除(需 Manage)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 9.2 授权
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /nodes/{id}/grants # 授权列表(需 Manage)
|
|
||||||
PUT /nodes/{id}/grants # 批量授予/修改 [{principalType:user|group, principalId, role}]
|
|
||||||
DELETE /nodes/{id}/grants/{grantId} # 收回
|
|
||||||
GET /nodes/{id}/effective-permission # 当前用户在此节点的生效权限(自查)
|
|
||||||
PUT /projects/{id}/independent-permission # {enabled: bool} 独立权限开关
|
|
||||||
```
|
|
||||||
|
|
||||||
### 9.3 文件内容(编辑团队的主接口)
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /projects/{id}/files?prefix= # 文件清单
|
|
||||||
GET /projects/{id}/files/{path} # → {content, version} 编辑起手式:拿到起始版本
|
|
||||||
PUT /projects/{id}/files/{path} # 新建/上传 {content} (需 Edit)
|
|
||||||
POST /projects/{id}/files/{path}/commits # 提交编辑 {baseVersion, content, message?}
|
|
||||||
# → 200 {version} | 409 {currentVersion}
|
|
||||||
GET /projects/{id}/files/{path}/diff?from=&to= # 冲突时取差异(unified diff)
|
|
||||||
GET /projects/{id}/files/{path}/history # 版本历史
|
|
||||||
DELETE /projects/{id}/files/{path} # {baseVersion} (需 Edit)
|
|
||||||
```
|
|
||||||
|
|
||||||
**冲突合并流程(配合编辑团队 2.5)**:编辑 UI 用 `GET files/{path}` 记录 `version` → 用户编辑 → `POST commits` 带 `baseVersion` → 若 `409`,UI 用 `diff?from=base&to=current` 拉取差异,展示三方合并区 → 用户写出最终内容后再次 `POST commits`(baseVersion 换为 currentVersion)。
|
|
||||||
|
|
||||||
### 9.4 导出
|
|
||||||
|
|
||||||
```
|
|
||||||
POST /projects/{id}/exports {target, params} → {jobId} # D10 异步
|
|
||||||
GET /exports/{jobId} → {status, downloadUrl?}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. OPEN 清单(需对方/产品确认,不阻塞我方开工)
|
|
||||||
|
|
||||||
| 编号 | 事项 | 等谁 |
|
|
||||||
|---|---|---|
|
|
||||||
| OPEN-1 | Group resolve 的延迟/可用性 SLA 数值 | Group 团队 |
|
|
||||||
| OPEN-2 | 审计事件投递的 endpoint、鉴权方式、保留期确认(需求建议 ≥180 天) | 审计团队 |
|
|
||||||
| OPEN-3 | 创建者离职/转让后 Manage 链如何处理 | 产品 |
|
|
||||||
| OPEN-4 | 归档功能(需求已划线"暂时未定") | 产品 |
|
|
||||||
| OPEN-5 | 二进制材料的大小上限与在线预览诉求 | 产品 |
|
|
||||||
| OPEN-6 | 导出工具的参数清单(需求原文"待对接时确认") | 导出工具方 |
|
|
||||||
| OPEN-7 | 软删除的恢复入口、"后台标签"管理界面归属 | 产品 |
|
|
||||||
| OPEN-8 | 网站管理员强制调整的前端入口(我方做还是平台做)、审计查询页是否嵌入文件库前端 | 产品 |
|
|
||||||
Reference in New Issue
Block a user