mingdao-harness 0.6.16 → 0.6.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/CONFIG.md +102 -0
- package/package.json +1 -1
- package/src/config.js +16 -3
- package/src/model-discovery.js +26 -4
- package/src/models.js +94 -0
- package/src/web/app.js +946 -94
- package/src/web/index.html +399 -41
package/docs/CONFIG.md
CHANGED
|
@@ -379,6 +379,9 @@ completion 计费,防止推理吃满上限时空轮白烧)、`compactTrigger
|
|
|
379
379
|
`deepseek-v4-flash` → `deepseek-flash` → `DeepSeek-V4.1-Flash 1017` → 未来的 V4.2/V5)
|
|
380
380
|
**不需要改代码**:模型名一律以 `GET /v1/models` 实时返回的官方名为准。
|
|
381
381
|
|
|
382
|
+
> 官方**宣传名**(如 `DeepSeek-V4.1-Flash 1017`)与实际 API id(如 `deepseek-flash`)不一致时,
|
|
383
|
+
> **不要**把它写进请求:用下面的「显示别名 `config.modelLabels`」,请求仍用 API id。
|
|
384
|
+
|
|
382
385
|
### 家族模式表
|
|
383
386
|
|
|
384
387
|
`src/models.js` 的 `MODEL_FAMILIES` 用正则匹配名字,能力(上下文窗口、单次输出上限、工具调用、
|
|
@@ -423,6 +426,105 @@ completion 计费,防止推理吃满上限时空轮白烧)、`compactTrigger
|
|
|
423
426
|
- 完全不属于任何家族的线上新模型(例如非 DeepSeek 命名)仍会出现在列表里,可直接选中;
|
|
424
427
|
能力未知时按远程模型保守默认(上下文 128k)估算,可用 `customModels` 显式覆盖。
|
|
425
428
|
|
|
429
|
+
### 显示别名 `config.modelLabels`(v0.6.20,§3.69)
|
|
430
|
+
|
|
431
|
+
**厂家的「官方宣传名 / 构建名」不等于 API id,不能拿去请求。** 实测(2026-10,真机):
|
|
432
|
+
|
|
433
|
+
```text
|
|
434
|
+
GET https://api.deepseek.com/v1/models
|
|
435
|
+
→ ["deepseek-flash", "deepseek-v4-pro"]
|
|
436
|
+
|
|
437
|
+
POST /v1/chat/completions {"model":"deepseek-flash"}
|
|
438
|
+
→ 200(响应 model = "deepseek-flash")
|
|
439
|
+
|
|
440
|
+
POST /v1/chat/completions {"model":"DeepSeek-V4.1-Flash 1017"}
|
|
441
|
+
→ 400 {"message":"The supported API model names are deepseek-flash, deepseek-v4-pro,
|
|
442
|
+
but you passed DeepSeek-V4.1-Flash 1017."}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
也就是说:`DeepSeek-V4.1-Flash 1017` **既不会出现在 `/models` 里,也不能用于请求**。
|
|
446
|
+
界面想显示官方宣传名,只能用「显示别名」——它**只影响渲染**,绝不参与出网请求的
|
|
447
|
+
`model` 字段(`config.model` / `config.routing.*` / 请求体 `model` 始终是 API id)。
|
|
448
|
+
|
|
449
|
+
```json
|
|
450
|
+
{
|
|
451
|
+
"model": "deepseek-flash",
|
|
452
|
+
"modelLabels": {
|
|
453
|
+
"deepseek-flash": "DeepSeek-V4.1-Flash 1017",
|
|
454
|
+
"deepseek-v4-pro": "DeepSeek-V4.1-Pro 1017"
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
校验规则(`src/models.js` 的 `normalizeModelLabels`,在 `loadConfig()` 时执行):
|
|
460
|
+
|
|
461
|
+
| 规则 | 非法时的行为 |
|
|
462
|
+
| --- | --- |
|
|
463
|
+
| `modelLabels` 必须是对象(不能是数组 / null / 字符串) | 整个字段忽略 + `console.warn` |
|
|
464
|
+
| key(API id)去空白后必须非空 | 该项忽略 + `console.warn` |
|
|
465
|
+
| value 必须是字符串、去空白后必须非空 | 该项忽略 + `console.warn` |
|
|
466
|
+
| value 长度 ≤ 80 字符(`MODEL_LABEL_MAX_LENGTH`) | 该项忽略 + `console.warn` |
|
|
467
|
+
| key 为 `__proto__` / `constructor` / `prototype` | 该项忽略 + `console.warn`(原型污染防线) |
|
|
468
|
+
|
|
469
|
+
非法项**不会阻断启动**(配置写坏一个字不该让应用起不来);同一非法项每个进程只提醒一次。
|
|
470
|
+
合法映射的值会去掉首尾空白后生效。
|
|
471
|
+
|
|
472
|
+
接口形状(`GET /api/models` 与 `GET /api/state` 的 `models[]`,**服务商线上模型**条目):
|
|
473
|
+
|
|
474
|
+
| 字段 | 含义 |
|
|
475
|
+
| --- | --- |
|
|
476
|
+
| `id` | API 实际 id(出网请求用它) |
|
|
477
|
+
| `name` | 同 `id`(保留给现有前端的选择 value,选择逻辑不变) |
|
|
478
|
+
| `label` | 显示名:`modelLabels[id]`;没有映射时**回退 `id`** |
|
|
479
|
+
| `labelSource` | 仅当 `label !== id` 时为 `"config"`(显示名来自 `config.modelLabels`) |
|
|
480
|
+
| `detail` | 能力 / 来源说明(原来的组合标签:离线兜底、能力未知、来源标注) |
|
|
481
|
+
|
|
482
|
+
`config.customModels` 的条目保持既有的「名字 — 用户标签(来源)」展示——它们不是官方
|
|
483
|
+
API id,不参与 `modelLabels`(其条目仍带 `id`,等于自定义模型名)。
|
|
484
|
+
|
|
485
|
+
回归:`test/model-names.js` 第 ⑧ 组断言 + `test/mutate/batch37-model-labels.mjs`(10/10 全中)。
|
|
486
|
+
|
|
487
|
+
### TODO(UI:显示别名落地,待另一道 / 下一轮实现;本次不改 UI 文件)
|
|
488
|
+
|
|
489
|
+
> 本次(v0.6.20 §3.69)只做内核 / 接口 / 文档;`src/web/app.js`、`src/web/index.html` 由并行
|
|
490
|
+
> UI 线持有(P1 已合入 `878d4cb`),故以下三处**尚未落地**。行号为 `878d4cb` 时;
|
|
491
|
+
> 后续改动会漂移,请以 `o.value=m.name; o.textContent=m.name; o.title=m.label` 等代码锚点为准。
|
|
492
|
+
|
|
493
|
+
1. **模型选择器显示 `label`、`title` 显示 API id**(只改显示,不改选择逻辑)
|
|
494
|
+
- 位置:`src/web/app.js` 两处重复的选项渲染(`:1859` 与 `:2189`),锚点
|
|
495
|
+
`o.value=m.name; o.textContent=m.name; o.title=m.label;`。
|
|
496
|
+
- 建议改法:
|
|
497
|
+
```js
|
|
498
|
+
const id = m.id || m.name;
|
|
499
|
+
const label = m.label || id;
|
|
500
|
+
const o = document.createElement('option');
|
|
501
|
+
o.value = id; // 选择 / 请求仍用 API id
|
|
502
|
+
o.textContent = label; // 界面显示官方宣传名
|
|
503
|
+
o.title = 'API id: ' + id + (m.detail ? ' · ' + m.detail : '');
|
|
504
|
+
if (id === j.model) o.selected = true;
|
|
505
|
+
```
|
|
506
|
+
- 期望效果:下拉显示 `DeepSeek-V4.1-Flash 1017`;悬浮提示能对上
|
|
507
|
+
`API id: deepseek-flash · (能力/来源说明)`;选择后提交的仍是 id。
|
|
508
|
+
2. **会话设置一行小字(仅当 `label !== id` 时出现)**
|
|
509
|
+
- `src/web/index.html`:会话设置的模型行(`:653` 已有 `ssModelLabel` / `ssModelId`),
|
|
510
|
+
在 `ssModelSrc` 后加:
|
|
511
|
+
```html
|
|
512
|
+
<div class="ss-src" id="ssModelAliasHint" hidden>显示名来自 config.modelLabels,请求使用 API id。</div>
|
|
513
|
+
```
|
|
514
|
+
- `src/web/app.js`:`renderSessionSettings()` 中 `set('#ssModelId', ...)` 附近加:
|
|
515
|
+
```js
|
|
516
|
+
const hint = $('#ssModelAliasHint');
|
|
517
|
+
if (hint) hint.hidden = !(md.label && md.label !== md.name);
|
|
518
|
+
```
|
|
519
|
+
(`md` 即已有的 `modelDisplay(当前模型条目)`,`app.js:966`。)
|
|
520
|
+
- 期望效果:配了映射的模型显示这行说明,没配时不出现。
|
|
521
|
+
3. **设置页说明文案(不是新表单)**
|
|
522
|
+
- `src/web/index.html`:`模型与 API Key` 标题下(`:757`)加一条
|
|
523
|
+
`cfg-hint`(带 `data-kw` 便于设置搜索命中,不新增任何输入框):
|
|
524
|
+
```html
|
|
525
|
+
<div class="cfg-hint" data-kw="模型 id 官方名 宣传名 modelLabels 显示名">模型 id 以 API 返回为准;官方宣传名可在 <code>config.modelLabels</code> 里映射为显示名(请求仍用 API id)。</div>
|
|
526
|
+
```
|
|
527
|
+
|
|
426
528
|
## 自定义模型(WebUI 添加后落盘的结构)
|
|
427
529
|
|
|
428
530
|
```json
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import os from 'node:os';
|
|
7
|
-
import { PROVIDERS, modelPreset, providerPreset, pickFamilyModel, DEFAULT_MODEL, DEFAULT_PLANNER_MODEL, DEFAULT_EXECUTOR_MODEL } from './models.js';
|
|
7
|
+
import { PROVIDERS, modelPreset, providerPreset, pickFamilyModel, normalizeModelLabels, DEFAULT_MODEL, DEFAULT_PLANNER_MODEL, DEFAULT_EXECUTOR_MODEL } from './models.js';
|
|
8
8
|
import { setStoredKey, resolveApiKey, maskKey } from './credentials.js';
|
|
9
9
|
import { atomicWriteFileSync } from './atomic-write.js';
|
|
10
10
|
|
|
@@ -139,6 +139,19 @@ function applyCachedModelDefaults(/** @type {any} */ cfg) {
|
|
|
139
139
|
return cfg;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/**
|
|
143
|
+
* 校验显示别名(v0.6.20 §3.69):`config.modelLabels` 只影响界面显示,**不参与任何出网请求**。
|
|
144
|
+
* 非法项忽略并 warn(不阻断启动);非对象/空值也在 normalizeModelLabels 里统一处理。
|
|
145
|
+
* 返回的是同一个对象(就地归一化),不新建配置副本。
|
|
146
|
+
* @param {any} cfg
|
|
147
|
+
*/
|
|
148
|
+
function applyModelLabels(/** @type {any} */ cfg) {
|
|
149
|
+
if (cfg && typeof cfg === 'object' && Object.prototype.hasOwnProperty.call(cfg, 'modelLabels')) {
|
|
150
|
+
cfg.modelLabels = normalizeModelLabels(cfg.modelLabels);
|
|
151
|
+
}
|
|
152
|
+
return cfg;
|
|
153
|
+
}
|
|
154
|
+
|
|
142
155
|
/** 读取配置对象(不存在/损坏返回 null);返回值为用户可编辑的任意 JSON 配置,类型不定
|
|
143
156
|
*
|
|
144
157
|
* 注意(v0.6.3 / H-7):null **同时**表示「不存在」与「读不出来」。凡是要**写**配置的调用点,
|
|
@@ -146,7 +159,7 @@ function applyCachedModelDefaults(/** @type {any} */ cfg) {
|
|
|
146
159
|
* @returns {any} */
|
|
147
160
|
export function loadConfig() {
|
|
148
161
|
const r = readConfigStrict();
|
|
149
|
-
return r.ok ? applyCachedModelDefaults(r.data) : null;
|
|
162
|
+
return r.ok ? applyModelLabels(applyCachedModelDefaults(r.data)) : null;
|
|
150
163
|
}
|
|
151
164
|
|
|
152
165
|
/**
|
|
@@ -173,7 +186,7 @@ export function loadConfigForWrite(why = '某个写配置的命令') {
|
|
|
173
186
|
* 不再要求先去终端跑 mingdao init)。CLI 的 mingdao init 向导不受影响。 */
|
|
174
187
|
export function ensureMinimalConfig() {
|
|
175
188
|
const strict = readConfigStrict();
|
|
176
|
-
if (strict.ok && strict.data) return strict.data;
|
|
189
|
+
if (strict.ok && strict.data) return applyModelLabels(strict.data);
|
|
177
190
|
// v0.6.3(H-7):存在但读不出来 → 先备份再写,绝不让"首启自动建配置"吃掉用户配置
|
|
178
191
|
if (strict.exists && !strict.ok) quarantineCorruptConfig(strict.error || '未知原因');
|
|
179
192
|
ensureHome();
|
package/src/model-discovery.js
CHANGED
|
@@ -9,7 +9,7 @@ import fs from 'node:fs';
|
|
|
9
9
|
import { safeFetchText } from './safe-fetch.js';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { mingdaoHome, ensureHome } from './config.js';
|
|
12
|
-
import { PROVIDERS, modelPreset, familyRepresentative, pickFamilyModel, modelCapabilityUnknown } from './models.js';
|
|
12
|
+
import { PROVIDERS, modelPreset, familyRepresentative, pickFamilyModel, modelCapabilityUnknown, normalizeModelLabels, modelDisplayLabel } from './models.js';
|
|
13
13
|
import { getStoredKey } from './credentials.js';
|
|
14
14
|
import { isLocalBaseUrl, resolveModelCaps } from './model-caps.js';
|
|
15
15
|
import { resolveProviderConfig, acquireLocalSlot, localQueueTimeoutMs } from './providers/index.js';
|
|
@@ -1137,6 +1137,11 @@ export async function resolveRoutingDefaults(/** @type {any} */ cfg, { force = f
|
|
|
1137
1137
|
export async function availableModels(/** @type {any} */ cfg, /** @type {any} */ currentModel) {
|
|
1138
1138
|
const out = [];
|
|
1139
1139
|
const seen = new Set();
|
|
1140
|
+
// v0.6.20(§3.69):显示别名只用于界面。官方 /models 条目的 `label` 是**显示名**
|
|
1141
|
+
// (`config.modelLabels[id]`,没有映射就回退 id),`id`/`name` 才是 API 实际 id。
|
|
1142
|
+
// 原来的能力/来源说明保留到 `detail` 字段(下拉 title / 提示可读)。
|
|
1143
|
+
// 出网请求的 model 字段只从 config.model / state.modelName 取,**永不**从 label 取。
|
|
1144
|
+
const modelLabels = normalizeModelLabels(cfg?.modelLabels, { warn: false });
|
|
1140
1145
|
for (const [pname, pp] of Object.entries(PROVIDERS)) {
|
|
1141
1146
|
if (pname === 'custom') continue;
|
|
1142
1147
|
if (!providerHasKey(pname)) continue;
|
|
@@ -1156,9 +1161,18 @@ export async function availableModels(/** @type {any} */ cfg, /** @type {any} */
|
|
|
1156
1161
|
: preset
|
|
1157
1162
|
? `${n} — ${preset.label}`
|
|
1158
1163
|
: `${n}(线上最新;能力未知(新版本),仍可选用)`;
|
|
1164
|
+
// 显示名与 API id 分离:label 可以来自 config.modelLabels,请求仍用 name/id。
|
|
1165
|
+
const displayLabel = modelDisplayLabel(modelLabels, n);
|
|
1159
1166
|
out.push({
|
|
1160
1167
|
name: n,
|
|
1161
|
-
|
|
1168
|
+
// v0.6.20(§3.69):显式保留 API 实际 id(前端 title 用「API id: <id>」随时对上 API)
|
|
1169
|
+
id: n,
|
|
1170
|
+
// label 是**给人看的显示名**;没有配 modelLabels 时就是 id 本身。
|
|
1171
|
+
label: displayLabel,
|
|
1172
|
+
// 原 label 的能力/来源说明(离线兜底、能力未知、来源标注)挪到这里,信息不丢。
|
|
1173
|
+
detail: label,
|
|
1174
|
+
// 只有"映射生效、显示名 != id"时才标来源,避免把自定义条目的组合展示名也误标成 config。
|
|
1175
|
+
...(displayLabel !== n ? { labelSource: 'config' } : {}),
|
|
1162
1176
|
provider: pname,
|
|
1163
1177
|
providerLabel: pp.label,
|
|
1164
1178
|
// dynamic 的既有语义(前端/旧测试读它):不在任何内置模式表里。家族命中不等于 dynamic。
|
|
@@ -1184,10 +1198,15 @@ export async function availableModels(/** @type {any} */ cfg, /** @type {any} */
|
|
|
1184
1198
|
if (seen.has(cmName)) continue;
|
|
1185
1199
|
seen.add(cmName);
|
|
1186
1200
|
const src = customSourceOf(cfg, cmName);
|
|
1201
|
+
// 自定义条目保留既有组合展示名(来源必须在展示名里,且 test/smoke §139 以此为准);
|
|
1202
|
+
// modelLabels 只映射服务商 /models 的官方 id,不改变自定义条目。
|
|
1203
|
+
const customLabel = `${cmName} — ${cm.label || '自定义模型'}(${src.sourceLabel})`;
|
|
1187
1204
|
out.push({
|
|
1188
1205
|
name: cmName,
|
|
1206
|
+
id: cmName,
|
|
1189
1207
|
// 展示名里也带上来源(不只是 title/tooltip):下拉标题行、设置面板、日志三处一致
|
|
1190
|
-
label:
|
|
1208
|
+
label: customLabel,
|
|
1209
|
+
detail: customLabel,
|
|
1191
1210
|
provider: 'custom',
|
|
1192
1211
|
providerLabel: src.providerLabel,
|
|
1193
1212
|
custom: true,
|
|
@@ -1201,9 +1220,12 @@ export async function availableModels(/** @type {any} */ cfg, /** @type {any} */
|
|
|
1201
1220
|
});
|
|
1202
1221
|
}
|
|
1203
1222
|
if (currentModel && !out.some((m) => m.name === currentModel)) {
|
|
1223
|
+
const currentLabel = `${currentModel}(当前配置)`;
|
|
1204
1224
|
out.unshift({
|
|
1205
1225
|
name: currentModel,
|
|
1206
|
-
|
|
1226
|
+
id: currentModel,
|
|
1227
|
+
label: currentLabel,
|
|
1228
|
+
detail: currentLabel,
|
|
1207
1229
|
provider: 'current',
|
|
1208
1230
|
providerLabel: '当前',
|
|
1209
1231
|
official: false,
|
package/src/models.js
CHANGED
|
@@ -422,6 +422,100 @@ export function modelPreset(/** @type {any} */ name) {
|
|
|
422
422
|
return /** @type {any} */ (MODELS)[n] || null;
|
|
423
423
|
}
|
|
424
424
|
|
|
425
|
+
// ---------- 模型显示别名(v0.6.20 §3.69):只影响界面,绝不参与出网请求 ----------
|
|
426
|
+
//
|
|
427
|
+
// 为什么要有:厂家的「官方宣传名 / 构建名」与 API 实际 id 可能不是一回事。实测(2026-10):
|
|
428
|
+
// GET https://api.deepseek.com/v1/models → ["deepseek-flash", "deepseek-v4-pro"]
|
|
429
|
+
// POST chat {model:"deepseek-flash"} → 200
|
|
430
|
+
// POST chat {model:"DeepSeek-V4.1-Flash 1017"} → 400
|
|
431
|
+
// {"message":"The supported API model names are deepseek-flash, deepseek-v4-pro,
|
|
432
|
+
// but you passed DeepSeek-V4.1-Flash 1017."}
|
|
433
|
+
// 也就是说宣传名**既不会出现在 /models 里,也不能用于请求**。想让它显示在界面上,只能用
|
|
434
|
+
// 「显示别名」:`config.modelLabels = { "<API id>": "<显示名>" }`,出网 model 字段一律仍是 id。
|
|
435
|
+
//
|
|
436
|
+
// 约束:非法项**忽略并 warn**,绝不抛错/阻断启动(配置写坏一个字不应让整个应用起不来)。
|
|
437
|
+
/** 显示名长度上限(字符):够放「DeepSeek-V4.1-Flash 1017(官方宣传名)」,又不撑破下拉。 */
|
|
438
|
+
export const MODEL_LABEL_MAX_LENGTH = 80;
|
|
439
|
+
|
|
440
|
+
/** 已经告警过的非法项(同一进程只提醒一次,避免每次 loadConfig 都刷屏)。 */
|
|
441
|
+
const warnedModelLabels = new Set();
|
|
442
|
+
|
|
443
|
+
/** @param {string} detail */
|
|
444
|
+
function warnModelLabel(detail) {
|
|
445
|
+
if (warnedModelLabels.has(detail)) return;
|
|
446
|
+
warnedModelLabels.add(detail);
|
|
447
|
+
console.warn(
|
|
448
|
+
`[MingDao] ⚠ config.modelLabels 已忽略非法项:${detail}(显示别名只影响界面,不影响请求;非法项不会阻断启动)`
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* 校验并归一化 `config.modelLabels`。
|
|
454
|
+
*
|
|
455
|
+
* 规则:
|
|
456
|
+
* - key(API 实际 id)必须是非空字符串(去空白后);
|
|
457
|
+
* - value(显示名)必须是字符串、去空白后非空、长度 ≤ maxLength;
|
|
458
|
+
* - 非法项**忽略并 warn**(同一项只提醒一次),不抛错、不阻断启动。
|
|
459
|
+
*
|
|
460
|
+
* 注意:这里只做「显示名」的清洗,**绝不**把结果写回 `config.model` /
|
|
461
|
+
* `config.routing.*`,也绝不被拼进请求体的 `model` 字段。
|
|
462
|
+
*
|
|
463
|
+
* @param {any} raw 用户配置里的原始值(任意类型都安全)
|
|
464
|
+
* @param {{maxLength?: number, warn?: boolean}} [opts]
|
|
465
|
+
* @returns {Record<string, string>} 干净的 `{ "<API id>": "<显示名>" }`
|
|
466
|
+
*/
|
|
467
|
+
export function normalizeModelLabels(raw, { maxLength = MODEL_LABEL_MAX_LENGTH, warn = true } = {}) {
|
|
468
|
+
/** @type {Record<string, string>} */
|
|
469
|
+
const out = {};
|
|
470
|
+
if (raw == null) return out;
|
|
471
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) {
|
|
472
|
+
if (warn) warnModelLabel('modelLabels 必须是对象(形如 { "deepseek-flash": "显示名" })');
|
|
473
|
+
return out;
|
|
474
|
+
}
|
|
475
|
+
for (const [rawKey, rawValue] of Object.entries(raw)) {
|
|
476
|
+
const key = String(rawKey).trim();
|
|
477
|
+
if (!key) {
|
|
478
|
+
if (warn) warnModelLabel(`键为空(原值 ${JSON.stringify(rawKey)})`);
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
// 原型污染防线:JSON.parse 出来的 `__proto__` 是 own property,直接写进普通对象会改原型。
|
|
482
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
|
483
|
+
if (warn) warnModelLabel(`键名 "${key}" 不安全`);
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
if (typeof rawValue !== 'string') {
|
|
487
|
+
if (warn) warnModelLabel(`"${key}" 的显示名必须是字符串(实际是 ${typeof rawValue})`);
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
const value = rawValue.trim();
|
|
491
|
+
if (!value) {
|
|
492
|
+
if (warn) warnModelLabel(`"${key}" 的显示名为空`);
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (value.length > maxLength) {
|
|
496
|
+
if (warn) warnModelLabel(`"${key}" 的显示名过长(${value.length} > ${maxLength} 字符)`);
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
out[key] = value;
|
|
500
|
+
}
|
|
501
|
+
return out;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* 取某个 API id 的显示名:命中 `config.modelLabels` 用别名,否则**回退到 id 本身**。
|
|
506
|
+
* 只用于展示;出网请求的 `model` 字段一律用 id(见 availableModels 的 id/label 分离)。
|
|
507
|
+
* @param {any} modelLabels 原始或已归一化的 modelLabels(非法值自动回退 id)
|
|
508
|
+
* @param {any} id API 实际模型 id
|
|
509
|
+
* @returns {string}
|
|
510
|
+
*/
|
|
511
|
+
export function modelDisplayLabel(/** @type {any} */ modelLabels, /** @type {any} */ id) {
|
|
512
|
+
const key = String(id ?? '');
|
|
513
|
+
if (!key) return key;
|
|
514
|
+
// 归一化一次(warn=false):既防 `{id: 123}` 之类原始值漏进界面,又不会在此处重复告警
|
|
515
|
+
const clean = normalizeModelLabels(modelLabels, { warn: false });
|
|
516
|
+
return Object.prototype.hasOwnProperty.call(clean, key) ? clean[key] : key;
|
|
517
|
+
}
|
|
518
|
+
|
|
425
519
|
/** 该名字的能力是否**未经验证**(家族能匹配但版本比已核对规格新,或完全不属于任何家族)。 */
|
|
426
520
|
export function modelCapabilityUnknown(/** @type {any} */ name) {
|
|
427
521
|
const preset = modelPreset(name);
|