@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.
- package/CHANGELOG.md +103 -1
- package/README.en.md +104 -304
- package/README.md +101 -302
- package/bin/dsh-iris.js +446 -64
- package/docs/AGENT_CORE_QUICKSTART.md +64 -0
- package/docs/AGENT_QUICKSTART.md +7 -5
- package/docs/ARCHITECTURE.md +20 -4
- package/docs/AUDIO_FLOWS.md +71 -0
- package/docs/CLI_MANAGEMENT.md +1 -1
- package/docs/CONTACT_SHEET.md +62 -0
- package/docs/CORE_CONNECTIONS.md +99 -0
- package/docs/CORE_QUICKSTART.md +108 -0
- package/docs/CORE_RUNTIME_CONTRACT.md +41 -5
- package/docs/CORE_SERVICE.md +97 -0
- package/docs/CORE_SKILLS.md +94 -0
- package/docs/DSH_COMPATIBILITY.md +14 -4
- package/docs/DSH_CORE_MIGRATION.md +4 -2
- package/docs/DSH_PROVIDER_CONNECTIONS.md +1 -1
- package/docs/DSH_RC2_ADAPTATION.md +1 -1
- package/docs/DSH_RUNTIME.md +40 -0
- package/docs/FUNCTION_MODEL_POOLS.md +1 -1
- package/docs/HEADLESS_CLI.md +12 -4
- package/docs/HOST_DOCTOR.md +13 -1
- package/docs/IMAGE_FLOW.md +75 -0
- package/docs/IMAGE_GENERATION.md +127 -0
- package/docs/LOCAL_API.md +62 -0
- package/docs/LOCAL_API_CONTRACT.md +140 -0
- package/docs/LOCAL_CREATION.md +56 -0
- package/docs/LOCAL_OPERATIONS.md +46 -0
- package/docs/LOCAL_SELECTION.md +57 -0
- package/docs/LOCAL_WEB.md +201 -0
- package/docs/MCP.md +191 -0
- package/docs/MUSIC.md +2 -0
- package/docs/PANEL_EXTENSIONS.md +64 -0
- package/docs/PLUGINS.md +189 -0
- package/docs/PLUGIN_STARTUP.md +100 -0
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +1 -1
- package/docs/PROVIDER_EXTENSIONS.md +78 -0
- package/docs/PROVIDER_HEALTH.md +4 -2
- package/docs/PROVIDER_SETTINGS.md +85 -0
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +6 -0
- package/docs/ROADMAP.md +28 -6
- package/docs/SECURITY.md +9 -1
- package/docs/SYNC_DELIVERY_RECOVERY.md +2 -0
- package/docs/VISION_CLI.md +2 -2
- package/docs/WEB_PROTOTYPE.md +62 -0
- package/docs/WEB_RECOVERY.md +46 -0
- package/docs/WORKBENCH_LOGIN.md +63 -0
- package/docs/assets/screenshots/core-connections-preview.webp +0 -0
- package/docs/assets/screenshots/core-model-pools-preview.webp +0 -0
- package/docs/assets/screenshots/core-overview-preview.webp +0 -0
- package/docs/assets/screenshots/core-spectrum-preview.webp +0 -0
- package/docs/assets/screenshots/core-studio-dark-preview.webp +0 -0
- package/docs/assets/screenshots/core-studio-mobile-preview.webp +0 -0
- package/docs/assets/screenshots/core-studio-preview.webp +0 -0
- package/docs/releases/0.2.7.md +62 -0
- package/docs/releases/0.3.0-alpha.1.md +51 -0
- package/docs/screenshots.md +41 -1
- package/lib/actions.js +132 -70
- package/lib/adapters.js +20 -11
- package/lib/api.js +38 -38
- package/lib/application-runtime.js +213 -0
- package/lib/audio-flow-input.js +37 -0
- package/lib/client.js +434 -55
- package/lib/comfyui-adapter.js +94 -0
- package/lib/comfyui-client.js +51 -0
- package/lib/comfyui-inspection.js +73 -0
- package/lib/comfyui-workflow.js +94 -0
- package/lib/config.js +36 -1
- package/lib/configured-image-runtime.js +50 -0
- package/lib/connection-skill.js +89 -0
- package/lib/connection-template.js +23 -0
- package/lib/core-artifact-store.js +21 -2
- package/lib/core-bridge-selection.js +31 -0
- package/lib/core-bridge-tools.js +83 -0
- package/lib/core-capability-catalog.js +182 -0
- package/lib/core-connection-diagnostics.js +49 -0
- package/lib/core-connection-store.js +277 -0
- package/lib/core-connections.js +225 -0
- package/lib/core-maintenance.js +31 -2
- package/lib/core-runtime.js +20 -0
- package/lib/core-service-client.js +170 -0
- package/lib/core-tasks.js +84 -13
- package/lib/core-writer-lease.js +16 -0
- package/lib/creation-request-store.js +133 -0
- package/lib/doctor.js +12 -1
- package/lib/dsh-core-adapter.js +666 -572
- package/lib/dsh-host-adapter.js +8 -2
- package/lib/dsh-service-bridge.js +66 -0
- package/lib/execution-ownership.js +43 -0
- package/lib/flow-store.js +290 -0
- package/lib/generation-input.js +10 -1
- package/lib/headless-vision.js +7 -7
- package/lib/image-edit-input.js +1 -1
- package/lib/image-flow.js +232 -0
- package/lib/index.js +94 -68
- package/lib/local-api-projection.js +32 -0
- package/lib/local-api-token-directory.js +31 -0
- package/lib/local-api.js +1083 -0
- package/lib/local-creation.js +150 -0
- package/lib/local-operations.js +36 -0
- package/lib/local-request-root.js +86 -0
- package/lib/local-web-assets.js +37 -0
- package/lib/mcp-bridge.js +58 -0
- package/lib/mcp-check.js +26 -0
- package/lib/mcp-config.js +42 -0
- package/lib/mcp-delivery.js +46 -0
- package/lib/mcp-http.js +101 -0
- package/lib/mcp-server.js +47 -0
- package/lib/model-directory-service.js +16 -6
- package/lib/model-directory.js +3 -3
- package/lib/models.js +2 -2
- package/lib/music-transport.js +2 -1
- package/lib/operation-runner.js +236 -0
- package/lib/operation-store.js +216 -0
- package/lib/panel-extension.js +36 -0
- package/lib/plugin-contract.js +194 -0
- package/lib/plugin-execution.js +51 -0
- package/lib/plugin-launch.js +86 -0
- package/lib/plugin-operation-store.js +101 -0
- package/lib/plugin-operations.js +28 -0
- package/lib/plugin-registry.js +109 -0
- package/lib/plugin-selection.js +15 -0
- package/lib/plugin-source-image.js +5 -0
- package/lib/plugin-web-assets.js +54 -0
- package/lib/plugins.d.ts +288 -0
- package/lib/plugins.js +58 -0
- package/lib/private-storage.js +12 -0
- package/lib/provider-adapter.js +5 -1
- package/lib/provider-adapters.js +56 -18
- package/lib/provider-catalog.js +40 -29
- package/lib/provider-config-service.js +105 -23
- package/lib/provider-diagnostics.js +56 -0
- package/lib/provider-extensions.js +29 -0
- package/lib/provider-health.js +8 -1
- package/lib/provider-monitor.js +79 -0
- package/lib/provider-protocol.js +70 -20
- package/lib/provider-registry.js +118 -0
- package/lib/provider-response.js +28 -0
- package/lib/provider-settings.js +134 -0
- package/lib/provider-task-runner.js +40 -7
- package/lib/render-address.js +35 -0
- package/lib/render.js +52 -13
- package/lib/skill-installation.js +211 -0
- package/lib/vision-model-routing.js +3 -0
- package/lib/web/app.js +642 -0
- package/lib/web/appearance.js +111 -0
- package/lib/web/comfyui.js +146 -0
- package/lib/web/connections.js +377 -0
- package/lib/web/creation.js +355 -0
- package/lib/web/flow-view.js +33 -0
- package/lib/web/gallery.js +132 -0
- package/lib/web/index.html +291 -0
- package/lib/web/media-client.js +204 -0
- package/lib/web/plugin-panel.js +221 -0
- package/lib/web/plugin-surface.css +17 -0
- package/lib/web/providers.js +492 -0
- package/lib/web/recovery.js +105 -0
- package/lib/web/startup.js +18 -0
- package/lib/web/styles.css +518 -0
- package/lib/web/transport.js +96 -0
- package/lib/web/workbench-login.js +165 -0
- package/lib/workbench-artifacts.js +1 -2
- package/lib/workbench-auth-cli.js +47 -0
- package/lib/workbench-auth.js +200 -0
- package/lib/workbench-management.js +8 -9
- package/package.json +22 -6
- package/plugins/contact-sheet/index.d.mts +4 -0
- package/plugins/contact-sheet/index.mjs +33 -0
- package/plugins/contact-sheet/web/board.css +14 -0
- package/plugins/contact-sheet/web/board.mjs +93 -0
- package/skills/iris-core/SKILL.md +44 -0
- package/skills/iris-core/agents/openai.yaml +4 -0
- package/skills/iris-core/references/cli.md +95 -0
- package/skills/iris-core/references/comfyui.md +69 -0
- package/skills/iris-core/references/tasks.md +60 -0
- 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
|
|
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(
|
|
487
|
+
if (!fn) throw new Error('iris: 未知动作 "' + name + '"');
|
|
478
488
|
const adapter = actionHost(host);
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
-
|
|
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
|
|
801
|
-
const lines = [`[${
|
|
802
|
-
if (
|
|
803
|
-
if (
|
|
804
|
-
if (
|
|
805
|
-
lines.push('artifacts:\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:
|
|
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),
|