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

12 KiB
Raw Blame History

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 概念)—— 已确认