Skip to content

refactor: 架构整理 —— 模块化客户端、零依赖构建、去掉祖先 :has()(流式样式重算 −94%)、工具链收拢 (0.6.1) - #3

Merged
rinDBeans merged 11 commits into
rinDBeans:mainfrom
wushi2333:refactor/architecture
Sep 29, 2026
Merged

rinDBeans merged 11 commits into
rinDBeans:mainfrom
wushi2333:refactor/architecture

Conversation

@wushi2333

Copy link
Copy Markdown
Contributor

概要

架构整理(0.6.0 → 0.6.1):功能不变,流式输出时明显更快,工程脚本收拢成三个入口,安装改走 DSH 标准的 dsh plugin add link:。

外观不变的证据:在真 dsh web(0.1.7-rc.2)上把 20 个界面状态(亮 / 暗、弹层开合、右栏收起、侧栏折叠、宿主设置页、组合包卡、关掉 B 面后的宿主菜单等)逐元素存下全部计算样式,与 1cbd6e8 逐项比对:除下面「修正」一节那一处有意的变化外,零差异(另有 4 个新令牌出现在 html / body 上,本身不影响任何元素)。仓库自带的 scripts/live/parity.mjs 复跑 14 个状态,结论相同。

性能

去掉落在祖先位置的 :has()(侧栏对齐 9 条、侧栏渐隐 2 条、模型选择器席位 1 条;输入区 hero 那条改成固定深度的子代 :has())。祖先位置的 :has() 让会话区每插入一个带 data-slot 的节点,Chromium 都要把受影响祖先下的整片子树重配一遍样式。新锚点的特异性与旧写法逐条相同,与宿主规则的先后关系不变。

模拟流式输出(200 帧,每帧 30 个 span + 1 个 data-slot 节点),5 次取中位:

形态 样式重算 整段耗时 脚本
web 4264 → 267 ms 6.3 → 2.3 s 47.5 → 10.1 ms
Windows 桌面壳(标题栏形态) 9518 → 442 ms 11.7 → 2.5 s 59.2 → 12.5 ms

桌面壳形态下皮肤相对裸宿主的额外重算已在噪声以内。脚本侧:模型选择器的 MutationObserver 只在属性变化、席位增删时全量扫描,其余只重同步被触及的席位。

修正(唯一的可见差异,单独一个提交)

深色下 html 底色从未生效:skin.css 的 :root:has(body[data-ds-dark-theme]) 被作用域化成了 html[data-codex-ui] :root:has(…)(后代选择器,永远匹配不到),深色时 html 一直是 #fff,平时被 body 盖住。作用域器现在把以 :root 开头的复合选择器映射到根本身,深色 html 底色 #fff → #111111(README「设置页」一节本来就是这么写的)。在 249e6d1,不想要可以单独 revert。

结构

  • 浏览器半拆成 src/client/ 下的 ES 模块:index.js(inject 与 apply)、stylesheet.js、settings.js、theme-preview.js、settings-card.js、override.js、model-picker/{index,component,view}.js、constants.js、host.js。加功能 = 一个模块导出 installXxx(ctx) + apply 里一行。
  • scripts/build.mjs 是唯一的构建:零依赖打包器(相对导入 → 解构,宿主包 → loader 的 require,样式作为虚拟模块内联),不认识的 import / export 写法直接报错;CSS 作用域器认得注释与字符串。
  • 删掉走不到的代码(设置卡摘要分支、无 primitives 降级、主题回退表及对应样式);重复的阴影与底色收成 4 个令牌,八份 CSS 2491 → 约 1820 行。
  • 模型选择器席位改为给席位出口打 data-codex-ui-seated(不在宿主子节点上,React 换掉子节点不会丢;夹具「宿主换掉自己的子节点后仍隐藏」照常通过)。

