# 开发指南（development）

> 本文件是本插件的仓库结构、特殊挂载限制、不可破坏约束、开发经验与测试策略的权威位置。
> 用户可见语义见 [`behavior.md`](behavior.md)，架构边界见 [`architecture.md`](architecture.md)，发布清单见 [`release.md`](release.md)。

## 仓库结构

| 路径 | 职责 |
| --- | --- |
| `src/index.ts` | host 插件入口与运行时装配 |
| `src/*.ts` | provider、MCP、凭据、设置与热重载模块 |
| `src/client.ts` / `src/client-logic.ts` | 浏览器配置表单（插件页）及纯状态逻辑 |
| `src/bin/` | CLI 与 patch 行管理源码 |
| `src/tests/` | `node:test` 测试源码 |
| `cordis.patch.yml` | 随包发布的持久 bundle patch |
| `lib/` / `bin/` / `.tsbuild/` | 构建产物，由 `npm run build`、`prepare` 或 `prepack` 生成 |
| `docs/` | 按主题拆分的权威文档 |

`src/` 是唯一手写程序源码；`lib/`、`bin/` 与 `.tsbuild/` 只能由构建命令生成。

## 热路径选择（智谱专属例外）

本插件的首次挂载因复杂 patch 存在额外限制：

| 场景 | 本插件正确通道 | 验证方式 |
| --- | --- | --- |
| 首次热挂载 | 使用项目 CLI 的桥接/临时行策略；不可仅凭 dsh-zh manifest reconcile 假定成功 | CLI `status` 之外，检查实际 Loader、provider registry 和现有 GUI |
| 让新构建进入长跑进程 | 当前 DSH 下没有免重启通道（见下）——由用户重启一次 `dsh web` | 重启后核对运行态行为，或包内暴露的运行态版本字段 |

原因：`cordis.patch.yml` 除 `insert` 外还含 `web.config`，而 dsh-zh 的简单 manifest `hotMount` 只接受 `insert → id + name`。0.1.6-alpha.2 的 `hmr` 服务面已移除 `registerConfig`/`partialReload`/`stashed`，`src/self-hot-reload.ts` 的旧式自监视因此恒为 no-op；官方 watcher 也不覆盖 profile bundle（`ignored` 默认含 `**/node_modules`）。本插件 Host 保持单文件 bundle 仍有价值（入口 query 可靠刷新），但**不再有任何 host 热通道**。

## 不可破坏的约束

### 包与 client bundle

1. `package.json` 必须保留本包的 `dsh-zhipu` CLI、`.` / `./client` / `./cordis.patch.yml` / `./package.json` exports，以及 `dsh.bundle.patch` / `dsh.client` 声明。
2. `dsh.client.inject` 填客户端**包名依赖**，浏览器插件 `exports.inject` 填 Cordis **服务名**，二者不可混用。
3. `lib/client.js` 由本项目 Client 构建生成经典 `window.__ModuleLoader__.load(...)` bundle，不能手改。

### 挂载、注册与卸载

4. 行 id 不得复用：持久行 `dsh-zhipu`、临时热行 `dsh-zhipu-hot`、桥接行 `dsh-zhipu-bridge`。
5. `cordis.patch.yml` 必须解析为顶层数组；清空最后一个条目后写回 `[]`，不能只剩注释或 `null`。
6. 三通道迁移窗口中的预期重复注册可幂等跳过，但不能吞掉其它异常；桥接卸载按 `entry.options.id` 删除对应 Loader entry。
7. 桥接代码访问 `ctx.loader` 必须声明 `inject: ['loader']`；`loader.create({ id, name })` 的 `name` 必须是包名，不能是 file URL。

### 凭据与设置

8. `dshHome()` 的非空 `$DSH_HOME` → `~/.dsh` 回退不可删除；三层凭据解析的 API Key 永不写入配置、日志或错误信息。
9. 客户端只向 `plugins.bundle.config`（键为本包 npm 包名）注入本插件配置表单并使用官方 `configForms`（DSH 0.1.7+，接替已退役的 `settingsScope`）；不增加自定义网络请求、遥测或独立持久数据。

## 实现经验

### 运行时门禁（DSH 0.2.1 起）

- `package.json` 同时有 `engines.node` 与 `engines.dsh: ">=0.2.0-rc.2 <0.3.0-0"`。
  **`engines` 必须保持对象，`engines.dsh` 必须是字符串**：官方判定
  （`packages/boot/app-boot/src/plugin-compatibility.ts`）在 `engines` 非对象或
  `engines.dsh` 非字符串时直接抛错并拒绝该包。
