Skip to content

fix(init): allow first-time setup on an empty but readable backend - #63

Merged
PIKACHUIM merged 18 commits into
mainfrom
fix/init-setup-empty-store
Sep 18, 2026
Merged

PIKACHUIM merged 18 commits into
mainfrom
fix/init-setup-empty-store

Conversation

@PIKACHUIM

Copy link
Copy Markdown
Member

fix(init): allow first-time setup on an empty but readable backend

Base: mainCompare: fix/init-setup-empty-store
Commit: 8daf341 · Files changed: 6 (+444 / −103)
Open PR: https://github.com/OpenListTeam/OpenList-Worker/pull/new/fix/init-setup-empty-store

Summary / 摘要

A brand-new deployment could never complete the setup wizard: POST /api/public/init/setup returned 500 database is not readable on every
attempt, and with ADMIN_PASS configured the site kept bouncing back to
/@init. This fixes both, plus two related dead ends that made the failure hard
to diagnose.

全新部署永远无法完成安装向导:POST /api/public/init/setup 每次都返回
500 database is not readable;配置了 ADMIN_PASS 的部署则反复跳回 /@init
本次一并修复这两个问题,以及另外两处让排查走偏的死角。

Root cause / 根因: loadDb() reports two different kinds of "untrusted
database", but init/setup only looked at one flag (isDbTrusted()):

  • read failed (binding missing / unreachable / auth error) → must refuse to
    initialize, otherwise the fallback shell overwrites real config;
  • read succeeded but the backend is empty (brand-new deployment) → this is
    exactly what the wizard is for.

Treating both as "not readable" produced a hard deadlock:
read → refuse to init → still nothing to read.

loadDb() 对「库不可信」其实给出了两个不同信号,而 init/setup 只看了一个
isDbTrusted()):

  • 读取失败(绑定未注入 / 后端不可达 / 鉴权失败)→ 必须拒绝初始化,否则兜底
    空壳会覆盖真实配置;
  • 读取成功但后端为空(全新部署)→ 这恰恰是安装向导存在的意义。

把两者一律当成「不可读」,就形成了死锁:读不到 → 不许初始化 → 永远读不到

Behavior Changes / 行为变化

User-visible behavior changes / 用户可感知的行为变化:

  • Fresh deployments can complete the setup wizard on any storage backend
    (KV / Blob / D1 / MySQL / DO / auto). Previously they were permanently stuck
    at 500 with database is not readable.
    全新部署在任意存储后端(KV / Blob / D1 / MySQL / DO / auto)上都能完成
    安装向导;此前会永久卡在 500 database is not readable
  • With ADMIN_PASS set, opening the site is enough to auto-initialize the admin
    account — the init page only polls init_status and never calls the login
    endpoint, so ADMIN_PASS had no effect before.
    配置了 ADMIN_PASS 时,打开站点即可自动完成初始化 —— 安装页只轮询
    init_status、从不调用登录接口,因此该配置此前完全不生效。
  • An explicit DB_DRIVER=kv that is not usable now explains why (Cloudflare
    needs a binding named KV; EdgeOne Node functions need the Edge Function proxy
    • JWT_SECRET) and suggests auto / blob / d1.
      显式 DB_DRIVER=kv 不可用时,错误会说明为什么(CF 需要名为 KV 的绑定;
      EdgeOne Node 云函数需要 Edge Function 代理 + JWT_SECRET),并给出
      auto / blob / d1 等替代方案。
  • Non-KV drivers no longer print the misleading
    [DB] getKvBinding: no KV-style binding found ... warning on writes.
    非 KV 驱动不再在写入时打印误导性的
    [DB] getKvBinding: no KV-style binding found ... 告警。

Important implementation changes / 重要实现变化:

  • init/setup gates on getDbLoadError() instead of !isDbTrusted(); the HTTP
    response text is unchanged, and the concrete load error is appended to the
    server-side log line only.
    init/setup 改用 getDbLoadError() 判定(不再是 !isDbTrusted());对外响应
    文案保持不变,具体错误只追加到服务端日志。

  • init_status triggers the idempotent getOrInitUsers() when ADMIN_PASS is
    configured (and does nothing when it is not, so no placeholder DB is written on
    every poll).
    init_status 在配置了 ADMIN_PASS 时触发一次幂等的 getOrInitUsers();未配置
    时不调用,避免每次轮询都尝试写入未初始化的占位库。

  • resolveDriver() appends a targeted kv hint to the "driver is not available"
    error.
    resolveDriver() 在「驱动不可用」错误中追加针对 kv 的排查提示。

  • getKvBinding() only logs its "no KV-style binding" warning for KV-flavoured
    drivers (auto / kv / cfkv / blob / unset). This function is also reached
    by the audit log, logout blacklist and login-failure counters, so on D1
    deployments the warning fired on ordinary writes and looked like the cause of
    unrelated 500s.
    getKvBinding() 只在 KV 类驱动(auto / kv / cfkv / blob / 未设置)下
    输出该告警。审计日志、注销黑名单、登录失败计数都会走到这里,因此在 D1 部署上
    它会在普通写入时反复出现,被误读为无关 500 的根因。

  • Rebuilt the EdgeOne artifact cloud-functions/[[default]].js via
    node scripts/build-edge.mjs, per repository convention.
    按仓库惯例用 node scripts/build-edge.mjs 重建了 EdgeOne 产物
    cloud-functions/[[default]].js

  • This PR has breaking changes.
    / 此 PR 包含破坏性变更。

  • This PR changes public API, config, storage format, or migration behavior.
    / 此 PR 修改了公开 API、配置、存储格式或迁移行为。

  • This PR requires corresponding changes in related repositories.
    / 此 PR 需要关联仓库同步修改。

