Skip to content

feat(assets): inject ASSET_URLS into index.html for direct CDN loading - #60

Open
Wudarensheng wants to merge 10 commits into
OpenListTeam:mainfrom
Wudarensheng:main
Open

Wudarensheng wants to merge 10 commits into
OpenListTeam:mainfrom
Wudarensheng:main

Conversation

@Wudarensheng

Copy link
Copy Markdown
Member

Summary / 摘要

本 PR 按 OpenList-Worker Issue #26 优化 ASSET_URLS 功能。

  • 将原先的静态资源 Worker 302 重定向改为在 index.html 中注入 window.OPENLIST_CONFIG.cdn

  • 浏览器后续直接向 CDN 请求 JS、CSS、图片等静态资源,不再让每个资源请求都经过 Worker。

  • 未配置 ASSET_URLS 时保持现有行为。

  • 支持 $version 占位符:从数据库 version 设置中提取前端版本;无法解析时回退为 latest

  • 未使用 $version 时不读取数据库,避免额外存储请求。

  • 覆盖 Cloudflare ASSETS、Cloudflare SPA fallback、EdgeOne/ESA spaFallbackHtml 三种 HTML 返回路径。

  • 保留 logo/favicon 兼容性重定向。

  • 删除原有不可用的 /:folder/:filepath* 302 路由:该实现无法正确取得 filepath,且不适配 map 数据库模式。

  • 增加单元测试与集成测试,并补充环境变量文档。

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

  • This PR changes public API, config, storage format, or migration behavior.
    / 此 PR 修改了公开 API、配置、存储格式或迁移行为。
    说明:新增公开配置项 ASSET_URLS;不修改存储格式或迁移行为。

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

Related repository PRs / 关联仓库 PR:

  • OpenList: 无
  • OpenList-Docs: 无

Related Issues / 关联 Issue

Relates to #26

Testing / 测试

已在 Windows 11、Node.js v24.16.0 环境执行:

  • go test ./...(不适用:本仓库为 TypeScript/Hono 项目,无 Go 测试套件)
  • npx tsx --test src/backend/server/assets.test.ts
    9/9 通过。
  • npx tsx --test src/backend/server/index_cdn.test.ts
    5/5 通过,覆盖 ASSETS 入口、ASSETS SPA fallback、无 ASSETS 的 spaFallbackHtml 以及 $version 回退。
  • npx tsc -p tsconfig.json --noEmit
    类型检查通过。
  • npx tsx --test "src/backend/server/*.test.ts"
    54 个测试中 51 个通过,3 个失败为本次改动前已存在的 F-11/CAS 测试失败;新增 ASSET_URLS 测试均通过。
  • Manual test / 手动测试:
    未在实际 CDN 部署环境中进行浏览器手动验证;已使用模拟 ASSETS 绑定完成请求级集成验证。

Checklist / 检查清单

  • I have read CONTRIBUTING.
    / 我已阅读 CONTRIBUTING
  • I confirm this contribution follows the repository license, contribution policy, and code of conduct.
    / 我确认此贡献符合仓库许可证、贡献规范和行为准则。
  • I have formatted the changed code with gofmt, go fmt, or prettier where applicable.
    / 我已按适用情况使用 gofmtgo fmtprettier 格式化变更代码。
    说明:本次已完成 TypeScript 类型检查,但未单独运行 Prettier。
  • I have requested review from relevant maintainers or code owners where applicable.
    / 我已在适用情况下请求相关维护者或代码所有者审查。

AI Disclosure / AI 使用声明

  • This PR includes AI-assisted content.
    / 此 PR 包含 AI 辅助内容。

Tools used / 使用工具:

  • ChatGPT
  • Codex
  • GitHub Copilot
  • Claude
  • Gemini
  • Other (please specify) / 其他(请注明): CodeBuddy Code

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-By attribution.
    / 我已确保所有 AI 辅助提交都包含 Co-Authored-By 归属信息。
    说明:当前提交未包含 Co-Authored-By 归属信息,故不勾选。

  • I can reproduce all AI-assisted content included in this PR without any AI tools.
    / 我可以在没有任何 AI 工具的情况下重现此 PR 中包含的所有 AI 辅助内容。

Replace the broken 302-redirect ASSET_URLS implementation with HTML
injection of window.OPENLIST_CONFIG.cdn, as discussed in issue OpenListTeam#26.
The browser then loads JS/CSS/images directly from the CDN, bypassing
the Worker for the dozens of per-page asset requests (1-2 orders of
magnitude fewer Worker invocations).

- Remove the non-functional /:folder/:filepath* 302 route (filepath
  param was always undefined, multi-segment paths never matched, and
  db.get() targeted a method absent on map-mode DB)
- Add resolveCdnUrl() + injectCdnIntoHtml() mirroring Go's
  server/static/static.go cdn injection;  resolves best-effort
  from the version setting, falling back to "latest", and skips DB
  reads entirely when no $version placeholder is present
- Wire injection into all three HTML-serving paths in index.ts
  (ASSETS entry, ASSETS SPA fallback, spaFallbackHtml)
- Add unit + integration tests (14 passing)
- Document ASSET_URLS in .env.example / .dev.vars.example / wrangler.jsonc
@PIKACHUIM

Copy link
Copy Markdown
Member

感谢您的贡献,可以看看和#52 的差异

