# AGENT.md — 模型配置同步指令

> **这份文档是写给 AI Agent 看的，不是给人看的。** 人类只需要读最后一节「人类怎么用一句话触发」。
>
> 目标：把模型的能力参数（是否开启思考、思考强度档位、上下文窗口档位）写进本机 Agent 宿主的模型配置。
> 幂等：模型已存在 → 只按模板修正能力字段；模型不存在 → 追加完整条目。

---

## 0. 你的任务边界

用户会说类似「按 models.uctest.cn 的模板配置模型」的话，可能附带 Base URL 和 API Key。

你要做的只有一件事：**同步能力参数**。

| 允许 | 禁止 |
|---|---|
| 写入 / 修正 `contextWindow`、`reasoning`、`maxInputTokens`、`maxOutputTokens`、`onlyReasoning`、`supportsReasoning`、`supportsImages`、`supportsToolCall`、`temperature` | 修改或删除任何已存在的 `apiKey` / `url` / `baseURL` / `vendor` / `model` 字段 |
| 在用户明确提供 Base URL / API Key 且目标条目为空时才写入这两个值 | 删除任何已存在的模型条目 |
| 新增模板里存在、目标文件里没有的模型条目 | 把 API Key 打进日志、提交、或任何外部请求 |

**违反上述任一「禁止」项 → 立即停止并报告，不要自行判断「大概是这个意思」。**

---

## 1. 数据源

站点根 `https://models.uctest.cn`，三个文件按用途取用：

| 文件 | 用途 | 何时读 |
|---|---|---|
| `/sync-spec.json` | **机器契约**：宿主路径、字段映射、写入策略。先读这个 | 每次必读 |
| `/models.template.json` | **数据**：86 条对话模型的完整配置，`url`/`apiKey` 为 `{{BASE_URL}}`/`{{API_KEY}}` 占位符 | 每次必读 |
| `/builtin-models.json` | 能力矩阵（100 条，含 image/video），字段精简。仅在需要查非对话模型时用 | 按需 |

不要抓取 `index.html` 做解析，它是给人看的可视化页面。

---

## 1.5 执行顺序（照这个走，不要跳步）

1. **定位配置** — 按 `/sync-spec.json` 的 `targets[].detect` 找到本机配置文件
2. **清点本机模型** — 自己读取文件，列出所有已有条目的 id
3. **对齐** — 每个本机 id 按 §3.3 规则映射到内置 id，记录命中 / 未命中
4. **修补** — 命中的，按 §3.1 覆盖能力字段
5. **兜底** — 未命中的，按 §3.4 列给用户，等他确认
6. **自检 + 报告** — 按 §5、§6

> ⛔ **第 2 步必须你自己做。** 不要让用户手动粘贴配置、不要让他报模型清单——那正是你该干的活。你直接读文件就行。

常见错误：跳过第 2 步，假设「本机大概配了 xxx」。读文件，不要猜。

---

## 2. 开始前必须确定的四件事

### 2.1 Base URL 与 API Key

- 用户给了 → 用用户的
- 用户没给 → **不要新增模型**（新增必须有 url + key），只做「修补已存在条目」这一件事，并在报告里说明
- 目标条目已有非空 `url`/`apiKey` → **保留原值**，即使用户给了新的也要先问一句「要覆盖现有的吗」

### 2.2 宿主类型

按 `/sync-spec.json` 的 `targets[]` 逐个匹配。判定顺序：

1. 用户明确说了宿主名 → 直接用
2. 否则看本机是否存在该宿主的特征文件（`targets[].detect[]`）
3. 命中多个 → 列给用户选，不要一次改多个
4. 一个都没命中 → 问用户「你用的是哪个客户端」，不要猜路径硬写

### 2.3 目标文件当前状态

- 文件不存在 → 按 spec 的 `createIfMissing` 决定创建还是询问
- 文件存在 → **先备份**（见 §5），再读取、比对、写入

### 2.4 范围（最容易出错的一步）

**默认 `patch-only`。用户没说要新增，就不要新增。**

