chore: agent entrypoint + centralized secrets + slimmer handoff doc

- AGENTS.md: AI session entrypoint (task routing table + iron rules)
- SECRETS.md: single credentials file; distributing the project to
  others = delete this one file
- PROJECT_STATE.md: 190 -> 70 lines, credential quick-ref block removed
  (points to SECRETS.md), incident postmortems kept, pitfall details
  delegated to skill/API_INTEGRATION
This commit is contained in:
2026-08-24 22:05:59 +08:00
parent f26164023e
commit fd62775fcd
3 changed files with 114 additions and 172 deletions
+34
View File
@@ -0,0 +1,34 @@
# AGENTS.md — AI 会话入口(开工先读这个)
> 本文件是 AI agent 的路由表:新会话从这里开始,按需再往下读,不要一上来通读全部文档。
## 开工四步
1. 读本文件(30 秒)
2. 读 `docs/PROJECT_STATE.md`(当前进度 + 下一步,≤150 行)
3. 凭据/地址 → **只看 `SECRETS.md`**(唯一凭据文件;外发给别人 = 删它即可)
4. 连不上系统?DSM IP 可能漂移了——先探测内网(ping / 试 5244、10380、5678 端口),别死磕旧 IP
## 任务路由表
| 要做的事 | 去哪里 |
|---|---|
| 改 workflow 逻辑 | `n8n/w1..w4-body.js` 或 `n8n/shared/effective-grants.js`,改完跑 `node n8n/gen_w1.js`…重新生成 JSON。**禁止手编 `W*.json`** |
| 查表/字段/link 列 ID | `docs/DATABASE_SCHEMA.md`(ID 不是机密,留在文档里) |
| 查 AList / NocoDB / n8n API 怪癖 | skill `alist-serve-resource` 的 Pitfalls(唯一权威,比本文档全) |
| 理解权限算法 | `n8n/shared/effective-grants.js` 注释 + `docs/PERMISSIONS.md` |
| 理解整体架构 | `docs/ARCHITECTURE.md` |
| 查某次事故结论 | `docs/PROJECT_STATE.md` 结案区 |
## 铁律(违反必返工)
1. **勿手编 `n8n/W*.json`** —— 它们是生成物,源头在 body/shared
2. **验证 n8n 是否真的执行:看 AList/NocoDB 实际状态变化**,别数 executions 表(`SAVE_ON_SUCCESS=none`)
3. **DSM IP 会漂移** —— 连不上先探测(probe_env.py),别死磕旧 IP
4. AList 多目录授权:`base_path="/"` + role scope 绝对路径(设 `/Class` 会双重前缀全 403)
5. expiry 边界是 `<= today`(到期当天即失效);NocoDB link 查询必须 `(link.Id,eq,N)`
6. 改共享代码后 grep 一遍裸 `(customer,eq,` / `(product,eq,` —— 此 bug 复发过两次
## 项目一句话
NocoDB(业务 SOT) --webhook/schedule--> n8n W1-W4 --> AList(role scopes)。
Customers → CustomerProducts → Products ↔ Resources 决定客户在 AList 能看哪些目录。
## 外发 SOP
把项目给其他人:**删除 `SECRETS.md` 这一个文件即可**,其余内容均可公开。
+30
View File
@@ -0,0 +1,30 @@
# SECRETS.md — 凭据总表(唯一)
> ⚠️ **本文件随私有仓库入库。把项目发给任何人之前:删除本文件即可完成脱敏。**
> 技术文档里的表 ID / workflow ID 不是机密(没有下面的 token/密码谁也访问不了),留在原处。
## DSM(Synology NAS)
- SSH:`192.168.137.2:9998`(**IP 会漂移**,连不上先探测内网)user `hoelee` / `987][pOIUY/.,mnbVCXZ`
- sudo 免密;docker 命令用 `/usr/local/bin/docker`
- paramiko 只能用密码认证(ed25519 对其 OpenSSH 8.2 失败);sshd 无 SFTP 子系统
## AList(本项目核心服务)
- 容器 `alist-serve-resource`(xhofe/alist v3.63.0),网络 bridge_hoelee
- 地址:Windows 直连 `http://192.168.137.2:5244` · DSM host `http://localhost:5244` · n8n 容器内 `http://alist-serve-resource:5244`
- admin:`hoelee` / `8qiksf6D5JmGJCkfGycR`(2FA 已关)
## NocoDB
- 公网 `https://nocodb.hoelee.com`(⚠ 2026-08-24 起解析/连通未恢复)· 内网 `http://192.168.137.2:10380`
- API token:`nc_pat_mDk7f6t4ZMAYYzv9GDrQvKpSr4oFZyiStxf0wxlL`
- base `AlistAccess` = `p3lzkresswyowqg`
- 元数据库(含业务数据物理表 `nc_jitu___*`):mysql-server 容器,库 `nocodb`,user `nocodb` / `A834EwsSckTHpdXiXH`
## n8n
- 公网 `https://auto.hoelee.com` · 内网 `http://192.168.137.2:5678`
- REST API key:**未记录**(旧速查表已脱敏;需要时从已登录浏览器或 Portainer stack 配置里取)
- 数据库:postgres-server 容器,db `n8n`,user `n8n` / `Hoelee@7296`
- 容器由 **Portainer stack 144** 管理(改 env 必须 rebuild,restart 无效)
## Git 远程
- Gitea:`https://git.hoelee.com/hoelee/alist-serve-resource.git`(先 Gitea 后 GitHub 的惯例)
- 推送令牌:`hoelee:6495a6318ed156fe9a7b0636eb3dc30a3155a4d6`(Basic auth 用)
+50 -172
View File
@@ -1,192 +1,70 @@
# Project State (Handoff) — 2026-08-24
> 交接文档:压缩/新会话后,按此文档续做。细节见各 doc。
> 交接文档:压缩/新会话后按此续做。**入口顺序:AGENTS.md → 本文件 → SECRETS.md(凭据)。**
> 本文件只记「状态与下一步」;API 怪癖/坑的唯一权威 = Hermes skill `alist-serve-resource` + `docs/API_INTEGRATION.md`。
## 🔑 连接凭据速查(新会话直接取用)
## 凭据
全部集中在根目录 **`SECRETS.md`**(唯一凭据文件;把项目给别人 = 删它即可)。
表 ID / workflow ID 不是机密,见 `docs/DATABASE_SCHEMA.md` 与下文。
> ⚠️ **安全警告:本块含生产真实凭据(密码/API token/key)。只留本地用于交接——推 Gitea 前必须整块删除或脱敏,否则凭据随 git 历史永久泄露。**
## 系统现状(2026-08-24 实测)
- 容器全绿:`alist-serve-resource`(healthy) · `nocodb` · `n8n`
- W1–W4 四个 workflow 激活正常(postgres 复核 `activeVersionId` 全对齐)
- **n8n「执行消失」已结案**:容器 env `EXECUTIONS_DATA_SAVE_ON_SUCCESS=none`,成功执行不落库是配置使然,管道健康(详见下方结案记录)
- NocoDB 数据:Products 176 / Resources 171(商品目录健在);Customers / CustomerProducts / AccessGrants / ReconcileLogs = **0**(旧测试数据已被清空)
- AList 已清理至干净态:仅 guest/hoelee 用户 + guest/admin 角色
| 系统 | 地址 / 凭据 |
|---|---|
| **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 注明,不会炸)
## 已完成 ✅(细节见各 DESIGN 文档与 git log)
1. **Phase 1** AList stock v3.63.0 :5244(bridge_hoelee,4 只读挂载;旧 alist :5243 未处理)
2. **Phase 2** NocoDB base `AlistAccess`(`p3lzkresswyowqg`) 8 表(7 业务 + ReconcileLogs);脚本 `scripts/create_nocodb_schema.py` / `add_reconcilelogs_table.py`
3. **W1** 客户开通 + 5 状态 lifecycle(pending/active/suspended/expired/deleted)— 实测跑通;old/new 字段比对 loop guard + `sync_required` 强制重算 + writeBack 值比对守卫(`test_w1_lifecycle.js` 10 场景全过)
4. **W2** CustomerProduct 权限同步(调共享算法)— 实测跑通
5. **W3** 每 5 分钟 expiry sweep(`W3_DRY_RUN` / `ALIST_AUTO_DISABLE_ON_EXPIRE` 开关)— 实测跑通
6. **共享库** `n8n/shared/effective-grants.js` 单一事实源,构建期内联(`gen_w*.js`),三 workflow 零复制算法;union 语义:CP 派生 ∪ AccessGrants,同 path 取 MAX(expires_at),expiry 边界 `<= today`
7. **W4** 每日 03:00 全量对账 + ReconcileLogs 审计 — 本地 + 直连生产实测通过(dry-run→repair→复跑幂等;34 项 mock 测试过);单向同步 NocoDB=SOT,密码不随 repair 推送(方案 C)
8. 文档体系:ARCHITECTURE / PERMISSIONS / ADMIN_GUIDE / DATABASE_SCHEMA / N8N_WORKFLOWS 等;git 已推 Gitea
## 🔴 下一步(按序)
1. **端到端实测(最优先)**:建测试客户走完 W1(provision)→W2(sync)→W3(sweep)。验证看 AList/NocoDB 的实际状态变化,别数 executions 表
2. **补 n8n env 并 rebuild(Portainer stack 144)**:`NOCODB_TABLE_RECONCILELOGS=mktsb7le6fqr2u1` + `NOCODB_FK_RECONCILELOG_CUSTOMER=nc_jitu___Customers_id`(改 env 必须 rebuild,restart 无效)
3. AccessGrants 不物化(保持手工人为授权入口,n8n 永不写)— 已定案
4. 密码保持方案 C(客户可自改 AList 密码 → NocoDB plaintext 失真;不强制回写)
5. 公网域名 `nocodb.hoelee.com` 连通未恢复(DSM 内网端口全正常)— 待查 DNS / 隧道
> 2026-08-24 状态更新:**「执行管道停摆」已定性为误报**(见下),W1-W4 全部激活正常。
> NocoDB 测试数据已被清空(Customers/CP/AccessGrants=0,Products/Resources=176/171 商品目录健在),
> AList 孤儿测试账号已于 08-24 清理(现仅剩 guest/hoelee + guest/admin)。
## 结案记录
- **n8n 执行管道停摆(08-23 报告 → 08-24 结案,误报)**
- 现象:webhook 全 200 但 executions 表零记录、schedule 无痕迹
- 根因:`EXECUTIONS_DATA_SAVE_ON_SUCCESS=none` → 成功执行从不落库,只有 error 会记(期间唯一入库记录恰是一条无关备份 workflow 的 error,完全吻合)
- 时间线:最后 success 记录 08-22 23:00:51Z ≈ 容器重建(08-23 01:21Z Started)前后;激活错位(activeVersionId)已在重建后修复
- 若要恢复可见性:env 改 `all` / `all-with-logs`(EXECUTIONS_DATA_PRUNE 已配,不会无限涨)
- 另注:DSM IP 已漂移为 `192.168.137.2`;Windows 直连内网端口(AList 5244 / NocoDB 10380 / n8n 5678)均通
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`,凭据已脱敏)。
## ⚠️ 坑速览(一行版;详情看 skill `alist-serve-resource` Pitfalls + API_INTEGRATION.md)
- AList:空 role 建用户必 400(先建 role)|base_path 必须 `/` 否则双重前缀全 403|delete 用 `?id=`|裸 token 无 Bearer
- NocoDB:link 查询必须 `(link.Id,eq,N)`|写 bt 链接要写 FK 列 `nc_jitu___*_id`|删行 DELETE+body|v1 列端点已废|SingleSelect 先建空列再 PATCH
- n8n:Code 节点 `$env` 可用、HTTP 表达式不行(Load Env 模式)|API 导入节点 id 必须 UUID 且顶层无 pinData|webhook 节点缺 webhookId 则生产 URL 不注册|激活真标志=DB 的 activeVersionId|执行消失假象=SAVE_ON_SUCCESS
- 环境:DSM IP 会漂移(连不上先探测)|DSM sshd 无 SFTP(POST body 用 stdin pipe 传)
### 「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 语义已定稿**:`pending/active/suspended/expired/deleted`,account-level 整户开关,
> **不进** permission calc。详见 `docs/ARCHITECTURE.md`、`docs/PERMISSIONS.md`、`docs/DATABASE_SCHEMA.md`。
> ⚠️ **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/
├── AGENTS.md ← 🤖 AI 会话入口(任务路由 + 铁律,开工先读)
├── SECRETS.md ← 🔑 唯一凭据文件(外发 = 删除此文件)
├── 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
│ ├── W1..W4-*.json (生成物,勿手编) · w1..w4-body.js (运行时逻辑源头)
│ ├── shared/effective-grants.js (算法单一事实源) · gen_w1..w4.js + inline_shared.js (构建)
│ ├── test_*.js · SETUP.md · W2/W3/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
└── docs/ API_INTEGRATION · ARCHITECTURE · PERMISSIONS · ADMIN_GUIDE · DATABASE_SCHEMA ·
N8N_WORKFLOWS · PROJECT_STATE(本文件) · INSTALL · CONFIGURATION · SECURITY ·
BACKUP_RESTORE · UPGRADE · TROUBLESHOOTING · W4-DESIGN
```
## 待确认的设计点
1. **AccessGrants 物化**(已定案:**不物化**,保持手工人为授权入口源 b;n8n 永不写它)——W4 已确认
2. 密码:客户可自改 AList 密码 → NocoDB plaintext 会失真;保持方案 C(W4 repair 也不推密码,仅 provision 新建时用)
1. AccessGrants 物化 —— **定案:不物化**(手工人为授权入口,n8n 永不写)
2. 密码失真 —— **定案:方案 C**(不强制回写;W4 repair 不推密码,仅 provision 新建时用)
3. 对账/到期只靠 n8n 定时扫描(AList 无 expiry 概念)—— 已确认
4. ~~n8n 执行管道停摆~~ —— **已结案**:是 `EXECUTIONS_DATA_SAVE_ON_SUCCESS=none` 造成的
「执行消失」假象,管道本身健康(见「下一步」后的结案说明)