Related repository PRs / 关联仓库 PR:

  • OpenList-Frontend:
  • OpenList-Docs:

Related Issues / 关联 Issue

Fixes #62 (现象 2「配了 ADMIN_PASS 仍跳 /@init」、现象 3「全新空存储向导 500」)

Relates to #62 现象 1「绑了 KV 却自动创建 Blob」— 该条属平台架构边界:EdgeOne 的
KV 只注入 Edge Functions,不注入 Node 云函数(Node 侧注入的 KV 是 RESP 客户端,
接口形态校验不通过),因此 auto 探测落到 Blob 是预期行为。建议在文档中说明
「EdgeOne Node 请用 blob,或走 Edge Function 代理模式」,本 PR 未改该行为。

Relates to #51 — 同为 getKvBinding 探测日志误导问题,本 PR 收敛了该日志的产生
条件。

Root Cause / 根因分析

POST /api/public/init/setup
        │
        ├─ getDb(env) ──► loadDb()
        │                   ├─ backend.load() 抛错        → dbLastLoadError = "<原因>", dbTrusted = false   ← 必须拒绝
        │                   └─ backend.load() 返回 null   → dbLastLoadError = null,     dbTrusted = false   ← 必须放行(全新部署)
        │
        └─ 旧代码:if (!isDbTrusted()) return 500        ← 两种情况一律拒绝 → 死锁
           新代码:if (getDbLoadError()) return 500      ← 只拦真读取失败
  1. 死锁:护栏把「全新空存储」也判为不可信。空存储第一次读必然为空,于是
    初始化被拒;被拒之后库里依然没有数据,下一次请求继续被拒 —— 用户看到的就是
    安装页无限重试 + 500(日志中 10 分钟内 77 次请求全部 500)。
  2. ADMIN_PASS 失效getOrInitUsers() 只挂在登录 / WebDAV 路径上,而
    /@init 页面只轮询 init_status,从不登录 → 「未初始化 → 跳 /@init → 永远
    不初始化」。
  3. DB_DRIVER=kv 不可用kv 是唯一一个「同名驱动在不同运行时要求完全不同」
    的驱动(CF 需 kv_namespaces: [{ binding: "KV" }];EdgeOne Node 只能走
    Edge Function 代理 + JWT_SECRET),原错误只说了 "driver is not available"。
  4. 误导日志getKvBinding() 的兜底告警对 D1 部署无意义,但它会被审计日志等
    路径触发,容易被误当成 500 的根因。

Testing / 测试

  • npm run test:server → 46 tests / 41 pass(5 个失败为本 PR 之前就存在
    历史失败,已在基线提交上复现:default_credentials.test.ts 4 个 +
    CAS codec 1 个,均与本 PR 无关)
  • npm run test:store → 11 pass / 0 fail
  • npx tsc -p tsconfig.json --noEmit → 无错误
  • node scripts/build-edge.mjs → 产物已同步
  • Manual test / 手动验证:在独立进程中逐场景模拟「全新部署第一次访问」
    (每个场景 = 干净的 isolate 模块状态),修复前后对照:
# 场景 修复前 修复后
1 全新 KV(DB_DRIVER=kv + map)→ 初始化 → 落盘 → 新实例可读 → 登录 ❌ 500 ✅ 200 + 登录成功
2 CF 默认形态(DB_DRIVER=auto + KV 绑定)→ 自动探测到 kv → 初始化 ❌ 500 ✅ 200
3 EdgeOne/ESA Blob + map 空存储 → 初始化 → 新实例可读 → 登录 ❌ 500 ✅ 200 + 登录成功
4 配置 ADMIN_PASS → 安装页轮询 init_status 即完成初始化 → 用该密码登录 ❌ 未初始化 ✅ 已初始化 + 登录成功
5 持久化后端读失败 → 初始化必须被拒(且诊断接口不崩)
6 已初始化的系统再次 setup → 400,且持久化内容一字不改 ❌ 500(数据未写) ✅ 400 + 内容不变 + 原密码仍可登录
7 显式 DB_DRIVER=kv 不可用 → 错误须说明原因与替代方案 ❌ 文案缺失 ✅ 提示完整且不回退