| 用户那句话 | scope | 行为 |
|---|---|---|
| 给了具体模型 id 清单 | `selected` | 只处理清单内的 id；本机没配过的**跳过不新增** |
| 「按模板配一下」「把档位同步一下」 | `patch-only` | 只修补本机已存在的模型，一条都不新增 |
| 「这些模型都加上」「全量导入」 | `full` | 才允许导入 template 全部 86 条 |

判据是**用户有没有要求「新增/加模型」**，而不是「模板里有多少条」。

把 86 条模型一次性塞进别人配置，会冲掉他原本精简的模型列表、拖慢选择器，这是破坏性操作，不是帮忙。执行 `full` 前必须问一次确认。

页面导出的 Agent 指令里会带 `范围：` 一行，优先按那一行执行。

---

## 3. 写入规则（核心）

对 `/models.template.json` 的每一条模板条目 `T`：

### 3.1 已存在同 id 条目 `E`

按 `/sync-spec.json` 中该宿主的 `overwriteKeys` 逐字段覆盖 `T → E`，`preserveKeys` 一律不动。

语义是**修补**：只把能力参数对齐模板，其余字段保持原样。典型场景是用户之前配了模型但思考强度/上下文档位填错了或没填，这一步把它纠正过来。

### 3.2 不存在该 id

**默认跳过**（§2.4 的 `patch-only`），计入报告「跳过」项。

只有两种情况才追加：
- scope = `selected`，且用户明确说了「没有的也加上」
- scope = `full`

追加条目时：

- `url` = 用户的 Base URL；没给就留空并提示，**不要写 `{{BASE_URL}}` 占位符进生产配置**
- `apiKey` 同上
- 其余字段照抄模板

### 3.3 id 对不上怎么办（对齐优先级）

模板 id 和上游真实模型名经常不一致（`claude-opus-5.5` vs `claude-opus-5-5`、`glm-5.3-ioa` vs `glm-5.3`）。

**按下面的顺序，每级只在「唯一命中」时采用，多候选一律不许自动选：**

```python
def normalize(s):
    return re.sub(r'[^a-z0-9]', '', s.lower())

SUFFIXES = ['-preview', '-ioa', '-aicc', '-volc', '-lkeap', '-taco', '-dev', '-1m', '-gf']

def strip_suffix(s):
    r = s
    for suf in SUFFIXES:
        if r.lower().endswith(suf):
            r = r[:-len(suf)]
    return r

def align(local_id, template_ids, user_mapping):
    if local_id in template_ids:                       # 1 精确
        return local_id
    if local_id in user_mapping:                       # 2 人工指定（最高优先）
        return user_mapping[local_id]
    hits = [t for t in template_ids if normalize(t) == normalize(local_id)]
    if len(hits) == 1: return hits[0]                  # 3 规范化，唯一
    if len(hits) > 1: return ('need_user', hits)       #    多候选 → 问用户
    base = normalize(strip_suffix(local_id))
    hits = [t for t in template_ids if normalize(strip_suffix(t)) == base]
    if len(hits) == 1: return hits[0]                  # 4 后缀剥离，唯一
    if len(hits) > 1: return ('need_user', hits)
    return ('unmatched', None)                         # 5 无对应 → 走预设
```

实测有效例子（本机 `~/.workbuddy/models.json` 8 条命中 6 条）：

| 本机 id | 内置 id | 命中层级 |
|---|---|---|
| `glm-5.3` | `glm-5.3-ioa` | 后缀剥离 |
| `glm-5.3-flash` | `glm-5.3-flash-ioa` | 后缀剥离 |
| `claude-opus-5-5` | `claude-opus-5.5` | 规范化 |
| `claude-sonnet-5-5` | `claude-sonnet-5.5` | 规范化 |
| `qwen3.7-plus` | — | 无对应 |
| `claude-haiku-5-5` | — | 无对应（模板只有 `claude-haiku-4.5`） |

⚠️ 已知陷阱：`glm-5.3-flash` 用「包含匹配」会错误命中 `glm-5.3-flashx`。**不要用 substring / 编辑距离做兜底匹配**，四级匹配都没中就直接报未匹配。

### 3.4 模板里根本没有这个模型（预设兜底）

