Files
alist-serve-resource/docs/PROJECT_STATE.md
T
hoelee 29ff66de88 docs(PROJECT_STATE): sync handoff to final state (status 5-value 定稿, W4 next)
- Fix stale '待决策 status 语义' section (now 定稿 as 5 account-level statuses)
- Rewrite 下一步 to reflect: features done + tested, remaining = reimport W1
  to n8n for real acceptance, then W4 reconciliation, then AccessGrants design
- Correct test count (W1 = 10 scenarios, not 26 assertions)
- Add ARCHITECTURE/PERMISSIONS/ADMIN_GUIDE to project structure
- Fix lifecycle value suspend_or_delete -> deactivate
- Drop obsolete Load Env template (now in gen_w1.js)
2026-08-23 07:50:25 +08:00

142 lines
12 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-23
> 交接文档:压缩/新会话后,按此文档续做。细节见各 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>` |
| **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 客户开通 + Status Lifecycle — **实测跑通** ✅
- 文件:`n8n/W1-customer-provision.json`(由 `n8n/w1-body.js` + 共享函数库经 `gen_w1.js` 生成)
- 流程:Webhook → Extract customerId(同时透传 webhook 的 old/new rows)→ Load Env → **单 Code 节点 "Provision & Apply Lifecycle"**(含 login + status 分支 + write-back)
- **Lifecycle matrix(5 状态,2026-08-23 定稿)**:
- `active`(无 user)→ provision(建 role→建 user)+ recompute + enable
- `active`(有 user)→ re-enable + recompute(不恢复旧 scopes)+ apply
- `pending`/`suspended`/`expired`/`deleted`(有 user/role)→ disable AList user + 清空 role scopes(soft,保留 user/role/CP/history)
- `pending`/`suspended`/`expired`/`deleted`(无 user)→ **no-op**(纯 NocoDB 标签,不建 AList user)
- 所有 `X → active` resume → recompute(从当前有效 CP + expiry),**永不恢复旧 scopes**,**不自动 mutate CustomerProducts**
- **幂等 + loop 防护**:见 `n8n/test_w1_lifecycle.js`(10 场景全过,覆盖完整 matrix + echo 短路 + sync_required 强制重算)
- **Loop guard(2026-08-23 重写,核心)**:不再用 `sync_status==='synced'`,改用 **old/new field comparison + `sync_required` 标志**:
- `businessChanged()` = 业务字段(status/username/password/display_name/email/notes)在 `previous_rows`→`rows` 间变化,**或** `sync_required` 置位,**或 after-insert(有 new 无 old)**
- echo(只动了 alist_user_id/alist_role_id/sync_*/last_sync*)→ 立即 `skip_writeback_echo`,零 AList 变更、零 PATCH
- **write-back 值比对守卫**:writeBack() 先 GET 当前记录,只 PATCH 值真会变的字段;值没变 → 无 PATCH → 无 webhook → 从物理上掐断 loop
- 写回时始终清 `sync_required: false`
- base_path 硬编码 `'/'`;空 role 数组先建 role 再建 user
- ⚠ **AccessGrants 本次不物化**:不作为 lifecycle source-of-truth,保持现状,等 W4 统一设计(用户明确指示)
### 3. W2 CustomerProduct 权限同步 — **实测跑通** ✅
- 文件:`n8n/W2-customerproduct-sync.json`(`n8n/w2-body.js` + 共享库 → `gen_w2.js`)
- 流程:Webhook → Extract customerProductId → Load Env → **Compute Effective Grants & Apply**(单 Code 节点调共享 `calculateEffectiveGrants`)→ Skip if no change → No-op
- customer link 为空时优雅跳过(`skipWriteBack:true`,不回写,补填后再触发)
- expiry 边界 `<= today`(到期当天视为过期)
### 4. W3 Expiry Sync(全量对账)— **实测跑通** ✅
- 文件:`n8n/W3-expiry-sync.json`(`n8n/w3-body.js` + 共享库 → `gen_w3.js`)
- Schedule `*/5` → 单 Code 节点 "Reconcile & Apply"(逐客户调共享 `calculateEffectiveGrants` → diff → 应用)
- 修过 Bearer 前缀 bug(AList 要裸 token)+ expiry 边界 `<= today`
- Env:`W3_DRY_RUN`、`ALIST_AUTO_DISABLE_ON_EXPIRE`(默认 off)
### 5. 共享函数库(Step 3 核心重构)🆕
- **`n8n/shared/effective-grants.js`** = 单一事实源:`calculateEffectiveGrants(customerId)` + `ncGet`/`alistReq`/`linkId`
- 算法:desired = 有效 CP 派生路径 **∪** 有效 AccessGrants 路径;同 path 多 product 取 **MAX(expires_at)**;permission 取 readwrite 覆盖
- expiry 边界 `<= today`;status 过滤非 active
- **构建时内联**:`n8n/inline_shared.js` 的 `sharedSource()` 剥离 `module.exports`(`/* SHARED_EXPORT_BEGIN */` 之后),三个 `gen_w*.js` 把 body(`__SHARED_INLINE__` 占位)+ 共享源拼成 JSON
- **W1/W2/W3 零复制算法**:三处都调同一个 `calculateEffectiveGrants`
- **测试**(全过):`test_effective_grants.js`(共享算法)、`test_w3_refactored.js`(W3 回归)、`test_w1_lifecycle.js`(W1 lifecycle,10 场景全覆盖:pending/active/suspended/expired/deleted 全转换 + echo 短路 + sync_required 强制重算)
## 🔴 下一步(按序)
> 功能已全部实现并通过本地测试(用户已确认「已测试功能正常」)。剩余事项如下:
1. **reimport W1 到 n8n 并做真实验收**(W2/W3 也重新生成过,可一并覆盖):
- 改 `Customers.status` 一次 → 只产生**一次**真正 W1 execution(`active_provision` 或 `deactivate`),其后 echo 全是 `skip_writeback_echo`
- 验证 W1 execution 的 `desiredPaths` 非空、AList role `permission_scopes` 落对
2. **(Step 4) W4 Full Reconciliation**:NocoDB desired vs AList actual 全量 drift 修复 + reconciliation 日志(W3 已具备基础,W4 补每日全量 + 日志)
3. **(待设计) AccessGrants 物化**:当前 W2/W3 不写 AccessGrants 表,不作为 source-of-truth。W4 时统一设计 reconciliation/materialization 策略
4. 密码回写问题(客户自改 AList 密码 → NocoDB plaintext 失真):当前不强制回写(用户已选方案 C),必要时再议
5. 本地 git 已留档并推 Gitea(`git.hoelee.com/hoelee/alist-serve-resource`,凭据已脱敏)
> ⚠️ **Status 语义已定稿(不再待决策)**:5 个值 `pending/active/suspended/expired/deleted`,
> account-level 整户开关,**不进** permission calc。详见 `docs/ARCHITECTURE.md`、`docs/PERMISSIONS.md`、`docs/DATABASE_SCHEMA.md`。
## 🐛 已沉淀的坑(勿再踩)
- **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`
- `gen_w*.js` 里的 Load Env 节点已含所有表 env + `previousRow`/`newRow` 透传(W1);直接看 `n8n/gen_w1.js` 最新版为准
- **Webhook write-back loop(2026-08-23 已修)**:
- 症状:n8n 写回 NocoDB(sync_status 等)→ 触发 webhook → 又写回 → 无限循环;且 `sync_status==='synced'` 做 guard 不可靠(suspend 分支也写 synced)
- 正确做法:**old/new field comparison**。NocoDB v2 webhook payload `data.rows`(改后)+ `data.previous_rows`(改前)。只有业务字段(status/username/password/display_name/email/notes)变化才是 admin 变更;只动了 alist_user_id/alist_role_id/sync_*/last_sync* = echo → 短路
- 加 `sync_required`(Checkbox)作为强制重算开关:admin 需要强制 provision 时勾选;写回时始终清 false
- **writeBack 值比对守卫**:PATCH 前先 GET 当前值,只写真会变的字段;值没变 → 不发 PATCH → 不触发 webhook(物理层面掐断 loop)
- extract-id 节点必须透传 `previousRow`/`newRow`(经 Load Env 转发)到 lifecycle Code 节点
## 📁 项目结构
```
alist-serve-resource/
├── docker-compose.yml / .env.example / .gitignore
├── n8n/
│ W1/W2/W3-*.json (generated) · w1/w2/w3-body.js (runtime logic)
│ shared/effective-grants.js (single source of truth)
│ gen_w1/w2/w3.js + inline_shared.js (build-time inlining)
│ test_effective_grants.js · test_w1_lifecycle.js · test_w3_refactored.js
│ SETUP.md · W2-DESIGN.md · W3-DESIGN.md
├── scripts/ create_nocodb_schema.py · healthcheck.sh
└── docs/ API_INTEGRATION.md · ARCHITECTURE.md · PERMISSIONS.md · ADMIN_GUIDE.md · DATABASE_SCHEMA.md · N8N_WORKFLOWS.md · INSTALL.md · CONFIGURATION.md · SECURITY.md · BACKUP_RESTORE.md · UPGRADE.md · TROUBLESHOOTING.md · PROJECT_STATE.md
```
## 待确认的设计点
1. **AccessGrants 物化**(已定:本次不物化,不作为 source-of-truth)—— W4 时统一设计 reconciliation/materialization 策略,需避免 AccessGrant→n8n loop
2. 密码:客户可自改 AList 密码 → NocoDB plaintext 会失真;当前不强制回写(用户已选方案 C)
3. 对账/到期只靠 n8n 定时扫描(AList 无 expiry 概念)—— 已确认