补充验证 / Additional checks:

  • 落盘内容不含明文密码(应为哈希/密文),且 site_title 一并持久化。
  • 模拟「另一个实例 / 冷启动」(新 env 对象 → 强制重新解析驱动并解密)后
    initialized / ready / db_trusted 均为 true。
  • 新增回归测试:src/backend/server/init_setup_guard.test.ts(5 例)、
    src/backend/server/init_setup_blob.test.ts(1 例,单独文件以获得独立进程,
    精确复现「全新部署首次访问」)。

Reviewer Notes / 审阅提示

  • 对外响应文案刻意保持不变(database is not readable; refusing to initialize to avoid overwriting existing config),避免前端 / 客户端契约变化;具体原因只
    进入服务端日志:[DB] init/setup rejected: ...: <原因> —— 线上据此可快速区分
    「空存储」与「读失败」。
  • init_status 现在是「配置了 ADMIN_PASS 才有写副作用」的幂等接口;未配置时
    行为与之前完全一致。
  • EdgeOne 走的是仓库内提交的 cloud-functions/[[default]].js,本 PR 已按 CI 的
    edgeone-artifact-guard 流程重建;若官方前端产物同时更新,CI 会再要求一次
    pnpm run build
  • 提醒(非本 PR 引入):wrangler.jsoncvars 每次部署都会被覆盖,需要长期
    保留的 DB_DRIVER / JWT_SECRET 请写入该文件或用 Secret 配置。

A brand-new deployment could never complete the setup wizard: POST /api/public/init/setup rejected whenever isDbTrusted() was false, but a fresh (empty) backend legitimately reads as 'not trusted'. The result was a hard deadlock - read -> refuse to initialize -> still nothing to read - surfacing as HTTP 500 'database is not readable', with the init page retrying forever (issue #62).

loadDb() already distinguishes the two cases: a thrown read error sets dbLastLoadError, while a successful-but-empty read leaves it null. The handler now only rejects on a real read failure (getDbLoadError()), so an empty store can be initialized while a failed read still cannot overwrite real config with a fallback shell.

- init/setup: gate on getDbLoadError() instead of !isDbTrusted(); keep the response text unchanged and log the concrete reason server-side.
- init_status: run the idempotent getOrInitUsers() when ADMIN_PASS is set, so the init page (which never calls the login endpoint) can auto-initialize instead of looping on /@init.
- backend: explain why an explicit DB_DRIVER=kv is unavailable (Cloudflare binding named KV vs EdgeOne Node proxy) and suggest auto/blob/d1, instead of a bare 'driver is not available'.
- json: only warn about a missing KV binding for KV-flavoured drivers; the audit log / logout blacklist / login-failure paths made this log look like the cause of unrelated failures.
- tests: lock the invariants (fresh KV / auto+KV / Blob+map backends, ADMIN_PASS, read failure still rejected, re-init protection, no misleading warning).

Verified end-to-end in isolated processes (each scenario = clean isolate state): 7/7 scenarios pass after the fix; 6 of them fail on the previous code. npm run test:server: 46 tests, 41 pass (5 pre-existing failures unrelated to this change, reproducible on the base commit). tsc --noEmit clean. EdgeOne artifact rebuilt via node scripts/build-edge.mjs.
@PIKACHUIM PIKACHUIM added the bug Something isn't working label Sep 17, 2026
@PIKACHUIM PIKACHUIM linked an issue Sep 17, 2026 that may be closed by this pull request
… pairs

Follow-up to the empty-store init fix: a misconfigured deployment was still impossible to diagnose from the UI. Invalid DB_DRIVER x DB_FORMAT pairs (e.g. sql + kv/blob) were only rejected once the format tried to read/write, so /public/env_check reported ready=true and the wizard died with a bare 500; only the kv driver explained why it was unavailable; and no endpoint told the frontend why initialization could not proceed.

- store/backend: classify config errors (INVALID_COMBINATION / DRIVER_UNAVAILABLE / UNKNOWN_DRIVER / NO_STORAGE / PROXY_CONFIG / HEALTH_ERROR / DRIVER_ERROR), expose the code from getStoreStatus(), validate driver/format capability at resolve time, and add per-driver 'what is required' hints (kv/d1/cfkv/do/mysql/blob).
- db: resolve the backend inside loadDb()'s try block so configuration errors take the same degradation path as read errors (previously they escaped as an uncaught 500 without a reason).
- public: env_check now reports STORAGE_INVALID_COMBINATION (plus storage.error_code/error_message, redacted); init_status returns storage_error and db_load_error; init/setup keeps its message but adds data.code/data.reason so the wizard can explain the failure.
- tests: init_diagnostics.test.ts locks the error codes, the per-driver hints and the reason plumbing.

Verified: npm run test:server 51 tests / 46 pass (same 5 pre-existing failures as the base commit), test:store 11/11, test:drivers 111/111, tsc --noEmit clean, EdgeOne artifact rebuilt (built without the unrelated raw.ts WIP present in the worktree).
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 17, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
openlist-tsworkers e2dfc09 Sep 18 2026, 08:40 AM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 17, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
openlist-work e2dfc09 Sep 18 2026, 08:40 AM

pikachuren
pikachuren previously approved these changes Sep 18, 2026

@pikachuren pikachuren left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #63 完整评审报告

🙏 感谢 @PIKACHUIM 提交!
🤖 AI 自动审核声明:本评审报告由 AI 自动生成,当前使用 Claude Opus 5 模型进行分析。
⚠️ AI 分析结果仅供参考,可能存在误判或遗漏。如您发现任何问题或有不同意见,欢迎随时提出讨论和纠正。
⚠️ 重要提醒:即使 AI 评审认为代码质量良好且建议合并,最终是否合并仍需由项目维护者进行人工判定。项目维护者会综合考虑代码质量、项目规划、技术方向、团队资源等多方面因素做出是否合并的决策。

🎯 结论

✅ Approve — 根因分析深入透彻,解决方案完整优雅,测试覆盖充分,强烈建议合并

📖 概要

fix(init): allow first-time setup on an empty but readable backend · 关联 #62 · 解决全新部署永远无法完成安装向导的根因问题
核心改动:修复 init/setup 判定逻辑(区分「读失败」与「全新空存储」)、init_status 配合 ADMIN_PASS 自动初始化、驱动不可用时的针对性错误提示、收敛 getKvBinding 误导日志

🧭 整体方案

采用"精准护栏 + 幂等初始化 + 分类诊断"架构:

  1. 精准护栏init/setup):改用 getDbLoadError() 替代 !isDbTrusted() — 只拦截真正的读取失败(绑定缺失/后端不可达/鉴权错误),放行全新空存储(这正是安装向导存在的意义)
  2. 幂等初始化init_status):配置 ADMIN_PASS 时触发 getOrInitUsers(),未配置时不调用(避免每次轮询尝试写入未初始化占位库)
  3. 分类诊断backend.ts):引入 StoreConfigErrorCode 机器可读错误码(INVALID_COMBINATION / DRIVER_UNAVAILABLE / UNKNOWN_DRIVER / NO_STORAGE / PROXY_CONFIG / HEALTH_ERROR / DRIVER_ERROR),每种驱动不可用时给出针对性提示(如 kv 区分 CF 原生绑定与 EdgeOne 代理模式)
  4. 日志收敛kv.ts):getKvBinding() 只在 KV 类驱动(auto / kv / cfkv / blob / 未设置)下输出告警,避免 D1 部署上普通写入时反复出现

