@mokuyoaxis/dsh-iris 0.2.6 → 0.3.0-alpha.1

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 (177) hide show
  1. package/CHANGELOG.md +103 -1
  2. package/README.en.md +104 -304
  3. package/README.md +101 -302
  4. package/bin/dsh-iris.js +446 -64
  5. package/docs/AGENT_CORE_QUICKSTART.md +64 -0
  6. package/docs/AGENT_QUICKSTART.md +7 -5
  7. package/docs/ARCHITECTURE.md +20 -4
  8. package/docs/AUDIO_FLOWS.md +71 -0
  9. package/docs/CLI_MANAGEMENT.md +1 -1
  10. package/docs/CONTACT_SHEET.md +62 -0
  11. package/docs/CORE_CONNECTIONS.md +99 -0
  12. package/docs/CORE_QUICKSTART.md +108 -0
  13. package/docs/CORE_RUNTIME_CONTRACT.md +41 -5
  14. package/docs/CORE_SERVICE.md +97 -0
  15. package/docs/CORE_SKILLS.md +94 -0
  16. package/docs/DSH_COMPATIBILITY.md +14 -4
  17. package/docs/DSH_CORE_MIGRATION.md +4 -2
  18. package/docs/DSH_PROVIDER_CONNECTIONS.md +1 -1
  19. package/docs/DSH_RC2_ADAPTATION.md +1 -1
  20. package/docs/DSH_RUNTIME.md +40 -0
  21. package/docs/FUNCTION_MODEL_POOLS.md +1 -1
  22. package/docs/HEADLESS_CLI.md +12 -4
  23. package/docs/HOST_DOCTOR.md +13 -1
  24. package/docs/IMAGE_FLOW.md +75 -0
  25. package/docs/IMAGE_GENERATION.md +127 -0
  26. package/docs/LOCAL_API.md +62 -0
  27. package/docs/LOCAL_API_CONTRACT.md +140 -0
  28. package/docs/LOCAL_CREATION.md +56 -0
  29. package/docs/LOCAL_OPERATIONS.md +46 -0
  30. package/docs/LOCAL_SELECTION.md +57 -0
  31. package/docs/LOCAL_WEB.md +201 -0
  32. package/docs/MCP.md +191 -0
  33. package/docs/MUSIC.md +2 -0
  34. package/docs/PANEL_EXTENSIONS.md +64 -0
  35. package/docs/PLUGINS.md +189 -0
  36. package/docs/PLUGIN_STARTUP.md +100 -0
  37. package/docs/PROVIDER_ADAPTER_CONTRACT.md +1 -1
  38. package/docs/PROVIDER_EXTENSIONS.md +78 -0
  39. package/docs/PROVIDER_HEALTH.md +4 -2
  40. package/docs/PROVIDER_SETTINGS.md +85 -0
  41. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +6 -0
  42. package/docs/ROADMAP.md +28 -6
  43. package/docs/SECURITY.md +9 -1
  44. package/docs/SYNC_DELIVERY_RECOVERY.md +2 -0
  45. package/docs/VISION_CLI.md +2 -2
  46. package/docs/WEB_PROTOTYPE.md +62 -0
  47. package/docs/WEB_RECOVERY.md +46 -0
  48. package/docs/WORKBENCH_LOGIN.md +63 -0
  49. package/docs/assets/screenshots/core-connections-preview.webp +0 -0
  50. package/docs/assets/screenshots/core-model-pools-preview.webp +0 -0
  51. package/docs/assets/screenshots/core-overview-preview.webp +0 -0
  52. package/docs/assets/screenshots/core-spectrum-preview.webp +0 -0
  53. package/docs/assets/screenshots/core-studio-dark-preview.webp +0 -0
  54. package/docs/assets/screenshots/core-studio-mobile-preview.webp +0 -0
  55. package/docs/assets/screenshots/core-studio-preview.webp +0 -0
  56. package/docs/releases/0.2.7.md +62 -0
  57. package/docs/releases/0.3.0-alpha.1.md +51 -0
  58. package/docs/screenshots.md +41 -1
  59. package/lib/actions.js +132 -70
  60. package/lib/adapters.js +20 -11
  61. package/lib/api.js +38 -38
  62. package/lib/application-runtime.js +213 -0
  63. package/lib/audio-flow-input.js +37 -0
  64. package/lib/client.js +434 -55
  65. package/lib/comfyui-adapter.js +94 -0
  66. package/lib/comfyui-client.js +51 -0
  67. package/lib/comfyui-inspection.js +73 -0
  68. package/lib/comfyui-workflow.js +94 -0
  69. package/lib/config.js +36 -1
  70. package/lib/configured-image-runtime.js +50 -0
  71. package/lib/connection-skill.js +89 -0
  72. package/lib/connection-template.js +23 -0
  73. package/lib/core-artifact-store.js +21 -2
  74. package/lib/core-bridge-selection.js +31 -0
  75. package/lib/core-bridge-tools.js +83 -0
  76. package/lib/core-capability-catalog.js +182 -0
  77. package/lib/core-connection-diagnostics.js +49 -0
  78. package/lib/core-connection-store.js +277 -0
  79. package/lib/core-connections.js +225 -0
  80. package/lib/core-maintenance.js +31 -2
  81. package/lib/core-runtime.js +20 -0
  82. package/lib/core-service-client.js +170 -0
  83. package/lib/core-tasks.js +84 -13
  84. package/lib/core-writer-lease.js +16 -0
  85. package/lib/creation-request-store.js +133 -0
  86. package/lib/doctor.js +12 -1
  87. package/lib/dsh-core-adapter.js +666 -572
  88. package/lib/dsh-host-adapter.js +8 -2
  89. package/lib/dsh-service-bridge.js +66 -0
  90. package/lib/execution-ownership.js +43 -0
  91. package/lib/flow-store.js +290 -0
  92. package/lib/generation-input.js +10 -1
  93. package/lib/headless-vision.js +7 -7
  94. package/lib/image-edit-input.js +1 -1
  95. package/lib/image-flow.js +232 -0
  96. package/lib/index.js +94 -68
  97. package/lib/local-api-projection.js +32 -0
  98. package/lib/local-api-token-directory.js +31 -0
  99. package/lib/local-api.js +1083 -0
  100. package/lib/local-creation.js +150 -0
  101. package/lib/local-operations.js +36 -0
  102. package/lib/local-request-root.js +86 -0
  103. package/lib/local-web-assets.js +37 -0
  104. package/lib/mcp-bridge.js +58 -0
  105. package/lib/mcp-check.js +26 -0
  106. package/lib/mcp-config.js +42 -0
  107. package/lib/mcp-delivery.js +46 -0
  108. package/lib/mcp-http.js +101 -0
  109. package/lib/mcp-server.js +47 -0
  110. package/lib/model-directory-service.js +16 -6
  111. package/lib/model-directory.js +3 -3
  112. package/lib/models.js +2 -2
  113. package/lib/music-transport.js +2 -1
  114. package/lib/operation-runner.js +236 -0
  115. package/lib/operation-store.js +216 -0
  116. package/lib/panel-extension.js +36 -0
  117. package/lib/plugin-contract.js +194 -0
  118. package/lib/plugin-execution.js +51 -0
  119. package/lib/plugin-launch.js +86 -0
  120. package/lib/plugin-operation-store.js +101 -0
  121. package/lib/plugin-operations.js +28 -0
  122. package/lib/plugin-registry.js +109 -0
  123. package/lib/plugin-selection.js +15 -0
  124. package/lib/plugin-source-image.js +5 -0
  125. package/lib/plugin-web-assets.js +54 -0
  126. package/lib/plugins.d.ts +288 -0
  127. package/lib/plugins.js +58 -0
  128. package/lib/private-storage.js +12 -0
  129. package/lib/provider-adapter.js +5 -1
  130. package/lib/provider-adapters.js +56 -18
  131. package/lib/provider-catalog.js +40 -29
  132. package/lib/provider-config-service.js +105 -23
  133. package/lib/provider-diagnostics.js +56 -0
  134. package/lib/provider-extensions.js +29 -0
  135. package/lib/provider-health.js +8 -1
  136. package/lib/provider-monitor.js +79 -0
  137. package/lib/provider-protocol.js +70 -20
  138. package/lib/provider-registry.js +118 -0
  139. package/lib/provider-response.js +28 -0
  140. package/lib/provider-settings.js +134 -0
  141. package/lib/provider-task-runner.js +40 -7
  142. package/lib/render-address.js +35 -0
  143. package/lib/render.js +52 -13
  144. package/lib/skill-installation.js +211 -0
  145. package/lib/vision-model-routing.js +3 -0
  146. package/lib/web/app.js +642 -0
  147. package/lib/web/appearance.js +111 -0
  148. package/lib/web/comfyui.js +146 -0
  149. package/lib/web/connections.js +377 -0
  150. package/lib/web/creation.js +355 -0
  151. package/lib/web/flow-view.js +33 -0
  152. package/lib/web/gallery.js +132 -0
  153. package/lib/web/index.html +291 -0
  154. package/lib/web/media-client.js +204 -0
  155. package/lib/web/plugin-panel.js +221 -0
  156. package/lib/web/plugin-surface.css +17 -0
  157. package/lib/web/providers.js +492 -0
  158. package/lib/web/recovery.js +105 -0
  159. package/lib/web/startup.js +18 -0
  160. package/lib/web/styles.css +518 -0
  161. package/lib/web/transport.js +96 -0
  162. package/lib/web/workbench-login.js +165 -0
  163. package/lib/workbench-artifacts.js +1 -2
  164. package/lib/workbench-auth-cli.js +47 -0
  165. package/lib/workbench-auth.js +200 -0
  166. package/lib/workbench-management.js +8 -9
  167. package/package.json +22 -6
  168. package/plugins/contact-sheet/index.d.mts +4 -0
  169. package/plugins/contact-sheet/index.mjs +33 -0
  170. package/plugins/contact-sheet/web/board.css +14 -0
  171. package/plugins/contact-sheet/web/board.mjs +93 -0
  172. package/skills/iris-core/SKILL.md +44 -0
  173. package/skills/iris-core/agents/openai.yaml +4 -0
  174. package/skills/iris-core/references/cli.md +95 -0
  175. package/skills/iris-core/references/comfyui.md +69 -0
  176. package/skills/iris-core/references/tasks.md +60 -0
  177. package/user_guide.md +19 -9
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,17 @@ 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
+ import { compileComfyWorkflow, projectComfyWorkflow } from './comfyui-workflow.js';
44
+ import { createComfyUiClient } from './comfyui-client.js';
45
+ import { inspectComfyWorkflow } from './comfyui-inspection.js';
43
46
 
