@mokuyoaxis/dsh-iris 0.2.6 → 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/CHANGELOG.md +23 -1
- package/README.en.md +28 -19
- package/README.md +27 -18
- package/bin/dsh-iris.js +140 -26
- package/docs/AGENT_QUICKSTART.md +5 -5
- package/docs/ARCHITECTURE.md +9 -3
- package/docs/CLI_MANAGEMENT.md +1 -1
- package/docs/CORE_RUNTIME_CONTRACT.md +36 -5
- package/docs/CORE_SKILLS.md +62 -0
- package/docs/DSH_COMPATIBILITY.md +4 -4
- package/docs/DSH_CORE_MIGRATION.md +4 -2
- package/docs/DSH_PROVIDER_CONNECTIONS.md +1 -1
- package/docs/DSH_RUNTIME.md +36 -0
- package/docs/FUNCTION_MODEL_POOLS.md +1 -1
- package/docs/HEADLESS_CLI.md +11 -3
- package/docs/HOST_DOCTOR.md +13 -1
- 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/PROVIDER_HEALTH.md +1 -1
- package/docs/ROADMAP.md +14 -2
- package/docs/SECURITY.md +9 -1
- package/docs/SYNC_DELIVERY_RECOVERY.md +2 -0
- package/docs/VISION_CLI.md +1 -1
- package/docs/WEB_PROTOTYPE.md +47 -0
- package/docs/releases/0.2.7.md +62 -0
- package/lib/actions.js +79 -68
- package/lib/api.js +38 -38
- package/lib/application-runtime.js +186 -0
- package/lib/client.js +89 -3
- 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 +12 -1
- package/lib/dsh-core-adapter.js +666 -572
- package/lib/dsh-host-adapter.js +4 -1
- package/lib/flow-store.js +195 -0
- package/lib/image-flow.js +188 -0
- package/lib/index.js +59 -53
- package/lib/local-api-projection.js +27 -0
- package/lib/local-api.js +381 -0
- package/lib/local-creation.js +85 -0
- package/lib/private-storage.js +12 -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/workbench-artifacts.js +1 -2
- package/lib/workbench-management.js +8 -9
- package/package.json +3 -2
- 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 +14 -8
package/lib/actions.js
CHANGED
|
@@ -7,7 +7,7 @@ import { visionInputLimits } from './vision-image-input.js';
|
|
|
7
7
|
* ① `POST /iris/api/actions/:name` 路由(GUI 直连,submit-only 不等待)
|
|
8
8
|
* ② 工具 execute 体(Agent 调用,可等待)
|
|
9
9
|
*
|
|
10
|
-
* 每个 action
|
|
10
|
+
* 每个 action 接收 `(host, args, { signal, core })`,显式使用所属实例,
|
|
11
11
|
* 结果只有可序列化 JSON(无 live 对象)。
|
|
12
12
|
*/
|
|
13
13
|
import fs from 'node:fs';
|
|
@@ -23,7 +23,6 @@ import { registerMedia, mediaLinksOf, webBase } from './media.js';
|
|
|
23
23
|
import { RED_TEST_IMAGE } from './vision.js';
|
|
24
24
|
import { runVisionRequest, readVisionImageFile, probeVisionModel, visionImageFromDataUrl } from './vision-model-routing.js';
|
|
25
25
|
import { HOST_CONTRACT_VERSION, HOST_PORTS, defineHostAdapter, hasHostPort, requireHostPort } from './host-contract.js';
|
|
26
|
-
import { coreSnapshotForDsh, importImageForDsh, cropForDsh, diffForDsh, framesForDsh, summaryFramesForDsh, visionImageForDsh, htmlForDsh, inspectProviderTaskForDsh, submitMusicForDsh, submitProviderTaskForDsh, watchProviderTaskForDsh } from './dsh-core-adapter.js';
|
|
27
26
|
import { runLocateRequest, runSummaryRequest } from './composite-vision-routing.js';
|
|
28
27
|
import { normalizeSummarySource, summaryFrameRecords, summaryTimelineLabel } from './summary-input.js';
|
|
29
28
|
import { normalizeVisionImageSource } from './vision-input.js';
|
|
@@ -40,12 +39,14 @@ import { normalizeGenerationInput } from './generation-input.js';
|
|
|
40
39
|
import { videoTaskCandidates } from './video-input.js';
|
|
41
40
|
import { normalizeDshProvider, withDshProviderConfig } from './dsh-provider-config.js';
|
|
42
41
|
import { modelDirectorySummary, modelUnavailableReason } from './model-directory.js';
|
|
42
|
+
import { ModelPortError } from './model-port-contract.js';
|
|
43
43
|
|
|
44
44
|
const PROVIDER_ACTIONS = new Set(['image', 'image_edit', 'video', 'music', 'tts', 'transcribe', 'look', 'relook',
|
|
45
45
|
'locate', 'ocr', 'media_summarize', 'task_reobserve', 'task_redeliver', 'task_manual_retry',
|
|
46
46
|
'providers_discover', 'providers_sync_directory', 'providers_test_vision', 'providers_test_model']);
|
|
47
47
|
const AUTO_DIRECTORY_ACTIONS = new Set(['image', 'image_edit', 'video', 'music', 'tts', 'transcribe', 'look', 'relook',
|
|
48
48
|
'locate', 'ocr', 'media_summarize']);
|
|
49
|
+
const VISION_ACTIONS = new Set(['look', 'relook', 'locate', 'ocr', 'media_summarize']);
|
|
49
50
|
|
|
50
51
|
const MEDIA_TYPES = {
|
|
51
52
|
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
@@ -242,8 +243,8 @@ function recordCoreTaskAttemptHealth(task, capability) {
|
|
|
242
243
|
}
|
|
243
244
|
|
|
244
245
|
/** DSH 图片入口:同步和异步候选共用一份 Core Task/Attempt/Artifact 事实。 */
|
|
245
|
-
async function submitCoreImage(routes, providerInput, signal, sourceArtifactId) {
|
|
246
|
-
const submitted = await submitProviderTaskForDsh({
|
|
246
|
+
async function submitCoreImage(core, routes, providerInput, signal, sourceArtifactId) {
|
|
247
|
+
const submitted = await core.submitProviderTaskForDsh({
|
|
247
248
|
capability: 'image',
|
|
248
249
|
candidates: routes.map((route) => ({
|
|
249
250
|
adapter: route.adapter,
|
|
@@ -267,7 +268,7 @@ async function submitCoreImage(routes, providerInput, signal, sourceArtifactId)
|
|
|
267
268
|
if (!selected) throw taskActionError(task.id, 'Core Task 的供应商身份无法映射回当前路由', task);
|
|
268
269
|
if (task.acceptance === 'accepted' && task.remoteTaskId
|
|
269
270
|
&& ['none', 'unknown'].includes(task.outcome) && task.phase !== 'terminal') {
|
|
270
|
-
await watchProviderTaskForDsh({ taskId: task.id, signal });
|
|
271
|
+
await core.watchProviderTaskForDsh({ taskId: task.id, signal });
|
|
271
272
|
} else if (task.outcome !== 'succeeded' || task.deliveryState !== 'ready' || !task.artifactIds.length) {
|
|
272
273
|
throw taskActionError(task.id, '图片路由没有得到可观察任务或可交付产物;事实已保留在 Core,未回退重提', task);
|
|
273
274
|
}
|
|
@@ -279,7 +280,7 @@ async function submitCoreImage(routes, providerInput, signal, sourceArtifactId)
|
|
|
279
280
|
* 不共用图片状态机——视频走自己的交付 Profile(video/mp4 / generated-video)。
|
|
280
281
|
* s2v 在写前 Attempt 之后按账号上传首帧和音频,交付复用视频 Profile。
|
|
281
282
|
*/
|
|
282
|
-
async function submitCoreVideo(routes, frame, prompt, args, signal) {
|
|
283
|
+
async function submitCoreVideo(core, routes, frame, prompt, args, signal) {
|
|
283
284
|
const normalized = normalizeGenerationInput('video', {
|
|
284
285
|
...(prompt ? { prompt } : {}),
|
|
285
286
|
...(frame ? { first_frame_path: frame.absPath } : {}),
|
|
@@ -287,7 +288,7 @@ async function submitCoreVideo(routes, frame, prompt, args, signal) {
|
|
|
287
288
|
size: optionalActionText(args.size),
|
|
288
289
|
duration: args.duration
|
|
289
290
|
}, { allowVideoPaths: true });
|
|
290
|
-
const submitted = await submitProviderTaskForDsh({
|
|
291
|
+
const submitted = await core.submitProviderTaskForDsh({
|
|
291
292
|
capability: 'video',
|
|
292
293
|
candidates: videoTaskCandidates(routes.map((route) => ({
|
|
293
294
|
adapter: route.adapter,
|
|
@@ -310,7 +311,7 @@ async function submitCoreVideo(routes, frame, prompt, args, signal) {
|
|
|
310
311
|
if (!selected) throw taskActionError(task.id, 'Core Task 的供应商身份无法映射回当前路由', task);
|
|
311
312
|
if (task.acceptance === 'accepted' && task.remoteTaskId
|
|
312
313
|
&& ['none', 'unknown'].includes(task.outcome) && task.phase !== 'terminal') {
|
|
313
|
-
await watchProviderTaskForDsh({ taskId: task.id, signal });
|
|
314
|
+
await core.watchProviderTaskForDsh({ taskId: task.id, signal });
|
|
314
315
|
} else if (task.outcome !== 'succeeded' || task.deliveryState !== 'ready' || !task.artifactIds.length) {
|
|
315
316
|
throw taskActionError(task.id, '视频路由没有得到可观察任务或可交付产物;事实已保留在 Core,未回退重提', task);
|
|
316
317
|
}
|
|
@@ -472,24 +473,34 @@ export function listActions() {
|
|
|
472
473
|
}
|
|
473
474
|
|
|
474
475
|
/** 执行一个动作 */
|
|
475
|
-
export async function runAction(host, name, args, { signal } = {}) {
|
|
476
|
+
export async function runAction(host, name, args, { signal, core } = {}) {
|
|
476
477
|
const fn = actions[name];
|
|
477
|
-
if (!fn) throw new Error(
|
|
478
|
+
if (!fn) throw new Error('iris: 未知动作 "' + name + '"');
|
|
478
479
|
const adapter = actionHost(host);
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
480
|
+
const owned = structuredClone(args || {});
|
|
481
|
+
return core.runHostOperation(operationSignal => {
|
|
482
|
+
const execute = async () => {
|
|
483
|
+
if (AUTO_DIRECTORY_ACTIONS.has(name) && !owned.model) await store.refreshAutomaticDirectories({
|
|
484
|
+
capability: ({ image: 'image-gen', image_edit: 'image-gen', video: 'video-gen', music: 'music-gen', tts: 'tts', transcribe: 'transcribe' })[name] || 'vision', signal: operationSignal
|
|
485
|
+
});
|
|
486
|
+
operationSignal.throwIfAborted();
|
|
487
|
+
return fn(adapter, owned, { signal: operationSignal, core });
|
|
488
|
+
};
|
|
489
|
+
return PROVIDER_ACTIONS.has(name) ? withDshProviderConfig(adapter, execute, { signal: operationSignal }) : execute();
|
|
490
|
+
}, { signal }).catch(error => {
|
|
491
|
+
// Ownership can stop the request before it enters the Model Port's error boundary.
|
|
492
|
+
if (VISION_ACTIONS.has(name) && signal?.aborted && error === signal.reason) {
|
|
493
|
+
throw new ModelPortError('IRIS_MODEL_ABORTED');
|
|
494
|
+
}
|
|
495
|
+
throw error;
|
|
496
|
+
});
|
|
486
497
|
}
|
|
487
498
|
|
|
488
499
|
/* ============ 动作实现 ============ */
|
|
489
500
|
|
|
490
|
-
register('image_import', async (_host, args, { signal }) => {
|
|
501
|
+
register('image_import', async (_host, args, { signal, core }) => {
|
|
491
502
|
if (args.image_path !== undefined && args.image_bytes !== undefined) throw new Error('图片导入只能选择一个来源');
|
|
492
|
-
const { artifact } = await importImageForDsh({ imagePath: args.image_path, imageBytes: args.image_bytes, signal });
|
|
503
|
+
const { artifact } = await core.importImageForDsh({ imagePath: args.image_path, imageBytes: args.image_bytes, signal });
|
|
493
504
|
const imageUrl = '/iris/api/core/artifact/' + artifact.id + '/media';
|
|
494
505
|
const text = `已导入图片(${artifact.metadata.width}×${artifact.metadata.height}),原文件保留。\n作品ID:${artifact.id}`;
|
|
495
506
|
return { ok: true, storage: 'core', artifact, artifactId: artifact.id, artifactIds: [artifact.id], imageUrl, text,
|
|
@@ -497,7 +508,7 @@ register('image_import', async (_host, args, { signal }) => {
|
|
|
497
508
|
});
|
|
498
509
|
|
|
499
510
|
/* ---------- 🖼️ 画图 ---------- */
|
|
500
|
-
async function imageAction(host, args, { signal }, editing = false) {
|
|
511
|
+
async function imageAction(host, args, { signal, core }, editing = false) {
|
|
501
512
|
if (editing && !args.source_artifact_id) throw new Error('iris: 改图必须提供 source_artifact_id');
|
|
502
513
|
const { providerInput, sourceArtifactId } = normalizeGenerationInput('image', {
|
|
503
514
|
prompt: args.prompt, size: optionalActionText(args.size), n: args.n,
|
|
@@ -506,7 +517,7 @@ async function imageAction(host, args, { signal }, editing = false) {
|
|
|
506
517
|
|
|
507
518
|
const imageProviders = selectProviders(cap.CAPABILITIES.IMAGE, '图像', args.model);
|
|
508
519
|
const routes = imageProviders.map((provider) => imageRoute(provider, args));
|
|
509
|
-
const submitted = await submitCoreImage(routes, providerInput, signal, sourceArtifactId);
|
|
520
|
+
const submitted = await submitCoreImage(core, routes, providerInput, signal, sourceArtifactId);
|
|
510
521
|
const firstArtifactId = submitted.task.artifactIds[0];
|
|
511
522
|
const ready = submitted.task.outcome === 'succeeded'
|
|
512
523
|
&& submitted.task.deliveryState === 'ready' && Boolean(firstArtifactId);
|
|
@@ -525,7 +536,7 @@ register('image', imageAction);
|
|
|
525
536
|
register('image_edit', (host, args, context) => imageAction(host, args, context, true));
|
|
526
537
|
|
|
527
538
|
/* ---------- 🎬 视频生成 ---------- */
|
|
528
|
-
register('video', async (host, args, { signal }) => {
|
|
539
|
+
register('video', async (host, args, { signal, core }) => {
|
|
529
540
|
const prompt = String(args.prompt || '').trim();
|
|
530
541
|
const frame = await resolveFirstFrameFile(host, args, signal);
|
|
531
542
|
const providers = selectProviders(cap.CAPABILITIES.VIDEO, '视频', args.model);
|
|
@@ -545,7 +556,7 @@ register('video', async (host, args, { signal }) => {
|
|
|
545
556
|
if (!coreRoutes.length) {
|
|
546
557
|
throw new Error(s2v ? 'iris: 未配置可用 s2v 数字人模型' : 'iris: 当前视频候选只有 s2v 数字人模型;请使用 audio_path 的数字人入口');
|
|
547
558
|
}
|
|
548
|
-
const submitted = await submitCoreVideo(coreRoutes, frame, prompt, args, signal);
|
|
559
|
+
const submitted = await submitCoreVideo(core, coreRoutes, frame, prompt, args, signal);
|
|
549
560
|
const firstArtifactId = submitted.task.artifactIds[0];
|
|
550
561
|
const ready = submitted.task.outcome === 'succeeded'
|
|
551
562
|
&& submitted.task.deliveryState === 'ready' && Boolean(firstArtifactId);
|
|
@@ -565,12 +576,12 @@ register('video', async (host, args, { signal }) => {
|
|
|
565
576
|
* 直接交付 generated-audio Artifact(无 remote 长轮询)。整条候选链共用一个
|
|
566
577
|
* voice(Core providerInput 静态;legacy 曾按候选各自取 provider.ttsVoice)。
|
|
567
578
|
*/
|
|
568
|
-
async function submitCoreTts(routes, normalizedInput, signal) {
|
|
579
|
+
async function submitCoreTts(core, routes, normalizedInput, signal) {
|
|
569
580
|
const voice = normalizedInput.voice || String(routes[0].provider.ttsVoice || '').trim();
|
|
570
581
|
const { providerInput } = normalizeGenerationInput('tts', {
|
|
571
582
|
text: normalizedInput.text, ...(voice ? { voice } : {})
|
|
572
583
|
});
|
|
573
|
-
const submitted = await submitProviderTaskForDsh({
|
|
584
|
+
const submitted = await core.submitProviderTaskForDsh({
|
|
574
585
|
capability: 'tts',
|
|
575
586
|
candidates: routes.map((route) => ({
|
|
576
587
|
adapter: route.adapter,
|
|
@@ -598,7 +609,7 @@ async function submitCoreTts(routes, normalizedInput, signal) {
|
|
|
598
609
|
}
|
|
599
610
|
|
|
600
611
|
/* ---------- 🔊 语音合成 ---------- */
|
|
601
|
-
register('music', async (host, args, { signal }) => {
|
|
612
|
+
register('music', async (host, args, { signal, core }) => {
|
|
602
613
|
const normalized = normalizeGenerationInput('music', {
|
|
603
614
|
prompt: args.prompt,
|
|
604
615
|
...(optionalActionText(args.timeout_ms) !== undefined ? { timeout_ms: args.timeout_ms } : {}),
|
|
@@ -606,7 +617,7 @@ register('music', async (host, args, { signal }) => {
|
|
|
606
617
|
...(optionalActionText(args.retry_delay_ms) !== undefined ? { retry_delay_ms: args.retry_delay_ms } : {})
|
|
607
618
|
});
|
|
608
619
|
const routes = musicCandidatesFromCatalog({ providers: store.runtimeProviders(), assignments: store.assignments() }, args.model);
|
|
609
|
-
const submitted = await submitMusicForDsh({
|
|
620
|
+
const submitted = await core.submitMusicForDsh({
|
|
610
621
|
candidates: routes.map(route => ({ adapter: createConfiguredProviderAdapter(route.provider), model: route.modelRef,
|
|
611
622
|
selectionReason: route.selectionReason, providerBinding: providerTaskBinding(route.provider) })),
|
|
612
623
|
providerInput: normalized.providerInput, musicOptions: normalized.musicOptions, signal
|
|
@@ -625,13 +636,13 @@ register('music', async (host, args, { signal }) => {
|
|
|
625
636
|
text: `[iris] 音乐已生成(${task.modelRef},重试${submitted.retries}次,Core task: ${task.id})` };
|
|
626
637
|
});
|
|
627
638
|
|
|
628
|
-
register('tts', async (host, args, { signal }) => {
|
|
639
|
+
register('tts', async (host, args, { signal, core }) => {
|
|
629
640
|
const { providerInput } = normalizeGenerationInput('tts', {
|
|
630
641
|
text: args.text, voice: optionalActionText(args.voice)
|
|
631
642
|
});
|
|
632
643
|
const providers = selectProviders(cap.CAPABILITIES.TTS, '语音', args.model);
|
|
633
644
|
const routes = providers.map((provider) => simpleModelRoute(provider, args, cap.CAPABILITIES.TTS));
|
|
634
|
-
const submitted = await submitCoreTts(routes, providerInput, signal);
|
|
645
|
+
const submitted = await submitCoreTts(core, routes, providerInput, signal);
|
|
635
646
|
const firstArtifactId = submitted.task.artifactIds[0];
|
|
636
647
|
return {
|
|
637
648
|
ok: true, storage: 'core', taskId: submitted.task.id, model: submitted.model,
|
|
@@ -648,7 +659,7 @@ register('tts', async (host, args, { signal }) => {
|
|
|
648
659
|
* 与人工控制面复用同一 Task/Attempt 轴;正文物化为 text/plain transcript Artifact。
|
|
649
660
|
* 已知边界:临时音频 URL 属于首选候选的账号,failover 到不同账号时如实记录失败。
|
|
650
661
|
*/
|
|
651
|
-
async function submitCoreTranscribe(routes, normalized, signal) {
|
|
662
|
+
async function submitCoreTranscribe(core, routes, normalized, signal) {
|
|
652
663
|
let providerInput = normalized.providerInput;
|
|
653
664
|
if (normalized.audioPath) {
|
|
654
665
|
const prepared = await prepareProviderInput(routes[0].adapter, {
|
|
@@ -656,7 +667,7 @@ async function submitCoreTranscribe(routes, normalized, signal) {
|
|
|
656
667
|
});
|
|
657
668
|
providerInput = normalizeGenerationInput('transcribe', { audio_url: prepared.url }).providerInput;
|
|
658
669
|
}
|
|
659
|
-
const submitted = await submitProviderTaskForDsh({
|
|
670
|
+
const submitted = await core.submitProviderTaskForDsh({
|
|
660
671
|
capability: 'transcribe',
|
|
661
672
|
candidates: routes.map((route) => ({
|
|
662
673
|
adapter: route.adapter,
|
|
@@ -679,7 +690,7 @@ async function submitCoreTranscribe(routes, normalized, signal) {
|
|
|
679
690
|
if (!selected) throw taskActionError(task.id, 'Core Task 的供应商身份无法映射回当前路由', task);
|
|
680
691
|
if (task.acceptance === 'accepted' && task.remoteTaskId
|
|
681
692
|
&& ['none', 'unknown'].includes(task.outcome) && task.phase !== 'terminal') {
|
|
682
|
-
await watchProviderTaskForDsh({ taskId: task.id, signal });
|
|
693
|
+
await core.watchProviderTaskForDsh({ taskId: task.id, signal });
|
|
683
694
|
} else if (task.outcome !== 'succeeded' || task.deliveryState !== 'ready' || !task.artifactIds.length) {
|
|
684
695
|
throw taskActionError(task.id, '转写路由没有得到可观察任务或可交付产物;事实已保留在 Core,未回退重提', task);
|
|
685
696
|
}
|
|
@@ -687,7 +698,7 @@ async function submitCoreTranscribe(routes, normalized, signal) {
|
|
|
687
698
|
}
|
|
688
699
|
|
|
689
700
|
/* ---------- 👁 看图 ---------- */
|
|
690
|
-
register('look', async (host, args, { signal }) => {
|
|
701
|
+
register('look', async (host, args, { signal, core }) => {
|
|
691
702
|
const source = normalizeVisionImageSource(args);
|
|
692
703
|
const p = String(args.image_path || '').trim();
|
|
693
704
|
const mt = MEDIA_TYPES[path.extname(p).toLowerCase()];
|
|
@@ -699,18 +710,18 @@ register('look', async (host, args, { signal }) => {
|
|
|
699
710
|
const { answer, via, model } = await askSingleVisionWithHealth(host, { model: args.model,
|
|
700
711
|
question: String(args.question || '').trim() || '详细描述这张图片。',
|
|
701
712
|
prepareImage: operationSignal => source === 'artifact_id'
|
|
702
|
-
? visionImageForDsh({ artifactId: args.artifact_id, signal: operationSignal }) : readVisionImageFile(p, mt, operationSignal), signal });
|
|
713
|
+
? core.visionImageForDsh({ artifactId: args.artifact_id, signal: operationSignal }) : readVisionImageFile(p, mt, operationSignal), signal });
|
|
703
714
|
const viaLabel = via === 'selfstack' ? 'iris 自持栈' : 'DSH 全局视觉模型';
|
|
704
715
|
return { ok: true, ...(args.artifact_id ? { artifactId: args.artifact_id } : {}),
|
|
705
716
|
text: `[iris] 看图回答(${model} · ${viaLabel}):\n${answer}` };
|
|
706
717
|
});
|
|
707
718
|
|
|
708
719
|
/* ---------- ✂️ 裁剪 ---------- */
|
|
709
|
-
register('crop', async (host, args, { signal }) => {
|
|
720
|
+
register('crop', async (host, args, { signal, core }) => {
|
|
710
721
|
const p = String(args.image_path || '').trim();
|
|
711
722
|
if (!p || !path.isAbsolute(p)) throw new Error('iris: image_path 必须是绝对路径');
|
|
712
723
|
if (!fs.existsSync(p)) throw new Error('iris: 图片不存在');
|
|
713
|
-
const result = await cropForDsh({
|
|
724
|
+
const result = await core.cropForDsh({
|
|
714
725
|
imagePath: p, left: args.left, top: args.top, width: args.width, height: args.height, signal
|
|
715
726
|
});
|
|
716
727
|
const b64 = result.bytes.toString('base64');
|
|
@@ -718,9 +729,9 @@ register('crop', async (host, args, { signal }) => {
|
|
|
718
729
|
});
|
|
719
730
|
|
|
720
731
|
/* ---------- 📷 像素 diff ---------- */
|
|
721
|
-
register('diff', async (host, args, { signal }) => {
|
|
732
|
+
register('diff', async (host, args, { signal, core }) => {
|
|
722
733
|
if (!args.image_a_path || !args.image_b_path) throw new Error('iris: 需要 image_a_path 和 image_b_path');
|
|
723
|
-
const result = await diffForDsh({
|
|
734
|
+
const result = await core.diffForDsh({
|
|
724
735
|
imageA: { imagePath: args.image_a_path }, imageB: { imagePath: args.image_b_path },
|
|
725
736
|
grid: args.grid, topRegions: args.top_regions, signal
|
|
726
737
|
});
|
|
@@ -736,7 +747,7 @@ register('diff', async (host, args, { signal }) => {
|
|
|
736
747
|
});
|
|
737
748
|
|
|
738
749
|
/* ---------- 📍 定位 ---------- */
|
|
739
|
-
register('locate', async (host, args, { signal }) => {
|
|
750
|
+
register('locate', async (host, args, { signal, core }) => {
|
|
740
751
|
const source = normalizeVisionImageSource(args);
|
|
741
752
|
const p = String(args.image_path || '').trim();
|
|
742
753
|
if (source === 'image_path' && (!p || !path.isAbsolute(p))) throw new Error('iris: image_path 必须是绝对路径');
|
|
@@ -744,25 +755,25 @@ register('locate', async (host, args, { signal }) => {
|
|
|
744
755
|
const r = await runLocateRequest(host, { providers: store.pickAllFor(cap.CAPABILITIES.VISION), onRateLimit: store.recordRateLimit,
|
|
745
756
|
onModelAvailability: store.recordAvailability, model: args.model,
|
|
746
757
|
target: args.target, prepareImage: operationSignal => source === 'artifact_id'
|
|
747
|
-
? visionImageForDsh({ artifactId: args.artifact_id, signal: operationSignal }) : readVisionImageFile(p, mt, operationSignal), signal });
|
|
758
|
+
? core.visionImageForDsh({ artifactId: args.artifact_id, signal: operationSignal }) : readVisionImageFile(p, mt, operationSignal), signal });
|
|
748
759
|
const { width, height } = r;
|
|
749
760
|
const provenance = args.artifact_id ? { artifactId: args.artifact_id } : {};
|
|
750
761
|
if (!r.found) return { ok: true, ...provenance, text: `[iris] 在图片中未找到「${args.target}」` };
|
|
751
762
|
return { ok: true, ...provenance, text: `[iris] 定位「${args.target}」:bbox (${r.x1},${r.y1},${r.x2},${r.y2}) / ${width}x${height}(${r.via} · ${r.model})` };
|
|
752
763
|
});
|
|
753
764
|
/* ---------- 🖼️ HTML 截图 ---------- */
|
|
754
|
-
register('html', async (host, args, { signal }) => {
|
|
765
|
+
register('html', async (host, args, { signal, core }) => {
|
|
755
766
|
const html = String(args.html || '').trim();
|
|
756
767
|
if (!html) throw new Error('iris: html 不能为空');
|
|
757
768
|
const browser = requireHostPort(host, 'browser', 'HTML 截图');
|
|
758
|
-
const rendered = await htmlForDsh({ browser, html, width: args.width, height: args.height,
|
|
769
|
+
const rendered = await core.htmlForDsh({ browser, html, width: args.width, height: args.height,
|
|
759
770
|
fullPage: args.fullPage !== false, signal });
|
|
760
771
|
const png = Buffer.from(rendered.bytes);
|
|
761
772
|
return { ok: true, storage: 'core', artifactIds: [rendered.artifact.id], text: `[iris] HTML 截图完成(fullPage: ${args.fullPage !== false})`, imageDataUrl: `data:image/png;base64,${png.toString('base64')}` };
|
|
762
773
|
});
|
|
763
774
|
|
|
764
775
|
/* ---------- 📄 长截图 OCR ---------- */
|
|
765
|
-
register('ocr', async (host, args, { signal }) => {
|
|
776
|
+
register('ocr', async (host, args, { signal, core }) => {
|
|
766
777
|
const source = normalizeVisionImageSource(args);
|
|
767
778
|
const p = String(args.image_path || '').trim();
|
|
768
779
|
const mediaType = MEDIA_TYPES[path.extname(p).toLowerCase()];
|
|
@@ -773,7 +784,7 @@ register('ocr', async (host, args, { signal }) => {
|
|
|
773
784
|
const result = await runOcrRequest(host, { providers: store.pickAllFor(cap.CAPABILITIES.VISION),
|
|
774
785
|
onRateLimit: store.recordRateLimit, onModelAvailability: store.recordAvailability,
|
|
775
786
|
prepareImage: operationSignal => source === 'artifact_id'
|
|
776
|
-
? visionImageForDsh({ artifactId: args.artifact_id, signal: operationSignal }) : readVisionImageFile(p, mediaType, operationSignal),
|
|
787
|
+
? core.visionImageForDsh({ artifactId: args.artifact_id, signal: operationSignal }) : readVisionImageFile(p, mediaType, operationSignal),
|
|
777
788
|
chunkHeight: args.chunk_height, overlap: args.overlap, signal });
|
|
778
789
|
return { ok: result.status !== 'failed', status: result.status,
|
|
779
790
|
totalChunks: result.totalChunks, successfulChunks: result.successfulChunks,
|
|
@@ -782,7 +793,7 @@ register('ocr', async (host, args, { signal }) => {
|
|
|
782
793
|
});
|
|
783
794
|
|
|
784
795
|
/* ---------- 📋 任务查询 ---------- */
|
|
785
|
-
register('status', async (host, args, { signal }) => {
|
|
796
|
+
register('status', async (host, args, { signal, core }) => {
|
|
786
797
|
const requestedId = String(args.task_id || '').trim();
|
|
787
798
|
if (requestedId) {
|
|
788
799
|
const legacy = tasks.get(requestedId);
|
|
@@ -797,14 +808,14 @@ register('status', async (host, args, { signal }) => {
|
|
|
797
808
|
}
|
|
798
809
|
if (/^task_[a-f0-9]{24}$/.test(requestedId)) {
|
|
799
810
|
try {
|
|
800
|
-
const
|
|
801
|
-
const lines = [`[${
|
|
802
|
-
if (
|
|
803
|
-
if (
|
|
804
|
-
if (
|
|
805
|
-
lines.push('artifacts:\n' +
|
|
811
|
+
const coreTask = await core.inspectProviderTaskForDsh(requestedId, { signal });
|
|
812
|
+
const lines = [`[${coreTask.capability}] ${coreTask.id} · Core ${coreTask.phase} · ${coreTask.outcome} / ${coreTask.deliveryState} · ${coreTask.modelRef || '未选择模型'}`];
|
|
813
|
+
if (coreTask.lastError) lines.push('error: ' + coreTask.lastError.safeMessage);
|
|
814
|
+
if (coreTask.remoteTaskId) lines.push('remoteTask: ' + coreTask.remoteTaskId);
|
|
815
|
+
if (coreTask.artifactIds.length) {
|
|
816
|
+
lines.push('artifacts:\n' + coreTask.artifactIds.map((id) => id + ' · /iris/api/core/artifact/' + encodeURIComponent(id) + '/media').join('\n'));
|
|
806
817
|
}
|
|
807
|
-
return { ok: true, storage: 'core', taskId:
|
|
818
|
+
return { ok: true, storage: 'core', taskId: coreTask.id, artifactIds: coreTask.artifactIds, text: lines.join('\n') };
|
|
808
819
|
} catch (_) { /* 对外统一为不存在,不暴露 Core 数据根 */ }
|
|
809
820
|
}
|
|
810
821
|
throw new Error('iris: 没有这个任务:' + args.task_id);
|
|
@@ -816,7 +827,7 @@ register('status', async (host, args, { signal }) => {
|
|
|
816
827
|
}));
|
|
817
828
|
let coreRows = [];
|
|
818
829
|
try {
|
|
819
|
-
const snapshot = await coreSnapshotForDsh({ limit: 5, signal });
|
|
830
|
+
const snapshot = await core.coreSnapshotForDsh({ limit: 5, signal });
|
|
820
831
|
coreRows = (snapshot.tasks.recent || []).map((task) => ({
|
|
821
832
|
at: task.updatedAt,
|
|
822
833
|
text: `[${task.capability}] ${task.id} · Core ${task.phase} · ${task.outcome} / ${task.deliveryState} · ${task.modelRef || '未选择模型'}`
|
|
@@ -843,7 +854,7 @@ register('task_reobserve', async (host, args) => {
|
|
|
843
854
|
return { ok: true, taskId: id, text: '[iris] 已重新观察远端任务;不会重新提交生成请求。' };
|
|
844
855
|
});
|
|
845
856
|
|
|
846
|
-
register('task_redeliver', async (host, args, { signal }) => {
|
|
857
|
+
register('task_redeliver', async (host, args, { signal, core }) => {
|
|
847
858
|
const id = String(args.task_id || '').trim();
|
|
848
859
|
const task = tasks.get(id);
|
|
849
860
|
if (!task || !tasks.isV2Task(task)) throw new Error('iris: 找不到可重新交付的 Task v2');
|
|
@@ -885,7 +896,7 @@ register('task_restore_attention', async (_host, args) => {
|
|
|
885
896
|
return { ok: true, taskId: id, text: '[iris] 已恢复任务提醒。' };
|
|
886
897
|
});
|
|
887
898
|
|
|
888
|
-
register('task_manual_retry', async (host, args, { signal }) => {
|
|
899
|
+
register('task_manual_retry', async (host, args, { signal, core }) => {
|
|
889
900
|
const id = String(args.task_id || '').trim();
|
|
890
901
|
if (args.confirm_duplicate_charge !== true) throw new Error('iris: 必须明确确认本次操作可能产生重复任务或重复费用');
|
|
891
902
|
const task = tasks.get(id);
|
|
@@ -909,7 +920,7 @@ register('task_manual_retry', async (host, args, { signal }) => {
|
|
|
909
920
|
if (task.modelRef) retryArgs.model = task.modelRef;
|
|
910
921
|
retryArgs._retryOf = id;
|
|
911
922
|
try {
|
|
912
|
-
const result = await runAction(host, action, retryArgs, { signal });
|
|
923
|
+
const result = await runAction(host, action, retryArgs, { signal, core });
|
|
913
924
|
if (result.taskId) tasks.linkManualRetry(id, result.taskId);
|
|
914
925
|
return { ...result, retryOf: id, text: (result.text || '[iris] 已创建新任务')
|
|
915
926
|
+ '\n[iris] 原任务提醒已归档,并保留新旧任务关联;本次新任务可能与远端未知任务重复。' };
|
|
@@ -1022,7 +1033,7 @@ register('tasks_purge_orphans', async () => {
|
|
|
1022
1033
|
});
|
|
1023
1034
|
|
|
1024
1035
|
/* ---------- 👁 重看(GUI:只查 iris 自己的任务产物,无会话上下文) ---------- */
|
|
1025
|
-
register('relook', async (host, args, { signal }) => {
|
|
1036
|
+
register('relook', async (host, args, { signal, core }) => {
|
|
1026
1037
|
const id = String(args.attachment_id || '').trim();
|
|
1027
1038
|
if (!id) throw new Error('iris: attachment_id 不能为空');
|
|
1028
1039
|
const question = String(args.question || '').trim();
|
|
@@ -1142,7 +1153,7 @@ register('providers_set_models', async (host, args) => {
|
|
|
1142
1153
|
* 模型发现:读取供应商目录 → 明确能力元数据 → 合并模型池。
|
|
1143
1154
|
* 免费只读;拉不到/不支持则原样保留现有池(优雅降级),报人话错误。
|
|
1144
1155
|
*/
|
|
1145
|
-
register('providers_discover', async (host, args, { signal }) => {
|
|
1156
|
+
register('providers_discover', async (host, args, { signal, core }) => {
|
|
1146
1157
|
const p = store.providerById(String(args.id || ''));
|
|
1147
1158
|
if (!p) throw new Error('iris: 供应商不存在或未启用');
|
|
1148
1159
|
const [updated] = await store.refreshProviderDirectories({ providerId: p.id, force: true, signal });
|
|
@@ -1155,7 +1166,7 @@ register('providers_discover', async (host, args, { signal }) => {
|
|
|
1155
1166
|
});
|
|
1156
1167
|
|
|
1157
1168
|
/** 只更新目录快照;新增项在管理视图待确认,不改模型用途与功能名单。 */
|
|
1158
|
-
register('providers_sync_directory', async (host, args, { signal }) => {
|
|
1169
|
+
register('providers_sync_directory', async (host, args, { signal, core }) => {
|
|
1159
1170
|
const id = String(args.id || '').trim();
|
|
1160
1171
|
if (!store.providerById(id)) throw new Error('iris: 供应商不存在或连接未就绪');
|
|
1161
1172
|
const [result] = await store.refreshProviderDirectories({ providerId: id, force: args.force === true, signal });
|
|
@@ -1167,7 +1178,7 @@ register('providers_sync_directory', async (host, args, { signal }) => {
|
|
|
1167
1178
|
});
|
|
1168
1179
|
|
|
1169
1180
|
/** 能力实测(红色测试图):对某供应商的 vision 模型跑 testVisionCapability */
|
|
1170
|
-
register('providers_test_vision', async (host, args, { signal }) => {
|
|
1181
|
+
register('providers_test_vision', async (host, args, { signal, core }) => {
|
|
1171
1182
|
const p = store.providerById(String(args.id || ''));
|
|
1172
1183
|
if (!p) throw new Error('iris: 供应商不存在');
|
|
1173
1184
|
const vision = models.providerModels(p).find(m => m.capabilities.includes(cap.CAPABILITIES.VISION));
|
|
@@ -1329,7 +1340,7 @@ register('providers_set_model_vision_input', async (host, args) => {
|
|
|
1329
1340
|
});
|
|
1330
1341
|
|
|
1331
1342
|
/** 逐模型实测某能力 → 存 verified("test it, don't guess it";用户手动触发,发现不自动跑) */
|
|
1332
|
-
register('providers_test_model', async (host, args, { signal }) => {
|
|
1343
|
+
register('providers_test_model', async (host, args, { signal, core }) => {
|
|
1333
1344
|
const p = store.providerById(String(args.id || ''));
|
|
1334
1345
|
if (!p) throw new Error('iris: 供应商不存在或未启用');
|
|
1335
1346
|
const modelId = String(args.model_id || '').trim();
|
|
@@ -1485,7 +1496,7 @@ register('attachments_list', async (host, args) => {
|
|
|
1485
1496
|
* 把一个附件(会话 image 或 iris 产物)导出为宿主可读的文件路径,供工具消费。
|
|
1486
1497
|
* iris 产物直接返回其 outputs 路径;会话附件读字节存 uploads/(按 attachmentId 哈希缓存,不重复落盘)。
|
|
1487
1498
|
*/
|
|
1488
|
-
register('attachment_export', async (host, args, { signal }) => {
|
|
1499
|
+
register('attachment_export', async (host, args, { signal, core }) => {
|
|
1489
1500
|
const id = String(args.attachment_id || '').trim();
|
|
1490
1501
|
if (!id) throw new Error('iris: attachment_id 不能为空');
|
|
1491
1502
|
// 1) iris 自有产物:文件已在 outputs/
|
|
@@ -1516,7 +1527,7 @@ register('attachment_export', async (host, args, { signal }) => {
|
|
|
1516
1527
|
|
|
1517
1528
|
/* ============ 音频转写(阶段 7.2) ============ */
|
|
1518
1529
|
|
|
1519
|
-
register('transcribe', async (host, args, { signal }) => {
|
|
1530
|
+
register('transcribe', async (host, args, { signal, core }) => {
|
|
1520
1531
|
const normalized = normalizeGenerationInput('transcribe', {
|
|
1521
1532
|
audio_url: args.audio_url, audio_path: args.audio_path
|
|
1522
1533
|
}, { allowAudioPath: true });
|
|
@@ -1525,7 +1536,7 @@ register('transcribe', async (host, args, { signal }) => {
|
|
|
1525
1536
|
const routes = providers.map((provider) =>
|
|
1526
1537
|
simpleModelRoute(provider, args, cap.CAPABILITIES.TRANSCRIBE)
|
|
1527
1538
|
);
|
|
1528
|
-
const submitted = await submitCoreTranscribe(routes, normalized, signal);
|
|
1539
|
+
const submitted = await submitCoreTranscribe(core, routes, normalized, signal);
|
|
1529
1540
|
const firstArtifactId = submitted.task.artifactIds[0];
|
|
1530
1541
|
const ready = submitted.task.outcome === 'succeeded'
|
|
1531
1542
|
&& submitted.task.deliveryState === 'ready' && Boolean(firstArtifactId);
|
|
@@ -1542,8 +1553,8 @@ register('transcribe', async (host, args, { signal }) => {
|
|
|
1542
1553
|
|
|
1543
1554
|
/* ============ 视频抽帧(阶段 7.1) ============ */
|
|
1544
1555
|
|
|
1545
|
-
register('video_frames', async (host, args, { signal }) => {
|
|
1546
|
-
const { media: meta, frames } = await framesForDsh({
|
|
1556
|
+
register('video_frames', async (host, args, { signal, core }) => {
|
|
1557
|
+
const { media: meta, frames } = await core.framesForDsh({
|
|
1547
1558
|
videoPath: args.video_path, maxFrames: args.max_frames, targetWidth: args.target_width,
|
|
1548
1559
|
format: optionalActionText(args.format), quality: args.quality, signal
|
|
1549
1560
|
});
|
|
@@ -1563,7 +1574,7 @@ register('video_frames', async (host, args, { signal }) => {
|
|
|
1563
1574
|
|
|
1564
1575
|
/* ============ 多模态视频摘要(阶段 7.3,GUI 同步快速版) ============ */
|
|
1565
1576
|
|
|
1566
|
-
register('media_summarize', async (host, args, { signal }) => {
|
|
1577
|
+
register('media_summarize', async (host, args, { signal, core }) => {
|
|
1567
1578
|
const source = normalizeSummarySource(args);
|
|
1568
1579
|
const p = String(args.video_path || '').trim();
|
|
1569
1580
|
if (source === 'video') {
|
|
@@ -1576,7 +1587,7 @@ register('media_summarize', async (host, args, { signal }) => {
|
|
|
1576
1587
|
providers: store.pickAllFor(cap.CAPABILITIES.VISION), model: args.model, question: args.question, signal,
|
|
1577
1588
|
async prepareMedia(operationSignal) {
|
|
1578
1589
|
if (source === 'core-artifacts') {
|
|
1579
|
-
return { ...await summaryFramesForDsh({ frameArtifactIds: args.frame_artifact_ids, signal: operationSignal }), transcript: args.transcribe_text };
|
|
1590
|
+
return { ...await core.summaryFramesForDsh({ frameArtifactIds: args.frame_artifact_ids, signal: operationSignal }), transcript: args.transcribe_text };
|
|
1580
1591
|
}
|
|
1581
1592
|
const meta = probeVideo(p);
|
|
1582
1593
|
const frames = await extractFrames({ inputPath: p, maxFrames: Math.min(Number(args.max_frames) || 8, 12),
|