# 开发

[← 返回 README](../README.md) · [文档索引](./index.md)

本地跑起来、改代码时必须知道的硬约束、以及在哪验证。

**先读这两条硬约束**，它们决定了「为什么我改完看不到效果」：

- `lib/client.js` 是交给 DSH 浏览器模块加载器的产物
  （`window.__ModuleLoader__.load({ id, factory })` 工厂），**不是**普通 ES 模块 ——
  与 DSH 自身 `@deepseek-ai/dsh-client-ui-*` 的产物同构。所以 `node --check` 能做语法自检，
  但**不要**当 ESM 引入它。
- `lib/index.js` 是 host 侧：拥有样式表目录、挂载围栏内的 JSON 接口。
  路由挂载方式与 `dsh-smooth-stream` 一致（本代内核的 `connection.rpc.handle()` 会因服务内部
  Context 未注入 `webServer` 而失败，直挂 Web 服务器是受支持的兜底）。

> **host 半不热更新。** 改 `lib/index.js` 后刷新页面毫无变化 —— 必须重启 dsh。
> 只有浏览器半走 `dsh-client-hmr` 热更新。这条踩过：host 改动在磁盘上躺了一整夜，
> 期间所有人都在测旧代码。见 [已知坑](./known-issues.md)。

## 控件只有一套：`ui.*`

界面里的按钮、开关、标签、下拉、行布局，**全是这个插件自己画的**，而且只有一套实现 ——
`lib/client.js` 里的 `ui` 对象（详见 **[UI 组件库](./ui-kit.md)**）。

| 控件 | 类 | 在哪定义 | 从哪调 |
| --- | --- | --- | --- |
| 按钮 | `.dshUi_button` | `ROW_CSS` | `ui.button(props, …children)` |
| 开关 | `.dshUi_switch` + `-Track` / `-Thumb` / `-Label` | `ROW_CSS` | `ui.switch({ checked, ariaLabel, labelText, onChange })` |
| 徽标 | `.dshUi_badge` | `ROW_CSS` | `ui.badge(text)` |
| 下拉 | `.dshUi_choice`（`Dropdown` 的触发钮） | `ROW_CSS` | `ui.choice({ ariaLabel, choices, value, onChange })` |
| 文本框 | `.dshUi_field` | `ROW_CSS` | `ui.field(props)` |
| 设置行 | `.dshUi_row` + `-Label` / `-Controls` | `WALLPAPER_CSS` | `ui.row(label, control, note?)` |

`ui` 与 `Dropdown` 都在 `lib/client.js` 里，两张样式表在模块加载时就注入 ——
所以插件详情页不需要先打开设置界面也拿得到这些样式。

**`tests/audit-ui.mjs` 是这条规则的守门人**：组件库之外手写 `dshUi_*` 类名会直接失败。

**为什么是这一套，不是组件库**：`@deepseek-ai/dsh-client-ui-primitives` 曾经短暂接进来过
（0.3.2），它确实能在运行时的模块表里取到，但那样这个插件就有了**两套控件**：设置行是自绘的，
详情页是组件库的，同一个扩展看起来像两个产品。现在统一在自绘这一套上：

- **零 require**：浏览器半一个 `@deepseek-ai/*` 模块都不在运行时 `require`，这正是它跨 34 个
  版本没坏的原因之一；
- **值照抄 DSH 规格**：36px 按钮、`--dsw-alias-bg-module-platform` 填充、`radius-sm` 的 chip、
  `l4` 边框的 34px 字段 —— 是「看着像 DSH」，不是「依赖 DSH 的实现」；
- **状态画在属性上**（`aria-checked` / `aria-pressed` / `aria-invalid` / `aria-busy`）：
  外观不可能和读屏器读到的状态分家。

> 组件库那一路的实测结论仍然留着做参考：它有 `Button`/`Switch`/`Tag`/`Checkbox`/`Input`/
> `Tooltip`/`StateDot`/`Menu`/`Toast`/`SettingsForm`/`SettingsValueField` 等，**没有**
> `Select`/`Slider`。哪天要切回去，这份清单还在 `git log` 里（022e1f4）。

---

## 命令