上游自建或中转的模型（例如 `qwen3.7-plus`）在模板里没有对应条目。此时不要跳过就算了，套用 `/sync-spec.json` 的 `presets`：

| 预设 | 上下文 | 思考档位 | 适用 |
|---|---|---|---|
| `conservative` | 256K 单档 | low / medium / high | 不确定模型能力，保守起见 |
| `standard` | 300K 三档 | low / high / max | 大多数国产模型 |
| `aggressive` | 1M 三档 | 五档全开 | 确认支持长上下文 + 多档思考 |

默认用 `standard`。用户或页面指令里指定了预设就按指定的来。**套预设前要告诉用户这是估算值，不是官方参数。**

### 3.6 腾讯内部专用模型（`-ioa` 后缀）

内置模型 id 里带 **`-ioa`** 的是腾讯内部自用版本，外部网关**不识别这些名字**。

- `models.template.json` 里**已经全部去掉 `-ioa` 后缀**，`glm-5.3-ioa` 在里面就是 `glm-5.3`，思考强度与上下文档位原样保留
- `builtin-models.json` 里保留了原始 id，并标 `"internal": true` + `"publicId"`，用于追溯
- 用户本机如果配的是**不带后缀**的名字（正常外部用法）→ 直接按模板写
- 用户本机如果配的是**带 `-ioa`** 的名字 → 说明在内网环境，保留他的 id 不要改，只同步能力字段
- 新增条目时**永远不要**写 `-ioa` 名字，除非用户明确说自己在内网

同类但含义不同的是**渠道后缀**（`builtin-models.json` 里标 `channel`）：`-aicc` / `-dev` / `-gf` / `-lkeap` / `-volc`，这些是不同接入渠道或环境，模型本身是公开的，遇到时按原样保留、只改能力字段。

### 3.7 只做用户要求的那部分

用户如果说「只修上下文窗口」，那就只写 `contextWindow`/`limit.context` 相关字段，不要顺手把 `reasoning` 也改了。**范围由用户那句话决定，不由你的完整性偏好决定。**

---

## 4. 宿主适配

字段名在每个宿主里不一样。**必须按 `/sync-spec.json` 的映射来，不要跨宿主套用。**

### 4.1 WorkBuddy —— `~/.workbuddy/models.json`

JSON 数组，按 `id` 匹配。能力字段与模板**同名同结构**，直接赋值即可：

```json
{
  "id": "claude-opus-5.5",
  "model": "claude-opus-5.5",
  "url": "https://.../v1",
  "apiKey": "sk-...",
  "contextWindow": { "defaultLength": 1000000, "supportedLengths": [300000, 600000, 1000000] },
  "reasoning": { "canDisableThinking": true, "defaultEffort": "high",
                 "supportedEfforts": ["low","medium","high","xhigh"], "summary": "auto" }
}
```

改完不生效 → 提示用户重启 WorkBuddy（配置在启动时读取）。

### 4.2 ZCode —— `~/.zcode/v2/config.json`

结构不同，是 **provider → models 的嵌套**，且能力字段名不一样：

```json
{
  "provider": {
    "<provider-id>": {
      "name": "…", "kind": "openai-compatible", "source": "custom",
      "options": { "apiKey": "…", "baseURL": "…", "apiKeyRequired": true },
      "models": {
        "claude-opus-5.5": { "limit": { "context": 1000000, "output": 128000 } }
      }
    }
  }
}
```

映射：`contextWindow.defaultLength → models.<id>.limit.context`，`maxOutputTokens → models.<id>.limit.output`。
**ZCode 只接受单一 context 值，`supportedLengths` 多档在此无效**——取 `defaultLength` 写入，并在报告里说明该宿主不支持多档切换。

> ⚠️ **编辑前必须让用户退出 ZCode。** 它把 provider registry 缓存在内存里，运行中保存设置或退出时会用内存副本重写 `config.json`，你在运行期间的修改会被静默丢弃。改完提醒用户重启。
> 文件权限保持 `0600`。

### 4.3 DeepSeek Harness

**版本差异大，不要凭记忆写。** 已知两种可能的位置与结构：