工程

  • 脚本 25 个文件 / 4886 行 → 15 个 / 3356 行,三个入口,公共部分在 scripts/lib/:
    • scripts/check.mjs:原 check-repo + audit-codex-ink,60 项,CI 入口;新增 client.js 的 DSH 插件契约检查(隔离执行:loader id = 包名、inject 恰为三个必需服务、内嵌样式与 theme.css 一致)与 peerDependencies 检查。
    • scripts/verify.mjs + scripts/specs/:原 8 个 *-verify.mjs 夹具,断言名与项数不变,共 195 项。
    • scripts/live/:gui.mjs、settings.mjs(并入 theme-flash-probe),新增 parity.mjs(改动前后逐元素计算样式对账)。
  • 安装改走 dsh plugin --profile web add link:<仓库绝对路径>(桌面端:插件 → 添加插件 → 填路径);删掉 install-plugin / make-verify-profile / pack-host-asar / make-preview 与 scripts/fixtures/;package.json 在 peerDependencies 声明 @deepseek-ai/schemastery。已用旧安装器装过的:先删掉 vendor/codex-ui 副本、node_modules/codex-ui junction 与注册条目,见 README「安装」。
  • 夹具修正:旧 hero / composer-shadow 夹具把宿主会话根样式在第一个 content:"" 处截断了(6717 字符只读到 2672);0.6.0 上已跑不通的 A 面菜单与右栏夹具修好;夹具按源码注释剪段时拒绝不唯一的标记(旧写法会静默剪错)。
  • 删掉 docs/plan-settings-page.zh-CN.md 与 README 不再引用的 18 张验收截图(参考图一张不动)。

验收

命令 结果
node scripts/check.mjs 60/60
node scripts/verify.mjs 195/195(npm node_modules 与 app.asar 两种宿主各一遍)
dsh plugin --profile … add link: 标准安装后 live/gui.mjs 14/14,启动无激活告警
同上 live/settings.mjs 30/30
live/parity.mjs 对 1cbd6e8 14 个状态,忽略 4 个新令牌后只剩 5 个深色状态的 html 底色(上面的修正)

没做的(会改变行为,留给维护者决定)

  • 模型位改成注册 slot 接管,而不是往宿主席位里追加 DOM;
  • configForms / theme 改为可选服务(缺了也能激活,但设置卡不可用);
  • 宿主半在首帧前注入样式,消除加载时的闪烁;
  • 设置卡不再依赖宿主 primitives、文案改走宿主 locale 服务;
  • settings.configure({auto:false})、immediately:true 与去掉 package.json 的 dsh.client.inject 等时序 / 清单调整。

wushi2333 and others added 11 commits September 28, 2026 19:15
构建
- 构建移到 scripts/build.mjs(CLI + 库):src/client/ 下是真 ES 模块,零依赖打包器按依赖顺序
  包成 IIFE,相对导入变解构、包导入变 require()(求值时机与原来相同),导出名自动收集 ——
  去掉 4 个占位符、stripExports 与两份手工同步的导出名单
- 样式作用域化改为跳过注释与字符串的分词器,产物去注释(theme.css 92.8k → 52.4k 字符);
  「注释里不许写花括号」的限制随之消失

客户端(src/client/)
- index.js(inject + apply)/ stylesheet / settings(覆盖层 + 配置卡座位)/ theme-preview /
  settings-card / override / model-picker/{index, component, view} / constants / host
- 删掉宿主保证过的防御分支:require 的 try/catch、typeof ctx.effect/inject/on、configForms 缺席、
  getTheme 回落、卡片 view==='summary' 分支(宿主只以 view:'page' 渲染该座位)、
  matchMedia / MutationObserver 探测、未用的 refresh/isActive 与 document/window 注入口
- 语言判断、「设置文档未到则保持现状」合并为 host.js 的 isEnglish / formValue
- 性能:模型选择器的 MutationObserver 只在「新增/移除含席位的元素、会话标记变化、席位自身子节点变化」
  时对账(流式输出不再每段全量扫描);未落定的席位仍在任何变动时重试;覆盖层 CSS 未变不重写

验收(行为不变)
- 真实 dsh web 0.1.7-rc.2 上 14 个界面状态的逐元素计算样式快照(含伪元素与 html/body 全部令牌)
  与重构前逐项比对:0 处差异
- check-repo 改为在 node:vm 里执行 client.js,直接断言 DSH 契约(id、inject、apply)
- power-rail-verify 47/47 · live-gui-probe 14/14 · settings-page-verify 29/29 · elevation 19 ·
  composer-shadow 23 · hero 25 · sidebar-align 6 · sidebar-surface 13 · audit 36/36
- model-picker-verify / rightbar-verify 靠 theme.css 里的注释分节定位,下一个提交随工具链一起改
- 注释只留取值依据与非显然的约束,历史挪回 CHANGELOG:skins/ 2491 行 116 KB → 1822 行 65 KB
- 明暗两档合进令牌层:--dsw-codex-ambient / -menu-shadow / -suggest-shadow / -suggest-fill,
  窗口与右栏影、宿主模型菜单、模型选择器弹层、指令建议菜单不再各写一条深色规则(删 5 条、11 个选择器)