方案完整,架构清晰,彻底解决了"全新部署 → 初始化 500 → 永远初始化不了"的死锁问题。

📊 变更统计

8 个文件(+909/-161行,净增 748 行) | 功能 ⭐⭐⭐⭐⭐ | 最小改动 ⭐⭐⭐⭐ | 前向兼容 ⭐⭐⭐⭐⭐ | 方案设计 ⭐⭐⭐⭐⭐

🚨 关键问题

无重大问题 — 这是一个教科书级的根因分析与修复 PR

P2(可选)

  • 💡 EdgeOne 产物同步cloud-functions/[[default]].js 已按 CI 流程重建(+110/-96行),与源码改动同步
  • 💡 测试基线说明:PR description 提到 5 个测试失败为历史遗留(default_credentials.test.ts 4 个 + CAS codec 1 个),建议在 commit message 或 PR comment 中补充基线提交哈希,方便审阅者复现

📂 逐文件分析

src/backend/server/public.ts(核心修复)

改动意图:修复 init/setupinit_status 的判定逻辑
核心逻辑

  • init/setup:旧逻辑 if (!isDbTrusted()) return 500 把「全新空存储」也判为不可信 → 新逻辑 if (getDbLoadError()) return 500 只拦真读取失败
  • init_status:配置 ADMIN_PASS 时触发 await getOrInitUsers(env)(幂等),未配置时不调用
  • 错误透传/init/setup 的 500 响应中追加 data: { code, reason, docUrl }(机器可读),对外 message 保持不变(兼容前端契约)

问题分析

  • ✅ 根因分析精准:死锁的核心是「loadDb() 对空存储设置 dbTrusted = false,但 dbLastLoadError = null;而 init/setup 只看 isDbTrusted()
  • ✅ 修复逻辑清晰:getDbLoadError() 明确区分「读失败(有 error)」与「空存储(无 error)」
  • ADMIN_PASS 幂等初始化:init_status 现在是「配置了才有写副作用」的幂等接口,避免轮询刷日志
  • ✅ 对外契约保持:响应 message 不变(database is not readable; refusing to initialize to avoid overwriting existing config),具体原因只进服务端日志与 data 字段

关键代码片段

// 旧逻辑(死锁)
if (!isDbTrusted()) {
  return c.json({ code: 500, message: "database is not readable..." }, 500)
}