44
47
  const PROVIDER_ACTIONS = new Set(['image', 'image_edit', 'video', 'music', 'tts', 'transcribe', 'look', 'relook',
45
48
  'locate', 'ocr', 'media_summarize', 'task_reobserve', 'task_redeliver', 'task_manual_retry',
46
- 'providers_discover', 'providers_sync_directory', 'providers_test_vision', 'providers_test_model']);
49
+ 'providers_discover', 'providers_sync_directory', 'providers_test_vision', 'providers_test_model', 'providers_check_comfy_workflow']);
47
50
  const AUTO_DIRECTORY_ACTIONS = new Set(['image', 'image_edit', 'video', 'music', 'tts', 'transcribe', 'look', 'relook',
48
51
  'locate', 'ocr', 'media_summarize']);
52
+ const VISION_ACTIONS = new Set(['look', 'relook', 'locate', 'ocr', 'media_summarize']);
49
53
 
50
54
  const MEDIA_TYPES = {
51
55
  '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
@@ -63,6 +67,12 @@ function imageRoutingForModel(provider, modelId) {
63
67
  return { ...routing, editSupported: supportsImageEdit(routing.mediaProtocol, modelId) };
64
68
  }
65
69
 
70
+ function comfySummaryForModel(provider, modelId) {
71
+ const route = providerForImageModel(provider, modelId);
72
+ return route.mediaProtocol === 'comfyui' && route.comfyWorkflow !== undefined
73
+ ? { comfyWorkflow: projectComfyWorkflow(route.comfyWorkflow) } : {};
74
+ }
75
+
66
76
  const EMPTY_ACTION_HOST = defineHostAdapter({
67
77
  id: 'iris-hostless',
68
78
  ports: {},
@@ -242,8 +252,8 @@ function recordCoreTaskAttemptHealth(task, capability) {
242
252
  }
243
253
 
244
254
  /** DSH 图片入口:同步和异步候选共用一份 Core Task/Attempt/Artifact 事实。 */
245
- async function submitCoreImage(routes, providerInput, signal, sourceArtifactId) {
246
- const submitted = await submitProviderTaskForDsh({
255
+ async function submitCoreImage(core, routes, providerInput, signal, sourceArtifactId) {
256
+ const submitted = await core.submitProviderTaskForDsh({
247
257
  capability: 'image',
248
258
  candidates: routes.map((route) => ({
249
259
  adapter: route.adapter,
@@ -267,7 +277,7 @@ async function submitCoreImage(routes, providerInput, signal, sourceArtifactId)
267
277
  if (!selected) throw taskActionError(task.id, 'Core Task 的供应商身份无法映射回当前路由', task);
268
278
  if (task.acceptance === 'accepted' && task.remoteTaskId
269
279
  && ['none', 'unknown'].includes(task.outcome) && task.phase !== 'terminal') {
270
- await watchProviderTaskForDsh({ taskId: task.id, signal });
280
+ await core.watchProviderTaskForDsh({ taskId: task.id, signal });
271
281
  } else if (task.outcome !== 'succeeded' || task.deliveryState !== 'ready' || !task.artifactIds.length) {
272
282
  throw taskActionError(task.id, '图片路由没有得到可观察任务或可交付产物;事实已保留在 Core,未回退重提', task);
273
283
  }
@@ -279,7 +289,7 @@ async function submitCoreImage(routes, providerInput, signal, sourceArtifactId)
279
289
  * 不共用图片状态机——视频走自己的交付 Profile(video/mp4 / generated-video)。
280
290
  * s2v 在写前 Attempt 之后按账号上传首帧和音频,交付复用视频 Profile。
281
291
  */
282
- async function submitCoreVideo(routes, frame, prompt, args, signal) {
292
+ async function submitCoreVideo(core, routes, frame, prompt, args, signal) {
283
293
  const normalized = normalizeGenerationInput('video', {
284
294
  ...(prompt ? { prompt } : {}),
285
295
  ...(frame ? { first_frame_path: frame.absPath } : {}),
@@ -287,7 +297,7 @@ async function submitCoreVideo(routes, frame, prompt, args, signal) {
287
297
  size: optionalActionText(args.size),
288
298
  duration: args.duration
289
299
  }, { allowVideoPaths: true });
290
- const submitted = await submitProviderTaskForDsh({
300
+ const submitted = await core.submitProviderTaskForDsh({
291
301
  capability: 'video',
292
302
  candidates: videoTaskCandidates(routes.map((route) => ({
293
303
  adapter: route.adapter,
@@ -310,7 +320,7 @@ async function submitCoreVideo(routes, frame, prompt, args, signal) {
310
320
  if (!selected) throw taskActionError(task.id, 'Core Task 的供应商身份无法映射回当前路由', task);
311
321
  if (task.acceptance === 'accepted' && task.remoteTaskId
312
322
  && ['none', 'unknown'].includes(task.outcome) && task.phase !== 'terminal') {
313
- await watchProviderTaskForDsh({ taskId: task.id, signal });
323
+ await core.watchProviderTaskForDsh({ taskId: task.id, signal });
314
324
  } else if (task.outcome !== 'succeeded' || task.deliveryState !== 'ready' || !task.artifactIds.length) {
315
325
  throw taskActionError(task.id, '视频路由没有得到可观察任务或可交付产物;事实已保留在 Core,未回退重提', task);
316
326
  }
@@ -472,24 +482,34 @@ export function listActions() {
472
482
  }
473
483
 
474
484
  /** 执行一个动作 */
475
- export async function runAction(host, name, args, { signal } = {}) {
485
+ export async function runAction(host, name, args, { signal, core } = {}) {
476
486
  const fn = actions[name];
477
- if (!fn) throw new Error(`iris: 未知动作 "${name}"`);
487
+ if (!fn) throw new Error('iris: 未知动作 "' + name + '"');
478
488
  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();
489
+ const owned = structuredClone(args || {});
490
+ return core.runHostOperation(operationSignal => {
491
+ const execute = async () => {
492
+ if (AUTO_DIRECTORY_ACTIONS.has(name) && !owned.model) await store.refreshAutomaticDirectories({
493
+ capability: ({ image: 'image-gen', image_edit: 'image-gen', video: 'video-gen', music: 'music-gen', tts: 'tts', transcribe: 'transcribe' })[name] || 'vision', signal: operationSignal
494
+ });
495
+ operationSignal.throwIfAborted();
496
+ return fn(adapter, owned, { signal: operationSignal, core });
497
+ };
498
+ return PROVIDER_ACTIONS.has(name) ? withDshProviderConfig(adapter, execute, { signal: operationSignal }) : execute();
499
+ }, { signal }).catch(error => {
500
+ // Ownership can stop the request before it enters the Model Port's error boundary.
501
+ if (VISION_ACTIONS.has(name) && signal?.aborted && error === signal.reason) {
502
+ throw new ModelPortError('IRIS_MODEL_ABORTED');
503
+ }
504
+ throw error;
505
+ });
486
506
  }
487
507
 
488
508
  /* ============ 动作实现 ============ */
489
509
 
490
- register('image_import', async (_host, args, { signal }) => {
510
+ register('image_import', async (_host, args, { signal, core }) => {
491
511
  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 });
512
+ const { artifact } = await core.importImageForDsh({ imagePath: args.image_path, imageBytes: args.image_bytes, signal });
493
513
  const imageUrl = '/iris/api/core/artifact/' + artifact.id + '/media';
494
514
  const text = `已导入图片(${artifact.metadata.width}×${artifact.metadata.height}),原文件保留。\n作品ID:${artifact.id}`;
495
515
  return { ok: true, storage: 'core', artifact, artifactId: artifact.id, artifactIds: [artifact.id], imageUrl, text,
@@ -497,16 +517,27 @@ register('image_import', async (_host, args, { signal }) => {
497
517
  });
498
518
 
499
519
  /* ---------- 🖼️ 画图 ---------- */
500
- async function imageAction(host, args, { signal }, editing = false) {
520
+ async function imageAction(host, args, { signal, core }, editing = false) {
501
521
  if (editing && !args.source_artifact_id) throw new Error('iris: 改图必须提供 source_artifact_id');
502
522
  const { providerInput, sourceArtifactId } = normalizeGenerationInput('image', {
503
523
  prompt: args.prompt, size: optionalActionText(args.size), n: args.n,
524
+ ...(args.workflow_parameters !== undefined ? { model_ref: args.model, workflow_parameters: args.workflow_parameters } : {}),
504
525
  ...(editing ? { source_artifact_id: args.source_artifact_id } : {})
505
526
  });
506
527
 
507
528
  const imageProviders = selectProviders(cap.CAPABILITIES.IMAGE, '图像', args.model);
508
529
  const routes = imageProviders.map((provider) => imageRoute(provider, args));
509
- const submitted = await submitCoreImage(routes, providerInput, signal, sourceArtifactId);
530
+ if (args.workflow_sha !== undefined && (typeof args.workflow_sha !== 'string' || routes.length !== 1
531
+ || projectComfyWorkflow(routes[0].provider.comfyWorkflow)?.sha256 !== args.workflow_sha)) {
532
+ throw Object.assign(new Error('工作流已变化,请重新选择并核对参数'), { code: 'IRIS_CONFIG_CHANGED' });
533
+ }
534
+ if (providerInput.workflow_parameters !== undefined) {
535
+ if (!models.parseModelRef(args.model) || routes.length !== 1 || routes[0].protocol !== 'comfyui') {
536
+ throw new TypeError('工作流参数需要明确选择一个 ComfyUI 工作流');
537
+ }
538
+ compileComfyWorkflow(routes[0].provider.comfyWorkflow, providerInput);
539
+ }
540
+ const submitted = await submitCoreImage(core, routes, providerInput, signal, sourceArtifactId);
510
541
  const firstArtifactId = submitted.task.artifactIds[0];
511
542
  const ready = submitted.task.outcome === 'succeeded'
512
543
  && submitted.task.deliveryState === 'ready' && Boolean(firstArtifactId);
@@ -525,7 +556,7 @@ register('image', imageAction);
525
556
  register('image_edit', (host, args, context) => imageAction(host, args, context, true));
526
557
 
527
558
  /* ---------- 🎬 视频生成 ---------- */
528
- register('video', async (host, args, { signal }) => {
559
+ register('video', async (host, args, { signal, core }) => {
529
560
  const prompt = String(args.prompt || '').trim();
530
561
  const frame = await resolveFirstFrameFile(host, args, signal);
531
562
  const providers = selectProviders(cap.CAPABILITIES.VIDEO, '视频', args.model);
@@ -545,7 +576,7 @@ register('video', async (host, args, { signal }) => {
545
576
  if (!coreRoutes.length) {
546
577
  throw new Error(s2v ? 'iris: 未配置可用 s2v 数字人模型' : 'iris: 当前视频候选只有 s2v 数字人模型;请使用 audio_path 的数字人入口');
547
578
  }
548
- const submitted = await submitCoreVideo(coreRoutes, frame, prompt, args, signal);
579
+ const submitted = await submitCoreVideo(core, coreRoutes, frame, prompt, args, signal);
549
580
  const firstArtifactId = submitted.task.artifactIds[0];
550
581
  const ready = submitted.task.outcome === 'succeeded'
551
582
  && submitted.task.deliveryState === 'ready' && Boolean(firstArtifactId);
@@ -565,12 +596,12 @@ register('video', async (host, args, { signal }) => {
565
596
  * 直接交付 generated-audio Artifact(无 remote 长轮询)。整条候选链共用一个
566
597
  * voice(Core providerInput 静态;legacy 曾按候选各自取 provider.ttsVoice)。
567
598
  */
568
- async function submitCoreTts(routes, normalizedInput, signal) {
599
+ async function submitCoreTts(core, routes, normalizedInput, signal) {
569
600
  const voice = normalizedInput.voice || String(routes[0].provider.ttsVoice || '').trim();
570
601
  const { providerInput } = normalizeGenerationInput('tts', {
571
602
  text: normalizedInput.text, ...(voice ? { voice } : {})
572
603
  });
573
- const submitted = await submitProviderTaskForDsh({
604
+ const submitted = await core.submitProviderTaskForDsh({
574
605
  capability: 'tts',
575
606
  candidates: routes.map((route) => ({
576
607
  adapter: route.adapter,
@@ -598,7 +629,7 @@ async function submitCoreTts(routes, normalizedInput, signal) {
598
629
  }
599
630
 
600
631
  /* ---------- 🔊 语音合成 ---------- */
601
- register('music', async (host, args, { signal }) => {
632
+ register('music', async (host, args, { signal, core }) => {
602
633
  const normalized = normalizeGenerationInput('music', {
603
634
  prompt: args.prompt,
604
635
  ...(optionalActionText(args.timeout_ms) !== undefined ? { timeout_ms: args.timeout_ms } : {}),
@@ -606,7 +637,7 @@ register('music', async (host, args, { signal }) => {
606
637
  ...(optionalActionText(args.retry_delay_ms) !== undefined ? { retry_delay_ms: args.retry_delay_ms } : {})
607
638
  });
608
639
  const routes = musicCandidatesFromCatalog({ providers: store.runtimeProviders(), assignments: store.assignments() }, args.model);
609
- const submitted = await submitMusicForDsh({
640
+ const submitted = await core.submitMusicForDsh({
610
641
  candidates: routes.map(route => ({ adapter: createConfiguredProviderAdapter(route.provider), model: route.modelRef,
611
642
  selectionReason: route.selectionReason, providerBinding: providerTaskBinding(route.provider) })),
612
643
  providerInput: normalized.providerInput, musicOptions: normalized.musicOptions, signal
@@ -625,13 +656,13 @@ register('music', async (host, args, { signal }) => {
625
656
  text: `[iris] 音乐已生成(${task.modelRef},重试${submitted.retries}次,Core task: ${task.id})` };
626
657
  });
627
658
 
628
- register('tts', async (host, args, { signal }) => {
659
+ register('tts', async (host, args, { signal, core }) => {
629
660
  const { providerInput } = normalizeGenerationInput('tts', {
630
661
  text: args.text, voice: optionalActionText(args.voice)
631
662
  });
632
663
  const providers = selectProviders(cap.CAPABILITIES.TTS, '语音', args.model);
633
664
  const routes = providers.map((provider) => simpleModelRoute(provider, args, cap.CAPABILITIES.TTS));
634
- const submitted = await submitCoreTts(routes, providerInput, signal);
665
+ const submitted = await submitCoreTts(core, routes, providerInput, signal);
635
666
  const firstArtifactId = submitted.task.artifactIds[0];
636
667
  return {
637
668
  ok: true, storage: 'core', taskId: submitted.task.id, model: submitted.model,
@@ -648,7 +679,7 @@ register('tts', async (host, args, { signal }) => {
648
679
  * 与人工控制面复用同一 Task/Attempt 轴;正文物化为 text/plain transcript Artifact。
649
680
  * 已知边界:临时音频 URL 属于首选候选的账号,failover 到不同账号时如实记录失败。
650
681
  */
651
- async function submitCoreTranscribe(routes, normalized, signal) {
682
+ async function submitCoreTranscribe(core, routes, normalized, signal) {
652
683
  let providerInput = normalized.providerInput;
653
684
  if (normalized.audioPath) {
654
685
  const prepared = await prepareProviderInput(routes[0].adapter, {
@@ -656,7 +687,7 @@ async function submitCoreTranscribe(routes, normalized, signal) {
656
687
  });
657
688
  providerInput = normalizeGenerationInput('transcribe', { audio_url: prepared.url }).providerInput;
658
689
  }
659
- const submitted = await submitProviderTaskForDsh({
690
+ const submitted = await core.submitProviderTaskForDsh({
660
691
  capability: 'transcribe',
661
692
  candidates: routes.map((route) => ({
662
693
  adapter: route.adapter,
@@ -679,7 +710,7 @@ async function submitCoreTranscribe(routes, normalized, signal) {
679
710
  if (!selected) throw taskActionError(task.id, 'Core Task 的供应商身份无法映射回当前路由', task);
680
711
  if (task.acceptance === 'accepted' && task.remoteTaskId
681
712
  && ['none', 'unknown'].includes(task.outcome) && task.phase !== 'terminal') {
682
- await watchProviderTaskForDsh({ taskId: task.id, signal });
713
+ await core.watchProviderTaskForDsh({ taskId: task.id, signal });
683
714
  } else if (task.outcome !== 'succeeded' || task.deliveryState !== 'ready' || !task.artifactIds.length) {
684
715
  throw taskActionError(task.id, '转写路由没有得到可观察任务或可交付产物;事实已保留在 Core,未回退重提', task);
685
716
  }
@@ -687,7 +718,7 @@ async function submitCoreTranscribe(routes, normalized, signal) {
687
718
  }
688
719
 
689
720
  /* ---------- 👁 看图 ---------- */
690
- register('look', async (host, args, { signal }) => {
721
+ register('look', async (host, args, { signal, core }) => {
691
722
  const source = normalizeVisionImageSource(args);
692
723
  const p = String(args.image_path || '').trim();
693
724
  const mt = MEDIA_TYPES[path.extname(p).toLowerCase()];
@@ -699,18 +730,18 @@ register('look', async (host, args, { signal }) => {
699
730
  const { answer, via, model } = await askSingleVisionWithHealth(host, { model: args.model,
700
731
  question: String(args.question || '').trim() || '详细描述这张图片。',
701
732
  prepareImage: operationSignal => source === 'artifact_id'
702
- ? visionImageForDsh({ artifactId: args.artifact_id, signal: operationSignal }) : readVisionImageFile(p, mt, operationSignal), signal });
733
+ ? core.visionImageForDsh({ artifactId: args.artifact_id, signal: operationSignal }) : readVisionImageFile(p, mt, operationSignal), signal });
703
734
  const viaLabel = via === 'selfstack' ? 'iris 自持栈' : 'DSH 全局视觉模型';
704
735
  return { ok: true, ...(args.artifact_id ? { artifactId: args.artifact_id } : {}),
705
736
  text: `[iris] 看图回答(${model} · ${viaLabel}):\n${answer}` };
706
737
  });
707
738
 
708
739
  /* ---------- ✂️ 裁剪 ---------- */
709
- register('crop', async (host, args, { signal }) => {
740
+ register('crop', async (host, args, { signal, core }) => {
710
741
  const p = String(args.image_path || '').trim();
711
742
  if (!p || !path.isAbsolute(p)) throw new Error('iris: image_path 必须是绝对路径');
712
743
  if (!fs.existsSync(p)) throw new Error('iris: 图片不存在');
713
- const result = await cropForDsh({
744
+ const result = await core.cropForDsh({
714
745
  imagePath: p, left: args.left, top: args.top, width: args.width, height: args.height, signal
715
746
  });
716
747
  const b64 = result.bytes.toString('base64');
@@ -718,9 +749,9 @@ register('crop', async (host, args, { signal }) => {
718
749
  });
719
750
 
720
751
  /* ---------- 📷 像素 diff ---------- */
721
- register('diff', async (host, args, { signal }) => {
752
+ register('diff', async (host, args, { signal, core }) => {
722
753
  if (!args.image_a_path || !args.image_b_path) throw new Error('iris: 需要 image_a_path 和 image_b_path');
723
- const result = await diffForDsh({
754
+ const result = await core.diffForDsh({
724
755
  imageA: { imagePath: args.image_a_path }, imageB: { imagePath: args.image_b_path },
725
756
  grid: args.grid, topRegions: args.top_regions, signal
726
757
  });
@@ -736,7 +767,7 @@ register('diff', async (host, args, { signal }) => {
736
767
  });
737
768
 
738
769
  /* ---------- 📍 定位 ---------- */
739
- register('locate', async (host, args, { signal }) => {
770
+ register('locate', async (host, args, { signal, core }) => {
740
771
  const source = normalizeVisionImageSource(args);
741
772
  const p = String(args.image_path || '').trim();
742
773
  if (source === 'image_path' && (!p || !path.isAbsolute(p))) throw new Error('iris: image_path 必须是绝对路径');
@@ -744,25 +775,25 @@ register('locate', async (host, args, { signal }) => {
744
775
  const r = await runLocateRequest(host, { providers: store.pickAllFor(cap.CAPABILITIES.VISION), onRateLimit: store.recordRateLimit,
745
776
  onModelAvailability: store.recordAvailability, model: args.model,
746
777
  target: args.target, prepareImage: operationSignal => source === 'artifact_id'
747
- ? visionImageForDsh({ artifactId: args.artifact_id, signal: operationSignal }) : readVisionImageFile(p, mt, operationSignal), signal });
778
+ ? core.visionImageForDsh({ artifactId: args.artifact_id, signal: operationSignal }) : readVisionImageFile(p, mt, operationSignal), signal });
748
779
  const { width, height } = r;
749
780
  const provenance = args.artifact_id ? { artifactId: args.artifact_id } : {};
750
781
  if (!r.found) return { ok: true, ...provenance, text: `[iris] 在图片中未找到「${args.target}」` };
751
782
  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
783
  });
753
784
  /* ---------- 🖼️ HTML 截图 ---------- */
754
- register('html', async (host, args, { signal }) => {
785
+ register('html', async (host, args, { signal, core }) => {
755
786
  const html = String(args.html || '').trim();
756
787
  if (!html) throw new Error('iris: html 不能为空');
757
788
  const browser = requireHostPort(host, 'browser', 'HTML 截图');
758
- const rendered = await htmlForDsh({ browser, html, width: args.width, height: args.height,
789
+ const rendered = await core.htmlForDsh({ browser, html, width: args.width, height: args.height,
759
790
  fullPage: args.fullPage !== false, signal });
760
791
  const png = Buffer.from(rendered.bytes);
761
792
  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
793
  });
763
794
 
764
795
  /* ---------- 📄 长截图 OCR ---------- */
765
- register('ocr', async (host, args, { signal }) => {
796
+ register('ocr', async (host, args, { signal, core }) => {
766
797
  const source = normalizeVisionImageSource(args);
767
798
  const p = String(args.image_path || '').trim();
768
799
  const mediaType = MEDIA_TYPES[path.extname(p).toLowerCase()];
@@ -773,7 +804,7 @@ register('ocr', async (host, args, { signal }) => {
773
804
  const result = await runOcrRequest(host, { providers: store.pickAllFor(cap.CAPABILITIES.VISION),
774
805
  onRateLimit: store.recordRateLimit, onModelAvailability: store.recordAvailability,
775
806
  prepareImage: operationSignal => source === 'artifact_id'
776
- ? visionImageForDsh({ artifactId: args.artifact_id, signal: operationSignal }) : readVisionImageFile(p, mediaType, operationSignal),
807
+ ? core.visionImageForDsh({ artifactId: args.artifact_id, signal: operationSignal }) : readVisionImageFile(p, mediaType, operationSignal),
777
808
  chunkHeight: args.chunk_height, overlap: args.overlap, signal });
778
809
  return { ok: result.status !== 'failed', status: result.status,
779
810
  totalChunks: result.totalChunks, successfulChunks: result.successfulChunks,
@@ -782,7 +813,7 @@ register('ocr', async (host, args, { signal }) => {
782
813
  });
783
814
 
784
815
  /* ---------- 📋 任务查询 ---------- */
785
- register('status', async (host, args, { signal }) => {
816
+ register('status', async (host, args, { signal, core }) => {
786
817
  const requestedId = String(args.task_id || '').trim();
787
818
  if (requestedId) {
788
819
  const legacy = tasks.get(requestedId);
@@ -797,14 +828,14 @@ register('status', async (host, args, { signal }) => {
797
828
  }
798
829
  if (/^task_[a-f0-9]{24}$/.test(requestedId)) {
799
830
  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'));
831
+ const coreTask = await core.inspectProviderTaskForDsh(requestedId, { signal });
832
+ const lines = [`[${coreTask.capability}] ${coreTask.id} · Core ${coreTask.phase} · ${coreTask.outcome} / ${coreTask.deliveryState} · ${coreTask.modelRef || '未选择模型'}`];
833
+ if (coreTask.lastError) lines.push('error: ' + coreTask.lastError.safeMessage);
834
+ if (coreTask.remoteTaskId) lines.push('remoteTask: ' + coreTask.remoteTaskId);
835
+ if (coreTask.artifactIds.length) {
836
+ lines.push('artifacts:\n' + coreTask.artifactIds.map((id) => id + ' · /iris/api/core/artifact/' + encodeURIComponent(id) + '/media').join('\n'));
806
837
  }
807
- return { ok: true, storage: 'core', taskId: core.id, artifactIds: core.artifactIds, text: lines.join('\n') };
838
+ return { ok: true, storage: 'core', taskId: coreTask.id, artifactIds: coreTask.artifactIds, text: lines.join('\n') };
808
839
  } catch (_) { /* 对外统一为不存在,不暴露 Core 数据根 */ }
809
840
  }
810
841
  throw new Error('iris: 没有这个任务:' + args.task_id);
@@ -816,7 +847,7 @@ register('status', async (host, args, { signal }) => {
816
847
  }));
817
848
  let coreRows = [];
818
849
  try {
819
- const snapshot = await coreSnapshotForDsh({ limit: 5, signal });
850
+ const snapshot = await core.coreSnapshotForDsh({ limit: 5, signal });
820
851
  coreRows = (snapshot.tasks.recent || []).map((task) => ({
821
852
  at: task.updatedAt,
822
853
  text: `[${task.capability}] ${task.id} · Core ${task.phase} · ${task.outcome} / ${task.deliveryState} · ${task.modelRef || '未选择模型'}`
@@ -843,7 +874,7 @@ register('task_reobserve', async (host, args) => {
843
874
  return { ok: true, taskId: id, text: '[iris] 已重新观察远端任务;不会重新提交生成请求。' };
844
875
  });
845
876
 
846
- register('task_redeliver', async (host, args, { signal }) => {
877
+ register('task_redeliver', async (host, args, { signal, core }) => {
847
878
  const id = String(args.task_id || '').trim();
848
879
  const task = tasks.get(id);
849
880
  if (!task || !tasks.isV2Task(task)) throw new Error('iris: 找不到可重新交付的 Task v2');
@@ -885,7 +916,7 @@ register('task_restore_attention', async (_host, args) => {
885
916
  return { ok: true, taskId: id, text: '[iris] 已恢复任务提醒。' };
886
917
  });
887
918
 
888
- register('task_manual_retry', async (host, args, { signal }) => {
919
+ register('task_manual_retry', async (host, args, { signal, core }) => {
889
920
  const id = String(args.task_id || '').trim();
890
921
  if (args.confirm_duplicate_charge !== true) throw new Error('iris: 必须明确确认本次操作可能产生重复任务或重复费用');
891
922
  const task = tasks.get(id);
@@ -909,7 +940,7 @@ register('task_manual_retry', async (host, args, { signal }) => {
909
940
  if (task.modelRef) retryArgs.model = task.modelRef;
910
941
  retryArgs._retryOf = id;
911
942
  try {
912
- const result = await runAction(host, action, retryArgs, { signal });
943
+ const result = await runAction(host, action, retryArgs, { signal, core });
913
944
  if (result.taskId) tasks.linkManualRetry(id, result.taskId);
914
945
  return { ...result, retryOf: id, text: (result.text || '[iris] 已创建新任务')
915
946
  + '\n[iris] 原任务提醒已归档,并保留新旧任务关联;本次新任务可能与远端未知任务重复。' };
@@ -1022,7 +1053,7 @@ register('tasks_purge_orphans', async () => {
1022
1053
  });
1023
1054
 
1024
1055
  /* ---------- 👁 重看(GUI:只查 iris 自己的任务产物,无会话上下文) ---------- */
1025
- register('relook', async (host, args, { signal }) => {
1056
+ register('relook', async (host, args, { signal, core }) => {
1026
1057
  const id = String(args.attachment_id || '').trim();
1027
1058
  if (!id) throw new Error('iris: attachment_id 不能为空');
1028
1059
  const question = String(args.question || '').trim();
@@ -1080,6 +1111,7 @@ register('providers_list', async (host, args) => {
1080
1111
  ...(m.directoryOnly ? { directoryOnly: true } : {}),
1081
1112
  ...(m.catalogStatus ? { catalogStatus: m.catalogStatus } : {}),
1082
1113
  ...(m.imageProtocol !== undefined ? { imageProtocol: m.imageProtocol } : {}),
1114
+ ...comfySummaryForModel(p, m.id),
1083
1115
  ...(m.musicProtocol !== undefined ? { musicProtocol: m.musicProtocol } : {}),
1084
1116
  ...(m.visionInput !== undefined ? { visionInput: m.visionInput } : {}),
1085
1117
  ...(m.capabilities.includes(cap.CAPABILITIES.VISION) ? { visionInputEffective: visionInputLimits(p, m.id) } : {}),
@@ -1101,6 +1133,34 @@ register('providers_host_connections', async host => {
1101
1133
  return { ok: true, connections: await host.ports.providerConnections.list() };
1102
1134
  });
1103
1135
 
1136
+ /** Explicit template save; no provider request or model execution. */
1137
+ register('providers_save_comfy_workflow', async (_host, args) => {
1138
+ if (Object.keys(args).some(key => !['id', 'model_id', 'workflow', 'expectedSha', 'enableAutomatic'].includes(key))) throw new TypeError('工作流保存包含不支持的字段');
1139
+ const entry = store.setComfyWorkflow(args.id, args.model_id, args.workflow, {
1140
+ expectedSha: args.expectedSha, enableAutomatic: args.enableAutomatic
1141
+ });
1142
+ return { ok: true, modelId: entry.id, comfyWorkflow: projectComfyWorkflow(entry.comfyWorkflow),
1143
+ text: '工作流已保存;未执行生成' + (args.enableAutomatic ? ',图片功能已使用自动池' : ',功能池设置保持') };
1144
+ });
1145
+
1146
+ /** Uses only an already saved connection. /object_info never enqueues a prompt. */
1147
+ register('providers_check_comfy_workflow', async (_host, args, { signal }) => {
1148
+ if (Object.keys(args).some(key => !['id', 'model_id', 'workflow', 'workflow_parameters'].includes(key))
1149
+ || (args.workflow !== undefined) === (args.model_id !== undefined)) throw new TypeError('请选择已保存工作流或导入一份草稿进行检查');
1150
+ const provider = store.providerById(args.id);
1151
+ if (!provider) throw new TypeError('请先保存并启用 ComfyUI 连接');
1152
+ const route = args.workflow !== undefined ? provider : providerForImageModel(provider, args.model_id);
1153
+ if (route.mediaProtocol !== 'comfyui') throw new TypeError('此检查仅用于 ComfyUI 连接');
1154
+ const file = path.join(store.irisHome(), 'providers.json'), before = fs.readFileSync(file);
1155
+ const workflow = compileComfyWorkflow(args.workflow !== undefined ? args.workflow : route.comfyWorkflow,
1156
+ { workflow_parameters: args.workflow_parameters });
1157
+ const report = await inspectComfyWorkflow(createComfyUiClient(route), workflow, { signal });
1158
+ signal?.throwIfAborted();
1159
+ if (!fs.readFileSync(file).equals(before)) throw Object.assign(new Error('检查期间配置已变化,请重新检查'), { code: 'IRIS_CONFIG_CHANGED' });
1160
+ const key = route.apiKey;
1161
+ return { ok: true, report: key ? JSON.parse(JSON.stringify(report).split(key).join('[redacted]')) : report };
1162
+ });
1163
+
1104
1164
  /** 新增/更新供应商 */
1105
1165
  register('providers_upsert', async (host, args) => {
1106
1166
  const id = args.id || undefined;
@@ -1110,6 +1170,7 @@ register('providers_upsert', async (host, args) => {
1110
1170
  for (const key of allowed) {
1111
1171
  if (Object.prototype.hasOwnProperty.call(args, key)) patch[key] = args[key];
1112
1172
  }
1173
+ if (Object.hasOwn(patch, 'auth') && !['none', 'bearer'].includes(patch.auth)) throw new TypeError('认证方式必须为 bearer 或 none');
1113
1174
  if (Object.hasOwn(args, 'dshProvider')) {
1114
1175
  patch.dshProvider = normalizeDshProvider(args.dshProvider);
1115
1176
  const source = requireHostPort(host, 'providerConnections', '复用 DSH 供应商连接');
@@ -1142,7 +1203,7 @@ register('providers_set_models', async (host, args) => {
1142
1203
  * 模型发现:读取供应商目录 → 明确能力元数据 → 合并模型池。
1143
1204
  * 免费只读;拉不到/不支持则原样保留现有池(优雅降级),报人话错误。
1144
1205
  */
1145
- register('providers_discover', async (host, args, { signal }) => {
1206
+ register('providers_discover', async (host, args, { signal, core }) => {
1146
1207
  const p = store.providerById(String(args.id || ''));
1147
1208
  if (!p) throw new Error('iris: 供应商不存在或未启用');
1148
1209
  const [updated] = await store.refreshProviderDirectories({ providerId: p.id, force: true, signal });
@@ -1155,7 +1216,7 @@ register('providers_discover', async (host, args, { signal }) => {
1155
1216
  });
1156
1217
 
1157
1218
  /** 只更新目录快照;新增项在管理视图待确认,不改模型用途与功能名单。 */
1158
- register('providers_sync_directory', async (host, args, { signal }) => {
1219
+ register('providers_sync_directory', async (host, args, { signal, core }) => {
1159
1220
  const id = String(args.id || '').trim();
1160
1221
  if (!store.providerById(id)) throw new Error('iris: 供应商不存在或连接未就绪');
1161
1222
  const [result] = await store.refreshProviderDirectories({ providerId: id, force: args.force === true, signal });
@@ -1167,7 +1228,7 @@ register('providers_sync_directory', async (host, args, { signal }) => {
1167
1228
  });
1168
1229
 
1169
1230
  /** 能力实测(红色测试图):对某供应商的 vision 模型跑 testVisionCapability */
1170
- register('providers_test_vision', async (host, args, { signal }) => {
1231
+ register('providers_test_vision', async (host, args, { signal, core }) => {
1171
1232
  const p = store.providerById(String(args.id || ''));
1172
1233
  if (!p) throw new Error('iris: 供应商不存在');
1173
1234
  const vision = models.providerModels(p).find(m => m.capabilities.includes(cap.CAPABILITIES.VISION));
@@ -1224,6 +1285,7 @@ async function probeModel(host, p, modelId, capability, signal) {
1224
1285
  }
1225
1286
  if (capability === cap.CAPABILITIES.IMAGE) {
1226
1287
  p = providerForImageModel(p, modelId);
1288
+ if (p.mediaProtocol === 'comfyui') return { skipped: true, ok: false, note: 'ComfyUI 请在画图表单选择工作流验证,以持久 Task 保留受理与结果;只读工作流检查不生成图片' };
1227
1289
  const providerAdapter = createConfiguredProviderAdapter(p, { allowRateLimited: true, modelId });
1228
1290
  const submitted = await invokeProviderOperation(providerAdapter, 'submit', {
1229
1291
  capability: 'image', model: modelId,
@@ -1329,7 +1391,7 @@ register('providers_set_model_vision_input', async (host, args) => {
1329
1391
  });
1330
1392
 
1331
1393
  /** 逐模型实测某能力 → 存 verified("test it, don't guess it";用户手动触发,发现不自动跑) */
1332
- register('providers_test_model', async (host, args, { signal }) => {
1394
+ register('providers_test_model', async (host, args, { signal, core }) => {
1333
1395
  const p = store.providerById(String(args.id || ''));
1334
1396
  if (!p) throw new Error('iris: 供应商不存在或未启用');
1335
1397
  const modelId = String(args.model_id || '').trim();
@@ -1396,7 +1458,7 @@ register('assignments_get', async (host, args) => {
1396
1458
  ? (c === cap.CAPABILITIES.TTS && selectMediaProtocol(provider).mediaProtocol === 'dashscope'
1397
1459
  && dashscopeTtsProtocolIssue(m.id)) || '当前接口协议不支持此功能'
1398
1460
  : modelUnavailable ? modelUnavailableReason() : m.rateLimited ? '限流冷却或额度不可用,请稍后或检查账号' : m.source === 'discovered' ? '已发现,待确认模型用途' : '',
1399
- ...(c === cap.CAPABILITIES.IMAGE && provider ? { imageRouting: imageRoutingForModel(provider, m.id) } : {}) });
1461
+ ...(c === cap.CAPABILITIES.IMAGE && provider ? { imageRouting: imageRoutingForModel(provider, m.id), ...comfySummaryForModel(provider, m.id) } : {}) });
1400
1462
  }
1401
1463
  }
1402
1464
  // order:归一化的有序分配列表(GUI 直接渲染 failover 顺序,无需处理单字符串旧格式)
@@ -1485,7 +1547,7 @@ register('attachments_list', async (host, args) => {
1485
1547
  * 把一个附件(会话 image 或 iris 产物)导出为宿主可读的文件路径,供工具消费。
1486
1548
  * iris 产物直接返回其 outputs 路径;会话附件读字节存 uploads/(按 attachmentId 哈希缓存,不重复落盘)。
1487
1549
  */
1488
- register('attachment_export', async (host, args, { signal }) => {
1550
+ register('attachment_export', async (host, args, { signal, core }) => {
1489
1551
  const id = String(args.attachment_id || '').trim();
1490
1552
  if (!id) throw new Error('iris: attachment_id 不能为空');
1491
1553
  // 1) iris 自有产物:文件已在 outputs/
@@ -1516,7 +1578,7 @@ register('attachment_export', async (host, args, { signal }) => {
1516
1578
 
1517
1579
  /* ============ 音频转写(阶段 7.2) ============ */
1518
1580
 
1519
- register('transcribe', async (host, args, { signal }) => {
1581
+ register('transcribe', async (host, args, { signal, core }) => {
1520
1582
  const normalized = normalizeGenerationInput('transcribe', {
1521
1583
  audio_url: args.audio_url, audio_path: args.audio_path
1522
1584
  }, { allowAudioPath: true });
@@ -1525,7 +1587,7 @@ register('transcribe', async (host, args, { signal }) => {
1525
1587
  const routes = providers.map((provider) =>
1526
1588
  simpleModelRoute(provider, args, cap.CAPABILITIES.TRANSCRIBE)
1527
1589
  );
1528
- const submitted = await submitCoreTranscribe(routes, normalized, signal);
1590
+ const submitted = await submitCoreTranscribe(core, routes, normalized, signal);
1529
1591
  const firstArtifactId = submitted.task.artifactIds[0];
1530
1592
  const ready = submitted.task.outcome === 'succeeded'
1531
1593
  && submitted.task.deliveryState === 'ready' && Boolean(firstArtifactId);
@@ -1542,8 +1604,8 @@ register('transcribe', async (host, args, { signal }) => {
1542
1604
 
1543
1605
  /* ============ 视频抽帧(阶段 7.1) ============ */
1544
1606
 
1545
- register('video_frames', async (host, args, { signal }) => {
1546
- const { media: meta, frames } = await framesForDsh({
1607
+ register('video_frames', async (host, args, { signal, core }) => {
1608
+ const { media: meta, frames } = await core.framesForDsh({
1547
1609
  videoPath: args.video_path, maxFrames: args.max_frames, targetWidth: args.target_width,
1548
1610
  format: optionalActionText(args.format), quality: args.quality, signal
1549
1611
  });
@@ -1563,7 +1625,7 @@ register('video_frames', async (host, args, { signal }) => {
1563
1625
 
1564
1626
  /* ============ 多模态视频摘要(阶段 7.3,GUI 同步快速版) ============ */
1565
1627
 
1566
- register('media_summarize', async (host, args, { signal }) => {
1628
+ register('media_summarize', async (host, args, { signal, core }) => {
1567
1629
  const source = normalizeSummarySource(args);
1568
1630
  const p = String(args.video_path || '').trim();
1569
1631
  if (source === 'video') {
@@ -1576,7 +1638,7 @@ register('media_summarize', async (host, args, { signal }) => {
1576
1638
  providers: store.pickAllFor(cap.CAPABILITIES.VISION), model: args.model, question: args.question, signal,
1577
1639
  async prepareMedia(operationSignal) {
1578
1640
  if (source === 'core-artifacts') {
1579
- return { ...await summaryFramesForDsh({ frameArtifactIds: args.frame_artifact_ids, signal: operationSignal }), transcript: args.transcribe_text };
1641
+ return { ...await core.summaryFramesForDsh({ frameArtifactIds: args.frame_artifact_ids, signal: operationSignal }), transcript: args.transcribe_text };
1580
1642
  }
1581
1643
  const meta = probeVideo(p);
1582
1644
  const frames = await extractFrames({ inputPath: p, maxFrames: Math.min(Number(args.max_frames) || 8, 12),