- 一旦声明 `engines.dsh`，运行时判定**完全忽略** `@deepseek-ai/dsh*` 的 `peerDependencies`。
  本包本来就没有 dsh peer（全靠 profile 解析），所以这次只新增 `engines.dsh`。
- `engines.dsh` 里**不能**写 `workspace:*` / `workspace:^`（只有 peer 允许），否则判为不兼容。
- 官方判定带 `{ includePrerelease: true }`。手写 semver 判断若漏了它，`0.2.1-rc.2` 会被
  误判不兼容——"门禁写了却装不上"的典型原因。
- **负向验证不要用 `0.2.0-rc.2`**：它是 `>=0.2.0-rc.2` 的**下界本身**，必然通过。
  用 `0.1.7-alpha.2`、`0.2.0-rc.1` 或 `0.3.0` 验证拒绝。

验证（在 DSH 检出目录用 tsx 跑共享验证器，它直接调用官方实现，不是复刻逻辑）：

```sh
npx tsx <workspace>/temp/check-engines-dsh.mjs deepseek-harness-zhipu_plan_tools
npx tsx <workspace>/temp/check-engines-dsh.mjs deepseek-harness-zhipu_plan_tools --runtime 0.1.7-alpha.2
```

前者退出码 0（兼容 0.2.1-rc.2），后者退出码 1（不兼容），两者都过才证明门禁真的生效。

### Schema 与工具调用

- `src/settings-schema.ts` 的 `loadSchemastery` 从 **profile 的模块解析上下文**加载
  `@deepseek-ai/schemastery`（`createRequire(<profile>/package.json)`），本包不自带副本、也不在
  `package.json` 声明它——由 profile 供给。本机 `web` profile 实测解析到 3.18.4，`.volatile()`
  可用（`z.boolean().volatile()` → `meta.volatile = true`，`.get()` 引用正常）。代码里
  "3.18.2 无 `.volatile()`" 的 meta 回退分支因此是**防御性代码**，当前环境不会走到，不要删。
- 该函数自 DSH 0.1.7-rc 起为「`require.resolve` 只解析 + exports import 条目 + 文件 URL 异步
  `import` 预载（TLA）」：宿主组合批次经模块 hooks 管线并发 import ESM，同步 `require(esm)`
  （含 schemastery CJS 入口内部对 cosmokit 的 require）会撞「not yet fully loaded」且同步重试
  无效；预载失败才同步兜底，再失败降级为无 schema。
- 三个 `github_*` 和 scoped `web_search` 的 parameters/output 都使用纯 JSON Schema，不与 schemastery 简写混用；`required` 只放在对象层数组中。
- scoped `web_search` 只在原工具对目标 Agent 可见时建立阴影,并保持 1–4 查询、30 秒预算、轮询合并与同批失败取消;结果上限由本插件定为 **40**(= 上游单查询硬上限 10 条 × 4 查询,一次调用内全量透传,高于内置 tool-web 的 8),不能因改说明文字而退化执行契约;若下游要下调上限,注意超过该值时轮询合并会真实丢弃来源并置 `truncated`,用户会看到「来源列表已截断」。
- `repo_name` 的执行校验保持 `owner/repo`；历史展示可降级，但不能放宽实际 schema。
- 工具参数变更至少走一次 DSH 真实调用，确认实际注册的 schema，而不只验证 TypeScript。

### 回退传输

- `http-fallback` 不得退化为裸 `fetch(..., { redirect: 'follow' })`:必须保留 HTTP(S)/凭据/显式私网 URL 校验、同源重定向、超时、响应字节/字符上限与文本内容类型检查。
- 调用方取消与插件本地超时必须分开分类;search/reader 的 MCP 失败映射为 `WEB_PROVIDER_ERROR`,仓库工具使用 `ZHIPU_PROVIDER_ERROR`,结构化内容过滤保持 `ZHIPU_CONTENT_FILTERED`。

### 跨端设置

- 设置卡订阅建立后立即按当前快照渲染；host 与 client 各自维护默认值时，改名后全仓库搜索旧名和新名，避免六个设置字段跨端漂移。

### 安装与解析

- 持续开发使用 CLI 的 `--link <项目路径>`；`.npmrc` 的 `omit=peer` 用于避免 link 安装时在插件根生成第二份宿主 peer。
- 不在文档或脚本硬编码本机绝对路径。
- Open Design 使用独立 `open-design` profile；安装/验收不能以 `headless` 代替。其 probe/models/stdio stdout 是严格 JSONL，`src/index.ts` 的信息日志在该 profile 必须写 stderr，不能污染协议帧。

### 上游异常分类

