Conversation
根因:TSWorker 缺少服务端 HTML 占位符替换环节。前端产物 dist/index.html 里保留了 字面量占位符,需要服务端做替换;上游 Go 版在 static.go 的 UpdateIndex() 里完成了 这件事,而 TSWorker 是原样透传给浏览器,占位符被当成注释忽略。对前端主 bundle 全文扫描,customize 出现 0 次,说明前端也没有运行时注入实现,因此整条链路缺环。 缺失的占位符(对应设置键): - <!-- 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 改动: - 新增 server/index-html.ts:applyIndexHtmlSettings() 按 Go 版语义替换上述占位符; buildIndexHtml() 带 WeakMap 缓存,避免兜底热路径每请求读 DB - index.ts:ASSETS 首页、ASSETS 深链 fallback、内联 SPA 壳三条路径统一接入注入, 并把 Content-Type / Cache-Control 收敛到 indexHtmlResponse() - db.ts:新增 onDbWrite() 写入通知钩子(用注册回调避免与 index-html.ts 形成循环依赖), 在 saveDb() 内触发缓存失效,保证改完设置立即生效 - 新增 10 个测试(8 单测 + 2 集成,集成测试直接打真实 Hono app) 顺带修正: - 用 split().join() 替代 String.replace(),避免自定义 JS 中的 $& / $1 被当作替换模式展开 - main_color 补引号与反斜杠转义,避免内联脚本语法错误导致白屏 - SPA fallback 分支此前未设置 Content-Type 安全说明:customize_head / customize_body 是管理员配置的任意 HTML/JS,注入后可在页面 执行脚本,这是上游 OpenList 的既有设计(管理员=可信主体)。本改动未放松任何鉴权。
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
openlist-work | 782505f | Sep 14 2026, 06:31 AM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
openlist-tsworkers | 782505f | Sep 14 2026, 06:31 AM |
pikachuren
left a comment
There was a problem hiding this comment.
🙏 感谢 @PIKACHUIM 提交!
🤖 AI 自动审核声明:本评审报告由 AI 自动生成,当前使用 Claude Opus 5 模型进行分析。
🎯 结论
✅ Approve — 关键功能修复完善,与 Go 版语义对齐,安全说明清晰,测试覆盖充分
📖 概要
fix(spa): 修复系统全局设置中的自定义头部/CSS/JS 片段不生效 · 补齐缺失的服务端 HTML 注入环节
核心改动:新增 index-html.ts 实现占位符替换、接入 index.ts 两条返回路径、添加 DB 写入通知钩子
🧭 整体方案
新增 applyIndexHtmlSettings() 与 Go 版 UpdateIndex() 语义对齐,使用 split/join 避免特殊模式误解;在 index.ts 的 ASSETS 直出与 SPA fallback 两条路径接入注入;引入 onDbWrite() 钩子在 saveDb() 时失效缓存,保证设置改动立刻生效。方案完善,解决了「设置保存成功但不生效」的困惑用户体验。
📊 变更统计
4 个文件(+491 / -25 行) | 功能 ⭐⭐⭐⭐⭐ | 最小改动 ⭐⭐⭐⭐ | 前向兼容 ⭐⭐⭐⭐⭐ | 方案设计 ⭐⭐⭐⭐⭐
🚨 关键问题
无重大问题
📂 逐文件分析
src/backend/server/index-html.ts(新增,核心实现)
改动意图:实现与 Go 版 UpdateIndex() 一致的服务端 HTML 注入
代码逻辑:
applyIndexHtmlSettings()逐项替换 6 个占位符(favicon/logo/site_title/main_color/customize_head/customize_body)buildIndexHtml()按 env 缓存注入结果,失败降级返回原始 HTMLinvalidateIndexHtmlCache()清除 env 对应的缓存条目- 模块顶层调用
onDbWrite()挂接 saveDb() 的写入通知
问题分析:✅ 设计精准、细节完善:
- ✅ 用 split/join 避免
$&/$1被当特殊模式解释(管理员自定义 JS 可能包含这些) - ✅ main_color 做引号/反斜杠转义防止内联脚本语法错误
- ✅ logo 取第一行与 Go 版 strings.Split()[0] 对齐
- ✅ 空值跳过替换避免清空占位符(favicon/logo 裂开、标题变空)
- ✅ WeakMap 缓存按 env 隔离,避免多 isolate 串扰
- ✅ 失败降级到原始 HTML(不 500,页面可用)
- ✅ 模块顶层挂钩保证 saveDb() 前缓存已就位
安全说明:✅ 合理透彻:
- customize_head/customize_body 是管理员后台配置,属「管理员=可信主体」模型
- 仅通过 /admin/setting/save(需 admin token)才能写入
- 不额外转义避免破坏自定义 CSS/JS 功能
- 符合 Go 版既有行为,无新增风险
src/backend/server/index-html.test.ts(新增测试)
改动意图:锁定占位符替换行为
代码逻辑:
- 单测 8 条:customize_head/body 注入、favicon/logo/title/mainColor 对齐、特殊字符处理、引号转义、缓存命中/失效
- 集成测试 2 条:EdgeOne 无 ASSETS 内联壳注入、Cloudflare 有 ASSETS 首页+深链都注入
问题分析:
- ✅ 单测覆盖替换逻辑的正确性
- ✅ $&/$1 特殊模式用例是真实的回归防护(修复前会失败)
- ✅ 集成测试直接打真实 app 验证路由确实调用了注入(本次 bug 的根本)
- ✅ 两条集成测试覆盖三个部署目标(CF/EdgeOne/ESA)
src/backend/index.ts
改动意图:接入占位符替换、补齐缺失的 Content-Type
代码逻辑:
- 新增
readIndexHtmlTemplate()从 ASSETS 读取模板 - 新增
indexHtmlResponse()统一 HTML 响应(Content-Type + Cache-Control) /或/index.html路径:读模板 → 注入 → 响应- SPA fallback 路径:同样先注入再响应
- 内联 SPA 壳:通过 buildIndexHtml() 一并处理
问题分析:
- ✅ 三条返回路径(ASSETS 首页、ASSETS fallback、内联壳)都接入注入
- ✅ Content-Type 补齐避免浏览器错误解析
- ✅ Cache-Control 保留 no-cache 防止版本切换时旧 HTML 缓存
- ✅ 失败降级到原始 ASSETS 直出,不影响可用性
- ✅ readIndexHtmlTemplate() 失败返回 null 而非抛错,体现 fail-safe 原则
src/backend/internal/model/db.ts
改动意图:新增 DB 写入通知钩子
代码逻辑:
onDbWrite()注册监听器(供 index-html.ts 挂接)notifyDbWrite()触发所有监听器(在 saveDb() 内存更新后调用)- 监听器异常不影响写入本身的返回值
问题分析:
- ✅ 用注册钩子而非直接 import 避免循环依赖(index-html.ts 需要 getDb())
- ✅ 反向暴露注册点保持依赖单向性
- ✅ 在「内存已更新、落盘之前」触发失效确保一致性
- ✅ 异常捕获避免监听器失败拖累业务逻辑
✅ 待处理清单
无待处理项
🎯 结论:✅ Approve — 功能修复完善、与 Go 版对齐、安全设计清晰、测试全面、可直接合并
fix(spa): 修复系统全局设置中的自定义头部 / CSS / JS 片段不生效
摘要
系统设置里的 自定义头部(customize_head)、自定义内容(customize_body) —— 也就是用户用来注入自定义 CSS 与 JS 片段的地方 —— 在 TSWorker 后端上完全不生效。本 PR 补齐缺失的服务端 HTML 注入环节,并顺带对齐 favicon / logo / 站点标题 / 主题色这四项同源设置。
分支
fix/customize-html-injectionmainOpenList-TSWorker问题
复现
<style>body{background:#000}</style>,或「自定义内容」填入<script>console.log('hi')</script>hi根因
前端产物
dist/index.html里保留了字面量占位符,等待服务端做字符串替换:上游 Go 版在
server/static/static.go的UpdateIndex()里完成了这层替换。TSWorker 完全没有这个环节:src/backend/index.ts(修复前)对 HTML 入口是直接透传的:因此两条返回路径都把带占位符的 HTML 交给了浏览器。浏览器把
<!-- customize body -->当注释忽略 —— 表现为「设置保存成功但页面无任何变化」。补充证据:对前端主 bundle
dist/assets/index-DWvZbU_h.js(1.35 MB)全文扫描,customize出现 0 次,insertAdjacentHTML0 次。前端也没有运行时注入实现,所以这不是「前端少个 effect」,而是整条链路缺环。影响范围
customize_headcustomize_bodysite_titleLoading...)faviconlogomain_color方案
新增
src/backend/server/index-html.ts,实现与 Go 版UpdateIndex()语义一致的服务端替换,并在index.ts的两条 HTML 返回路径上接入。关键设计
1. 与 Go 版语义严格对齐,避免两套后端行为分叉:
<!-- customize head -->customize_head<!-- customize body -->customize_bodyhttps://res.oplist.org/logo/logo.svgfaviconhttps://res.oplist.org/logo/logo.pnglogostrings.Split(...)[0])<title>Loading...</title>site_titlemain_color: undefinedmain_colormain_color: '<value>'2. 用
split().join()而非String.replace()replace会把替换串里的$&、$1、$'当特殊模式展开。管理员的自定义 JS 里出现$&完全可能(压缩器产物常见),会导致注入结果被静默破坏。已加测试锁定。3.
main_color做引号/反斜杠转义Go 版是
fmt.Sprintf("main_color: '%s'"),值里含'会直接让内联脚本语法错误、整站白屏。这里补上转义(不与 Go 的这个缺陷对齐)。4. 缓存 + 写入即失效
app.all("*")是整站兜底路由,每次 SPA 导航都会命中;每请求读 DB + 6 组字符串替换在 ESA / EdgeOne 上代价过高。因此:WeakMap<env, string>缓存注入结果(与db.ts既有缓存策略一致,按 env 隔离避免多 isolate 串扰)db.ts新增onDbWrite()写入通知钩子,saveDb()触发 →index-html.ts注册的监听器立刻清缓存之所以用「注册回调」而不是直接
import:index-html.ts需要getDb(),而getDb()定义在db.ts,直接互相 import 会形成循环依赖——在 EdgeOne/ESA 的 CJS 产物里容易产生「模块只初始化了一半」的难查运行时问题。反向暴露注册点可保持依赖单向。把失效挂在
saveDb()上,是因为所有设置写入(/admin/setting/save、/admin/setting/default、/admin/setting/delete、updateSettingValue等)最终都汇聚到这里,覆盖全部路径且不会漏掉未来新增的写入入口。5. 失败降级,绝不 500
取模板失败、读 DB 失败都回退到「原始未注入的 HTML」。HTML 入口 500 会让用户连错误页都看不到,代价远大于设置未生效。
覆盖面
修复同时覆盖三个部署目标(都经由
app.all("*")这一处):env.ASSETS+/或/index.htmlenv.ASSETS+ SPA fallback/login、/@manage/*)spaFallbackHtmlapi/_makers.ts)、阿里云 ESA(esa-entry.ts)变更清单
src/backend/server/index-html.tsapplyIndexHtmlSettings()、buildIndexHtml()、缓存与失效src/backend/server/index-html.test.tssrc/backend/index.tsbuildIndexHtml();新增readIndexHtmlTemplate()/indexHtmlResponse();SPA fallback 补Content-Typesrc/backend/internal/model/db.tsonDbWrite()/notifyDbWrite(),在saveDb()内触发前端(
OpenList-Frontend)无需改动(对齐 Go 版:纯服务端注入)。安全说明
customize_head/customize_body是管理员配置的任意 HTML/JS,注入后必然可在页面执行脚本。这是上游 OpenList 的既有设计(Go 版同样如此),属于「管理员 = 可信主体」模型。本 PR 不放松任何鉴权:只有已通过管理员接口(需 admin token)写入的值才会出现在这里。请勿在后续评审中把它当作 XSS 漏洞要求转义——那会直接破坏自定义 CSS/JS 功能本身。
测试
新增用例(10/10 通过)
单测:
customize_head/customize_body片段被注入到正确位置(head / body 各自就位)$&/$1不被当作替换模式解释main_color引号 / 反斜杠转义buildIndexHtml缓存命中与saveDb失效集成(直接打真实 Hono app,锁死「路由确实调用了注入」):
9. EdgeOne(无 ASSETS)内联 SPA 壳注入
10. Cloudflare(有 ASSETS)首页 + 深链刷新都注入
验证结果
test:server:45 pass / 3 fail —— 这 3 个失败已在干净的main上复现,为既有问题,与本次改动无关(Security(F-11)×2、CAS codec×1)npm run lint(tsc --noEmit):通过npm run build:edge:通过;已确认注入逻辑进入cloud-functions/[[default]].js与dist-server/api/[...route].js部署须知
改动源码后必须重新构建,否则线上仍是旧代码:
生成
cloud-functions/[[default]].js与dist-server/api/[...route].js。自查清单
tsc --noEmit通过前端改动(不需要)