@mokuyoaxis/dsh-iris 0.2.5 → 0.2.7
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-use/SKILL.md +2 -2
- package/.dsh/skills/iris-use/references/media-contract.md +2 -2
- package/CHANGELOG.md +49 -1
- package/README.en.md +39 -22
- package/README.md +40 -21
- package/bin/dsh-iris.js +166 -35
- package/docs/AGENT_QUICKSTART.md +7 -7
- package/docs/ARCHITECTURE.md +9 -3
- package/docs/CLI_MANAGEMENT.md +5 -1
- package/docs/CORE_RUNTIME_CONTRACT.md +36 -5
- package/docs/CORE_SKILLS.md +62 -0
- package/docs/DSH_COMPATIBILITY.md +8 -4
- package/docs/DSH_CORE_MIGRATION.md +4 -2
- package/docs/DSH_PROVIDER_CONNECTIONS.md +34 -0
- package/docs/DSH_RUNTIME.md +36 -0
- package/docs/FUNCTION_MODEL_ASSIGNMENTS.md +2 -0
- package/docs/FUNCTION_MODEL_POOLS.md +84 -0
- package/docs/HEADLESS_CLI.md +16 -8
- package/docs/HOST_ADAPTER_CONTRACT.md +7 -2
- package/docs/HOST_DOCTOR.md +14 -2
- package/docs/IMAGE_FLOW.md +71 -0
- package/docs/LOCAL_API.md +60 -0
- package/docs/LOCAL_API_CONTRACT.md +115 -0
- package/docs/LOCAL_CREATION.md +50 -0
- package/docs/MUSIC.md +2 -2
- package/docs/PROVIDER_HEALTH.md +26 -2
- package/docs/ROADMAP.md +18 -4
- package/docs/SECURITY.md +20 -2
- package/docs/SYNC_DELIVERY_RECOVERY.md +2 -0
- package/docs/VISION_CLI.md +2 -2
- package/docs/WEB_PROTOTYPE.md +47 -0
- package/docs/releases/0.2.6.md +50 -0
- package/docs/releases/0.2.7.md +62 -0
- package/lib/actions.js +208 -90
- package/lib/adapters.js +36 -9
- package/lib/api.js +40 -39
- package/lib/application-runtime.js +186 -0
- package/lib/client.js +805 -182
- package/lib/composite-vision-routing.js +6 -6
- package/lib/config.js +140 -29
- package/lib/core-artifact-store.js +5 -0
- package/lib/core-maintenance.js +25 -2
- package/lib/core-runtime.js +19 -0
- package/lib/core-tasks.js +62 -8
- package/lib/creation-request-store.js +159 -0
- package/lib/doctor.js +20 -2
- package/lib/dsh-core-adapter.js +669 -564
- package/lib/dsh-host-adapter.js +14 -1
- package/lib/dsh-provider-config.js +35 -0
- package/lib/dsh-provider-source.js +102 -0
- package/lib/flow-store.js +195 -0
- package/lib/headless-vision.js +5 -5
- package/lib/host-contract.js +1 -0
- package/lib/http-vision-model-adapter.js +5 -2
- package/lib/image-flow.js +188 -0
- package/lib/index.js +103 -78
- package/lib/local-api-projection.js +27 -0
- package/lib/local-api.js +381 -0
- package/lib/local-creation.js +85 -0
- package/lib/model-directory-service.js +53 -0
- package/lib/model-directory.js +103 -0
- package/lib/models.js +71 -6
- package/lib/ocr-model-routing.js +2 -2
- package/lib/private-storage.js +12 -0
- package/lib/provider-adapters.js +42 -9
- package/lib/provider-catalog.js +14 -9
- package/lib/provider-config-scope.js +16 -0
- package/lib/provider-config-service.js +82 -13
- package/lib/provider-health.js +33 -0
- package/lib/provider-protocol.js +30 -0
- package/lib/provider-task-runner.js +22 -2
- package/lib/render-address.js +35 -0
- package/lib/render.js +52 -13
- package/lib/skill-installation.js +199 -0
- package/lib/vision-model-routing.js +25 -12
- package/lib/workbench-artifacts.js +1 -2
- package/lib/workbench-management.js +8 -9
- package/package.json +4 -3
- package/skills/iris-core/SKILL.md +40 -0
- package/skills/iris-core/agents/openai.yaml +4 -0
- package/skills/iris-core/references/cli.md +95 -0
- package/skills/iris-core/references/tasks.md +58 -0
- package/user_guide.md +32 -20
package/lib/index.js
CHANGED
|
@@ -18,7 +18,9 @@ import { runVisionRequest, readVisionImageFile } from './vision-model-routing.js
|
|
|
18
18
|
import { waitForModelWork } from './model-call-runtime.js';
|
|
19
19
|
import { hasHostPort, requireHostPort } from './host-contract.js';
|
|
20
20
|
import { createDshHostAdapter } from './dsh-host-adapter.js';
|
|
21
|
-
import {
|
|
21
|
+
import { withDshProviderConfig } from './dsh-provider-config.js';
|
|
22
|
+
import { createDshRuntime, dshCoreDataRoot } from './dsh-core-adapter.js';
|
|
23
|
+
import { ModelPortError } from './model-port-contract.js';
|
|
22
24
|
import { imageDimensions } from './pixels.js';
|
|
23
25
|
import { runLocateRequest, runSummaryRequest, describeGeneratedImage } from './composite-vision-routing.js';
|
|
24
26
|
import { normalizeSummarySource, summaryFrameRecords, summaryTimelineLabel } from './summary-input.js';
|
|
@@ -64,12 +66,11 @@ function outputDir() {
|
|
|
64
66
|
|
|
65
67
|
/* ---------------- 媒体路由:/iris/media/:taskId/:token/:name ---------------- */
|
|
66
68
|
|
|
67
|
-
|
|
68
|
-
function mountIrisRoutes(routeCtx) {
|
|
69
|
+
function mountIrisRoutes(routeCtx, core, routes) {
|
|
69
70
|
// Cordis 服务只在 DSH 宿主边界探测;Command 与通用工具辅助函数不接收原始 ctx
|
|
70
71
|
const host = dshHost(routeCtx);
|
|
71
72
|
const reg = hasHostPort(host, 'routes') ? host.ports.routes : undefined;
|
|
72
|
-
if (!reg || typeof reg.register !== 'function' ||
|
|
73
|
+
if (!reg || typeof reg.register !== 'function' || routes.mounted) return;
|
|
73
74
|
try {
|
|
74
75
|
const disposeMedia = reg.register({
|
|
75
76
|
kind: 'prefix',
|
|
@@ -79,21 +80,21 @@ function mountIrisRoutes(routeCtx) {
|
|
|
79
80
|
const disposeApi = reg.register({
|
|
80
81
|
kind: 'prefix',
|
|
81
82
|
path: '/iris/api',
|
|
82
|
-
handler: guarded((req, res) => serveApi(req, res, dshHost(routeCtx)))
|
|
83
|
+
handler: guarded((req, res) => serveApi(req, res, dshHost(routeCtx), core))
|
|
83
84
|
});
|
|
84
85
|
const disposeRender = reg.register({
|
|
85
86
|
kind: 'prefix',
|
|
86
87
|
path: '/iris/render',
|
|
87
88
|
handler: guarded(serveRender)
|
|
88
89
|
});
|
|
89
|
-
|
|
90
|
+
routes.mounted = true;
|
|
90
91
|
recordHostRoutes(EXPECTED_IRIS_ROUTES);
|
|
91
92
|
routeCtx.effect(() => () => {
|
|
92
93
|
if (typeof disposeMedia === 'function') disposeMedia();
|
|
93
94
|
if (typeof disposeApi === 'function') disposeApi();
|
|
94
95
|
if (typeof disposeRender === 'function') disposeRender();
|
|
95
96
|
closeAllSse(); // SSE 长连接随插件停用一起关闭,不留悬挂连接
|
|
96
|
-
|
|
97
|
+
routes.mounted = false;
|
|
97
98
|
}, 'iris: media+api+actions+render routes');
|
|
98
99
|
console.log('[iris] 媒体+工作台+操作路由已挂载:/iris/media · /iris/api · /iris/api/actions · /iris/render');
|
|
99
100
|
} catch (err) {
|
|
@@ -179,11 +180,11 @@ function coreTaskTerminalProblem(task, label = '任务') {
|
|
|
179
180
|
return label + '需要人工处理';
|
|
180
181
|
}
|
|
181
182
|
|
|
182
|
-
async function awaitCoreTerminal(taskId, { timeoutMs, signal }) {
|
|
183
|
+
async function awaitCoreTerminal(core, taskId, { timeoutMs, signal }) {
|
|
183
184
|
const deadline = Date.now() + timeoutMs;
|
|
184
185
|
for (;;) {
|
|
185
186
|
if (signal?.aborted) throw new Error('已取消;Core 任务仍保留远端受理事实,可稍后查询');
|
|
186
|
-
const task = await inspectProviderTaskForDsh(taskId, { signal });
|
|
187
|
+
const task = await core.inspectProviderTaskForDsh(taskId, { signal });
|
|
187
188
|
if (coreTaskWaitFinished(task)) return task;
|
|
188
189
|
if (Date.now() > deadline) return null;
|
|
189
190
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
@@ -219,6 +220,14 @@ function safeRegister(host, definition) {
|
|
|
219
220
|
|
|
220
221
|
export async function apply(ctx) {
|
|
221
222
|
beginHostRuntime({ pluginId: IRIS_PLUGIN_ID, version: PACKAGE_VERSION });
|
|
223
|
+
const core = createDshRuntime({ dataRoot: dshCoreDataRoot(), getHost: () => dshHost(ctx) });
|
|
224
|
+
ctx.provide('irisRuntime', core);
|
|
225
|
+
const routes = { mounted: false };
|
|
226
|
+
const { cropForDsh, diffForDsh, framesForDsh, summaryFramesForDsh, visionImageForDsh, htmlForDsh, inspectProviderTaskForDsh, projectCoreTaskForDsh, readCoreArtifactMediaForDsh, resumeProviderTaskWatchesForDsh } = core;
|
|
227
|
+
ctx.effect(() => () => {
|
|
228
|
+
tasks.stopWatchAll();
|
|
229
|
+
return core.dispose();
|
|
230
|
+
}, 'iris: DSH runtime');
|
|
222
231
|
guard('Iris 私有存储权限收紧', () => hardenPrivateTree(store.irisHome()));
|
|
223
232
|
|
|
224
233
|
guard('上传临时文件清理', () => purgeStaleUploads());
|
|
@@ -242,26 +251,18 @@ export async function apply(ctx) {
|
|
|
242
251
|
if (resumed.length) console.log(`[iris] 已恢复接管 ${resumed.length} 个后台任务`);
|
|
243
252
|
});
|
|
244
253
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if (resumed.length) console.log(
|
|
248
|
-
}
|
|
249
|
-
console.error('[iris] 恢复 Core
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Fiber 清理:插件停用/更新时停掉 legacy 与 Core 的全部 Host 盯守句柄
|
|
253
|
-
guard('盯守句柄注册', () => {
|
|
254
|
-
ctx.effect(() => () => {
|
|
255
|
-
tasks.stopWatchAll();
|
|
256
|
-
stopProviderTaskWatchesForDsh();
|
|
257
|
-
});
|
|
254
|
+
// Owned background setup lets unload cancel a slow connection resolver during takeover.
|
|
255
|
+
void resumeProviderTaskWatchesForDsh().then(resumed => {
|
|
256
|
+
if (resumed.length) console.log('[iris] 已恢复接管 ' + resumed.length + ' 个 Core 媒体任务');
|
|
257
|
+
}, error => {
|
|
258
|
+
if (core.snapshot().state === 'open') console.error('[iris] 恢复 Core 后台任务失败:', error?.code || 'unknown');
|
|
258
259
|
});
|
|
259
260
|
|
|
260
261
|
// 媒体路由(视频/音频在对话流里的可播通路;图片仍走原生附件)
|
|
261
|
-
ctx.inject(['webServer'], mountIrisRoutes);
|
|
262
|
-
ctx.inject(['httpServer'], mountIrisRoutes);
|
|
262
|
+
ctx.inject(['webServer'], routeCtx => mountIrisRoutes(routeCtx, core, routes));
|
|
263
|
+
ctx.inject(['httpServer'], routeCtx => mountIrisRoutes(routeCtx, core, routes));
|
|
263
264
|
// 若 webServer 已在运行,inject 回调也会触发;这里兜底立即挂载一次
|
|
264
|
-
guard('媒体路由兜底挂载', () => mountIrisRoutes(ctx));
|
|
265
|
+
guard('媒体路由兜底挂载', () => mountIrisRoutes(ctx, core, routes));
|
|
265
266
|
|
|
266
267
|
// DSH 的 Skill registry 是可选宿主能力。存在时把 npm 包内三项 Skill 注册到
|
|
267
268
|
// 全局层;项目目录中的同名 Skill 仍由 DSH 原生优先级覆盖。
|
|
@@ -274,6 +275,29 @@ export async function apply(ctx) {
|
|
|
274
275
|
});
|
|
275
276
|
|
|
276
277
|
const host = dshHost(ctx);
|
|
278
|
+
const providerTools = new Set(['iris_draw_image', 'iris_edit_image', 'iris_generate_video', 'iris_generate_music',
|
|
279
|
+
'iris_speak_text', 'iris_transcribe_audio', 'iris_look_at_image', 'iris_relook_attachment',
|
|
280
|
+
'iris_media_summarize', 'iris_locate', 'iris_long_ocr']);
|
|
281
|
+
const visionTools = new Set(['iris_look_at_image', 'iris_relook_attachment', 'iris_media_summarize', 'iris_locate', 'iris_long_ocr']);
|
|
282
|
+
function registerIrisTool(definition) {
|
|
283
|
+
safeRegister(host, { ...definition, execute(args, exec = {}) {
|
|
284
|
+
const owned = structuredClone(args || {});
|
|
285
|
+
return core.runHostOperation(signal => {
|
|
286
|
+
const execute = async () => {
|
|
287
|
+
if (visionTools.has(definition.name) && !owned.model) await store.refreshAutomaticDirectories({ capability: 'vision', signal });
|
|
288
|
+
signal.throwIfAborted();
|
|
289
|
+
return definition.execute(owned, { ...exec, signal });
|
|
290
|
+
};
|
|
291
|
+
return providerTools.has(definition.name)
|
|
292
|
+
? withDshProviderConfig(dshHost(ctx), execute, { signal }) : execute();
|
|
293
|
+
}, { signal: exec.signal }).catch(error => {
|
|
294
|
+
if (visionTools.has(definition.name) && exec.signal?.aborted && error === exec.signal.reason) {
|
|
295
|
+
throw new ModelPortError('IRIS_MODEL_ABORTED');
|
|
296
|
+
}
|
|
297
|
+
throw error;
|
|
298
|
+
});
|
|
299
|
+
} });
|
|
300
|
+
}
|
|
277
301
|
if (!hasHostPort(host, 'tools')) {
|
|
278
302
|
console.error('[iris] tools 服务不可用,跳过全部工具注册(宿主不受影响)');
|
|
279
303
|
return;
|
|
@@ -281,7 +305,7 @@ export async function apply(ctx) {
|
|
|
281
305
|
|
|
282
306
|
async function coreImageToolResult(submitted, prompt, signal) {
|
|
283
307
|
let final = await inspectProviderTaskForDsh(submitted.taskId, { signal });
|
|
284
|
-
if (!coreTaskWaitFinished(final)) final = await awaitCoreTerminal(submitted.taskId, { timeoutMs: AWAIT_MS.image, signal });
|
|
308
|
+
if (!coreTaskWaitFinished(final)) final = await awaitCoreTerminal(core, submitted.taskId, { timeoutMs: AWAIT_MS.image, signal });
|
|
285
309
|
if (!final) return { blocks: [{ type: 'text', text: `[iris] Core 图片任务仍在后台观察(task: ${submitted.taskId})。稍后可用 iris_task_status 查询进度与 Artifact。` }] };
|
|
286
310
|
const terminalProblem = coreTaskTerminalProblem(final, '图像生成');
|
|
287
311
|
if (terminalProblem) throw new Error(terminalProblem);
|
|
@@ -291,7 +315,7 @@ export async function apply(ctx) {
|
|
|
291
315
|
let note = '';
|
|
292
316
|
try {
|
|
293
317
|
const described = await describeGeneratedImage(dshHost(ctx), {
|
|
294
|
-
providers: store.pickAllFor(cap.CAPABILITIES.VISION), onRateLimit: store.recordRateLimit, originalPrompt: prompt, signal,
|
|
318
|
+
providers: store.pickAllFor(cap.CAPABILITIES.VISION), onRateLimit: store.recordRateLimit, onModelAvailability: store.recordAvailability, originalPrompt: prompt, signal,
|
|
295
319
|
async prepareImage(inputSignal) {
|
|
296
320
|
const first = await readCoreArtifactMediaForDsh(projected.artifacts[0].artifact.id, { signal: inputSignal });
|
|
297
321
|
return { bytes: new Uint8Array(first.bytes), mediaType: first.artifact.mediaType };
|
|
@@ -309,7 +333,7 @@ export async function apply(ctx) {
|
|
|
309
333
|
return { blocks };
|
|
310
334
|
}
|
|
311
335
|
|
|
312
|
-
|
|
336
|
+
registerIrisTool({
|
|
313
337
|
name: 'iris_import_image',
|
|
314
338
|
description: 'Save a local image or a current-session image attachment as a persistent Iris image Artifact for later viewing, editing and export. Provide exactly one of image_path or attachment_id. Supports static PNG/JPEG/WebP up to 20 MiB, preserves the source, and never calls a model.',
|
|
315
339
|
parameters: {
|
|
@@ -327,14 +351,14 @@ export async function apply(ctx) {
|
|
|
327
351
|
const attachmentId = String(args.attachment_id || '').trim();
|
|
328
352
|
if (Boolean(imagePath) === Boolean(attachmentId)) throw new Error('图片导入须选择image_path或attachment_id之一');
|
|
329
353
|
const input = imagePath ? { image_path: imagePath } : {
|
|
330
|
-
image_bytes: (await resolveVisionImageInput(dshHost(ctx), exec, { attachment_id: attachmentId }, exec.signal)).bytes
|
|
354
|
+
image_bytes: (await resolveVisionImageInput(core, dshHost(ctx), exec, { attachment_id: attachmentId }, exec.signal)).bytes
|
|
331
355
|
};
|
|
332
|
-
return runAction(dshHost(ctx), 'image_import', input, { signal: exec.signal });
|
|
356
|
+
return runAction(dshHost(ctx), 'image_import', input, { signal: exec.signal, core });
|
|
333
357
|
}
|
|
334
358
|
});
|
|
335
359
|
|
|
336
360
|
/* ---------- 🖼️ 画图 ---------- */
|
|
337
|
-
|
|
361
|
+
registerIrisTool({
|
|
338
362
|
name: 'iris_draw_image',
|
|
339
363
|
description:
|
|
340
364
|
'Generate an image from a detailed prompt using the user-configured image model and its configured image protocol. ' +
|
|
@@ -345,7 +369,7 @@ export async function apply(ctx) {
|
|
|
345
369
|
prompt: { type: 'string', description: 'Detailed visual description of the image to create' },
|
|
346
370
|
size: { type: 'string', description: "Output size; DashScope uses 'W*H' (e.g. '1024*1024'), OpenAI uses 'WxH'. Default provider default." },
|
|
347
371
|
n: { type: 'string', description: "Number of images, default '1'" },
|
|
348
|
-
model: { type: 'string', description: 'Optional
|
|
372
|
+
model: { type: 'string', description: 'Optional providerId::modelId in the enabled function pool; selects only this item (legacy bare model id also accepted)' }
|
|
349
373
|
},
|
|
350
374
|
required: ['prompt']
|
|
351
375
|
},
|
|
@@ -356,7 +380,7 @@ export async function apply(ctx) {
|
|
|
356
380
|
isConcurrencySafe: () => false,
|
|
357
381
|
async execute(args, exec) {
|
|
358
382
|
const prompt = String(args.prompt || '').trim();
|
|
359
|
-
const submitted = await runAction(dshHost(ctx), 'image', args, { signal: exec.signal });
|
|
383
|
+
const submitted = await runAction(dshHost(ctx), 'image', args, { signal: exec.signal, core });
|
|
360
384
|
if (submitted.storage === 'core') {
|
|
361
385
|
return coreImageToolResult(submitted, prompt, exec.signal);
|
|
362
386
|
}
|
|
@@ -390,7 +414,7 @@ export async function apply(ctx) {
|
|
|
390
414
|
let note = '';
|
|
391
415
|
try {
|
|
392
416
|
const described = await describeGeneratedImage(dshHost(ctx), {
|
|
393
|
-
providers: store.pickAllFor(cap.CAPABILITIES.VISION), onRateLimit: store.recordRateLimit, originalPrompt: prompt, signal: exec.signal,
|
|
417
|
+
providers: store.pickAllFor(cap.CAPABILITIES.VISION), onRateLimit: store.recordRateLimit, onModelAvailability: store.recordAvailability, originalPrompt: prompt, signal: exec.signal,
|
|
394
418
|
prepareImage: signal => readVisionImageFile(files[0], MEDIA_TYPES[path.extname(files[0]).toLowerCase()] || 'image/png', signal)
|
|
395
419
|
});
|
|
396
420
|
if (described) note = `画面内容:${described}`;
|
|
@@ -405,27 +429,27 @@ export async function apply(ctx) {
|
|
|
405
429
|
}
|
|
406
430
|
});
|
|
407
431
|
|
|
408
|
-
|
|
432
|
+
registerIrisTool({
|
|
409
433
|
name: 'iris_edit_image',
|
|
410
|
-
description: 'Edit an existing Core image by Artifact ID and an instruction using an
|
|
434
|
+
description: 'Edit an existing Core image by Artifact ID and an instruction using an allowed chat-image model or native DashScope Qwen edit model. Returns new Core Artifacts and DSH image attachments; preserves the source image and records its provenance.',
|
|
411
435
|
parameters: {
|
|
412
436
|
type: 'object', properties: {
|
|
413
437
|
source_artifact_id: { type: 'string', description: 'Core Artifact ID of the source PNG, JPEG or WebP image (up to 20 MiB)' },
|
|
414
438
|
prompt: { type: 'string', description: 'Describe the changes and what should be preserved in the source image' },
|
|
415
|
-
model: { type: 'string', description: 'Optional
|
|
439
|
+
model: { type: 'string', description: 'Optional providerId::modelId in the enabled image pool; selects only this item; supports openai-chat-images or DashScope Qwen edit/max/plus and 2.0/pro models' },
|
|
416
440
|
n: { type: 'string', description: "Number of images, default '1'" }
|
|
417
441
|
}, required: ['source_artifact_id', 'prompt']
|
|
418
442
|
},
|
|
419
443
|
output: { schema: { type: 'object' }, render: (_args, value) => value.blocks },
|
|
420
444
|
isConcurrencySafe: () => false,
|
|
421
445
|
async execute(args, exec) {
|
|
422
|
-
const submitted = await runAction(dshHost(ctx), 'image_edit', args, { signal: exec.signal });
|
|
446
|
+
const submitted = await runAction(dshHost(ctx), 'image_edit', args, { signal: exec.signal, core });
|
|
423
447
|
return coreImageToolResult(submitted, String(args.prompt || '').trim(), exec.signal);
|
|
424
448
|
}
|
|
425
449
|
});
|
|
426
450
|
|
|
427
451
|
/* ---------- 🎬 视频生成 ---------- */
|
|
428
|
-
|
|
452
|
+
registerIrisTool({
|
|
429
453
|
name: 'iris_generate_video',
|
|
430
454
|
description:
|
|
431
455
|
'Generate a video with the configured DashScope video model. Three modes: ' +
|
|
@@ -443,18 +467,18 @@ export async function apply(ctx) {
|
|
|
443
467
|
resolution: { type: 'string', description: "s2v only: output tier '480P' (default) or '720P'" },
|
|
444
468
|
size: { type: 'string', description: "t2v/i2v only: output size 'W*H', e.g. '1280*720' (default)" },
|
|
445
469
|
duration: { type: 'number', description: 't2v/i2v only: duration in seconds, if the model supports it' },
|
|
446
|
-
model: { type: 'string', description: 'Optional
|
|
470
|
+
model: { type: 'string', description: 'Optional providerId::modelId in the enabled function pool; selects only this item (legacy bare model id accepted)' }
|
|
447
471
|
},
|
|
448
472
|
required: []
|
|
449
473
|
},
|
|
450
474
|
output: { schema: { type: 'string' }, render: (_args, v) => [{ type: 'text', text: v }] },
|
|
451
475
|
isConcurrencySafe: () => false,
|
|
452
476
|
async execute(args, exec) {
|
|
453
|
-
const submitted = await runAction(dshHost(ctx), 'video', { ...args, session_id: exec.agent?.session?.id }, { signal: exec.signal });
|
|
477
|
+
const submitted = await runAction(dshHost(ctx), 'video', { ...args, session_id: exec.agent?.session?.id }, { signal: exec.signal, core });
|
|
454
478
|
if (submitted.storage === 'core') {
|
|
455
479
|
let final = await inspectProviderTaskForDsh(submitted.taskId, { signal: exec.signal });
|
|
456
480
|
if (!coreTaskWaitFinished(final)) {
|
|
457
|
-
final = await awaitCoreTerminal(submitted.taskId, { timeoutMs: AWAIT_MS.video, signal: exec.signal });
|
|
481
|
+
final = await awaitCoreTerminal(core, submitted.taskId, { timeoutMs: AWAIT_MS.video, signal: exec.signal });
|
|
458
482
|
}
|
|
459
483
|
if (!final) {
|
|
460
484
|
return `[iris] Core 视频任务已提交并转入后台观察(task: ${submitted.taskId},remoteTask: ${submitted.remoteTaskId})。\n` +
|
|
@@ -487,12 +511,12 @@ export async function apply(ctx) {
|
|
|
487
511
|
}
|
|
488
512
|
});
|
|
489
513
|
|
|
490
|
-
|
|
514
|
+
registerIrisTool({
|
|
491
515
|
name: 'iris_generate_music',
|
|
492
|
-
description: 'Generate music using a model
|
|
516
|
+
description: 'Generate music using a model in the enabled Music pool. Returns audio artifact IDs and playback links. Lyrics, vocals and duration are prompt intentions. Configured 502 retries may generate again.',
|
|
493
517
|
parameters: { type: 'object', properties: {
|
|
494
518
|
prompt: { type: 'string', description: 'Describe the music, including any lyrics, vocals, mood or intended duration' },
|
|
495
|
-
model: { type: 'string', description: 'Optional
|
|
519
|
+
model: { type: 'string', description: 'Optional providerId::modelId in the enabled Music pool; selects only this item' },
|
|
496
520
|
timeout_ms: { type: 'integer', description: 'Per-attempt timeout; default 300000 ms' },
|
|
497
521
|
retry_502: { type: 'integer', description: '502 regeneration retries, 0–5; default 2; each retry consumes another request' },
|
|
498
522
|
retry_delay_ms: { type: 'integer', description: 'Delay between 502 retries; default 5000 ms' }
|
|
@@ -500,14 +524,14 @@ export async function apply(ctx) {
|
|
|
500
524
|
output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: value }] },
|
|
501
525
|
isConcurrencySafe: () => true,
|
|
502
526
|
async execute(args, exec) {
|
|
503
|
-
const submitted = await runAction(dshHost(ctx), 'music', args, { signal: exec.signal });
|
|
527
|
+
const submitted = await runAction(dshHost(ctx), 'music', args, { signal: exec.signal, core });
|
|
504
528
|
return submitted.text + '\nartifact: ' + submitted.artifactIds.join(', ') + '\n' +
|
|
505
529
|
submitted.artifactIds.map(id => '[♪ 音乐播放](/iris/api/core/artifact/' + encodeURIComponent(id) + '/media)').join('\n');
|
|
506
530
|
}
|
|
507
531
|
});
|
|
508
532
|
|
|
509
533
|
/* ---------- 🔊 语音合成 ---------- */
|
|
510
|
-
|
|
534
|
+
registerIrisTool({
|
|
511
535
|
name: 'iris_speak_text',
|
|
512
536
|
description: 'Synthesize spoken audio from text using the configured TTS model. Returns Core audio Artifact IDs and playback links; export an Artifact to a host file before passing it as audio_path to another tool. Use iris_generate_music for songs or background music.',
|
|
513
537
|
parameters: {
|
|
@@ -515,14 +539,14 @@ export async function apply(ctx) {
|
|
|
515
539
|
properties: {
|
|
516
540
|
text: { type: 'string', description: 'Text to speak' },
|
|
517
541
|
voice: { type: 'string', description: "Voice name, e.g. 'Cherry' (DashScope qwen-tts voices)" },
|
|
518
|
-
model: { type: 'string', description: 'Optional
|
|
542
|
+
model: { type: 'string', description: 'Optional providerId::modelId in the enabled function pool; selects only this item (legacy bare model id accepted)' }
|
|
519
543
|
},
|
|
520
544
|
required: ['text']
|
|
521
545
|
},
|
|
522
546
|
output: { schema: { type: 'string' }, render: (_args, v) => [{ type: 'text', text: v }] },
|
|
523
547
|
isConcurrencySafe: () => true,
|
|
524
548
|
async execute(args, exec) {
|
|
525
|
-
const submitted = await runAction(dshHost(ctx), 'tts', args, { signal: exec.signal });
|
|
549
|
+
const submitted = await runAction(dshHost(ctx), 'tts', args, { signal: exec.signal, core });
|
|
526
550
|
if (submitted.storage === 'core') {
|
|
527
551
|
const final = await inspectProviderTaskForDsh(submitted.taskId, { signal: exec.signal });
|
|
528
552
|
const terminalProblem = coreTaskTerminalProblem(final, '语音合成');
|
|
@@ -545,25 +569,25 @@ export async function apply(ctx) {
|
|
|
545
569
|
});
|
|
546
570
|
|
|
547
571
|
/* ---------- 🎙️ 音频转写(阶段 7.2) ---------- */
|
|
548
|
-
|
|
572
|
+
registerIrisTool({
|
|
549
573
|
name: 'iris_transcribe_audio',
|
|
550
574
|
description: 'Transcribe an audio file (wav/mp3) to text using the configured DashScope provider (qwen-audio-3.0-asr-flash-filetrans). The audio is uploaded to Bailian temp storage, then an async task is submitted. Returns the full recognized text.',
|
|
551
575
|
parameters: {
|
|
552
576
|
type: 'object',
|
|
553
577
|
properties: {
|
|
554
578
|
audio_path: { type: 'string', description: 'Absolute path to the audio file (wav/mp3)' },
|
|
555
|
-
model: { type: 'string', description: 'Optional
|
|
579
|
+
model: { type: 'string', description: 'Optional providerId::modelId in the enabled transcription pool; selects only this item (legacy bare model id accepted)' }
|
|
556
580
|
},
|
|
557
581
|
required: ['audio_path']
|
|
558
582
|
},
|
|
559
583
|
output: { schema: { type: 'string' }, render: (_args, v) => [{ type: 'text', text: v }] },
|
|
560
584
|
isConcurrencySafe: () => false,
|
|
561
585
|
async execute(args, exec) {
|
|
562
|
-
const submitted = await runAction(dshHost(ctx), 'transcribe', args, { signal: exec.signal });
|
|
586
|
+
const submitted = await runAction(dshHost(ctx), 'transcribe', args, { signal: exec.signal, core });
|
|
563
587
|
if (submitted.storage === 'core') {
|
|
564
588
|
let final = await inspectProviderTaskForDsh(submitted.taskId, { signal: exec.signal });
|
|
565
589
|
if (!coreTaskWaitFinished(final)) {
|
|
566
|
-
final = await awaitCoreTerminal(submitted.taskId, { timeoutMs: AWAIT_MS.transcribe, signal: exec.signal });
|
|
590
|
+
final = await awaitCoreTerminal(core, submitted.taskId, { timeoutMs: AWAIT_MS.transcribe, signal: exec.signal });
|
|
567
591
|
}
|
|
568
592
|
if (!final) {
|
|
569
593
|
return `[iris] Core 转写任务已提交并转入后台观察(task: ${submitted.taskId})。完成后用 iris_task_status 查询进度与 Artifact。`;
|
|
@@ -588,7 +612,7 @@ export async function apply(ctx) {
|
|
|
588
612
|
});
|
|
589
613
|
|
|
590
614
|
/* ---------- 🎞️ 视频抽帧(阶段 7.1,ffmpeg 可选) ---------- */
|
|
591
|
-
|
|
615
|
+
registerIrisTool({
|
|
592
616
|
name: 'iris_video_frames',
|
|
593
617
|
description:
|
|
594
618
|
'Extract N frames (uniformly sampled across the video) from a local video file using the system ffmpeg. ' +
|
|
@@ -646,7 +670,7 @@ export async function apply(ctx) {
|
|
|
646
670
|
});
|
|
647
671
|
|
|
648
672
|
/* ---------- 📝 多模态视频摘要(阶段 7.3,ffmpeg 可选) ---------- */
|
|
649
|
-
|
|
673
|
+
registerIrisTool({
|
|
650
674
|
name: 'iris_media_summarize',
|
|
651
675
|
description:
|
|
652
676
|
'Summarize a local video or existing Core video-frame Artifacts as one contact sheet (with timestamps) plus an optional ' +
|
|
@@ -664,7 +688,7 @@ export async function apply(ctx) {
|
|
|
664
688
|
target_width: { type: 'integer', description: 'Frame width for sampling (default 640)' },
|
|
665
689
|
transcribe: { type: 'boolean', description: 'Auto-transcribe the audio track for video_path (default true unless transcribe_text is provided). Existing frames only consume explicit text.' },
|
|
666
690
|
transcribe_text: { type: 'string', description: 'Optional existing transcript text; does not create a transcription task' },
|
|
667
|
-
model: { type: 'string', description: 'Optional item from the vision
|
|
691
|
+
model: { type: 'string', description: 'Optional item from the enabled vision pool (providerId::modelId or host:current-vision); selects only this item' }
|
|
668
692
|
},
|
|
669
693
|
required: []
|
|
670
694
|
},
|
|
@@ -679,7 +703,7 @@ export async function apply(ctx) {
|
|
|
679
703
|
if (!ffmpegAvailable()) throw new Error('iris: ffmpeg 不可用,无法分析视频(可选安装 ffmpeg/ffprobe 后启用)');
|
|
680
704
|
}
|
|
681
705
|
const { answer, via, model, sheet, meta, frames, transcriptNote } = await runSummaryRequest(dshHost(ctx), {
|
|
682
|
-
providers: store.pickAllFor(cap.CAPABILITIES.VISION), onRateLimit: store.recordRateLimit, model: args.model, question: args.question, signal: exec.signal,
|
|
706
|
+
providers: store.pickAllFor(cap.CAPABILITIES.VISION), onRateLimit: store.recordRateLimit, onModelAvailability: store.recordAvailability, model: args.model, question: args.question, signal: exec.signal,
|
|
683
707
|
async prepareMedia(signal) {
|
|
684
708
|
if (source === 'core-artifacts') {
|
|
685
709
|
return { ...await summaryFramesForDsh({ frameArtifactIds: args.frame_artifact_ids, signal }), transcript: args.transcribe_text };
|
|
@@ -696,8 +720,8 @@ export async function apply(ctx) {
|
|
|
696
720
|
try {
|
|
697
721
|
const audio = await extractAudioTrack({ inputPath: p, signal });
|
|
698
722
|
try {
|
|
699
|
-
const submitted = await runAction(dshHost(ctx), 'transcribe', { audio_path: audio.filePath }, { signal });
|
|
700
|
-
const final = await awaitCoreTerminal(submitted.taskId, { timeoutMs: 120000, signal });
|
|
723
|
+
const submitted = await runAction(dshHost(ctx), 'transcribe', { audio_path: audio.filePath }, { signal, core });
|
|
724
|
+
const final = await awaitCoreTerminal(core, submitted.taskId, { timeoutMs: 120000, signal });
|
|
701
725
|
if (final?.outcome === 'succeeded' && final.deliveryState === 'ready') {
|
|
702
726
|
const texts = [];
|
|
703
727
|
for (const artifactId of final.artifactIds) {
|
|
@@ -745,7 +769,7 @@ export async function apply(ctx) {
|
|
|
745
769
|
|
|
746
770
|
/* ---------- 👁 视觉路由(M3):显式工具,自持栈为主 ---------- */
|
|
747
771
|
|
|
748
|
-
|
|
772
|
+
registerIrisTool({
|
|
749
773
|
name: 'iris_look_at_image',
|
|
750
774
|
description:
|
|
751
775
|
'Look at a local image file or an existing Core image Artifact and answer one question with the configured vision model (self-hosted provider stack, then DSH default vision). Provide exactly one of image_path or artifact_id. Local files are saved as durable DSH attachments; Core images are read directly without export or Core writes.',
|
|
@@ -755,7 +779,7 @@ export async function apply(ctx) {
|
|
|
755
779
|
image_path: { type: 'string', description: 'Absolute path to the image file (png/jpg/jpeg/webp)' },
|
|
756
780
|
artifact_id: { type: 'string', description: 'Existing Core image Artifact ID in the current profile (alternative to image_path)' },
|
|
757
781
|
question: { type: 'string', description: 'Question or extraction request about the image; defaults to a detailed description' },
|
|
758
|
-
model: { type: 'string', description: 'Optional item from the vision
|
|
782
|
+
model: { type: 'string', description: 'Optional item from the enabled vision pool (providerId::modelId or host:current-vision); selects only this item' }
|
|
759
783
|
},
|
|
760
784
|
required: []
|
|
761
785
|
},
|
|
@@ -784,7 +808,7 @@ export async function apply(ctx) {
|
|
|
784
808
|
}
|
|
785
809
|
});
|
|
786
810
|
|
|
787
|
-
|
|
811
|
+
registerIrisTool({
|
|
788
812
|
name: 'iris_relook_attachment',
|
|
789
813
|
description:
|
|
790
814
|
'Ask a NEW question about an image that was already seen in this session: pass its attachment_id (from user uploads, tool results, or any iris-generated image). Pixels are re-read via the vision model.',
|
|
@@ -793,7 +817,7 @@ export async function apply(ctx) {
|
|
|
793
817
|
properties: {
|
|
794
818
|
attachment_id: { type: 'string', description: 'The attachment_id of an image that appeared in this session or was generated by iris' },
|
|
795
819
|
question: { type: 'string', description: 'New question about the image pixels' },
|
|
796
|
-
model: { type: 'string', description: 'Optional item from the vision
|
|
820
|
+
model: { type: 'string', description: 'Optional item from the enabled vision pool (providerId::modelId or host:current-vision); selects only this item' }
|
|
797
821
|
},
|
|
798
822
|
required: ['attachment_id', 'question']
|
|
799
823
|
},
|
|
@@ -820,7 +844,7 @@ export async function apply(ctx) {
|
|
|
820
844
|
});
|
|
821
845
|
|
|
822
846
|
/* ---------- 📋 任务查询 ---------- */
|
|
823
|
-
|
|
847
|
+
registerIrisTool({
|
|
824
848
|
name: 'iris_task_status',
|
|
825
849
|
description:
|
|
826
850
|
'Query Iris tasks (image/video/tts/music/transcription). Pass a task_id for one task, or omit it to list recent tasks. Core results provide Artifact IDs and playback links; legacy tasks may provide file paths. Querying does not re-observe or redeliver a task.',
|
|
@@ -834,13 +858,13 @@ export async function apply(ctx) {
|
|
|
834
858
|
output: { schema: { type: 'string' }, render: (_args, v) => [{ type: 'text', text: v }] },
|
|
835
859
|
isConcurrencySafe: () => true,
|
|
836
860
|
async execute(args) {
|
|
837
|
-
const result = await runAction(dshHost(ctx), 'status', args || {});
|
|
861
|
+
const result = await runAction(dshHost(ctx), 'status', args || {}, { core });
|
|
838
862
|
return result.text;
|
|
839
863
|
}
|
|
840
864
|
});
|
|
841
865
|
|
|
842
866
|
/* ---------- ✂️ 确定性像素工具(阶段 2) ---------- */
|
|
843
|
-
|
|
867
|
+
registerIrisTool({
|
|
844
868
|
name: 'iris_crop',
|
|
845
869
|
description:
|
|
846
870
|
'Crop a rectangular region from an image (absolute local path or a session attachment_id) and save the result as a durable DSH attachment. ' +
|
|
@@ -890,7 +914,7 @@ export async function apply(ctx) {
|
|
|
890
914
|
}
|
|
891
915
|
});
|
|
892
916
|
|
|
893
|
-
|
|
917
|
+
registerIrisTool({
|
|
894
918
|
name: 'iris_pixel_diff',
|
|
895
919
|
description:
|
|
896
920
|
'Compute a pixel-level difference between two images (absolute local paths or session attachment_ids). ' +
|
|
@@ -945,7 +969,7 @@ export async function apply(ctx) {
|
|
|
945
969
|
});
|
|
946
970
|
|
|
947
971
|
/* ---------- 📍 模型驱动定位(阶段 3A) ---------- */
|
|
948
|
-
|
|
972
|
+
registerIrisTool({
|
|
949
973
|
name: 'iris_locate',
|
|
950
974
|
description:
|
|
951
975
|
'Locate an object/region described by `target` in an image (exactly one of absolute image_path, Core artifact_id, or session attachment_id), ' +
|
|
@@ -958,7 +982,7 @@ export async function apply(ctx) {
|
|
|
958
982
|
artifact_id: { type: 'string', description: 'Existing Core image Artifact ID in the current profile (alternative to image_path or attachment_id)' },
|
|
959
983
|
attachment_id: { type: 'string', description: 'Session attachment id of an image (alternative to image_path)' },
|
|
960
984
|
target: { type: 'string', description: 'The object or UI element to locate, e.g. "send button"' },
|
|
961
|
-
model: { type: 'string', description: 'Optional item from the vision
|
|
985
|
+
model: { type: 'string', description: 'Optional item from the enabled vision pool (providerId::modelId or host:current-vision); selects only this item' }
|
|
962
986
|
},
|
|
963
987
|
required: ['target']
|
|
964
988
|
},
|
|
@@ -966,8 +990,8 @@ export async function apply(ctx) {
|
|
|
966
990
|
isConcurrencySafe: () => true,
|
|
967
991
|
async execute(args, exec) {
|
|
968
992
|
const r = await runLocateRequest(dshHost(ctx), {
|
|
969
|
-
providers: store.pickAllFor(cap.CAPABILITIES.VISION), onRateLimit: store.recordRateLimit, model: args.model, target: args.target,
|
|
970
|
-
prepareImage: signal => resolveVisionImageInput(dshHost(ctx), exec, args, signal), signal: exec.signal
|
|
993
|
+
providers: store.pickAllFor(cap.CAPABILITIES.VISION), onRateLimit: store.recordRateLimit, onModelAvailability: store.recordAvailability, model: args.model, target: args.target,
|
|
994
|
+
prepareImage: signal => resolveVisionImageInput(core, dshHost(ctx), exec, args, signal), signal: exec.signal
|
|
971
995
|
});
|
|
972
996
|
const { width, height } = r;
|
|
973
997
|
const sourceNote = args.artifact_id ? `\nCore Artifact: ${args.artifact_id}` : '';
|
|
@@ -980,7 +1004,7 @@ export async function apply(ctx) {
|
|
|
980
1004
|
});
|
|
981
1005
|
|
|
982
1006
|
/* ---------- 🖼️ HTML 截图(阶段 3C,基于 dsh-builtin-browser) ---------- */
|
|
983
|
-
|
|
1007
|
+
registerIrisTool({
|
|
984
1008
|
name: 'iris_html_screenshot',
|
|
985
1009
|
description:
|
|
986
1010
|
'Render an HTML string to a page screenshot using the shared browser. ' +
|
|
@@ -1033,7 +1057,7 @@ export async function apply(ctx) {
|
|
|
1033
1057
|
});
|
|
1034
1058
|
|
|
1035
1059
|
/* ---------- 📄 长截图 OCR(阶段 3B,视觉模型分块) ---------- */
|
|
1036
|
-
|
|
1060
|
+
registerIrisTool({
|
|
1037
1061
|
name: 'iris_long_ocr',
|
|
1038
1062
|
description:
|
|
1039
1063
|
'OCR a (possibly long) image from exactly one of image_path, Core artifact_id, or session attachment_id: slice it into chunks (1200px tall with 120px overlap by default) ' +
|
|
@@ -1055,8 +1079,8 @@ export async function apply(ctx) {
|
|
|
1055
1079
|
async execute(args, exec) {
|
|
1056
1080
|
const host = dshHost(ctx);
|
|
1057
1081
|
const result = await runOcrRequest(host, {
|
|
1058
|
-
providers: store.pickAllFor(cap.CAPABILITIES.VISION), onRateLimit: store.recordRateLimit,
|
|
1059
|
-
prepareImage: operationSignal => resolveVisionImageInput(host, exec, args, operationSignal),
|
|
1082
|
+
providers: store.pickAllFor(cap.CAPABILITIES.VISION), onRateLimit: store.recordRateLimit, onModelAvailability: store.recordAvailability,
|
|
1083
|
+
prepareImage: operationSignal => resolveVisionImageInput(core, host, exec, args, operationSignal),
|
|
1060
1084
|
chunkHeight: args.chunk_height,
|
|
1061
1085
|
overlap: args.overlap,
|
|
1062
1086
|
signal: exec.signal
|
|
@@ -1115,10 +1139,10 @@ async function resolveImageInput(host, exec, { image_path, attachment_id }) {
|
|
|
1115
1139
|
}
|
|
1116
1140
|
|
|
1117
1141
|
/** 视觉解引用在整体预算内;路径限额读,Host 会话/附件等待可取消。 */
|
|
1118
|
-
async function resolveVisionImageInput(host, exec, input, signal) {
|
|
1142
|
+
async function resolveVisionImageInput(core, host, exec, input, signal) {
|
|
1119
1143
|
const source = normalizeVisionImageSource(input, { allowAttachment: true });
|
|
1120
1144
|
const { image_path, artifact_id, attachment_id } = input;
|
|
1121
|
-
if (source === 'artifact_id') return visionImageForDsh({ artifactId: artifact_id, signal });
|
|
1145
|
+
if (source === 'artifact_id') return core.visionImageForDsh({ artifactId: artifact_id, signal });
|
|
1122
1146
|
if (image_path) {
|
|
1123
1147
|
const file = String(image_path).trim();
|
|
1124
1148
|
const mediaType = MEDIA_TYPES[path.extname(file).toLowerCase()];
|
|
@@ -1180,7 +1204,8 @@ export async function sessionAttachmentRef(host, exec, attachmentId) {
|
|
|
1180
1204
|
*/
|
|
1181
1205
|
export async function askVision(host, { question, dataUrl, image, prepareImage, signal, model, budget }) {
|
|
1182
1206
|
const visionProviders = store.pickAllFor(cap.CAPABILITIES.VISION);
|
|
1183
|
-
return runVisionRequest(host, { providers: visionProviders, model, question, dataUrl, image, prepareImage, signal, budget,
|
|
1207
|
+
return runVisionRequest(host, { providers: visionProviders, model, question, dataUrl, image, prepareImage, signal, budget,
|
|
1208
|
+
onRateLimit: store.recordRateLimit, onModelAvailability: store.recordAvailability });
|
|
1184
1209
|
}
|
|
1185
1210
|
|
|
1186
1211
|
/**
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// HTTP DTOs whitelist existing Core facts; never expose records or configuration wholesale.
|
|
2
|
+
const STATUSES = new Set(['not_started', 'running', 'needs_attention', 'delivery_failed',
|
|
3
|
+
'needs_comparison', 'awaiting_selection', 'selected']);
|
|
4
|
+
|
|
5
|
+
export function projectLocalApiFlow(facts) {
|
|
6
|
+
if (facts.contractVersion !== 0 || facts.run.recipe !== 'image-edit/v0' || !STATUSES.has(facts.status)) {
|
|
7
|
+
throw new TypeError('不支持的流程契约或状态');
|
|
8
|
+
}
|
|
9
|
+
const { run, task } = facts;
|
|
10
|
+
const ref = value => value ? { artifactId: value.artifactId } : null;
|
|
11
|
+
return {
|
|
12
|
+
contractVersion: 0, id: run.id, recipe: run.recipe, revision: run.revision,
|
|
13
|
+
createdAt: run.createdAt, updatedAt: run.updatedAt, status: facts.status,
|
|
14
|
+
input: { prompt: run.input.prompt, modelRef: run.input.model_ref,
|
|
15
|
+
...(run.input.size !== undefined ? { size: run.input.size } : {}) },
|
|
16
|
+
source: ref(run.source), output: ref(run.output), comparison: ref(run.comparison), selection: ref(run.selection),
|
|
17
|
+
task: task ? { id: task.id, acceptance: task.acceptance, outcome: task.outcome,
|
|
18
|
+
deliveryState: task.deliveryState, attemptCount: task.attempts.length } : null,
|
|
19
|
+
metrics: facts.metrics ? Object.fromEntries(['ratio', 'diffPixels', 'totalPixels', 'width', 'height']
|
|
20
|
+
.map(key => [key, facts.metrics[key]])) : null
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function projectLocalApiRunSummary(run) {
|
|
25
|
+
return { id: run.id, recipe: run.recipe, createdAt: run.createdAt,
|
|
26
|
+
modelRef: run.modelRef, source: { artifactId: run.source.artifactId } };
|
|
27
|
+
}
|