@mokuyoaxis/dsh-iris 0.1.4 → 0.2.1
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/.dsh/skills/iris-compose-media/SKILL.md +10 -5
- package/.dsh/skills/iris-compose-media/references/workflows.md +7 -1
- package/.dsh/skills/iris-verify-ui/SKILL.md +2 -1
- package/CHANGELOG.md +99 -1
- package/README.en.md +124 -31
- package/README.md +124 -31
- package/bin/dsh-iris.js +593 -13
- package/docs/AGENT_QUICKSTART.md +39 -0
- package/docs/ARCHITECTURE.md +87 -64
- package/docs/ARTIFACT_LIBRARY.md +5 -5
- package/docs/ARTIFACT_MANIFEST.md +60 -0
- package/docs/CLI_MANAGEMENT.md +181 -0
- package/docs/COMPOSITE_VISION.md +35 -0
- package/docs/CORE_RUNTIME_CONTRACT.md +84 -0
- package/docs/DSH_CORE_MIGRATION.md +135 -0
- package/docs/DSH_RC2_ADAPTATION.md +38 -0
- package/docs/FAULT_INJECTION.md +12 -2
- package/docs/HEADLESS_CLI.md +223 -0
- package/docs/HOST_ADAPTER_CONTRACT.md +36 -17
- package/docs/HOST_DOCTOR.md +4 -2
- package/docs/MODEL_PORT_CONTRACT.md +305 -0
- package/docs/OCR_MODEL.md +39 -0
- package/docs/PROMPT_OPTIMIZER.md +47 -0
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +53 -10
- package/docs/PROVIDER_HEALTH.md +22 -5
- package/docs/PROVIDER_RUNTIME_HARNESS.md +33 -0
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +2 -2
- package/docs/ROADMAP.md +49 -83
- package/docs/SECURITY.md +8 -7
- package/docs/TASK_SEMANTICS.md +2 -2
- package/docs/VISION_CLI.md +132 -0
- package/docs/VISION_MODEL.md +51 -0
- package/docs/WORKBENCH_ARTIFACTS.md +67 -0
- package/docs/assets/logo/README.md +55 -0
- package/docs/assets/logo/iris-bubble-concept.svg +67 -0
- package/docs/assets/logo/iris-bubble.svg +14 -0
- package/docs/assets/logo/iris-interim-flower.png +0 -0
- package/docs/assets/logo/iris-logo-mono-inverse.svg +78 -0
- package/docs/assets/logo/iris-logo-mono.svg +78 -0
- package/docs/assets/logo/iris-logo.svg +78 -0
- package/docs/assets/logo/iris-mark-mono-inverse.svg +42 -0
- package/docs/assets/logo/iris-mark-mono.svg +42 -0
- package/docs/assets/logo/iris-mark.svg +42 -0
- package/docs/file-access-across-environments.md +8 -5
- package/docs/releases/0.2.0.md +48 -0
- package/docs/releases/0.2.1.md +56 -0
- package/lib/actions.js +520 -591
- package/lib/adapters.js +184 -36
- package/lib/api.js +200 -11
- package/lib/chromium-browser.js +117 -0
- package/lib/client.js +1390 -105
- package/lib/command-service.js +534 -0
- package/lib/composite-vision-routing.js +42 -0
- package/lib/config.js +143 -51
- package/lib/core-artifact-store.js +630 -0
- package/lib/core-artifacts.js +4 -0
- package/lib/core-attention.js +103 -0
- package/lib/core-contract.js +105 -0
- package/lib/core-doctor.js +41 -0
- package/lib/core-lease-recovery.js +170 -0
- package/lib/core-maintenance.js +213 -0
- package/lib/core-runtime.js +232 -0
- package/lib/core-tasks.js +567 -0
- package/lib/core-user-projection.js +193 -0
- package/lib/doctor.js +92 -33
- package/lib/dsh-core-adapter.js +653 -0
- package/lib/dsh-host-adapter.js +58 -7
- package/lib/dsh-text-model-adapter.js +222 -0
- package/lib/dsh-vision-model-adapter.js +75 -0
- package/lib/generation-input.js +126 -0
- package/lib/guard.js +3 -3
- package/lib/headless-vision.js +181 -0
- package/lib/host-runtime.js +1 -0
- package/lib/http-vision-model-adapter.js +187 -0
- package/lib/image-edit-input.js +34 -0
- package/lib/index.js +323 -188
- package/lib/locate.js +86 -28
- package/lib/media-probe.js +2 -1
- package/lib/model-call-runtime.js +45 -0
- package/lib/model-invoker.js +220 -0
- package/lib/model-port-contract.js +341 -0
- package/lib/models.js +105 -14
- package/lib/ocr-model-routing.js +26 -0
- package/lib/ocr.js +110 -64
- package/lib/prompt-optimizer-config.js +2 -1
- package/lib/prompt-optimizer-core.js +121 -0
- package/lib/prompt-optimizer.js +37 -193
- package/lib/provider-adapter.js +49 -23
- package/lib/provider-adapters.js +165 -26
- package/lib/provider-catalog.js +183 -0
- package/lib/provider-config-service.js +236 -0
- package/lib/provider-contract.js +74 -4
- package/lib/provider-health.js +80 -1
- package/lib/provider-protocol.js +74 -2
- package/lib/provider-task-runner.js +370 -0
- package/lib/summarize.js +31 -20
- package/lib/summary-input.js +52 -0
- package/lib/task-wait.js +40 -0
- package/lib/tasks.js +16 -4
- package/lib/video-input.js +30 -0
- package/lib/vision-core.js +42 -0
- package/lib/vision-image-input.js +83 -0
- package/lib/vision-input.js +31 -0
- package/lib/vision-model-routing.js +180 -0
- package/lib/vision.js +31 -9
- package/lib/workbench-artifacts.js +52 -0
- package/lib/workbench-management.js +151 -0
- package/lib/workbench-zip.js +39 -0
- package/package.json +8 -8
- package/user_guide.md +71 -26
package/lib/config.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
+
import { normalizeVisionInputLimits } from './vision-image-input.js';
|
|
2
3
|
/**
|
|
3
4
|
* Iris 配置存储:$DSH_HOME/iris/v1/providers.json
|
|
4
5
|
* Iris 独立持有配置;旧工作台仅作为用户显式指定的一次性导入来源。
|
|
@@ -10,14 +11,15 @@ import os from 'node:os';
|
|
|
10
11
|
import * as cap from './capability.js';
|
|
11
12
|
import * as models from './models.js';
|
|
12
13
|
import { atomicWritePrivate, chmodPrivateFile, privateSibling } from './private-storage.js';
|
|
13
|
-
import {
|
|
14
|
+
import { selectMediaProtocol, selectImageProtocol, normalizeImageProtocol, isConfiguredProvider } from './provider-protocol.js';
|
|
14
15
|
import { redactProviderMessage } from './provider-contract.js';
|
|
15
16
|
import {
|
|
16
17
|
PROVIDER_HEALTH_FRESH_MS,
|
|
17
18
|
aggregateHealthStates,
|
|
18
19
|
healthObservationState,
|
|
19
20
|
isDefinitiveHealthFailure,
|
|
20
|
-
newestHealthEvidence
|
|
21
|
+
newestHealthEvidence,
|
|
22
|
+
modelRateLimit, recordModelRateLimit, clearModelRateLimit, clearModelCapabilityHealth
|
|
21
23
|
} from './provider-health.js';
|
|
22
24
|
|
|
23
25
|
function isPlainObject(value) {
|
|
@@ -32,6 +34,7 @@ function validateStoredConfig(value) {
|
|
|
32
34
|
if (!isPlainObject(provider)) throw new Error('provider 条目必须是对象');
|
|
33
35
|
if (provider.id !== undefined && typeof provider.id !== 'string') throw new Error('provider.id 必须是字符串');
|
|
34
36
|
if (provider.baseUrl !== undefined && typeof provider.baseUrl !== 'string') throw new Error('provider.baseUrl 必须是字符串');
|
|
37
|
+
if (provider.mediaBaseUrl !== undefined && typeof provider.mediaBaseUrl !== 'string') throw new Error('provider.mediaBaseUrl 必须是字符串');
|
|
35
38
|
if (provider.apiKey !== undefined && typeof provider.apiKey !== 'string') throw new Error('provider.apiKey 必须是字符串');
|
|
36
39
|
if (provider.models !== undefined && !Array.isArray(provider.models)) throw new Error('provider.models 必须是数组');
|
|
37
40
|
for (const model of provider.models || []) {
|
|
@@ -43,10 +46,27 @@ function validateStoredConfig(value) {
|
|
|
43
46
|
}
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
/**
|
|
50
|
+
* 供应商认证方式:'bearer'(默认,需要 apiKey)或 'none'(本地/内网无鉴权端点)。
|
|
51
|
+
* T-01:解除 ComfyUI、Ollama、本地 TTS 等本地部署的硬编码阻塞(审计 C-1)。
|
|
52
|
+
* 无 auth 字段时一律视为 'bearer'——现有用户行为完全不变。
|
|
53
|
+
*/
|
|
54
|
+
function authModeOf(provider) {
|
|
55
|
+
return provider && provider.auth === 'none' ? 'none' : 'bearer';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 该供应商是否缺少必需凭据('none' 认证永不缺)。 */
|
|
59
|
+
function missingCredentials(provider) {
|
|
60
|
+
if (!provider || authModeOf(provider) === 'none') return false;
|
|
61
|
+
return typeof provider.apiKey !== 'string' || !provider.apiKey.trim();
|
|
62
|
+
}
|
|
63
|
+
|
|
46
64
|
function normalizeStoredProvider(provider) {
|
|
47
|
-
|
|
48
|
-
provider.mediaProtocol
|
|
49
|
-
|
|
65
|
+
const selected = selectMediaProtocol(provider);
|
|
66
|
+
const changed = provider.mediaProtocol !== selected.mediaProtocol
|
|
67
|
+
|| (provider.protocolInferred === true) !== selected.protocolInferred;
|
|
68
|
+
Object.assign(provider, selected);
|
|
69
|
+
return models.migrateProviderModels(provider) || changed;
|
|
50
70
|
}
|
|
51
71
|
|
|
52
72
|
export function irisHome() {
|
|
@@ -134,15 +154,17 @@ export function importFromWorkbench(workbenchConfigPath) {
|
|
|
134
154
|
for (const p of list) {
|
|
135
155
|
if (!p || typeof p.apiKey !== 'string' || !p.apiKey.trim() ||
|
|
136
156
|
typeof p.baseUrl !== 'string' || !p.baseUrl.trim()) continue;
|
|
137
|
-
|
|
157
|
+
const imported = {
|
|
138
158
|
id: 'iris_' + Math.random().toString(36).slice(2, 8),
|
|
139
159
|
name: p.name || p.id,
|
|
140
|
-
type: p.type === '
|
|
160
|
+
type: typeof p.type === 'string' && p.type.trim() ? p.type.trim() : 'openai',
|
|
141
161
|
baseUrl: p.baseUrl,
|
|
142
162
|
apiKey: p.apiKey,
|
|
143
163
|
enabled: p.enabled !== false,
|
|
144
|
-
mediaProtocol:
|
|
145
|
-
}
|
|
164
|
+
mediaProtocol: p.mediaProtocol
|
|
165
|
+
};
|
|
166
|
+
normalizeStoredProvider(imported);
|
|
167
|
+
c.providers.push(imported);
|
|
146
168
|
n++;
|
|
147
169
|
}
|
|
148
170
|
if (n) persist();
|
|
@@ -150,7 +172,7 @@ export function importFromWorkbench(workbenchConfigPath) {
|
|
|
150
172
|
}
|
|
151
173
|
|
|
152
174
|
export function providers() {
|
|
153
|
-
return load().providers.filter(
|
|
175
|
+
return load().providers.filter(isConfiguredProvider);
|
|
154
176
|
}
|
|
155
177
|
|
|
156
178
|
export function providerById(id) {
|
|
@@ -171,7 +193,7 @@ export function pickFor(capability) {
|
|
|
171
193
|
const pool = models.modelPool(providers());
|
|
172
194
|
let m = null;
|
|
173
195
|
for (const ref of assignmentOrder(capability)) {
|
|
174
|
-
m = pool.find((x) => x.ref === ref && x.capabilities.includes(capability));
|
|
196
|
+
m = pool.find((x) => !x.rateLimited && x.ref === ref && x.capabilities.includes(capability));
|
|
175
197
|
if (m) break;
|
|
176
198
|
}
|
|
177
199
|
if (!m) m = models.pickModel(pool, capability);
|
|
@@ -187,18 +209,9 @@ export function pickFor(capability) {
|
|
|
187
209
|
* 已分配顺序优先(阶段 6 条目 4),其余按池顺序补齐(去重)。
|
|
188
210
|
* 一个 provider 若池里有多个该能力模型,会返回多条(各自带不同模型)。
|
|
189
211
|
*/
|
|
190
|
-
export function pickAllFor(capability) {
|
|
212
|
+
export function pickAllFor(capability, { includeRateLimited = false } = {}) {
|
|
191
213
|
const pool = models.modelPool(providers());
|
|
192
|
-
const chosen = [];
|
|
193
|
-
const seen = new Set();
|
|
194
|
-
const add = (m) => {
|
|
195
|
-
const k = m.providerId + '\u0000' + m.id;
|
|
196
|
-
if (!seen.has(k)) { seen.add(k); chosen.push(m); }
|
|
197
|
-
};
|
|
198
|
-
for (const ref of assignmentOrder(capability)) {
|
|
199
|
-
for (const m of pool) if (m.ref === ref && m.capabilities.includes(capability)) add(m);
|
|
200
|
-
}
|
|
201
|
-
for (const m of pool) if (m.capabilities.includes(capability)) add(m);
|
|
214
|
+
const chosen = models.orderedModels(pool, capability, assignments()[capability], { includeRateLimited });
|
|
202
215
|
const out = [];
|
|
203
216
|
for (const m of chosen) {
|
|
204
217
|
const p = providerById(m.providerId);
|
|
@@ -214,21 +227,7 @@ export function assignments() {
|
|
|
214
227
|
return load().assignments || {};
|
|
215
228
|
}
|
|
216
229
|
|
|
217
|
-
|
|
218
|
-
let providerId = '';
|
|
219
|
-
let modelId = '';
|
|
220
|
-
if (raw && typeof raw === 'object') {
|
|
221
|
-
providerId = String(raw.providerId || '');
|
|
222
|
-
modelId = String(raw.modelId || raw.id || '');
|
|
223
|
-
} else if (typeof raw === 'string') {
|
|
224
|
-
const parsed = models.parseModelRef(raw);
|
|
225
|
-
if (parsed) ({ providerId, modelId } = parsed);
|
|
226
|
-
else modelId = raw; // v1 旧格式:纯 model id,按池顺序取第一个
|
|
227
|
-
}
|
|
228
|
-
return pool.find((m) => m.id === modelId
|
|
229
|
-
&& (!providerId || m.providerId === providerId)
|
|
230
|
-
&& m.capabilities.includes(capability)) || null;
|
|
231
|
-
}
|
|
230
|
+
const resolveAssignment = models.resolvePoolModel;
|
|
232
231
|
|
|
233
232
|
/** 归一化某能力的有序分配列表 → [providerId::modelId, ...]。 */
|
|
234
233
|
export function assignmentOrder(capability) {
|
|
@@ -302,7 +301,7 @@ export function capabilitiesOf(p) {
|
|
|
302
301
|
}
|
|
303
302
|
|
|
304
303
|
const HEALTH_SENSITIVE_FIELDS = new Set([
|
|
305
|
-
'type', 'baseUrl', 'apiKey', 'mediaProtocol',
|
|
304
|
+
'auth', 'type', 'baseUrl', 'mediaBaseUrl', 'apiKey', 'mediaProtocol',
|
|
306
305
|
'imageModel', 'videoModel', 'ttsModel', 'transcribeModel', 'visionModel'
|
|
307
306
|
]);
|
|
308
307
|
|
|
@@ -355,8 +354,10 @@ function applyProviderHealth(provider, modelId, capability, result, fallbackSour
|
|
|
355
354
|
health.observations.push(observation);
|
|
356
355
|
}
|
|
357
356
|
const evidence = observationEvidence(provider, result || {}, fallbackSource);
|
|
357
|
+
recordModelRateLimit(provider, model, { ...evidence, retryAfterMs: result?.retryAfterMs, until: result?.until, reason: result?.reason });
|
|
358
358
|
if (result && result.ok === true) {
|
|
359
359
|
observation.lastSuccess = evidence;
|
|
360
|
+
if (evidence.source === 'probe') clearModelRateLimit(provider, model);
|
|
360
361
|
} else if (isDefinitiveHealthFailure({ ...result, ...evidence })) {
|
|
361
362
|
observation.lastFailure = evidence;
|
|
362
363
|
} else {
|
|
@@ -394,10 +395,11 @@ function resetHealthOnCriticalChange(previous, next, patch) {
|
|
|
394
395
|
if (!changed) return;
|
|
395
396
|
const current = previous.health;
|
|
396
397
|
const revision = Number.isSafeInteger(current && current.revision) ? current.revision + 1 : 1;
|
|
397
|
-
next.health = { version: 1, revision, observations: []
|
|
398
|
+
next.health = { version: 1, revision, observations: [],
|
|
399
|
+
...(current?.rateLimits?.length ? { rateLimits: current.rateLimits } : {}) };
|
|
398
400
|
}
|
|
399
401
|
|
|
400
|
-
/**
|
|
402
|
+
/** 记录显式实测或真实任务观察;限流资格另存,保留原成功事实。 */
|
|
401
403
|
export function recordProviderHealth(id, modelId, capability, result) {
|
|
402
404
|
const c = load();
|
|
403
405
|
const provider = c.providers.find((item) => item.id === id);
|
|
@@ -408,10 +410,24 @@ export function recordProviderHealth(id, modelId, capability, result) {
|
|
|
408
410
|
return true;
|
|
409
411
|
}
|
|
410
412
|
|
|
413
|
+
/** DSH 调用边界记录冷却或明确耗尽;覆盖同账号该模型的所有能力。 */
|
|
414
|
+
export function recordRateLimit({ providerId, modelId, capability, httpStatus = 429, category = 'rate_limit', at, until, retryAfterMs, reason }) {
|
|
415
|
+
const known = { image: 'image-gen', video: 'video-gen' }[capability] || capability;
|
|
416
|
+
return recordProviderHealth(providerId, modelId, known, {
|
|
417
|
+
ok: false, source: 'task', category, httpStatus, at, until, retryAfterMs, reason,
|
|
418
|
+
note: category === 'quota' ? '额度或预算耗尽后已停用,手动实测成功后恢复' : '429 限流冷却,暂时跳过'
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
411
422
|
/** 返回单个 Provider × Model × Capability 的安全状态。 */
|
|
412
423
|
export function modelHealth(id, modelId, capability, { now = Date.now() } = {}) {
|
|
413
424
|
const provider = load().providers.find((item) => item.id === id);
|
|
414
|
-
if (!provider || provider.enabled === false ||
|
|
425
|
+
if (!provider || provider.enabled === false || missingCredentials(provider)) return { status: 'unconfigured' };
|
|
426
|
+
const blocked = modelRateLimit(provider, modelId, { now });
|
|
427
|
+
if (blocked) return { status: 'failed', rateLimited: true, observedAt: blocked.at,
|
|
428
|
+
source: blocked.source, category: blocked.category, httpStatus: blocked.httpStatus,
|
|
429
|
+
...(blocked.until ? { retryAt: blocked.until } : { reason: blocked.reason }),
|
|
430
|
+
note: blocked.until ? '429 限流冷却,暂时跳过' : '额度或预算耗尽后已停用,手动实测成功后恢复' };
|
|
415
431
|
const observation = healthObservation(provider, modelId, capability);
|
|
416
432
|
const status = healthObservationState(observation, { now });
|
|
417
433
|
const evidence = status === 'verified'
|
|
@@ -433,7 +449,7 @@ export function modelHealth(id, modelId, capability, { now = Date.now() } = {})
|
|
|
433
449
|
export function providerHealthSnapshot({ now = Date.now() } = {}) {
|
|
434
450
|
const capabilities = {};
|
|
435
451
|
for (const capability of Object.values(cap.CAPABILITIES)) {
|
|
436
|
-
const candidates = pickAllFor(capability).map((provider) => {
|
|
452
|
+
const candidates = pickAllFor(capability, { includeRateLimited: true }).map((provider) => {
|
|
437
453
|
const modelId = String(provider[FIELD_OF[capability]] || '');
|
|
438
454
|
const health = modelHealth(provider.id, modelId, capability, { now });
|
|
439
455
|
return { providerId: provider.id, modelId, ...health };
|
|
@@ -446,14 +462,21 @@ export function providerHealthSnapshot({ now = Date.now() } = {}) {
|
|
|
446
462
|
status,
|
|
447
463
|
candidateCount: candidates.length,
|
|
448
464
|
...(newest ? { observedAt: newest.observedAt } : {}),
|
|
465
|
+
...(status === 'failed' && candidates.every(item => item.retryAt) ? {
|
|
466
|
+
rateLimited: true, retryAt: candidates.map(item => item.retryAt).sort()[0]
|
|
467
|
+
} : {}),
|
|
449
468
|
candidates
|
|
450
469
|
};
|
|
451
470
|
}
|
|
471
|
+
const overall = aggregateHealthStates(Object.values(capabilities).map((item) => item.status));
|
|
472
|
+
const configured = Object.values(capabilities).filter(item => item.status !== 'unconfigured');
|
|
452
473
|
return {
|
|
453
474
|
schemaVersion: 1,
|
|
454
475
|
freshForMs: PROVIDER_HEALTH_FRESH_MS,
|
|
455
476
|
generatedAt: new Date(Number(now)).toISOString(),
|
|
456
|
-
overall
|
|
477
|
+
overall,
|
|
478
|
+
...(overall === 'failed' && configured.every(item => item.retryAt)
|
|
479
|
+
? { retryAt: configured.map(item => item.retryAt).sort()[0] } : {}),
|
|
457
480
|
capabilities
|
|
458
481
|
};
|
|
459
482
|
}
|
|
@@ -463,14 +486,19 @@ export function upsert(provider) {
|
|
|
463
486
|
const { id, ...input } = provider;
|
|
464
487
|
// PATCH 语义:缺失字段和 undefined 都不覆盖旧值;null/空串仍是显式更新。
|
|
465
488
|
const rest = Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
|
|
489
|
+
if (rest.visionInput !== undefined) rest.visionInput = normalizeVisionInputLimits(rest.visionInput) || null;
|
|
490
|
+
if (Array.isArray(rest.models)) rest.models = rest.models.map(model => typeof model === 'string' ? model : ({ ...model,
|
|
491
|
+
...(model?.imageProtocol !== undefined ? { imageProtocol: normalizeImageProtocol(model.imageProtocol) } : {}),
|
|
492
|
+
...(model?.visionInput !== undefined ? { visionInput: normalizeVisionInputLimits(model.visionInput) || null } : {}) }));
|
|
466
493
|
if (id) {
|
|
467
494
|
const i = c.providers.findIndex((p) => p.id === id);
|
|
468
495
|
if (i >= 0) {
|
|
469
496
|
const previous = c.providers[i];
|
|
470
|
-
if (rest.mediaProtocol === 'auto') rest.mediaProtocol = inferMediaProtocol(rest.baseUrl ?? previous.baseUrl);
|
|
471
497
|
const next = { ...previous, ...rest };
|
|
498
|
+
if (Object.prototype.hasOwnProperty.call(rest, 'mediaProtocol')) next.protocolInferred = false;
|
|
472
499
|
normalizeStoredProvider(next);
|
|
473
500
|
resetHealthOnCriticalChange(previous, next, rest);
|
|
501
|
+
resetChangedImageProtocolHealth(previous, next);
|
|
474
502
|
if (Object.prototype.hasOwnProperty.call(rest, 'models')) pruneHealthToEffectiveModels(next);
|
|
475
503
|
c.providers[i] = next;
|
|
476
504
|
persist();
|
|
@@ -478,7 +506,7 @@ export function upsert(provider) {
|
|
|
478
506
|
}
|
|
479
507
|
}
|
|
480
508
|
const fresh = { id: 'iris_' + Math.random().toString(36).slice(2, 8), ...rest };
|
|
481
|
-
|
|
509
|
+
fresh.protocolInferred = false;
|
|
482
510
|
normalizeStoredProvider(fresh);
|
|
483
511
|
c.providers.push(fresh);
|
|
484
512
|
persist();
|
|
@@ -500,37 +528,99 @@ export function removeProvider(id) {
|
|
|
500
528
|
return true;
|
|
501
529
|
}
|
|
502
530
|
|
|
503
|
-
/**
|
|
531
|
+
/** 显式替换模型池;空数组表示没有可用模型。发现使用合并入口。 */
|
|
504
532
|
export function setProviderModels(id, modelEntries) {
|
|
505
533
|
const c = load();
|
|
506
534
|
const p = c.providers.find((x) => x.id === id);
|
|
507
535
|
if (!p) return null;
|
|
536
|
+
const previous = { ...p };
|
|
508
537
|
if (Array.isArray(modelEntries)) {
|
|
509
|
-
//
|
|
510
|
-
const prev =
|
|
511
|
-
for (const m of p.models || []) if (m && m.id) prev
|
|
538
|
+
// 保留同名模型的实测结果;手工能力标注优先于后续自动发现。
|
|
539
|
+
const prev = new Map();
|
|
540
|
+
for (const m of p.models || []) if (m && m.id) prev.set(m.id, m);
|
|
512
541
|
p.models = modelEntries.map((m) => {
|
|
513
|
-
const entry = typeof m === 'string' ? { id: m } : { id: m.id, capabilities: m.capabilities
|
|
514
|
-
|
|
542
|
+
const entry = typeof m === 'string' ? { id: m } : { id: m.id, capabilities: m.capabilities,
|
|
543
|
+
...(m.imageProtocol !== undefined ? { imageProtocol: normalizeImageProtocol(m.imageProtocol) } : {}),
|
|
544
|
+
...(m.visionInput !== undefined ? { visionInput: normalizeVisionInputLimits(m.visionInput) || null } : {}) };
|
|
545
|
+
const old = prev.get(entry.id);
|
|
515
546
|
if (old) {
|
|
516
547
|
if (old.verified) entry.verified = old.verified;
|
|
517
548
|
if (old.source) entry.source = old.source;
|
|
549
|
+
if (old.source === 'manual' && Array.isArray(old.capabilities)) entry.capabilities = old.capabilities;
|
|
550
|
+
if (m?.imageProtocol === undefined && old.imageProtocol !== undefined) entry.imageProtocol = old.imageProtocol;
|
|
551
|
+
if (m?.visionInput === undefined && old.visionInput !== undefined) entry.visionInput = old.visionInput;
|
|
518
552
|
}
|
|
519
553
|
if (m && m.verified) entry.verified = m.verified;
|
|
520
554
|
if (m && m.source) entry.source = m.source;
|
|
521
555
|
return entry;
|
|
522
556
|
});
|
|
523
557
|
}
|
|
558
|
+
resetChangedImageProtocolHealth(previous, p);
|
|
524
559
|
pruneHealthToEffectiveModels(p);
|
|
525
560
|
persist();
|
|
526
561
|
return p;
|
|
527
562
|
}
|
|
528
563
|
|
|
564
|
+
/** 发现只追加或更新;用户手工项和未再返回的旧项始终保留。 */
|
|
565
|
+
export function mergeDiscoveredModels(id, modelEntries) {
|
|
566
|
+
const provider = load().providers.find((p) => p.id === id);
|
|
567
|
+
if (!provider) return null;
|
|
568
|
+
const merged = new Map(models.providerModels(provider).map((m) => [m.id, m]));
|
|
569
|
+
for (const model of modelEntries) {
|
|
570
|
+
const previous = merged.get(model.id);
|
|
571
|
+
if (previous?.source === 'manual') continue;
|
|
572
|
+
merged.set(model.id, { ...previous, ...model, source: 'discovered' });
|
|
573
|
+
}
|
|
574
|
+
return setProviderModels(id, [...merged.values()]);
|
|
575
|
+
}
|
|
576
|
+
|
|
529
577
|
/** 找 provider 里的某个模型条目(可变引用) */
|
|
530
578
|
function findModelEntry(p, modelId) {
|
|
531
579
|
return (p.models || []).find((m) => m && m.id === modelId);
|
|
532
580
|
}
|
|
533
581
|
|
|
582
|
+
function resetChangedImageProtocolHealth(previous, next) {
|
|
583
|
+
for (const model of models.providerModels(previous)) {
|
|
584
|
+
if (selectImageProtocol(previous, model.id).mediaProtocol !== selectImageProtocol(next, model.id).mediaProtocol) {
|
|
585
|
+
clearModelCapabilityHealth(next, model.id, cap.CAPABILITIES.IMAGE);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** 手动选择单模型图片协议;auto 恢复账号默认,不解除模型停用。 */
|
|
591
|
+
export function setModelImageProtocol(id, modelId, value) {
|
|
592
|
+
const protocol = normalizeImageProtocol(value);
|
|
593
|
+
const p = load().providers.find(provider => provider.id === id);
|
|
594
|
+
if (!p) return false;
|
|
595
|
+
const entries = Array.isArray(p.models) ? p.models : models.providerModels(p).map(({ id, capabilities }) => ({ id, capabilities }));
|
|
596
|
+
const index = entries.findIndex(model => (typeof model === 'string' ? model : model?.id)?.trim() === modelId);
|
|
597
|
+
if (index < 0) return false;
|
|
598
|
+
p.models = entries;
|
|
599
|
+
const entry = typeof p.models[index] === 'string' ? { id: p.models[index] } : p.models[index];
|
|
600
|
+
p.models[index] = entry;
|
|
601
|
+
const previous = selectImageProtocol(p, modelId).mediaProtocol;
|
|
602
|
+
if (protocol === 'auto') delete entry.imageProtocol;
|
|
603
|
+
else entry.imageProtocol = protocol;
|
|
604
|
+
if (previous !== selectImageProtocol(p, modelId).mediaProtocol) clearModelCapabilityHealth(p, modelId, cap.CAPABILITIES.IMAGE);
|
|
605
|
+
persist();
|
|
606
|
+
return true;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** 看图预算变更不会伪造实测成功、解除额度停用或改写原图。 */
|
|
610
|
+
export function setModelVisionInput(id, modelId, value) {
|
|
611
|
+
const limits = normalizeVisionInputLimits(value);
|
|
612
|
+
const p = load().providers.find(provider => provider.id === id);
|
|
613
|
+
if (!p) return false;
|
|
614
|
+
const entries = (Array.isArray(p.models) ? p.models : models.providerModels(p)).map(model => typeof model === 'string' ? { id: model } : { ...model });
|
|
615
|
+
const entry = entries.find(model => model.id === modelId);
|
|
616
|
+
if (!entry) return false;
|
|
617
|
+
if (limits) entry.visionInput = limits;
|
|
618
|
+
else delete entry.visionInput;
|
|
619
|
+
p.models = entries;
|
|
620
|
+
persist();
|
|
621
|
+
return true;
|
|
622
|
+
}
|
|
623
|
+
|
|
534
624
|
/** 手动添加一个模型到池(source=manual,能力由调用方给) */
|
|
535
625
|
export function addProviderModel(id, modelId, capabilities) {
|
|
536
626
|
const c = load();
|
|
@@ -546,6 +636,7 @@ export function addProviderModel(id, modelId, capabilities) {
|
|
|
546
636
|
} else if (Array.isArray(capabilities)) {
|
|
547
637
|
entry.capabilities = capabilities;
|
|
548
638
|
}
|
|
639
|
+
entry.source = 'manual';
|
|
549
640
|
pruneHealthToEffectiveModels(p);
|
|
550
641
|
persist();
|
|
551
642
|
return entry;
|
|
@@ -573,6 +664,7 @@ export function setModelCapabilities(id, modelId, capabilities) {
|
|
|
573
664
|
if (!entry) return false;
|
|
574
665
|
const nextCapabilities = (Array.isArray(capabilities) ? capabilities : []).filter((x) => cap.isKnownCapability(x));
|
|
575
666
|
entry.capabilities = nextCapabilities;
|
|
667
|
+
entry.source = 'manual';
|
|
576
668
|
pruneHealthToEffectiveModels(p);
|
|
577
669
|
persist();
|
|
578
670
|
return true;
|