Files
alist-serve-resource/docs/PROJECT_STATE.md
T
hoelee a907a85a57 feat(W3): expiry sync / full reconciliation sweep (schedule */5) + design + tested compute
- Single Code-node sweep: desired = CP-active paths UNION active AccessGrants;
  expiry excluded -> role/update rewrite; optional auto-disable (env-gated, default off)
- disable guard: only when had scopes before (never fresh empty-role customer)
- loop guard: no NocoDB Customers.status flip (avoids W1 re-provision loop)
- test_w3.js mock-verified both AUTO_DISABLE modes; gen_w3.js builds JSON from w3-compute.js
2026-08-23 01:58:58 +08:00

123 lines
9.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Project State (Handoff) — 2026-08-22
> 交接文档:压缩/新会话后,按此文档续做。细节见各 doc。
## 🔑 连接凭据速查(新会话直接取用)
> ⚠️ **安全警告:本块含生产真实凭据(密码/API token/key)。只留本地用于交接——推 Gitea 前必须整块删除或脱敏,否则凭据随 git 历史永久泄露。**
| 系统 | 地址 / 凭据 |
|---|---|
| **SSH DSM** | `192.168.1.1:9998` user `hoelee` / `<redacted>`;sudo 免密;docker 用 `/usr/local/bin/docker` |
| **AList** | `http://alist-serve-resource:5244`(容器内)/ `localhost:5244`(DSM host)admin `hoelee` / `<redacted>`(2FA 关) |
| **NocoDB** | `https://nocodb.hoelee.com`;API token: `<redacted>`;base AlistAccess = `p3lzkresswyowqg` |
| **n8n** | `https://auto.hoelee.com`;API key: `<redacted><redacted>` |
| **NocoDB 表 ID** | Customers `maqb9zgvza2te3k` · Products `molyb3pxv81l75g` · Resources `m6cm7tkxzd8m18c` · CustomerProducts `mvyqbv8ao7u6bct` · AccessGrants `mxdx3yhfcyej3ao` · AccessLogs `ml12zcyzlehgi21` · Purchases `mwp12z9lcohhsuk`;M2M link col `c4ko4x8kwi16swg` |
| **n8n workflow ID** | W1=Cust Provision `gU2ee9Eg5w1KzB5p` · W2=CP Sync `D9OWAI7JUjKnRdOn` |
## 已完成 ✅
### 1. AList stock 部署(Phase 1)— 运行中
- Container `alist-serve-resource` (xhofe/alist v3.63.0),DSM port **5244**,网络 `bridge_hoelee`
- 挂载 `/volume1/Class` `/volume1/Book` `/volume1/Comic` `/volume1/Template`(只读 → /mnt/Class 等)
- 旧 alist (v3.54.0) 仍运行在 5243,**暂不处理**
- Admin: username `hoelee`,2FA 已关闭;compose 在项目根 `docker-compose.yml`
- DSM 上从 host shell 访问内网容器要用 `localhost:5244`;n8n 容器内用 `http://alist-serve-resource:5244`
### 2. NocoDB schema(Phase 2 重建)— 7 表最终模型
Base: **`AlistAccess`**(`p3lzkresswyowqg`),与旧 `accessDB` 完全隔离。表 + model_id + 关键点:
| 表 | model_id | 备注 |
|---|---|---|
| Customers | maqb9zgvza2te3k | plaintext password + **password_updated_at** |
| Products | molyb3pxv81l75g | default_duration_days;M2M→Resources |
| Resources | m6cm7tkxzd8m18c | alist_path |
| CustomerProducts | mvyqbv8ao7u6bct | 核心表(customer+product+expiry) |
| AccessGrants | mxdx3yhfcyej3ao | **物化状态表,不是 trigger**(禁止 loop) |
| AccessLogs | ml12zcyzlehgi21 | 审计 |
| Purchases | mwp12z9lcohhsuk | 纯记录 |
- 建表脚本:`scripts/create_nocodb_schema.py`(幂等)
- 表/字段/链接列 ID 全表:`docs/DATABASE_SCHEMA.md`(已更新为 7 表)
- NocoDB API token(workspace 级,来自 nc_api_tokens 表 n8n 那行):在 `docs/API_INTEGRATION.md` / SETUP.md 有记(或查 NocoDB UI)
### 2. W1 客户开通 workflow — **已实测跑通** ✅
- 文件:`n8n/W1-customer-provision.json`
- 流程:Webhook → Extract customerId → **Load Env(step 2, 用 $env)** → Fetch 记录 → 判断 create/update → Create Role → Find Role Id → Create User(role:[roleId]) → Find User Id → Write Back
- 关键:AList user/create 传 **空 role 数组会被拒**(自动填 guest→报 "admin or guest user can not be created")。必须先 Create Role 拿自增 id,再 Create User 带 `role:[id]`
- **base_path 已硬编码 `'/'`**(不是 `/Class`,否则 JoinPath 双重前缀 → 403):commit 1ac1f17
- **Loop guard**:新增 "Skip if already synced" IF 节点(sync_status=synced 时短路,不写回 NocoDB → 打破 webhook 循环);同时去掉 last_sync 写入(时间戳每次必变会破坏 guard)
- Load Env 已同步为 `$env` 版本(process.env 在 Code 节点不可用):commit 58c2f57
### 3. W2 CustomerProduct 权限同步 — **已改好,需重新 import 测试** ⏳
- 文件:`n8n/W2-customerproduct-sync.json`
- 流程:Webhook → Extract customerProductId → Load Env → **Compute Effective Grants**(登录 AList→读 customer→读全部 active CP→读 Product 的 M2M Resources→**per-Resource MAX(expires_at)** 合并→读 AList role 现状→diff→算 newScopes) → Skip if no change → AList Update Role Scopes → Write Back CP Sync Status
- **已修 bug(commit 36a6b3b)**:查询 active CP 的 where 语法从 `(customer,eq,${id})` → `(customer.Id,eq,${id})`。NocoDB link 字段用 display name 直接 eq 返回 0 条;必须用 `customer.Id` 子字段或外键列 `nc_jitu___Customers_id`
- **已修 bug**:link API 返回 `{Id,name}` 不含 `alist_path` → 新增 resById 反查(先加载全量 Resources 按 Id 索引,再按 link 结果的 Id 反查 alist_path)
- **Loop guard**:新增 "Skip if no change" IF + No-op 死端(scopes 无 diff 且已 synced 时短路);去掉 last_sync
- ⚠️ 待用户重新 import + 测试
### 4. W3 Expiry Sync(全量对账)— **已实现,待 import + dry-run 测试** 🆕
- 文件:`n8n/W3-expiry-sync.json`(Schedule `*/5` → 单 Code 节点"Reconcile & Apply")
- 设计:`n8n/W3-DESIGN.md`;参考实现 `n8n/w3-compute.js`(自包含,read+write 都在 Code 内)
- **核心逻辑**:对每个有 `alist_role_id` 的 customer → desired = 有效 CP 派生路径 ∪ 有效 AccessGrants 路径(**并集,防止整写 scopes 清掉 manual 授权**)→ 过期排除 → diff → `role/update` 整体重写 → 选项 disable
- **Env 开关**:`ALIST_AUTO_DISABLE_ON_EXPIRE`(默认 false=只观察)、`W3_DRY_RUN`(true=只打印计划不写)
- **disable 护栏**:仅当 `nowEmpty && status=active && currentPaths>0`(原本有 scope 现在空)才禁用;空 role 的新客户不动(否则刚开通没买产品就被禁,W2 不会重新启用)
- **已本地验证**:`n8n/test_w3.js` mock 数据跑通 OFF/ON 两模式,断言全过(manual 授权保留、到期清除、disable 正确、空 role 客户不动)
- **loop 防护**:只写 AList user disabled + role scopes;**不**翻 NocoDB Customers.status(Customers update webhook→W1 可能 re-provision/re-enable=loop)
## 🔴 下一步(按序)
1. **用户测试 W2**(import 覆盖 → 用例 2、3 → AList 前端实测客户登录看目录)→ 报错贴 execution 错误
1.5. **W3 测试**:import `W3-expiry-sync.json` → 设 `W3_DRY_RUN=true` 跑一次看计划 → 造过期 CP 观察 scope 移除 → 开关 auto-disable 做 M3(详见 W3-DESIGN 里程碑)
2. 验证通过后:**expiry scheduler**(每 5 分钟对账:全量 diff NocoDB↔AList,到期的移除 scope)—— 规格 §26/§37
3. 其余 workflow(W3-W6):Product/Resource 变更 → 重算受影响 customer;手动 sync;日志;通知
4. 本地 git 留档 → 验证稳定后推 Gitea(git.hoelee.com)
## 🐛 已沉淀的坑(勿再踩)
- **AList**:
- `user/create` 空 role 数组 → 400 "admin or guest"(源码 `req.Role==0 → 默认 guest` + `IsGuest()` 检查);先建 role 拿 id
- `role/create`/`update` 返回 `data:null`;create 后要 **list 按 name 定位 id**
- delete 用 `?id=` query param,不是 body
- role/list 返回 `data.content[]` + `data.total`(PageResp)
- GUEST=1 / ADMIN=2 / GENERAL=0;用户级 bit14=16384 = Check path limit
- **多文件夹授权正确做法(实测确认)**:`base_path="/"` + role scope 用**绝对路径**。base_path 设成
`/Class` 会导致 scope 绝对路径被 JoinPath 双重前缀 → 全部 403。见 API_INTEGRATION §9.8。
- 源码位置:`internal/op/user.go` CreateUser / `server/handles/role.go` / `internal/model/roles.go`
- **NocoDB**(`nocodb-selfhosted` skill 已更新):
- SingleSelect 不能建表内联选项;要先建空列再 PATCH `{"colOptions":{options},"dtxp":"a,b,c"}`
- Link 字段用 `type:"bt"`(belongs-to)建,`hm` 会把列名搞乱
- **Link 字段 where 查询**:`(linkField,eq,id)` 匹配的是 display name,永远返回 0 条。必须用 `(linkField.Id,eq,id)` 子字段语法或外键列 `nc_jitu___*_id`。已验证:`(customer,eq,5)`→0 条;`(customer.Id,eq,5)`→对。
- **Link API 返回结构**:`/links/{col}/records/{id}` 只返回 `{Id,name}`(display 列),不含业务字段。如需 alist_path 等字段,要按 Id 反查全表
- base/table/column ID 全在 DATABASE_SCHEMA.md
- **n8n env 访问(v1.123.72)**(memory 已记):
- Code 节点内 `$env` ✅(**用户实测可用**,如 `$env.NOCODB_URL`)
- HTTP 节点表达式字段里 `$env` / `process.env` ❌ 都不行
- 模式:step 2 放 "Load Env" Code 节点输出全部 env **并透传 payload 字段**,下游 HTTP 用 `$('Load Env').item.json.VAR`
- step 2 已验证模板:
```js
const prev = $input.item.json;
return { json: {
customerId: prev.customerId,
eventType: prev.eventType,
NOCODB_URL: $env.NOCODB_URL,
NOCODB_TOKEN: $env.NOCODB_TOKEN,
T_CUSTOMERS: $env.NOCODB_TABLE_CUSTOMERS,
ALIST_URL: $env.ALIST_URL,
ALIST_ADMIN_USERNAME: $env.ALIST_ADMIN_USERNAME,
ALIST_ADMIN_PASSWORD: $env.ALIST_ADMIN_PASSWORD,
ALIST_ADMIN_OTP: $env.ALIST_ADMIN_OTP || '',
} };
```
## 📁 项目结构
```
alist-serve-resource/
├── docker-compose.yml / .env.example / .gitignore
├── n8n/ W1-customer-provision.json · W2-customerproduct-sync.json · SETUP.md · W2-DESIGN.md · w2-compute.js
├── scripts/ create_nocodb_schema.py · healthcheck.sh
└── docs/ API_INTEGRATION.md · DATABASE_SCHEMA.md · INSTALL.md · CONFIGURATION.md · SECURITY.md · BACKUP_RESTORE.md · UPGRADE.md · TROUBLESHOOTING.md · PROJECT_STATE.md
```
## 待确认的设计点
1. W2 当前不写 AccessGrants 表 —— 是否在验证后补物化写入?(避免 AccessGrant→n8n loop 的前提下,由 W2 单向写)
2. 密码:客户可自改 AList 密码 → NocoDB plaintext 会失真;当前不强制回写(用户已选方案 C)
3. 对账/到期只靠 n8n 定时扫描(AList 无 expiry 概念)—— 已确认