@mokuyoaxis/dsh-iris 0.2.2 → 0.2.3
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 +41 -0
- package/.dsh/skills/iris-use/agents/openai.yaml +4 -0
- package/.dsh/skills/iris-use/references/media-contract.md +73 -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 +26 -2
- package/README.en.md +39 -32
- package/README.md +37 -31
- package/bin/dsh-iris.js +27 -6
- package/docs/AGENT_QUICKSTART.md +9 -9
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/CORE_RUNTIME_CONTRACT.md +3 -1
- package/docs/DSH_CORE_MIGRATION.md +6 -2
- package/docs/FAULT_INJECTION.md +1 -0
- package/docs/HEADLESS_CLI.md +8 -4
- package/docs/HOST_DOCTOR.md +3 -3
- 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 +14 -25
- package/docs/SECURITY.md +3 -2
- package/docs/SYNC_DELIVERY_RECOVERY.md +39 -0
- package/docs/TASK_SEMANTICS.md +4 -4
- package/docs/releases/0.2.3.md +54 -0
- package/lib/actions.js +40 -3
- package/lib/adapters.js +1 -0
- package/lib/api.js +14 -5
- package/lib/bundled-skills.js +1 -1
- package/lib/capability.js +2 -0
- package/lib/client.js +111 -26
- package/lib/command-service.js +12 -3
- package/lib/config.js +26 -2
- 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/dsh-core-adapter.js +14 -4
- package/lib/dsh-text-model-adapter.js +15 -2
- package/lib/generation-input.js +10 -0
- package/lib/host-runtime.js +4 -2
- package/lib/index.js +23 -4
- package/lib/models.js +4 -1
- package/lib/music-generation.js +21 -0
- package/lib/music-options.js +18 -0
- package/lib/music-transport.js +46 -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 +24 -1
- package/lib/provider-catalog.js +18 -7
- package/lib/provider-config-service.js +13 -1
- package/lib/provider-protocol.js +12 -0
- package/lib/provider-task-runner.js +80 -5
- package/package.json +4 -2
- package/user_guide.md +30 -16
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,
|
|
@@ -196,7 +196,7 @@ export function pickFor(capability) {
|
|
|
196
196
|
m = pool.find((x) => !x.rateLimited && x.ref === ref && x.capabilities.includes(capability));
|
|
197
197
|
if (m) break;
|
|
198
198
|
}
|
|
199
|
-
if (!m) m = models.pickModel(pool, capability);
|
|
199
|
+
if (!m && capability !== cap.CAPABILITIES.MUSIC) m = models.pickModel(pool, capability);
|
|
200
200
|
if (!m) return null;
|
|
201
201
|
const p = providerById(m.providerId);
|
|
202
202
|
if (!p) return null;
|
|
@@ -489,6 +489,7 @@ export function upsert(provider) {
|
|
|
489
489
|
if (rest.visionInput !== undefined) rest.visionInput = normalizeVisionInputLimits(rest.visionInput) || null;
|
|
490
490
|
if (Array.isArray(rest.models)) rest.models = rest.models.map(model => typeof model === 'string' ? model : ({ ...model,
|
|
491
491
|
...(model?.imageProtocol !== undefined ? { imageProtocol: normalizeImageProtocol(model.imageProtocol) } : {}),
|
|
492
|
+
...(model?.musicProtocol !== undefined ? { musicProtocol: normalizeMusicProtocol(model.musicProtocol) } : {}),
|
|
492
493
|
...(model?.visionInput !== undefined ? { visionInput: normalizeVisionInputLimits(model.visionInput) || null } : {}) }));
|
|
493
494
|
if (id) {
|
|
494
495
|
const i = c.providers.findIndex((p) => p.id === id);
|
|
@@ -541,6 +542,7 @@ export function setProviderModels(id, modelEntries) {
|
|
|
541
542
|
p.models = modelEntries.map((m) => {
|
|
542
543
|
const entry = typeof m === 'string' ? { id: m } : { id: m.id, capabilities: m.capabilities,
|
|
543
544
|
...(m.imageProtocol !== undefined ? { imageProtocol: normalizeImageProtocol(m.imageProtocol) } : {}),
|
|
545
|
+
...(m.musicProtocol !== undefined ? { musicProtocol: normalizeMusicProtocol(m.musicProtocol) } : {}),
|
|
544
546
|
...(m.visionInput !== undefined ? { visionInput: normalizeVisionInputLimits(m.visionInput) || null } : {}) };
|
|
545
547
|
const old = prev.get(entry.id);
|
|
546
548
|
if (old) {
|
|
@@ -548,6 +550,7 @@ export function setProviderModels(id, modelEntries) {
|
|
|
548
550
|
if (old.source) entry.source = old.source;
|
|
549
551
|
if (old.source === 'manual' && Array.isArray(old.capabilities)) entry.capabilities = old.capabilities;
|
|
550
552
|
if (m?.imageProtocol === undefined && old.imageProtocol !== undefined) entry.imageProtocol = old.imageProtocol;
|
|
553
|
+
if (m?.musicProtocol === undefined && old.musicProtocol !== undefined) entry.musicProtocol = old.musicProtocol;
|
|
551
554
|
if (m?.visionInput === undefined && old.visionInput !== undefined) entry.visionInput = old.visionInput;
|
|
552
555
|
}
|
|
553
556
|
if (m && m.verified) entry.verified = m.verified;
|
|
@@ -590,6 +593,9 @@ function findModelEntry(p, modelId) {
|
|
|
590
593
|
|
|
591
594
|
function resetChangedImageProtocolHealth(previous, next) {
|
|
592
595
|
for (const model of models.providerModels(previous)) {
|
|
596
|
+
if (model.musicProtocol !== models.providerModels(next).find(item => item.id === model.id)?.musicProtocol) {
|
|
597
|
+
clearModelCapabilityHealth(next, model.id, cap.CAPABILITIES.MUSIC);
|
|
598
|
+
}
|
|
593
599
|
if (selectImageProtocol(previous, model.id).mediaProtocol !== selectImageProtocol(next, model.id).mediaProtocol) {
|
|
594
600
|
clearModelCapabilityHealth(next, model.id, cap.CAPABILITIES.IMAGE);
|
|
595
601
|
}
|
|
@@ -615,6 +621,24 @@ export function setModelImageProtocol(id, modelId, value) {
|
|
|
615
621
|
return true;
|
|
616
622
|
}
|
|
617
623
|
|
|
624
|
+
/** 音乐协议独立绑定到已配置模型,不覆盖账号生图协议。 */
|
|
625
|
+
export function setModelMusicProtocol(id, modelId, value) {
|
|
626
|
+
const protocol = normalizeMusicProtocol(value);
|
|
627
|
+
const provider = load().providers.find(item => item.id === id);
|
|
628
|
+
if (!provider) return false;
|
|
629
|
+
const entries = Array.isArray(provider.models) ? provider.models
|
|
630
|
+
: models.providerModels(provider).map(({ id, capabilities }) => ({ id, capabilities }));
|
|
631
|
+
const index = entries.findIndex(model => (typeof model === 'string' ? model : model?.id)?.trim() === modelId);
|
|
632
|
+
if (index < 0) return false;
|
|
633
|
+
const entry = typeof entries[index] === 'string' ? { id: entries[index] } : entries[index];
|
|
634
|
+
entries[index] = entry;
|
|
635
|
+
provider.models = entries;
|
|
636
|
+
if (entry.musicProtocol !== protocol) clearModelCapabilityHealth(provider, modelId, cap.CAPABILITIES.MUSIC);
|
|
637
|
+
entry.musicProtocol = protocol;
|
|
638
|
+
persist();
|
|
639
|
+
return true;
|
|
640
|
+
}
|
|
641
|
+
|
|
618
642
|
/** 看图预算变更不会伪造实测成功、解除额度停用或改写原图。 */
|
|
619
643
|
export function setModelVisionInput(id, modelId, value) {
|
|
620
644
|
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/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();
|
|
@@ -179,6 +180,13 @@ export function submitProviderTaskForDsh(input = {}) {
|
|
|
179
180
|
}), input.signal);
|
|
180
181
|
}
|
|
181
182
|
|
|
183
|
+
export function submitMusicForDsh(input) {
|
|
184
|
+
return withWriter(runtime => generateMusicTask({
|
|
185
|
+
runner: createProviderTaskRunner(runtime), candidates: input.candidates,
|
|
186
|
+
providerInput: input.providerInput, options: input.musicOptions, signal: input.signal
|
|
187
|
+
}), input.signal);
|
|
188
|
+
}
|
|
189
|
+
|
|
182
190
|
/** 单步观察已经受理的 Core Task;不会自动循环,也不会创建第二次 submit。 */
|
|
183
191
|
export function observeProviderTaskForDsh(input = {}) {
|
|
184
192
|
return withWriter((runtime) => createProviderTaskRunner(runtime).observe(
|
|
@@ -221,7 +229,7 @@ export function reobserveProviderTaskForDsh(input = {}) {
|
|
|
221
229
|
|
|
222
230
|
/**
|
|
223
231
|
* D2 人工 redeliver:只对 outcome=succeeded / deliveryState=failed 的 Task
|
|
224
|
-
*
|
|
232
|
+
* 重新交付原结果(同步读缓存,异步 re-poll → download),绝不 submit、
|
|
225
233
|
* 绝不重新生成。
|
|
226
234
|
*/
|
|
227
235
|
export function redeliverProviderTaskForDsh(input = {}) {
|
|
@@ -277,12 +285,14 @@ export function retryProviderTaskForDsh(input = {}) {
|
|
|
277
285
|
return withWriter(async (runtime) => {
|
|
278
286
|
const commands = createCommandService(runtime, {
|
|
279
287
|
resolveTaskCandidates: ({ capability, modelRef }) => {
|
|
280
|
-
const catalog = { providers: allProviders() };
|
|
288
|
+
const catalog = { providers: allProviders(), ...(capability === 'music' ? { assignments: assignments() } : {}) };
|
|
281
289
|
const ref = modelRef || undefined;
|
|
282
290
|
const candidates = capability === 'video'
|
|
283
291
|
? videoCandidatesFromCatalog(catalog, ref)
|
|
284
292
|
: capability === 'tts'
|
|
285
293
|
? ttsCandidatesFromCatalog(catalog, ref)
|
|
294
|
+
: capability === 'music'
|
|
295
|
+
? musicCandidatesFromCatalog(catalog, ref)
|
|
286
296
|
: capability === 'transcribe'
|
|
287
297
|
? transcribeCandidatesFromCatalog(catalog, ref)
|
|
288
298
|
: imageCandidatesFromCatalog(catalog, ref);
|
|
@@ -65,7 +65,11 @@ export async function prepareDshTextModelPort(textModel, binding, { signal } = {
|
|
|
65
65
|
if (signal?.aborted) throw abortError(signal, 'prepare', 'not_invoked');
|
|
66
66
|
return textModel.resolveModelInfo(route.provider, route.model, signal);
|
|
67
67
|
}), signal, 'prepare', 'not_invoked'); }
|
|
68
|
-
catch (_) {
|
|
68
|
+
catch (_) {
|
|
69
|
+
if (signal?.aborted) throw abortError(signal, 'prepare', 'not_invoked');
|
|
70
|
+
// 查询失败不同于成功查询但能力缺失;不要吞掉错误后进入付费生成。
|
|
71
|
+
throw new ModelPortError('IRIS_MODEL_UNAVAILABLE', { stage: 'prepare', invocation: 'not_invoked' });
|
|
72
|
+
}
|
|
69
73
|
}
|
|
70
74
|
if (signal?.aborted) throw abortError(signal, 'prepare', 'not_invoked');
|
|
71
75
|
return createDshTextModelPort(textModel, { ...route, sessionId }, { metadata });
|
|
@@ -73,7 +77,16 @@ export async function prepareDshTextModelPort(textModel, binding, { signal } = {
|
|
|
73
77
|
|
|
74
78
|
/** 已绑定事实的纯构造;用于缓存元数据/协议 fixture,不执行元数据查询。 */
|
|
75
79
|
export function createDshTextModelPort(textModel, binding, { metadata } = {}) {
|
|
76
|
-
|
|
80
|
+
const bound = createDshBoundModelPort(textModel, binding, { metadata });
|
|
81
|
+
const modalities = metadata?.inputModalities;
|
|
82
|
+
if (modalities !== undefined) {
|
|
83
|
+
if (!Array.isArray(modalities) || modalities.some(value => typeof value !== 'string' || !value.trim())) {
|
|
84
|
+
throw new ModelPortError('IRIS_MODEL_PROTOCOL_INVALID', { stage: 'prepare', invocation: 'not_invoked' });
|
|
85
|
+
}
|
|
86
|
+
if (!modalities.includes('text')) throw new ModelPortError('IRIS_MODEL_UNSUPPORTED', { stage: 'prepare', invocation: 'not_invoked' });
|
|
87
|
+
}
|
|
88
|
+
// 缺失能力只表示未知;宿主接受文本输入也不证明远端必定只返回文字。
|
|
89
|
+
return Object.freeze({ ...bound, textInput: modalities === undefined ? 'unknown' : 'supported' });
|
|
77
90
|
}
|
|
78
91
|
|
|
79
92
|
/** 文本与视觉共用经过验证的 DSH 事件读取器;图片桥接仅由视觉适配器注入。 */
|
package/lib/generation-input.js
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
|
+
import { normalizeMusicOptions } from './music-options.js';
|
|
5
6
|
|
|
6
7
|
const FIELDS = Object.freeze({
|
|
7
8
|
image: ['prompt', 'size', 'n', 'source_artifact_id'],
|
|
8
9
|
video: ['prompt', 'size', 'duration', 'img_data_url', 'first_frame_path', 'audio_path', 'resolution'],
|
|
9
10
|
tts: ['text', 'voice'],
|
|
11
|
+
music: ['prompt', 'timeout_ms', 'retry_502', 'retry_delay_ms'],
|
|
10
12
|
transcribe: ['audio_url', 'audio_path']
|
|
11
13
|
});
|
|
12
14
|
|
|
@@ -47,6 +49,7 @@ export function normalizeGenerationInput(capability, input, { allowAudioPath = f
|
|
|
47
49
|
const providerInput = {};
|
|
48
50
|
let audioPath;
|
|
49
51
|
let sourceArtifactId;
|
|
52
|
+
let musicOptions;
|
|
50
53
|
let firstFramePath, videoMode;
|
|
51
54
|
if (capability === 'video') {
|
|
52
55
|
audioPath = String(input.audio_path || '').trim();
|
|
@@ -101,6 +104,12 @@ export function normalizeGenerationInput(capability, input, { allowAudioPath = f
|
|
|
101
104
|
const voice = text(input.voice, 'voice', 64);
|
|
102
105
|
if (voice !== undefined) providerInput.voice = voice;
|
|
103
106
|
}
|
|
107
|
+
if (capability === 'music') {
|
|
108
|
+
providerInput.prompt = text(input.prompt, 'prompt', 20000, true);
|
|
109
|
+
try { musicOptions = normalizeMusicOptions(input); }
|
|
110
|
+
catch (error) { fail(error.message); }
|
|
111
|
+
providerInput.timeoutMs = musicOptions.timeoutMs;
|
|
112
|
+
}
|
|
104
113
|
if (capability === 'transcribe') {
|
|
105
114
|
const audioUrl = String(input.audio_url || '').trim();
|
|
106
115
|
audioPath = String(input.audio_path || '').trim();
|
|
@@ -119,6 +128,7 @@ export function normalizeGenerationInput(capability, input, { allowAudioPath = f
|
|
|
119
128
|
return Object.freeze({
|
|
120
129
|
modelRef,
|
|
121
130
|
providerInput: Object.freeze(providerInput),
|
|
131
|
+
...(musicOptions ? { musicOptions } : {}),
|
|
122
132
|
...(sourceArtifactId ? { sourceArtifactId } : {}),
|
|
123
133
|
...(audioPath ? { audioPath } : {}),
|
|
124
134
|
...(capability === 'video' ? { firstFramePath, videoMode } : {})
|
package/lib/host-runtime.js
CHANGED
|
@@ -13,6 +13,7 @@ export const EXPECTED_IRIS_TOOLS = Object.freeze([
|
|
|
13
13
|
'iris_draw_image',
|
|
14
14
|
'iris_edit_image',
|
|
15
15
|
'iris_generate_video',
|
|
16
|
+
'iris_generate_music',
|
|
16
17
|
'iris_speak_text',
|
|
17
18
|
'iris_transcribe_audio',
|
|
18
19
|
'iris_task_status',
|
|
@@ -28,8 +29,9 @@ export const EXPECTED_IRIS_TOOLS = Object.freeze([
|
|
|
28
29
|
]);
|
|
29
30
|
|
|
30
31
|
export const EXPECTED_IRIS_SKILLS = Object.freeze([
|
|
31
|
-
'iris-
|
|
32
|
-
'iris-compose-media'
|
|
32
|
+
'iris-use',
|
|
33
|
+
'iris-compose-media',
|
|
34
|
+
'iris-verify-ui'
|
|
33
35
|
]);
|
|
34
36
|
|
|
35
37
|
export const EXPECTED_IRIS_ROUTES = Object.freeze([
|