mingdao-harness 0.6.15 → 0.6.16
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 +50 -0
- package/package.json +1 -1
- package/src/config.js +76 -10
- package/src/cost-guard.js +1 -1
- package/src/help.js +1 -1
- package/src/model-discovery.js +150 -5
- package/src/models.js +250 -93
- package/src/web/app.js +293 -69
- package/src/web/attachments.js +1 -1
- package/src/web/index.html +60 -11
- package/src/web/routes/domains/config.js +46 -8
- package/src/web/server.js +35 -0
package/docs/CONFIG.md
CHANGED
|
@@ -373,6 +373,56 @@ completion 计费,防止推理吃满上限时空轮白烧)、`compactTrigger
|
|
|
373
373
|
|
|
374
374
|
长期偏好仍走用户记忆(`AGENTS.md`,见上),不受此开关影响。
|
|
375
375
|
|
|
376
|
+
## 模型名称以 API 为准(v0.6.19,§3.67)
|
|
377
|
+
|
|
378
|
+
**仓库不维护具体版本号,只维护「家族能力 / 定价模式」。** 官方改名、升版本(例如
|
|
379
|
+
`deepseek-v4-flash` → `deepseek-flash` → `DeepSeek-V4.1-Flash 1017` → 未来的 V4.2/V5)
|
|
380
|
+
**不需要改代码**:模型名一律以 `GET /v1/models` 实时返回的官方名为准。
|
|
381
|
+
|
|
382
|
+
### 家族模式表
|
|
383
|
+
|
|
384
|
+
`src/models.js` 的 `MODEL_FAMILIES` 用正则匹配名字,能力(上下文窗口、单次输出上限、工具调用、
|
|
385
|
+
视觉)与定价挂在**模式**上:
|
|
386
|
+
|
|
387
|
+
| 家族 | 匹配(大小写 / 空格 / 连字符 / 版本号无关) | 典型官方名 |
|
|
388
|
+
| --- | --- | --- |
|
|
389
|
+
| `pro` | `deepseek` + 可选版本 + `pro` | `DeepSeek-V4.1-Pro 1017` |
|
|
390
|
+
| `flash` | `deepseek` + 可选版本 + `flash` | `DeepSeek-V4.1-Flash 1017` |
|
|
391
|
+
| `vision` | `deepseek` + 可选版本 + `flash` + `vision` | `DeepSeek-V4.1-Flash-Vision 1017` |
|
|
392
|
+
|
|
393
|
+
- 精确 id 只作为**已知别名**存在(如旧名 `deepseek-v4-flash`),老配置与历史会话照常可用;
|
|
394
|
+
路由比较前会归一化到家族代表名,不会因为厂家改名而静默失效。
|
|
395
|
+
- 比已核对规格更新的版本(例如 `DeepSeek-V4.2-Flash 1101`)**仍可选中**,列表会标
|
|
396
|
+
「能力未知(新版本)」并按家族规格保守估算上下文与价格;需要精确规格时用
|
|
397
|
+
`customModels.<模型名>.contextWindow` / `config.pricing.overrides` 覆盖。
|
|
398
|
+
|
|
399
|
+
### 默认值如何解析
|
|
400
|
+
|
|
401
|
+
默认模型(新建配置、WebUI 模型下拉、自动路由的 planner / executor)在**运行时**按下列顺序解析:
|
|
402
|
+
|
|
403
|
+
1. 用户显式配置优先:`config.model`、`config.routing.planner`、`config.routing.executor`;
|
|
404
|
+
2. 否则从**当前 API 名单**里按家族挑官方名:planner → `pro` 家族,executor → `flash` 家族,
|
|
405
|
+
多个版本并存时取最高版本;
|
|
406
|
+
3. 名单里没有对应家族、或 API 不可达时,才退回**内置家族代表名**,并输出提示:
|
|
407
|
+
- 服务端日志:`[MingDao] ⚠ … 默认模型走兜底 …`
|
|
408
|
+
- WebUI / HTTP:`GET /api/models` 与 `GET /api/state` 的 `note` 字段,离线时
|
|
409
|
+
`offlineFallback: true`,文案为「离线兜底:模型名可能不是最新的」。
|
|
410
|
+
|
|
411
|
+
离线时模型列表使用内置家族代表名(不是最新官方版本),并在每个条目上标
|
|
412
|
+
「离线兜底」;连上 API 后刷新即恢复官方名。
|
|
413
|
+
|
|
414
|
+
**自定义端点不主动探测。** `provider: "custom"`(以及未内置的服务商)不会收到
|
|
415
|
+
`GET /models` 请求——很多自定义网关只实现 chat,空 body 的 `/models` 请求会让它们直接崩。
|
|
416
|
+
这类端点的默认 planner / executor 用家族代表名兜底,需要指定时在 `config.routing.*`
|
|
417
|
+
里写端点自己的模型名。
|
|
418
|
+
|
|
419
|
+
### 旧名与自定义模型
|
|
420
|
+
|
|
421
|
+
- 旧名(`deepseek-v4-flash`、`deepseek-flash`、`deepseek-v4-pro`、`deepseek-v4-flash-vision-exp`)
|
|
422
|
+
仍可作为 `--model` / `config.model` 的取值;能力与定价照旧按家族解析。
|
|
423
|
+
- 完全不属于任何家族的线上新模型(例如非 DeepSeek 命名)仍会出现在列表里,可直接选中;
|
|
424
|
+
能力未知时按远程模型保守默认(上下文 128k)估算,可用 `customModels` 显式覆盖。
|
|
425
|
+
|
|
376
426
|
## 自定义模型(WebUI 添加后落盘的结构)
|
|
377
427
|
|
|
378
428
|
```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, DEFAULT_MODEL, DEFAULT_PLANNER_MODEL, DEFAULT_EXECUTOR_MODEL } from './models.js';
|
|
7
|
+
import { PROVIDERS, modelPreset, providerPreset, pickFamilyModel, 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
|
|
|
@@ -87,6 +87,58 @@ export function quarantineCorruptConfig(reason) {
|
|
|
87
87
|
return dest;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* 同步读取 model-cache.json 里某服务商的线上名单(最近一次 `GET /models` 的结果)。
|
|
92
|
+
* `ensureMinimalConfig` / `loadConfig` 都是同步接口,无法 await 网络请求,用这份缓存
|
|
93
|
+
* 把「默认模型」对齐到 API 返回的官方名;缓存为空时才用家族代表名兜底并告警。
|
|
94
|
+
* @param {string} provider
|
|
95
|
+
* @returns {string[]}
|
|
96
|
+
*/
|
|
97
|
+
function cachedModelNames(provider) {
|
|
98
|
+
try {
|
|
99
|
+
const j = JSON.parse(fs.readFileSync(path.join(mingdaoHome(), 'model-cache.json'), 'utf8'));
|
|
100
|
+
const list = j?.[provider]?.models;
|
|
101
|
+
return Array.isArray(list) ? list.map(String) : [];
|
|
102
|
+
} catch {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let warnedCachedFallback = false;
|
|
108
|
+
function warnCachedFallback(/** @type {string} */ what, /** @type {string} */ rep) {
|
|
109
|
+
if (warnedCachedFallback) return;
|
|
110
|
+
warnedCachedFallback = true;
|
|
111
|
+
console.warn(`[MingDao] ⚠ ${what} 没有可用的 API 官方名缓存:暂用内置家族代表名 ${rep}(兜底,模型名可能不是最新的;连上 API 后会自动刷新)。`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* v0.6.19(§3.67):把配置里的**缺省**模型补成 API 名单里的官方名。
|
|
116
|
+
* 只补缺省值,绝不覆盖用户显式写下的模型名/路由(用户意图优先)。
|
|
117
|
+
* @param {any} cfg
|
|
118
|
+
*/
|
|
119
|
+
function applyCachedModelDefaults(/** @type {any} */ cfg) {
|
|
120
|
+
if (!cfg || typeof cfg !== 'object') return cfg;
|
|
121
|
+
const provider = String(cfg.provider || 'deepseek');
|
|
122
|
+
const cached = cachedModelNames(provider);
|
|
123
|
+
if (!cfg.model) {
|
|
124
|
+
const m = pickFamilyModel(cached, 'flash');
|
|
125
|
+
if (m) cfg.model = m;
|
|
126
|
+
}
|
|
127
|
+
if (cfg.routing && cfg.routing.enabled) {
|
|
128
|
+
if (!cfg.routing.planner) {
|
|
129
|
+
const p = pickFamilyModel(cached, 'pro');
|
|
130
|
+
cfg.routing.planner = p || DEFAULT_PLANNER_MODEL;
|
|
131
|
+
if (!p) warnCachedFallback('routing.planner', DEFAULT_PLANNER_MODEL);
|
|
132
|
+
}
|
|
133
|
+
if (!cfg.routing.executor) {
|
|
134
|
+
const e = pickFamilyModel(cached, 'flash');
|
|
135
|
+
cfg.routing.executor = e || DEFAULT_EXECUTOR_MODEL;
|
|
136
|
+
if (!e) warnCachedFallback('routing.executor', DEFAULT_EXECUTOR_MODEL);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return cfg;
|
|
140
|
+
}
|
|
141
|
+
|
|
90
142
|
/** 读取配置对象(不存在/损坏返回 null);返回值为用户可编辑的任意 JSON 配置,类型不定
|
|
91
143
|
*
|
|
92
144
|
* 注意(v0.6.3 / H-7):null **同时**表示「不存在」与「读不出来」。凡是要**写**配置的调用点,
|
|
@@ -94,7 +146,7 @@ export function quarantineCorruptConfig(reason) {
|
|
|
94
146
|
* @returns {any} */
|
|
95
147
|
export function loadConfig() {
|
|
96
148
|
const r = readConfigStrict();
|
|
97
|
-
return r.ok ? r.data : null;
|
|
149
|
+
return r.ok ? applyCachedModelDefaults(r.data) : null;
|
|
98
150
|
}
|
|
99
151
|
|
|
100
152
|
/**
|
|
@@ -126,7 +178,11 @@ export function ensureMinimalConfig() {
|
|
|
126
178
|
if (strict.exists && !strict.ok) quarantineCorruptConfig(strict.error || '未知原因');
|
|
127
179
|
ensureHome();
|
|
128
180
|
const pp = PROVIDERS['deepseek'] || Object.values(PROVIDERS)[0];
|
|
129
|
-
|
|
181
|
+
// v0.6.19(§3.67):首选最近一次 API 名单里的 flash 官方名;没有缓存才用家族代表名。
|
|
182
|
+
// (首次运行还没填 Key,API 拉不到,此时代表名就是合理的兜底,后续拉到官方名会自动替换。)
|
|
183
|
+
const officialFlash = pickFamilyModel(cachedModelNames('deepseek'), 'flash');
|
|
184
|
+
const model = officialFlash || (modelPreset(DEFAULT_MODEL) ? DEFAULT_MODEL : pp.models[0]);
|
|
185
|
+
if (!officialFlash) warnCachedFallback('默认模型', model);
|
|
130
186
|
const cfg = {
|
|
131
187
|
provider: 'deepseek',
|
|
132
188
|
model,
|
|
@@ -212,25 +268,28 @@ export async function runWizard(io) {
|
|
|
212
268
|
if (models) io.print(`✓ API Key 验证通过:该服务商线上可用模型 ${models.length} 个。`);
|
|
213
269
|
else if (verifyError) io.print('(已跳过验证,稍后可在 WebUI 设置面板「刷新模型」处再次校验)');
|
|
214
270
|
|
|
215
|
-
// ③
|
|
271
|
+
// ③ 选择模型(允许暂时跳过——跳过时默认用 **API 名单里的 flash 官方名**,/model 可随时换)
|
|
216
272
|
let model = null;
|
|
217
273
|
if (provider === 'custom') {
|
|
218
274
|
const m = await io.ask('③ 模型名(可留空跳过,稍后 /model 再选):');
|
|
219
275
|
model = m.trim() || null;
|
|
220
276
|
} else {
|
|
221
|
-
const options = [{ value: '__skip__', label: '
|
|
277
|
+
const options = [{ value: '__skip__', label: '暂时跳过(用 API 返回的官方默认名,/model 可换)' }];
|
|
222
278
|
for (const m of models || pp.models) {
|
|
223
279
|
const preset = modelPreset(m);
|
|
224
|
-
options.push({ value: m, label: preset ? `${m} — ${preset.label}` : m });
|
|
280
|
+
options.push({ value: m, label: preset ? `${m} — ${preset.label}` : `${m}(能力未知(新版本),仍可选用)` });
|
|
225
281
|
}
|
|
226
282
|
options.push({ value: '__custom__', label: '自定义模型名(手动输入)' });
|
|
227
283
|
const choice = await io.choose('③ 选择模型:', options);
|
|
228
|
-
if (choice === '__skip__') model =
|
|
284
|
+
if (choice === '__skip__') model = pickFamilyModel(models || [], 'flash');
|
|
229
285
|
else if (choice === '__custom__') model = (await io.ask('模型名:')).trim() || null;
|
|
230
286
|
else model = choice;
|
|
231
287
|
}
|
|
288
|
+
const chosenPreset = model ? modelPreset(model) : null;
|
|
232
289
|
if (model) io.print(`✓ 已选择模型:${model}`);
|
|
233
290
|
else io.print('✓ 已跳过模型选择:进入后自动使用该服务商默认模型,输入 /model 可随时切换。');
|
|
291
|
+
if (chosenPreset?.capabilityUnknown) io.print('⚠ 该模型是 API 返回的新版本:能力未知(按家族规格保守估算,仍可选用)。');
|
|
292
|
+
if (!models && provider !== 'custom') io.print('(未取得 API 模型名单:模型名用内置家族代表名——离线兜底,模型名可能不是最新的)');
|
|
234
293
|
|
|
235
294
|
const perm = await io.choose('权限模式(写文件 / 执行命令时):', [
|
|
236
295
|
{ value: 'ask', label: 'ask — 每次询问(推荐,最安全)' },
|
|
@@ -245,12 +304,19 @@ export async function runWizard(io) {
|
|
|
245
304
|
]);
|
|
246
305
|
|
|
247
306
|
let routing = null;
|
|
248
|
-
|
|
307
|
+
// v0.6.19(§3.67):路由默认名同样以 API 名单里的官方名为准(planner→pro 家族、executor→flash 家族);
|
|
308
|
+
// 名单不可用才退回家族代表名,并在下面明确提示「兜底」。
|
|
309
|
+
const plannerDefault = pickFamilyModel(models || [], 'pro') || DEFAULT_PLANNER_MODEL;
|
|
310
|
+
const executorDefault = pickFamilyModel(models || [], 'flash') || DEFAULT_EXECUTOR_MODEL;
|
|
311
|
+
if (modelPreset(plannerDefault) && modelPreset(executorDefault)) {
|
|
249
312
|
const routeChoice = await io.choose('自动模型路由(规划类任务→pro,执行类→flash):', [
|
|
250
313
|
{ value: 'on', label: 'on — 开启(省钱又高效,推荐)' },
|
|
251
314
|
{ value: 'off', label: 'off — 关闭(始终用当前模型)' },
|
|
252
315
|
]);
|
|
253
|
-
if (routeChoice === 'on')
|
|
316
|
+
if (routeChoice === 'on') {
|
|
317
|
+
routing = { enabled: true, planner: plannerDefault, executor: executorDefault };
|
|
318
|
+
if (!models) io.print(`⚠ 路由默认名走兜底(未取得 API 名单):planner=${plannerDefault} / executor=${executorDefault}——模型名可能不是最新的。`);
|
|
319
|
+
}
|
|
254
320
|
}
|
|
255
321
|
|
|
256
322
|
const preset = model ? modelPreset(model) : null;
|
|
@@ -274,7 +340,7 @@ export async function runWizard(io) {
|
|
|
274
340
|
`服务商 ${provider}(${pp.label})`,
|
|
275
341
|
model ? `模型 ${model}` : '模型 (暂未选择,进入后 /model 随时切换)',
|
|
276
342
|
`权限 ${perm} · 沙箱 ${sandbox}`,
|
|
277
|
-
`路由 ${routing ?
|
|
343
|
+
`路由 ${routing ? `自动(${routing.planner} ⇄ ${routing.executor})` : '关闭'}`,
|
|
278
344
|
`密钥 ${apiKey ? '凭证库 ' + maskKey(apiKey) : '环境变量 ' + (pp.envKey || 'MINGDAO_API_KEY')}`,
|
|
279
345
|
`保存于 ${configPath()}`,
|
|
280
346
|
]);
|
package/src/cost-guard.js
CHANGED
|
@@ -125,7 +125,7 @@ export function checkCostGuard(modelName) {
|
|
|
125
125
|
// P0-4(v0.4.5):无价格数据时护栏无法累计——显式告警而非静默放行(绝不当「没花钱」)
|
|
126
126
|
return {
|
|
127
127
|
blocked: false,
|
|
128
|
-
message: '⚠ 费用护栏:当前模型无价格数据,今日费用无法累计、dailyLimitYuan 不生效——请在 config.pricing.overrides 为模型补充定价,或改用有价的 DeepSeek
|
|
128
|
+
message: '⚠ 费用护栏:当前模型无价格数据,今日费用无法累计、dailyLimitYuan 不生效——请在 config.pricing.overrides 为模型补充定价,或改用有价的 DeepSeek pro / flash 家族模型(具体名称以 API 返回的官方名为准)。',
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
131
|
if (st.overLimit && st.action === 'block') {
|
package/src/help.js
CHANGED
|
@@ -56,7 +56,7 @@ export function helpLines({ variant = 'cli', home = '' } = {}) {
|
|
|
56
56
|
[' mingdao --continue 继续最近一次会话', null],
|
|
57
57
|
[' mingdao --journal 新会话带上最近会话日志(默认不注入,新会话全新开始)', null],
|
|
58
58
|
[' mingdao --resume 从会话列表选择恢复', null],
|
|
59
|
-
[' mingdao --model <模型名>
|
|
59
|
+
[' mingdao --model <模型名> 指定模型(pro / flash 家族;具体名称以 API 返回的官方名为准)', null],
|
|
60
60
|
...cliOnly,
|
|
61
61
|
[' mingdao init 初始化配置向导', null],
|
|
62
62
|
[' mingdao update [--check] 一键自更新(git 安装形态;--check 只对比版本)', null],
|
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,
|
|
12
|
+
import { PROVIDERS, modelPreset, familyRepresentative, pickFamilyModel, modelCapabilityUnknown } 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';
|
|
@@ -1005,6 +1005,128 @@ export function isDiscoveredModel(/** @type {any} */ name, /** @type {string} */
|
|
|
1005
1005
|
return false;
|
|
1006
1006
|
}
|
|
1007
1007
|
|
|
1008
|
+
// ---------------------------------------------------------------------------
|
|
1009
|
+
// v0.6.19(§3.67):**默认模型 = API 实时返回的官方名**
|
|
1010
|
+
//
|
|
1011
|
+
// 以前 DEFAULT_MODEL / DEFAULT_PLANNER_MODEL 是写死的具体版本,厂家升版本就要改代码;
|
|
1012
|
+
// 现在默认值在运行时解析:
|
|
1013
|
+
// ① 用户显式配置(config.routing.planner/executor、config.model)优先——不覆盖用户意图;
|
|
1014
|
+
// ② 否则从该服务商当前 `/models` 名单里按**家族模式**挑(planner→pro、executor→flash);
|
|
1015
|
+
// ③ 名单里没有该家族 / API 不可达时,才退回家族代表名,并明确提示「兜底」。
|
|
1016
|
+
// 提示会出现在 console.warn(服务端日志)与 /api/models 的 note 字段(界面 banner 可读)。
|
|
1017
|
+
// ---------------------------------------------------------------------------
|
|
1018
|
+
|
|
1019
|
+
/** 离线兜底提示(测试与界面共用同一句,避免两处漂移)。 */
|
|
1020
|
+
export const OFFLINE_MODEL_NOTE = '离线兜底:模型名可能不是最新的(API 不可达,已使用内置家族代表名)';
|
|
1021
|
+
|
|
1022
|
+
/** 角色 → 家族(planner 用 pro,executor/model 用 flash)。 */
|
|
1023
|
+
export const MODEL_ROLE_FAMILY = /** @type {Record<string, string>} */ ({ planner: 'pro', executor: 'flash', model: 'flash' });
|
|
1024
|
+
|
|
1025
|
+
/** 兜底告警只打一次/角色,避免每轮刷屏(但**不能不打**,否则用户不知道用的是旧名)。 */
|
|
1026
|
+
const warnedFallback = new Set();
|
|
1027
|
+
|
|
1028
|
+
function warnFallback(/** @type {string} */ role, /** @type {string} */ reason, /** @type {string} */ model) {
|
|
1029
|
+
const key = `${role}:${reason}`;
|
|
1030
|
+
if (warnedFallback.has(key)) return;
|
|
1031
|
+
warnedFallback.add(key);
|
|
1032
|
+
console.warn(`[MingDao] ⚠ ${role} 默认模型走兜底(${reason}):使用内置家族代表名 ${model}——模型名可能不是最新的,请检查 API 连通性/密钥。`);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* 把模型列表里「离线兜底 / 能力未知」的条目标成一句提示,供 /api/models 与界面 banner 使用。
|
|
1037
|
+
* @param {any} models
|
|
1038
|
+
* @returns {string|null}
|
|
1039
|
+
*/
|
|
1040
|
+
export function modelsCatalogNote(/** @type {any} */ models) {
|
|
1041
|
+
const list = Array.isArray(models) ? models : [];
|
|
1042
|
+
if (list.some((m) => m && m.offlineFallback === true)) return OFFLINE_MODEL_NOTE;
|
|
1043
|
+
const unknown = list.filter((m) => m && m.capabilityUnknown === true).length;
|
|
1044
|
+
if (unknown) return `${unknown} 个模型是 API 返回的新版本:能力未知(按家族规格保守估算,仍可选用)`;
|
|
1045
|
+
return null;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* 解析某个角色的默认模型(**以 API 实时返回的官方名为准**)。
|
|
1050
|
+
* @param {any} cfg
|
|
1051
|
+
* @param {'planner'|'executor'|'model'} [role]
|
|
1052
|
+
* @param {{force?: boolean}} [opts]
|
|
1053
|
+
* @returns {Promise<{model: string, role: string, family: string, source: 'config'|'api'|'fallback', official: boolean, fallback: boolean, offline: boolean, note: string|null}>}
|
|
1054
|
+
*/
|
|
1055
|
+
export async function resolveDefaultModel(/** @type {any} */ cfg, /** @type {any} */ role = 'executor', { force = false } = {}) {
|
|
1056
|
+
const r = String(role || 'executor');
|
|
1057
|
+
const family = MODEL_ROLE_FAMILY[r] || 'flash';
|
|
1058
|
+
const explicit = r === 'planner' ? cfg?.routing?.planner : r === 'executor' ? cfg?.routing?.executor : cfg?.model;
|
|
1059
|
+
if (explicit) {
|
|
1060
|
+
return {
|
|
1061
|
+
model: String(explicit),
|
|
1062
|
+
role: r,
|
|
1063
|
+
family,
|
|
1064
|
+
source: 'config',
|
|
1065
|
+
official: false,
|
|
1066
|
+
fallback: false,
|
|
1067
|
+
offline: false,
|
|
1068
|
+
note: null,
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
const provider = String(cfg?.provider || 'deepseek');
|
|
1072
|
+
// 既有策略(v0.6.13):**自定义端点不主动探测 `GET /models`**——真实世界里很多自定义网关
|
|
1073
|
+
// 只实现 chat,`/models` 可能返回空 body/非 JSON(e2e-web 的 mock 就会因此 JSON.parse 崩)。
|
|
1074
|
+
// 自定义端点/未内置的服务商还没有可枚举的名单元数据,直接走家族代表名兜底(不发任何请求)。
|
|
1075
|
+
if (provider === 'custom' || !/** @type {any} */ (PROVIDERS)[provider]) {
|
|
1076
|
+
const rep = familyRepresentative(family);
|
|
1077
|
+
return {
|
|
1078
|
+
model: rep,
|
|
1079
|
+
role: r,
|
|
1080
|
+
family,
|
|
1081
|
+
source: 'fallback',
|
|
1082
|
+
official: false,
|
|
1083
|
+
fallback: true,
|
|
1084
|
+
offline: false,
|
|
1085
|
+
note: `自定义端点/未内置服务商不主动探测 /models,使用内置家族代表名 ${rep}(可用 config.routing.* 显式指定端点自己的模型名)`,
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
let fetched = /** @type {any} */ ({ error: '该服务商未设置 API Key' });
|
|
1089
|
+
try {
|
|
1090
|
+
fetched = await fetchProviderModels(cfg, provider, { force });
|
|
1091
|
+
} catch (/** @type {any} */ e) {
|
|
1092
|
+
fetched = { error: String(e?.message || e) };
|
|
1093
|
+
}
|
|
1094
|
+
if (Array.isArray(fetched?.models) && fetched.models.length) {
|
|
1095
|
+
const picked = pickFamilyModel(fetched.models, family);
|
|
1096
|
+
if (picked) {
|
|
1097
|
+
return {
|
|
1098
|
+
model: picked,
|
|
1099
|
+
role: r,
|
|
1100
|
+
family,
|
|
1101
|
+
source: 'api',
|
|
1102
|
+
official: true,
|
|
1103
|
+
fallback: false,
|
|
1104
|
+
offline: false,
|
|
1105
|
+
note: null,
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
const rep = familyRepresentative(family);
|
|
1109
|
+
const note = `兜底:API 列表里没有 ${family} 家族模型,改用内置家族代表名 ${rep}`;
|
|
1110
|
+
warnFallback(r, 'API 列表无匹配家族', rep);
|
|
1111
|
+
return { model: rep, role: r, family, source: 'fallback', official: false, fallback: true, offline: false, note };
|
|
1112
|
+
}
|
|
1113
|
+
const rep = familyRepresentative(family);
|
|
1114
|
+
warnFallback(r, String(fetched?.error || '未知原因'), rep);
|
|
1115
|
+
return { model: rep, role: r, family, source: 'fallback', official: false, fallback: true, offline: true, note: OFFLINE_MODEL_NOTE };
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* 一次解析 planner + executor 的默认值(WebUI 开启自动路由时调用)。
|
|
1120
|
+
* @param {any} cfg
|
|
1121
|
+
* @param {{force?: boolean}} [opts]
|
|
1122
|
+
*/
|
|
1123
|
+
export async function resolveRoutingDefaults(/** @type {any} */ cfg, { force = false } = {}) {
|
|
1124
|
+
const [planner, executor] = await Promise.all([resolveDefaultModel(cfg, 'planner', { force }), resolveDefaultModel(cfg, 'executor', { force })]);
|
|
1125
|
+
const fellBack = [planner, executor].filter((x) => x.fallback);
|
|
1126
|
+
const note = planner.offline || executor.offline ? OFFLINE_MODEL_NOTE : fellBack.length ? fellBack.map((x) => x.note).filter(Boolean).join(';') : null;
|
|
1127
|
+
return { planner, executor, offline: planner.offline || executor.offline, note };
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1008
1130
|
// 合并可用模型列表:只含已设置 Key 的服务商;动态名单优先、预设回退;自定义模型恒在。
|
|
1009
1131
|
//
|
|
1010
1132
|
// v0.6.13(B):**每个条目都要能看出"来自哪个服务商/端点"**。此前自定义条目只有
|
|
@@ -1019,21 +1141,42 @@ export async function availableModels(/** @type {any} */ cfg, /** @type {any} */
|
|
|
1019
1141
|
if (pname === 'custom') continue;
|
|
1020
1142
|
if (!providerHasKey(pname)) continue;
|
|
1021
1143
|
const dynamic = await fetchProviderModels(cfg, pname);
|
|
1144
|
+
// §3.67:拉取失败时用内置名单,但必须**明确标成离线兜底**——
|
|
1145
|
+
// 否则界面会把这些可能过期的名字当成「官方实时名单」展示。
|
|
1146
|
+
const offline = Boolean(dynamic?.error) && !dynamic?.models?.length;
|
|
1022
1147
|
const names = dynamic.models?.length ? dynamic.models : pp.models || [];
|
|
1023
1148
|
for (const n of names) {
|
|
1024
1149
|
if (seen.has(n)) continue;
|
|
1025
1150
|
seen.add(n);
|
|
1026
|
-
|
|
1151
|
+
// 家族模式优先:`DeepSeek-V4.1-Flash 1017` 这类 API 官方名不在精确表里,但能力照样可解析。
|
|
1152
|
+
const preset = modelPreset(n);
|
|
1153
|
+
const capabilityUnknown = modelCapabilityUnknown(n);
|
|
1154
|
+
const label = offline
|
|
1155
|
+
? `${n} — ${preset ? preset.label : '能力未知(新版本/非家族)'}(离线兜底,模型名可能不是最新的)`
|
|
1156
|
+
: preset
|
|
1157
|
+
? `${n} — ${preset.label}`
|
|
1158
|
+
: `${n}(线上最新;能力未知(新版本),仍可选用)`;
|
|
1027
1159
|
out.push({
|
|
1028
1160
|
name: n,
|
|
1029
|
-
label
|
|
1161
|
+
label,
|
|
1030
1162
|
provider: pname,
|
|
1031
1163
|
providerLabel: pp.label,
|
|
1032
|
-
dynamic
|
|
1164
|
+
// dynamic 的既有语义(前端/旧测试读它):不在任何内置模式表里。家族命中不等于 dynamic。
|
|
1165
|
+
dynamic: !preset,
|
|
1033
1166
|
// 来源标注(B):内置预设 vs 服务商线上名单——两者都是"该服务商自己的模型"
|
|
1034
1167
|
official: Boolean(preset),
|
|
1168
|
+
family: preset?.family || null,
|
|
1169
|
+
capabilityUnknown,
|
|
1170
|
+
offlineFallback: offline,
|
|
1171
|
+
// 这一行保持原样:既有变异批 batch28 以它作锚点;离线来源在下一行覆盖。
|
|
1035
1172
|
source: preset ? 'preset' : 'discovered',
|
|
1036
|
-
|
|
1173
|
+
...(offline ? { source: 'offline' } : {}),
|
|
1174
|
+
sourceLabel: offline
|
|
1175
|
+
? `离线兜底 · ${pp.label}`
|
|
1176
|
+
: preset
|
|
1177
|
+
? `线上名单 · ${pp.label}(家族模式匹配)`
|
|
1178
|
+
: `线上名单 · ${pp.label}`,
|
|
1179
|
+
note: offline ? OFFLINE_MODEL_NOTE : capabilityUnknown ? '能力未知(新版本):按家族规格保守估算,仍可选用' : undefined,
|
|
1037
1180
|
});
|
|
1038
1181
|
}
|
|
1039
1182
|
}
|
|
@@ -1066,6 +1209,8 @@ export async function availableModels(/** @type {any} */ cfg, /** @type {any} */
|
|
|
1066
1209
|
official: false,
|
|
1067
1210
|
source: 'current',
|
|
1068
1211
|
sourceLabel: '当前配置(不在任何已知名单里)',
|
|
1212
|
+
family: modelPreset(currentModel)?.family || null,
|
|
1213
|
+
capabilityUnknown: modelCapabilityUnknown(currentModel),
|
|
1069
1214
|
note: '该名称不在任何服务商名单/自定义条目里——只是 config.json 里当前写着的模型名。',
|
|
1070
1215
|
});
|
|
1071
1216
|
}
|