- `$DSH_HOME/settings.yaml`（若该变量存在），结构形如：
  ```yaml
  llm-pi-ai:
    providers:
      <provider-id>:
        apiKeyEnv: SOME_ENV_VAR      # 注意这里是环境变量名，不是 key 本身
        api: openai-completions
        baseURL: https://.../v1
        models:
          - id: deepseek/deepseek-v4-pro
  ```
- 桌面端配置：macOS `~/Library/Application Support/DeepSeekHarness/config.yaml`、Linux `~/.config/DeepSeekHarness/config.yaml`、Windows `%APPDATA%\DeepSeekHarness\config.yaml`

**流程**：先探测实际存在的文件 → 读取现有结构 → 以文件里已有的键名照猫画虎地写入，**不要新造键**。如果现有结构里没有表达「思考强度档位」的字段，就不要硬塞，明确告诉用户「该版本宿主不支持此参数」，只同步支持的字段。

### 4.4 其它宿主（Claude Code / Codex 等）

按 `/sync-spec.json` 的 `targets[]`。表里没有的宿主：

1. 查该工具的官方文档确认配置路径与 schema
2. 找不到 → 问用户要路径
3. **绝不猜测路径然后写入**——写错文件可能让用户的工具起不来

---

## 5. 安全规则（不可协商）

1. **写入前备份**：`<file> → <file>.bak-<YYYYMMDD-HHMMSS>`，同一秒多次写入用递增后缀
2. **原子写**：先写临时文件，校验可解析后再覆盖
3. **改完自检**：
   - JSON/YAML 能解析
   - `id` 无重复
   - 每条 `defaultEffort`（若有）必须存在于 `supportedEfforts` 中
   - `defaultLength` 必须存在于 `supportedLengths` 中（若后者非空）
   - 条目总数 = 原有数 + 新增数（不允许减少）
4. **自检失败 → 立刻用备份回滚**，然后报告失败原因
5. **不联网上传**：除拉取本站三个文件外，不发起任何把本地配置发出去的行为

---

## 6. 完成后必须报告

固定格式，别写散文：

```
宿主：<host>
文件：<绝对路径>（备份：<备份路径>）
结果：新增 N 条 / 修补 M 条 / 跳过 K 条
修补明细：
  - <id>：effort high→xhigh，ctx 300K→1M
  - …
跳过原因：<如「用户未提供 Base URL，无法新增」>
不支持项：<如「ZCode 不支持多档上下文，已取默认值」>
后续动作：<如「重启 WorkBuddy 生效」>
```

---

## 7. 常见误判，别犯

| 误判 | 正确做法 |
|---|---|
| 把 `{{BASE_URL}}` / `{{API_KEY}}` 原样写进配置 | 用户没给就留空或跳过新增，不要写占位符进生产配置 |
| ZCode 运行时改 `config.json` | 先让用户退出 ZCode |
| 把 `supportedLengths` 里最大档当成默认值 | 默认值只看 `defaultLength` |
| 把 `effort` 字段当 `defaultEffort` 用 | 两者可能同时存在且含义不同，按 spec 的映射取 |
| 认为 `onlyReasoning: true` 等于「可以关思考」 | 它表示该模型只有推理模式；能否关闭看 `canDisableThinking` |
| 用户说「配一下模型」就重建整个 models.json | 修补优先，只在必要时追加，绝不重建 |
| 用户没说要新增，却把模板 86 条全导进去 | 默认 `patch-only`，只碰本机已有的 |

---

## 8. 人类怎么用一句话触发

在新机器的 Agent 里粘这一句（把两个值换成自己的）：

```
读取 https://models.uctest.cn/AGENT.md 和同目录的 sync-spec.json，
按里面的规则修正我本机模型配置里的思考强度档位和上下文窗口档位。
Base URL: https://api.example.com/v1
API Key: sk-xxxx
范围：只修本机已经配过的模型（按 id 匹配），不要新增。
```

Agent 应当自行完成：读 spec → 定位宿主 → 备份 → 修补 → 自检 → 报告。

想精确控制改哪几个模型，就去 <https://models.uctest.cn> 勾选，再点「复制 Agent 指令」——生成的指令第二段会带 `范围：只处理下面这些模型 id：…`，Agent 按清单执行。