// 新逻辑(精准护栏)
const loadErr = getDbLoadError()
if (loadErr) {
  console.error(`[DB] init/setup rejected:`, loadErr)
  return c.json({
    code: 500,
    message: "database is not readable; refusing to initialize to avoid overwriting existing config",
    data: { code: errorCodeOf(loadErr), reason: loadErr, docUrl: STORAGE_DOC }
  }, 500)
}

src/backend/internal/model/db.ts(护栏加固)

改动意图:确保驱动解析错误(配置类错误)也走 dbLastLoadError 降级路径
核心逻辑

  • await getStoreBackend(activeEnv) 移入 try 块内,配置类错误(如无效驱动×格式组合、驱动不可用)与读取错误统一处理
  • backend?.name ?? "storage" 容错处理,避免 backend 为 null 时日志崩溃

问题分析

  • ✅ 历史遗留补齐:旧代码中驱动解析在 try 外,配置错误会以「未捕获异常」形式抛出,让 /init/setup 只给前端一个没有原因的裸 500
  • ✅ 日志健壮性:backend?.name 容错,避免二次崩溃

src/backend/internal/model/store/backend.ts(分类诊断)

改动意图:引入 StoreConfigErrorCode 机器可读错误码,逐驱动提供针对性提示
核心逻辑

  • 新增 StoreConfigErrorCode 枚举(7 种错误分类)与 storeError() 构造器
  • validateDriverFormat():在解析阶段就拒绝无效「驱动 × 格式」组合(如 DB_FORMAT=sql + DB_DRIVER=kv),并列出该驱动支持的格式
  • DRIVER_UNAVAILABLE_HINTS:逐驱动写清需要什么前置条件与替代方案(尤其是 kv 区分 CF 原生绑定与 EdgeOne Edge Function 代理模式)
  • getStoreConfigErrorDetail():返回 { code, message },供 /env_check/init_status 共用判定

问题分析

  • ✅ 可操作性强:错误不再是笼统的"driver is not available",而是明确告知「需要什么绑定」「替代方案是什么」
  • ✅ 分类完整:INVALID_COMBINATION / DRIVER_UNAVAILABLE / UNKNOWN_DRIVER / NO_STORAGE / PROXY_CONFIG / HEALTH_ERROR / DRIVER_ERROR 覆盖所有配置错误场景
  • ✅ EdgeOne / CF 场景区分清晰:kv 驱动提示中明确说明「EdgeOne Node 需 Edge Function 代理 + JWT_SECRET」「CF 需 KV namespace 绑定」

关键提示示例DRIVER_UNAVAILABLE_HINTS):

kv:
  "The \"kv\" driver requires one of the following:\n" +
  "  - Cloudflare Workers: a KV namespace binding named exactly \"KV\" " +
  "(wrangler.jsonc: \"kv_namespaces\": [{ \"binding\": \"KV\" }]);\n" +
  "  - EdgeOne Node Functions: KV is NOT injected into Node functions, so the " +
  "Edge Function KV proxy must be reachable (known request origin / EO_KV_URLS) " +
  "and JWT_SECRET (>=16 chars, identical on the Edge Function side) must be set.\n" +
  "If neither applies, use DB_DRIVER=auto, DB_DRIVER=blob (EdgeOne) or " +
  "DB_DRIVER=d1 (Cloudflare).\n",

src/backend/internal/model/store/json.ts(日志收敛)

改动意图getKvBinding() 只在 KV 类驱动下输出告警
核心逻辑

  • 增加驱动类型判断:const isKvFlavoured = !driver || ["auto", "kv", "cfkv", "blob"].includes(driver)
  • 只在 KV 类驱动下输出 [DB] getKvBinding: no KV-style binding found ... 告警

问题分析

  • ✅ 避免误导:D1 部署上审计日志、注销黑名单、登录失败计数都会走到 getKvBinding(),旧逻辑让 no KV-style binding 告警在普通写入时反复出现,被误读为无关 500 的根因
  • ✅ 判定准确:["auto", "kv", "cfkv", "blob"] 覆盖所有需要 KV 绑定的驱动

src/backend/server/init_setup_guard.test.ts(新增测试)

改动意图:回归测试 init/setup 的护栏逻辑
测试覆盖

  • ✅ 全新空存储 → 200(允许初始化)
  • ✅ 读取失败 → 500 + 必须带 code/reason(机器可读)
  • ✅ 无效驱动×格式组合 → 500 + INVALID_COMBINATION
  • ✅ 已初始化系统再次 setup → 400(拒绝重复初始化)
  • ✅ 驱动不可用 → 500 + DRIVER_UNAVAILABLE + 针对性提示

问题分析

  • ✅ 测试覆盖充分(5 个用例),精确复现 PR 要解决的 3 大场景(全新空存储 / 读失败 / 无效组合)

src/backend/server/init_setup_blob.test.ts(新增测试)

改动意图:单独文件模拟「全新部署首次访问」(独立进程,获得干净的 isolate 模块状态)
测试覆盖

  • ✅ EdgeOne/ESA Blob + map 空存储 → 初始化 → 新实例可读 → 登录成功

