Files
alist-serve-resource/docs/PROJECT_STATE.md
T
hoelee 3ecee3099e feat(W1): status lifecycle (suspend/delete/resume) + shared effective-grants lib
- Extract calculateEffectiveGrants into n8n/shared/effective-grants.js
  (single source of truth), inlined at build time into W1/W2/W3 via
  gen_w*.js + inline_shared.js — removes the duplicated algorithm.
- W1 now provisions with a status branch:
  suspended/deleted -> disable AList user + clear role scopes (soft delete)
  active -> create/update user+role, RECOMPUTE desired scopes (never restore
  stale), re-enable
- Loop guard corrected: check AList user LIVE disabled state via
  GET /api/admin/user/get (sync_status alone breaks suspend->resume).
- W2/W3 rebuilt on shared lib (identical behaviour).
- Tests: test_effective_grants.js (15), test_w3_refactored.js (13),
  test_w1_lifecycle.js (23) — all green.
- Drop superseded w2-compute.js / w3-compute.js / test_w3.js.
2026-08-23 04:59:43 +08:00

134 lines
9.6 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 → Load Env → **单 Code 节点 "Provision & Apply Lifecycle"**(含 login + status 分支 + write-back)
- **Step 3 新增 status lifecycle**:
- `suspended`/`deleted` → disable AList user + 清空 role scopes(soft delete,保留 user + NocoDB 历史)
- `active` → provision(建/更新 user+role)→ **重算 desired scopes(复用共享 calculateEffectiveGrants,不恢复旧值)** → re-enable
- 其他 → no-op
- **幂等**:见 `n8n/test_w1_lifecycle.js`(23 断言全过,含 echo/suspend 复用/suspend→active resume 重算不恢复陈旧)
- 关键坑:loop guard 不能只看 `sync_status==='synced'`(suspend 也写 synced),要**查 AList user 实际 disabled 状态**(`GET /api/admin/user/get?id=`)——disabled 说明是 resume,须走完整 provision
- base_path 硬编码 `'/'`;空 role 数组先建 role 再建 user
### 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`(共享算法 15 断言)、`test_w3_refactored.js`(W3 重构回归 13 断言)、`test_w1_lifecycle.js`(W1 lifecycle 幂等 23 断言)——全过
## 🔴 下一步(按序)
1. **重新 import 三个 workflow**(W1/W2/W3 都改了,共享库重构 + W1 status lifecycle)到 n8n,覆盖旧版本
2. **实测 W1 status lifecycle**:NocoDB 改 customer.status=suspended → 看 AList user disabled + scopes 清空;改回 active → 看 re-enable + scopes 重算(不是恢复旧值);deleted → soft delete 不删 user
3. **(Step 4) W4 Full Reconciliation**:NocoDB desired vs AList actual 全量 drift 修复(W3 已具备基础,W4 补 reconciliation 日志 + 每日全量)
4. (Step 5 之后) 暂不做 hard delete(W6)
5. 本地 git 留档 + 验证稳定后推 Gitea(git.hoelee.com;凭据已脱敏 + 历史已重写,tag v0.3-core-sync-working 已推)
## 🐛 已沉淀的坑(勿再踩)
- **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/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 · 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 概念)—— 已确认