Conversation
…penListTeam#63) * fix(init): allow first-time setup on an empty but readable backend 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 OpenListTeam#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. * feat(init): surface storage problems and reject invalid driver/format 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). * chore(edgeone): refresh cloud-functions artifact after merging main * feat(store): report which driver auto-detection would pick 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. * fix(store): degrade to the auto-detected backend instead of 503-ing the 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 OpenListTeam#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. * chore(jwt): drop the noisy short-secret warning 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. * fix(store): honour DB_DRIVER_STRICT from process.env too 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. * chore(edgeone): refresh cloud-functions artifact for the driver fallback 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. * refactor(store): an explicitly configured driver never falls back 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. * chore(edgeone): refresh cloud-functions artifact after removing the fallback 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. * fix(worker): break the KV proxy probe self-call loop `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). * refactor(diagnostics): a one-line reason and one fix line in the setup 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".` * chore(edgeone): refresh cloud-functions artifact Keeps the EdgeOne Node bundle in sync with the worker changes above (KV proxy endpoints answer 410, diagnostics carry the one-line summary). * fix(storage): 精确化存储组合校验、统一 JWT 密钥策略并修复自动生成不生效 三条常见部署约束此前与检查逻辑不符,本次逐条对齐: 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 锁定上述不变量, 并已逐条验证:在旧逻辑下对应用例确实失败。 * fix(ci): remove docs --------- Co-authored-by: PIKACHUIM <PIKACHUIM@users.noreply.github.com>
Member
|
你好,感谢您的贡献,OpenListTeam:fix/init-setup-empty-store已经合并到main,您应该直接提p合并到main |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
openlist-work | 1a41776 | Sep 19 2026, 03:22 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
openlist-tsworkers | 1a41776 | Sep 19 2026, 03:21 PM |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
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.
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.
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).
chore(edgeone): refresh cloud-functions artifact after merging main
feat(store): report which driver auto-detection would pick
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.
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).
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.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:
suggestion, so the setup wizard still tells the user exactly which value to set.Cleanups requested in the same review:
suggestion).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.
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 withGET /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.The 503 payload also starts carrying
summary(see the next commit, which turns it into the one-line reason shown in the setup 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.
summarynext to the fullmessage(kept for logs/tooling). The UI shows summary + suggestion only.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).
三条常见部署约束此前与检查逻辑不符,本次逐条对齐:
blob 只支持 map
旧实现仅做「能力探测」(get/put/delete/list),blob 恰好四项齐备,
于是 blob+key 被误判为合法组合,直到真正读写才炸。
新增 DRIVER_FORMAT_WHITELIST 显式声明每个驱动支持的格式
(blob 仅 map),能力探测与白名单双重校验。
JWT_SECRET 支持自动生成,但此前实际不生效
三处缺陷叠加,使「自动生成」看着写了、实际没用:
统一 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 锁定上述不变量, 并已逐条验证:在旧逻辑下对应用例确实失败。
Summary / 摘要
/ 此 PR 包含破坏性变更。
/ 此 PR 修改了公开 API、配置、存储格式或迁移行为。
/ 此 PR 需要关联仓库同步修改。
Related repository PRs / 关联仓库 PR:
Related Issues / 关联 Issue
Testing / 测试
go test ./...Checklist / 检查清单
/ 我已阅读 CONTRIBUTING。
/ 我确认此贡献符合仓库许可证、贡献规范和行为准则。
gofmt,go fmt, orprettierwhere applicable./ 我已按适用情况使用
gofmt、go fmt或prettier格式化变更代码。/ 我已在适用情况下请求相关维护者或代码所有者审查。
AI Disclosure / AI 使用声明
/ 此 PR 包含 AI 辅助内容。
Tools used / 使用工具:
Usage scope / 使用范围:
Code generation / 代码生成
Refactoring / 重构
Documentation / 文档
Tests / 测试
Translation / 翻译
Review assistance / 审查辅助
I have reviewed and validated all AI-assisted content included in this PR.
/ 我已审核并验证此 PR 中的所有 AI 辅助内容。
I have ensured that all AI-assisted commits include
Co-Authored-Byattribution./ 我已确保所有 AI 辅助提交都包含
Co-Authored-By归属信息。I can reproduce all AI-assisted content included in this PR without any AI tools.
/ 我可以在没有任何 AI 工具的情况下重现此 PR 中包含的所有 AI 辅助内容。