@jyxjjj

jyxjjj commented Sep 16, 2026

Copy link
Copy Markdown
Member

@PIKACHUIM

这里有 3 个需要重新确认的问题:

  1. revert(customize): remove backend-side index.html injection 这个提交 (fa3437ce) 并没有对应 PR,是直接进入主分支的。这个改动实际上改变了 customize_head/customize_body 的实现方式,影响不算小,不建议把它当成已经经过正常 review 的既定设计。

  2. 当前 ASSET_URLS 的实现依然受到 run_worker_first 影响。

现在的逻辑是在 Worker 里:

const res = await env.ASSETS.fetch(...)
const html = await res.text()
html = await injectCdnIntoHtml(html, env)
return new Response(html, ...)

但如果 Cloudflare Static Assets 已经直接命中 //index.html,且没有配置合适的 run_worker_first,请求根本不会进入 Worker。

也就是说,“Worker 内部直接读取并输出 HTML”并不能解决这个问题,因为问题发生在 Worker 执行之前。

如果继续采用服务端修改 index.html 的方案,至少需要保证 HTML 入口先进 Worker,例如只针对 //index.html 配置 selective run_worker_first,而不是假设当前代码天然可以拦截 index。

  1. 我不认为 customize_head/customize_body 应该改成前端 runtime 注入。

这两个配置的语义本身就是修改最终 HTML 的 <head> / <body>。更合理的实现应该是在后端返回 index.html 时完成注入,而不是等 React/JS 启动后再由前端补进去。

前端注入会带来几个明显区别:

  • 内容不再属于初始 HTML;
  • 依赖 JS 成功加载和执行;
  • <head> 中某些要求在页面初始化前存在的内容无法保证原有语义;
  • JS 被禁用、加载失败或者前端初始化异常时,自定义内容完全不会生效;
  • 与 OpenList 原本服务端 UpdateIndex() 的行为也不一致。

所以我建议不要因为之前 Cloudflare 的 asset-first 路由导致“后端注入不生效”,就把注入职责移动到前端。真正需要修的是 HTML 请求如何可靠进入后端处理链路

而且 当前 PR #60 现在实际上又重新采用了“后端读取 index HTML → 修改 → 返回”的方式来注入 cdn。既然 ASSET_URLS 需要这样做,那么 customize_head/customize_body 也应该使用同一套 HTML transform 机制,而不是维护两套行为不同的实现。

@PIKACHUIM

Copy link
Copy Markdown
Member

@PIKACHUIM

这里有 3 个需要重新确认的问题:

  1. revert(customize): remove backend-side index.html injection 这个提交 (fa3437ce) 并没有对应 PR,是直接进入主分支的。这个改动实际上改变了 customize_head/customize_body 的实现方式,影响不算小,不建议把它当成已经经过正常 review 的既定设计。
  2. 当前 ASSET_URLS 的实现依然受到 run_worker_first 影响。

现在的逻辑是在 Worker 里:

const res = await env.ASSETS.fetch(...)
const html = await res.text()
html = await injectCdnIntoHtml(html, env)
return new Response(html, ...)

但如果 Cloudflare Static Assets 已经直接命中 //index.html,且没有配置合适的 run_worker_first,请求根本不会进入 Worker。

也就是说,“Worker 内部直接读取并输出 HTML”并不能解决这个问题,因为问题发生在 Worker 执行之前。

如果继续采用服务端修改 index.html 的方案,至少需要保证 HTML 入口先进 Worker,例如只针对 //index.html 配置 selective run_worker_first,而不是假设当前代码天然可以拦截 index。

  1. 我不认为 customize_head/customize_body 应该改成前端 runtime 注入。

这两个配置的语义本身就是修改最终 HTML 的 <head> / <body>。更合理的实现应该是在后端返回 index.html 时完成注入,而不是等 React/JS 启动后再由前端补进去。

前端注入会带来几个明显区别:

  • 内容不再属于初始 HTML;
  • 依赖 JS 成功加载和执行;
  • <head> 中某些要求在页面初始化前存在的内容无法保证原有语义;
  • JS 被禁用、加载失败或者前端初始化异常时,自定义内容完全不会生效;
  • 与 OpenList 原本服务端 UpdateIndex() 的行为也不一致。

所以我建议不要因为之前 Cloudflare 的 asset-first 路由导致“后端注入不生效”,就把注入职责移动到前端。真正需要修的是 HTML 请求如何可靠进入后端处理链路

而且 当前 PR #60 现在实际上又重新采用了“后端读取 index HTML → 修改 → 返回”的方式来注入 cdn。既然 ASSET_URLS 需要这样做,那么 customize_head/customize_body 也应该使用同一套 HTML transform 机制,而不是维护两套行为不同的实现。

这个是需要斟酌的点:
1、customize_*需要改前端(已合入),但好处是纯静态资源,一方面是不占用CF/EO之类的请求计数,另一方面是纯前端静态资源能吃缓存和CF/EO分布式响应的优势,并且还能设置CDN_URL指向加速站点
2、后端修改页面内容,唯一的好处是响应一致性——也就是避免前端fetch后后端还没响应之间页面表现不一致

我更倾向于1,也就是revert(customize): remove backend-side index.html injection 这个提交 (fa3437c) 实现逻辑
另外CF付费用户可以用Cloudflare Snippets,它很完美,但有两个缺点——只支持CF并且需要付费

