Codex Dream Skin 皮肤修复实战:Codex 26.727 兼容与持久双层部署

记录一次 Codex Dream Skin 皮肤在 Codex 26.727 版本更新后的完整修复过程,涵盖问题诊断、源码兼容、持久部署和验证回滚。

背景

Codex Dream Skin 是一个为 OpenAI Codex Windows 客户端定制的皮肤项目,灵感来自《Needy Girl Overdose》中的超天酱(Internet Angel)。它通过 CDP(Chrome DevTools Protocol)注入 CSS 和 JavaScript,为 Codex 界面添加动态背景插画、眨眼动画、广播装饰浮层等效果。

我一直在用这套皮肤,某天电脑重启后发现主页白屏、动画全部消失。排查下来发现是 Codex 自动更新到了 26.727 版本,旧版皮肤的选择器和路由逻辑全部对不上了。这篇文章记录了从发现问题到彻底修复的完整过程,方便以后再次遇到时快速定位。

皮肤的工作原理

皮肤的"动态背景"不是整张 GIF 或视频,而是由两部分组成:

  1. 一张静态 2560 × 1440 背景图 dream-reference.jpg
  2. 渲染器叠加的 CSS/SVG 动画,包括眨眼、广播滚动条、状态模块、心跳和装饰浮层

正常主页效果

"我们该构建什么?"这个标题是 Codex 自身主页的 DOM 文字,不是烘焙在图片里的字体。功能卡、输入框和侧栏也都是 Codex 的真实组件,皮肤只是给它们加了分类和样式。

正确的路由分工应该是这样的:

页面 正确效果
新对话主页 完整超天酱插画、四张功能卡、广播装饰与短暂眨眼动画
普通对话/任务页 深色不透明阅读工作区,背景图不穿透正文
输入框 清晰、接近不透明;任务页不使用高成本实时模糊
左侧栏 保留皮肤色彩,但不能影响原生导航和点击

问题爆发

电脑重启后,我用旧版 Dream Skin 运行库启动,注入器倒是成功连上了渲染进程,但页面验证直接失败了。启动脚本按安全策略回滚,自动重新打开了无皮肤的官方 Codex 界面。

一通排查后,发现是三个独立问题同时存在,叠加在一起才导致全面失效。

问题一:Codex 26.727 更换了主容器标识

旧版皮肤的 CSS 选择器依赖 main.main-surface,但 Codex 26.727 把稳定主容器改成了 main[data-app-shell-main-surface]

皮肤还能注入部分组件——侧栏、边框和功能卡看着是生效的,但主页和任务页的路由分类没有命中新主容器。结果就是主页大面积白屏,或者刷新后失效、背景错误透明。

问题二:正确图片被保存成了 custom 主题

活动图片跟作者原图的字节完全一致,但活动 theme.json 的主题 ID 被写成了 custom。渲染器只为 Internet Angel/Choten 预设启用 .dream-choten-art 类,所以眨眼和作者动态浮层被 CSS 正常隐藏了。

这里有个容易踩的坑:开发者工具里能查到动画名称存在,但这不代表动画真的在显示——隐藏元素也可以拥有 animation-name。必须同时检查主题身份、主页 class、元素 display 和实际几何位置才能确认。

问题三:启动入口和重启生命周期混淆

Windows 上同时存在好几个入口,一开始我搞混了:

图标 用途
Codex Dream Skin 正确的日常启动/重新应用入口
Codex Dream Skin - Tray 托盘主题控制器,不是主应用启动入口
Codex Dream Skin - Restore 恢复官方外观并关闭皮肤会话
官方 Codex 直接启动原版应用;没有皮肤 CDP 会话时会显示原版界面

还有个更隐蔽的坑:如果在 Codex 自己的前台 PowerShell 里执行"关闭 Codex → 安装 → 重启"流程,Codex 关闭时会把它承载的进程一并终止。结果就是只完成了关闭动作,后面的安装和重启都没执行到。解决办法是在关闭之前先启动一个独立的外部协调进程,让它来负责整个重启流程。