- 删逐字重复:深色 markdown 标题令牌副本、深色 elevation 的 stroke/panel 副本(与亮色同式,只需换颜色令牌)、
  输入卡 @supports 里的重复圆角、⑰ 相邻两条同选择器规则合并
- 删死规则:[data-codex-filebar](没有任何人打这个标记)、.cx-row--stack(卡片不用)、radius 的永不生效的回落值
- 选择器提速:侧栏对齐 9 条后代 :has() 改为固定深度子代 :has()(rc.1 / rc.2 渲染代码与真 DOM 实测深度一致)——
  不再让侧栏以上每层 div 扫整棵子树、任何带 data-slot 的插入都重判;hero 席位的 :has() 同理(实测深度 3);
  底栏 :is(button, select) 拆成两条类型选择器(特异性不变,可按标签名分桶)
- 顶栏 header:has() 不动:需要带页签的活跃会话才能验证,本轮取证状态里没有

验收:静态逐条规则比对只含上述改动;真 dsh web 18 个界面状态(新增宿主菜单深色、指令建议菜单亮/暗)
逐元素计算样式与 1cbd6e8 比对 0 差异(唯一差别是 html/body 上多出 4 个新令牌名);
check-repo · audit 36/36 · elevation 19 · composer-shadow 23 · power-rail 47 · hero 25 · sidebar-align 6 · sidebar-surface 13
skin.css 的「画布自持」规则 :root:has(body[data-ds-dark-theme]) 被作用域化成
html[data-codex-ui] :root:has(…)(根的后代里找根,永远不匹配),深色下 html 一直是 #fff,
复刻规格 §4.2 要求的「html 那一份用 :has() 跟上」自 0.2.0 起就没生效过。
作用域器现在把以 :root 开头的选择器整体映射到根:→ html[data-codex-ui]:has(…)。

这是本轮重构里唯一有意的可见变化(单独成提交,可单独回退):真 dsh web 18 个状态比对,
差异恰好 8 处 —— 每个深色状态的 html background-color #fff → #111111,其余全部一致。
真 dsh web 上做流式式 DOM 插入基准(400 帧,每帧插 30 个 span + 1 个 data-slot 出口,强制样式与布局):
Chromium 的 RecalcStyleDuration 原来 4264ms,关掉本皮肤只有 184ms —— 皮肤让每帧样式重算贵了 ~25 倍。
逐文件、逐规则二分定位到两类规则::has() 落在祖先位置、且目标是常见元素(span / *)。会话区每插入一个带
data-slot 的节点,Chromium 都得把受影响祖先下的全部候选重新匹配。

- 模型选择器席位:[data-slot=…]:has(> .codex-mp-trigger) > :not(…) 改为组件在放入触发器的同一步给席位出口
  打 data-codex-ui-seated、撤走时摘掉(宿主换掉自己的子节点不影响标记;特异性不变 0,4,1)
- 侧栏对齐 9 条:div:has(> * > 工作区出口):not(:has(… 折叠徽标)) 改为 [data-slot=sidebar] > div:not([class*=_collapsed])
  —— rc.1 / rc.2 渲染代码里 _collapsed 类与折叠徽标是同一个条件(!wide);特异性不变
- 侧栏渐隐:div:has(> 工作区出口) … 改为 div > 工作区出口 …(特异性不变)
- check-repo:model-picker.css 里不许再出现 :has()

结果(中位数,各 5 次):RecalcStyle 4264 → 267ms(-94%),整段耗时 6303 → 2302ms(-63%),脚本 47.5 → 10.1ms;
皮肤相对裸宿主的额外重算从 ~10ms/帧降到 ~0.2ms/帧。
验收:真 dsh web 20 个界面状态(新增左侧栏折叠亮/暗)逐元素比对 1cbd6e8,差异只有 9 个深色状态的 html 底色
(上一个提交的有意修复);check-repo · power-rail-verify 47/47
插件本体(skins/、src/、index.js、client.js、theme.css、cordis.patch.yml)零改动。

