@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.
Files changed (58) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/README.en.md +28 -19
  3. package/README.md +27 -18
  4. package/bin/dsh-iris.js +140 -26
  5. package/docs/AGENT_QUICKSTART.md +5 -5
  6. package/docs/ARCHITECTURE.md +9 -3
  7. package/docs/CLI_MANAGEMENT.md +1 -1
  8. package/docs/CORE_RUNTIME_CONTRACT.md +36 -5
  9. package/docs/CORE_SKILLS.md +62 -0
  10. package/docs/DSH_COMPATIBILITY.md +4 -4
  11. package/docs/DSH_CORE_MIGRATION.md +4 -2
  12. package/docs/DSH_PROVIDER_CONNECTIONS.md +1 -1
  13. package/docs/DSH_RUNTIME.md +36 -0
  14. package/docs/FUNCTION_MODEL_POOLS.md +1 -1
  15. package/docs/HEADLESS_CLI.md +11 -3
  16. package/docs/HOST_DOCTOR.md +13 -1
  17. package/docs/IMAGE_FLOW.md +71 -0
  18. package/docs/LOCAL_API.md +60 -0
  19. package/docs/LOCAL_API_CONTRACT.md +115 -0
  20. package/docs/LOCAL_CREATION.md +50 -0
  21. package/docs/PROVIDER_HEALTH.md +1 -1
  22. package/docs/ROADMAP.md +14 -2
  23. package/docs/SECURITY.md +9 -1
  24. package/docs/SYNC_DELIVERY_RECOVERY.md +2 -0
  25. package/docs/VISION_CLI.md +1 -1
  26. package/docs/WEB_PROTOTYPE.md +47 -0
  27. package/docs/releases/0.2.7.md +62 -0
  28. package/lib/actions.js +79 -68
  29. package/lib/api.js +38 -38
  30. package/lib/application-runtime.js +186 -0
  31. package/lib/client.js +89 -3
  32. package/lib/core-artifact-store.js +5 -0
  33. package/lib/core-maintenance.js +25 -2
  34. package/lib/core-runtime.js +19 -0
  35. package/lib/core-tasks.js +62 -8
  36. package/lib/creation-request-store.js +159 -0
  37. package/lib/doctor.js +12 -1
  38. package/lib/dsh-core-adapter.js +666 -572
  39. package/lib/dsh-host-adapter.js +4 -1
  40. package/lib/flow-store.js +195 -0
  41. package/lib/image-flow.js +188 -0
  42. package/lib/index.js +59 -53
  43. package/lib/local-api-projection.js +27 -0
  44. package/lib/local-api.js +381 -0
  45. package/lib/local-creation.js +85 -0
  46. package/lib/private-storage.js +12 -0
  47. package/lib/provider-task-runner.js +22 -2
  48. package/lib/render-address.js +35 -0
  49. package/lib/render.js +52 -13
  50. package/lib/skill-installation.js +199 -0
  51. package/lib/workbench-artifacts.js +1 -2
  52. package/lib/workbench-management.js +8 -9
  53. package/package.json +3 -2
  54. package/skills/iris-core/SKILL.md +40 -0
  55. package/skills/iris-core/agents/openai.yaml +4 -0
  56. package/skills/iris-core/references/cli.md +95 -0
  57. package/skills/iris-core/references/tasks.md +58 -0
  58. 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 是纯函数 `(host, args, { signal }) => result`,
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(`iris: 未知动作 "${name}"`);
478
+ if (!fn) throw new Error('iris: 未知动作 "' + name + '"');
478
479
  const adapter = actionHost(host);
479
- const execute = async () => {
480
- if (AUTO_DIRECTORY_ACTIONS.has(name) && !args?.model) await store.refreshAutomaticDirectories({
481
- capability: ({ image: 'image-gen', image_edit: 'image-gen', video: 'video-gen', music: 'music-gen', tts: 'tts', transcribe: 'transcribe' })[name] || 'vision', signal
482
- });
483
- return fn(adapter, args || {}, { signal });
484
- };
485
- return PROVIDER_ACTIONS.has(name) ? withDshProviderConfig(adapter, execute, { signal }) : execute();
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 core = await inspectProviderTaskForDsh(requestedId, { signal });
801
- const lines = [`[${core.capability}] ${core.id} · Core ${core.phase} · ${core.outcome} / ${core.deliveryState} · ${core.modelRef || '未选择模型'}`];
802
- if (core.lastError) lines.push('error: ' + core.lastError.safeMessage);
803
- if (core.remoteTaskId) lines.push('remoteTask: ' + core.remoteTaskId);
804
- if (core.artifactIds.length) {
805
- lines.push('artifacts:\n' + core.artifactIds.map((id) => id + ' · /iris/api/core/artifact/' + encodeURIComponent(id) + '/media').join('\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: core.id, artifactIds: core.artifactIds, text: lines.join('\n') };
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),