症状诊断表

整理了一张诊断表,方便下次遇到问题快速定位:

症状 优先检查 常见根因
主页大面积白色,侧栏和卡片已有皮肤 主容器选择器、主页类 新版 data-app-shell-main-surface 未兼容
主页有图但完全静态 活动主题 ID、.dream-choten-art、眨眼层 display 图片被保存为 custom
普通对话页背景图穿透,文字难读 路由分类、任务页背景、临时全局补丁 把主页壁纸错误应用到整个 body
滚动或输入卡顿 task composer 的 backdrop-filter、全屏 fixed wallpaper 任务页仍在绘制壁纸和实时模糊
闪退后自动打开一个未美化应用 启动日志、状态文件、重复注入器、启动入口 启动验证回滚、前台重启被杀、打开了官方图标

源码兼容修复

选择器兼容

核心修改是把旧版单一选择器扩展为同时兼容新旧两种容器标记:

const SHELL_MAIN_SELECTOR = ":is(main.main-surface, main[data-app-shell-main-surface])";
const APPLICATION_HEADER_SELECTOR = ":is(header.app-header-tint, header[data-app-shell-application-menu-bar])";

这两个常量需要在所有用到主容器的地方统一引用:shell main 发现、Safe CSS 部件注册、主页/任务页路由分类、几何同步、侧边工作区发现、MutationObserver/重渲染后的重新应用。

特别要注意的是不要只改第一个 querySelector——如果 CSS 仍只匹配旧容器,主页还是白;如果路由分类仍只匹配旧容器,任务页还是会透出背景。

CSS 路由约束

全窗口背景图只能在主页存在时绘制:

html.codex-dream-skin.dream-art-wide:has(
  :is(main.main-surface, main[data-app-shell-main-surface]).dream-home-shell
) body {
  background-color: var(--dream-canvas) !important;
  background-image: var(--dream-art) !important;
  background-position: var(--dream-art-position) !important;
  background-size: cover !important;
  background-repeat: no-repeat !important;
  background-attachment: fixed !important;
}

任务页必须重新建立不透明阅读面,并关闭 composer 的实时模糊:

html.codex-dream-skin.dream-art-wide:is(.dream-task-ambient, .dream-task-banner)
  :is(main.main-surface, main[data-app-shell-main-surface]).dream-task:not(.dream-home-shell) {
  background: var(--dream-surface) !important;
}

html.codex-dream-skin.dream-art-wide:is(.dream-task-ambient, .dream-task-banner):has(
  :is(main.main-surface, main[data-app-shell-main-surface]).dream-task:not(.dream-home-shell)
) .composer-surface-chrome {
  background: var(--dream-surface-raised) !important;
  backdrop-filter: none !important;
}

深色不透明任务页

主题身份恢复

活动图片和作者原图的 SHA-256 完全一致时,可以通过项目内置的 Set-DreamSkinActiveTheme 函数原子恢复预设身份。恢复后主题 ID 从 custom 变回 preset-internet-angel-default,眨眼和动态浮层重新生效。

如果哈希不一致,说明图片被替换过(可能是之前手动换的),那就保留当前主题不动,不自动覆盖。

恢复后的主题配置:

{
  "id": "preset-internet-angel-default",
  "appearance": "auto",
  "art": {
    "focusX": 0.72,
    "focusY": 0.42,
    "safeArea": "left",
    "taskMode": "ambient"
  },
  "palette": {
    "accent": "#ff45c8"
  }
}

持久双层修复

第一次修复时我只更新了 engine(实际运行库),当时确实恢复了。但后来运行库被重新安装时,它从旧 payload(安装器持久载荷)退回了修复前的版本,皮肤又坏了。

