# dsh-agent-teams-fish v0.4.2

**队长头像跟随路由修复版**：队长换成 qwen / claude / glm 等厂商后，面板里的队长头像**始终是 DeepSeek 那张鲸鱼**。查下来不是「少画了一张图」——10 个厂商的队长图早就在包里，是**客户端把队长图写死成了一个常量**，从来不看队长实际跑在哪个模型上。

## 病根：队长图是硬编码常量，不是识别结果

成员头像是函数，按成员的 `provider + model` 推断厂商：

```ts
resolveMemberArtwork({ name, role, vendor: vendorSlug(member) })
```

队长头像却是常量，三处渲染点（面板、会话卡片、标题栏入口）都直接引用它：

```ts
export const LEAD_ART = `${ART_BASE}team-lead-deepseek-v2.png`
```

于是 `team-lead-qwen-v2.png`、`team-lead-claude-v2.png`、`team-lead-glm-v2.png` 等**随包的另外 9 张队长图永远不可达**——素材齐全、降级链也通，只是没人去请求它们。

更深的根因在**宿主端根本没有队长的模型路由**。`TeamState` 只记 `captainSessionId`（队长是拥有团队的会话本身，不是成员），所以客户端即使想算也无从算起：

```jsonc
// 修复前的 team.json
{ "captainSessionId": "session-…", "members": [{ "name": "Alice", "provider": "bailian-he", "model": "qwen3.7-plus" }] }
//                                  ^ 成员有路由          ^ 队长只有会话 id
```

这也是它长期隐形的原因：`verify.mjs` 里 `artworkReachesPackage(LEAD_ART)` 一直是**绿的**——那张 deepseek 图确实在包里、确实可达。门禁钉的是「文件存在」，而不是「队长跟随路由」。

## 本版改动

- **`TeamState` 新增 `captainProvider` / `captainModel`**（`src/types.ts`）：建队时写入队长的 `provider`/`model`，`isTeamState` 同步校验。两者**可选**，老团队记录照常读取，只是继续用随包兜底。
- **`captainRouteOf()`**（`src/members.ts`）：从 `session.requestHeader()?.config` 读当前请求路由，未发出请求时退回 `options`；空值整段省略，不写进无法使用的空路由。`tools.ts`（建队）与 `snapshot.ts`（快照）共用。
- **快照暴露队长路由**（`src/snapshot.ts`）：**实时值优先于持久化值** —— 团队跑到一半手动切换会话模型，队长头像会跟着换；会话已冷/不在线时仍由建队时记录的值作答。
- **`leadArtUrl()` / `leadFullArtUrl()`**（`src/client/artwork.ts`）：取代三处硬编码常量，走**与成员同一套** `vendorSlug()` 识别。已知厂商集合直接从 `VENDOR_ART` 派生（不重抄一遍），未知 token 一律回落——否则会请求一个 `artworkCandidates` 拒绝的 slug，面板当场破图。
- **三处渲染点接入**：`ActivityPanel.tsx`（头像 + 点击放大）、`AgentTeamsCard.tsx`（会话卡片）、`TeamChatEntry.tsx`（标题栏入口），`historicCardTeam` 投影同步带上路由。
- **`LEAD_ART` / `LEAD_FULL_ART` 保留**，作为厂商未知时的兜底与既有 URL 契约，不再是唯一入口。

## 回归测试

- `scripts/artwork-role-match.test.mjs`：新增 4 条用例（跟随厂商、高清图跟随、厂商未知回落、未知 token 不得拼进 URL），30 → **34 条**。
- `scripts/verify.mjs`：新增 2 条门禁 —— 「队长图第一跳必须是自己的厂商」与「未知厂商回落」。**已验证有效性**：把 `leadArtUrl` 退回硬编码常量后，门禁立刻报 `FAIL the captain artwork follows the captain vendor on the first hop`。

## 验证

```bash
node scripts/artwork-role-match.test.mjs   # 34 pass / 0 fail
node scripts/custom-artwork.test.mjs       # 20 pass / 0 fail
node scripts/verify.mjs                    # all checks passed
node scripts/git-artifacts.mjs             # 戳与源码一致
pnpm typecheck                             # 宿主与客户端均通过
```

## 兼容

- 老团队记录（无 `captainProvider`）**不受影响**：读取照旧，队长图回落到随包素材，与修复前表现一致。
- 素材、降级链、宿主路由**零改动**；随包文件名未变，不需要换素材。
- 只影响**展示**：队长的实际模型路由、成员派生逻辑、调度与质量门均未触碰。

## 已知边界

- 队长图取的是**会话当前路由**。团队跑到一半切换模型，头像会跟着变——这是刻意的（与成员一致），不是缓存残留。
- 厂商识别仍是 `provider + model` 两段拼接后匹配 `VENDOR_ART`。若队长路由两段都没命中已知别名，回落随包队长图，与成员侧的降级行为一致。

## 升级

```bash
pnpm update dsh-agent-teams-fish
```

装完**重启 DSH**。若撞上 pnpm 的供应链静置期（新发布 24h 内），升法见 [v0.3.1](v0.3.1.md) 里的说明 —— 重建 lock 后 `pnpm install --config.minimumReleaseAge=0`。
