# 修改说明 本仓库是上游 [dsh-codex-effort-slider](https://github.com/Microqian2th/dsh-codex-effort-slider) `1.0.0` 的**修改分支**, 当前版本 `1.3.10`。这份文档说明**改了什么、为什么改、怎么验证、哪些是已知取舍**。 按主题分四类: | 类别 | 一句话 | | --- | --- | | [观感](#一观感球比轨道大而且是真的圆) | 滑条对齐 GPT 客户端;DSH 的全局超椭圆把球压成了圆角矩形 | | [手感](#二手感一档一格快慢两档动画) | 旋钮不再跟手,改成一档一格;拖得慢动画也慢 | | [性能](#三性能连续拖动不再打宿主) | 连续拖动期间 0 次写回宿主(原来约每秒 8 次) | | [工程](#四工程可复现打包与死代码清理) | 补上打包脚本;产物的可复现性做了实测 | --- ## 一、观感:球比轨道大,而且是真的圆 ### 1. 球直径 ≠ 轨道高度 上游把球的直径做成和轨道高度相等(都是 28px),于是球上下与轨道齐平、两端和轨道端头糊在一起, **看着不像一颗球**。参考 GPT 客户端实测:轨道 80px / 球 110px,比值 ≈ **1.375**。 本分支取轨道 20px、球保持 28px(比值 1.4,同一量级),球上下各浮出轨道 4px; 同时球心在两端只内缩**轨道端头半径**(而不是球半径),所以最左/最右档时球会压过轨道端头—— 和客户端一样。 ``` BEFORE(上游 1.0.0) AFTER(本分支 1.1.0) 轨道 28px,球 28px 轨道 20px,球 28px ┌────────────────────────┐ ┌────────────────────────┐ │███████● │ │███████ ⬤ │ ← 球上下浮出 4px └────────────────────────┘ └────────────────────────┘ 两端球与端头齐平、糊成一体 两端球压过端头 ``` 涉及常量(`lib/client.js` 顶部):`TRACK_HEIGHT = 20`、`KNOB_SIZE = 28`、`TRACK_RADIUS = TRACK_HEIGHT / 2`。 ### 2. 球被渲染成"圆角矩形"——根因是 DSH 的全局超椭圆 这一条花了最久,因为**在独立预览页里球是正圆,真机上却是圆角矩形**。 根因:DSH 主题(`@deepseek-ai/dsh-client-ui-theme`)注入了全局规则 ```css @supports (corner-shape:superellipse(1.5)) { :root { --dsw-corner-shape: superellipse(1.5) } *, :before, :after { corner-shape: var(--dsw-corner-shape) } } ``` 于是**每个元素**的 `border-radius` 都按超椭圆(squircle)渲染,`border-radius:50%` 画出来不是正圆。 DSH 自己的 Switch 也为此显式退出(其 CSS 注释写着 "The capsule track and circular thumb opt out of the global superellipse"),本插件照做:**6 条带圆角的规则全部显式写 `corner-shape:round`**。 `*` 的特异度是 (0,0,0),本插件的规则是 (0,1,0),直接声明即可覆盖,**不需要 `!important`**。 验证方式:把上面那段超椭圆规则注入预览页 → 球立刻变圆角矩形;加上 `corner-shape:round` → 恢复正圆。 计算值可从 `superellipse(1.5)` 变回 `superellipse(1)`。 ### 3. off 档左侧漏出一弯蓝色月牙 填充层左端原本写 `border-radius:999px`,在窄填充上(off 档宽度只有一个半径)CSS 会把四角一起收缩, 填充轮廓从球里鼓出去,在球左侧漏出一道蓝边。 改为**左端圆角 = 轨道端头半径**(`TRACK_RADIUS`,不是 999px),与轨道端头同心等径;右端永远直角、 永远躲在球内(填充终点 = 球心,而球在其圆心那一列占满 2×球半径 ≥ 轨道高度)。 验证方式:按圆角矩形轮廓做 0.25px 步长采样,断言填充不超出球;测试里还**反向验证了旧写法确实会漏**。 ### 4. Max 档不再显示其他档位的定位点 那一档轨道已经是紫色星云,灰/白小点压在上面只剩噪点。改为**只保留当前位置那一个点** (它本来就被球完整盖住),其余 `opacity:0` ——只改透明度、不移除节点,所以拖动时不会增删 DOM、 不会打断拖拽与动画。 --- ## 二、手感:一档一格,快慢两档动画 ### 1. 旋钮不再跟手 上游是连续跟手拖动。改成**一档一格**:指针越过"当前档位 + **1/3 档距**"的判定线才切到下一档, 旋钮因此停在档位点上一格一格走。 - **点空白处** = 点哪去哪:落到离指针**最近**的那一档(不是 1/3 判定),到位过程仍是滑动。 - 多档连跳 = **一段滑到位**,不逐档连滑。 - 按下时先记录 `{u, index, conflict}`:只有在"最近档"与"1/3 判定档"不一致时才启用死区, 否则按下的那一格会被立即撤销。 > ⚠️ **一处刻意的不对称**:判定线只有一条(在 `level+1/3` 处),所以**前进要 1/3 档距、后退要 2/3 档距**。 > 对称的 ±1/3 迟滞在数学上会自相矛盾(同一点同时满足"进"和"退"→ 来回抖)。 > 若想要对称,把 `DETENT_THIRD` 改成 `1/2` 即可(判定线落在中点,进退都是 1/2)。 ### 2. 快慢两档动画 判据是**离上一次换档过了多久**(不用指针速度 px/ms —— 那会被轨道宽度、缩放影响): | 两次换档的间隔 | 用哪档 | | --- | --- | | > 320ms(手在慢慢挪) | **慢档** `.34s ease-in-out` | | < 200ms(手快) | **快档** `.16s cubic-bezier(.2,.9,.3,1)` | | 200–320ms 之间 | **保持当前档**(120ms 迟滞带) | 迟滞是必要的:没有它,速度刚好卡在阈值上时快慢会一格一跳地来回切。「按下 = 点哪去哪」一律用快档。 实测(同一页面模拟两种拖动,量"到 90% 位置"的耗时): | | `data-slide` | 实际生效的过渡 | 到位耗时 | | --- | --- | --- | --- | | 慢拖(换档间隔 430ms) | `slow` | `0.34s ease-in-out` | **270.5ms** | | 快拖(换档间隔 30ms) | `fast` | `0.16s cubic-bezier(.2,.9,.3,1)` | **85.5ms** | **实现要点**:四层(球 / 填充 / 能量 / 星空)的过渡读同一个 CSS 变量 `--ces-slide`,慢档只换变量值: ```css .ces-inline { --ces-slide: .16s cubic-bezier(.2,.9,.3,1) } .ces-inline[data-slide='slow'] { --ces-slide: .34s ease-in-out } ``` 而不是逐条写四份 `transition` 覆盖——**逐条覆盖的选择器特异度更高,会把 `prefers-reduced-motion` 里的 `transition:none` 压住**,那样开了"减少动态效果"反而还在动。用变量就没这个问题。 ### 3. 去掉键盘操作 滑条不再接管方向键、不进 tab 序,官方菜单的键盘导航恢复原样。 (`role="slider"` 与 `aria-*` 保留,读屏仍能念出当前档位;相应的 `.ces-track:focus-visible` 规则一并删除。) --- ## 三、性能:连续拖动不再打宿主 **先量后改。** 在无头浏览器里跑"拖动扫过全部档位"的帧节奏(预览页用的是真产物的 CSS): | 指标 | 实测 | | --- | --- | | 帧数 | 174 | | 帧间隔 中位 / p90 / p99 | 16.7 / 16.9 / 17.9 ms(满 60fps) | | 超过 20ms 的帧 | **0** | | 超过 33ms 的帧 | **0** | | 长任务 | **0 个** | 结论:**动画这条路径不是瓶颈**,没必要去动它。真正的 churn 在写回: - 上游是 **120ms 节流**:连续拖动 ≈ **每秒 8 次**请求宿主改这个会话的推理档位。 而 DSH 每次改档位都要动会话状态(落盘 + 重渲染)——这正是"实测有点卡"和"预览里很顺"能同时成立的原因。 - 另外,指针**每动一下**都会去读一次会话状态(每秒几十次),也是白烧。 改法是**停顿去抖**: ```diff - var COMMIT_THROTTLE_MS = 120; // 节流:每 120ms 打一次宿主 + var COMMIT_SETTLE_MS = 260; // 去抖:指针还在动就一直不打 - if (pending.timer !== null) return; // 领先沿 + if (pending.timer !== null) clearTimeout(pending.timer); // 每次重置 ``` ```diff - requestCommit(...) // 每次指针事件 + if (changed || immediate === true) requestCommit(...) // 只有真的换档 ``` 效果:**连续拖动期间 0 次写回**;指针停住 260ms 落地一次;松手立即落地一次。 "拖到某档停一下也生效"这个原有行为保住了。 --- ## 四、工程:可复现打包与死代码清理 ### 1. 补上 `pack-dist.ps1` `package.json` 的 `npm run pack` 一直引用它,但发布包里**从来没有这个文件**。补齐后: ```powershell npm run pack # → dist/better-dsh-codex-effort-slider-1.1.0.zip ``` zip 结构与上游一致:**单层同名根目录**,解开后那一层目录即可作为 DSH 面板要填的本地路径。 打包脚本里踩到并修掉的三个坑(都写进脚本注释了): 1. **PS 5.1 按 ANSI 读无 BOM 的 `.ps1`** → 中文注释糊成乱码 → 直接语法错误。 本仓库所有 `.ps1` 都是 **UTF-8 带 BOM**(厂商的 `install-profile.ps1` 本来就是),且脚本注释里写明了原因。 2. **.NET Framework 的 `ZipFile.CreateFromDirectory` 把条目名写成 `a\b\c`**(反斜杠)。 ZIP 规范要求 `/`,反斜杠在 macOS/Linux 解压时会变成文件名的一部分。 改为手工建条目,并在脚本里断言"不许出现反斜杠"。 3. `ZipArchiveMode` 在 `System.IO.Compression`、`ZipFile` 在 `System.IO.Compression.FileSystem`, PS 5.1 上两个都要显式 `Add-Type`。 ### 2. 死代码清理(1.1.0;1.2.0 无新增死代码) | 删掉的东西 | 为什么是死代码 | | --- | --- | | `.ces-track:focus-visible` | 去掉键盘操作后该元素不可聚焦,这条规则永远不会命中 | | `indexFromPct()` | 按下取最近档现在直接用 `Math.round`;定义、导出、预览页抽取列表一并移除 | ### 3. 发布产物的可复现性(实测) 对最终的 `1.1.0` 包做了四项校验,全绿: - zip **18 个条目**、单层根目录、**0 个反斜杠**; - 解压后与源码树**逐文件哈希 0 差异**; - **从解压副本跑测试:337/337 通过**(包是自足的,不依赖仓库外任何东西); > 这一组数字是 **1.1.0 发布时**的实测记录(档位断言套件当时是 337 项)。 > `1.2.0` 增加了第 19–26 节、`1.3.0` 增加了第 27–40 节、`1.3.1` 增加了第 41 节、 > `1.3.2` 增加了第 42 节并在第 11 节补了 5 条接线断言、`1.3.3` 增加了第 43 节 > (并改写了第 30 节那条"搜不到时高度"的断言)、`1.3.4` 扩写了第 35 节(滚动改重定位 / > 右缘对齐)与第 38 节(星尘固定浅色),现在是 **908 项**; > 打包脚本与 zip 形状没有变化。 - **从解压副本再打一次包,SHA256 与第一次完全一致**。 --- ## 五、验证与回归 ### 怎么跑 ```bash node test/client.test.mjs # 908 项断言,零依赖,不需要装任何东西 node preview/make-preview.mjs # 生成 preview/panel.html,直接用浏览器打开 ``` `preview/panel.html` 是**真的用产物生成**的,不是手抄的:CSS 从 `lib/client.js` 里抽出来, 拖动数学(`pctFromIndex` / `energyFor` / 粒子参数)也是抽函数体内联进页面, **抽取失败就抛错**,绝不悄悄生成一个数学不一样的预览。所以它可以拿来当"能不能重启"之外的快速验证。 ### 断言分布(共 908 项) | 段 | 主题 | 条数 | | --- | --- | ---: | | 1 | 接线与产物 | 10 | | 2 / 2b / 2c | 只动官方那一行 / 顶头适配 / off 档不漏色 | 23 / 15 / 8 | | 2d / 2e | Max 档隐藏定位点 / 圆角 opt-out | 10 / 3 | | 3 / 4 | fail-open / 关菜单后逐个还原 | 11 / 9 | | 5 / 5b | 写回官方档位 / 连续拖动 0 次写回 | 8 / 3 | | 6–9 | 不误触 / 去重限频 / 失败自愈 / 快照稳定 | 5 / 7 / 9 / 2 | | 10 / 10b | 位置驱动的色彩与粒子 / 减少动态效果 | 76 / 10 | | 11 / 12 / 13 | 无消息注入 / DOM 锚点契约 / 宿主半边 | 6 / 7 / 2 | | 14 / 15 / 16 | 数值文字颜色 / 收起态模型席 / Off 档刻度 | 20 / 16 / 22 | | 17 / 18 | 拖动手感 / 快慢两档动画 | 21 / 6 | | **19** | **档位字典 / tiers / 三级检测 / 目录优先**(v1.2.0) | **27** | | **20 / 21 / 22** | **模型行徽标 / provider 归属标签 / 档位 API 取值**(v1.2.0) | **15 / 9 / 8** | | **23** | **翻页滚动稳定化 + focusNoScroll + 慢档缓动**(v1.2.0) | **14** | | **24 / 25** | **禁用态投影 / 8 档能量阶梯**(v1.2.0) | **16 / 16** | | **26** | **预览页自洽性(id 齐全 + 能真的跑起来)**(v1.2.0) | **9** | | **27** | **自建浮层:收起态按钮 / 一级 provider 页 / 二级模型列表与档位徽标**(v1.3.0) | **31** | | **28 / 29** | **浮层滑条与写回 / 档位弹层与 Esc 逐层退避**(v1.3.0) | **30 / 16** | | **30 / 31** | **双页 carousel 舞台与视口高度 / 禁用态与 fail-open**(v1.3.0) | **14 / 17** | | **32 / 33** | **包内 chunk 的加载契约 / 平台缺 window 的降级**(v1.3.0) | **12 / 2** | | **34** | **样式真的进了文档(注入类函数的静默失败)**(v1.3.0) | **15** | | **35 / 36** | **触发器/面板拆分挂载 + 定位冻结 / 环境缺 window 的降级**(v1.3.0) | **10 / 2** | | **37** | **审核回归:滑条死区 / 星空宽度 / 样式标记 / slow 规则 / 目录换实例 / 陈旧弹层**(v1.3.0) | **25** | | **38** | **主题一致性:旋钮在浅色与深色下都是同一颗白球**(v1.3.0) | **7** | | **39 / 40** | **多会话各一颗按钮(删全局守卫的回归钉)/ 面板按会话归属**(v1.3.0) | **11 / 7** | | **41** | **二级页模型列表按「新发布 → 旧发布」排序(版本号 / 日期 / 别名与快照版)**(v1.3.1) | **20** | | **42** | **接管官方模型席(single 槽 priority −1 遮蔽 / 三条时序的退路 / locked / 紧凑态)+ 第 11 节的接线断言**(v1.3.2) | **35** | | **43** | **真机事故:目录为空时面板一片空白 → 提示可见 / 失败清单 / load 自愈 / "就绪但空"主动刷新**(v1.3.3) | **17** | | **35 / 38(扩写)** | **滚动改成重定位(流式输出不再关面板)+ 右缘对齐输入框 / 星尘改成固定浅色**(v1.3.4,两节净增) | **16** | | **35 / 38(扩写)** | **阴影不再被 `overflow` 裁成矩形(滚动安全网挪到面板那层)+ 底色再透一档(官方色相降 alpha)+ 卡片半透明**(v1.3.10) | **6** | | **38(扩写)** | **面板材料对齐官方菜单:毛玻璃(blur(40px))+ 半透明填充令牌 + 浅色下看得见的阴影**(v1.3.9) | **13** | | **46** | **换模型时的默认档位 = 正中(偶数档取「中间的下一个」)**(v1.3.8) | **19** | | **44 / 45** | **包清单不许带 BOM(宿主 JSON.parse 它)/会话 id 两条来路 + 空态说人话**(v1.3.6) | **14** | | **31 / 43(扩写)** | **空态区分"我们没拿到目录"与"宿主没列 provider"(`access.phase()` + `data-phase`)**(v1.3.5,两节净增) | **4** | > 10 段的 76 项来自色彩与粒子的**采样断言**(在档位轴上密集取点比对颜色与密度曲线), > 不是 76 个独立场景。 > > 这张表是**按主题**归并的手工分布(同一节的断言会拆到不同主题、有的主题横跨多节), > 所以**逐行相加不等于**标题里的总数 —— 总数以 `node test/client.test.mjs` 的输出为准 > (当前 **908 项**)。 ### 兼容性约定 以下三处**故意沿用上游的标识**,以免与上游的既有安装冲突时产生歧义,也方便对照上游代码: - **bundle 注册 id / 模块 id** = `better-dsh-codex-effort-slider`(改名了,与上游不同包名,两者可并存) - **插槽条目 id** = `codex-effort-slider`(**故意不改名** —— 它是写进 `cordis.patch.yml` 的 include 锚点, 测试里对这一点有专门断言,避免以后有人"顺手"改掉而悄悄破坏加载器那一行) ### 官方 DOM 锚点的全部依赖(v1.2.0 新增三处注记之后) 三处注记用到的官方结构都**逐字对照过真产物**(`dsh-client-ui-model-selection/lib/client.js`): | 锚点 | 官方形状 | 用在哪 | | --- | --- | --- | | 模型行 | `button[role="menuitemradio"][id="${id}-model-${index}"]` → `.optionCopy > .modelName` | 档位徽标(**按行上的模型名认**,不按 id 序号 —— 那个序号是筛选后的位置) | | 档位行 | `button[role="menuitemradio"]`(**没有 id**) | 原始 API 取值(按文档顺序对 `reasoning.efforts`) | | 根面板行 | `button[role="menuitem"]` → `.cellLabel` / `.cellValue` / `.cellChevron` | provider 归属标签(追加在最后) | | 滚动容器 | `.scrollable`(`wq12jW_groups` 上的**字面量 class**) | 翻页稳定化(`overflow-anchor:none` + 滚动清零) | 第 12 节断言会**直接读解包出来的官方产物**校验这些锚点还在(`aria-haspopup` / `aria-expanded` / `aria-controls` / `role="menuitem"` + 官方文案 + `resize` 重测),官方改版导致锚点漂移时会先炸测试, 而不是静默失效。本机没有解包产物时该节自动跳过。 --- ## 六、已知限制与取舍 - **判定线不对称**:前进 1/3、后退 2/3(见上文说明,改 `DETENT_THIRD` 可换)。 - **`preview/` 下 4 张 README 配图是上游版本拍的旧观感**(球=轨道高的那版)。 文字说明是准确的,图会显得旧。重拍方式:`node preview/make-preview.mjs` 后用浏览器截图。 - **颜色曲线未跟进上游之外的口味**:本插件从 pct=1/3 起由蓝转紫,GPT 客户端则一直保持蓝色、只在最高档变紫。 想对齐就把 `ENERGY_START` 从 `1/3` 改成 `1`。 - **写回仍是"停手/松手"两次**:若想拖动期间**零**宿主流量,可以改成只在松手写回, 代价是官方那一行的档位文字要等松手才变。 - **8 档不是"只有 Ultra 亮"**:见上一节第 4 条(这是有意的口径统一,不是漏改)。 - **徽标依赖"模型行有 id、档位行没有 id"这条形状**:官方若给档位行也编上 `model-` 样式的 id, 徽标会停止工作(fail-open:不再写徽标,滑条照常)。第 12 节的锚点契约会先炸测试。 - **本分支未跑过 DSH 之外的宿主**:测试是离线假 DOM,真实观感需要重启 DSH Desktop 后用眼睛确认。 --- ## 七、v1.2.0 的移植对照(逐条) > **`1.3.0` 之后这一节的口径变了**:参考实现里那些"不适用(真宿主没有对应的东西)"的条目, > 现在**在自建浮层里全部落地了**(双页 carousel、provider 页、搜索框、档位弹层、外点/Esc 关闭链…)—— > 因为插件现在**自己画那个面板**了(`lib/client.picker.js`,见 [`CHANGELOG.md`](CHANGELOG.md) 的 1.3.0 一节)。 > 下面保留原始对照,因为"官方菜单那一行"那条路(1.2.0 加的)**仍然存在**,那一半的取舍仍然成立。 参考实现是 `code.html`(自建菜单的静态原型,669 行)。真插件只改造**官方菜单里「推理等级」那一行**, 所以逐条对照后分三类:**已移植**、**等价替换**、**不适用(含原因)**。这一节就是那份清单。 ### 1. 已移植(原样落地) | 参考实现里的东西 | 落到哪里 | 说明 | | --- | --- | --- | | `LV` 字典(9 键,含补上的 `off`) | `lib/client.js` 的 `LV` | 只当兜底:目录给了名字就用目录的 | | `tiers()` 的"认不出就拒绝"纪律 | `tiers()` | 原型对未知档位**抛错**(它那是自己写的常量,抛错只暴露笔误);真宿主的档位表是运行时下发的,所以改成**返回 null**,对应那句"一条脏数据最多禁用滑条" | | 名字覆盖用**拷贝式**(不污染共享对象) | `tiers()` / `levelsOf()` | 每行都是新对象,没有共享引用可被改写 | | `resolveModel` 三级检测 | `resolveModel()` | 精确 → 最长前缀(`-` 或 `@`)→ null;只用做目录查不到时的兜底 | | 快照 id 继承(`gpt-6-astra-2026-01-13` → `gpt-6-astra`) | `DEF_ALIASES` + 前缀检测 | 双向都认(别名表 + 前缀),并修掉原型的**自环爆栈** | | 模型行档位徽标(`8 档 / 7 档 / 5 档 / 不支持`) | `decorateMenu()` 第 1 段 | 落在**官方模型列表**每一行的 `.optionCopy` 里 | | provider 行当前模型标签 | `decorateMenu()` 第 3 段 | 真宿主没有 provider 页,改为落在**根面板「模型」那一行**(同样是"一级页就能确认") | | 档位弹层每行灰色显示 API 取值 | `decorateMenu()` 第 2 段 | 落在**官方档位列表**每一行(`xhigh` 等) | | 禁用态 = 渲染函数的完整投影(`classList.toggle` 语义) | `disabled` + `data-disabled` | 每帧重算、投影到属性上,不做增量 patch | | 脏数据 try/catch 降级 | `buildCatalogIndex()` | 一行坏了只影响那一行的徽标 | | `focusNoScroll`(菜单内 focus 一律 `preventScroll`) | `focusNoScroll()` | 官方自己那句改不了 → 由滚动稳定化兜住 | | `setView` 里的防御性滚动清零 | `stabilizeScroll()` | 在 mutation 回调里同步做(微任务,早于绘制) | | 慢档缓动 `.34s cubic-bezier(.45,.05,.35,1)` | `SLIDE_EASE_SLOW` | 快慢两档机制本身没动 | | 双页位移(`page1 0→-100%` / `page2 +100%→0`) | `stabilizeScroll()` + `overflow-anchor:none` | 真宿主是**同一面板换内容**(没有双页),所以移植的是它真正解决的问题:换内容那一帧的位移;官方面板的间距/圆角/阴影一个没动 | ### 2. 等价替换(形状不同,效果同一个) | 参考实现 | 本插件 | 为什么不一样 | | --- | --- | --- | | `state.provider = subProvider`(provider 随模型切换) | **不需要**:官方 `directory.select()` 一次写 `{provider, model, reasoningEffort}` | 原型自己持有 `provider` 状态才会有那个 bug;本插件没有这份状态,也就不存在这个根因 | | `OPENAI_DEFS`(10 条静态能力清单) | `buildCatalogIndex(directory snapshot)` | 真宿主的能力清单是**活数据**(`groups[].models[].reasoning.efforts`),客户端只做检测与渲染——这正是原型自己那句"生产应由宿主侧目录下发" | | `energyStart ?? (n-2)/(n-1)`(两种口径) | 单一位置公式 `(pct-1/3)/(2/3)` | 原型给 DeepSeek 写 `1/3`、给 OpenAI 走回退,于是 8 档只有末档亮;本插件一条连续曲线,8 档 Max = 11/14、Ultra = 1(见下) | | `.cnt` 的 `font-size:10.5px` 胶囊 | `.ces-menu-badge`(同样 10.5px/6px 圆角,颜色走 DSH 令牌) | 原型是写死深色的 demo,本插件必须浅深主题都跟随 | | `.pop-row .api`(11px `#9aa0ab`) | `.ces-menu-api`(11px + `--dsw-alias-label-caption`) | 同上:令牌化 | ### 3. 不适用(真宿主里没有对应的东西) - **双页 carousel 的那套 CSS/JS 本体**(`.viewport` / `.page1` / `.page2` / `dataset.view` / `inert`): 官方菜单是同一个 `MenuSurface` 里换 pane,没有两页可位移。移植的是"换内容时不要多出位移", 不是那套 transform。**"一级页右缘无阴影"在真宿主里等价于"不给官方面板加任何边框/阴影"**—— 原型靠"从没给 `.page` 写过 box-shadow",本插件靠"一条都不加"(测试第 23 节盯着这一点)。 - **`setView` 之外的菜单定位**(`PANEL_W`、`clamp`、`openMenu` 的翻转):官方自己 `place()` 夹取, 插件只在加高那一行后派发一次 `resize` 让它重测。 - **原型自己的键盘导航 / 搜索框 / roving tabindex / 外点关闭链**:官方菜单全都有,插件不碰。 - **`gpt-*` 型号名与档位名(X-High / Ultra)**:本机 DSH 侧是 DeepSeek 的 `off/low/high/max`; 那些名字只出现在**静态兜底清单**里,且只在目录没给出条目时才会被显示。 ### 4. 一处**故意没有照抄**的地方(要说清楚) 参考实现给 OpenAI 路径用的是回退公式 `start = (n-2)/(n-1)`,于是 **8 档只有末档(Ultra)点亮,Max 的能量是 0**; 而它给 DeepSeek 的 4 档写的是 `energyStart: 1/3` —— "Max 半能量"这个观感其实来自 **4 档模型的 high(位置 2/3)**, 两个模型族的口径本来就不一致(子代理的复算把这件事实锤了:8 档下 `en = clamp(idx-6,0,1)`)。 本插件只有**一套**位置公式,所以: | 位置 | 4 档 | 8 档 | | --- | --- | --- | | 1/3 | 能量 0(Low) | 能量 0 | | 2/3 | **能量 0.5(High = "半能量"锚点,保持不变)** | 能量 0.5 | | 6/7 | — | 能量 **11/14 ≈ 0.786**(Max 已是中后段,不是 0) | | 1 | 能量 1(Max,满星云) | 能量 1(Ultra,满星云) | 也就是说:**"Ultra 满星云"照旧,"Max 半能量"这个锚点在它原本的 4 档位置上精确成立**, 而 8 档从"只有末档亮"变成了连续阶梯(拖动时不再忽明忽暗地跳)。这一条是有意的取舍, 测试第 25 节把这几组数值全部钉住了。 --- ## 八、目录结构 ``` lib/client.js 客户端·接线(官方菜单那条控件 + 两条条目的注册) lib/client.picker.src.js 自建浮层的**源码**(人写) lib/client.picker.js 自建浮层的**发布产物**(npm run build 生成的自注册 chunk) build/pack-picker.mjs 那一步构建(幂等;pretest / prepack 自动跑) lib/index.js 宿主半边(零依赖空壳,只做 bundle 静态自检) cordis.patch.yml 加载器插桩:往官方菜单那一行 insert 一个 bundle test/client.test.mjs 908 项离线断言 test/harness.mjs 假 DOM / 假宿主(够用的最小实现) preview/make-preview.mjs 从产物抽 CSS 与函数体,生成可交互的 panel.html preview/panel.html 预览页(生成物,可直接用浏览器打开) pack-dist.ps1 打发布 zip(npm run pack) install.cmd / install-profile.ps1 安装/卸载辅助 CHANGELOG.md 按主题归并的变更记录 ``` 许可:MIT(沿用上游,`LICENSE` 中保留了上游版权声明并另加本分支一行)。