# 架构与实现细节 > 这份文档回答"它是怎么做到的、为什么这么做",面向要改代码或排查问题的人。 > > - 安装、使用、常见问题 → [README.md](README.md) > - 逐版改动与真机事故复盘(现场 → 根因 → 修法 → 断言) → [CHANGELOG.md](CHANGELOG.md) > - 主题式改动清单与取舍 → [MODIFICATIONS.md](MODIFICATIONS.md) --- ## 1. 包的三半 | 半边 | 干什么 | |---|---| | `lib/index.js`(宿主) | 空逻辑 + 一次客户端产物静态核对(`lib/client.js` 与 `lib/client.picker.js` 分开报)。存在的唯一理由是让本包成为 cordis 树上的一行,否则 `dsh.client` 的 bundle 不会被扫进启动图 | | `lib/client.js`(客户端·接线) | 不可见锚点(注册进 `conversation.input.right`,用来拿 `sessionId` 与 `ctx`)+ **DOM 桥** + 官方菜单那行的滑条(`react-dom` portal 注入官方那一行)+ 自建浮层的接线 | | `lib/client.picker.js`(客户端·自建面板) | 按参考实现复刻的自建浮层:收起态按钮 / 两级面板 / 模型档位徽标 / 常驻滑条 / 档位弹层。它是**包内动态 chunk**(宿主的 `client.*.js` 规则),由 `require.async("./client.picker.js")` 取;取不到时只有它消失,官方菜单那条路照常 | `lib/client.js` 是**手写的发布产物**(`require("react")` / `require("react-dom")` 走宿主模块表的平台种子词); 自建浮层那一半有一步构建:源码 `lib/client.picker.src.js` → `npm run build` → 自注册 chunk `lib/client.picker.js`。 **chunk 必须自己注册**(真机踩过一次):客户端模块表按包名建表,同步 `require("包名/子路径")` 会抛 "missed the module table";包内 chunk 要用 `require.async("./client.picker.js")` 取,文件名必须匹配 `client.*.js`。 宿主对 chunk 的响应体**只做拼接、不包壳**,所以裸 ESM 的 `export` 会在脚本标签里直接语法错 —— 表现是"入口按钮根本不出现、控制台很安静"。第 32 节断言按真宿主的方式加载这个文件,防止再退回去。 --- ## 2. 两套界面(A / B)与默认开关 默认只启用 B。两套 UI 同时挂在同一个输入区里会互相干扰(官方那一行里会多出一条滑条), 所以 `lib/client.js` 里的编译期开关二选一: ```js ENABLE_OFFICIAL_MENU_SLIDER = false; // true = 回到 A(改造官方菜单),同时关掉 B 的接管 ``` ### A. 官方菜单那一行(1.x 起,默认关闭) 直接改造官方模型菜单里的「推理等级」那一行 —— 那一行被加高上下内距、允许折行,滑条作为第二行铺满整行; 拖到最高档时整条轨道亮起紫色能量层。另外往官方菜单里加**三处只读注记**(都只 append,不改官方任何文字): ``` 点开官方模型菜单后: ┌──────────────────────────────────────────────┐ │ 模型 DeepSeek-V41-Flash › deepseek│ ← 归属标签(provider,一级页就能确认) ├──────────────────────────────────────────────┤ │ 推理等级 Max › max │ ← 数值文字跟着变色;右侧灰字是原始 API 取值 │ ●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ ← 插件注入的滑条(这一行因此被加高) └──────────────────────────────────────────────┘ 点「模型」进二级页(官方那一屏): DeepSeek-V41-Flash [4 档] gpt-6-astra [8 档] gpt-6-luna [7 档] gpt-6-spark [5 档] gpt-4o [不支持] 输入框工具行:**不加任何东西**(没有胶囊、没有浮层) ``` ### B. 自建浮层(1.3.0 起;1.3.2 起**接管官方那颗模型席按钮**) 输入框右侧那颗 `模型名 · 档位名 ⌄` 就是它 —— **没有第二颗**(见第 3 节), 点开是**两级面板**(挂在 `conversation.input.overlay`,所以不会被 composer 卡片裁掉): ``` 收起态: [ DeepSeek-V41-Flash · Max ⌄ ] 点开后: ┌──────────────────────┐ │ 模型 │ │ DeepSeek V41-Flash ›│ ← 一级页:provider 行 + 该 provider 当前选中的模型名 │ OpenAI ›│ ├──────────────────────┤ 点 provider → 同一块面板**横向翻页**到二级: │ 推理等级 Max ⌄│ ┌──────────────────────┐ │ ●━━━━━━━━━━━━━━━━━━━ │ │ ‹ DeepSeek │ └──────────────────────┘ │ [搜索模型… ] │ │ DeepSeek-V41-Flash ✓ │ │ DeepSeek-V41-Pro 4 档│ │ Plain Model 不支持 │ └──────────────────────┘ 点档位名展开弹层:每行 = 档位名 + 原始 API 取值 Off off Low low High high ✓ Max max ``` 两页是**同步位移**的(`page1 0 → -100%` / `page2 +100% → 0`),所以边界全程贴合; 点模型**不关面板**、滑条立刻跟到新模型的档位(这是"自建面板"相对内置注入的关键好处: 内置注入借官方菜单的 DOM,官方菜单一关就跟着没了)。 面板的位置口径(1.3.4 定稿):**右缘贴 composer 卡片右缘**、**底边贴触发器上方 8px**; **滚动 / 窗口缩放只重定位、不关闭** —— 助手流式输出时聊天区会一直自动滚动, 早先"滚动即关闭"会把面板关掉。触发器被宿主 `hidden`(助手在跑时 composer 会藏起控件行)时保持原位。 面板材料(1.3.9 / 1.3.10)走官方菜单那一套令牌: | 项 | 取值 | |---|---| | 模糊 | `backdrop-filter: var(--dsw-menu-backdrop-filter, blur(40px) saturate(150%))`(官方令牌定义在 `body` 上,自定义属性会继承) | | 底色 | 官方色相 + 比官方**再透一档**:浅色 `rgba(248,249,250,.46)`、深色 `rgba(67,69,74,.40)`(官方填充令牌是 `.58` / `.45`) | | 阴影 | `--dsw-elevation-stroke` + `--dsw-shadow-lv3` + 一层落地阴影;深色另给一套 | | 推理等级卡片 | 半透明(浅色 `rgba(255,255,255,.55)`、深色 `.06`)——实心卡片会把毛玻璃整个抵消 | | 滚动安全网 | 在**面板自己**那层(`.cxp-panel{overflow:auto}`);定位壳 `.cxp-root` 必须 `overflow:visible`,否则会把面板的 `box-shadow` 裁成矩形 | --- ## 3. 接管官方模型席(1.3.2 起) 官方那颗席按钮注册在槽位 `conversation.input.model`,那是一个 **single 槽**,官方注册时没写 `priority`(= 0)。 single 槽**同优先级撞了会抛错**,换优先级就是"遮蔽"(最低者渲染)—— 所以本插件用 `priority: -1` 接管: | 时序 | 行为 | |---|---| | 官方席先出现 | 直接接管(注册 `priority: -1` 的 occupant) | | 工具行先出现 | 先挂在 `conversation.input.right`,官方席随后出现时撤掉那条再接管(两个回调同一批同步执行,看不到闪动) | | 官方席始终没有 / 注册抛错 | 工具行那条就是最终落点 | **最差也只是回到"两颗按钮",绝不会两边都没有。** 另外接管意味着那份"报告"也归我们了: 官方菜单会显示"目录正在加载 / 没有可用 provider / 某个 provider 加载失败", 这些状态现在画在自建面板里(含宿主**逐 provider** 的失败原文),并且高度算得下它们。 `locked` 是那个槽的 **owner prop**(官方席按钮就吃它):接管之后这条语义跟着接过来, 输入区被锁住时我们那颗也不可点。紧凑态沿用官方的两个变量 (`--dsh-composer-model-text-display` / `--dsh-composer-model-icon-display`)。 --- ## 4. DOM 桥依赖的官方锚点(全是**语义属性**,不是哈希 class) | 锚点 | 官方产物里的形状 | |---|---| | 席:`button[aria-haspopup="menu"][aria-expanded="true"]` | `