# Project State (Handoff) — 2026-08-23 > 交接文档:压缩/新会话后,按此文档续做。细节见各 doc。 ## 🔑 连接凭据速查(新会话直接取用) > ⚠️ **安全警告:本块含生产真实凭据(密码/API token/key)。只留本地用于交接——推 Gitea 前必须整块删除或脱敏,否则凭据随 git 历史永久泄露。** | 系统 | 地址 / 凭据 | |---|---| | **SSH DSM** | `192.168.1.1:9998` user `hoelee` / ``;sudo 免密;docker 用 `/usr/local/bin/docker` | | **AList** | `http://alist-serve-resource:5244`(容器内)/ `localhost:5244`(DSM host)admin `hoelee` / ``(2FA 关) | | **NocoDB** | `https://nocodb.hoelee.com`;API token: ``;base AlistAccess = `p3lzkresswyowqg` | | **n8n** | `https://auto.hoelee.com`;API key: `` | | **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) - **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`(26 断言全过,含 echo/suspend 复用/suspend→active resume 重算不恢复陈旧/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` 置位 - 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 ### 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 幂等 26 断言)——全过 ## 🔴 下一步(按序) 1. **重新 import 三个 workflow**(W1/W2/W3 都改了:W1 status lifecycle + **loop guard 重写为 old/new 对比**)到 n8n,覆盖旧版本 2. **实测 loop 修复**:NocoDB 改 customer.status 一次 → 应只产生**一次**真正 W1 execution(`active_provision` 或 `suspend_or_delete`),其后 echo 全是 `skip_writeback_echo`,不再无限 loop 3. **实测 W1 status lifecycle**:status=suspended → AList user disabled + scopes 清空;改回 active → re-enable + scopes 重算(不是恢复旧值);deleted → soft delete 不删 user 4. **⚠️ 待决策**:`Customers.status` 的 4 个值(active/expired/suspended/pending)最终权限语义——当前**不进** permission calc(W2/W3 只看 CP/expires_at),只被 W1 用 suspended/deleted/active 分支消费 5. **(Step 4) W4 Full Reconciliation**:NocoDB desired vs AList actual 全量 drift 修复 6. (Step 5 之后) 暂不做 hard delete(W6) 7. 本地 git 留档 + 验证稳定后推 Gitea(git.hoelee.com;凭据已脱敏 + 历史已重写) ## 🐛 已沉淀的坑(勿再踩) - **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 || '', } }; ``` - **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 · 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 概念)—— 已确认