# 开发笔记 面向改这个插件的人(包括未来的我)。用户文档在 [README.md](./README.md)。 ## ⚠️ 改代码前务必先读:踩过的坑 0. **服务改名会让座位"静默消失",必须注入新旧两个名字。** DSH 0.1.7 把客户端设置镜像服务从 `settingsScope` 改名为 `configForms`(由 `@deepseek-ai/dsh-client-ui-settings` 提供,`describe()` 返回的还是同一个 mirror 对象)。 cordis 的 `ctx.inject([...])` **只要有一个服务永不出现,回调就永远不执行** —— 不报错、不打日志。所以 0.1.7 一升级,本插件的设置页浮窗就凭空消失了(输入框的模型菜单照常,因为它注入的 `slots/modelDirectories/sessions` 都没变)。 现在 `apply()` 里同时注入两个名字,用一次性标志保证只挂载一次: ```js ctx.inject(["slots", "configForms", "remote", "remote.settings"], (s) => mountOrderPanel(s, s.configForms.describe())); ctx.inject(["slots", "settingsScope", "remote", "remote.settings"], (s) => mountOrderPanel(s, s.settingsScope.describe())); ``` **推论**:升级 DSH 后如果某个座位不见了,第一件事是去新版本的 `dsh-client-ui-*` 包里 grep 自己注入的每个服务名。 1. **`file:` 依赖是打包快照,`link:` 才是符号链接 —— 改仓库可能根本不生效。** profile 里写 `"dsh-model-organizer": "file:../../../dsh-model-organizer"` 时,pnpm 把目录**打包复制**进自己的 store;只有 `pnpm add ./dir`(直接路径)或 `link:` 才是符号链接。 症状:改完仓库代码、强刷浏览器,界面毫无变化;查 `window.__dshModelOrganizerBuild` 会发现跑的还是旧构建号,而服务端 bundle 里明明是新代码 —— 因为 DSH 加载的是 `profiles/

