fix(init): allow first-time setup on an empty but readable backend - #63
Conversation
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.
… 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).
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
openlist-tsworkers | e2dfc09 | Sep 18 2026, 08:40 AM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
openlist-work | e2dfc09 | Sep 18 2026, 08:40 AM |
pikachuren
left a comment
There was a problem hiding this comment.
PR #63 完整评审报告
🙏 感谢 @PIKACHUIM 提交!
🤖 AI 自动审核声明:本评审报告由 AI 自动生成,当前使用 Claude Opus 5 模型进行分析。
🎯 结论
✅ Approve — 根因分析深入透彻,解决方案完整优雅,测试覆盖充分,强烈建议合并
📖 概要
fix(init): allow first-time setup on an empty but readable backend · 关联 #62 · 解决全新部署永远无法完成安装向导的根因问题
核心改动:修复 init/setup 判定逻辑(区分「读失败」与「全新空存储」)、init_status 配合 ADMIN_PASS 自动初始化、驱动不可用时的针对性错误提示、收敛 getKvBinding 误导日志
🧭 整体方案
采用"精准护栏 + 幂等初始化 + 分类诊断"架构:
- 精准护栏(
init/setup):改用getDbLoadError()替代!isDbTrusted()— 只拦截真正的读取失败(绑定缺失/后端不可达/鉴权错误),放行全新空存储(这正是安装向导存在的意义) - 幂等初始化(
init_status):配置ADMIN_PASS时触发getOrInitUsers(),未配置时不调用(避免每次轮询尝试写入未初始化占位库) - 分类诊断(
backend.ts):引入StoreConfigErrorCode机器可读错误码(INVALID_COMBINATION / DRIVER_UNAVAILABLE / UNKNOWN_DRIVER / NO_STORAGE / PROXY_CONFIG / HEALTH_ERROR / DRIVER_ERROR),每种驱动不可用时给出针对性提示(如kv区分 CF 原生绑定与 EdgeOne 代理模式) - 日志收敛(
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.ts4 个 +CAS codec1 个),建议在 commit message 或 PR comment 中补充基线提交哈希,方便审阅者复现
📂 逐文件分析
src/backend/server/public.ts(核心修复)
改动意图:修复 init/setup 与 init_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,展现了深入的问题排查能力与系统性思维:
- 问题定位精准:死锁的核心不是「护栏太严」,而是「
loadDb()对空存储设置了dbTrusted = false,但init/setup只看isDbTrusted()无法区分『读失败』与『空存储』」 - 方案设计完整:不仅修复了核心死锁,还一并解决了
ADMIN_PASS不生效、驱动错误无提示、KV 日志误导等 4 个相关问题 - 诊断体验升级:引入
StoreConfigErrorCode机器可读分类 + 逐驱动针对性提示,用户不再困惑于「我明明绑了为什么不行」 - 测试覆盖充分:新增 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 锁定上述不变量,
并已逐条验证:在旧逻辑下对应用例确实失败。
e1d9ac9 to
cfd4568
Compare
# Conflicts: # src/backend/internal/model/store/json.ts # src/backend/server/middlewares.ts
fix(init): allow first-time setup on an empty but readable backend
Base:
main← Compare:fix/init-setup-empty-storeCommit:
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/setupreturned500 database is not readableon everyattempt, and with
ADMIN_PASSconfigured the site kept bouncing back to/@init. This fixes both, plus two related dead ends that made the failure hardto diagnose.
全新部署永远无法完成安装向导:
POST /api/public/init/setup每次都返回500 database is not readable;配置了ADMIN_PASS的部署则反复跳回/@init。本次一并修复这两个问题,以及另外两处让排查走偏的死角。
Root cause / 根因:
loadDb()reports two different kinds of "untrusteddatabase", but
init/setuponly looked at one flag (isDbTrusted()):initialize, otherwise the fallback shell overwrites real config;
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 / 用户可感知的行为变化:
(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。ADMIN_PASSset, opening the site is enough to auto-initialize the adminaccount — the init page only polls
init_statusand never calls the loginendpoint, so
ADMIN_PASShad no effect before.配置了
ADMIN_PASS时,打开站点即可自动完成初始化 —— 安装页只轮询init_status、从不调用登录接口,因此该配置此前完全不生效。DB_DRIVER=kvthat is not usable now explains why (Cloudflareneeds a binding named
KV; EdgeOne Node functions need the Edge Function proxyJWT_SECRET) and suggestsauto/blob/d1.显式
DB_DRIVER=kv不可用时,错误会说明为什么(CF 需要名为KV的绑定;EdgeOne Node 云函数需要 Edge Function 代理 +
JWT_SECRET),并给出auto/blob/d1等替代方案。[DB] getKvBinding: no KV-style binding found ...warning on writes.非 KV 驱动不再在写入时打印误导性的
[DB] getKvBinding: no KV-style binding found ...告警。Important implementation changes / 重要实现变化:
init/setupgates ongetDbLoadError()instead of!isDbTrusted(); the HTTPresponse text is unchanged, and the concrete load error is appended to the
server-side log line only.
init/setup改用getDbLoadError()判定(不再是!isDbTrusted());对外响应文案保持不变,具体错误只追加到服务端日志。
init_statustriggers the idempotentgetOrInitUsers()whenADMIN_PASSisconfigured (and does nothing when it is not, so no placeholder DB is written on
every poll).
init_status在配置了ADMIN_PASS时触发一次幂等的getOrInitUsers();未配置时不调用,避免每次轮询都尝试写入未初始化的占位库。
resolveDriver()appends a targetedkvhint to the "driver is not available"error.
resolveDriver()在「驱动不可用」错误中追加针对kv的排查提示。getKvBinding()only logs its "no KV-style binding" warning for KV-flavoureddrivers (
auto/kv/cfkv/blob/ unset). This function is also reachedby 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]].jsvianode 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:
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 / 根因分析
初始化被拒;被拒之后库里依然没有数据,下一次请求继续被拒 —— 用户看到的就是
安装页无限重试 + 500(日志中 10 分钟内 77 次请求全部 500)。
ADMIN_PASS失效:getOrInitUsers()只挂在登录 / WebDAV 路径上,而/@init页面只轮询init_status,从不登录 → 「未初始化 → 跳 /@init → 永远不初始化」。
DB_DRIVER=kv不可用:kv是唯一一个「同名驱动在不同运行时要求完全不同」的驱动(CF 需
kv_namespaces: [{ binding: "KV" }];EdgeOne Node 只能走Edge Function 代理 +
JWT_SECRET),原错误只说了 "driver is not available"。getKvBinding()的兜底告警对 D1 部署无意义,但它会被审计日志等路径触发,容易被误当成 500 的根因。
Testing / 测试
npm run test:server→ 46 tests / 41 pass(5 个失败为本 PR 之前就存在的历史失败,已在基线提交上复现:
default_credentials.test.ts4 个 +CAS codec1 个,均与本 PR 无关)npm run test:store→ 11 pass / 0 failnpx tsc -p tsconfig.json --noEmit→ 无错误node scripts/build-edge.mjs→ 产物已同步(每个场景 = 干净的 isolate 模块状态),修复前后对照:
DB_DRIVER=kv+map)→ 初始化 → 落盘 → 新实例可读 → 登录DB_DRIVER=auto+ KV 绑定)→ 自动探测到 kv → 初始化map空存储 → 初始化 → 新实例可读 → 登录ADMIN_PASS→ 安装页轮询init_status即完成初始化 → 用该密码登录DB_DRIVER=kv不可用 → 错误须说明原因与替代方案补充验证 / Additional checks:
site_title一并持久化。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才有写副作用」的幂等接口;未配置时行为与之前完全一致。
cloud-functions/[[default]].js,本 PR 已按 CI 的edgeone-artifact-guard流程重建;若官方前端产物同时更新,CI 会再要求一次pnpm run build。wrangler.jsonc的vars每次部署都会被覆盖,需要长期保留的
DB_DRIVER/JWT_SECRET请写入该文件或用 Secret 配置。