所以最终方案是"持久双层修复":同时更新 enginepayload 两个位置,确保下次安装器运行时也不会覆盖掉修复。

闭眼预览——证明 SVG 眼皮层存在且位置正确

上面的闭眼图是通过项目已有的 dream-preview-blink 类临时强制显示的,用来证明 SVG 眼皮层确实存在且位置正确。截图后这个类就移除了。日常动画按约 11.4 秒周期运行,真正闭眼只占其中很短的时间,一直盯着也可能错过。

部署流程

我采用了"暂存 → 校验 → 替换"的流程,确保每一步都可回退:

  1. 先把修复文件复制到独立暂存目录
  2. 在暂存目录执行语法检查、JSON 校验、CSS 校验和注入负载检查
  3. 逐个替换 payload 中的对应文件,每个替换后复核 SHA-256
  4. 逐个替换 engine 中的对应文件,同样复核哈希
  5. 确认两层核心文件与修复源完全一致

避免踩坑

这些临时方案不要用:

  • 无条件给 body 添加 background-image: var(--dream-art)——会让任务页也穿透
  • 把所有 task/main/message 背景改成透明——文字根本没法看
  • 依赖 CSS module 生成的随机哈希类——更新后直接失效
  • 只修当前页面、不处理新对话、刷新和路由切换——切个页面就白修了
  • 只改 engine 目录却不改 payload——下次安装器跑完就回退了

验证与结果

修复后的核心文件哈希

修复后的核心文件在源码、engine 和 payload 三处 SHA-256 完全一致,确保没有遗漏:

文件 SHA-256
selectors.json AA1528EF169EDAED0D8DCFAE367452B29196CA6319DEFAD072E55F65936A0B3
renderer-inject.js 3F8A2196194DE3968F18D2E8B3DC346187283EE4B2069CFBADD2BA53603A86D9
dream-skin.css 65707A1CFF52D308C780638C4A5570ABFD1414B45658211100BC5FA9065EBA96

最终验证

修复完成后连续观察了 90 秒,再次验证通过,没有闪退、回滚或重复启动。注入器错误日志为 0 字节。

首页最终验证

对话页最终验证

验证结果 JSON:

{
  "Success": true,
  "CompletedAt": "2026-08-04T02:12:53+08:00",
  "StabilityWindowSeconds": 90,
  "Port": 9335,
  "InjectorPid": 21528,
  "InjectorAlive": true,
  "ThemeId": "preset-internet-angel-default",
  "ErrorLogLength": 0
}

回滚与备份

修复前建立了完整的备份链,确保任何一步出问题都能恢复:

  • 备份目录:%LOCALAPPDATA%\CodexDreamSkin\backups\persistent-dual-repair-20260804-020341-750
  • 独立 ZIP:persistent-dual-repair-20260804-020341-750.zip
  • ZIP SHA-256:6D9B7D3D6F06F58C5612E6E5561FB2F6689965E755EB8B37FD2CAB442A4B4018

开机托盘快捷方式没有永久删除,归档在备份目录的 archived-startup 文件夹里了。任何部署失败都优先从备份恢复。

关键经验总结

  1. 不要假设旧路径仍然成立——换电脑或版本更新后,先做只读环境发现,确认当前路径、版本和状态,不要照抄旧路径
  2. 选择器兼容要全覆盖——不是只改第一个 querySelector 就完事,CSS、路由分类、几何同步全都要同步更新
  3. 动画名称存在 ≠ 动画可见——隐藏元素也可以拥有 animation-name,必须检查 display 和实际几何
  4. 双层部署避免回退——只改运行库不够,安装器载荷也要同步,否则下次安装直接打回原形
  5. 重启必须由独立进程协调——在 Codex 前台进程里重启 Codex,会把它自己一起杀掉
  6. 每一步都留下证据——执行了什么、成功判据是什么、失败了从哪里回滚,不要只记个"应该好了"

项目信息