@mokuyoaxis/dsh-iris 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dsh/skills/iris-compose-media/SKILL.md +10 -5
- package/.dsh/skills/iris-compose-media/references/workflows.md +7 -1
- package/.dsh/skills/iris-verify-ui/SKILL.md +2 -1
- package/CHANGELOG.md +69 -2
- package/README.en.md +67 -31
- package/README.md +67 -31
- package/bin/dsh-iris.js +222 -21
- package/docs/AGENT_QUICKSTART.md +10 -6
- package/docs/ARCHITECTURE.md +34 -19
- package/docs/ARTIFACT_LIBRARY.md +5 -5
- package/docs/ARTIFACT_MANIFEST.md +1 -1
- package/docs/CLI_MANAGEMENT.md +183 -0
- package/docs/COMPOSITE_VISION.md +35 -0
- package/docs/CORE_RUNTIME_CONTRACT.md +16 -7
- package/docs/DSH_CORE_MIGRATION.md +48 -19
- package/docs/DSH_RC2_ADAPTATION.md +4 -2
- package/docs/FAULT_INJECTION.md +21 -2
- package/docs/HEADLESS_CLI.md +15 -7
- package/docs/HOST_ADAPTER_CONTRACT.md +10 -10
- package/docs/HOST_DOCTOR.md +3 -3
- package/docs/MODEL_PORT_CONTRACT.md +21 -14
- package/docs/OCR_MODEL.md +39 -0
- package/docs/PROMPT_OPTIMIZER.md +77 -0
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +15 -5
- package/docs/PROVIDER_HEALTH.md +22 -5
- package/docs/PROVIDER_RUNTIME_HARNESS.md +1 -1
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +2 -2
- package/docs/ROADMAP.md +37 -19
- package/docs/SECURITY.md +5 -5
- package/docs/TASK_SEMANTICS.md +2 -2
- package/docs/VISION_CLI.md +132 -0
- package/docs/VISION_MODEL.md +51 -0
- package/docs/WORKBENCH_ARTIFACTS.md +67 -0
- package/docs/file-access-across-environments.md +8 -5
- package/docs/releases/0.2.1.md +56 -0
- package/docs/releases/0.2.2.md +31 -0
- package/lib/actions.js +199 -286
- package/lib/adapters.js +216 -41
- package/lib/api.js +49 -7
- package/lib/capability.js +23 -16
- package/lib/chromium-browser.js +117 -0
- package/lib/client.js +933 -117
- package/lib/command-service.js +121 -7
- package/lib/composite-vision-routing.js +42 -0
- package/lib/config.js +102 -14
- package/lib/core-artifact-store.js +45 -13
- package/lib/core-lease-recovery.js +64 -92
- package/lib/core-maintenance.js +213 -0
- package/lib/core-runtime.js +45 -47
- package/lib/core-tasks.js +12 -5
- package/lib/core-user-projection.js +1 -0
- package/lib/core-writer-lease.js +134 -0
- package/lib/dsh-core-adapter.js +63 -9
- package/lib/dsh-text-model-adapter.js +222 -0
- package/lib/dsh-vision-model-adapter.js +75 -0
- package/lib/generation-input.js +36 -5
- package/lib/headless-vision.js +181 -0
- package/lib/host-runtime.js +1 -0
- package/lib/http-vision-model-adapter.js +187 -0
- package/lib/image-edit-input.js +34 -0
- package/lib/index.js +205 -175
- package/lib/locate.js +86 -28
- package/lib/media-probe.js +2 -1
- package/lib/model-call-runtime.js +45 -0
- package/lib/model-invoker.js +4 -2
- package/lib/model-port-contract.js +13 -5
- package/lib/models.js +29 -42
- package/lib/ocr-model-routing.js +26 -0
- package/lib/ocr.js +110 -64
- package/lib/prompt-optimizer-config.js +21 -10
- package/lib/prompt-optimizer-core.js +247 -0
- package/lib/prompt-optimizer.js +37 -193
- package/lib/provider-adapters.js +87 -10
- package/lib/provider-catalog.js +19 -7
- package/lib/provider-config-service.js +235 -0
- package/lib/provider-contract.js +3 -2
- package/lib/provider-health.js +80 -1
- package/lib/provider-protocol.js +84 -0
- package/lib/provider-task-runner.js +63 -19
- package/lib/summarize.js +31 -20
- package/lib/summary-input.js +52 -0
- package/lib/task-wait.js +40 -0
- package/lib/video-input.js +30 -0
- package/lib/vision-core.js +42 -0
- package/lib/vision-image-input.js +83 -0
- package/lib/vision-input.js +31 -0
- package/lib/vision-model-routing.js +180 -0
- package/lib/vision.js +3 -2
- package/lib/workbench-artifacts.js +52 -0
- package/lib/workbench-management.js +151 -0
- package/lib/workbench-zip.js +39 -0
- package/package.json +1 -1
- package/user_guide.md +74 -30
package/lib/index.js
CHANGED
|
@@ -14,19 +14,22 @@ import * as cap from './capability.js';
|
|
|
14
14
|
import * as tasks from './tasks.js';
|
|
15
15
|
import { registerMedia, mediaLinksOf, authorizeMedia, serveMedia } from './media.js';
|
|
16
16
|
import { serveApi, closeAllSse, purgeStaleUploads } from './api.js';
|
|
17
|
-
import {
|
|
17
|
+
import { runVisionRequest, readVisionImageFile } from './vision-model-routing.js';
|
|
18
|
+
import { waitForModelWork } from './model-call-runtime.js';
|
|
18
19
|
import { hasHostPort, requireHostPort } from './host-contract.js';
|
|
19
20
|
import { createDshHostAdapter } from './dsh-host-adapter.js';
|
|
20
|
-
import { cropForDsh, diffForDsh, framesForDsh, inspectProviderTaskForDsh, projectCoreTaskForDsh, readCoreArtifactMediaForDsh, resumeProviderTaskWatchesForDsh, stopProviderTaskWatchesForDsh } from './dsh-core-adapter.js';
|
|
21
|
+
import { cropForDsh, diffForDsh, framesForDsh, summaryFramesForDsh, visionImageForDsh, htmlForDsh, inspectProviderTaskForDsh, projectCoreTaskForDsh, readCoreArtifactMediaForDsh, resumeProviderTaskWatchesForDsh, stopProviderTaskWatchesForDsh } from './dsh-core-adapter.js';
|
|
21
22
|
import { imageDimensions } from './pixels.js';
|
|
22
|
-
import {
|
|
23
|
+
import { runLocateRequest, runSummaryRequest, describeGeneratedImage } from './composite-vision-routing.js';
|
|
24
|
+
import { normalizeSummarySource, summaryFrameRecords, summaryTimelineLabel } from './summary-input.js';
|
|
25
|
+
import { normalizeVisionImageSource } from './vision-input.js';
|
|
23
26
|
import { serveRender } from './render.js';
|
|
24
27
|
import { ensurePrivateDir, hardenPrivateTree } from './private-storage.js';
|
|
25
28
|
import { guarded } from './guard.js';
|
|
26
|
-
import {
|
|
29
|
+
import { formatOcrResult } from './ocr.js';
|
|
30
|
+
import { runOcrRequest } from './ocr-model-routing.js';
|
|
27
31
|
import { runAction, listActions, taskPollDeps } from './actions.js';
|
|
28
32
|
import { ffmpegAvailable, extractFrames, probeVideo, extractAudioTrack } from './media-probe.js';
|
|
29
|
-
import { buildContactSheet, summarizeMedia } from './summarize.js';
|
|
30
33
|
import { registerBundledSkills } from './bundled-skills.js';
|
|
31
34
|
import {
|
|
32
35
|
EXPECTED_IRIS_ROUTES, IRIS_PLUGIN_ID, beginHostRuntime, recordHostRoutes, recordHostSkills, recordHostTool
|
|
@@ -187,13 +190,6 @@ async function awaitCoreTerminal(taskId, { timeoutMs, signal }) {
|
|
|
187
190
|
}
|
|
188
191
|
}
|
|
189
192
|
|
|
190
|
-
/* ---------------- 图片 data URL(视觉工具共用) ---------------- */
|
|
191
|
-
|
|
192
|
-
function toDataUrl(absPath, mediaType) {
|
|
193
|
-
const buf = fs.readFileSync(absPath);
|
|
194
|
-
return `data:${mediaType};base64,${buf.toString('base64')}`;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
193
|
export const name = 'mokuyoaxis-dsh-iris';
|
|
198
194
|
export const inject = ['tools'];
|
|
199
195
|
|
|
@@ -283,11 +279,41 @@ export async function apply(ctx) {
|
|
|
283
279
|
return;
|
|
284
280
|
}
|
|
285
281
|
|
|
282
|
+
async function coreImageToolResult(submitted, prompt, signal) {
|
|
283
|
+
let final = await inspectProviderTaskForDsh(submitted.taskId, { signal });
|
|
284
|
+
if (!coreTaskWaitFinished(final)) final = await awaitCoreTerminal(submitted.taskId, { timeoutMs: AWAIT_MS.image, signal });
|
|
285
|
+
if (!final) return { blocks: [{ type: 'text', text: `[iris] Core 图片任务仍在后台观察(task: ${submitted.taskId})。稍后可用 iris_task_status 查询进度与 Artifact。` }] };
|
|
286
|
+
const terminalProblem = coreTaskTerminalProblem(final, '图像生成');
|
|
287
|
+
if (terminalProblem) throw new Error(terminalProblem);
|
|
288
|
+
const projected = await projectCoreTaskForDsh(dshHost(ctx), submitted.taskId, { signal });
|
|
289
|
+
const refs = projected.artifacts.map((item) => item.attachment);
|
|
290
|
+
const blocks = projected.artifacts.map((item) => ({ type: 'image', attachment: item.attachment }));
|
|
291
|
+
let note = '';
|
|
292
|
+
try {
|
|
293
|
+
const described = await describeGeneratedImage(dshHost(ctx), {
|
|
294
|
+
providers: store.pickAllFor(cap.CAPABILITIES.VISION), onRateLimit: store.recordRateLimit, originalPrompt: prompt, signal,
|
|
295
|
+
async prepareImage(inputSignal) {
|
|
296
|
+
const first = await readCoreArtifactMediaForDsh(projected.artifacts[0].artifact.id, { signal: inputSignal });
|
|
297
|
+
return { bytes: new Uint8Array(first.bytes), mediaType: first.artifact.mediaType };
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
if (described) note = `画面内容:${described}`;
|
|
301
|
+
} catch (_) { /* 自述失败不影响 Core 交付 */ }
|
|
302
|
+
blocks.unshift({ type: 'text', text:
|
|
303
|
+
`[iris] 图像已生成(${submitted.model},Core task: ${submitted.taskId})。` +
|
|
304
|
+
(submitted.sourceArtifactId ? `\n原图 artifact: ${submitted.sourceArtifactId}` : '') +
|
|
305
|
+
(note ? `\n${note}` : '') +
|
|
306
|
+
`\nartifact: ${projected.artifacts.map((item) => item.artifact.id).join(', ')}` +
|
|
307
|
+
`\nattachment: ${refs.map((ref) => ref.attachmentId).join(', ')}`
|
|
308
|
+
});
|
|
309
|
+
return { blocks };
|
|
310
|
+
}
|
|
311
|
+
|
|
286
312
|
/* ---------- 🖼️ 画图 ---------- */
|
|
287
313
|
safeRegister(host, {
|
|
288
314
|
name: 'iris_draw_image',
|
|
289
315
|
description:
|
|
290
|
-
'Generate an image from a detailed prompt using the user-configured image model
|
|
316
|
+
'Generate an image from a detailed prompt using the user-configured image model and its configured image protocol. ' +
|
|
291
317
|
'Returns a durable DSH attachment rendered in the conversation plus a vision-model description so you know what was drawn.',
|
|
292
318
|
parameters: {
|
|
293
319
|
type: 'object',
|
|
@@ -308,32 +334,7 @@ export async function apply(ctx) {
|
|
|
308
334
|
const prompt = String(args.prompt || '').trim();
|
|
309
335
|
const submitted = await runAction(dshHost(ctx), 'image', args, { signal: exec.signal });
|
|
310
336
|
if (submitted.storage === 'core') {
|
|
311
|
-
|
|
312
|
-
if (!coreTaskWaitFinished(final)) {
|
|
313
|
-
final = await awaitCoreTerminal(submitted.taskId, { timeoutMs: AWAIT_MS.image, signal: exec.signal });
|
|
314
|
-
}
|
|
315
|
-
if (!final) {
|
|
316
|
-
return { blocks: [{ type: 'text', text: `[iris] Core 图片任务仍在后台观察(task: ${submitted.taskId})。稍后可用 iris_task_status 查询进度与 Artifact。` }] };
|
|
317
|
-
}
|
|
318
|
-
const terminalProblem = coreTaskTerminalProblem(final, '图像生成');
|
|
319
|
-
if (terminalProblem) throw new Error(terminalProblem);
|
|
320
|
-
const projected = await projectCoreTaskForDsh(dshHost(ctx), submitted.taskId, { signal: exec.signal });
|
|
321
|
-
const refs = projected.artifacts.map((item) => item.attachment);
|
|
322
|
-
const blocks = projected.artifacts.map((item) => ({ type: 'image', attachment: item.attachment }));
|
|
323
|
-
let note = '';
|
|
324
|
-
try {
|
|
325
|
-
const first = await readCoreArtifactMediaForDsh(projected.artifacts[0].artifact.id, { signal: exec.signal });
|
|
326
|
-
const described = await describeImageData(dshHost(ctx), refs[0],
|
|
327
|
-
`data:${first.artifact.mediaType};base64,${first.bytes.toString('base64')}`, prompt);
|
|
328
|
-
if (described) note = `画面内容:${described}`;
|
|
329
|
-
} catch (_) { /* 自述是增强项,失败不影响 Core 交付 */ }
|
|
330
|
-
blocks.unshift({ type: 'text', text:
|
|
331
|
-
`[iris] 图像已生成(${submitted.model},Core task: ${submitted.taskId})。` +
|
|
332
|
-
(note ? `\n${note}` : '') +
|
|
333
|
-
`\nartifact: ${projected.artifacts.map((item) => item.artifact.id).join(', ')}` +
|
|
334
|
-
`\nattachment: ${refs.map((ref) => ref.attachmentId).join(', ')}`
|
|
335
|
-
});
|
|
336
|
-
return { blocks };
|
|
337
|
+
return coreImageToolResult(submitted, prompt, exec.signal);
|
|
337
338
|
}
|
|
338
339
|
let final = tasks.get(submitted.taskId);
|
|
339
340
|
if (final && !taskWaitFinished(final)) {
|
|
@@ -364,7 +365,10 @@ export async function apply(ctx) {
|
|
|
364
365
|
|
|
365
366
|
let note = '';
|
|
366
367
|
try {
|
|
367
|
-
const described = await
|
|
368
|
+
const described = await describeGeneratedImage(dshHost(ctx), {
|
|
369
|
+
providers: store.pickAllFor(cap.CAPABILITIES.VISION), onRateLimit: store.recordRateLimit, originalPrompt: prompt, signal: exec.signal,
|
|
370
|
+
prepareImage: signal => readVisionImageFile(files[0], MEDIA_TYPES[path.extname(files[0]).toLowerCase()] || 'image/png', signal)
|
|
371
|
+
});
|
|
368
372
|
if (described) note = `画面内容:${described}`;
|
|
369
373
|
} catch (_) { /* 增强失败不影响生成结果 */ }
|
|
370
374
|
blocks.unshift({
|
|
@@ -377,6 +381,25 @@ export async function apply(ctx) {
|
|
|
377
381
|
}
|
|
378
382
|
});
|
|
379
383
|
|
|
384
|
+
safeRegister(host, {
|
|
385
|
+
name: 'iris_edit_image',
|
|
386
|
+
description: 'Edit an existing Core image by Artifact ID and an instruction using a configured openai-chat-images model. Returns new Core Artifacts and DSH image attachments; preserves the source image and records its provenance.',
|
|
387
|
+
parameters: {
|
|
388
|
+
type: 'object', properties: {
|
|
389
|
+
source_artifact_id: { type: 'string', description: 'Core Artifact ID of the source PNG, JPEG or WebP image (up to 20 MiB)' },
|
|
390
|
+
prompt: { type: 'string', description: 'Describe the changes and what should be preserved in the source image' },
|
|
391
|
+
model: { type: 'string', description: 'Optional providerId::modelId; image protocol must be openai-chat-images' },
|
|
392
|
+
n: { type: 'string', description: "Number of images, default '1'" }
|
|
393
|
+
}, required: ['source_artifact_id', 'prompt']
|
|
394
|
+
},
|
|
395
|
+
output: { schema: { type: 'object' }, render: (_args, value) => value.blocks },
|
|
396
|
+
isConcurrencySafe: () => false,
|
|
397
|
+
async execute(args, exec) {
|
|
398
|
+
const submitted = await runAction(dshHost(ctx), 'image_edit', args, { signal: exec.signal });
|
|
399
|
+
return coreImageToolResult(submitted, String(args.prompt || '').trim(), exec.signal);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
|
|
380
403
|
/* ---------- 🎬 视频生成 ---------- */
|
|
381
404
|
safeRegister(host, {
|
|
382
405
|
name: 'iris_generate_video',
|
|
@@ -389,7 +412,7 @@ export async function apply(ctx) {
|
|
|
389
412
|
parameters: {
|
|
390
413
|
type: 'object',
|
|
391
414
|
properties: {
|
|
392
|
-
prompt: { type: 'string', description: '
|
|
415
|
+
prompt: { type: 'string', description: 'Motion/scene description, required for t2v/i2v; optional for s2v' },
|
|
393
416
|
first_frame_attachment_id: { type: 'string', description: 'Attachment id of an image generated by iris_draw_image, used as the first frame' },
|
|
394
417
|
first_frame_path: { type: 'string', description: 'Absolute path to a local image used as the first frame (alternative to attachment id)' },
|
|
395
418
|
audio_path: { type: 'string', description: 'Absolute path to a wav/mp3 file (<15MB, <20s, clear human voice). Required for s2v models like wan2.2-s2v' },
|
|
@@ -398,12 +421,12 @@ export async function apply(ctx) {
|
|
|
398
421
|
duration: { type: 'number', description: 't2v/i2v only: duration in seconds, if the model supports it' },
|
|
399
422
|
model: { type: 'string', description: 'Override with a providerId::modelId reference (legacy bare model id accepted)' }
|
|
400
423
|
},
|
|
401
|
-
required: [
|
|
424
|
+
required: []
|
|
402
425
|
},
|
|
403
426
|
output: { schema: { type: 'string' }, render: (_args, v) => [{ type: 'text', text: v }] },
|
|
404
427
|
isConcurrencySafe: () => false,
|
|
405
428
|
async execute(args, exec) {
|
|
406
|
-
const submitted = await runAction(dshHost(ctx), 'video', args, { signal: exec.signal });
|
|
429
|
+
const submitted = await runAction(dshHost(ctx), 'video', { ...args, session_id: exec.agent?.session?.id }, { signal: exec.signal });
|
|
407
430
|
if (submitted.storage === 'core') {
|
|
408
431
|
let final = await inspectProviderTaskForDsh(submitted.taskId, { signal: exec.signal });
|
|
409
432
|
if (!coreTaskWaitFinished(final)) {
|
|
@@ -572,6 +595,7 @@ export async function apply(ctx) {
|
|
|
572
595
|
`[iris] 视频抽帧完成:${frames.length} 帧(${meta.durationSec.toFixed(1)}s 视频,` +
|
|
573
596
|
`${frames[0].width}x${frames[0].height},${mediaType === 'image/png' ? 'PNG' : 'JPEG'})。\n` +
|
|
574
597
|
`时间戳:${ats}\n` +
|
|
598
|
+
`frame_artifact_ids: ${JSON.stringify(frames.map(f => f.artifact.id))}\n` +
|
|
575
599
|
`attachments: ${refs.map((r) => r.attachmentId).join(', ')}`
|
|
576
600
|
});
|
|
577
601
|
return { blocks, artifactIds: frames.map((frame) => frame.artifact.id) };
|
|
@@ -582,83 +606,91 @@ export async function apply(ctx) {
|
|
|
582
606
|
safeRegister(host, {
|
|
583
607
|
name: 'iris_media_summarize',
|
|
584
608
|
description:
|
|
585
|
-
'Summarize a local video
|
|
609
|
+
'Summarize a local video or existing Core video-frame Artifacts as one contact sheet (with timestamps) plus an optional ' +
|
|
586
610
|
'auto-transcribed audio track, then asking the vision model to describe content. Returns the summary text plus the contact sheet as a DSH image attachment. ' +
|
|
587
|
-
'
|
|
611
|
+
'Provide exactly one of video_path or frame_artifact_ids. Existing frames are read in timestamp order without ffmpeg or the original video. ' +
|
|
612
|
+
'The video_path mode requires ffmpeg + ffprobe and can auto-transcribe the audio track with the configured provider.',
|
|
588
613
|
parameters: {
|
|
589
614
|
type: 'object',
|
|
590
615
|
properties: {
|
|
591
616
|
video_path: { type: 'string', description: 'Absolute path to the local video file (mp4/webm/mov etc.)' },
|
|
617
|
+
frame_artifact_ids: { type: 'array', items: { type: 'string' }, minItems: 1, maxItems: 20, uniqueItems: true,
|
|
618
|
+
description: 'Existing Core video-frame IDs from iris_video_frames; sorted by timestamp. Use instead of video_path, without sampling options or auto-transcription.' },
|
|
592
619
|
question: { type: 'string', description: 'Custom question about the video content (default: summarize scenes/theme)' },
|
|
593
620
|
max_frames: { type: 'integer', description: 'Frames to sample for the contact sheet, clamped to 1..12 (default 8)' },
|
|
594
621
|
target_width: { type: 'integer', description: 'Frame width for sampling (default 640)' },
|
|
595
|
-
transcribe: { type: 'boolean', description: 'Auto-transcribe the audio track
|
|
622
|
+
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.' },
|
|
623
|
+
transcribe_text: { type: 'string', description: 'Optional existing transcript text; does not create a transcription task' },
|
|
596
624
|
model: { type: 'string', description: 'Override the configured vision model id' }
|
|
597
625
|
},
|
|
598
|
-
required: [
|
|
626
|
+
required: []
|
|
599
627
|
},
|
|
600
628
|
output: { schema: { type: 'object' }, render: (_args, value) => value.blocks },
|
|
601
629
|
isConcurrencySafe: () => false,
|
|
602
630
|
async execute(args, exec) {
|
|
603
631
|
if (exec.signal && exec.signal.aborted) throw new Error('已取消');
|
|
632
|
+
const source = normalizeSummarySource(args);
|
|
604
633
|
const p = String(args.video_path || '').trim();
|
|
605
|
-
if (
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
signal: exec.signal
|
|
615
|
-
});
|
|
616
|
-
|
|
617
|
-
// 音轨转写(可选;与独立 transcribe capability 共用同一动作与 failover)
|
|
618
|
-
let transcript = '';
|
|
619
|
-
let transcriptNote = '';
|
|
620
|
-
const wantTranscribe = args.transcribe !== false && meta.hasAudio;
|
|
621
|
-
if (wantTranscribe) {
|
|
622
|
-
if (store.pickAllFor(cap.CAPABILITIES.TRANSCRIBE).length) {
|
|
623
|
-
try {
|
|
624
|
-
const audio = await extractAudioTrack({ inputPath: p, signal: exec.signal });
|
|
625
|
-
try {
|
|
626
|
-
const submitted = await runAction(dshHost(ctx), 'transcribe', { audio_path: audio.filePath }, { signal: exec.signal });
|
|
627
|
-
const final = await awaitTerminal(submitted.taskId, { timeoutMs: 120000, signal: exec.signal });
|
|
628
|
-
if (final && final.status === 'succeeded') transcript = final.transcribeText || '';
|
|
629
|
-
else if (!final) transcriptNote = `\n(音轨转写仍在后台:task ${submitted.taskId},本次摘要未含语音内容)`;
|
|
630
|
-
} finally {
|
|
631
|
-
fs.rmSync(audio.outDir, { recursive: true, force: true });
|
|
632
|
-
}
|
|
633
|
-
} catch (err) {
|
|
634
|
-
transcriptNote = '\n(音轨转写失败,本次摘要仅画面:' + String(err.message || err).slice(0, 160) + ')';
|
|
634
|
+
if (source === 'video') {
|
|
635
|
+
if (!p || !path.isAbsolute(p)) throw new Error('iris: video_path 必须是绝对路径');
|
|
636
|
+
if (!ffmpegAvailable()) throw new Error('iris: ffmpeg 不可用,无法分析视频(可选安装 ffmpeg/ffprobe 后启用)');
|
|
637
|
+
}
|
|
638
|
+
const { answer, via, model, sheet, meta, frames, transcriptNote } = await runSummaryRequest(dshHost(ctx), {
|
|
639
|
+
providers: store.pickAllFor(cap.CAPABILITIES.VISION), onRateLimit: store.recordRateLimit, model: args.model, question: args.question, signal: exec.signal,
|
|
640
|
+
async prepareMedia(signal) {
|
|
641
|
+
if (source === 'core-artifacts') {
|
|
642
|
+
return { ...await summaryFramesForDsh({ frameArtifactIds: args.frame_artifact_ids, signal }), transcript: args.transcribe_text };
|
|
635
643
|
}
|
|
636
|
-
|
|
637
|
-
|
|
644
|
+
const meta = probeVideo(p);
|
|
645
|
+
const frames = await extractFrames({
|
|
646
|
+
inputPath: p, maxFrames: Math.min(Number(args.max_frames) || 8, 12),
|
|
647
|
+
targetWidth: args.target_width, format: 'jpeg', quality: 70, signal
|
|
648
|
+
});
|
|
649
|
+
// 显式选项保留自动转写;消费已完成的 Core Artifact,不重提远端任务。
|
|
650
|
+
let transcript = args.transcribe_text || '', transcriptNote = '';
|
|
651
|
+
if (args.transcribe !== false && !transcript.trim() && meta.hasAudio) {
|
|
652
|
+
if (store.pickAllFor(cap.CAPABILITIES.TRANSCRIBE).length) {
|
|
653
|
+
try {
|
|
654
|
+
const audio = await extractAudioTrack({ inputPath: p, signal });
|
|
655
|
+
try {
|
|
656
|
+
const submitted = await runAction(dshHost(ctx), 'transcribe', { audio_path: audio.filePath }, { signal });
|
|
657
|
+
const final = await awaitCoreTerminal(submitted.taskId, { timeoutMs: 120000, signal });
|
|
658
|
+
if (final?.outcome === 'succeeded' && final.deliveryState === 'ready') {
|
|
659
|
+
const texts = [];
|
|
660
|
+
for (const artifactId of final.artifactIds) {
|
|
661
|
+
const media = await readCoreArtifactMediaForDsh(artifactId, { signal });
|
|
662
|
+
texts.push(Buffer.from(media.bytes).toString('utf8'));
|
|
663
|
+
}
|
|
664
|
+
transcript = texts.join('\n');
|
|
665
|
+
} else transcriptNote = '\n(音轨转写未完成,本次摘要仅画面;可用 iris_task_status 查询 Core task)';
|
|
666
|
+
} finally {
|
|
667
|
+
fs.rmSync(audio.outDir, { recursive: true, force: true });
|
|
668
|
+
}
|
|
669
|
+
} catch (err) {
|
|
670
|
+
if (signal.aborted) throw signal.reason;
|
|
671
|
+
transcriptNote = '\n(音轨转写失败,本次摘要仅画面;可用 iris_task_status 查询转写任务)';
|
|
672
|
+
}
|
|
673
|
+
} else transcriptNote = '\n(视频含音轨但未配置转写供应商,本次摘要仅画面)';
|
|
674
|
+
}
|
|
675
|
+
return { meta, frames, transcript, transcriptNote };
|
|
638
676
|
}
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
const sheet = await buildContactSheet({ frames, cols: 3 });
|
|
677
|
+
});
|
|
678
|
+
// 成功摘要的同一张拼图作为显式工具产物发布。
|
|
679
|
+
if (exec.signal?.aborted) throw exec.signal.reason;
|
|
643
680
|
const ref = await attachmentService(host).saveImage({
|
|
644
681
|
data: new Uint8Array(sheet.buffer),
|
|
645
682
|
mediaType: 'image/png',
|
|
646
683
|
name: `iris-sheet-${Date.now()}.png`
|
|
647
684
|
});
|
|
648
|
-
|
|
649
|
-
const backends = buildVisionBackendsFromHost(dshHost(ctx), { providers: visionProviders, model: args.model });
|
|
650
|
-
const { answer, via, model } = await summarizeMedia({
|
|
651
|
-
backends, question: args.question, transcript,
|
|
652
|
-
imageDataUrl: `data:image/png;base64,${sheet.buffer.toString('base64')}`,
|
|
653
|
-
ref, signal: exec.signal
|
|
654
|
-
});
|
|
685
|
+
if (exec.signal?.aborted) throw exec.signal.reason;
|
|
655
686
|
const viaLabel = via === 'selfstack' ? 'iris 自持栈' : 'DSH 全局视觉模型';
|
|
656
687
|
return {
|
|
688
|
+
frames: summaryFrameRecords(frames),
|
|
657
689
|
blocks: [
|
|
658
690
|
{
|
|
659
691
|
type: 'text',
|
|
660
692
|
text:
|
|
661
|
-
`[iris] 视频摘要完成(${model} · ${viaLabel},${meta
|
|
693
|
+
`[iris] 视频摘要完成(${model} · ${viaLabel},${summaryTimelineLabel(meta)} / ${frames.length} 帧联系表):\n` +
|
|
662
694
|
`${answer}${transcriptNote || ''}\n` +
|
|
663
695
|
`contact sheet attachment: ${ref.attachmentId}`
|
|
664
696
|
},
|
|
@@ -673,30 +705,39 @@ export async function apply(ctx) {
|
|
|
673
705
|
safeRegister(host, {
|
|
674
706
|
name: 'iris_look_at_image',
|
|
675
707
|
description:
|
|
676
|
-
'Look at a local image file and answer one question
|
|
708
|
+
'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.',
|
|
677
709
|
parameters: {
|
|
678
710
|
type: 'object',
|
|
679
711
|
properties: {
|
|
680
712
|
image_path: { type: 'string', description: 'Absolute path to the image file (png/jpg/jpeg/webp)' },
|
|
713
|
+
artifact_id: { type: 'string', description: 'Existing Core image Artifact ID in the current profile (alternative to image_path)' },
|
|
681
714
|
question: { type: 'string', description: 'Question or extraction request about the image; defaults to a detailed description' },
|
|
682
715
|
model: { type: 'string', description: 'Override the configured vision model id (default qwen-vl-plus)' }
|
|
683
716
|
},
|
|
684
|
-
required: [
|
|
717
|
+
required: []
|
|
685
718
|
},
|
|
686
719
|
output: { schema: { type: 'string' }, render: (_args, v) => [{ type: 'text', text: v }] },
|
|
687
720
|
isConcurrencySafe: () => true,
|
|
688
721
|
async execute(args, exec) {
|
|
722
|
+
const source = normalizeVisionImageSource(args);
|
|
689
723
|
const p = String(args.image_path || '').trim();
|
|
690
|
-
if (!path.isAbsolute(p)) throw new Error('iris: image_path 必须是绝对路径');
|
|
691
724
|
const mt = MEDIA_TYPES[path.extname(p).toLowerCase()];
|
|
692
|
-
if (
|
|
693
|
-
|
|
725
|
+
if (source === 'image_path') {
|
|
726
|
+
if (!path.isAbsolute(p)) throw new Error('iris: image_path 必须是绝对路径');
|
|
727
|
+
if (!mt || mt === 'image/gif') throw new Error('iris: 不支持的图片格式(png/jpg/jpeg/webp):' + p);
|
|
728
|
+
if (!fs.existsSync(p)) throw new Error('iris: 图片不存在:' + p);
|
|
729
|
+
}
|
|
694
730
|
const question = String(args.question || '').trim() || '详细描述这张图片。';
|
|
695
731
|
if (exec.signal && exec.signal.aborted) throw new Error('已取消');
|
|
696
732
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
733
|
+
return runVisionTool(dshHost(ctx), exec, { origin: 'tool', model: args.model, question, artifactId: args.artifact_id,
|
|
734
|
+
async prepareImage(signal) {
|
|
735
|
+
if (source === 'artifact_id') return visionImageForDsh({ artifactId: args.artifact_id, signal });
|
|
736
|
+
const image = await readVisionImageFile(p, mt, signal);
|
|
737
|
+
await waitForModelWork(() => attachmentService(host).saveImage({ data: image.bytes, mediaType: mt, name: path.basename(p) }), signal);
|
|
738
|
+
return image;
|
|
739
|
+
}
|
|
740
|
+
});
|
|
700
741
|
}
|
|
701
742
|
});
|
|
702
743
|
|
|
@@ -722,18 +763,16 @@ export async function apply(ctx) {
|
|
|
722
763
|
if (!question) throw new Error('iris: question 不能为空');
|
|
723
764
|
if (exec.signal && exec.signal.aborted) throw new Error('已取消');
|
|
724
765
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
}
|
|
736
|
-
return runVisionTool(dshHost(ctx), exec, { origin: 'relook', model: args.model, question, ref: hit.ref, dataUrl });
|
|
766
|
+
return runVisionTool(dshHost(ctx), exec, { origin: 'relook', model: args.model, question,
|
|
767
|
+
async prepareImage(signal) {
|
|
768
|
+
// 会话扫描、解引用与读图都属于这次操作的总预算。
|
|
769
|
+
const hit = (await waitForModelWork(() => sessionAttachmentRef(host, exec, id), signal)) || findOwnAttachment(id);
|
|
770
|
+
if (!hit) throw new Error('iris: 图片引用不可用');
|
|
771
|
+
if (hit.absPath) return readVisionImageFile(hit.absPath, hit.ref.mediaType || MEDIA_TYPES[path.extname(hit.absPath).toLowerCase()] || 'image/png', signal);
|
|
772
|
+
const stored = await waitForModelWork(() => attachmentService(host).readImage(hit.ref, signal), signal);
|
|
773
|
+
return { bytes: new Uint8Array(stored.data), mediaType: stored.mediaType || hit.ref.mediaType };
|
|
774
|
+
}
|
|
775
|
+
});
|
|
737
776
|
}
|
|
738
777
|
});
|
|
739
778
|
|
|
@@ -866,13 +905,14 @@ export async function apply(ctx) {
|
|
|
866
905
|
safeRegister(host, {
|
|
867
906
|
name: 'iris_locate',
|
|
868
907
|
description:
|
|
869
|
-
'Locate an object/region described by `target` in an image (absolute
|
|
908
|
+
'Locate an object/region described by `target` in an image (exactly one of absolute image_path, Core artifact_id, or session attachment_id), ' +
|
|
870
909
|
'returning the pixel bounding box (x1,y1,x2,y2) in original image coordinates. ' +
|
|
871
|
-
'Use the
|
|
910
|
+
'Use the bbox with iris_crop for path/session images or CLI run crop with artifact_id for Core images; do not treat a Core ID as a file path.',
|
|
872
911
|
parameters: {
|
|
873
912
|
type: 'object',
|
|
874
913
|
properties: {
|
|
875
914
|
image_path: { type: 'string', description: 'Absolute path to the source image (png/jpg/jpeg/webp)' },
|
|
915
|
+
artifact_id: { type: 'string', description: 'Existing Core image Artifact ID in the current profile (alternative to image_path or attachment_id)' },
|
|
876
916
|
attachment_id: { type: 'string', description: 'Session attachment id of an image (alternative to image_path)' },
|
|
877
917
|
target: { type: 'string', description: 'The object or UI element to locate, e.g. "send button"' },
|
|
878
918
|
model: { type: 'string', description: 'Override the vision model for this locate call (e.g. qwen3-vl-235b-a22b-thinking for higher grounding precision; default is the provider visionModel, typically qwen-vl-plus)' }
|
|
@@ -882,23 +922,15 @@ export async function apply(ctx) {
|
|
|
882
922
|
output: { schema: { type: 'string' }, render: (_args, v) => [{ type: 'text', text: v }] },
|
|
883
923
|
isConcurrencySafe: () => true,
|
|
884
924
|
async execute(args, exec) {
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
attachment_id: args.attachment_id
|
|
925
|
+
const r = await runLocateRequest(dshHost(ctx), {
|
|
926
|
+
providers: store.pickAllFor(cap.CAPABILITIES.VISION), onRateLimit: store.recordRateLimit, model: args.model, target: args.target,
|
|
927
|
+
prepareImage: signal => resolveVisionImageInput(dshHost(ctx), exec, args, signal), signal: exec.signal
|
|
889
928
|
});
|
|
890
|
-
const { width, height } =
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
target: args.target,
|
|
896
|
-
imageDataUrl: dataUrl,
|
|
897
|
-
width,
|
|
898
|
-
height,
|
|
899
|
-
signal: exec.signal
|
|
900
|
-
});
|
|
901
|
-
if (!r.found) return `[iris] 在图片中未找到「${args.target}」(${r.via} · ${r.model})`;
|
|
929
|
+
const { width, height } = r;
|
|
930
|
+
const sourceNote = args.artifact_id ? `\nCore Artifact: ${args.artifact_id}` : '';
|
|
931
|
+
if (!r.found) return `[iris] 在图片中未找到「${args.target}」(${r.via} · ${r.model})${sourceNote}`;
|
|
932
|
+
if (args.artifact_id) return `[iris] 定位「${args.target}」:bbox (${r.x1},${r.y1},${r.x2},${r.y2}) / ${width}x${height}(${r.via} · ${r.model})${sourceNote}\n` +
|
|
933
|
+
`裁剪区域:left=${r.x1}, top=${r.y1}, width=${r.x2 - r.x1}, height=${r.y2 - r.y1}`;
|
|
902
934
|
return `[iris] 定位「${args.target}」:bbox (${r.x1},${r.y1},${r.x2},${r.y2}) / ${width}x${height}(${r.via} · ${r.model})\n` +
|
|
903
935
|
`裁剪指令:iris_crop(image_path="${args.image_path || args.attachment_id}", left=${r.x1}, top=${r.y1}, width=${r.x2 - r.x1}, height=${r.y2 - r.y1})`;
|
|
904
936
|
}
|
|
@@ -928,7 +960,8 @@ export async function apply(ctx) {
|
|
|
928
960
|
async execute(args, exec) {
|
|
929
961
|
if (exec.signal && exec.signal.aborted) throw new Error('已取消');
|
|
930
962
|
const browser = requireHostPort(dshHost(ctx), 'browser', 'HTML 截图');
|
|
931
|
-
const rendered = await
|
|
963
|
+
const rendered = await htmlForDsh({
|
|
964
|
+
browser,
|
|
932
965
|
html: args.html,
|
|
933
966
|
width: args.width,
|
|
934
967
|
height: args.height,
|
|
@@ -947,7 +980,7 @@ export async function apply(ctx) {
|
|
|
947
980
|
{
|
|
948
981
|
type: 'text',
|
|
949
982
|
text: `[iris] HTML 截图完成(${outW}x${outH},fullPage: ${args.fullPage !== false})\n` +
|
|
950
|
-
`
|
|
983
|
+
`artifact: ${rendered.artifact.id}\nattachment: ${ref.attachmentId}` +
|
|
951
984
|
`\n注意:渲染在共享浏览器临时标签页中完成,页面短暂可见属正常行为。`
|
|
952
985
|
},
|
|
953
986
|
{ type: 'image', attachment: ref }
|
|
@@ -960,13 +993,14 @@ export async function apply(ctx) {
|
|
|
960
993
|
safeRegister(host, {
|
|
961
994
|
name: 'iris_long_ocr',
|
|
962
995
|
description:
|
|
963
|
-
'OCR a (possibly long) image: slice it into chunks (1200px tall with 120px overlap by default) ' +
|
|
996
|
+
'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) ' +
|
|
964
997
|
'and read the text chunk by chunk with the vision model, then join the results in top-to-bottom order. ' +
|
|
965
|
-
'
|
|
998
|
+
'Marks partial results when chunks fail. Cancellation or timeout stops the whole operation. Good for long screenshots / documents.',
|
|
966
999
|
parameters: {
|
|
967
1000
|
type: 'object',
|
|
968
1001
|
properties: {
|
|
969
1002
|
image_path: { type: 'string', description: 'Absolute path to the image (png/jpg/jpeg/webp)' },
|
|
1003
|
+
artifact_id: { type: 'string', description: 'Existing Core image Artifact ID in the current profile (alternative to image_path or attachment_id)' },
|
|
970
1004
|
attachment_id: { type: 'string', description: 'Session attachment id of the image (alternative to image_path)' },
|
|
971
1005
|
chunk_height: { type: 'integer', description: 'Chunk height in pixels, default 1200' },
|
|
972
1006
|
overlap: { type: 'integer', description: 'Overlap between chunks in pixels to avoid cutting text lines, default 120' }
|
|
@@ -976,28 +1010,19 @@ export async function apply(ctx) {
|
|
|
976
1010
|
output: { schema: { type: 'string' }, render: (_args, v) => [{ type: 'text', text: v }] },
|
|
977
1011
|
isConcurrencySafe: () => true,
|
|
978
1012
|
async execute(args, exec) {
|
|
979
|
-
|
|
980
|
-
const
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
const backends = buildVisionBackendsFromHost(dshHost(ctx), { providers: visionProviders });
|
|
986
|
-
const result = await longOcr({
|
|
987
|
-
input: input.buffer,
|
|
988
|
-
backends,
|
|
989
|
-
chunkHeight: args.chunk_height || 1200,
|
|
990
|
-
overlap: args.overlap || 120,
|
|
1013
|
+
const host = dshHost(ctx);
|
|
1014
|
+
const result = await runOcrRequest(host, {
|
|
1015
|
+
providers: store.pickAllFor(cap.CAPABILITIES.VISION), onRateLimit: store.recordRateLimit,
|
|
1016
|
+
prepareImage: operationSignal => resolveVisionImageInput(host, exec, args, operationSignal),
|
|
1017
|
+
chunkHeight: args.chunk_height,
|
|
1018
|
+
overlap: args.overlap,
|
|
991
1019
|
signal: exec.signal
|
|
992
1020
|
});
|
|
993
|
-
|
|
994
|
-
const head = `[iris] 长截图 OCR 完成:${result.width}x${result.height}px,${result.totalChunks} 块${errCount ? '(' + errCount + ' 块失败)' : ''}\n`;
|
|
995
|
-
if (!result.fullText) return head + '(未识别到文字)';
|
|
996
|
-
return head + result.fullText;
|
|
1021
|
+
return formatOcrResult(result) + (args.artifact_id ? `\nCore Artifact: ${args.artifact_id}` : '');
|
|
997
1022
|
}
|
|
998
1023
|
});
|
|
999
1024
|
|
|
1000
|
-
console.log('[iris] tools registered: iris_draw_image, iris_generate_video, iris_speak_text, iris_transcribe_audio, iris_task_status, iris_look_at_image, iris_relook_attachment, iris_crop, iris_pixel_diff, iris_locate, iris_html_screenshot, iris_long_ocr, iris_video_frames, iris_media_summarize');
|
|
1025
|
+
console.log('[iris] tools registered: iris_draw_image, iris_edit_image, iris_generate_video, iris_speak_text, iris_transcribe_audio, iris_task_status, iris_look_at_image, iris_relook_attachment, iris_crop, iris_pixel_diff, iris_locate, iris_html_screenshot, iris_long_ocr, iris_video_frames, iris_media_summarize');
|
|
1001
1026
|
};
|
|
1002
1027
|
|
|
1003
1028
|
/* ---------------- 视觉路由(M3 补全):look/relook 共用执行器 + 降级链 ---------------- */
|
|
@@ -1046,12 +1071,35 @@ async function resolveImageInput(host, exec, { image_path, attachment_id }) {
|
|
|
1046
1071
|
throw new Error('iris: 需要 image_path 或 attachment_id');
|
|
1047
1072
|
}
|
|
1048
1073
|
|
|
1074
|
+
/** 视觉解引用在整体预算内;路径限额读,Host 会话/附件等待可取消。 */
|
|
1075
|
+
async function resolveVisionImageInput(host, exec, input, signal) {
|
|
1076
|
+
const source = normalizeVisionImageSource(input, { allowAttachment: true });
|
|
1077
|
+
const { image_path, artifact_id, attachment_id } = input;
|
|
1078
|
+
if (source === 'artifact_id') return visionImageForDsh({ artifactId: artifact_id, signal });
|
|
1079
|
+
if (image_path) {
|
|
1080
|
+
const file = String(image_path).trim();
|
|
1081
|
+
const mediaType = MEDIA_TYPES[path.extname(file).toLowerCase()];
|
|
1082
|
+
if (!path.isAbsolute(file) || !mediaType || mediaType === 'image/gif') throw new Error('invalid vision image');
|
|
1083
|
+
return readVisionImageFile(file, mediaType, signal);
|
|
1084
|
+
}
|
|
1085
|
+
if (attachment_id) {
|
|
1086
|
+
const id = String(attachment_id).trim();
|
|
1087
|
+
const hit = await waitForModelWork(() => sessionAttachmentRef(host, exec, id), signal) || findOwnAttachment(id);
|
|
1088
|
+
if (!hit) throw new Error('unavailable vision attachment');
|
|
1089
|
+
if (hit.absPath) return readVisionImageFile(hit.absPath,
|
|
1090
|
+
hit.ref.mediaType || MEDIA_TYPES[path.extname(hit.absPath).toLowerCase()] || 'image/png', signal);
|
|
1091
|
+
const stored = await waitForModelWork(() => attachmentService(host).readImage(hit.ref, signal), signal);
|
|
1092
|
+
return { bytes: stored.data, mediaType: stored.mediaType || hit.ref.mediaType || 'image/png' };
|
|
1093
|
+
}
|
|
1094
|
+
throw new Error('missing vision image');
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1049
1097
|
/** 在本插件的任务产物里按 attachmentId 找 iris 自生成的图片(relook 的兜底) */
|
|
1050
1098
|
export function findOwnAttachment(attachmentId) {
|
|
1051
1099
|
for (const t of tasks.all()) {
|
|
1052
1100
|
for (const a of t.attachments || []) {
|
|
1053
1101
|
if (a.attachmentId === attachmentId) {
|
|
1054
|
-
const absPath = path.join(
|
|
1102
|
+
const absPath = path.join(tasks.outputsDir(), a.file);
|
|
1055
1103
|
if (fs.existsSync(absPath)) {
|
|
1056
1104
|
return { ref: { attachmentId: a.attachmentId, mediaType: a.mediaType }, absPath };
|
|
1057
1105
|
}
|
|
@@ -1087,44 +1135,26 @@ export async function sessionAttachmentRef(host, exec, attachmentId) {
|
|
|
1087
1135
|
* 全局视觉模型兜底;每个后端的失败现场保留在 errors 里。
|
|
1088
1136
|
* @returns {{answer:string,via:'selfstack'|'global',model?:string,backendId:string}}
|
|
1089
1137
|
*/
|
|
1090
|
-
export async function askVision(host, { question,
|
|
1138
|
+
export async function askVision(host, { question, dataUrl, image, prepareImage, signal, model, budget }) {
|
|
1091
1139
|
const visionProviders = store.pickAllFor(cap.CAPABILITIES.VISION);
|
|
1092
|
-
|
|
1093
|
-
return askWithBackends(backends, { question, ref, imageDataUrl: dataUrl, signal });
|
|
1140
|
+
return runVisionRequest(host, { providers: visionProviders, model, question, dataUrl, image, prepareImage, signal, budget, onRateLimit: store.recordRateLimit });
|
|
1094
1141
|
}
|
|
1095
1142
|
|
|
1096
1143
|
/**
|
|
1097
1144
|
* look / relook 共用执行器:看图问答 → 人话结果(标注模型归属,方便排查走哪条链)。
|
|
1098
1145
|
* @returns {Promise<string>} 直接作为工具输出
|
|
1099
1146
|
*/
|
|
1100
|
-
export async function runVisionTool(host, exec, { origin, model, question,
|
|
1147
|
+
export async function runVisionTool(host, exec, { origin, model, question, dataUrl, image, prepareImage, artifactId }) {
|
|
1101
1148
|
if (exec && exec.signal && exec.signal.aborted) throw new Error('已取消');
|
|
1102
1149
|
const { answer, via, model: usedModel } = await askVision(host, {
|
|
1103
1150
|
question,
|
|
1104
|
-
ref,
|
|
1105
1151
|
dataUrl,
|
|
1152
|
+
image,
|
|
1153
|
+
prepareImage,
|
|
1106
1154
|
signal: exec && exec.signal,
|
|
1107
1155
|
model
|
|
1108
1156
|
});
|
|
1109
1157
|
const label = origin === 'relook' ? '重看回答' : '看图回答';
|
|
1110
1158
|
const viaLabel = via === 'selfstack' ? 'iris 自持栈' : 'DSH 全局视觉模型';
|
|
1111
|
-
return `[iris] ${label}(${usedModel || '默认'} · ${viaLabel}):\n${answer}
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
/* ---------------- 视觉自述(画图后增强项):走 askVision 降级链,失败静默 ---------------- */
|
|
1115
|
-
async function describeFirstImage(host, ref, absPath, originalPrompt) {
|
|
1116
|
-
try {
|
|
1117
|
-
const mt = MEDIA_TYPES[path.extname(absPath).toLowerCase()] || 'image/png';
|
|
1118
|
-
return await describeImageData(host, ref, toDataUrl(absPath, mt), originalPrompt);
|
|
1119
|
-
} catch (_) {
|
|
1120
|
-
return ''; // 自述是增强项,失败不影响交付
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
async function describeImageData(host, ref, dataUrl, originalPrompt) {
|
|
1125
|
-
const { answer } = await askVision(host, {
|
|
1126
|
-
question: `用不超过两句话描述这张图片的主题与构图。生成该图的提示词是:「${String(originalPrompt).slice(0, 200)}」`,
|
|
1127
|
-
ref, dataUrl, signal: undefined
|
|
1128
|
-
});
|
|
1129
|
-
return answer.slice(0, 300);
|
|
1159
|
+
return `[iris] ${label}(${usedModel || '默认'} · ${viaLabel}):\n${answer}` + (artifactId ? `\nCore Artifact: ${artifactId}` : '');
|
|
1130
1160
|
}
|