@mokuyoaxis/dsh-iris 0.2.2 → 0.2.4
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 -73
- package/.dsh/skills/iris-compose-media/agents/openai.yaml +1 -1
- package/.dsh/skills/iris-compose-media/references/task-v2-recovery.md +23 -22
- package/.dsh/skills/iris-compose-media/references/workflows.md +25 -30
- package/.dsh/skills/iris-use/SKILL.md +42 -0
- package/.dsh/skills/iris-use/agents/openai.yaml +4 -0
- package/.dsh/skills/iris-use/references/media-contract.md +83 -0
- package/.dsh/skills/iris-verify-ui/SKILL.md +9 -7
- package/.dsh/skills/iris-verify-ui/agents/openai.yaml +1 -1
- package/.dsh/skills/iris-verify-ui/references/verdict-and-degradation.md +3 -0
- package/CHANGELOG.md +41 -2
- package/README.en.md +50 -38
- package/README.md +48 -37
- package/bin/dsh-iris.js +35 -6
- package/docs/AGENT_QUICKSTART.md +9 -9
- package/docs/ARCHITECTURE.md +4 -2
- package/docs/CLI_MANAGEMENT.md +11 -3
- package/docs/CORE_RUNTIME_CONTRACT.md +3 -1
- package/docs/DSH_COMPATIBILITY.md +43 -0
- package/docs/DSH_CORE_MIGRATION.md +6 -2
- package/docs/DSH_RC2_ADAPTATION.md +4 -2
- package/docs/FAULT_INJECTION.md +1 -0
- package/docs/FUNCTION_MODEL_ASSIGNMENTS.md +63 -0
- package/docs/HEADLESS_CLI.md +14 -8
- package/docs/HOST_ADAPTER_CONTRACT.md +10 -7
- package/docs/HOST_DOCTOR.md +6 -4
- package/docs/IMAGE_IMPORT.md +49 -0
- package/docs/MODEL_PORT_CONTRACT.md +6 -2
- package/docs/MUSIC.md +77 -0
- package/docs/PROMPT_OPTIMIZER.md +48 -10
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +2 -0
- package/docs/ROADMAP.md +16 -25
- package/docs/SECURITY.md +3 -2
- package/docs/SYNC_DELIVERY_RECOVERY.md +39 -0
- package/docs/TASK_SEMANTICS.md +4 -4
- package/docs/VISION_CLI.md +3 -3
- package/docs/VISION_MODEL.md +1 -1
- package/docs/WORKBENCH_ARTIFACTS.md +4 -2
- package/docs/releases/0.2.3.md +54 -0
- package/docs/releases/0.2.4.md +27 -0
- package/lib/actions.js +71 -14
- package/lib/adapters.js +6 -2
- package/lib/api.js +14 -5
- package/lib/bundled-skills.js +1 -1
- package/lib/capability.js +2 -0
- package/lib/client.js +131 -47
- package/lib/command-service.js +28 -4
- package/lib/config.js +41 -23
- package/lib/core-artifact-store.js +44 -6
- package/lib/core-delivery-cache.js +145 -0
- package/lib/core-maintenance.js +12 -1
- package/lib/core-tasks.js +19 -5
- package/lib/core-user-projection.js +2 -2
- package/lib/doctor.js +4 -10
- package/lib/dsh-compatibility.js +10 -0
- package/lib/dsh-core-adapter.js +20 -4
- package/lib/dsh-text-model-adapter.js +15 -2
- package/lib/generation-input.js +10 -0
- package/lib/host-runtime.js +5 -2
- package/lib/image-edit-input.js +7 -5
- package/lib/image-import.js +61 -0
- package/lib/index.js +60 -17
- package/lib/models.js +12 -3
- package/lib/music-generation.js +21 -0
- package/lib/music-options.js +18 -0
- package/lib/music-transport.js +46 -0
- package/lib/ocr-model-routing.js +1 -0
- package/lib/prompt-optimizer-config.js +92 -21
- package/lib/prompt-optimizer-core.js +51 -6
- package/lib/prompt-optimizer.js +20 -14
- package/lib/provider-adapter.js +1 -0
- package/lib/provider-adapters.js +35 -3
- package/lib/provider-catalog.js +20 -11
- package/lib/provider-config-service.js +18 -6
- package/lib/provider-protocol.js +18 -0
- package/lib/provider-task-runner.js +80 -5
- package/lib/vision-model-routing.js +22 -8
- package/package.json +12 -9
- package/user_guide.md +45 -23
package/lib/config.js
CHANGED
|
@@ -11,7 +11,7 @@ import os from 'node:os';
|
|
|
11
11
|
import * as cap from './capability.js';
|
|
12
12
|
import * as models from './models.js';
|
|
13
13
|
import { atomicWritePrivate, chmodPrivateFile, privateSibling } from './private-storage.js';
|
|
14
|
-
import { selectMediaProtocol, selectImageProtocol, normalizeImageProtocol, isConfiguredProvider } from './provider-protocol.js';
|
|
14
|
+
import { selectMediaProtocol, selectImageProtocol, normalizeImageProtocol, normalizeMusicProtocol, isConfiguredProvider } from './provider-protocol.js';
|
|
15
15
|
import { redactProviderMessage } from './provider-contract.js';
|
|
16
16
|
import {
|
|
17
17
|
PROVIDER_HEALTH_FRESH_MS,
|
|
@@ -183,37 +183,29 @@ export function providerById(id) {
|
|
|
183
183
|
const FIELD_OF = models.CAP_FIELD;
|
|
184
184
|
|
|
185
185
|
/**
|
|
186
|
-
*
|
|
187
|
-
* 否则从全局模型池挑第一个有能力标签的模型。
|
|
186
|
+
* 严格选择:仅从功能分配名单取第一个可用模型。
|
|
188
187
|
* 返回该模型所属 provider 的副本,并把该能力的模型字段设为选中模型。
|
|
189
188
|
* 向后兼容:调用方继续读 provider.apiKey / provider.imageModel 等。
|
|
190
189
|
* 无可用模型 → null(不再兜底到任意 provider)。
|
|
191
190
|
*/
|
|
192
191
|
export function pickFor(capability) {
|
|
193
|
-
|
|
194
|
-
let m = null;
|
|
195
|
-
for (const ref of assignmentOrder(capability)) {
|
|
196
|
-
m = pool.find((x) => !x.rateLimited && x.ref === ref && x.capabilities.includes(capability));
|
|
197
|
-
if (m) break;
|
|
198
|
-
}
|
|
199
|
-
if (!m) m = models.pickModel(pool, capability);
|
|
200
|
-
if (!m) return null;
|
|
201
|
-
const p = providerById(m.providerId);
|
|
202
|
-
if (!p) return null;
|
|
203
|
-
const field = FIELD_OF[capability];
|
|
204
|
-
return field ? { ...p, [field]: m.id } : p;
|
|
192
|
+
return pickAllFor(capability)[0] || null;
|
|
205
193
|
}
|
|
206
194
|
|
|
207
195
|
/**
|
|
208
196
|
* 同一能力的有序 provider 列表(供 failover 依次尝试):
|
|
209
|
-
*
|
|
197
|
+
* 只使用已分配顺序,不补入全局池。
|
|
210
198
|
* 一个 provider 若池里有多个该能力模型,会返回多条(各自带不同模型)。
|
|
211
199
|
*/
|
|
212
200
|
export function pickAllFor(capability, { includeRateLimited = false } = {}) {
|
|
213
|
-
const pool = models.modelPool(providers());
|
|
201
|
+
const pool = models.modelPool(providers(), { includeHostVision: true });
|
|
214
202
|
const chosen = models.orderedModels(pool, capability, assignments()[capability], { includeRateLimited });
|
|
215
203
|
const out = [];
|
|
216
204
|
for (const m of chosen) {
|
|
205
|
+
if (m.ref === models.HOST_VISION_REF) {
|
|
206
|
+
out.push({ id: models.HOST_VISION_REF, visionModel: m.id });
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
217
209
|
const p = providerById(m.providerId);
|
|
218
210
|
if (!p) continue;
|
|
219
211
|
const field = FIELD_OF[capability];
|
|
@@ -233,11 +225,12 @@ const resolveAssignment = models.resolvePoolModel;
|
|
|
233
225
|
export function assignmentOrder(capability) {
|
|
234
226
|
const a = assignments()[capability];
|
|
235
227
|
if (!a) return [];
|
|
236
|
-
const pool = models.modelPool(
|
|
228
|
+
const pool = models.modelPool(allProviders(), { includeHostVision: true });
|
|
237
229
|
const refs = [];
|
|
238
230
|
for (const raw of (Array.isArray(a) ? a : [a])) {
|
|
239
231
|
const m = resolveAssignment(pool, raw, capability);
|
|
240
232
|
if (m && !refs.includes(m.ref)) refs.push(m.ref);
|
|
233
|
+
else if (!m && typeof raw === 'string' && models.parseModelRef(raw) && !refs.includes(raw)) refs.push(raw);
|
|
241
234
|
}
|
|
242
235
|
return refs;
|
|
243
236
|
}
|
|
@@ -245,7 +238,7 @@ export function assignmentOrder(capability) {
|
|
|
245
238
|
/** 设置某能力分配的模型 id(需在全局池中存在且具备该能力) */
|
|
246
239
|
export function setAssignment(capability, ref) {
|
|
247
240
|
const c = load();
|
|
248
|
-
const pool = models.modelPool(
|
|
241
|
+
const pool = models.modelPool(allProviders(), { includeHostVision: true });
|
|
249
242
|
const m = resolveAssignment(pool, ref, capability);
|
|
250
243
|
if (!m) return false;
|
|
251
244
|
if (!c.assignments) c.assignments = {};
|
|
@@ -256,7 +249,7 @@ export function setAssignment(capability, ref) {
|
|
|
256
249
|
|
|
257
250
|
/**
|
|
258
251
|
* 设置某能力的有序 failover 列表(阶段 6 条目 4):每个模型须在全局池中存在且具备该能力。
|
|
259
|
-
* 空数组 =
|
|
252
|
+
* 空数组 = 未分配,不自动选择。重复 id 自动去重。
|
|
260
253
|
*/
|
|
261
254
|
export function setAssignmentOrder(capability, modelRefs) {
|
|
262
255
|
const c = load();
|
|
@@ -268,7 +261,7 @@ export function setAssignmentOrder(capability, modelRefs) {
|
|
|
268
261
|
}
|
|
269
262
|
return true;
|
|
270
263
|
}
|
|
271
|
-
const pool = models.modelPool(
|
|
264
|
+
const pool = models.modelPool(allProviders(), { includeHostVision: true });
|
|
272
265
|
const refs = [];
|
|
273
266
|
for (const raw of modelRefs) {
|
|
274
267
|
const m = resolveAssignment(pool, raw, capability);
|
|
@@ -281,7 +274,7 @@ export function setAssignmentOrder(capability, modelRefs) {
|
|
|
281
274
|
return true;
|
|
282
275
|
}
|
|
283
276
|
|
|
284
|
-
/**
|
|
277
|
+
/** 清除功能分配;此后不再发起该功能的新调用。 */
|
|
285
278
|
export function clearAssignment(capability) {
|
|
286
279
|
const c = load();
|
|
287
280
|
if (c.assignments && c.assignments[capability]) {
|
|
@@ -451,7 +444,8 @@ export function providerHealthSnapshot({ now = Date.now() } = {}) {
|
|
|
451
444
|
for (const capability of Object.values(cap.CAPABILITIES)) {
|
|
452
445
|
const candidates = pickAllFor(capability, { includeRateLimited: true }).map((provider) => {
|
|
453
446
|
const modelId = String(provider[FIELD_OF[capability]] || '');
|
|
454
|
-
const health =
|
|
447
|
+
const health = provider.id === models.HOST_VISION_REF ? { status: 'configured', source: 'configuration' }
|
|
448
|
+
: modelHealth(provider.id, modelId, capability, { now });
|
|
455
449
|
return { providerId: provider.id, modelId, ...health };
|
|
456
450
|
});
|
|
457
451
|
const status = aggregateHealthStates(candidates.map((item) => item.status));
|
|
@@ -489,6 +483,7 @@ export function upsert(provider) {
|
|
|
489
483
|
if (rest.visionInput !== undefined) rest.visionInput = normalizeVisionInputLimits(rest.visionInput) || null;
|
|
490
484
|
if (Array.isArray(rest.models)) rest.models = rest.models.map(model => typeof model === 'string' ? model : ({ ...model,
|
|
491
485
|
...(model?.imageProtocol !== undefined ? { imageProtocol: normalizeImageProtocol(model.imageProtocol) } : {}),
|
|
486
|
+
...(model?.musicProtocol !== undefined ? { musicProtocol: normalizeMusicProtocol(model.musicProtocol) } : {}),
|
|
492
487
|
...(model?.visionInput !== undefined ? { visionInput: normalizeVisionInputLimits(model.visionInput) || null } : {}) }));
|
|
493
488
|
if (id) {
|
|
494
489
|
const i = c.providers.findIndex((p) => p.id === id);
|
|
@@ -541,6 +536,7 @@ export function setProviderModels(id, modelEntries) {
|
|
|
541
536
|
p.models = modelEntries.map((m) => {
|
|
542
537
|
const entry = typeof m === 'string' ? { id: m } : { id: m.id, capabilities: m.capabilities,
|
|
543
538
|
...(m.imageProtocol !== undefined ? { imageProtocol: normalizeImageProtocol(m.imageProtocol) } : {}),
|
|
539
|
+
...(m.musicProtocol !== undefined ? { musicProtocol: normalizeMusicProtocol(m.musicProtocol) } : {}),
|
|
544
540
|
...(m.visionInput !== undefined ? { visionInput: normalizeVisionInputLimits(m.visionInput) || null } : {}) };
|
|
545
541
|
const old = prev.get(entry.id);
|
|
546
542
|
if (old) {
|
|
@@ -548,6 +544,7 @@ export function setProviderModels(id, modelEntries) {
|
|
|
548
544
|
if (old.source) entry.source = old.source;
|
|
549
545
|
if (old.source === 'manual' && Array.isArray(old.capabilities)) entry.capabilities = old.capabilities;
|
|
550
546
|
if (m?.imageProtocol === undefined && old.imageProtocol !== undefined) entry.imageProtocol = old.imageProtocol;
|
|
547
|
+
if (m?.musicProtocol === undefined && old.musicProtocol !== undefined) entry.musicProtocol = old.musicProtocol;
|
|
551
548
|
if (m?.visionInput === undefined && old.visionInput !== undefined) entry.visionInput = old.visionInput;
|
|
552
549
|
}
|
|
553
550
|
if (m && m.verified) entry.verified = m.verified;
|
|
@@ -590,6 +587,9 @@ function findModelEntry(p, modelId) {
|
|
|
590
587
|
|
|
591
588
|
function resetChangedImageProtocolHealth(previous, next) {
|
|
592
589
|
for (const model of models.providerModels(previous)) {
|
|
590
|
+
if (model.musicProtocol !== models.providerModels(next).find(item => item.id === model.id)?.musicProtocol) {
|
|
591
|
+
clearModelCapabilityHealth(next, model.id, cap.CAPABILITIES.MUSIC);
|
|
592
|
+
}
|
|
593
593
|
if (selectImageProtocol(previous, model.id).mediaProtocol !== selectImageProtocol(next, model.id).mediaProtocol) {
|
|
594
594
|
clearModelCapabilityHealth(next, model.id, cap.CAPABILITIES.IMAGE);
|
|
595
595
|
}
|
|
@@ -615,6 +615,24 @@ export function setModelImageProtocol(id, modelId, value) {
|
|
|
615
615
|
return true;
|
|
616
616
|
}
|
|
617
617
|
|
|
618
|
+
/** 音乐协议独立绑定到已配置模型,不覆盖账号生图协议。 */
|
|
619
|
+
export function setModelMusicProtocol(id, modelId, value) {
|
|
620
|
+
const protocol = normalizeMusicProtocol(value);
|
|
621
|
+
const provider = load().providers.find(item => item.id === id);
|
|
622
|
+
if (!provider) return false;
|
|
623
|
+
const entries = Array.isArray(provider.models) ? provider.models
|
|
624
|
+
: models.providerModels(provider).map(({ id, capabilities }) => ({ id, capabilities }));
|
|
625
|
+
const index = entries.findIndex(model => (typeof model === 'string' ? model : model?.id)?.trim() === modelId);
|
|
626
|
+
if (index < 0) return false;
|
|
627
|
+
const entry = typeof entries[index] === 'string' ? { id: entries[index] } : entries[index];
|
|
628
|
+
entries[index] = entry;
|
|
629
|
+
provider.models = entries;
|
|
630
|
+
if (entry.musicProtocol !== protocol) clearModelCapabilityHealth(provider, modelId, cap.CAPABILITIES.MUSIC);
|
|
631
|
+
entry.musicProtocol = protocol;
|
|
632
|
+
persist();
|
|
633
|
+
return true;
|
|
634
|
+
}
|
|
635
|
+
|
|
618
636
|
/** 看图预算变更不会伪造实测成功、解除额度停用或改写原图。 */
|
|
619
637
|
export function setModelVisionInput(id, modelId, value) {
|
|
620
638
|
const limits = normalizeVisionInputLimits(value);
|
|
@@ -305,20 +305,46 @@ function finishObject(dataRoot, input) {
|
|
|
305
305
|
const relations = normalizedRelations(input.relations);
|
|
306
306
|
validateRelationTargets(dataRoot, relations);
|
|
307
307
|
const metadata = jsonClone(input.metadata, 'metadata');
|
|
308
|
-
|
|
308
|
+
// 同步交付缓存预留的 ID 可重入提交,避免多产物交付或 Task 落盘中断后重复作品。
|
|
309
|
+
const resumable = input.artifactId !== undefined;
|
|
310
|
+
const id = resumable ? input.artifactId : 'artifact_' + crypto.randomBytes(12).toString('hex');
|
|
311
|
+
if (!ID.test(id) || (resumable && (!HASH.test(input.expectedDigest)
|
|
312
|
+
|| !Number.isSafeInteger(input.expectedSize) || input.expectedSize < 1))) {
|
|
313
|
+
fail('IRIS_ARTIFACT_INPUT_INVALID', '可恢复 Artifact 身份或内容证据无效');
|
|
314
|
+
}
|
|
309
315
|
const store = ensureStore(dataRoot);
|
|
310
316
|
const objectName = id + extension;
|
|
311
317
|
const object = path.join(store.objects, objectName);
|
|
318
|
+
const exists = (file) => {
|
|
319
|
+
try { fs.lstatSync(file); return true; }
|
|
320
|
+
catch (error) { if (error.code === 'ENOENT') return false; throw error; }
|
|
321
|
+
};
|
|
322
|
+
if (resumable && exists(recordPath(dataRoot, id))) {
|
|
323
|
+
const previous = readArtifact(dataRoot, id).manifest;
|
|
324
|
+
if (previous.size !== input.expectedSize || previous.digest.value !== input.expectedDigest
|
|
325
|
+
|| previous.kind !== kind || previous.mediaType !== mediaType
|
|
326
|
+
|| JSON.stringify(previous.metadata) !== JSON.stringify(metadata)
|
|
327
|
+
|| JSON.stringify(previous.relations) !== JSON.stringify(relations)) {
|
|
328
|
+
fail('IRIS_ARTIFACT_INPUT_INVALID', '可恢复 Artifact 与已提交内容不一致');
|
|
329
|
+
}
|
|
330
|
+
try { refreshIndex(dataRoot); } catch (_) { /* 不撤销已有提交 */ }
|
|
331
|
+
return publicArtifact(previous);
|
|
332
|
+
}
|
|
312
333
|
let stat;
|
|
313
334
|
try {
|
|
314
|
-
|
|
335
|
+
if (resumable && exists(object)) {
|
|
336
|
+
if (regularFile(object).size !== input.expectedSize || hashFile(object) !== input.expectedDigest) {
|
|
337
|
+
fail('IRIS_ARTIFACT_INPUT_INVALID', '未提交 Artifact 与恢复内容不一致');
|
|
338
|
+
}
|
|
339
|
+
} else input.writeObject(object);
|
|
315
340
|
stat = regularFile(object);
|
|
316
|
-
} catch (
|
|
341
|
+
} catch (error) {
|
|
342
|
+
if (resumable) throw error;
|
|
317
343
|
try { fs.rmSync(object, { force: true }); } catch (_) { /* 当前随机 ID */ }
|
|
318
344
|
fail('IRIS_ARTIFACT_WRITE_FAILED', 'Artifact 对象无法安全写入');
|
|
319
345
|
}
|
|
320
346
|
if (!stat.size) {
|
|
321
|
-
try { fs.rmSync(object, { force: true }); } catch (_) { /* 只清理当前随机 ID */ }
|
|
347
|
+
if (!resumable) try { fs.rmSync(object, { force: true }); } catch (_) { /* 只清理当前随机 ID */ }
|
|
322
348
|
fail('IRIS_ARTIFACT_INPUT_INVALID', 'Artifact 内容不能为空');
|
|
323
349
|
}
|
|
324
350
|
const manifest = {
|
|
@@ -333,12 +359,23 @@ function finishObject(dataRoot, input) {
|
|
|
333
359
|
relations,
|
|
334
360
|
metadata
|
|
335
361
|
};
|
|
362
|
+
if (resumable && (manifest.size !== input.expectedSize || manifest.digest.value !== input.expectedDigest)) {
|
|
363
|
+
fail('IRIS_ARTIFACT_INPUT_INVALID', 'Artifact 复制结果与恢复内容证据不一致');
|
|
364
|
+
}
|
|
365
|
+
if (resumable && exists(manifestPath(dataRoot, id))) {
|
|
366
|
+
const previous = validateManifest(jsonFile(manifestPath(dataRoot, id)), id);
|
|
367
|
+
if (JSON.stringify(previous) !== JSON.stringify(manifest)) {
|
|
368
|
+
fail('IRIS_ARTIFACT_INPUT_INVALID', '未提交 Artifact Manifest 与恢复内容不一致');
|
|
369
|
+
}
|
|
370
|
+
}
|
|
336
371
|
try {
|
|
337
372
|
writeManifest(dataRoot, manifest);
|
|
338
373
|
commitRecord(dataRoot, id);
|
|
339
374
|
} catch (_) {
|
|
340
|
-
|
|
341
|
-
|
|
375
|
+
if (!resumable) {
|
|
376
|
+
try { fs.rmSync(object, { force: true }); } catch (_) { /* 当前 ID */ }
|
|
377
|
+
try { fs.rmSync(manifestPath(dataRoot, id), { force: true }); } catch (_) { /* 当前 ID */ }
|
|
378
|
+
}
|
|
342
379
|
fail('IRIS_ARTIFACT_WRITE_FAILED', 'Artifact 无法安全提交');
|
|
343
380
|
}
|
|
344
381
|
try { refreshIndex(dataRoot); } catch (_) { /* index 是可重建缓存,不撤销已提交 Artifact */ }
|
|
@@ -366,6 +403,7 @@ export function adoptCoreArtifactFile(dataRoot, input = {}) {
|
|
|
366
403
|
return finishObject(dataRoot, {
|
|
367
404
|
...input,
|
|
368
405
|
mediaType,
|
|
406
|
+
...(input.artifactId ? { expectedSize: stat.size, expectedDigest: hashFile(source) } : {}),
|
|
369
407
|
kind: input.kind || 'adopted',
|
|
370
408
|
writeObject(object) {
|
|
371
409
|
const tmp = privateSibling(object, 'adopt');
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/** 同步完成结果的私有交付证据;来源 URL/字节不进入 Task、Manifest 或日志。 */
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { atomicWritePrivate, ensurePrivateDir } from './private-storage.js';
|
|
8
|
+
|
|
9
|
+
const TASK_ID = /^task_[a-f0-9]{24}$/;
|
|
10
|
+
const ARTIFACT_ID = /^artifact_[a-f0-9]{24}$/;
|
|
11
|
+
const PAYLOAD_FILE = /^payload-[a-f0-9]{24}\.bin$/;
|
|
12
|
+
const HASH = /^[a-f0-9]{64}$/;
|
|
13
|
+
|
|
14
|
+
export class DeliveryCacheError extends Error {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'DeliveryCacheError';
|
|
18
|
+
this.code = 'IRIS_DELIVERY_CACHE_INVALID';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function invalid() { throw new DeliveryCacheError('同步交付缓存缺失、损坏或与 Task 身份不一致;不会重新生成'); }
|
|
23
|
+
|
|
24
|
+
function directory(dataRoot, taskId, create = false) {
|
|
25
|
+
if (!TASK_ID.test(taskId)) invalid();
|
|
26
|
+
let current = dataRoot;
|
|
27
|
+
for (const part of ['delivery-cache', 'v0', taskId]) {
|
|
28
|
+
current = path.join(current, part);
|
|
29
|
+
if (create) {
|
|
30
|
+
try { fs.lstatSync(current); }
|
|
31
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; ensurePrivateDir(current); }
|
|
32
|
+
}
|
|
33
|
+
const stat = fs.lstatSync(current);
|
|
34
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) invalid();
|
|
35
|
+
}
|
|
36
|
+
return current;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function regular(file) {
|
|
40
|
+
const stat = fs.lstatSync(file);
|
|
41
|
+
if (!stat.isFile() || stat.isSymbolicLink()) invalid();
|
|
42
|
+
return stat;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function fingerprint(file) {
|
|
46
|
+
const stat = regular(file);
|
|
47
|
+
if (!stat.size) invalid();
|
|
48
|
+
const hash = crypto.createHash('sha256');
|
|
49
|
+
const chunk = Buffer.alloc(1024 * 1024);
|
|
50
|
+
const fd = fs.openSync(file, 'r');
|
|
51
|
+
try {
|
|
52
|
+
let count;
|
|
53
|
+
while ((count = fs.readSync(fd, chunk, 0, chunk.length, null))) hash.update(chunk.subarray(0, count));
|
|
54
|
+
} finally { fs.closeSync(fd); }
|
|
55
|
+
return { size: stat.size, sha256: hash.digest('hex') };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function save(dataRoot, cache) {
|
|
59
|
+
atomicWritePrivate(path.join(directory(dataRoot, cache.taskId), 'result.json'), JSON.stringify(cache, null, 2) + '\n');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createDeliveryCache(dataRoot, task, artifacts) {
|
|
63
|
+
if (!Array.isArray(artifacts) || !artifacts.length) invalid();
|
|
64
|
+
const root = directory(dataRoot, task.id, true);
|
|
65
|
+
try { fs.lstatSync(path.join(root, 'result.json')); invalid(); }
|
|
66
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
67
|
+
const cache = {
|
|
68
|
+
schemaVersion: 0, resultKind: 'completed', taskId: task.id, attemptId: task.activeAttemptId,
|
|
69
|
+
providerId: task.providerId, modelRef: task.modelRef, capability: task.capability,
|
|
70
|
+
...(task.providerBinding ? { providerBinding: task.providerBinding } : {}),
|
|
71
|
+
createdAt: new Date().toISOString(), items: []
|
|
72
|
+
};
|
|
73
|
+
for (const remote of artifacts) {
|
|
74
|
+
const item = { artifactId: 'artifact_' + crypto.randomBytes(12).toString('hex') };
|
|
75
|
+
if (remote.kind === 'remote-url') {
|
|
76
|
+
item.source = { kind: 'remote-url', url: remote.url, ...(remote.mediaType ? { mediaType: remote.mediaType } : {}) };
|
|
77
|
+
} else if (remote.kind === 'inline-base64') {
|
|
78
|
+
const bytes = Buffer.from(remote.data, 'base64');
|
|
79
|
+
if (!bytes.length || bytes.toString('base64') !== remote.data) invalid();
|
|
80
|
+
const file = 'payload-' + crypto.randomBytes(12).toString('hex') + '.bin';
|
|
81
|
+
atomicWritePrivate(path.join(root, file), bytes);
|
|
82
|
+
item.payload = { file, size: bytes.length, sha256: crypto.createHash('sha256').update(bytes).digest('hex'),
|
|
83
|
+
...(remote.mediaType ? { mediaType: remote.mediaType } : {}) };
|
|
84
|
+
} else invalid();
|
|
85
|
+
cache.items.push(item);
|
|
86
|
+
}
|
|
87
|
+
// result.json 是完整同步响应的提交证据;未发布的文件不作为可恢复结果。
|
|
88
|
+
save(dataRoot, cache);
|
|
89
|
+
return cache;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function readDeliveryCache(dataRoot, task, { allowAbsent = false } = {}) {
|
|
93
|
+
let cache;
|
|
94
|
+
try {
|
|
95
|
+
const file = path.join(directory(dataRoot, task.id), 'result.json');
|
|
96
|
+
regular(file);
|
|
97
|
+
cache = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (allowAbsent && error.code === 'ENOENT') return null;
|
|
100
|
+
invalid();
|
|
101
|
+
}
|
|
102
|
+
if (!cache || cache.schemaVersion !== 0 || cache.resultKind !== 'completed'
|
|
103
|
+
|| cache.taskId !== task.id || cache.attemptId !== task.activeAttemptId
|
|
104
|
+
|| cache.providerId !== task.providerId || cache.modelRef !== task.modelRef
|
|
105
|
+
|| cache.capability !== task.capability || cache.providerBinding !== task.providerBinding
|
|
106
|
+
|| !Array.isArray(cache.items) || !cache.items.length
|
|
107
|
+
|| typeof cache.createdAt !== 'string' || !Number.isFinite(Date.parse(cache.createdAt))) invalid();
|
|
108
|
+
const ids = new Set();
|
|
109
|
+
for (const item of cache.items) {
|
|
110
|
+
if (!item || !ARTIFACT_ID.test(item.artifactId) || ids.has(item.artifactId)) invalid();
|
|
111
|
+
ids.add(item.artifactId);
|
|
112
|
+
if (item.source && (item.source.kind !== 'remote-url' || typeof item.source.url !== 'string' || !item.source.url.trim()
|
|
113
|
+
|| (item.source.mediaType !== undefined && typeof item.source.mediaType !== 'string'))) invalid();
|
|
114
|
+
if (item.payload && (!PAYLOAD_FILE.test(item.payload.file) || !HASH.test(item.payload.sha256)
|
|
115
|
+
|| !Number.isSafeInteger(item.payload.size) || item.payload.size < 1
|
|
116
|
+
|| (item.payload.mediaType !== undefined && typeof item.payload.mediaType !== 'string'))) invalid();
|
|
117
|
+
if (!item.payload && !item.source) invalid();
|
|
118
|
+
}
|
|
119
|
+
return cache;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function deliveryCacheNeedsProvider(dataRoot, task) {
|
|
123
|
+
return readDeliveryCache(dataRoot, task).items.some(item => !item.payload);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function newDeliveryPayloadPath(dataRoot, cache) {
|
|
127
|
+
return path.join(directory(dataRoot, cache.taskId), 'payload-' + crypto.randomBytes(12).toString('hex') + '.bin');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function recordDeliveryPayload(dataRoot, cache, index, file, mediaType) {
|
|
131
|
+
const root = directory(dataRoot, cache.taskId);
|
|
132
|
+
if (path.dirname(file) !== root || !PAYLOAD_FILE.test(path.basename(file))) invalid();
|
|
133
|
+
cache.items[index].payload = { file: path.basename(file), ...fingerprint(file), mediaType };
|
|
134
|
+
delete cache.items[index].source;
|
|
135
|
+
save(dataRoot, cache);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function readDeliveryPayload(dataRoot, cache, index) {
|
|
139
|
+
const payload = cache.items[index].payload;
|
|
140
|
+
const file = path.join(directory(dataRoot, cache.taskId), payload.file);
|
|
141
|
+
let actual;
|
|
142
|
+
try { actual = fingerprint(file); } catch (_) { invalid(); }
|
|
143
|
+
if (actual.size !== payload.size || actual.sha256 !== payload.sha256) invalid();
|
|
144
|
+
return file;
|
|
145
|
+
}
|
package/lib/core-maintenance.js
CHANGED
|
@@ -4,11 +4,12 @@ import crypto from 'node:crypto';
|
|
|
4
4
|
import { inspectCoreArtifact, refreshCoreArtifactInventory } from './core-artifacts.js';
|
|
5
5
|
import { inspectCoreTask, listCoreTasks } from './core-tasks.js';
|
|
6
6
|
import { atomicWritePrivate } from './private-storage.js';
|
|
7
|
+
import { readDeliveryCache } from './core-delivery-cache.js';
|
|
7
8
|
|
|
8
9
|
const TASK = /^task_[a-f0-9]{24}$/;
|
|
9
10
|
const ARTIFACT = /^artifact_[a-f0-9]{24}$/;
|
|
10
11
|
const TRANSACTION = /^delete_[a-f0-9]{24}$/;
|
|
11
|
-
const MANAGED = /^(?:task-store\/v0\/tasks\/task_[a-f0-9]{24}\.json|artifact-store\/v0\/(?:records|manifests)\/artifact_[a-f0-9]{24}\.json|artifact-store\/v0\/objects\/artifact_[a-f0-9]{24}\.(?:png|jpg|webp|gif|mp4|webm|wav|mp3|ogg|flac|txt|json)|provider-staging\/v0\/download-[1-9][0-9]*-[a-f0-9]{16}\.part)$/;
|
|
12
|
+
const MANAGED = /^(?:task-store\/v0\/tasks\/task_[a-f0-9]{24}\.json|artifact-store\/v0\/(?:records|manifests)\/artifact_[a-f0-9]{24}\.json|artifact-store\/v0\/objects\/artifact_[a-f0-9]{24}\.(?:png|jpg|webp|gif|mp4|webm|wav|mp3|ogg|flac|txt|json)|provider-staging\/v0\/download-[1-9][0-9]*-[a-f0-9]{16}\.part|delivery-cache\/v0\/task_[a-f0-9]{24}\/(?:result\.json|payload-[a-f0-9]{24}\.bin))$/;
|
|
12
13
|
const fail = (code, message) => { throw Object.assign(new Error(message), { code }); };
|
|
13
14
|
function fileDigest(file) {
|
|
14
15
|
const hash = crypto.createHash('sha256'), chunk = Buffer.allocUnsafe(1024 * 1024), fd = fs.openSync(file, 'r');
|
|
@@ -68,6 +69,8 @@ export function planCoreDeletion(root, input) {
|
|
|
68
69
|
const task = inspectCoreTask(root, id);
|
|
69
70
|
if (task.phase !== 'terminal' || task.acceptance === 'unknown' || task.outcome === 'unknown') blockers.push({ id, reason: 'task_not_settled' });
|
|
70
71
|
files.push(managedFile(root, 'task-store/v0/tasks/' + id + '.json'));
|
|
72
|
+
const cache = 'delivery-cache/v0/' + id;
|
|
73
|
+
for (const name of names(root, cache)) files.push(managedFile(root, cache + '/' + name));
|
|
71
74
|
}
|
|
72
75
|
for (const id of artifactIds) {
|
|
73
76
|
inspectCoreArtifact(root, id);
|
|
@@ -83,6 +86,11 @@ export function planCoreDeletion(root, input) {
|
|
|
83
86
|
for (const task of existing.tasks) {
|
|
84
87
|
if (selectedTasks.has(task.id)) continue;
|
|
85
88
|
for (const id of task.artifactIds) if (selectedArtifacts.has(id)) blockers.push({ id, referencedBy: task.id, reason: 'task_artifact_reference' });
|
|
89
|
+
if (task.deliveryCache && task.deliveryState !== 'ready') {
|
|
90
|
+
for (const item of readDeliveryCache(root, task).items) {
|
|
91
|
+
if (selectedArtifacts.has(item.artifactId)) blockers.push({ id: item.artifactId, referencedBy: task.id, reason: 'delivery_cache_reference' });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
86
94
|
if (selectedArtifacts.has(task.sourceArtifactId)) blockers.push({ id: task.sourceArtifactId, referencedBy: task.id, reason: 'task_input_reference' });
|
|
87
95
|
if (selectedTasks.has(task.retriedFrom)) blockers.push({ id: task.retriedFrom, referencedBy: task.id, reason: 'retry_reference' });
|
|
88
96
|
}
|
|
@@ -109,6 +117,9 @@ export function planCoreCleanup(root, { older_than_ms = 3600000 } = {}) {
|
|
|
109
117
|
const referenced = new Set(existing.artifacts.map(artifact => artifact.id));
|
|
110
118
|
for (const artifact of existing.artifacts) for (const edge of artifact.relations) referenced.add(edge.artifactId);
|
|
111
119
|
for (const task of existing.tasks) for (const id of task.artifactIds) referenced.add(id);
|
|
120
|
+
for (const task of existing.tasks) if (task.deliveryCache && task.deliveryState !== 'ready') {
|
|
121
|
+
for (const item of readDeliveryCache(root, task).items) referenced.add(item.artifactId);
|
|
122
|
+
}
|
|
112
123
|
for (const task of existing.tasks) if (task.sourceArtifactId) referenced.add(task.sourceArtifactId);
|
|
113
124
|
for (const name of names(root, 'artifact-store/v0/manifests')) {
|
|
114
125
|
if (/^artifact_[a-f0-9]{24}\.json$/.test(name)) {
|
package/lib/core-tasks.js
CHANGED
|
@@ -7,6 +7,7 @@ import { atomicWritePrivate, ensurePrivateDir } from './private-storage.js';
|
|
|
7
7
|
import { parseModelRef } from './models.js';
|
|
8
8
|
import { providerErrorRecord } from './provider-contract.js';
|
|
9
9
|
import { deriveLegacyStatus, semanticViolations } from './task-semantics.js';
|
|
10
|
+
import { readDeliveryCache } from './core-delivery-cache.js';
|
|
10
11
|
|
|
11
12
|
const STORE_DIR = 'task-store';
|
|
12
13
|
const STORE_VERSION = 'v0';
|
|
@@ -14,7 +15,7 @@ const TASK_ID = /^task_[a-f0-9]{24}$/;
|
|
|
14
15
|
const ARTIFACT_ID = /^artifact_[a-f0-9]{24}$/;
|
|
15
16
|
const ATTEMPT_ID = /^attempt_[a-f0-9]{24}$/;
|
|
16
17
|
const PROVIDER_BINDING = /^sha256:[a-f0-9]{64}$/;
|
|
17
|
-
const CAPABILITIES = new Set(['image', 'video', 'tts', 'transcribe']);
|
|
18
|
+
const CAPABILITIES = new Set(['image', 'video', 'tts', 'music', 'transcribe']);
|
|
18
19
|
const ATTEMPT_SELECTION_REASONS = new Set(['explicit', 'assignment', 'pool']);
|
|
19
20
|
const ATTEMPT_STAGE_TIMESTAMPS = Object.freeze([
|
|
20
21
|
'queuedAt', 'submitStartedAt', 'submittedAt', 'remoteCompletedAt',
|
|
@@ -106,6 +107,7 @@ function validateRecord(record, expectedId) {
|
|
|
106
107
|
|| !Array.isArray(record.attempts) || !Array.isArray(record.artifactIds)
|
|
107
108
|
|| !Number.isSafeInteger(record.revision) || record.revision < 1
|
|
108
109
|
|| typeof record.createdAt !== 'string' || typeof record.updatedAt !== 'string'
|
|
110
|
+
|| (record.deliveryCache !== undefined && record.deliveryCache !== true)
|
|
109
111
|
|| (record.providerBinding !== undefined && !PROVIDER_BINDING.test(record.providerBinding))
|
|
110
112
|
|| (record.retriedFrom !== undefined && !TASK_ID.test(record.retriedFrom))
|
|
111
113
|
|| (record.sourceArtifactId !== undefined && (record.capability !== 'image' || !ARTIFACT_ID.test(record.sourceArtifactId)))) {
|
|
@@ -292,6 +294,7 @@ export function beginCoreAttempt(dataRoot, taskId, fields = {}) {
|
|
|
292
294
|
task.deliveryState = 'none';
|
|
293
295
|
task.cancelState = 'none';
|
|
294
296
|
delete task.remoteTaskId;
|
|
297
|
+
delete task.deliveryCache;
|
|
295
298
|
delete task.lastError;
|
|
296
299
|
});
|
|
297
300
|
return Object.freeze(clone(created));
|
|
@@ -327,6 +330,7 @@ export function recordCoreAttemptResult(dataRoot, taskId, snapshot) {
|
|
|
327
330
|
task.phase = 'running';
|
|
328
331
|
task.outcome = 'succeeded';
|
|
329
332
|
task.deliveryState = 'pending';
|
|
333
|
+
if (snapshot.deliveryCache === true) task.deliveryCache = true;
|
|
330
334
|
}
|
|
331
335
|
if (snapshot.remoteTaskId) task.remoteTaskId = String(snapshot.remoteTaskId);
|
|
332
336
|
if (attempt.error) task.lastError = attempt.error;
|
|
@@ -436,7 +440,7 @@ export function recordCoreDownloadCompleted(dataRoot, taskId) {
|
|
|
436
440
|
|
|
437
441
|
export function prepareCoreRedelivery(dataRoot, taskId) {
|
|
438
442
|
return mutate(dataRoot, taskId, (task) => {
|
|
439
|
-
if (task.outcome !== 'succeeded' || task.deliveryState !== 'failed' || !task.remoteTaskId) {
|
|
443
|
+
if (task.outcome !== 'succeeded' || task.deliveryState !== 'failed' || (!task.remoteTaskId && !task.deliveryCache)) {
|
|
440
444
|
fail('IRIS_TASK_NOT_REDELIVERABLE', 'Task 不处于可重新交付状态');
|
|
441
445
|
}
|
|
442
446
|
task.phase = 'running';
|
|
@@ -515,11 +519,21 @@ export function recordCoreCancelResult(dataRoot, taskId, result) {
|
|
|
515
519
|
}
|
|
516
520
|
|
|
517
521
|
/**
|
|
518
|
-
*
|
|
519
|
-
*
|
|
522
|
+
* 同步完整缓存可补回尚未落盘的完成事实;否则提交中断仍收口为受理未知。
|
|
523
|
+
* 已有 remoteTaskId 的任务保持可观察。恢复不重新提交,也不自动交付缓存。
|
|
520
524
|
*/
|
|
521
525
|
export function recoverCoreTask(dataRoot, taskId) {
|
|
522
|
-
|
|
526
|
+
let current = readRecord(dataRoot, taskId);
|
|
527
|
+
if (current.phase === 'submitting' && current.acceptance === 'none') {
|
|
528
|
+
const cache = readDeliveryCache(dataRoot, current, { allowAbsent: true });
|
|
529
|
+
if (cache) {
|
|
530
|
+
recordCoreAttemptResult(dataRoot, taskId, {
|
|
531
|
+
id: cache.attemptId, providerId: cache.providerId, model: cache.modelRef,
|
|
532
|
+
acceptance: 'accepted', resultKind: 'completed', deliveryCache: true
|
|
533
|
+
});
|
|
534
|
+
current = readRecord(dataRoot, taskId);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
523
537
|
if (current.cancelState === 'requested') {
|
|
524
538
|
return mutate(dataRoot, taskId, (task) => {
|
|
525
539
|
task.phase = 'terminal';
|
|
@@ -79,7 +79,7 @@ export function coreTaskObservable(task) {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
|
-
* 与 task.redeliver
|
|
82
|
+
* 与 task.redeliver 同一组交付事实门:远端已成功、交付失败,有远端 ID 或同步缓存。
|
|
83
83
|
* 只读持久化事实轴,不解析 Provider、不接触网络;binding 与能力校验仍在
|
|
84
84
|
* Command Service。五类状态里只有 delivery_failed 会标记 redeliverable。
|
|
85
85
|
*/
|
|
@@ -87,7 +87,7 @@ export function coreTaskRedeliverable(task) {
|
|
|
87
87
|
if (!task || typeof task !== 'object' || Array.isArray(task)) return false;
|
|
88
88
|
return String(task.outcome || '') === 'succeeded'
|
|
89
89
|
&& String(task.deliveryState || '') === 'failed'
|
|
90
|
-
&& Boolean(String(task.remoteTaskId || '').trim());
|
|
90
|
+
&& (Boolean(String(task.remoteTaskId || '').trim()) || task.deliveryCache === true);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/**
|
package/lib/doctor.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
|
+
import { isSupportedDshVersion, SUPPORTED_DSH_VERSIONS } from './dsh-compatibility.js';
|
|
4
5
|
import os from 'node:os';
|
|
5
6
|
import path from 'node:path';
|
|
6
7
|
import { spawnSync } from 'node:child_process';
|
|
@@ -331,13 +332,6 @@ function summarizeChecks(checks) {
|
|
|
331
332
|
};
|
|
332
333
|
}
|
|
333
334
|
|
|
334
|
-
function dshVersionSupported(value) {
|
|
335
|
-
/* 当前 0.2 候选只支持经真实安装服务隔离验收的准确 rc.2。
|
|
336
|
-
Iris 0.1.4 的旧宿主窗口保留在历史发行中,不用于放行当前源码。 */
|
|
337
|
-
const version = String(value || '').trim().replace(/\s+/g, '');
|
|
338
|
-
return /^0\.2\.0-rc\.2(?:\+.*)?$/.test(version);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
335
|
function names(value) {
|
|
342
336
|
return [...new Set((Array.isArray(value) ? value : []).map((item) => String(item || '').trim()).filter(Boolean))];
|
|
343
337
|
}
|
|
@@ -379,12 +373,12 @@ export function hostDoctor(host, evidence = {}, options = {}) {
|
|
|
379
373
|
if (snapshot.host.id === 'deepseek-harness') {
|
|
380
374
|
if (snapshot.host.version === 'unknown') {
|
|
381
375
|
checks.push(result('dsh-version', 'warn', '无法从当前进程识别 DSH 版本',
|
|
382
|
-
'使用 dsh --version
|
|
383
|
-
} else if (
|
|
376
|
+
'使用 dsh --version 核对;已验证版本为 DSH ' + SUPPORTED_DSH_VERSIONS.join('、') + '。'));
|
|
377
|
+
} else if (isSupportedDshVersion(snapshot.host.version)) {
|
|
384
378
|
checks.push(result('dsh-version', 'ok', 'DSH ' + snapshot.host.version + ' 位于已验证支持范围'));
|
|
385
379
|
} else {
|
|
386
380
|
checks.push(result('dsh-version', 'error', 'DSH ' + snapshot.host.version + ' 超出已验证支持范围',
|
|
387
|
-
'
|
|
381
|
+
'当前支持 DSH ' + SUPPORTED_DSH_VERSIONS.join('、') + ';旧 DSH 0.1.2 线或 0.1.5-rc.1 使用 Iris @mokuyoaxis/dsh-iris@0.1.4。其他版本须另行验收。'));
|
|
388
382
|
}
|
|
389
383
|
}
|
|
390
384
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/** 已完成宿主接口验收的版本;manifest/Doctor/验收脚本共用同一边界。 */
|
|
4
|
+
export const SUPPORTED_DSH_VERSIONS = Object.freeze(['0.2.0-rc.2', '0.2.1-alpha.4']);
|
|
5
|
+
export const DSH_SUPPORT_RANGE = SUPPORTED_DSH_VERSIONS.join(' || ');
|
|
6
|
+
|
|
7
|
+
export function isSupportedDshVersion(value) {
|
|
8
|
+
const version = String(value || '').trim().split('+')[0];
|
|
9
|
+
return SUPPORTED_DSH_VERSIONS.includes(version);
|
|
10
|
+
}
|
package/lib/dsh-core-adapter.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { allProviders, irisHome, recordRateLimit } from './config.js';
|
|
5
|
+
import { allProviders, assignments, irisHome, recordRateLimit } from './config.js';
|
|
6
6
|
import { createCommandService } from './command-service.js';
|
|
7
7
|
import { createCoreArtifact, inspectCoreArtifact, listCoreArtifacts, listCoreArtifactMetadata, readCoreArtifactBytes } from './core-artifacts.js';
|
|
8
8
|
import { createCoreRuntime } from './core-runtime.js';
|
|
@@ -15,7 +15,8 @@ import { requireHostPort } from './host-contract.js';
|
|
|
15
15
|
import { createProviderTaskRunner } from './provider-task-runner.js';
|
|
16
16
|
import { createConfiguredProviderAdapter as createProviderAdapter } from './provider-adapters.js';
|
|
17
17
|
import { parseModelRef } from './models.js';
|
|
18
|
-
import { imageCandidatesFromCatalog, providerForTaskFromCatalog, providerTaskBinding, transcribeCandidatesFromCatalog, ttsCandidatesFromCatalog, videoCandidatesFromCatalog } from './provider-catalog.js';
|
|
18
|
+
import { imageCandidatesFromCatalog, musicCandidatesFromCatalog, providerForTaskFromCatalog, providerTaskBinding, transcribeCandidatesFromCatalog, ttsCandidatesFromCatalog, videoCandidatesFromCatalog } from './provider-catalog.js';
|
|
19
|
+
import { generateMusicTask } from './music-generation.js';
|
|
19
20
|
|
|
20
21
|
let writerTail = Promise.resolve();
|
|
21
22
|
const providerWatchers = new Map();
|
|
@@ -67,6 +68,12 @@ function withReader(callback, signal) {
|
|
|
67
68
|
return withRuntime('reader', callback, signal);
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
/** 本地图片与会话附件都交给同一导入Command,不创建生成任务。 */
|
|
72
|
+
export function importImageForDsh(input = {}) {
|
|
73
|
+
return withWriter((_runtime, commands) => commands.execute('artifact.import', input.imageBytes !== undefined
|
|
74
|
+
? { image_bytes: input.imageBytes } : { image_path: input.imagePath }), input.signal);
|
|
75
|
+
}
|
|
76
|
+
|
|
70
77
|
/**
|
|
71
78
|
* 将 DSH 图片输入投影到共享 crop Command,再把中性 Artifact 读成宿主可保存的字节。
|
|
72
79
|
* bytes 输入先成为 host-input Artifact,输出通过 derived-from 保留关系。
|
|
@@ -179,6 +186,13 @@ export function submitProviderTaskForDsh(input = {}) {
|
|
|
179
186
|
}), input.signal);
|
|
180
187
|
}
|
|
181
188
|
|
|
189
|
+
export function submitMusicForDsh(input) {
|
|
190
|
+
return withWriter(runtime => generateMusicTask({
|
|
191
|
+
runner: createProviderTaskRunner(runtime), candidates: input.candidates,
|
|
192
|
+
providerInput: input.providerInput, options: input.musicOptions, signal: input.signal
|
|
193
|
+
}), input.signal);
|
|
194
|
+
}
|
|
195
|
+
|
|
182
196
|
/** 单步观察已经受理的 Core Task;不会自动循环,也不会创建第二次 submit。 */
|
|
183
197
|
export function observeProviderTaskForDsh(input = {}) {
|
|
184
198
|
return withWriter((runtime) => createProviderTaskRunner(runtime).observe(
|
|
@@ -221,7 +235,7 @@ export function reobserveProviderTaskForDsh(input = {}) {
|
|
|
221
235
|
|
|
222
236
|
/**
|
|
223
237
|
* D2 人工 redeliver:只对 outcome=succeeded / deliveryState=failed 的 Task
|
|
224
|
-
*
|
|
238
|
+
* 重新交付原结果(同步读缓存,异步 re-poll → download),绝不 submit、
|
|
225
239
|
* 绝不重新生成。
|
|
226
240
|
*/
|
|
227
241
|
export function redeliverProviderTaskForDsh(input = {}) {
|
|
@@ -277,12 +291,14 @@ export function retryProviderTaskForDsh(input = {}) {
|
|
|
277
291
|
return withWriter(async (runtime) => {
|
|
278
292
|
const commands = createCommandService(runtime, {
|
|
279
293
|
resolveTaskCandidates: ({ capability, modelRef }) => {
|
|
280
|
-
const catalog = { providers: allProviders() };
|
|
294
|
+
const catalog = { providers: allProviders(), assignments: assignments() };
|
|
281
295
|
const ref = modelRef || undefined;
|
|
282
296
|
const candidates = capability === 'video'
|
|
283
297
|
? videoCandidatesFromCatalog(catalog, ref)
|
|
284
298
|
: capability === 'tts'
|
|
285
299
|
? ttsCandidatesFromCatalog(catalog, ref)
|
|
300
|
+
: capability === 'music'
|
|
301
|
+
? musicCandidatesFromCatalog(catalog, ref)
|
|
286
302
|
: capability === 'transcribe'
|
|
287
303
|
? transcribeCandidatesFromCatalog(catalog, ref)
|
|
288
304
|
: imageCandidatesFromCatalog(catalog, ref);
|