@mokuyoaxis/dsh-iris 0.1.3 → 0.2.0
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 +23 -49
- package/.dsh/skills/iris-compose-media/references/task-v2-recovery.md +30 -0
- package/.dsh/skills/iris-compose-media/references/workflows.md +38 -0
- package/.dsh/skills/iris-verify-ui/SKILL.md +3 -25
- package/.dsh/skills/iris-verify-ui/agents/openai.yaml +4 -0
- package/.dsh/skills/iris-verify-ui/references/verdict-and-degradation.md +31 -0
- package/CHANGELOG.md +77 -2
- package/README.en.md +98 -19
- package/README.md +98 -19
- package/bin/dsh-iris.js +392 -13
- package/docs/AGENT_QUICKSTART.md +35 -0
- package/docs/ARCHITECTURE.md +82 -52
- package/docs/ARTIFACT_LIBRARY.md +24 -0
- package/docs/ARTIFACT_MANIFEST.md +60 -0
- package/docs/CORE_RUNTIME_CONTRACT.md +83 -0
- package/docs/DSH_CORE_MIGRATION.md +106 -0
- package/docs/DSH_RC2_ADAPTATION.md +36 -0
- package/docs/HEADLESS_CLI.md +217 -0
- package/docs/HOST_ADAPTER_CONTRACT.md +105 -0
- package/docs/HOST_DOCTOR.md +49 -0
- package/docs/MODEL_PORT_CONTRACT.md +300 -0
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +138 -0
- package/docs/PROVIDER_HEALTH.md +39 -0
- package/docs/PROVIDER_RUNTIME_HARNESS.md +33 -0
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +2 -2
- package/docs/ROADMAP.md +42 -76
- package/docs/SECURITY.md +10 -5
- package/docs/TASK_SEMANTICS.md +3 -2
- package/docs/assets/diagrams/iris-architecture.png +0 -0
- package/docs/assets/diagrams/iris-architecture.svg +5 -5
- 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/releases/0.1.4.md +59 -0
- package/docs/releases/0.2.0.md +48 -0
- package/lib/actions.js +673 -525
- package/lib/adapters.js +58 -21
- package/lib/api.js +182 -16
- package/lib/artifacts.js +239 -0
- package/lib/bundled-skills.js +5 -5
- package/lib/client.js +1116 -86
- package/lib/command-service.js +420 -0
- package/lib/config.js +245 -49
- package/lib/core-artifact-store.js +598 -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-runtime.js +232 -0
- package/lib/core-tasks.js +560 -0
- package/lib/core-user-projection.js +192 -0
- package/lib/doctor.js +260 -30
- package/lib/dsh-core-adapter.js +599 -0
- package/lib/dsh-host-adapter.js +240 -0
- package/lib/generation-input.js +95 -0
- package/lib/guard.js +3 -3
- package/lib/host-contract.js +131 -0
- package/lib/host-runtime.js +156 -0
- package/lib/index.js +255 -166
- package/lib/locate.js +1 -1
- package/lib/media.js +16 -6
- package/lib/model-invoker.js +218 -0
- package/lib/model-port-contract.js +333 -0
- package/lib/models.js +97 -11
- package/lib/prompt-optimizer.js +10 -20
- package/lib/provider-adapter.js +397 -0
- package/lib/provider-adapters.js +342 -0
- package/lib/provider-catalog.js +171 -0
- package/lib/provider-contract.js +72 -3
- package/lib/provider-health.js +59 -0
- package/lib/provider-protocol.js +51 -2
- package/lib/provider-task-runner.js +328 -0
- package/lib/render.js +43 -12
- package/lib/tasks.js +45 -9
- package/lib/vision.js +51 -56
- package/package.json +20 -14
- package/user_guide.md +67 -12
- package/docs/assets/diagrams/iris-architecture.drawio +0 -1
- package/docs/assets/diagrams/iris-task-lifecycle.drawio +0 -1
- package/docs/assets/diagrams/iris-workflow-compose.drawio +0 -1
- package/docs/releases/0.1.1.md +0 -51
- package/docs/releases/0.1.2.md +0 -26
- package/docs/releases/0.1.3.md +0 -30
package/lib/config.js
CHANGED
|
@@ -10,7 +10,15 @@ import os from 'node:os';
|
|
|
10
10
|
import * as cap from './capability.js';
|
|
11
11
|
import * as models from './models.js';
|
|
12
12
|
import { atomicWritePrivate, chmodPrivateFile, privateSibling } from './private-storage.js';
|
|
13
|
-
import {
|
|
13
|
+
import { selectMediaProtocol, isConfiguredProvider } from './provider-protocol.js';
|
|
14
|
+
import { redactProviderMessage } from './provider-contract.js';
|
|
15
|
+
import {
|
|
16
|
+
PROVIDER_HEALTH_FRESH_MS,
|
|
17
|
+
aggregateHealthStates,
|
|
18
|
+
healthObservationState,
|
|
19
|
+
isDefinitiveHealthFailure,
|
|
20
|
+
newestHealthEvidence
|
|
21
|
+
} from './provider-health.js';
|
|
14
22
|
|
|
15
23
|
function isPlainObject(value) {
|
|
16
24
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
@@ -24,6 +32,7 @@ function validateStoredConfig(value) {
|
|
|
24
32
|
if (!isPlainObject(provider)) throw new Error('provider 条目必须是对象');
|
|
25
33
|
if (provider.id !== undefined && typeof provider.id !== 'string') throw new Error('provider.id 必须是字符串');
|
|
26
34
|
if (provider.baseUrl !== undefined && typeof provider.baseUrl !== 'string') throw new Error('provider.baseUrl 必须是字符串');
|
|
35
|
+
if (provider.mediaBaseUrl !== undefined && typeof provider.mediaBaseUrl !== 'string') throw new Error('provider.mediaBaseUrl 必须是字符串');
|
|
27
36
|
if (provider.apiKey !== undefined && typeof provider.apiKey !== 'string') throw new Error('provider.apiKey 必须是字符串');
|
|
28
37
|
if (provider.models !== undefined && !Array.isArray(provider.models)) throw new Error('provider.models 必须是数组');
|
|
29
38
|
for (const model of provider.models || []) {
|
|
@@ -35,10 +44,27 @@ function validateStoredConfig(value) {
|
|
|
35
44
|
}
|
|
36
45
|
}
|
|
37
46
|
|
|
47
|
+
/**
|
|
48
|
+
* 供应商认证方式:'bearer'(默认,需要 apiKey)或 'none'(本地/内网无鉴权端点)。
|
|
49
|
+
* T-01:解除 ComfyUI、Ollama、本地 TTS 等本地部署的硬编码阻塞(审计 C-1)。
|
|
50
|
+
* 无 auth 字段时一律视为 'bearer'——现有用户行为完全不变。
|
|
51
|
+
*/
|
|
52
|
+
function authModeOf(provider) {
|
|
53
|
+
return provider && provider.auth === 'none' ? 'none' : 'bearer';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 该供应商是否缺少必需凭据('none' 认证永不缺)。 */
|
|
57
|
+
function missingCredentials(provider) {
|
|
58
|
+
if (!provider || authModeOf(provider) === 'none') return false;
|
|
59
|
+
return typeof provider.apiKey !== 'string' || !provider.apiKey.trim();
|
|
60
|
+
}
|
|
61
|
+
|
|
38
62
|
function normalizeStoredProvider(provider) {
|
|
39
|
-
|
|
40
|
-
provider.mediaProtocol
|
|
41
|
-
|
|
63
|
+
const selected = selectMediaProtocol(provider);
|
|
64
|
+
const changed = provider.mediaProtocol !== selected.mediaProtocol
|
|
65
|
+
|| (provider.protocolInferred === true) !== selected.protocolInferred;
|
|
66
|
+
Object.assign(provider, selected);
|
|
67
|
+
return models.migrateProviderModels(provider) || changed;
|
|
42
68
|
}
|
|
43
69
|
|
|
44
70
|
export function irisHome() {
|
|
@@ -126,15 +152,17 @@ export function importFromWorkbench(workbenchConfigPath) {
|
|
|
126
152
|
for (const p of list) {
|
|
127
153
|
if (!p || typeof p.apiKey !== 'string' || !p.apiKey.trim() ||
|
|
128
154
|
typeof p.baseUrl !== 'string' || !p.baseUrl.trim()) continue;
|
|
129
|
-
|
|
155
|
+
const imported = {
|
|
130
156
|
id: 'iris_' + Math.random().toString(36).slice(2, 8),
|
|
131
157
|
name: p.name || p.id,
|
|
132
|
-
type: p.type === '
|
|
158
|
+
type: typeof p.type === 'string' && p.type.trim() ? p.type.trim() : 'openai',
|
|
133
159
|
baseUrl: p.baseUrl,
|
|
134
160
|
apiKey: p.apiKey,
|
|
135
161
|
enabled: p.enabled !== false,
|
|
136
|
-
mediaProtocol:
|
|
137
|
-
}
|
|
162
|
+
mediaProtocol: p.mediaProtocol
|
|
163
|
+
};
|
|
164
|
+
normalizeStoredProvider(imported);
|
|
165
|
+
c.providers.push(imported);
|
|
138
166
|
n++;
|
|
139
167
|
}
|
|
140
168
|
if (n) persist();
|
|
@@ -142,7 +170,7 @@ export function importFromWorkbench(workbenchConfigPath) {
|
|
|
142
170
|
}
|
|
143
171
|
|
|
144
172
|
export function providers() {
|
|
145
|
-
return load().providers.filter(
|
|
173
|
+
return load().providers.filter(isConfiguredProvider);
|
|
146
174
|
}
|
|
147
175
|
|
|
148
176
|
export function providerById(id) {
|
|
@@ -181,16 +209,7 @@ export function pickFor(capability) {
|
|
|
181
209
|
*/
|
|
182
210
|
export function pickAllFor(capability) {
|
|
183
211
|
const pool = models.modelPool(providers());
|
|
184
|
-
const chosen = [];
|
|
185
|
-
const seen = new Set();
|
|
186
|
-
const add = (m) => {
|
|
187
|
-
const k = m.providerId + '\u0000' + m.id;
|
|
188
|
-
if (!seen.has(k)) { seen.add(k); chosen.push(m); }
|
|
189
|
-
};
|
|
190
|
-
for (const ref of assignmentOrder(capability)) {
|
|
191
|
-
for (const m of pool) if (m.ref === ref && m.capabilities.includes(capability)) add(m);
|
|
192
|
-
}
|
|
193
|
-
for (const m of pool) if (m.capabilities.includes(capability)) add(m);
|
|
212
|
+
const chosen = models.orderedModels(pool, capability, assignments()[capability]);
|
|
194
213
|
const out = [];
|
|
195
214
|
for (const m of chosen) {
|
|
196
215
|
const p = providerById(m.providerId);
|
|
@@ -206,21 +225,7 @@ export function assignments() {
|
|
|
206
225
|
return load().assignments || {};
|
|
207
226
|
}
|
|
208
227
|
|
|
209
|
-
|
|
210
|
-
let providerId = '';
|
|
211
|
-
let modelId = '';
|
|
212
|
-
if (raw && typeof raw === 'object') {
|
|
213
|
-
providerId = String(raw.providerId || '');
|
|
214
|
-
modelId = String(raw.modelId || raw.id || '');
|
|
215
|
-
} else if (typeof raw === 'string') {
|
|
216
|
-
const parsed = models.parseModelRef(raw);
|
|
217
|
-
if (parsed) ({ providerId, modelId } = parsed);
|
|
218
|
-
else modelId = raw; // v1 旧格式:纯 model id,按池顺序取第一个
|
|
219
|
-
}
|
|
220
|
-
return pool.find((m) => m.id === modelId
|
|
221
|
-
&& (!providerId || m.providerId === providerId)
|
|
222
|
-
&& m.capabilities.includes(capability)) || null;
|
|
223
|
-
}
|
|
228
|
+
const resolveAssignment = models.resolvePoolModel;
|
|
224
229
|
|
|
225
230
|
/** 归一化某能力的有序分配列表 → [providerId::modelId, ...]。 */
|
|
226
231
|
export function assignmentOrder(capability) {
|
|
@@ -293,6 +298,163 @@ export function capabilitiesOf(p) {
|
|
|
293
298
|
return [...caps];
|
|
294
299
|
}
|
|
295
300
|
|
|
301
|
+
const HEALTH_SENSITIVE_FIELDS = new Set([
|
|
302
|
+
'auth', 'type', 'baseUrl', 'mediaBaseUrl', 'apiKey', 'mediaProtocol',
|
|
303
|
+
'imageModel', 'videoModel', 'ttsModel', 'transcribeModel', 'visionModel'
|
|
304
|
+
]);
|
|
305
|
+
|
|
306
|
+
function healthContainer(provider) {
|
|
307
|
+
const current = provider.health;
|
|
308
|
+
if (current && current.version === 1 && Number.isSafeInteger(current.revision)
|
|
309
|
+
&& Array.isArray(current.observations)) return current;
|
|
310
|
+
const revision = Number.isSafeInteger(current && current.revision) ? current.revision : 0;
|
|
311
|
+
provider.health = { version: 1, revision, observations: [] };
|
|
312
|
+
return provider.health;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function healthObservation(provider, modelId, capability) {
|
|
316
|
+
const health = provider && provider.health;
|
|
317
|
+
if (!health || health.version !== 1 || !Array.isArray(health.observations)) return null;
|
|
318
|
+
return health.observations.find((item) => item && item.modelId === modelId
|
|
319
|
+
&& item.capability === capability) || null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function safeHealthNote(provider, value) {
|
|
323
|
+
const raw = String(value ?? '').trim();
|
|
324
|
+
if (!raw) return '';
|
|
325
|
+
let note = redactProviderMessage(raw, 200);
|
|
326
|
+
const key = String(provider && provider.apiKey || '');
|
|
327
|
+
if (key) note = note.split(key).join('[REDACTED]');
|
|
328
|
+
return note;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function observationEvidence(provider, result, fallbackSource) {
|
|
332
|
+
const atMs = Date.parse(String(result && result.at || ''));
|
|
333
|
+
const status = Number(result && (result.httpStatus ?? result.status));
|
|
334
|
+
const note = safeHealthNote(provider, result && (result.note || result.safeMessage || result.message));
|
|
335
|
+
return {
|
|
336
|
+
at: new Date(Number.isFinite(atMs) ? atMs : Date.now()).toISOString(),
|
|
337
|
+
source: ['probe', 'task'].includes(result && result.source) ? result.source : fallbackSource,
|
|
338
|
+
category: String(result && result.category || (result && result.ok ? 'success' : 'unknown')).slice(0, 40),
|
|
339
|
+
...(Number.isInteger(status) && status >= 100 && status <= 599 ? { httpStatus: status } : {}),
|
|
340
|
+
...(note ? { note } : {})
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function applyProviderHealth(provider, modelId, capability, result, fallbackSource = 'task') {
|
|
345
|
+
const model = String(modelId || '').trim();
|
|
346
|
+
if (!provider || !model || !cap.isKnownCapability(capability)) return false;
|
|
347
|
+
const health = healthContainer(provider);
|
|
348
|
+
let observation = health.observations.find((item) => item && item.modelId === model
|
|
349
|
+
&& item.capability === capability);
|
|
350
|
+
if (!observation) {
|
|
351
|
+
observation = { modelId: model, capability };
|
|
352
|
+
health.observations.push(observation);
|
|
353
|
+
}
|
|
354
|
+
const evidence = observationEvidence(provider, result || {}, fallbackSource);
|
|
355
|
+
if (result && result.ok === true) {
|
|
356
|
+
observation.lastSuccess = evidence;
|
|
357
|
+
} else if (isDefinitiveHealthFailure({ ...result, ...evidence })) {
|
|
358
|
+
observation.lastFailure = evidence;
|
|
359
|
+
} else {
|
|
360
|
+
observation.lastTransient = evidence;
|
|
361
|
+
}
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function pruneHealthToEffectiveModels(provider) {
|
|
366
|
+
if (!provider) return;
|
|
367
|
+
const allowedByModel = new Map();
|
|
368
|
+
for (const entry of models.providerModels(provider)) {
|
|
369
|
+
allowedByModel.set(entry.id, new Set(entry.capabilities));
|
|
370
|
+
}
|
|
371
|
+
if (provider.health && Array.isArray(provider.health.observations)) {
|
|
372
|
+
provider.health.observations = provider.health.observations.filter((item) =>
|
|
373
|
+
item && allowedByModel.get(item.modelId)?.has(item.capability));
|
|
374
|
+
}
|
|
375
|
+
for (const entry of provider.models || []) {
|
|
376
|
+
if (!entry || typeof entry !== 'object' || !entry.verified || typeof entry.verified !== 'object') continue;
|
|
377
|
+
const allowed = allowedByModel.get(entry.id) || new Set();
|
|
378
|
+
entry.verified = Object.fromEntries(Object.entries(entry.verified).filter(([capability]) => allowed.has(capability)));
|
|
379
|
+
if (!Object.keys(entry.verified).length) delete entry.verified;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function pruneModelHealth(provider, modelId) {
|
|
384
|
+
if (!provider || !provider.health || !Array.isArray(provider.health.observations)) return;
|
|
385
|
+
provider.health.observations = provider.health.observations.filter((item) => item.modelId !== modelId);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function resetHealthOnCriticalChange(previous, next, patch) {
|
|
389
|
+
const changed = [...HEALTH_SENSITIVE_FIELDS].some((key) =>
|
|
390
|
+
Object.prototype.hasOwnProperty.call(patch, key) && previous[key] !== next[key]);
|
|
391
|
+
if (!changed) return;
|
|
392
|
+
const current = previous.health;
|
|
393
|
+
const revision = Number.isSafeInteger(current && current.revision) ? current.revision + 1 : 1;
|
|
394
|
+
next.health = { version: 1, revision, observations: [] };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** 记录一次显式实测或真实任务观察;临时错误不会覆盖近期成功。 */
|
|
398
|
+
export function recordProviderHealth(id, modelId, capability, result) {
|
|
399
|
+
const c = load();
|
|
400
|
+
const provider = c.providers.find((item) => item.id === id);
|
|
401
|
+
if (!provider || !applyProviderHealth(provider, modelId, capability, result, result && result.source || 'task')) {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
persist();
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** 返回单个 Provider × Model × Capability 的安全状态。 */
|
|
409
|
+
export function modelHealth(id, modelId, capability, { now = Date.now() } = {}) {
|
|
410
|
+
const provider = load().providers.find((item) => item.id === id);
|
|
411
|
+
if (!provider || provider.enabled === false || missingCredentials(provider)) return { status: 'unconfigured' };
|
|
412
|
+
const observation = healthObservation(provider, modelId, capability);
|
|
413
|
+
const status = healthObservationState(observation, { now });
|
|
414
|
+
const evidence = status === 'verified'
|
|
415
|
+
? observation && observation.lastSuccess
|
|
416
|
+
: (status === 'failed' ? observation && observation.lastFailure : newestHealthEvidence(observation));
|
|
417
|
+
return {
|
|
418
|
+
status,
|
|
419
|
+
...(evidence ? {
|
|
420
|
+
observedAt: evidence.at,
|
|
421
|
+
source: evidence.source,
|
|
422
|
+
category: evidence.category,
|
|
423
|
+
...(evidence.httpStatus ? { httpStatus: evidence.httpStatus } : {}),
|
|
424
|
+
...(evidence.note ? { note: evidence.note } : {})
|
|
425
|
+
} : {})
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** 当前有序 failover 路径的健康快照;只含脱敏标量,不触发网络。 */
|
|
430
|
+
export function providerHealthSnapshot({ now = Date.now() } = {}) {
|
|
431
|
+
const capabilities = {};
|
|
432
|
+
for (const capability of Object.values(cap.CAPABILITIES)) {
|
|
433
|
+
const candidates = pickAllFor(capability).map((provider) => {
|
|
434
|
+
const modelId = String(provider[FIELD_OF[capability]] || '');
|
|
435
|
+
const health = modelHealth(provider.id, modelId, capability, { now });
|
|
436
|
+
return { providerId: provider.id, modelId, ...health };
|
|
437
|
+
});
|
|
438
|
+
const status = aggregateHealthStates(candidates.map((item) => item.status));
|
|
439
|
+
const newest = [...candidates]
|
|
440
|
+
.filter((item) => item.status === status && item.observedAt)
|
|
441
|
+
.sort((a, b) => String(b.observedAt).localeCompare(String(a.observedAt)))[0];
|
|
442
|
+
capabilities[capability] = {
|
|
443
|
+
status,
|
|
444
|
+
candidateCount: candidates.length,
|
|
445
|
+
...(newest ? { observedAt: newest.observedAt } : {}),
|
|
446
|
+
candidates
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
schemaVersion: 1,
|
|
451
|
+
freshForMs: PROVIDER_HEALTH_FRESH_MS,
|
|
452
|
+
generatedAt: new Date(Number(now)).toISOString(),
|
|
453
|
+
overall: aggregateHealthStates(Object.values(capabilities).map((item) => item.status)),
|
|
454
|
+
capabilities
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
296
458
|
export function upsert(provider) {
|
|
297
459
|
const c = load();
|
|
298
460
|
const { id, ...input } = provider;
|
|
@@ -301,15 +463,19 @@ export function upsert(provider) {
|
|
|
301
463
|
if (id) {
|
|
302
464
|
const i = c.providers.findIndex((p) => p.id === id);
|
|
303
465
|
if (i >= 0) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
466
|
+
const previous = c.providers[i];
|
|
467
|
+
const next = { ...previous, ...rest };
|
|
468
|
+
if (Object.prototype.hasOwnProperty.call(rest, 'mediaProtocol')) next.protocolInferred = false;
|
|
469
|
+
normalizeStoredProvider(next);
|
|
470
|
+
resetHealthOnCriticalChange(previous, next, rest);
|
|
471
|
+
if (Object.prototype.hasOwnProperty.call(rest, 'models')) pruneHealthToEffectiveModels(next);
|
|
472
|
+
c.providers[i] = next;
|
|
307
473
|
persist();
|
|
308
|
-
return
|
|
474
|
+
return next;
|
|
309
475
|
}
|
|
310
476
|
}
|
|
311
477
|
const fresh = { id: 'iris_' + Math.random().toString(36).slice(2, 8), ...rest };
|
|
312
|
-
|
|
478
|
+
fresh.protocolInferred = false;
|
|
313
479
|
normalizeStoredProvider(fresh);
|
|
314
480
|
c.providers.push(fresh);
|
|
315
481
|
persist();
|
|
@@ -331,31 +497,46 @@ export function removeProvider(id) {
|
|
|
331
497
|
return true;
|
|
332
498
|
}
|
|
333
499
|
|
|
334
|
-
/**
|
|
500
|
+
/** 显式替换模型池;空数组表示没有可用模型。发现使用合并入口。 */
|
|
335
501
|
export function setProviderModels(id, modelEntries) {
|
|
336
502
|
const c = load();
|
|
337
503
|
const p = c.providers.find((x) => x.id === id);
|
|
338
504
|
if (!p) return null;
|
|
339
505
|
if (Array.isArray(modelEntries)) {
|
|
340
|
-
//
|
|
341
|
-
const prev =
|
|
342
|
-
for (const m of p.models || []) if (m && m.id) prev
|
|
506
|
+
// 保留同名模型的实测结果;手工能力标注优先于后续自动发现。
|
|
507
|
+
const prev = new Map();
|
|
508
|
+
for (const m of p.models || []) if (m && m.id) prev.set(m.id, m);
|
|
343
509
|
p.models = modelEntries.map((m) => {
|
|
344
510
|
const entry = typeof m === 'string' ? { id: m } : { id: m.id, capabilities: m.capabilities };
|
|
345
|
-
const old = prev
|
|
511
|
+
const old = prev.get(entry.id);
|
|
346
512
|
if (old) {
|
|
347
513
|
if (old.verified) entry.verified = old.verified;
|
|
348
514
|
if (old.source) entry.source = old.source;
|
|
515
|
+
if (old.source === 'manual' && Array.isArray(old.capabilities)) entry.capabilities = old.capabilities;
|
|
349
516
|
}
|
|
350
517
|
if (m && m.verified) entry.verified = m.verified;
|
|
351
518
|
if (m && m.source) entry.source = m.source;
|
|
352
519
|
return entry;
|
|
353
520
|
});
|
|
354
521
|
}
|
|
522
|
+
pruneHealthToEffectiveModels(p);
|
|
355
523
|
persist();
|
|
356
524
|
return p;
|
|
357
525
|
}
|
|
358
526
|
|
|
527
|
+
/** 发现只追加或更新;用户手工项和未再返回的旧项始终保留。 */
|
|
528
|
+
export function mergeDiscoveredModels(id, modelEntries) {
|
|
529
|
+
const provider = load().providers.find((p) => p.id === id);
|
|
530
|
+
if (!provider) return null;
|
|
531
|
+
const merged = new Map(models.providerModels(provider).map((m) => [m.id, m]));
|
|
532
|
+
for (const model of modelEntries) {
|
|
533
|
+
const previous = merged.get(model.id);
|
|
534
|
+
if (previous?.source === 'manual') continue;
|
|
535
|
+
merged.set(model.id, { ...previous, ...model, source: 'discovered' });
|
|
536
|
+
}
|
|
537
|
+
return setProviderModels(id, [...merged.values()]);
|
|
538
|
+
}
|
|
539
|
+
|
|
359
540
|
/** 找 provider 里的某个模型条目(可变引用) */
|
|
360
541
|
function findModelEntry(p, modelId) {
|
|
361
542
|
return (p.models || []).find((m) => m && m.id === modelId);
|
|
@@ -376,6 +557,8 @@ export function addProviderModel(id, modelId, capabilities) {
|
|
|
376
557
|
} else if (Array.isArray(capabilities)) {
|
|
377
558
|
entry.capabilities = capabilities;
|
|
378
559
|
}
|
|
560
|
+
entry.source = 'manual';
|
|
561
|
+
pruneHealthToEffectiveModels(p);
|
|
379
562
|
persist();
|
|
380
563
|
return entry;
|
|
381
564
|
}
|
|
@@ -388,6 +571,7 @@ export function removeProviderModel(id, modelId) {
|
|
|
388
571
|
const i = p.models.findIndex((m) => m && m.id === modelId);
|
|
389
572
|
if (i < 0) return false;
|
|
390
573
|
p.models.splice(i, 1);
|
|
574
|
+
pruneModelHealth(p, modelId);
|
|
391
575
|
persist();
|
|
392
576
|
return true;
|
|
393
577
|
}
|
|
@@ -399,20 +583,32 @@ export function setModelCapabilities(id, modelId, capabilities) {
|
|
|
399
583
|
if (!p) return false;
|
|
400
584
|
const entry = findModelEntry(p, modelId);
|
|
401
585
|
if (!entry) return false;
|
|
402
|
-
|
|
586
|
+
const nextCapabilities = (Array.isArray(capabilities) ? capabilities : []).filter((x) => cap.isKnownCapability(x));
|
|
587
|
+
entry.capabilities = nextCapabilities;
|
|
588
|
+
entry.source = 'manual';
|
|
589
|
+
pruneHealthToEffectiveModels(p);
|
|
403
590
|
persist();
|
|
404
591
|
return true;
|
|
405
592
|
}
|
|
406
593
|
|
|
407
|
-
/**
|
|
594
|
+
/** 记录某模型的显式实测结果,并同步到新健康事实。 */
|
|
408
595
|
export function setModelVerified(id, modelId, capability, result) {
|
|
409
596
|
const c = load();
|
|
410
597
|
const p = c.providers.find((x) => x.id === id);
|
|
411
598
|
if (!p) return false;
|
|
412
599
|
const entry = findModelEntry(p, modelId);
|
|
413
|
-
|
|
414
|
-
if (
|
|
415
|
-
|
|
600
|
+
const at = new Date().toISOString();
|
|
601
|
+
if (entry) {
|
|
602
|
+
if (!entry.verified) entry.verified = {};
|
|
603
|
+
entry.verified[capability] = {
|
|
604
|
+
ok: !!result.ok,
|
|
605
|
+
at,
|
|
606
|
+
note: safeHealthNote(p, result.note || ''),
|
|
607
|
+
source: 'probe',
|
|
608
|
+
category: String(result.category || (result.ok ? 'success' : 'unknown')).slice(0, 40)
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
applyProviderHealth(p, modelId, capability, { ...result, at, source: 'probe' }, 'probe');
|
|
416
612
|
persist();
|
|
417
613
|
return true;
|
|
418
614
|
}
|