- 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
17 KiB
17 KiB
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 + enableactive(有 user)→ re-enable + recompute(不恢复旧 scopes)+ applypending/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 → activeresume → 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(idpl8j9lpg5gjrd0dB,active,webhookalist-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 直连生产):
- dry-run:5 客户 → 3 drift(TEST-Alice: dangling_role_ref + base_path_drift + role_binding_drift)+ 1 error(customer 7 缺 username),3 skip,零写入 ✅
- 真实跑:3 repaired + 4 行 ReconcileLogs(FK 正确),verify-after-repair 全过(无 verify_failed)✅
- 复跑: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 个测试客户走完 W1(provision)→W2(sync)→W3(expiry sweep)
全链路。注意:
EXECUTIONS_DATA_SAVE_ON_SUCCESS=none导致成功的 execution 不落库—— 验证要看 AList/NocoDB 的实际状态变化,不能看 executions 表。 - 给 n8n 容器补 env 并 redeploy(Portainer stack 144):
NOCODB_TABLE_RECONCILELOGS=mktsb7le6fqr2u1+NOCODB_FK_RECONCILELOG_CUSTOMER=nc_jitu___Customers_id(2026-08-24 复核:env 仍未加;容器 env 改后必须重建,不是 restart) - AccessGrants 物化:维持现状(手工人为授权入口,n8n 不写)——W4 已定案不物化。
- 密码回写问题(客户自改 AList 密码 → NocoDB plaintext 失真):保持方案 C(不强制回写)。
- 本地 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 拿 idrole/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.goCreateUser /server/handles/role.go/internal/model/roles.go
-
NocoDB(
nocodb-selfhostedskill 已更新):- 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
- SingleSelect 不能建表内联选项;要先建空列再 PATCH
-
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最新版为准
- Code 节点内
-
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 节点
- 症状:n8n 写回 NocoDB(sync_status 等)→ 触发 webhook → 又写回 → 无限循环;且
-
W4 新坑(2026-08-23):
- n8n API 导入(
POST /api/v1/workflows)校验严格:节点 id 必须 UUID、顶层不能有pinData, 否则 400request/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(实测确认),envNOCODB_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 已配,表不会无限涨)
- n8n API 导入(
📁 项目结构
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
待确认的设计点
- AccessGrants 物化(已定案:不物化,保持手工人为授权入口源 b;n8n 永不写它)——W4 已确认
- 密码:客户可自改 AList 密码 → NocoDB plaintext 会失真;保持方案 C(W4 repair 也不推密码,仅 provision 新建时用)
- 对账/到期只靠 n8n 定时扫描(AList 无 expiry 概念)—— 已确认
n8n 执行管道停摆—— 已结案:是EXECUTIONS_DATA_SAVE_ON_SUCCESS=none造成的 「执行消失」假象,管道本身健康(见「下一步」后的结案说明)