Files
alist-serve-resource/docs/PROJECT_STATE.md
T
hoelee f26164023e docs(PROJECT_STATE): resolve n8n pipeline incident as SAVE_ON_SUCCESS=none artifact
- Root cause: EXECUTIONS_DATA_SAVE_ON_SUCCESS=none -> successful executions
  never persisted; only errors land in execution_entity
- W1-W4 activation verified healthy post-recreate (activeVersionId aligned)
- NocoDB test data wiped (Customers/CP=0; catalog 176/171 intact)
- AList orphan accounts cleaned: users TEST-Alice/mama/hoe/test and roles
  customer-*+newrole removed; only guest+hoelee remain
- Next step reprioritized: end-to-end live test (W1->W2->W3)
- Added current workflow IDs + drifting DSM IP to credential quick-ref
2026-08-24 21:51:58 +08:00

192 lines
17 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-24
> 交接文档:压缩/新会话后,按此文档续做。细节见各 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` · W3=Expiry Sync `zlEfjj5eK5IfILtk` · W4=Daily Reconcile `pl8j9lpg5gjrd0dB` |
| **DSM 当前 IP** | `192.168.137.2`(2026-08-24;IP 会漂移,连不上先探测) |
## 已完成 ✅
### 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 强制重算)
### 6. W4 每日全量对账(Step 4)— **本地 + 直连生产实测通过** ✅
- 文件:`n8n/W4-daily-reconcile.json`(`n8n/w4-body.js` + 共享库 → `gen_w4.js`);已导入 n8n(id `pl8j9lpg5gjrd0dB`,active,webhook `alist-w4-reconcile` 已注册)
- 触发:每日 03:00 cron + 手动 webhook(`?dryRun=true` 出计划)
- 单向同步(用户定稿):NocoDB status = SOT,W4 只修 AList,永不反向;密码不随 repair 推送(方案 C)
- Drift 矩阵 + ReconcileLogs 表(`mktsb7le6fqr2u1`,只记 drift/repair/error 行)→ 详见 `docs/W4-DESIGN.md`
- **实测记录(2026-08-23 直连生产)**:
1. dry-run:5 客户 → 3 drift(TEST-Alice: dangling_role_ref + base_path_drift + role_binding_drift)+ 1 error(customer 7 缺 username),3 skip,零写入 ✅
2. 真实跑:3 repaired + 4 行 ReconcileLogs(FK 正确),verify-after-repair 全过(无 verify_failed)✅
3. 复跑:0 drift / 0 repair / 4 skip → 幂等 ✅
- 测试:`n8n/test_w4_reconcile.js`(34 项全过,mock AList+NocoDB 12 场景)
- ⚠ **n8n env 还未加**(`NOCODB_TABLE_RECONCILELOGS` / `NOCODB_FK_RECONCILELOG_CUSTOMER`):容器 env 是 Portainer stack 144 管的,需 Portainer 访问后补并 redeploy(W4 代码有 env 缺失守卫:日志写入静默跳过并在 summary 注明,不会炸)
## 🔴 下一步(按序)
> 2026-08-24 状态更新:**「执行管道停摆」已定性为误报**(见下),W1-W4 全部激活正常。
> NocoDB 测试数据已被清空(Customers/CP/AccessGrants=0,Products/Resources=176/171 商品目录健在),
> AList 孤儿测试账号已于 08-24 清理(现仅剩 guest/hoelee + guest/admin)。
1. **端到端实测(当前最优先)**:建 1 个测试客户走完 W1(provision)→W2(sync)→W3(expiry sweep)
全链路。注意:`EXECUTIONS_DATA_SAVE_ON_SUCCESS=none` 导致成功的 execution 不落库——
验证要看 **AList/NocoDB 的实际状态变化**,不能看 executions 表。
2. **给 n8n 容器补 env 并 redeploy(Portainer stack 144)**:
`NOCODB_TABLE_RECONCILELOGS=mktsb7le6fqr2u1` + `NOCODB_FK_RECONCILELOG_CUSTOMER=nc_jitu___Customers_id`
(2026-08-24 复核:env 仍未加;容器 env 改后必须重建,不是 restart)
3. AccessGrants 物化:维持现状(手工人为授权入口,n8n 不写)——W4 已定案不物化。
4. 密码回写问题(客户自改 AList 密码 → NocoDB plaintext 失真):保持方案 C(不强制回写)。
5. 本地 git 已留档并推 Gitea(`git.hoelee.com/hoelee/alist-serve-resource`,凭据已脱敏)。
### 「n8n 执行管道停摆」结案(2026-08-24)
- 现象回顾:webhook 全 200 但 executions 表零记录、W3 schedule 无痕迹 → 曾疑管道死亡
- **根因:容器 env `EXECUTIONS_DATA_SAVE_ON_SUCCESS=none`** —— 成功执行从不落库,
只有 error 会记(过去 24h 唯一记录是一条无关备份 workflow 的 error,恰好吻合)
- 时间线吻合:最后 success 记录 08-22 23:00Z ≈ n8n 容器重建(01:21:24Z Started)前后;
激活问题(activeVersionId 错位)已在重建后修复——DB 复核 W1/W2/W3/W4 四个 workflow
active=t 且 activeVersionId==versionId
- 遗留副作用:成功不可观测。若要恢复可见性,把 env 改成 `all` 或 `all-with-logs`
(代价是 executions 表膨胀,已有 prune 配置兜底)
- 另注:DSM IP 已变更为 `192.168.137.2`(2026-08-24 实测);Windows 直连内网端口
(5244 AList / 10380 NocoDB / 5678 n8n)均通,但公网域名 nocodb.hoelee.com 解析/连通未恢复
> ⚠️ **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 节点
- **W4 新坑(2026-08-23)**:
- n8n API 导入(`POST /api/v1/workflows`)校验严格:**节点 id 必须 UUID、顶层不能有 `pinData`**,
否则 400 `request/body must NOT have additional properties`(UI 导入不受此限)
- webhook 节点必须有 `webhookId`,否则生产 URL 不注册(404 "The requested webhook ... is not registered")
- n8n 1.123 的 API 激活 `POST /workflows/{id}/activate` 返回 `active:true` 但运行时可能被权限拒绝
(日志 "User attempted to activate a workflow without permissions",webhook/cron 不注册)。
**激活的真实标记在 DB:`workflow_entity.activeVersionId`**(不是 `active` 列);boot 时只加载
`active + activeVersionId` 的工作流。手动救法:`UPDATE workflow_entity SET "activeVersionId"="versionId" WHERE id='...'` + 重启容器。
- **NocoDB 2026.08.x:v1 column GET/PATCH 端点已删**(列 id 从 `GET /api/v2/meta/tables/{id}` 的 columns 拿;
v2 的 columns create/PATCH 响应返回表对象而非列)
- **NocoDB v2 写 bt 链接:写 link 列本身无效**(标量/数组/对象全被忽略)→ 必须写 FK 列
`nc_jitu___Customers_id`(实测确认),env `NOCODB_FK_RECONCILELOG_CUSTOMER`
- **NocoDB v2 删行**:`DELETE /api/v2/tables/{t}/records` + body `[{"Id": N}, ...]`;
`/records/{id}` 和 `where=` 参数都不行
- **n8n「执行消失」假象(2026-08-24 定性)**:`EXECUTIONS_DATA_SAVE_ON_SUCCESS=none`
时成功的 execution 完全不落库(webhook 照样 200),只有 error 落库。
**验证 workflow 是否在跑:看 AList/NocoDB 的实际状态变化,别看 executions 表。**
想要可见性就改 `all`/`all-with-logs`(prune 已配,表不会无限涨)
## 📁 项目结构
```
alist-serve-resource/
├── docker-compose.yml / .env.example / .gitignore
├── n8n/
│ W1/W2/W3/W4-*.json (generated) · w1/w2/w3/w4-body.js (runtime logic)
│ shared/effective-grants.js (single source of truth)
│ gen_w1/w2/w3/w4.js + inline_shared.js (build-time inlining)
│ test_effective_grants.js · test_w1_lifecycle.js · test_w3_refactored.js · test_w4_reconcile.js
│ SETUP.md · W2-DESIGN.md · W3-DESIGN.md · W4-DESIGN.md
├── scripts/ create_nocodb_schema.py · add_reconcilelogs_table.py · live_dry_run_w4.js · 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 物化**(已定案:**不物化**,保持手工人为授权入口源 b;n8n 永不写它)——W4 已确认
2. 密码:客户可自改 AList 密码 → NocoDB plaintext 会失真;保持方案 C(W4 repair 也不推密码,仅 provision 新建时用)
3. 对账/到期只靠 n8n 定时扫描(AList 无 expiry 概念)—— 已确认
4. ~~n8n 执行管道停摆~~ —— **已结案**:是 `EXECUTIONS_DATA_SAVE_ON_SUCCESS=none` 造成的
「执行消失」假象,管道本身健康(见「下一步」后的结案说明)