# [IMPL-RPT-031] 多视角模型配置：单一目录图谱 → 八视角分组

> 实现报告 · 遵循 [MGMT-STD-001] 文档铁律 · 对应设计归 `docs/03_DESIGN_设计/`，本目录索引见 `00_INDEX_实现-分类索引.md`

- **日期**：2026-06-20
- **范围**：同一份「供应商 / 模型 / Key」数据，从多个维度查看与管理；CLI + Web 双端，全局 + 每用户双平面。
- **状态**：定稿（后端 36 + 回归 40 绿，每用户 8 绿，前端 build 绿；未提交）

## 一、背景与目标

此前模型配置只有**单一视角**：按适配器/供应商分组。用户要求按多个维度组织同一份数据：

1. 按模型 → 下面有供应商
2. 按供应商 → 下面有它的模型（已存在）
3. 按 Key → 下面是该 Key 支持的模型
4. 其它（按需补充）
5. 按能力类型：文本 / 语音 / 图片 / 视频

确认范围：CLI + Web 双端、全局 + 每用户双平面、**查看 + 视角内管理**（非只读）；额外视角全要：按档位 T0–T3 / 按健康状态 / 按连接模式 / 全局搜索扁平过滤。共**八视角**。

根因复杂点：数据被拆在三个全局存储（`custom_providers.json` + `api_keys.json` + 若干 env map）+ 每用户 DB 平面；**模型不是一等公民**（仅 `provider.models[]` 字符串 + 实时 `/v1/models` + overrides）；**Key 只绑 provider 不绑 model**；图像/视频模型**不在** provider 注册表里，只活在 `KHY_IMAGE_GEN_AGNES_*` / `KHY_VIDEO_GEN_AGNES_*` env 命名空间（见 [IMPL-RPT-030]）。

## 二、核心设计：单一读层，八视角全是纯 group-by

**一处 join，绝不建八个独立读取器。** 新建「目录图谱」模块产出扁平 edge 列表，所有视角都从这一份列表派生；新增**一个**后端 catalog 端点供 CLI 与 Web 共享，杜绝漂移。管理动作**复用现有**增删改路径，图谱只读。

Edge 形状（全局与每用户同形）：

```
{ provider, providerLabel, model, keyIds[], keyCount, capability,
  tier, status, connectionMode, isDefault, source }
```

**by-key 关系**：v1 采用**派生**——Key 支持的模型 = Key 所属 provider 的模型（零存储改动）；按 Key 加白名单（per-key allow-list）记为 future。

## 三、实现内容

### 3.1 后端图谱与分类器（全局平面）

- `services/backend/src/services/gateway/modelCapability.js`（新）：把 modelId 分类为 `text|audio|image|video`，与 `modelTier`（T0–T3）正交。解析顺序：来源提示 → env 覆盖 `KHY_MODEL_CAPABILITY_MAP` → 正则兜底（video 先于 image）→ 默认 text。env 缓存照搬 `modelTier.js` 写法。
- `services/backend/src/services/gateway/modelCatalogGraph.js`（新）：唯一 join。`buildCatalogGraph({live})` 合并 `customProviderRegistry.listProviders()` + 池中有注册表无的 builtin + `apiKeyPool.getPoolStatus` + `modelTier` + `modelCapability` + `imageGenService.catalogModels()` + `videoGenService.catalogModels()`。`status` 聚合 Key（任一 active→active，否则 cooldown，否则 disabled）；`connectionMode` 优先级：有池 Key→account-pool，否则 proxy 路由无 Key→proxy，否则 direct。`live=true` 复用 `apiAdapter.listModels()` 解析 `api:<poolKey>:<model>`。
- `services/backend/src/services/gateway/modelCatalogPivots.js`（新）：纯 group-by，八视角一次定义，CLI 与测试共用。
- `imageGenService.js` / `videoGenService.js`：各新增 `catalogModels()`，把 env 命名空间里的图像/视频模型暴露给图谱——**这是 by-capability 唯一能看到图像/视频的途径**。
- `apiAdapter.js`：导出 `getPoolDefaultModelMap`。
- 端点：`aiManagementServer.js` 新增 `handleAiGatewayCatalog`，在原始 dispatcher 只插一行路由 `GET /api/ai-gateway/catalog`，纯 dump。

**关键修正**：图谱对 chat 注册表模型**不再**强制 `source:'text'`。注册表经对话端点暴露，但其中可能列出真正的多模态模型（如 `<name>-image`）；放开后由 env 覆盖 + 正则 + 默认链按其**本质**分类，而非按暴露它的端点分类。来源提示仅保留给图像/视频 service（那里来源才是权威）。

### 3.2 CLI（全局）

`services/backend/src/cli/handlers/gateway.js`：`handleGatewayModels` 的 list 路径新增 `--view <…>` + `--search <q>`；`_renderCatalogView` 调图谱 + pivot，每组打印 section header + 表格（列：供应商/模型/能力/档位/状态/连接/Keys），`--json` 返回 `{ ok, view, search, groups, sources }`。`list/add/remove` 动词不动。