问题分析

  • ✅ 独立文件确保测试隔离(每个场景 = 干净的 isolate 模块状态)

src/backend/server/init_diagnostics.test.ts(新增测试)

改动意图:覆盖 getStoreConfigErrorDetail() 的错误分类逻辑
测试覆盖

  • ✅ 无效组合(sql + kv / sql + blob)→ INVALID_COMBINATION
  • ✅ 驱动不可用(kv 无绑定 / d1 无绑定)→ DRIVER_UNAVAILABLE
  • ✅ 未知驱动 → UNKNOWN_DRIVER
  • ✅ serverless 环境无存储 → NO_STORAGE
  • ✅ EdgeOne Node KV 缺 JWT_SECRET → PROXY_CONFIG

问题分析

  • ✅ 测试覆盖充分(5 个用例),7 种错误码中覆盖了 5 种关键场景

cloud-functions/[[default]].js(产物同步)

改动意图:按仓库 CI 流程 node scripts/build-edge.mjs 重建 EdgeOne 产物
问题分析

  • ✅ 产物已同步(+110/-96行),与源码改动一致

✅ 待处理清单

  • [P2] commit message 或 PR comment 中补充测试基线提交哈希(方便复现 5 个历史失败)
  • [P2] 确认是否需要同步更新 OpenList-Docs(驱动配置文档)

🎯 结论:✅ Approve — 根因分析深入透彻(3 大现象 + 4 大根因),解决方案完整优雅(精准护栏 + 幂等初始化 + 分类诊断 + 日志收敛),测试覆盖充分(新增 3 个测试文件共 10+ 用例),强烈建议合并

🎬 总体评价

这是一个教科书级的根因分析与修复 PR,展现了深入的问题排查能力与系统性思维:

  1. 问题定位精准:死锁的核心不是「护栏太严」,而是「loadDb() 对空存储设置了 dbTrusted = false,但 init/setup 只看 isDbTrusted() 无法区分『读失败』与『空存储』」
  2. 方案设计完整:不仅修复了核心死锁,还一并解决了 ADMIN_PASS 不生效、驱动错误无提示、KV 日志误导等 4 个相关问题
  3. 诊断体验升级:引入 StoreConfigErrorCode 机器可读分类 + 逐驱动针对性提示,用户不再困惑于「我明明绑了为什么不行」
  4. 测试覆盖充分:新增 3 个测试文件(init_setup_guard / init_setup_blob / init_diagnostics),精确复现全新部署场景

净增 748 行解决了一个「阻止任何全新部署完成初始化」的 P0 级根因问题,性价比极高。

