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

9.6 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 → 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 已验证模板:
      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 概念)—— 已确认