- lib/host.mjs:宿主路径(DSH_ASAR / DSH_GLOBAL_MODULES / DSH_CHROME → host.local.json → 扫描)与
  唯一的宿主读取入口 openHost():app.asar 与含 @deepseek-ai/* 的 node_modules 读法一致,
  pack-host-asar.mjs 随之删除;cssFor / cssAfter / mapFor / themeLayers / 字符串字面量反转义、
  前端主样式表按 index-*.css 模式找,dockkit 哈希从类名现认
- lib/cdp.mjs:--remote-debugging-port=0 + DevToolsActivePort 取端口(不占任何固定端口)、一次性 profile、
  evaluate 收真函数、鼠标 / 键盘 / 媒体、截图(后台页签先提前台)、PNG 解码、token 登录、关首启弹层、按文字点、
  命令超时与退出清理
- lib/checks.mjs:统一的 PASS / FAIL 输出与退出码
- verify.mjs + specs/:composer(composer-shadow + hero)、elevation、model-picker(host-menu A 面 + power-rail B 面)、
  rightbar、sidebar(align + surface);8 个小节 195 项,断言名与每个旧脚本的条数不变;
  截图默认进系统临时目录,--shots 才写别处;--host 指定宿主
- model-picker A 面与 rightbar 的 before 页改为按源文件注释标记剪段再走 scopeCss(stripComments()),修好产物去注释后的断链
- composer-shadow / hero 旧读法在第一个 content:"" 处截断宿主样式(ConversationRoot 只读到 2672/6717 字符);
  改用统一读法后宿主 .scrollBody 的 overflow 生效,composer-shadow 夹具把留白挪进滚动容器(与真 GUI 同构),读数不变
- check-repo.mjs → check.mjs,并入 audit-codex-ink 的 skin.json 结构自检、36 组 WCAG、彩色白名单(60 项);
  package.json 清单加一条:index.js 顶层 import 的包必须在 peerDependencies 里
- live/gui.mjs、live/settings.mjs(新增一条:主题切换亮→暗→亮逐帧无中间态,theme-flash-probe 并入后删除)、
  live/parity.mjs(snap / diff,自己起浏览器);settings 收工时把覆盖全部重置
- 安装走 DSH 标准路径:删 install-plugin / make-verify-profile / make-preview / fixtures/preview.html;
  package.json 加 peerDependencies @deepseek-ai/schemastery ~3.18.4(link: 安装时宿主半才能 import),
  去掉 install:web / install:desktop,加 verify
- CI 只跑 check.mjs(已含产物同源比对,build.mjs --check 那一步多余);.gitignore 去掉两条死规则;
  删截断的 docs/plan-settings-page.zh-CN.md

验收:check 60/60;verify 195/195(npm node_modules 与打包 asar 两种宿主各一遍);
dsh plugin --profile <p> add link:<仓库> 装进新 profile 起 dsh web:live/gui B 面 14/14、A 面 10/10,
live/settings 30/30,parity 同版本两拍 0 差异。
合并时的对齐:
- check.mjs 带上 fb91dad 的规则:model-picker.css 里 :has() 必须为 0。
- 夹具 before 页按源码注释剪段,注释瘦身后标记失效:⑫ / ⑯ / window-shadow 右栏段换成现在的注释;
  verify.mjs 的终点标记改为从起点之后找,并拒绝不唯一的起点标记(⑫⑬⑯⑰ 也出现在文件头目录里,
  旧写法会静默剪错或什么都不剪)。
- sidebar align 夹具补上宿主真实的 sidebar 席位出口(div[data-slot=sidebar] display:contents,
  实测真 GUI 就是这一层),与 fb91dad 的无 :has 锚点对上。
- live/parity.mjs 的 --ignore 也作用于 html / body 自定义属性(新增令牌时要用)。

验收:check 60/60;verify 195/195(npm node_modules 与 app.asar 两种宿主各一遍);
live/gui 14/14、live/settings 30/30;live/parity 对 1cbd6e8 的 14 个状态:忽略 4 个新令牌后
只剩 5 个深色状态的 html 底色(249e6d1 的有意修复)。
- README(中英):安装走标准 link 安装(含试用 profile、桌面端入口与从 0.6.0 迁移);新增「开发」一节
  (构建、加功能的方式、inject 与 ctx.inject 的分工);目录、验收(check / verify 分 spec / live 三件)、
  宿主与浏览器、边界(席位标记、类名锚点计数、:has 纪律)、CI 全部对齐现状。
- skins/codex-ink/README、docs/ 两份计划、样式与 spec 注释里的旧脚本名换成新入口。
- 版本 0.6.1(client.js 只有头注释的版本号变化)。
夹具截图现在默认写到系统临时目录(verify.mjs --shots 可指回 assets/screenshots 重新生成);
这些旧产出没有任何文档引用。参考图(assets/reference/)一张不动。
0.6.3 —— 输入卡与窗口边框阴影改为按实测像素拟合,不再照抄源码 token
(源码 --elevation-composer 的 0 4px 80px 8px 远场在参考图里量不到;环的 1px
在 DPR 2 下渲成 2 个设备像素,而参考图里只有 1 个):
  · composer.css:亮色收到两层 0 0 0 0.5px @10% + 0 2px 12px @9%,
    删掉窄屏(max-width 639px)的 80->40px 远场覆盖(已无远场可收)
  · window-shadow.css:中列环境影 0 0 24px @5% -> 0 0 13px @7%
    (同一夹具里 SSE 最小的拟合,47 -> 22);发丝线不动
  · composer-shadow-verify:4 条旧判据按新口径重写,21 项
  · rightbar-verify:2 条还钉着拟合前 24px 的判据改写为实测 13px,42 项

0.6.4 —— 亮暗两套侧栏换基准。Codex 没有「侧栏色」令牌:那一面是
--color-surface-tertiary 以 70% 叠在窗口底上的半透明遮罩(app-shared CSS 原文
记在 skin.css 注释里),因此渲染值随窗口背后的底而变:
  · 亮 侧栏 #eef4f9 -> #f6f6f6。旧值是某次蓝底下的读数,偏差 R-8 / G-2 / B+3,
    于是「侧栏 -> 主区」那一档读起来是色相变化而不是灰阶变化
  · 暗 侧栏 #181818 -> #0f0f0f。旧值取的是 jdi.dark.surface,那是卡片坐的表面,
    结果侧栏比窗口背景(#111111)还亮 7 级,层级方向是反的
  · 选中行 / 悬停随之:亮 #e9e9e9 / #f0f0f0,暗 #1f1f1f / #171717
  · 暗色表面链不动:--dsw-composer-surface 仍 #181818,输入卡实测仍 35,与 Codex 同值
  · 新增 scripts/sidebar-color-verify.mjs(16 项):六个面同页并排,靠切
    body[data-ds-dark-theme] 亮暗各截一次,每套主题一组反例对照

两套主题里侧栏都比主区低一档(亮 255 -> 246、暗 17 -> 15),与 Codex 同值。
refactor: 架构整理 —— 模块化客户端、零依赖构建、去掉祖先 :has()(流式样式重算 -94%)、工具链收拢 (0.6.1)

main 侧 0.6.2-0.6.4 的改动已一并移植进新结构:皮肤像素值取 main,结构取本 PR。
被本 PR 删除的旧脚本不复活,其判据逐条搬进 scripts/check.mjs 与 scripts/specs/。
model-picker 夹具的 locale 传 null,组件的 isEnglish() 于是退回 navigator.language;
headless Chromium 的 navigator.language 随机器/进程环境走(本机实测 en-US,
Emulation.setLocaleOverride 也改不动它),「两端刻度名 更快 / 更强」这条断言因此
会随机变红(同一份代码,前几次跑 PASS、之后连续 3 次 FAIL)。

改成显式给一个只有 getSnapshot 的 locale 快照(active='zh-CN'):语言钉死,
且「宿主字典缺席 → 落本表」这条路径照旧(没有 bind 方法,hostT 仍为 null)。
连跑 3 次均 82/82。
@rinDBeans
rinDBeans merged commit ef82bad into rinDBeans:main Sep 29, 2026
4 checks passed
rinDBeans pushed a commit to ase-sketch/codex-ui that referenced this pull request Sep 29, 2026
main 的 scripts/sidebar-color-verify.mjs(自带 CDP + 自写 PNG 解码)改写为
scripts/specs/sidebar-color.mjs,走 PR rinDBeans#3 的夹具 harness(t.page / page.pixels / t.shot):
同一页并排渲染新值与旧值两组,亮暗各截一次,采样点与判据不变。

验证:node scripts/verify.mjs sidebar-color → 16/16;全套 224/224;npm run check 67/67。
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.

2 participants