@jyxjjj

jyxjjj commented Sep 16, 2026

Copy link
Copy Markdown
Member

但我还是不太接受前端 runtime 注入,尤其是 head,这已经和“服务端返回的 head”不是一回事了。
我对SPA本身就很不爽,奈何现在各家框架其实只有SPA能用好用。
OpenList 这种 SPA 好就好在还能靠后端改 index.html 把语义补回来。
所以我的想法是要么别支持 customize_head,要么就强制让 HTML 走 Worker,接受这部分调用次数。

@jyxjjj

jyxjjj commented Sep 16, 2026

Copy link
Copy Markdown
Member

至于CDN这块 其实无所谓 前端拿到以后为后端补齐域名/后端提前返回 是差不多的

@PIKACHUIM

PIKACHUIM commented Sep 16, 2026

Copy link
Copy Markdown
Member

这里有个考虑的点,就是大部分用户其实是不会自定义head和body的
如果强制资源走worker,会导致大部分用户为这个用不上的功能买单

对,CDN其实无所谓,直接返回指向和先载入前端区别并不大
并且经过处理再返回index,可能会对页面加载速度的体验有影响

@jyxjjj

jyxjjj commented Sep 16, 2026

Copy link
Copy Markdown
Member

https://t.me/OpenListOfficial/44
https://t.me/OpenListTeam/253866
发了个投票 大家决定吧

@Wudarensheng

Copy link
Copy Markdown
Member Author

客户端js注入前端呢?

Configuring ASSET_URLS produced a flood of 404s (302 -> not found on
registry CDNs) and a blank page, because the served HTML was the local
build while the CDN served a different frontend build:

- $version always resolved to "latest" — the DB version setting never
  carries a "Frontend:" segment, so the resolution path was dead code
