Skip to content

feat(config): 配置项与内容统一分离——支持从内容仓库覆盖配置 - #551

Merged
Dawn6666666 merged 5 commits into
LyraVoid:masterfrom
Dawn6666666:feat/issue-549-config-overrides
Aug 11, 2026
Merged

feat(config): 配置项与内容统一分离——支持从内容仓库覆盖配置#551
Dawn6666666 merged 5 commits into
LyraVoid:masterfrom
Dawn6666666:feat/issue-549-config-overrides

Conversation

@Dawn6666666

@Dawn6666666 Dawn6666666 commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

背景

Closes #549

Mizuki 已经有了很完善的内容分离ENABLE_CONTENT_SYNC + sync-content),文章、页面数据、图片都可以放进独立仓库。但 src/config/ 下的配置项仍然必须直接改代码仓库的源文件。

对于 fork 上游、定期跟进版本更新的用户,配置文件是升级冲突的重灾区:上游每次调整配置结构,都会和本地的个人值冲突,需要逐个人工解决。实测参照:一个真实的个人站点相对上游改动了 9 个配置文件、约 630 行,每次合并上游都要在这些文件上处理冲突。

本 PR 把配置也纳入同一套分离机制:src/config/*.ts 保持上游原版,个人值放进内容仓库的 overrides/,构建时深合并。

⚠️ 实验性功能,慎用:该机制目前测试覆盖的场景有限,文档中已明确标注;采用前请备份现有配置,迁移后完整校验。

方案

内容仓库 overrides/  ──sync-content──>  代码仓库 src/config/overrides/
                                                    │
                     src/config/index.ts 在导出前深合并 ↓

              最终配置 = deepMerge(上游默认配置, 同名覆盖文件的 default 导出)
  • 覆盖文件名 = 被覆盖的导出常量名(overrides/siteConfig.ts 覆盖 siteConfig),共 18 个合法名
  • src/config/overrides/ 已加入 .gitignore,不进入代码仓库提交历史
  • 完全可选:不创建 overrides/ 时,import.meta.glob 返回空对象,所有配置等同于上游默认值,行为与现状逐字节一致

合并语义

情况 行为
双方都是普通对象 深合并,覆盖值只影响写到的字段
数组 整体替换,不拼接
标量、null 整体替换
覆盖值里显式写 undefined 的键 跳过,保留默认值
没有对应覆盖文件 原样使用上游默认值

举例:默认 banner.carousel{ enable: true, interval: 3, switchable: true },覆盖里只写 { interval: 8 },结果是 { enable: true, interval: 8, switchable: true }

类型安全

覆盖文件用 satisfies DeepPartial<XxxConfig> 约束。这里必须是 DeepPartial 而不是 issue 里最初设想的 Partial——Partial<T> 只让顶层键可选,而 SiteConfig 的嵌套块字段大多必填(themeColorhue/fixedbanner.carouselenable/interval/switchable),写 carousel: { interval: 8 } 会直接编译报错,恰恰是部分覆盖最常见的写法。

tsconfig.jsoninclude: src/**/* 已经覆盖同步目标目录,所以 pnpm type-check 会检查覆盖文件。三类错误都能在构建期拦住:

键名拼错   → TS2561  Object literal may only specify known properties,
                     but 'titel' does not exist... Did you mean to write 'title'?
类型不符   → TS2322  Type 'string' is not assignable to type 'number'.
联合值越界 → TS2322  Type '"middle"' is not assignable to type '"top" | "center" | "bottom"'.

一键导出:pnpm export-config

已经改了一堆配置的用户不用手抄。该命令以 git 上的上游版本为基准(自动依次尝试 upstream/masterupstream/mainorigin/masterorigin/main,可用 --ref= 指定,--out= 改输出目录),把当前 src/config/ 与那一版逐字段比对,只导出改过的字段到 overrides-export/(已 gitignore):

上游基准:upstream/master (453cc42)
导出目录:overrides-export

  已导出 siteConfig.ts(16 个顶层键)
  已导出 profileConfig.ts(3 个顶层键)
  ...
与上游一致、无需覆盖:sakuraConfig、licenseConfig、markdownConfig、...

三个要点:

  • 写文件前自检 deepMerge(上游默认, 覆盖) === 当前配置,对不上就报出是哪个配置、差在哪,不会产出「看着像但装上不一样」的覆盖
  • navBarConfig.links 里的 LinkPreset 按枚举名序列化(LinkPreset.Home),不是裸数字
  • 深合并表达不了的「上游有、你删掉」的键会单独列出,提示手工处理

改动清单

新增

文件 作用
src/config/deepMerge.ts 合并语义。刻意不含 Vite 专有语法,可直接用 node --experimental-strip-types 单测
src/config/overrideLoader.ts import.meta.glob 加载 + 18 名白名单校验
scripts/export-config.mjs 一键导出命令
scripts/read-site-config.mjs Node 脚本的「覆盖优先、默认回退」取值助手
tests/config-overrides.test.ts 合并语义 7 条
tests/site-config-reader.test.ts 取值回退矩阵 8 条

修改