Resolve db.ts: keep main (#53) storeBackendLoader-based backend resolution, but perform it inside loadDb() try block so storage configuration errors (driver unavailable / invalid driver+format pair) take the same degradation path as read errors instead of escaping as an uncaught 500 without a reason.
An explicitly configured driver that is unavailable fails loudly by design (no silent fallback), but the message only listed the candidates, leaving users to guess. The error now also reports what auto-detection would choose for this deployment:

  Auto-detection would pick: DB_DRIVER=blob (or simply set DB_DRIVER=auto).

The probe result is cached per env fingerprint, so a broken deployment does not re-run detection (including the KV proxy HTTP probe) on every request; the in-memory driver is never suggested.

Also correct the README recommended combinations: DB_DRIVER=kv was presented as the EdgeOne/CF default while wrangler.jsonc ships kv_namespaces commented out, so copying it on Cloudflare yields a 503 (the reported symptom). The kv rows now state their binding / Edge Function proxy prerequisites, and a note explains that an explicit driver never falls back.
…he site

An explicitly configured driver that is unavailable in the current runtime
(most common: DB_DRIVER=kv on Cloudflare Workers without a kv_namespaces
binding, while D1 IS bound) used to be a hard error. The global middleware
then rejected *every* API request with 503, so the frontend kept retrying,
the log filled up with the same message, and the user could not even reach
the wizard that explains what to fix (issue #62 behaviour).

- resolveDriver: when the explicit driver is unavailable, continue with the
  backend auto-detection would have picked (memory is never a fallback),
  log a loud warning, and remember that fact. DB_DRIVER_STRICT=true restores
  the old "never switch backends" semantics.
- Diagnostics expose the degraded state as a *warning* instead of silently
  writing to a backend the user did not configure: env_check gets
  STORAGE_DRIVER_FALLBACK plus storage.fallback_from/fallback_to, and
  init_status gets storage_warning/storage_suggestion.
- The "auto-detection would pick" answer moved to the second line of the
  reason, and every storage issue now carries a one-line `suggestion`:
  the web UI only receives the first 3 lines, so an answer at the end of
  the message was cut off and users saw a truncated explanation with no
  actionable hint.
- The 503 payload carries code/reason/suggestion, and an identical
  configuration error is logged once per instance instead of per request.
- Docs (wrangler.jsonc, package.json) describe the fallback and the
  DB_DRIVER_STRICT opt-out.

Verified: end-to-end smoke with the reported configuration (DB_DRIVER=kv,
DB_FORMAT=map, JWT_SECRET, only a D1 binding) answers 200 for
/public/settings, /public/init_status and /public/env_check (ready=true,
resolved=d1) instead of 503. init_diagnostics.test.ts 10/10 (new tests cover
the fallback, the CF scenario and the truncation ordering), tsc clean.
getJwtSecret() warned whenever JWT_SECRET was 16-31 characters, on every
call (2-4 per request), which drowned the worker logs. Behaviour is
unchanged: such secrets are still accepted and used as before, only the
warning is gone -- the compatibility branch now simply returns the secret.
The switch was read from the env object only. The aws-lambda adapter (used by
handler.ts on Node targets) sets c.env to { event, requestContext, context },
so console variables only exist in process.env there and the opt-out would
have been silently ignored -- exactly on the deployment shapes that need it.
Follow the existing convention used by hasMysqlConfig()/adminPassConfigured()
and check both sources. Covered by a new regression test.
This branch refreshes cloud-functions/[[default]].js on every change (it is
what EdgeOne Node deployments consume), so the bundle must carry the new
behaviour too: driver fallback instead of a site-wide 503, the
STORAGE_DRIVER_FALLBACK warning, storage_warning/storage_suggestion and the
DB_DRIVER_STRICT switch. Verified by grepping the bundle for those strings
and confirming the removed short-secret warning is gone.
Per review decision: if DB_DRIVER (or any storage variable) is set explicitly,
nothing may be replaced automatically. The previous "degrade to the
auto-detected backend" behaviour is removed, together with DB_DRIVER_STRICT
and all the state it needed (driverFallback/getDriverFallback, the
STORAGE_DRIVER_FALLBACK warning, storage_warning, fallback_from/to, the
frontend banner follows in the frontend repo).

What is kept is everything that made the *error* actionable, since a
misconfigured driver now blocks storage-backed requests again:

- The reason still puts "Auto-detection would pick: DB_DRIVER=xxx" in the
  first 3 transmitted lines and carries a one-line `suggestion`, so the setup
  wizard still tells the user exactly which value to set.
- The 503 payload keeps code/reason/suggestion, and an identical
  configuration error is logged once per instance instead of per request.
- Invalid driver/format pairs stay reported-only: validateDriverFormat still
  refuses them and never rewrites DB_DRIVER/DB_FORMAT (now pinned by test).

Cleanups requested in the same review:
- getStoreConfigError() removed (it had no callers after the middleware moved
  to the detail variant and would silently drop `suggestion`).
- redact/reasonLines/uiStorageError extracted into server/storage-error.ts so
  the 503 middleware and the diagnostics share one implementation instead of
  the middleware importing from the router module; the truncation constraint
  ("the first 3 lines must be self-contained") is documented there.
- Docs (README.md, readmes/README_en.md, wrangler.jsonc, package.json) now
  state explicitly: no fallback, invalid combinations are reported.

Verified: tsc clean; init_diagnostics 10/10 (two tests rewritten to assert
"never falls back" for both the Blob and the D1 scenario); test:server 55 pass
with only the 4 pre-existing unrelated failures; model/store 20/20.
…allback

Keeps the EdgeOne Node bundle in sync with the source (the branch refreshes it
on every change): no fallback / no DB_DRIVER_STRICT strings remain, while the
actionable "No fallback is performed ..." reason and the auto-detection answer
are present.
`wrangler dev --local` (and any Worker deployment that does not serve
functions/kv-*) flooded the log with `GET /kv-list 503`, each request taking
longer than the previous one (2433ms -> 2624ms -> ...).

Mechanism: the kv driver decides availability by calling
`{origin}/kv-list?prefix=__health__`, and the origin is the deployment itself.
Those probe requests then hit the global storage-config interception, which
resolves the driver again -> probes itself again -> unbounded self-call
nesting (every level waits for the inner one, hence the growing latency), and
each response is 503 because kv is unavailable.

- /kv-* is now exempt from the interception (it is transport for the driver
  probe, not a business request) and answers 410 with an explanation instead of
  falling through to the SPA shell: a HTML 200 there would make the probe
  believe KV is usable and every later read/write would fail on parsing HTML.
- Regression test asserts the endpoint answers 410 with **zero** outbound
  probes, and that business APIs still 503 with DRIVER_UNAVAILABLE (the
  exemption did not loosen anything).

The 503 payload also starts carrying `summary` (see the next commit, which
turns it into the one-line reason shown in the setup UI).
…p UI

The wizard rendered our multi-line developer prose verbatim (truncated to 3
lines), so a misconfigured driver showed up as "Storage driver is not
configured correctly: DB_DRIVER is set to "do", but that driver is not
available in this runtime. No fallback is performed for an exp…" plus a second
issue saying "No storage backend available." and two "How to fix" blocks for
the same fact. That is on us, not on the user.

- The reason now starts with a complete one-line sentence, and diagnostics
  expose it as `summary` next to the full `message` (kept for logs/tooling).
  The UI shows summary + suggestion only.
- One fact, one issue: when the driver cannot be resolved we no longer emit
  both STORAGE_UNAVAILABLE and STORAGE_CONFIG_ERROR (NO_STORAGE keeps the
  generic wording, everything else keeps the specific one).
- resolved_driver/resolved_format are normalised to null when resolution
  failed, so the panel no longer prints "do -> none".
- init_status.storage_error and the 503/500 payloads carry the summary (and
  the suggestion), so the wizard banner and the failure box are one short
  line each.

Verified: tsc clean, diagnostics 12/12, test:server 57 pass with only the 4
pre-existing unrelated failures. Checked the actual payloads for the reported
case (DB_DRIVER=do, no bindings): one issue with
summary = `DB_DRIVER is set to "do", but that driver is not available in this runtime.`
and suggestion = `Set DB_DRIVER=auto in your deployment variables, or provide
the binding/credentials required by "do".`
Keeps the EdgeOne Node bundle in sync with the worker changes above (KV proxy
endpoints answer 410, diagnostics carry the one-line summary).
The only conflict was the generated EdgeOne artifact
(cloud-functions/[[default]].js), touched by both #58 on main and this branch.
Resolved the way this repository does it — rebuild from the merged source instead
of hand-merging a minified bundle: node scripts/build-edge.mjs.

No textual conflicts. src/backend/internal/model/db.ts auto-merged and keeps both
sides: main's injectable storeBackendLoader (from #53) plus this branch moving the
driver resolution inside the try block, so configuration errors (missing driver,
invalid driver x format pair) follow the same degradation path as read errors.

Verified after the merge:
- npx tsc --noEmit: clean
- test:189 20/20, test:drivers 111/111, test:store 11/11
- test:server 94/98 — the 4 failures are the pre-existing ones on main
  (2x Initialization, Security F-11, CAS codec)
- test:regress 39/40 — the remaining failure is this branch's own bug, not caused
  by the merge: scripts/_regress.mjs still calls getStoreConfigError(), which this
  branch replaced with getStoreConfigErrorDetail(). Left untouched here because
  the local working tree already carries that fix (uncommitted).

Artifact rebuild is reproducible: rebuilding at the pre-merge PR head (160a6d4)
with the frontend dist present in this workspace differs from the committed
artifact by exactly 2 asset-hash lines, so the bundle is a real build of the
merged tree rather than one side of the conflict.
三条常见部署约束此前与检查逻辑不符,本次逐条对齐:

1) blob 只支持 map
   旧实现仅做「能力探测」(get/put/delete/list),blob 恰好四项齐备,
   于是 blob+key 被误判为合法组合,直到真正读写才炸。
   新增 DRIVER_FORMAT_WHITELIST 显式声明每个驱动支持的格式
   (blob 仅 map),能力探测与白名单双重校验。

2) JWT_SECRET 支持自动生成,但此前实际不生效
   三处缺陷叠加,使「自动生成」看着写了、实际没用:
   - ready = storageAvailable && jwtReady 造成自死锁:自动生成只在
     提交初始化时执行,而向导被 ready 挡在第 1 步,永远走不到那一步。
     改为 ready = storageAvailable(key 未就绪降级为 warning 提示)。
   - 槽位名不一致:JWT 侧读 openlist_jwt_secret,而 setup 自动生成写
     openlist_encryption_secret,两个名字互不可见 -> JWT 又另生成一把,
     同一部署两把密钥漂移(多实例验签失败、冷启动换钥)。
     getJwtSecret 现在优先复用加密密钥槽位。
   - 长度阈值三处不一(>=16 / >=32 / >=32),生成的密钥可能被读取路径
     判为无效而重新生成。统一为「非空即有效」。

3) 统一 JWT 长度策略为「推荐 32、不强制」
   修正 public.ts 提示 >=32 与实际校验 >=16 的不一致;长度不足仅作
   推荐,不再作为拒绝理由。

另:STORAGE_INVALID_COMBINATION 此前 suggestion 为 null,用户只知
「错」不知「怎么改」;现补上「改成 DB_FORMAT=map / 换关系型驱动」的
一句话建议。

顺带修正 resolveDriver 形参类型:DB_DRIVER=memory 是合法用户输入,
但 StorageDriver 联合类型故意不含 "memory",导致 name === "memory"
被 TS 判为永不成立。形参放宽为 StorageDriver | "memory"。

新增 src/backend/server/storage_policy.test.ts 锁定上述不变量,
并已逐条验证:在旧逻辑下对应用例确实失败。
@PIKACHUIM
PIKACHUIM force-pushed the fix/init-setup-empty-store branch from e1d9ac9 to cfd4568 Compare September 18, 2026 08:19
# Conflicts:
#	src/backend/internal/model/store/json.ts
#	src/backend/server/middlewares.ts
@PIKACHUIM
PIKACHUIM merged commit 1a41776 into main Sep 18, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

2 participants