**子命令注册修复**：`constants/commandSchema.js` 的 gateway 子命令数组原缺 `models`（只有单数 `model`），导致 `gateway models` 落到默认 status 分支。已补 `'models'`。

### 3.3 Web（全局）`apps/ai-frontend/src/views/AIGateway.vue`

- `composables/useModelPivots.js`（新）：后端 `modelCatalogPivots.js` 的**忠实 JS 镜像**——同八视角、同 group-by、同标签映射。前后端各保留一份单源，杜绝漂移。
- `composables/useGateway.js`：加 `fetchCatalog()` 调 `/api/ai-gateway/catalog`，存 `catalogEdges` / `catalogSources`，并入 `fetchAll`。
- `AIGateway.vue`：加分段控件（八视角）+ 搜索框。`by-provider` 且无搜索时**保留**原有富管理卡片（逐模型验证/改名/隐藏/设默认字节级不变）；其它视角或搜索时切到轻量 pivot 渲染器，每条 edge 展示真实解析出的供应商/能力/档位/状态/连接/Key 数。

### 3.4 每用户平面（多租户）

- `services/ai-backend/src/services/gateway/userModelCatalogGraph.js`（新）：全局图谱的每用户孪生，产出同形 edge。**数据模型现实**：每用户平面**不存模型列表**——一个用户至多一条 relay 上游（单模型）+ 若干 `UserProvider`（provider + key + baseUrl，**无模型**）。故诚实派生：relay 单模型 → 一条 edge；每个 `UserProvider` Key → 一条 provider 级 edge（模型留空，**绝不臆造**，请求时再解析）。能力/档位分类器从 backend 包 **import 复用**（两模块纯函数零依赖，避免跨包重复）。live `/v1/models` 发现记为 future。
- 端点：`services/ai-backend/src/routes/userGateway.js` 加 `GET /api/user-gateway/catalog`（`authenticateToken`，scope `req.user.id`，Key 脱敏只露不透明 id）。
- Web：`MyGateway.vue` 加「我的模型目录」卡片（分段控件 + 搜索），经 `useUserGateway.js` 的 `fetchCatalog()` 取数，**共用**全局同一个 `useModelPivots.js`。

## 四、零硬编码与状态透明

- 无任何 Key/URL/模型写死：供应商/Key/模型/URL/档位全部来自注册表、Key 池、env map、图像/视频 service 自身的 env 解析器。
- 每个视图都报告**真实解析**出的后端/供应商/Key；图谱返回 `sources` 块（实际读了什么、是否 live），部分态浮现而非隐藏。
- 每用户平面只用脱敏快照，绝不返回解密 Key 材料；模型未配置时留空并以「—」呈现，绝不编造模型名。
- 铁律延续：图像/视频模型**绝不**进入 `PROXY_MODEL_ROUTE_MAP`（[IMPL-RPT-030]）。

## 五、测试

后端（`node ~/Khy-OS/node_modules/jest/bin/jest.js`，从 `services/backend`）：

| 测试文件 | 用例 | 结果 |
| --- | --- | --- |
| tests/modelCapability.test.js | 来源/Agnes 三元组/env/正则/默认 | 绿 |
| tests/modelCatalogGraph.test.js | join/图像视频浮现/by-key 派生/档位/状态/连接 | 绿 |
| tests/modelCatalogPivots.test.js | 八视角 + flat 搜索 | 绿 |
| tests/gatewayModelsView.test.js | CLI 各 `--view` 渲染 + JSON + 旧路径不触图谱 | 绿 |
| tests/agnesProvisioner / customProviderRegistrar / agnesImage / agnesVideo（回归） | 40 | 绿 |

合计后端 36 新 + 40 回归绿。每用户（从 `services/ai-backend`）：

| 测试文件 | 用例 | 结果 |
| --- | --- | --- |
| test/userModelCatalogGraph.test.js | relay/provider 派生/标签/租户隔离/透明 | 5 绿 |
| test/userGatewayCatalog.route.test.js | 端点可达/鉴权/租户隔离 | 3 绿 |
| test/userGateway.routes.test.js（回归） | 8 | 绿 |

前端 `apps/ai-frontend` `npm run build` 绿。CLI 手验 `gateway models --view by-model/by-key/by-capability/by-tier/--view flat --search …` + `--json` 均出正确分组。

## 六、跨分类关联

- 单一读层：`modelCatalogGraph.js`（全局）/ `userModelCatalogGraph.js`（每用户）；纯分组：`modelCatalogPivots.js`（后端）/ `useModelPivots.js`（前端）。
- 图像/视频出注册表来源：[IMPL-RPT-030] Agnes 四模型一键置备。
- 环境持久化：`services/backend/src/services/gatewayEnvFile.js`。
- 文档总入口：`docs/00_INDEX_文档索引.md`；本目录索引：`00_INDEX_实现-分类索引.md`。