- even a correct version number does not match: hashed asset names are
  build-specific (local main build index-CKHDcXcr.js vs npm 4.2.6
  index-CelfHslL.js), so every /assets/* request 404s on the CDN

Fix, mirroring Go's server/static/static.go initIndex():

- getIndexHtmlWithCdn(): fetch ${cdn}/index.html (4s timeout, module
  cache with TTL, http(s) only, HTML sanity check), inject cdn into it,
  and serve that. HTML and hashed assets now come from the same build.
- Graceful degradation: if the CDN is unreachable or returns non-HTML,
  fall back to the local HTML WITHOUT injecting cdn, so assets load from
  the origin and the site keeps working instead of white-screening.
- fetch-frontend.mjs stamps <meta name="frontend-version"> into
  dist/index.html (guarded to the official frontend package name), so
  $version resolves to the version actually deployed.
- $version priority: build stamp -> DB "Frontend:" -> latest.

Tests: 17 unit + 6 integration (all passing); tsc clean; the 3 failing
server tests (F-11 x2, CAS) are pre-existing and unrelated.
Cloudflare 的静态资源路由默认「资源优先」:/ 与 /index.html 由静态层直接返回,
Worker 脚本根本不执行,导致 ASSET_URLS 的 CDN 注入永远不生效 —— 浏览器拿到
仍是 cdn: undefined 的源站 HTML,所有 JS/CSS/图片继续从 Worker 域名加载。

修复:
- wrangler.jsonc 增加 assets.run_worker_first: ["/", "/index.html"],只放行
  HTML 入口。带 hash 的 JS/CSS/图片仍由静态层直出,零 Worker 调用;仅页面导航
  多付一次 Worker 调用。
- 未配置 ASSET_URLS 时零开销直通:不读取 body,直接流式透传静态层响应,
  非 CDN 用户不为本次改动额外付出 HTML 缓冲与解析成本。
- 改写 HTML(注入 cdn)时清掉 content-encoding / content-length:body 已被读成
  字符串,保留编码头会让浏览器按「已编码」解析明文而报错。

测试:assets.test.ts + index_cdn.test.ts 25/25 通过(新增 2 例:零开销直通断言
body 流被原样透传、注入路径清编码头);tsc --noEmit 通过;wrangler deploy
--dry-run 配置校验通过。全量 server 套件 62/65,3 个失败为改动前既有。
…DN requirements

实测结论(2026-09-18):

registry.npmmirror.com/<pkg>/<ver>/files/
  dist/index.html   -> 451 {"error":"blocked"}(该端点只放行 js/css,拦截 .html)
  dist/assets/*.js  -> 200,但【不返回 Access-Control-Allow-Origin】
cdn.npmmirror.com/packages/<pkg>/<ver>/files/
  dist/index.html   -> 200
  dist/assets/*.js  -> 200,同样【不返回 Access-Control-Allow-Origin】,且不支持 latest

前端产物以 crossorigin 加载 module script / modulepreload / stylesheet / 字体,
缺少 CORS 头会被浏览器整批拦下;因此 npmmirror 的两个端点都无法作为 ASSET_URLS。
jsdelivr 与 unpkg 均返回 Access-Control-Allow-Origin: *,实测可用。

把文档里原先推荐的 npmmirror 示例换成 unpkg,并写明 ASSET_URLS 必须同时满足
「能返回 index.html」与「带 Access-Control-Allow-Origin」两个硬性条件。
代码逻辑无需改动:CDN 返回非 HTML(如 451 JSON)时已有降级回退。
@PIKACHUIM

Copy link
Copy Markdown
Member

PR #52 / #60 / #671 评审与「自定义 header·body」修复决策

评审对象(OpenListTeam)

结论基于逐行读码 + 本地 git 校验 + GitHub CI 状态,证据与核查命令见附录。


1. 结论摘要(TL;DR)

  1. [废弃] fix(spa): 注入站点设置到 index.html,修复自定义头部/CSS/JS 不生效 #52feat(assets): inject ASSET_URLS into index.html for direct CDN loading #60 不是"二选一",而是前后依赖关系feat(assets): inject ASSET_URLS into index.html for direct CDN loading #60 给 HTML 入口加了 assets.run_worker_first,恰好补上 [废弃] fix(spa): 注入站点设置到 index.html,修复自定义头部/CSS/JS 不生效 #52 在 Cloudflare 上唯一缺失的生效前提;两者改的是 src/backend/index.ts同一个 app.all("*") 的三块分支,语义上必须串成一条统一管线,否则就是 feat(assets): inject ASSET_URLS into index.html for direct CDN loading #60 讨论里被批评的"两套行为不同的实现"。
  2. feat(assets): inject ASSET_URLS into index.html for direct CDN loading #60 本身不修 customize_head/body,它只注入 window.OPENLIST_CONFIG.cdn。若目标是修自定义内容,只合 feat(assets): inject ASSET_URLS into index.html for direct CDN loading #60 无效。
  3. [废弃] fix(spa): 注入站点设置到 index.html,修复自定义头部/CSS/JS 不生效 #52 单独合入修不好:CF 首页不生效(静态层直出)、EdgeOne 全站导航不生效(middleware.js 已把导航 rewrite 给静态 CDN),且分支落后 main 7 个 commit、产物过期。
  4. #671 与 [废弃] fix(spa): 注入站点设置到 index.html,修复自定义头部/CSS/JS 不生效 #52 互斥(不是"可不合",是"不能都合"):TS 后端下 isTsWorker() 为真,两边都注入会造成自定义 JS 执行两遍。
  5. 走前端路线(#671)是当前最省成本且覆盖最广的选择,唯一实质性功能缺口是 favicon / apple-touch-icon 仍然失效(前端代码中 favicon 零引用),以及 SEO/无 JS/FOUC 场景。
  6. 硬阻断(两条都必须在合并前解决)

2. 基本事实

#52 #60 #671(前端)
仓库 OpenList-Worker OpenList-Worker OpenList-Frontend
作者 PIKACHUIM Wudarensheng PIKACHUIM
状态 Open Open(曾关闭后重开) Open
关联 issue #49 #26(Related)
提交数 1 4 3
变更规模 5 文件(+659 / −89,含产物 167 行) 8 文件(+723 / −46) 3 文件(+66 / −7)
是否修 customize
CI 分支落后 7 个 commit → 产物过期(推断失败,建议 gh pr checks 52 复核) Artifact Guard 失败(已实测) 未核

3. 背景时间线

时间 事件
09-09 23:10 Worker c745e12:在 index.ts 内联实现后端 customize 注入
09-09 23:46 前端 PR #671 提交(前端运行时注入)
09-09 23:56 Worker fa3437c 直推 main(无 PR)revert(customize): remove backend-side index.html injection。提交信息原文理由:"Customization injection is now handled by the frontend at runtime (OpenList-Frontend feat/customize-injection, only for TS backend). Keeping both server-side and client-side injection would double-apply on Cloudflare Workers deployments, and server-side injection never works on pure static deployments (EdgeOne/Vercel)."
09-14 Worker PR #52 提交:把后端注入加回来,且做得更完整(新增 server/index-html.ts、6 组占位符、缓存 + 失效钩子、10 个测试)
09-16 PR #60 讨论爆发。JYXJJJ 明确反对前端注入(<head> 语义、无 JS 失效、与 Go UpdateIndex() 不一致),主张"要么别支持 customize_head,要么强制 HTML 走 Worker";PIKACHUIM 倾向"大部分用户用不上这个功能,不该让所有请求走 Worker";JYXJJJ 发起 Telegram 投票
09-16 ~ 09-18 #60 追加 3 个 commit 修白屏 / 加 run_worker_first / 补 CDN 文档
现在 两边 main 都没有任何 customize 注入实现 → 除 Go 后端外,customize_head/body 完全失效

关键理解:#52 本质上是推翻 main 上已落地的 fa3437c 决策的路线复议;#671 是该决策的执行者(因未合并而落空)。


4. 三个 PR 各自做了什么

4.1 #60ASSET_URLS 注入 HTML,对齐 Go initIndex()

原实现 assetsRouter.get("/:folder/:filepath*") 把每个静态资源 302 重定向到 CDN,导致每个 JS/CSS/图片请求都要过一次 Worker。改为注入 window.OPENLIST_CONFIG.cdn,让浏览器直连 CDN。4 个 commit 是踩坑演进:

  1. cf8b831 初版:注入 cdn + 删除不可用的 302 路由;
  2. 6e7ccbd 白屏修复:本地 HTML 引用的哈希在 CDN 上不存在(版本偏差 / $version 恒为 latest)→ 全量 404。改为从 ${cdn}/index.html 拉 HTML(HTML 与哈希资产天然同源),加 4 秒超时、模块级 TTL 缓存、HTML 合法性校验、优雅降级(CDN 挂了则不注入 cdn、回退源站);
  3. 4252790 关键:CF 默认「资源优先」,//index.html 由静态层直出、Worker 根本不执行 → 加 "run_worker_first": ["/", "/index.html"];改写 HTML 时清掉 content-encoding / content-length
  4. 4315259 文档:实测 npmmirror 两个端点均不可用(registry 对 .html 返 451,且都无 Access-Control-Allow-Origin),推荐 jsdelivr / unpkg。

4.2 #52(站点设置注入 index.html)

新增 src/backend/server/index-html.ts(169 行):

  • INDEX_HTML_PLACEHOLDERS:6 组占位符 ↔ 设置键映射,语义对齐 Go UpdateIndex()
  • split().join() 而非 String.replace()(避免自定义 JS 中的 $& / $1 被当替换模式);
  • main_color 做引号 / 反斜杠转义(主动不与 Go 的缺陷对齐);
  • 空值跳过替换(避免 favicon 变空字符串);
  • buildIndexHtml(template, env)WeakMap<env, string> 缓存 + 失败降级返回原始 HTML(绝不 500);
  • db.ts 新增 onDbWrite() 通知钩子,saveDb() 内触发缓存失效(用注册回调规避循环依赖,设计合理);
  • index.ts 接入三块 HTML 分支,并统一 Content-Type + Cache-Control: no-cache

占位符 ↔ 设置键:

占位符 设置键 备注
<!-- customize head --> customize_head 原样注入
<!-- customize body --> customize_body 原样注入
https://res.oplist.org/logo/logo.svg favicon
https://res.oplist.org/logo/logo.png logo 只取第一行
<title>Loading...</title> site_title
main_color: undefined main_color 包成 main_color: '<value>'

4.3 #671(前端运行时注入)

  • src/utils/customize.ts(新增 43 行):<template> 解析 HTML → 对 <script> 节点手工重建(复制属性 + textContent)再 append,保证自定义 JS 真正执行;非 script 节点 cloneNode(true) 后 append;分别注入 document.head / document.body
  • src/app/App.tsx:在 /public/settings 返回后调用 applyCustomize(),并用 isTsWorker() 门控,避免 Go 后端重复注入。
  • 第 3 个 commit(05b634c夹带backup-restore.tsx 的解密修复(加密备份恢复后 settings 仍是密文)。

5. 实现方式对照

维度 #52 #60
改写的 HTML 位置 同一处三块env.ASSETS/ + /index.html、SPA fallback、内联 spaFallbackHtml 同一处三块(完全相同)
转换方式 6 组 split().join() 占位符替换 拉 CDN HTML 后 replace(/cdn:\s*undefined/, ...)
缓存 WeakMap<env, string>无 TTL,靠 saveDb() 通知失效 Map<cdnUrl, {html, ts}>TTL 5 分钟
降级 读 DB 失败 → 原始 HTML(不 500) CDN 不可达 / 非 HTML → 本地 HTML 且不注入 cdn
平台配置 ❌ 未改 wrangler.jsonc(CF 首页不生效) run_worker_first: ["/", "/index.html"]
响应头 复用原 headers,未删 content-encoding / content-length / 分支明确删除这两个头
附带修复 修掉 db.get("SELECT * FROM x_settings ...")(map 格式存储上不存在的方法);$version 三级解析(构建期 meta 戳 → DB「Frontend: vX」→ latest)
测试 10 个(8 单测 + 2 集成,直接打真实 Hono app) 9 单测 + 5 集成
文档 .env.example / .dev.vars.example / wrangler.jsonc 注释

冲突面:两者改 src/backend/index.ts 里同一个 app.all("*") 的三块分支,文本必然冲突,语义必须串成一条管线。


6. 关键关系:不是二选一

6.1 #60#52 补上了唯一缺失的拼图

origin/mainwrangler.jsonc 只有:

"assets": {
  "directory": "./dist",
  "binding": "ASSETS"
}

全仓库 grep run_worker_first = 0 命中。而 CF 静态资源默认「资源优先」://index.html 命中 dist/index.html 时由静态层直出、Worker 不执行 → #52 的注入在首页是死代码。#60run_worker_first 正是解药,且它的 .env.example 注释里已经写明这个前提。

6.2 组合必须是「先 CDN 后设置」

若两 PR 都合,必须在 CDN 拉回来的那份 HTML 上做设置注入,否则官方 HTML 会覆盖已注入的设置:

// 目标形态(示意)
let html = template
if (isCdnConfigured(env)) html = await getIndexHtmlWithCdn(env, html) // #60:定稿 HTML
html = await buildIndexHtml(html, env)                                // #52:在定稿 HTML 上注入设置
return htmlResponse(html, status, baseHeaders)                        // 统一头:删 content-encoding/length + no-cache

可行性已确认:官方前端 index.html 中确实保留了 <!-- customize head --> / <!-- customize body --> 占位符,因此从 CDN 拉回的 HTML 同样可被 #52 注入。

6.3 生效矩阵

部署形态 #52 单独 #52 + #60 #671(前端)
CF Workers / 首页 ❌ 静态层直出
CF Workers 深链 fallback
EdgeOne(Node 云函数) 仍不生效
ESA ✅(大概率)
纯静态 / CDN + ASSET_URLS

EdgeOne 是唯一"#52 + #60 都合了也修不好"的平台:导航请求在边缘中间件就被改写到静态资源:

  if (
    !isBackend &&
    (request.method === "GET" || request.method === "HEAD") &&
    accept.includes("text/html")
  ) {
    return rewrite("/index.html")
  }
  "rewrites": [
    {
      "source": "/*",
      "destination": "/index.html"
    }
  ],

→ 页面导航由静态 CDN 直出 dist/index.html,Node 云函数(api/_makers.tsspaFallbackHtml 分支)拿不到页面请求。
要修只有两条路:(a) 让导航请求进 Node 云函数(放行 / 与前端路由,代价是每次页面加载一次函数调用);(b) 交给前端 #671。


7. 待修问题清单

7.1 硬阻断

编号 问题 证据
B-1 #60 未提交重建后的 cloud-functions/[[default]].js → EdgeOne Artifact Guard 失败(exit 1),且 EdgeOne 线上仍是旧代码 GitHub checks 实测:Fail on out-of-sync artifact (pull request)git diff --name-only origin/main...pr-60 的 8 个文件中不含该产物;守卫规则见 .github/workflows/edgeone-artifact-guard.yml:102-107
B-2 #52 落后 origin/main 7 个 commit,产物与当前源码不匹配 git merge-base origin/main fix/customize-html-injection5cac7ebgit rev-list --count fix/customize-html-injection..origin/main7#54/#56/#50/#57/#59/#53/#58)。其中 db.tsstore/json.tspublic.tsraw.tsfs.ts 均被改动,产物必然不一致 → 需 rebase + 重建
B-3 两个 PR 都提交了 1.5MB 生成产物,先合的一方会让另一方产物失效 产物冲突不要手工解,rebase 后执行 node scripts/build-edge.mjs 重新生成

7.2 #60 的具体问题(4 点)

  1. SPA fallback 分支无条件缓冲 body/ 分支有 isCdnConfigured() 零开销直通,fallback 分支却是 let html = await rootRes.text() 无条件执行 —— 未配置 ASSET_URLS 时也白付一次缓冲 + 重建 Response,丢掉了原来的流式透传。
  2. 非 2xx 被包装成 200:原实现 return env.ASSETS.fetch(rootReq) 原样透传状态码;现在固定 new Response(html, { status: 200 }),若子请求返回 307/404 会变成 200 + 错误正文。([废弃] fix(spa): 注入站点设置到 index.html,修复自定义头部/CSS/JS 不生效 #52 在这一点上反而更稳:它先判断 2xx 才改写。)
  3. injectCdnIntoHtml 单引号未转义cdn: '${cdn}'ASSET_URLS' 会破坏内联脚本。对比 [废弃] fix(spa): 注入站点设置到 index.html,修复自定义头部/CSS/JS 不生效 #52main_color 特意做了引号/反斜杠转义,同一类风险处理不一致。
  4. 删除 302 路由属 breaking changeassetsRouter.get("/:folder/:filepath*") 整体移除。原实现确实有问题(db.get("SELECT ...") 在 map 格式存储上不存在),但 PR 标了 breaking 却无迁移说明,建议补 release note / README 说明。

7.3 #52 的具体问题(3 点)

  1. wrangler.jsonc 未加 run_worker_first → CF 首页不注入(由 feat(assets): inject ASSET_URLS into index.html for direct CDN loading #60 补齐)。
  2. 改写 body 后未删 content-encoding / content-length:而 feat(assets): inject ASSET_URLS into index.html for direct CDN loading #60 在同一个文件的同一分支里明确删掉了,并注释"否则浏览器按「已编码」解析明文"。同一代码库已有正确做法,说明这不是理论风险。
  3. 缓存设计不一致WeakMap<env, string> 且无 TTL → 跨 isolate 失效不了(其他 POP 的 isolate 会一直返回旧 HTML,直到被回收)。DB 缓存有 1s TTL、feat(assets): inject ASSET_URLS into index.html for direct CDN loading #60 的 CDN HTML 缓存有 5min TTL,唯独 HTML 注入结果没有。建议改用环境无关的版本戳(DB updated_at)或加短 TTL。

7.4 #671 的具体问题(3 点)

  1. applyCustomize() 无幂等:App 只要重新挂载一次(路由重建、HMR、未来某处再调一次)就会重复注入。建议加模块级 flag 或 document.head.dataset.openlistCustomize 标记。
  2. 夹带无关改动:第 3 个 commit(backup-restore.tsx 解密修复)本身是对的(加密备份恢复后 customize_head/body 仍是密文),但应拆为独立 PR。
  3. 无测试:PR 正文测试项为空,injectFragment 至少需覆盖「<script> 被重建并执行」与「重复调用不重复注入」。

8. 需求对照

编号 需求 提出方 #52(+#60) #671
R1 TS 后端下 customize_head/body 生效 共同
R2 不与 Go 后端重复注入 共同 ✅(isTsWorker() 门控)
R3 纯静态部署也生效 PIKACHUIM
R4 不让所有页面请求走计算层 PIKACHUIM ❌(需 run_worker_first
R5 <head> 语义 / 无 JS 可用 / SEO 可见 JYXJJJ
R6 改完设置尽快生效、无需冷启动 共同 ⚠️ 跨 isolate 有窗口
R7 与其它 PR 可共存、无双实现分叉 共同 ⚠️#60 需合成管线

功能性设置覆盖(走 #671 后)

设置项 是否生效 说明
customize_head / customize_body 核心目标达成
site_title ⚠️ 部分 前端 useTitle 会改 document.title;但初始 HTML 的 <title> 仍为 Loading...,爬虫/社交卡片读到旧值
main_color getMainColor() 兜底读 getSetting("main_color"),只差首屏一帧默认色
logo 前端 Login/Header/Init 自行读 settings
favicon 前端 .ts/.tsxfavicon 零引用index.html 硬编码 https://res.oplist.org/logo/logo.svg
apple-touch-icon 同上,硬编码 logo.png

补充:要判断 UI 是否误判,注意 /api/public/settings 一直正确返回这些字段(customize_head/body 的默认值在 public.tssettingsObj 中,属于公开白名单),所以接口层看起来完全正常。


9. 建议路线

路线 A(推荐,若接受 R5 的取舍):前端注入

路线 B:服务端注入(需要 SEO / 无 JS / <head> 语义硬需求)

路线 C(混合,成本最高)

服务端做主路径(#60 + #52 + 平台改道),前端 #671 作为兜底(带标记互斥)。只有同时需要"EdgeOne/纯静态可用"与"SEO/无 JS 可用"才值得。

9.3 统一 HTML 管线(路线 B 的落地形态)

resolveTemplate(ASSETS /内联壳 / CDN index.html)
  → #60 getIndexHtmlWithCdn           // 未配置 ASSET_URLS 时零开销直通
  → #52 buildIndexHtml                // 注入 customize / site_title / favicon / main_color
  → htmlResponse                      // 统一 Content-Type + no-cache,删除 content-encoding / content-length
缓存:只缓存最后一环,带 TTL + DB 写入失效(替代 WeakMap<env> 无 TTL 方案)

10. 合并顺序与 checklist

若走路线 A

若走路线 B


11. 待确认

  1. Telegram 投票 / 团队决议结果:customize 走「服务端 transform」还是「前端运行时」?(决定路线 A / B)
  2. EdgeOne 是否接受"页面导航进 Node 云函数"?这是服务端路线在 EdgeOne 上的唯一解法(代价是每次导航一次函数调用)。
  3. feat(assets): inject ASSET_URLS into index.html for direct CDN loading #60 删除 /:folder/:filepath* 302 路由是否可作为 breaking change 合入(是否需要过渡期/文档说明)。
  4. favicon 设置是否必须修(决定是否要给 #671 补运行时替换,或保留 [废弃] fix(spa): 注入站点设置到 index.html,修复自定义头部/CSS/JS 不生效 #52 的这一部分能力)。

附录 A:核查方法与证据

:: 1) 拉取 PR #60 分支并对比
git fetch origin pull/60/head:pr-60 --no-tags
git --no-pager diff --stat origin/main...pr-60
git --no-pager diff origin/main...pr-60 -- src/backend/index.ts wrangler.jsonc scripts/fetch-frontend.mjs
git --no-pager show pr-60:src/backend/server/assets.ts

:: 2) 判断 #52 是否落后 main
git --no-pager merge-base origin/main fix/customize-html-injection   :: 5cac7eb
git --no-pager rev-list --count fix/customize-html-injection..origin/main  :: 7
git --no-pager diff --name-only 5cac7eb origin/main

:: 3) 确认平台路由前提
findstr /S /I "run_worker_first" *            :: 0 命中
:: 生效自检(合并后)
curl -I https://<你的CF域名>/                  :: 期望 cache-control: no-cache, must-revalidate

已确认的关键事实:

事实 结论
origin/mainrun_worker_first #52 在 CF 首页不生效,需 #60 补齐
CF 资源优先 → //index.html 由静态层直出 之前那次 no-cache 修复在 / 上同样可能从未生效
middleware.js + edgeone.json 把导航 rewrite 到静态 index.html #52/#60 在 EdgeOne 上均不生效
public.tssettingsObjcustomize_head/body /api/public/settings 会返回它们,#671 的 getSetting() 有值
settings.tssetSettings() 同步调用 setBackendKind() #671 中 setSettings(data) 之后紧跟的 isTsWorker() 时序正确
前端 .ts/.tsxfavicon 零引用 走 #671 后 favicon 设置仍失效
#60 的 8 个变更文件中不含 cloud-functions/[[default]].js Artifact Guard 失败(已实测)
fix/customize-html-injection 距今落后 7 个 commit 需 rebase + 重建产物

@PIKACHUIM PIKACHUIM reopened this Sep 18, 2026
…mirror support)

原实现总是从 `${ASSET_URLS}/index.html` 取 HTML,于是撞上 npmmirror 的
反滥用规则:registry.npmmirror.com/<pkg>/<ver>/files/ 对所有包的 .html 一律
返回 451 {"error":"blocked"}(js/css/json 正常放行),导致 ASSET_URLS 指向
npmmirror 时注入永远失败。Go 版不受影响是因为 Release 版用内置 index.html,
只有资产走 CDN —— 官方文档原文:"Some NPM CDNs (like npmmirror) may prohibit
access to HTML files, but Release versions don't depend on CDN's index.html,
so they're unaffected"。

改为两条路径,自动选择:

  A. 本地 index.html + CDN 资产(首选,等价 Go Release 行为)
     先 HEAD 探测 CDN 上是否存在本地 HTML 引用的哈希入口资产;存在则直接下发
     本地 HTML 并注入 cdn。不要求 CDN 能返回 HTML,npmmirror 因此可用。
     哈希文件名内容寻址,入口存在即代表整套哈希一致,不会引到 404 上。

  B. CDN 的 index.html(本地构建与 CDN 版本不一致时兜底)
     保持原有行为,HTML 与哈希资产天然同源一致。

  A、B 都不可用 → 不注入,资源回退源站(不会白屏)。

顺带修正上一版文档里的错误结论:npmmirror 并非缺少 CORS —— 它在请求带 Origin
时回显 Access-Control-Allow-Origin(此前用 curl 不带 Origin 测试,误判为不发
CORS 头)。真正不可用的只有 .html 这一项,而新路径已不再依赖它。

测试:assets.test.ts 22 例 + index_cdn.test.ts 10 例全部通过(新增
extractEntryAsset 单测、两条路径的调用顺序断言、以及「探测失败 + CDN 拦截
.html 时降级不注入」);tsc --noEmit 通过;wrangler dry-run 配置校验通过。
@PIKACHUIM

Copy link
Copy Markdown
Member

暂时不再合并#52 等本PR修改完善后合并本PR和前端#671

对齐 Go 版 server/static/static.go:配置 cdn 后 /assets/、/images/、/streamer/、
/static/ 下的请求交给 CDN。Cloudflare 资源优先路由下资源存在时由静态层直出、
Worker 不执行,所以只在源站确实缺失时才触发 —— 此时原先会落到 SPA 兜底,把
index.html 当作 .js/.css 返回,浏览器按 text/html 解析后报错。

- cdnAssetRedirect():仅匹配「目录/具体文件」,目录本身与前端路由不重定向。
- 复用 HTML 那次解析出的 CDN 地址(按 ASSET_URLS 原文缓存),保证
  替换结果与 HTML 指向同一版本;冷启动直接命中资源时退回按 env 解析。
- 同时接在 ASSETS 未命中的分支与 EdgeOne/ESA 无 ASSETS 的兜底分支上。

测试:37/37 通过(新增 4 例:三个静态目录的重定向、前端路由与目录本身不重定向、
 解析、以及集成层面的 302 断言);tsc 与 wrangler dry-run 通过。
@PIKACHUIM

Copy link
Copy Markdown
Member

Wudarensheng and others added 4 commits September 18, 2026 21:51
- SPA 兜底分支不再无条件把 body 读成字符串并固定 status: 200:只有「已配置
  ASSET_URLS + 2xx + GET/HEAD」才改写 HTML,其余情况按「状态码 + headers +
  body 流」原样透传,避免把 307/404 抹成 200 空壳,并保住未配置 CDN 时的
  流式直通。
- spaFallbackHtml 分支同样只在配置了 ASSET_URLS 时才走 CDN 注入。
- injectCdnIntoHtml 对注入值做 JS 字面量转义(单引号、反斜杠、换行),避免
  URL 中含引号时提前闭合内联脚本、令 window.OPENLIST_CONFIG 语法报错、整站
  白屏(Go 版同样未处理,此处不与其缺陷对齐)。
- .env.example / .dev.vars.example 补记移除 /:folder/:filepath* 302 路由这一
  行为变更与迁移说明。
- 新增 4 个测试:引号与反斜杠转义、SPA 兜底流式透传、非 2xx 保留状态码、
  已配置 CDN 时 307 仍透传。
前端产物是内容哈希文件名,而 CDN(jsdelivr / unpkg / npmmirror)提供的正是
npm 包里那一份 dist。原先默认「克隆前端 main 现构建」,产物哈希与 CDN 不一致,
ASSET_URLS 的路径 A(下发本地 index.html + CDN 资产)HEAD 探测必然失败,只能
退化到拉 CDN 的 index.html;而 npmmirror 等镜像禁止访问 .html(451),于是
CDN 完全用不了。

- scripts/fetch-frontend.mjs 默认改为下载 npm 上【已发布】的 dist(版本取
  registry 的 latest,可用 FRONTEND_VERSION 固定);只解出 package/dist 与
  package/package.json,后者供 stampFrontendVersion 读出真实发布版本号,使
  ASSET_URLS 的 $version 正好解析到这份 dist 对应的版本。
- 原「克隆 main 现构建」保留在 FRONTEND_BUILD_FROM_SOURCE=1 之后,供开发联调。
- 下载失败直接报错终止,不静默回退到现构建:那条路的哈希与 CDN 不一致,会让
  路径 A 悄悄失效(npmmirror 直接不可用)。
- 修 Windows 上 tar 把 "C:\..." 盘符冒号当远程主机的问题:以 tmp 为 cwd、
  用相对路径解包。
- .env.example / .dev.vars.example 补记路径 A 对「同一份构建」的要求。

实测(wrangler dev + 真实 CDN):npmmirror 现在能注入并命中路径 A,$version
解析为 4.2.6,注入的入口资产在 CDN 上返回 200 + CORS。
@Wudarensheng

Copy link
Copy Markdown
Member Author

@PIKACHUIM 测试都过了

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

全局设置中的‘自定义内容’和‘自定义头部’配置后不生效

3 participants