文件 改动
src/config/index.ts 改为合并适配器,18 个配置在导出前合并;widgetConfigs 聚合合并后的对象
src/types/config.ts 新增 DeepPartial<T>(数组分支短路,避免 string[] 退化成 (string | undefined)[]
scripts/sync-content.js contentMappings 增加一条 overrides → src/config/overrides(复制同步;符号链接会让 Vite 把相对导入解析到内容仓库真实路径导致构建失败,未采用)
.gitignore 忽略 /src/config/overrides//overrides-export/
package.json 增加 export-config 脚本
docs/CONTENT_SEPARATION.md 新增「配置覆盖」章节,含完整迁移步骤
docs/CONTENT_REPOSITORY.md 目录结构与同步映射表补充 overrides/

顺带修正的一致性问题

实现过程中发现 4 处会让覆盖静默失效或读到错值的地方,都在本 PR 内修掉:

1. src/utils/image-utils.ts 绕过合并入口

原来直接 import { siteConfig } from "../config/siteConfig",导致 imageOptimizationformats/quality/noReferrerDomains 覆盖完全不生效。改为走 ../config。改完之后全仓库已无绕过合并入口的直接 import。

2. SITE_LANG 不跟随合并结果

siteConfig.tsconst SITE_LANG 是独立常量,index.ts 原样 re-export。覆盖 siteConfig.langSITE_LANG 仍是上游默认值。改为从合并后的 siteConfig.lang 派生。

3. 番剧数据脚本读不到覆盖

update-anime / update-bangumi / update-bilibili 用正则扒 siteConfig.ts 源码取 anime.modebangumi.userIdbilibili.vmid。这些恰恰是纯个人值,放进 overrides 后不生效,而 update-anime.mjs 就在 build 脚本链的第一环。统一改走 read-site-config.mjs,按「覆盖 → 默认」顺序取值。

4. 惰性正则在部分覆盖文件上会串块(这条是真实 bug)

原来的 /anime:\s*\{[\s\S]*?mode:\s*["']([^"']+)["']/完整的默认配置上没问题,但覆盖文件是部分配置且键序任意。实测复现:

// overrides/siteConfig.ts
anime: {},
font: { mode: "system" },

番剧模式被读成 "system"update-anime.mjs 直接跳过数据更新。改成花括号配平的块内取值,同时把 coverMirror / useWebp 也从全局匹配收敛进 bilibili 块内。

番剧模式的互斥性不受影响:update-anime.mjs 路由 + 子脚本各自自检,两边读同一个值,实测覆盖成 bilibiliupdate-bangumi.mjs 输出 Detected current anime mode is "bilibili", skipping

验证

基础门禁

  • pnpm type-check → 0 error(与本 PR 之前完全一致)
  • pnpm astro check → 322 files,0 errors / 0 warnings / 0 hints
  • pnpm build → 通过
  • 新增 15 条测试全部通过
  • biome ci 干净

机制验证

结果
overrides/ 构建成功,<title> 为上游默认值,与现状一致
部分覆盖 只写 banner.carousel.intervalenable/switchable 保留默认
数组替换 banner.src.desktop 覆盖为 1 张,结果就是 1 张;兄弟键 mobile 的 4 张不受影响
文件名拼错 构建期报错并列出 18 个合法名
漏写 export default 构建期报错并给出正确写法
同步链路 造真实内容目录跑 sync-content,overrides 复制进代码仓库,合并结果正确
取值回退 只覆盖 bilibili.vmid 时,coverMirror/useWebp/bangumi.userId 全部正确回退默认

真实规模端到端验收

拿一个真实个人站点的配置(9 个分歧文件)走完整流程:

  1. pnpm export-config → 自动生成 9 个最小化覆盖文件,自检通过
  2. git checkout <上游ref> -- src/config/ → 配置还原成上游原版
  3. 覆盖文件落到 src/config/overrides/
  4. pnpm type-check0 error
  5. 通过真实的 Astro/Vite 合并路径导出全部 18 个配置,与原始配置逐个深度比对 → 18/18 完全一致
  6. pnpm build → 站点标题、profile 链接、导航链接(与 profile 是不同的 B 站号,证明各配置独立合并)全部正确,默认 banner 残留 0 处

深合并能 100% 表达该站点的个人配置,没有出现「上游有、个人版删掉」这类无法表达的键。

一个能说明机制价值的观察:把基线从旧版本更新到最新上游后,siteConfig 需要覆盖的顶层键从 18 个降到 16 个——上游已经合入的字段自动从覆盖里消失。跟上游跟得越紧,覆盖越小。

迁移步骤

文档里写了完整流程,简述:

# 1. 导出个人配置(基准自动挑选,也可 --ref= 指定)
pnpm export-config

# 2. 放进内容仓库
cp overrides-export/*.ts ../Mizuki-Content/overrides/

# 3. 把代码仓库的配置还原成上游原版(收益来源:之后合并上游不再冲突)
git checkout upstream/master -- src/config/

# 4. 同步并校验
pnpm sync-content && pnpm type-check && pnpm build

内容仓库的 trigger-build.yml 需要把 overrides/** 加进 paths,否则只改配置不会触发重新构建。

回滚:覆盖机制是纯叠加的,删掉 overrides/ 里的文件重新同步即可回到上游默认值;想完全退回旧方式,把个人配置写回 src/config/*.ts,代码不需要任何改动。

已知限制

已写进文档:

  • 深合并表达不了「删除」。只能改值或加字段,没法去掉上游默认里的某个键。pnpm export-config 遇到会明确报出来
  • 评论语言不会自动跟随 siteConfig.langcommentConfig.ts 在模块顶层引用语言常量填充 Twikoo / Giscus 的 lang,覆盖 siteConfig.lang 时需要同时提供 overrides/commentConfig.ts
  • 读取配置请统一走 @/config 入口,直接 import 某个配置文件会绕过合并
  • 开发服务器不监听内容仓库src/config/overrides/ 在每次 dev/build 前从内容仓库复制而来;dev 运行中修改覆盖文件需要重启 pnpm dev 才会重新同步
  • scripts/compress-fonts/ 暂不读取覆盖值,该目录是独立的手动工具,不在 pnpm build 流程内

兼容性

  • 不新增任何环境变量,完全搭载现有 ENABLE_CONTENT_SYNC
  • 不创建 overrides/ 目录时行为与现状完全一致,可按需渐进采用,逐个配置迁移
  • 新增的两个测试文件没有加进 package.jsontest 脚本,跟随现有约定(tests/ 下多数文件也不在默认脚本里)

更新记录(2026-08-11)

  • 已 rebase 到最新 master(含 feat(layout): 优化 2K/4K 超宽屏文章布局、宽内容轨道与 TOC 策略 #548),解决文档命令表冲突
  • overrides/ 同步由符号链接改为复制:覆盖文件是带相对导入的 TS 模块,junction 会被 Vite 解析到内容仓库真实路径,../../types/config 找不到导致 astro build 失败(CI 克隆内容仓库到 ./content 时同样复现),原描述「Vite glob 能穿透」已修正
  • 新增:内容仓库删除 overrides/ 后,同步会清理代码仓库里的旧副本,避免失效配置残留
  • 文档标注配置分离为实验性功能,精简了迁移步骤

Copilot AI lite review requested due to automatic review settings August 11, 2026 01:26

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

配置项此前必须直接改代码仓库的 src/config/*.ts,fork 上游的用户每次合并
上游更新都要在配置文件上解冲突。现在把配置也纳入内容分离机制:

- sync-content 新增 overrides -> src/config/overrides 映射;
- src/config/index.ts 改为合并适配器,导出前把同名覆盖深合并进默认配置,
  各配置文件保持上游原版;
- 合并语义为对象深合并、数组整体替换,覆盖缺失时行为与现状完全一致;
- 覆盖文件用 satisfies DeepPartial<XxxConfig> 约束,字段拼错或类型不符在
  构建期报错。

同时修正三处会绕过合并入口的读取点:SITE_LANG 改为从合并后的 siteConfig
派生,image-utils 改走 @/config 入口,update-anime/bangumi/bilibili 通过
新增的 read-site-config 助手优先读取覆盖值。
- 新增 pnpm export-config:以 git 上的上游版本为基准,把 src/config/ 里
  改过的字段导出成最小化的 overrides/*.ts。导出前自检
  deepMerge(上游默认, 覆盖) 能否还原当前配置,对不上会指出是哪个配置;
  navBarConfig 的 LinkPreset 按枚举名序列化,深合并表达不了的删除键单独报出。
- read-site-config 改为花括号配平的块内取值。此前的惰性正则在部分覆盖文件上
  会串块:overrides 里写 anime:{} 且后面有 font:{mode:"system"} 时,番剧模式
  会被读成 "system" 而跳过数据更新。同时 coverMirror / useWebp 也收敛到
  bilibili 块内,不再全局匹配。
- 覆盖块里没写的字段继续回退默认值,保证「只覆盖 vmid、coverMirror 取默认」
  这类部分覆盖成立;番剧模式仍由 update-anime 路由 + 子脚本自检双重保证互斥。
- 补充 tests/site-config-reader.test.ts 覆盖上述回退矩阵。
- 文档补上完整迁移步骤(导出 → 放进内容仓库 → 还原 src/config → 同步校验 →
  触发部署 → 回滚)。
覆盖文件是带相对导入的 TS 模块,sync-content 建立的 junction 会被 Vite 解析到内容仓库的真实路径,../../types/config 在真实路径下不存在,astro build 直接失败(CI 克隆内容仓库到 ./content 时同样会命中)。改为复制同步:源目录缺失时清理旧副本,避免失效配置残留;文档同步更新。
@Dawn6666666
Dawn6666666 force-pushed the feat/issue-549-config-overrides branch from 0d276b1 to 3d9270a Compare August 11, 2026 02:09
@Dawn6666666
Dawn6666666 merged commit 48bd8a6 into LyraVoid:master Aug 11, 2026
3 checks passed
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.

[Feature]: 配置项与内容统一分离——将配置纳入内容仓库(overrides 机制)

2 participants