/node_modules/dsh-model-organizer/lib/client.js` 那份**副本**。 排查:直接 grep 那份副本的 `BUILD` 常量,和仓库对比。 修法:`dsh plugin --profile web remove dsh-model-organizer && dsh plugin --profile web add link:./dsh-model-organizer`(换成符号链接后只改一处)。在此之前必须**两份同步改**。 2. **0.1.7 的菜单色 token 是半透明的,浮层必须配 `backdrop-filter`。** `--dsw-specific-menu` 现在解析成 `rgba(48,49,54,0.5)`,官方菜单靠 `backdrop-filter: var(--dsw-menu-backdrop-filter)` 配合。只取颜色不取模糊 → 面板是「透视」的,底下的官方卡片按钮直接透上来。 3. **浮层要 `ReactDOM.createPortal` 到 `document.body`。** `settings.models.footer` 座位在设置页很深的子树里,`position: fixed` 会被困在那个子树的堆叠上下文里,被别的插件的悬浮组件(鲸鱼、地球图标等)盖住 —— 提高 z-index 也没用。挂到 body 之后 z-index 才是全局的(本插件用 10000,官方菜单是 1100)。 4. **profile bundle 必须声明 `dsh.bundle.patch`**(指向 `cordis.patch.yml`)。缺了它 dsh 启动即报 `profile bundle "..." declares no dsh.bundle in its package.json`。 2. **顶层 `inject` 必须声明座位 standardProps 会读取的每一个服务**(本插件:`locale, slots, sessions, remote, remote.session, remote.settings`)。 少一个 → 渲染时抛 `cannot get property "remote.session" without inject` → **条目被静默弃权(abdicate)并回退到内置 UI**,界面无任何报错。 调用 `remote.settings.mutate` 还必须显式声明 **`remote.settings`**。 3. **owner props 以组件 props 传入,不经过座位的 `inject` 工厂**:`settings.models.provider-card` 的 `inject` 不接收参数,必须读 `props.provider`。 4. **`single` 座位优先级**:同优先级注册会**直接抛错**;运行时为非 chain 座位**自动分配**优先级(`--nextPriority` 递减),**后注册者胜出**。 5. **drop 事件会在行与容器上各触发一次**(冒泡),需用 ref 加锁,否则重复写入。 6. **边框必须用 longhand**(`borderWidth/borderStyle/borderColor`)。用 shorthand `border` 再叠加 `borderColor` 时,React 会把 shorthand 展开成 longhand;回退时移除 `borderColor`,`border-color` 就落回 **currentColor** —— 表现为「拖拽结束后高亮边框一直不消失」。 7. **拖拽会把源行留在焦点上**。行加 `tabIndex:-1`、drop/dragend 时 `blur()`;并且**不要给行画 `:focus-visible` 背景**(官方 `option` 类自带一条,会变成「拖完还留一块灰底」)。 8. **原生拖拽会让文字变成可拖对象**(浏览器弹「松开鼠标即可搜索」)。解决:`-webkit-user-drag:none` 加在**行的内容**上,不要加在行本身(加在行上会让行也拖不动)。这条是从侧边栏会话行的实现里学来的。 9. **primitive 图标不转发 inline `style`**:需要旋转/变色请传 `className`(本插件为此注入一张极小的自有样式表)。 10. **数组顺序可控,record 键顺序不可控**:`providers` 是 record,宿主要规范化键顺序,整体 `set` 与 `unset`+`set` 都实测无效 —— 所以供应商顺序只能存本机偏好。 11. **面板里用到的每个局部变量都要真的声明**:曾在 `ProviderModelOrder` 里用了 `C`(官方类名映射)却没定义 → `ReferenceError` → 条目再次被静默弃权,表现为「面板整个不见了」。 12. 浏览器半只需 `react` / `react-dom` / `@deepseek-ai/dsh-client-ui-primitives`(都在 shell 的 PLATFORM_MODULES 种子里),无需 `dsh.client.external`。 ## 兼容性与鲁棒性(已核查) ### 已发现并修复的真实冲突 **`settings.models.provider-card` 被 `@linxin666/dsh-client-ui-model-capabilities` 占用。** 该座位是 **keyed**,key 由官方页面固定派发为供应商的 settings namespace(`llm-pi-ai`);而该插件已经用同一个 key 注册了「模型能力」面板。SlotCore 对 keyed 座位**一个 key 只保留一个占用者**,且同 key 同优先级会直接抛错 —— 它用 `try/catch` 吞掉异常,所以**本插件早期版本一直在静默压制它的「模型能力」面板**(实测:让出该座位后,每个供应商卡片立刻出现「模型能力」)。 **修复**:本插件不再占用该座位,模型排序改由自己拥有的 `settings.models.footer`(list 座位,`id: model-organizer-provider-order`)承载 —— list 座位按 id 去重,天然可与该插件的 `ui-model-capabilities` 共存。 ### 其它已做的加固 | 风险 | 处理 | | :-- | :-- | | 官方 CSS module 哈希变化 / 被别的模块误命中 | 运行时解析前缀,并用**只有该模块才有的第二个类**(`_optionCopy`)校验;缓存与样式表数量绑定;全部失败则退回内置回退样式 | | 座位契约变更(prop 改名等) | **不拦截**:让渲染抛错 → SlotCore 弃权该条目 → 官方内置组件自动回填 | | 座位被重命名 / 移除 | `slots.inject` 永不触发 → 对应面板静默消失,其它功能不受影响 | | 单个座位注册失败 | 每个座位注册独立 `guard`,失败只记一条 warn,**不会拖垮整个插件** | | 写入被拒绝 / ops 契约变化 | `mutate` 的同步抛错与 promise 拒绝都有捕获,并在表头显示失败原因 | | 本机偏好 | `localStorage` key 带命名空间,读写都有 try/catch | | 注入的样式表 | `