| 命令 | 做什么 |
| --- | --- |
| `node --check lib/client.js` | 语法自检（**不要**当 ESM 引入） |
| `npm test` | 两套冒烟测试，见 [测试](./testing.md) |
| `npm run compat` | 跨版本兼容断言（需要网络） |
| `npm run compat:check` | DSH 换代检查，只报告不写文件 |
| `npm run compat:update` | 同上，并把结果写回 `compat.json` / README / 社交卡 |
| `npm run audit` | 三个样式审计：圆角 / 字体 / 可访问名 |
| `npm run audit:duplication` | 重复率巡检 |
| `npm run dev` · `dev:down` · `dev:status` · `dev:dump` | 开发实例（端口 3081） |
| `npm run probe` | 无头 Edge + CDP 的量测探针 |
| `npm run ego` | 取件窗口：让 ego 浏览器带令牌进开发实例，令牌不落痕 |

---

## 开发实例

改这一行的界面时，用 `npm run dev` 起一个**只加载本插件**的 DSH：
独立 `DSH_HOME`（`dev/home`）、端口 **3081**、profile `ccdev`，与你正在用的那个实例**完全无关**。

```bash
npm run dev          # 起（幂等）；dev:down 停；dev:status 看状态
npm run probe -- --open-settings --sel .dshCc_row --shot
```

**它不是「复制一份 profiles/web」**：真机 profile 的 `cordis.patch.yml` 把 `webserver.port`
写死成 `3080`，而 DSH 出厂配置里那个字段是跟着 `--port` 走的 ——
复制过来的实例会去**抢真机的端口**。现在用官方模板 `--from-default-profile web`，
它只复制 bundle 列表，不读真机的本地状态。详见 [`dev/README.md`](../dev/README.md)。

## 视觉探针：在真 Chromium 里量，不靠看

`npm run probe` 用无头 Edge + CDP（**零依赖**：Node 24 自带 `WebSocket`）量几何、截图、
收控制台错误。

它**只测量、不断言** —— 「看起来有点挤」与「真的溢出 16px」是两回事，后者只有数字能说。
这条规矩救过一次：查找栏在窄窗口下把按钮挤到了第二行，肉眼看只是「有点挤」，
量出来才发现栏高翻倍（见 [已知坑](./known-issues.md)）。

## 要实时看着界面用 ego

`npm run ego` 会起一个**取件窗口**并打印一个本机地址。用 `ego_navigate` 打开它 ——
页面自己取走带令牌的 URL 并跳转，**令牌因此不出现在对话、终端或日志里**。

截图与临时产物落在 `temp/`（`temp/probe/` 是探针的，`temp/ego/` 是 ego 的），
整个目录不进版本库。

---

## 提交前

1. `npm test` —— 两套冒烟测试
2. `npm run audit` —— 样式审计（圆角 / 字体 / 可访问名）
3. 若改的是界面：`npm run probe` 量一遍，别只靠看
4. 若改了 host 半：**重启 dsh** 再验证

**每条修复都要做反向验证**：先把补丁反向打回去、确认测试会红。
否则你守的可能只是「实现长什么样」而不是行为。这条在本仓库是硬要求，
`tests/loader-smoke.cjs` 的注释里多处记着它抓出的「假守卫」。

发版流程见 [CONTRIBUTING.md](../CONTRIBUTING.md)；文档拆分与链接校验见 [测试](./testing.md)。

## 相关

- [测试](./testing.md) —— 两套冒烟测试、四个审计、CI 三个工作流
- [仓库结构](./repo-map.md) —— 每个目录是干什么的
- [已知坑](./known-issues.md) —— 尤其是 host 半不热更新那两条

## 已知结构债

`CustomCssRow` 仍是 2,200 行左右的单组件：编辑器、规则面板、补全、查找替换、撤销重做、历史版本都在它的闭包里，共享 ~25 个闭包与若干 ref。拆分需要把这些状态显式串成 props（或引入一个 context），并且**必须真机验证焦点、滚动、光标行为**——不是纯机械搬迁。控件层已经收进 `ui`，所以再拆它是「组件内部结构」的活，随时可以做，但不该顺手做。