宽泛搜索可能被上游以 `contentFilter` 拒绝，也可能返回 `No results found`。这不是判断协议参数错误的充分依据。实现只识别结构化过滤信号并映射稳定错误码；具体用户行为以 [`behavior.md#搜索工具的接管与说明替换`](behavior.md#搜索工具的接管与说明替换) 和 [`behavior.md#错误码速查`](behavior.md#错误码速查) 为准。
zread 上游对未收录/不存在的仓库在 `tools/call` 的 `isError` content 中返回双层 JSON 错误(内层 msg 含 "repo not found")。`mcp-http` 将其映射为 `ZHIPU_REPO_NOT_FOUND` 固定文案;正则漏检时安全降级为 `ZHIPU_PROVIDER_ERROR`,不放宽脱敏边界。
`github_*` 工具执行路径的错误消息(ZHIPU_DISABLED / ZHIPU_CREDENTIAL_MISSING / ZHIPU_PROVIDER_ERROR / ZHIPU_REPO_NOT_FOUND)随 `zhPrompt` 切换:默认英文,开启后中文;`zhPrompt` 经 `HttpOptions` 与 `resolveApiKey` 的 language 参数传入,search/reader 路径不传保持中文。
上游 `web_search_prime` 无结果条数参数(schema 实测,`additionalProperties:false`),单查询上限恒为 10 条,超量返回固定发生;搜索 provider 按 `request.maxResults` 预裁剪再返回,使 seam 的 `capSources` 不触发(seam 的 `truncated` 语义限定为 seam 自身丢弃来源,provider 预裁剪时报告 false)——对齐官方 Exa provider 的行为,UI 不再出现「来源列表已截断」。

## 测试策略

- **纯逻辑单测**：覆盖设置归一化、schema、凭据文件解析、patch 行、DeepSeek/HTTP 回退策略和搜索合并边界。
- **假 Context 装配测试**：验证 provider、Agent scope、动态装卸、HMR、可逆清理，以及 `open-design` profile 信息日志不进入 stdout。
- **mock MCP 端到端**：承接真实 initialize → initialized → tools/call → DELETE 请求形状，覆盖 search/reader 主路径、SSE/JSON、错误映射、取消、超时和清理。
- **构建契约验证**：检查产物存在、Node 语法、经典 client bundle 格式和 CLI usage。
- **真实管线验收**：Web 用现有 GUI；Open Design 用 `dsh --profile open-design --probe/--stdio` 检查 stdout 纯 JSONL，再检查新 `od-*` 会话首个 `request/header`。不得为验收重启 DSH。

## 验证分层

本插件提交审核前至少运行 `npm run typecheck`、`npm run build`、`npm test`、`npm run verify`；发布前再执行 [`release.md#发布前验证`](release.md#发布前验证) 的完整序列和打包检查。

若任一步失败，先定位失败原因；不要用重启、跳过检查或重复执行掩盖问题。

## 安全审计（2026-09）

项目专属要点（完整清单见工作区根 `docs/audit-2026-09.md`，勿在此复制）：

- 已确认高危：HTTP 回退无 DNS rebinding 防护（公网域名二次解析到内网不设防，需连接前解析并 pin IP）；duplicate 注册被当成功返回 no-op disposer（HMR/双行下旧 Fiber 卸载后 provider/tool 永久消失）。
- 中危：回退超时不覆盖 `credentials.resolve`（卡死占满并发槽）；MCP 错误把上游响应体原样进 error/cause（条件性凭据泄漏）；外部标题/正文/URL 直接拼 Markdown 工具输出（无不可信内容隔离）；CLI `--profile` 未校验。
- 正面范例（保持）：`self-hot-reload.ts` 全能力探测+静默降级；`web.config` 复杂 patch 不走 dsh-zh simple reconcile（安装只走项目 CLI 全量通道）。

## 桌面版 DSH 适配（2026-09-27）

- `src/settings-schema.ts`：`argvProfile()` 拆出纯函数 `profileNameFrom(argv, electronVersion)`——桌面 Host（Electron RunAsNode）argv 不带 `--profile`，`process.versions.electron` 有值时判 `desktop`；显式 `--profile` 仍最优先。
- `src/bin/cli/invocations.mts`：`runDshPlugin` 按名拒绝 `desktop` profile（含阻止 pnpm 兜底）；`src/bin/cli/main.mts`：desktop profile 未显式 `--port` 时默认 19387。
- 回归：`src/tests/settings-schema.test.mts`（profile 探测矩阵）+ `src/tests/patch-row.test.mts`（CLI 拦截 spawn 断言）。
- 共性事实与验收记录见工作区根 `docs/dsh-desktop-support.md`。
