@mokuyoaxis/dsh-iris 0.2.7 → 0.3.0-beta.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 +143 -1
- package/README.en.md +109 -311
- package/README.md +106 -309
- package/bin/dsh-iris.js +452 -92
- package/docs/AGENT_CORE_QUICKSTART.md +64 -0
- package/docs/AGENT_QUICKSTART.md +2 -0
- package/docs/ARCHITECTURE.md +13 -3
- package/docs/ARTIFACT_MANIFEST.md +4 -0
- package/docs/AUDIO_FLOWS.md +87 -0
- package/docs/BETA_SCOPE.md +42 -0
- package/docs/CONTACT_SHEET.md +62 -0
- package/docs/CORE_CONNECTIONS.md +101 -0
- package/docs/CORE_QUICKSTART.md +110 -0
- package/docs/CORE_RUNTIME_CONTRACT.md +7 -2
- package/docs/CORE_SERVICE.md +101 -0
- package/docs/CORE_SKILLS.md +35 -3
- package/docs/DSH_COMPATIBILITY.md +21 -1
- package/docs/DSH_CORE_MIGRATION.md +6 -0
- package/docs/DSH_RC2_ADAPTATION.md +1 -1
- package/docs/DSH_RUNTIME.md +6 -2
- package/docs/HEADLESS_CLI.md +30 -5
- package/docs/HOST_ADAPTER_CONTRACT.md +5 -3
- package/docs/IMAGE_FLOW.md +4 -0
- package/docs/IMAGE_GENERATION.md +127 -0
- package/docs/LOCAL_API.md +4 -2
- package/docs/LOCAL_API_CONTRACT.md +42 -15
- package/docs/LOCAL_CREATION.md +7 -1
- package/docs/LOCAL_OPERATIONS.md +46 -0
- package/docs/LOCAL_SELECTION.md +57 -0
- package/docs/LOCAL_WEB.md +207 -0
- package/docs/MCP.md +212 -0
- package/docs/MEDIA_SCHEDULING.md +72 -0
- package/docs/MUSIC.md +2 -0
- package/docs/NETWORK_RESOURCES.md +106 -0
- package/docs/NETWORK_ROUTING.md +103 -0
- package/docs/NETWORK_WORKBENCH.md +65 -0
- package/docs/PANEL_EXTENSIONS.md +64 -0
- package/docs/PLUGINS.md +193 -0
- package/docs/PLUGIN_DEVELOPMENT.md +39 -0
- package/docs/PLUGIN_STARTUP.md +113 -0
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +8 -2
- package/docs/PROVIDER_CREDENTIALS.md +67 -0
- package/docs/PROVIDER_EXTENSIONS.md +82 -0
- package/docs/PROVIDER_HEALTH.md +5 -1
- package/docs/PROVIDER_NETWORK.md +45 -0
- package/docs/PROVIDER_PLUGIN_CONTRACT.md +24 -0
- package/docs/PROVIDER_SETTINGS.md +91 -0
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +8 -0
- package/docs/ROADMAP.md +18 -8
- package/docs/SECURITY.md +1 -1
- package/docs/SYNC_DELIVERY_RECOVERY.md +5 -1
- package/docs/VIDEO_FLOWS.md +89 -0
- package/docs/VISION_CLI.md +1 -1
- package/docs/VISION_MODEL.md +2 -0
- package/docs/WEB_PROTOTYPE.md +21 -6
- package/docs/WEB_RECOVERY.md +46 -0
- package/docs/WORKBENCH_ARTIFACTS.md +6 -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.3.0-alpha.1.md +51 -0
- package/docs/releases/0.3.0-beta.1.md +34 -0
- package/docs/screenshots.md +41 -1
- package/lib/actions.js +56 -4
- package/lib/adapters.js +75 -49
- package/lib/api.js +23 -13
- package/lib/application-runtime.js +73 -17
- package/lib/application-writer.js +193 -0
- package/lib/audio-flow-input.js +37 -0
- package/lib/client.d.ts +3 -3
- package/lib/client.js +402 -72
- package/lib/comfyui-adapter.js +95 -0
- package/lib/comfyui-client.js +60 -0
- package/lib/comfyui-inspection.js +73 -0
- package/lib/comfyui-workflow.js +94 -0
- package/lib/command-service.js +18 -5
- package/lib/config.js +49 -10
- package/lib/configured-image-runtime.js +69 -0
- package/lib/connection-skill.js +98 -0
- package/lib/connection-template.js +23 -0
- package/lib/core-artifact-store.js +84 -28
- package/lib/core-bridge-selection.js +31 -0
- package/lib/core-bridge-tools.js +85 -0
- package/lib/core-capability-catalog.js +192 -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-delivery-cache.js +8 -3
- package/lib/core-maintenance.js +9 -3
- package/lib/core-runtime.js +2 -1
- package/lib/core-service-client.js +174 -0
- package/lib/core-tasks.js +113 -21
- package/lib/core-writer-lease.js +16 -0
- package/lib/creation-request-store.js +41 -64
- package/lib/doctor.js +2 -2
- package/lib/dsh-core-adapter.js +61 -9
- package/lib/dsh-host-adapter.js +4 -1
- package/lib/dsh-provider-config.js +1 -1
- package/lib/dsh-service-bridge.js +67 -0
- package/lib/execution-ownership.js +43 -0
- package/lib/flow-store.js +156 -14
- 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 +97 -31
- package/lib/index.js +47 -22
- package/lib/local-api-projection.js +15 -6
- package/lib/local-api-token-directory.js +31 -0
- package/lib/local-api.js +861 -71
- package/lib/local-creation.js +96 -24
- package/lib/local-operations.js +36 -0
- package/lib/local-request-root.js +86 -0
- package/lib/local-web-assets.js +39 -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 +174 -0
- package/lib/mcp-server.js +69 -0
- package/lib/media-scheduler.js +194 -0
- package/lib/model-directory-service.js +37 -10
- package/lib/model-directory.js +3 -3
- package/lib/models.js +2 -2
- package/lib/music-transport.js +6 -3
- package/lib/network-config-service.js +123 -0
- package/lib/network-config.js +127 -0
- package/lib/network-doctor.js +94 -0
- package/lib/network-management.js +51 -0
- package/lib/network-policy.js +19 -0
- package/lib/network-route-evidence.js +38 -0
- package/lib/network-routing.js +283 -0
- package/lib/operation-runner.js +281 -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 +307 -0
- package/lib/plugins.js +59 -0
- package/lib/provider-adapter.js +7 -3
- package/lib/provider-adapters.js +92 -22
- package/lib/provider-catalog.js +64 -36
- package/lib/provider-config-format.js +51 -0
- package/lib/provider-config-service.js +143 -31
- package/lib/provider-context.d.ts +90 -0
- package/lib/provider-context.js +42 -0
- package/lib/provider-credentials.js +97 -0
- package/lib/provider-diagnostics.js +56 -0
- package/lib/provider-extensions.js +29 -0
- package/lib/provider-health.js +18 -6
- package/lib/provider-monitor.js +81 -0
- package/lib/provider-network.js +52 -0
- package/lib/provider-protocol.js +73 -22
- package/lib/provider-registry.js +128 -0
- package/lib/provider-response.js +38 -0
- package/lib/provider-settings.js +136 -0
- package/lib/provider-task-runner.js +132 -43
- package/lib/provider-transport.js +16 -0
- package/lib/render.js +11 -1
- package/lib/scheduling-policy.js +66 -0
- package/lib/skill-installation.js +23 -11
- package/lib/tasks.js +3 -2
- package/lib/video-flow-input.js +16 -0
- package/lib/video-input.js +12 -5
- package/lib/vision-model-routing.js +5 -0
- package/lib/web/app.js +703 -0
- package/lib/web/appearance.js +111 -0
- package/lib/web/comfyui.js +146 -0
- package/lib/web/connections.js +516 -0
- package/lib/web/creation.js +359 -0
- package/lib/web/flow-view.js +34 -0
- package/lib/web/gallery.js +133 -0
- package/lib/web/index.html +344 -0
- package/lib/web/media-client.js +218 -0
- package/lib/web/network.js +232 -0
- package/lib/web/plugin-panel.js +223 -0
- package/lib/web/plugin-status.js +140 -0
- package/lib/web/plugin-surface.css +17 -0
- package/lib/web/providers.js +513 -0
- package/lib/web/recovery.js +105 -0
- package/lib/web/startup.js +18 -0
- package/lib/web/styles.css +571 -0
- package/lib/web/transport.js +101 -0
- package/lib/web/workbench-login.js +165 -0
- package/lib/workbench-artifacts.js +3 -2
- package/lib/workbench-auth-cli.js +47 -0
- package/lib/workbench-auth.js +200 -0
- package/package.json +24 -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 +8 -4
- package/skills/iris-core/references/cli.md +2 -2
- package/skills/iris-core/references/comfyui.md +69 -0
- package/skills/iris-core/references/tasks.md +2 -0
- package/templates/plugin-starter/LICENSE +21 -0
- package/templates/plugin-starter/README.md +74 -0
- package/templates/plugin-starter/assets/peach.png +0 -0
- package/templates/plugin-starter/assets/violet.png +0 -0
- package/templates/plugin-starter/index.mjs +27 -0
- package/templates/plugin-starter/package.json +18 -0
- package/templates/plugin-starter/scripts/core-cli.mjs +20 -0
- package/templates/plugin-starter/scripts/environment.mjs +65 -0
- package/templates/plugin-starter/scripts/mcp-probe.mjs +22 -0
- package/templates/plugin-starter/scripts/setup.mjs +42 -0
- package/templates/plugin-starter/scripts/smoke.mjs +76 -0
- package/templates/plugin-starter/web/board.css +27 -0
- package/templates/plugin-starter/web/board.mjs +160 -0
- package/templates/plugin-starter/web/png.mjs +23 -0
- package/templates/provider-starter/LICENSE +21 -0
- package/templates/provider-starter/README.md +50 -0
- package/templates/provider-starter/index.d.mts +5 -0
- package/templates/provider-starter/index.mjs +165 -0
- package/templates/provider-starter/package.json +13 -0
- package/templates/provider-starter/scripts/core-cli.mjs +22 -0
- package/templates/provider-starter/scripts/environment.mjs +122 -0
- package/templates/provider-starter/scripts/fixture.mjs +54 -0
- package/templates/provider-starter/scripts/mcp-probe.mjs +21 -0
- package/templates/provider-starter/scripts/protocol-smoke.mjs +48 -0
- package/templates/provider-starter/scripts/setup.mjs +44 -0
- package/templates/provider-starter/scripts/smoke.mjs +145 -0
- package/user_guide.md +5 -1
package/lib/image-flow.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
import { createCoreArtifact, exportCoreArtifact, readCoreArtifactBytes } from './core-artifacts.js';
|
|
3
|
+
import { createCoreArtifact, exportCoreArtifact, inspectCoreArtifactMetadata, readCoreArtifactBytes } from './core-artifacts.js';
|
|
4
4
|
import { assertFlowTaskMatches, claimFlowTask, findFlowTask, recoverCoreTask } from './core-tasks.js';
|
|
5
5
|
import { deliveryCacheNeedsProvider } from './core-delivery-cache.js';
|
|
6
6
|
import { normalizeGenerationInput } from './generation-input.js';
|
|
7
|
+
import { videoModelSupportsMode } from './video-input.js';
|
|
7
8
|
import { imageEditCandidates, readImageEditArtifact } from './image-edit-input.js';
|
|
8
9
|
import { parseModelRef } from './models.js';
|
|
9
10
|
import { pixelDiff } from './pixels.js';
|
|
10
11
|
import { createProviderTaskRunner } from './provider-task-runner.js';
|
|
11
|
-
import {
|
|
12
|
+
import { prepareProviderInput } from './provider-adapter.js';
|
|
13
|
+
import { sameNetworkRequirement } from './network-policy.js';
|
|
14
|
+
import { VIDEO_RECIPE, VIDEO_TYPES, MAX_VIDEO_BYTES, normalizeVideoFlowInput } from './video-flow-input.js';
|
|
15
|
+
import { AUDIO_RECIPES, AUDIO_TYPES, isAudioRecipe, normalizeAudioFlowInput, readImportAudio } from './audio-flow-input.js';
|
|
16
|
+
import { commitFlowCreation, flowFail, flowSha256, flowStep, flowStepId, initializeFlowRoot, inspectFlowRoot,
|
|
17
|
+
IMAGE_GENERATE_RECIPE, requireImageGenerationRoot, requireAudioRoot, requireVideoRoot, listFlowRuns, prepareFlowCreation, readFlowRun, updateFlowRun } from './flow-store.js';
|
|
12
18
|
|
|
13
19
|
function exact(input, allowed) {
|
|
14
20
|
if (!input || typeof input !== 'object' || Array.isArray(input) || Object.keys(input).some(key => !allowed.includes(key))) {
|
|
@@ -24,14 +30,15 @@ function readReference(root, expected) {
|
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
function facts(root, run) {
|
|
27
|
-
const source = readReference(root, run.source);
|
|
33
|
+
const source = run.source ? readReference(root, run.source) : null;
|
|
28
34
|
const task = findFlowTask(root, run);
|
|
29
35
|
if (run.output) {
|
|
30
36
|
if (task.deliveryState !== 'ready' || task.artifactIds.length !== 1 || task.artifactIds[0] !== run.output.artifactId) {
|
|
31
37
|
flowFail('IRIS_FLOW_TASK_CONFLICT', '流程输出与原 Task 的交付事实不一致');
|
|
32
38
|
}
|
|
33
39
|
const { artifact } = readReference(root, run.output);
|
|
34
|
-
if (artifact.metadata.taskId !== task.id || !artifact.relations.some(edge => edge.type === 'derived-from' && edge.artifactId === source.artifact.id)
|
|
40
|
+
if (artifact.metadata.taskId !== task.id || source && !artifact.relations.some(edge => edge.type === 'derived-from' && edge.artifactId === source.artifact.id)
|
|
41
|
+
|| !source && artifact.relations.length) {
|
|
35
42
|
flowFail('IRIS_FLOW_ARTIFACT_CONFLICT', '流程输出缺少原 Task 或来源关系');
|
|
36
43
|
}
|
|
37
44
|
}
|
|
@@ -45,35 +52,62 @@ function facts(root, run) {
|
|
|
45
52
|
}
|
|
46
53
|
let status = 'not_started';
|
|
47
54
|
if (task?.acceptance === 'unknown' || ['unknown', 'failed', 'canceled'].includes(task?.outcome)) status = 'needs_attention';
|
|
48
|
-
else if (task?.deliveryState === 'ready') status = run.
|
|
55
|
+
else if (task?.deliveryState === 'ready') status = isAudioRecipe(run.recipe) || run.recipe === VIDEO_RECIPE ? run.output ? 'ready' : 'needs_result'
|
|
56
|
+
: run.output && (run.recipe === IMAGE_GENERATE_RECIPE || run.comparison)
|
|
57
|
+
? run.selection ? 'selected' : 'awaiting_selection' : run.recipe === IMAGE_GENERATE_RECIPE ? 'needs_result' : 'needs_comparison';
|
|
49
58
|
else if (task?.deliveryState === 'failed') status = 'delivery_failed';
|
|
50
59
|
else if (task?.attempts.length) status = task.acceptance === 'accepted' ? 'running' : 'needs_attention';
|
|
51
60
|
return { contractVersion: 0, run, task, status, ...(comparison ? { metrics: comparison.metadata } : {}) };
|
|
52
61
|
}
|
|
53
62
|
|
|
54
|
-
/**
|
|
55
|
-
export function createImageFlowService(runtime, { resolveImageCandidates, resolveTaskAdapter } = {}) {
|
|
63
|
+
/** Fixed media recipes, sharing operation-scoped leases and the original Task. */
|
|
64
|
+
export function createImageFlowService(runtime, { resolveImageCandidates, resolveAudioCandidates, resolveVideoCandidates, resolveTaskAdapter } = {}) {
|
|
56
65
|
function creationInput(input) {
|
|
66
|
+
if (input?.recipe === VIDEO_RECIPE) { const { recipe, ...value } = input; return { recipe, ...normalizeVideoFlowInput(value) }; }
|
|
67
|
+
if (isAudioRecipe(input?.recipe)) {
|
|
68
|
+
const { recipe, ...value } = input;
|
|
69
|
+
return { recipe, ...normalizeAudioFlowInput(recipe, value) };
|
|
70
|
+
}
|
|
57
71
|
const normalized = normalizeGenerationInput('image', input);
|
|
58
|
-
if (!
|
|
59
|
-
flowFail('IRIS_FLOW_INPUT_INVALID', '
|
|
72
|
+
if (!parseModelRef(normalized.modelRef) || normalized.providerInput.n !== 1) {
|
|
73
|
+
flowFail('IRIS_FLOW_INPUT_INVALID', '图片流程需要明确 model_ref 和 n=1;改图还需来源 Artifact');
|
|
60
74
|
}
|
|
61
75
|
return { prompt: normalized.providerInput.prompt, model_ref: normalized.modelRef,
|
|
62
|
-
source_artifact_id: normalized.sourceArtifactId, n: 1,
|
|
63
|
-
...(normalized.providerInput.size !== undefined ? { size: normalized.providerInput.size } : {})
|
|
76
|
+
...(normalized.sourceArtifactId ? { source_artifact_id: normalized.sourceArtifactId } : {}), n: 1,
|
|
77
|
+
...(normalized.providerInput.size !== undefined ? { size: normalized.providerInput.size } : {}),
|
|
78
|
+
...(normalized.providerInput.workflow_parameters !== undefined ? { workflow_parameters: normalized.providerInput.workflow_parameters } : {}) };
|
|
64
79
|
}
|
|
65
80
|
async function prepareCreation(root, signal, input) {
|
|
66
81
|
const owned = creationInput(input);
|
|
67
82
|
inspectFlowRoot(root);
|
|
68
|
-
const
|
|
83
|
+
const audio = isAudioRecipe(owned.recipe);
|
|
84
|
+
const video = owned.recipe === VIDEO_RECIPE;
|
|
85
|
+
if (video) requireVideoRoot(root);
|
|
86
|
+
else if (audio) requireAudioRoot(root);
|
|
87
|
+
else if (!owned.source_artifact_id) requireImageGenerationRoot(root);
|
|
88
|
+
const source = owned.source_artifact_id ? audio ? readAudioSource(root, owned.source_artifact_id) : await readImageEditArtifact(root, owned.source_artifact_id, signal) : null;
|
|
69
89
|
const candidates = await candidatesFor(owned, signal);
|
|
70
|
-
|
|
71
|
-
|
|
90
|
+
const { recipe, ...audioInput } = owned;
|
|
91
|
+
return prepareFlowCreation({ ...(source ? { source: { artifactId: owned.source_artifact_id, sha256: flowSha256(source.bytes) } } : {}),
|
|
92
|
+
input: audio || video ? audioInput : owned, providerBinding: candidates[0].providerBinding,
|
|
93
|
+
...(candidates[0].adapter.networkRequirement ? { networkRequirement: candidates[0].adapter.networkRequirement } : {}), ...(audio || video ? { recipe } : {}) });
|
|
94
|
+
}
|
|
95
|
+
function readAudioSource(root, id) {
|
|
96
|
+
const source = readCoreArtifactBytes(root, id);
|
|
97
|
+
readImportAudio(source.bytes, source.artifact.mediaType);
|
|
98
|
+
return source;
|
|
72
99
|
}
|
|
73
100
|
async function candidatesFor(runInput, signal, binding) {
|
|
74
|
-
|
|
101
|
+
const audio = isAudioRecipe(runInput.recipe), video = runInput.recipe === VIDEO_RECIPE, resolver = video ? resolveVideoCandidates : audio ? resolveAudioCandidates : resolveImageCandidates;
|
|
102
|
+
// Keep historical records readable; restrict only creation/first dispatch.
|
|
103
|
+
if (video && !videoModelSupportsMode(parseModelRef(runInput.model_ref).modelId, 't2v')) {
|
|
104
|
+
flowFail('IRIS_VIDEO_FLOW_INPUT_INVALID', '固定文生视频流程需要匹配的模型,不接受图生视频、S2V 或参考视频模型;已有 Task 仍可按原身份观察');
|
|
105
|
+
}
|
|
106
|
+
if (!resolver) flowFail('IRIS_FLOW_PROVIDER_REQUIRED', '创建或首次提交流程需要显式 Provider 配置');
|
|
75
107
|
signal.throwIfAborted();
|
|
76
|
-
const
|
|
108
|
+
const resolve = () => resolver({ modelRef: runInput.model_ref, signal, input: runInput,
|
|
109
|
+
...(audio ? { capability: AUDIO_RECIPES[runInput.recipe].capability } : {}) });
|
|
110
|
+
const candidates = await (runtime.networkWait ? runtime.networkWait(resolve, 'candidates') : resolve());
|
|
77
111
|
signal.throwIfAborted();
|
|
78
112
|
const parsed = parseModelRef(runInput.model_ref);
|
|
79
113
|
if (!Array.isArray(candidates) || candidates.length !== 1 || candidates[0].model !== runInput.model_ref
|
|
@@ -81,12 +115,14 @@ export function createImageFlowService(runtime, { resolveImageCandidates, resolv
|
|
|
81
115
|
flowFail('IRIS_FLOW_INPUT_CONFLICT', '流程必须绑定单一明确模型和有效的连接摘要');
|
|
82
116
|
}
|
|
83
117
|
if (binding !== undefined && candidates[0].providerBinding !== binding) flowFail('IRIS_FLOW_PROVIDER_CHANGED', '连接或协议已改变;原流程不重新绑定或生成');
|
|
84
|
-
|
|
118
|
+
const resolved = [{ ...candidates[0], providerModel: parsed.modelId, selectionReason: 'explicit' }];
|
|
119
|
+
return runInput.source_artifact_id && !audio ? imageEditCandidates(resolved) : resolved;
|
|
85
120
|
}
|
|
86
121
|
|
|
87
122
|
async function adapterFor(task, signal) {
|
|
88
123
|
if (!resolveTaskAdapter) flowFail('IRIS_FLOW_PROVIDER_REQUIRED', '观察或远端补交付需要原 Task 的 Provider 配置');
|
|
89
|
-
const
|
|
124
|
+
const resolve = () => resolveTaskAdapter(task, { signal });
|
|
125
|
+
const adapter = await (runtime.networkWait ? runtime.networkWait(resolve, 'candidates') : resolve());
|
|
90
126
|
signal.throwIfAborted();
|
|
91
127
|
return adapter;
|
|
92
128
|
}
|
|
@@ -94,16 +130,29 @@ export function createImageFlowService(runtime, { resolveImageCandidates, resolv
|
|
|
94
130
|
async function continueRun(root, signal, id) {
|
|
95
131
|
let run = readFlowRun(root, id);
|
|
96
132
|
let task = facts(root, run).task;
|
|
97
|
-
if (task?.attempts.length && !run.
|
|
133
|
+
if (task?.attempts.length && !flowStep(run).claimed) flowFail('IRIS_FLOW_TASK_CONFLICT', '步骤检查点与已开始的 Task 冲突');
|
|
98
134
|
const runner = createProviderTaskRunner(runtime);
|
|
135
|
+
if (task) runner.assertIdle(task.id);
|
|
99
136
|
if (!task || !task.attempts.length) {
|
|
100
|
-
const
|
|
137
|
+
const audio = isAudioRecipe(run.recipe);
|
|
138
|
+
const video = run.recipe === VIDEO_RECIPE;
|
|
139
|
+
let candidates = await candidatesFor({ ...run.input, ...(audio || video ? { recipe: run.recipe } : {}) }, signal, run.providerBinding);
|
|
140
|
+
if (!sameNetworkRequirement(run.networkRequirement, candidates[0].adapter.networkRequirement)) flowFail('IRIS_NETWORK_REQUIREMENT_MISMATCH', '流程网络要求已改变,不会隐式转换或解除');
|
|
141
|
+
if (run.recipe === 'transcribe/v0') {
|
|
142
|
+
const source = readAudioSource(root, run.source.artifactId);
|
|
143
|
+
candidates = candidates.map(candidate => ({ ...candidate, prepareInput: async ({ signal, taskId, attemptId, networkBudget, recordNetworkRoute, modelRef, providerId }) => {
|
|
144
|
+
const prepared = await prepareProviderInput(candidate.adapter, { model: candidate.providerModel, bytes: source.bytes,
|
|
145
|
+
filename: source.artifact.id + '.' + AUDIO_TYPES[source.artifact.mediaType], signal }, { taskId, attemptId, networkBudget, recordNetworkRoute, modelRef, providerId });
|
|
146
|
+
return { audioUrl: prepared.url };
|
|
147
|
+
} }));
|
|
148
|
+
}
|
|
101
149
|
task = claimFlowTask(root, run.id);
|
|
102
|
-
if (!run.
|
|
150
|
+
if (!flowStep(run).claimed) run = updateFlowRun(root, run, { [flowStepId(run)]: { ...flowStep(run), claimed: true } });
|
|
103
151
|
signal.throwIfAborted();
|
|
104
|
-
const normalized =
|
|
105
|
-
|
|
106
|
-
|
|
152
|
+
const normalized = audio || video ? Object.fromEntries(Object.entries(run.input).filter(([key]) => key !== 'model_ref'))
|
|
153
|
+
: normalizeGenerationInput('image', run.input).providerInput;
|
|
154
|
+
task = (await runner.submit({ capability: video ? 'video' : audio ? AUDIO_RECIPES[run.recipe].capability : 'image', candidates, providerInput: normalized,
|
|
155
|
+
sourceArtifactId: run.source?.artifactId, flowRunId: run.id })).task;
|
|
107
156
|
} else {
|
|
108
157
|
task = recoverCoreTask(root, task.id);
|
|
109
158
|
assertFlowTaskMatches(task, run);
|
|
@@ -116,11 +165,17 @@ export function createImageFlowService(runtime, { resolveImageCandidates, resolv
|
|
|
116
165
|
}
|
|
117
166
|
signal.throwIfAborted();
|
|
118
167
|
if (task.deliveryState !== 'ready') return facts(root, run);
|
|
119
|
-
if (task.artifactIds.length !== 1) flowFail('IRIS_FLOW_RESULT_UNSUPPORTED', '
|
|
168
|
+
if (task.artifactIds.length !== 1) flowFail('IRIS_FLOW_RESULT_UNSUPPORTED', '当前流程只支持一个结果,原 Task 与作品已保留');
|
|
169
|
+
if (run.recipe === VIDEO_RECIPE) {
|
|
170
|
+
const metadata = inspectCoreArtifactMetadata(root, task.artifactIds[0]);
|
|
171
|
+
if (!Object.hasOwn(VIDEO_TYPES, metadata.mediaType) || metadata.size > MAX_VIDEO_BYTES) {
|
|
172
|
+
flowFail('IRIS_FLOW_RESULT_UNSUPPORTED', '视频首片只交付不超过 32 MiB 的 MP4/WebM;原 Task 与作品已保留');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
120
175
|
const output = readCoreArtifactBytes(root, task.artifactIds[0]);
|
|
121
176
|
if (!run.output) run = updateFlowRun(root, run, { output: reference(output.artifact) });
|
|
122
177
|
facts(root, run);
|
|
123
|
-
if (!run.comparison) {
|
|
178
|
+
if (run.source && !run.comparison && !isAudioRecipe(run.recipe)) {
|
|
124
179
|
const source = readReference(root, run.source);
|
|
125
180
|
const diff = await pixelDiff({ inputA: source.bytes, inputB: output.bytes, grid: 8, topRegions: 3 });
|
|
126
181
|
signal.throwIfAborted();
|
|
@@ -152,7 +207,7 @@ export function createImageFlowService(runtime, { resolveImageCandidates, resolv
|
|
|
152
207
|
return runtime.run('inspect', ({ dataRoot }) => {
|
|
153
208
|
const runs = listFlowRuns(dataRoot);
|
|
154
209
|
return { contractVersion: 0, total: runs.length, runs: runs.map(run => ({ id: run.id, recipe: run.recipe,
|
|
155
|
-
createdAt: run.createdAt, source: run.source, modelRef: run.input.model_ref, taskId: run.
|
|
210
|
+
createdAt: run.createdAt, ...(run.source ? { source: run.source } : {}), modelRef: run.input.model_ref, taskId: flowStep(run).taskId })) };
|
|
156
211
|
});
|
|
157
212
|
}
|
|
158
213
|
if (name === 'flow.inspect') {
|
|
@@ -164,13 +219,24 @@ export function createImageFlowService(runtime, { resolveImageCandidates, resolv
|
|
|
164
219
|
return runtime.run('execute', ({ dataRoot, signal }) => continueRun(dataRoot, signal, input.run_id));
|
|
165
220
|
}
|
|
166
221
|
if (name === 'flow.select' || name === 'flow.export') {
|
|
167
|
-
exact(input, name === 'flow.select' ? ['run_id', 'artifact_id'] : ['run_id', 'output_path']);
|
|
222
|
+
exact(input, name === 'flow.select' ? ['run_id', 'artifact_id', 'expected_revision'] : ['run_id', 'output_path']);
|
|
223
|
+
if (input.expected_revision !== undefined && (!Number.isSafeInteger(input.expected_revision) || input.expected_revision < 1)) {
|
|
224
|
+
flowFail('IRIS_FLOW_INPUT_INVALID', '预期流程版本必须是正整数');
|
|
225
|
+
}
|
|
168
226
|
return runtime.run('execute', ({ dataRoot }) => {
|
|
169
227
|
let run = readFlowRun(dataRoot, input.run_id);
|
|
228
|
+
if (input.expected_revision !== undefined && run.revision !== input.expected_revision) {
|
|
229
|
+
flowFail('IRIS_FLOW_REVISION_CONFLICT', '流程已改变,请读取当前版本后明确选择');
|
|
230
|
+
}
|
|
170
231
|
facts(dataRoot, run);
|
|
171
|
-
if (
|
|
232
|
+
if (isAudioRecipe(run.recipe) || run.recipe === VIDEO_RECIPE) {
|
|
233
|
+
if (name === 'flow.select') flowFail('IRIS_FLOW_SELECTION_INVALID', '视频、音频和转写为单一结果,无需选择图片版本');
|
|
234
|
+
if (!run.output) flowFail('IRIS_FLOW_NOT_READY', '请先接回原任务的结果');
|
|
235
|
+
return { ...facts(dataRoot, run), export: exportCoreArtifact(dataRoot, run.output.artifactId, input.output_path) };
|
|
236
|
+
}
|
|
237
|
+
if (!run.output || run.source && !run.comparison) flowFail('IRIS_FLOW_NOT_READY', '请先保存生成结果;改图还需完成比较');
|
|
172
238
|
if (name === 'flow.select') {
|
|
173
|
-
const selected = [run.source, run.output].find(item => item.artifactId === input.artifact_id);
|
|
239
|
+
const selected = [run.source, run.output].filter(Boolean).find(item => item.artifactId === input.artifact_id);
|
|
174
240
|
if (!selected) flowFail('IRIS_FLOW_SELECTION_INVALID', '只能选择本流程的原图或新版');
|
|
175
241
|
if (run.selection?.artifactId !== selected.artifactId) run = updateFlowRun(dataRoot, run, { selection: selected });
|
|
176
242
|
return facts(dataRoot, run);
|
|
@@ -182,7 +248,7 @@ export function createImageFlowService(runtime, { resolveImageCandidates, resolv
|
|
|
182
248
|
throw new TypeError('未知的内部 Flow 命令');
|
|
183
249
|
}
|
|
184
250
|
return Object.freeze({ execute, prepareCreation, inspectCreation: (root, intent, options) => {
|
|
185
|
-
readReference(root, intent.source);
|
|
251
|
+
if (intent.source) readReference(root, intent.source);
|
|
186
252
|
return facts(root, commitFlowCreation(root, intent, options));
|
|
187
253
|
} });
|
|
188
254
|
}
|
package/lib/index.js
CHANGED
|
@@ -66,38 +66,47 @@ function outputDir() {
|
|
|
66
66
|
|
|
67
67
|
/* ---------------- 媒体路由:/iris/media/:taskId/:token/:name ---------------- */
|
|
68
68
|
|
|
69
|
+
let latestNativeRuntime;
|
|
69
70
|
function mountIrisRoutes(routeCtx, core, routes) {
|
|
70
71
|
// Cordis 服务只在 DSH 宿主边界探测;Command 与通用工具辅助函数不接收原始 ctx
|
|
71
72
|
const host = dshHost(routeCtx);
|
|
72
73
|
const reg = hasHostPort(host, 'routes') ? host.ports.routes : undefined;
|
|
73
74
|
if (!reg || typeof reg.register !== 'function' || routes.mounted) return;
|
|
75
|
+
const disposers = []; let released = false;
|
|
76
|
+
const release = () => {
|
|
77
|
+
if (released) return;
|
|
78
|
+
released = true;
|
|
79
|
+
const errors = [];
|
|
80
|
+
for (const dispose of disposers.reverse()) { try { if (typeof dispose === 'function') dispose(); } catch (error) { errors.push(error); } }
|
|
81
|
+
if (routes.mounted && latestNativeRuntime === core) closeAllSse();
|
|
82
|
+
routes.mounted = false;
|
|
83
|
+
if (errors.length) throw new AggregateError(errors, 'Iris 路由释放失败');
|
|
84
|
+
};
|
|
74
85
|
try {
|
|
75
86
|
const disposeMedia = reg.register({
|
|
76
87
|
kind: 'prefix',
|
|
77
88
|
path: '/iris/media',
|
|
78
89
|
handler: guarded(serveMedia)
|
|
79
90
|
});
|
|
91
|
+
disposers.push(disposeMedia);
|
|
80
92
|
const disposeApi = reg.register({
|
|
81
93
|
kind: 'prefix',
|
|
82
94
|
path: '/iris/api',
|
|
83
95
|
handler: guarded((req, res) => serveApi(req, res, dshHost(routeCtx), core))
|
|
84
96
|
});
|
|
97
|
+
disposers.push(disposeApi);
|
|
85
98
|
const disposeRender = reg.register({
|
|
86
99
|
kind: 'prefix',
|
|
87
100
|
path: '/iris/render',
|
|
88
101
|
handler: guarded(serveRender)
|
|
89
102
|
});
|
|
103
|
+
disposers.push(disposeRender);
|
|
104
|
+
routeCtx.effect(() => release, 'iris: media+api+actions+render routes');
|
|
90
105
|
routes.mounted = true;
|
|
91
106
|
recordHostRoutes(EXPECTED_IRIS_ROUTES);
|
|
92
|
-
routeCtx.effect(() => () => {
|
|
93
|
-
if (typeof disposeMedia === 'function') disposeMedia();
|
|
94
|
-
if (typeof disposeApi === 'function') disposeApi();
|
|
95
|
-
if (typeof disposeRender === 'function') disposeRender();
|
|
96
|
-
closeAllSse(); // SSE 长连接随插件停用一起关闭,不留悬挂连接
|
|
97
|
-
routes.mounted = false;
|
|
98
|
-
}, 'iris: media+api+actions+render routes');
|
|
99
107
|
console.log('[iris] 媒体+工作台+操作路由已挂载:/iris/media · /iris/api · /iris/api/actions · /iris/render');
|
|
100
108
|
} catch (err) {
|
|
109
|
+
try { release(); } catch (cleanup) { console.error('[iris] 路由回滚失败:', cleanup.message); }
|
|
101
110
|
console.error('[iris] 路由挂载失败:', err && err.message);
|
|
102
111
|
}
|
|
103
112
|
}
|
|
@@ -221,13 +230,24 @@ function safeRegister(host, definition) {
|
|
|
221
230
|
export async function apply(ctx) {
|
|
222
231
|
beginHostRuntime({ pluginId: IRIS_PLUGIN_ID, version: PACKAGE_VERSION });
|
|
223
232
|
const core = createDshRuntime({ dataRoot: dshCoreDataRoot(), getHost: () => dshHost(ctx) });
|
|
233
|
+
latestNativeRuntime = core;
|
|
224
234
|
ctx.provide('irisRuntime', core);
|
|
225
235
|
const routes = { mounted: false };
|
|
226
236
|
const { cropForDsh, diffForDsh, framesForDsh, summaryFramesForDsh, visionImageForDsh, htmlForDsh, inspectProviderTaskForDsh, projectCoreTaskForDsh, readCoreArtifactMediaForDsh, resumeProviderTaskWatchesForDsh } = core;
|
|
227
237
|
ctx.effect(() => () => {
|
|
228
|
-
tasks.stopWatchAll();
|
|
238
|
+
if (latestNativeRuntime === core) tasks.stopWatchAll();
|
|
229
239
|
return core.dispose();
|
|
230
240
|
}, 'iris: DSH runtime');
|
|
241
|
+
// Explicit additional connection for shared Flow tools. Legacy native media/skills/UI retain their own roots.
|
|
242
|
+
if (process.env.IRIS_CORE_CONNECTION_FILE) {
|
|
243
|
+
const { createDshServiceBridge } = await import('./dsh-service-bridge.js');
|
|
244
|
+
const bridge = createDshServiceBridge({ connectionFile: process.env.IRIS_CORE_CONNECTION_FILE,
|
|
245
|
+
requestedScopes: (process.env.IRIS_CORE_SCOPES || 'read').split(','), getHost: () => dshHost(ctx), harnessVersion: dshHost(ctx).version });
|
|
246
|
+
ctx.provide('irisCoreService', bridge);
|
|
247
|
+
ctx.effect(() => () => bridge.dispose(), 'iris: Core service connection');
|
|
248
|
+
try { await bridge.start(); }
|
|
249
|
+
catch (error) { console.error('[iris] Core 服务连接失败:', error?.code || 'IRIS_CORE_SERVICE_UNAVAILABLE'); }
|
|
250
|
+
}
|
|
231
251
|
guard('Iris 私有存储权限收紧', () => hardenPrivateTree(store.irisHome()));
|
|
232
252
|
|
|
233
253
|
guard('上传临时文件清理', () => purgeStaleUploads());
|
|
@@ -242,13 +262,18 @@ export async function apply(ctx) {
|
|
|
242
262
|
else console.log('[iris] 工作台导入:' + imported.reason);
|
|
243
263
|
});
|
|
244
264
|
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
return
|
|
265
|
+
// Legacy 接管也要解析当次 DSH 连接;关闭会中止解析,禁止迟到的恢复再启动盯守。
|
|
266
|
+
void core.runHostOperation(signal => {
|
|
267
|
+
const resume = () => tasks.resumePending((task) => {
|
|
268
|
+
const provider = store.providerById(task.providerId);
|
|
269
|
+
return provider ? taskPollDeps(provider, task) : null;
|
|
250
270
|
});
|
|
271
|
+
if (!tasks.all().some(task => task.status === 'running' && task.remoteTaskId)) return resume();
|
|
272
|
+
return withDshProviderConfig(dshHost(ctx), resume, { signal, fresh: true });
|
|
273
|
+
}).then(resumed => {
|
|
251
274
|
if (resumed.length) console.log(`[iris] 已恢复接管 ${resumed.length} 个后台任务`);
|
|
275
|
+
}, error => {
|
|
276
|
+
if (core.snapshot().state === 'open') console.error('[iris] 恢复后台任务失败(已隔离,不影响宿主):', error?.code || 'unknown');
|
|
252
277
|
});
|
|
253
278
|
|
|
254
279
|
// Owned background setup lets unload cancel a slow connection resolver during takeover.
|
|
@@ -399,7 +424,7 @@ export async function apply(ctx) {
|
|
|
399
424
|
const refs = [];
|
|
400
425
|
for (const f of files) {
|
|
401
426
|
const mediaType = MEDIA_TYPES[path.extname(f).toLowerCase()] || 'image/png';
|
|
402
|
-
const ref = await attachmentService(
|
|
427
|
+
const ref = await attachmentService(dshHost(ctx)).saveImage({ data: new Uint8Array(fs.readFileSync(f)), mediaType, name: path.basename(f) });
|
|
403
428
|
refs.push(ref);
|
|
404
429
|
blocks.push({ type: 'image', attachment: ref });
|
|
405
430
|
}
|
|
@@ -647,7 +672,7 @@ export async function apply(ctx) {
|
|
|
647
672
|
const refs = [];
|
|
648
673
|
for (let i = 0; i < frames.length; i++) {
|
|
649
674
|
const f = frames[i];
|
|
650
|
-
const ref = await attachmentService(
|
|
675
|
+
const ref = await attachmentService(dshHost(ctx)).saveImage({
|
|
651
676
|
data: new Uint8Array(f.bytes),
|
|
652
677
|
mediaType,
|
|
653
678
|
name: `iris-frame-${Date.now()}-${i + 1}.${ext}`
|
|
@@ -744,7 +769,7 @@ export async function apply(ctx) {
|
|
|
744
769
|
});
|
|
745
770
|
// 成功摘要的同一张拼图作为显式工具产物发布。
|
|
746
771
|
if (exec.signal?.aborted) throw exec.signal.reason;
|
|
747
|
-
const ref = await attachmentService(
|
|
772
|
+
const ref = await attachmentService(dshHost(ctx)).saveImage({
|
|
748
773
|
data: new Uint8Array(sheet.buffer),
|
|
749
774
|
mediaType: 'image/png',
|
|
750
775
|
name: `iris-sheet-${Date.now()}.png`
|
|
@@ -801,7 +826,7 @@ export async function apply(ctx) {
|
|
|
801
826
|
async prepareImage(signal) {
|
|
802
827
|
if (source === 'artifact_id') return visionImageForDsh({ artifactId: args.artifact_id, signal });
|
|
803
828
|
const image = await readVisionImageFile(p, mt, signal);
|
|
804
|
-
await waitForModelWork(() => attachmentService(
|
|
829
|
+
await waitForModelWork(() => attachmentService(dshHost(ctx)).saveImage({ data: image.bytes, mediaType: mt, name: path.basename(p) }), signal);
|
|
805
830
|
return image;
|
|
806
831
|
}
|
|
807
832
|
});
|
|
@@ -833,10 +858,10 @@ export async function apply(ctx) {
|
|
|
833
858
|
return runVisionTool(dshHost(ctx), exec, { origin: 'relook', model: args.model, question,
|
|
834
859
|
async prepareImage(signal) {
|
|
835
860
|
// 会话扫描、解引用与读图都属于这次操作的总预算。
|
|
836
|
-
const hit = (await waitForModelWork(() => sessionAttachmentRef(
|
|
861
|
+
const hit = (await waitForModelWork(() => sessionAttachmentRef(dshHost(ctx), exec, id), signal)) || findOwnAttachment(id);
|
|
837
862
|
if (!hit) throw new Error('iris: 图片引用不可用');
|
|
838
863
|
if (hit.absPath) return readVisionImageFile(hit.absPath, hit.ref.mediaType || MEDIA_TYPES[path.extname(hit.absPath).toLowerCase()] || 'image/png', signal);
|
|
839
|
-
const stored = await waitForModelWork(() => attachmentService(
|
|
864
|
+
const stored = await waitForModelWork(() => attachmentService(dshHost(ctx)).readImage(hit.ref, signal), signal);
|
|
840
865
|
return { bytes: new Uint8Array(stored.data), mediaType: stored.mediaType || hit.ref.mediaType };
|
|
841
866
|
}
|
|
842
867
|
});
|
|
@@ -900,7 +925,7 @@ export async function apply(ctx) {
|
|
|
900
925
|
signal: exec.signal
|
|
901
926
|
});
|
|
902
927
|
if (exec.signal && exec.signal.aborted) throw new Error('已取消');
|
|
903
|
-
const ref = await attachmentService(
|
|
928
|
+
const ref = await attachmentService(dshHost(ctx)).saveImage({
|
|
904
929
|
data: new Uint8Array(result.bytes),
|
|
905
930
|
mediaType: result.mediaType,
|
|
906
931
|
name: `iris-crop-${Date.now()}.png`
|
|
@@ -946,7 +971,7 @@ export async function apply(ctx) {
|
|
|
946
971
|
grid: args.grid, topRegions: args.top_regions, signal: exec.signal
|
|
947
972
|
});
|
|
948
973
|
const { ratio, diffPixels, totalPixels, width, height, worstRegions } = result.metrics;
|
|
949
|
-
const ref = await attachmentService(
|
|
974
|
+
const ref = await attachmentService(dshHost(ctx)).saveImage({
|
|
950
975
|
data: new Uint8Array(result.bytes),
|
|
951
976
|
mediaType: result.mediaType,
|
|
952
977
|
name: `iris-diff-${Date.now()}.png`
|
|
@@ -1037,7 +1062,7 @@ export async function apply(ctx) {
|
|
|
1037
1062
|
});
|
|
1038
1063
|
const png = Buffer.from(rendered.bytes);
|
|
1039
1064
|
const { width: outW, height: outH } = await imageDimensions(png);
|
|
1040
|
-
const ref = await attachmentService(
|
|
1065
|
+
const ref = await attachmentService(dshHost(ctx)).saveImage({
|
|
1041
1066
|
data: new Uint8Array(png),
|
|
1042
1067
|
mediaType: 'image/png',
|
|
1043
1068
|
name: `iris-html-${Date.now()}.png`
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// HTTP DTOs whitelist existing Core facts; never expose records or configuration wholesale.
|
|
2
|
+
import { isAudioRecipe } from './audio-flow-input.js';
|
|
3
|
+
import { VIDEO_RECIPE } from './video-flow-input.js';
|
|
2
4
|
const STATUSES = new Set(['not_started', 'running', 'needs_attention', 'delivery_failed',
|
|
3
|
-
'needs_comparison', 'awaiting_selection', 'selected']);
|
|
5
|
+
'needs_comparison', 'needs_result', 'awaiting_selection', 'selected', 'ready']);
|
|
4
6
|
|
|
5
7
|
export function projectLocalApiFlow(facts) {
|
|
6
|
-
if (facts.contractVersion !== 0 ||
|
|
8
|
+
if (facts.contractVersion !== 0 || !['image-edit/v0', 'image-generate/v0', VIDEO_RECIPE].includes(facts.run.recipe) && !isAudioRecipe(facts.run.recipe) || !STATUSES.has(facts.status)) {
|
|
7
9
|
throw new TypeError('不支持的流程契约或状态');
|
|
8
10
|
}
|
|
9
11
|
const { run, task } = facts;
|
|
@@ -11,11 +13,16 @@ export function projectLocalApiFlow(facts) {
|
|
|
11
13
|
return {
|
|
12
14
|
contractVersion: 0, id: run.id, recipe: run.recipe, revision: run.revision,
|
|
13
15
|
createdAt: run.createdAt, updatedAt: run.updatedAt, status: facts.status,
|
|
14
|
-
input: {
|
|
15
|
-
...(run.input.
|
|
16
|
+
input: { ...(isAudioRecipe(run.recipe) ? run.recipe === 'speech/v0' ? { text: run.input.text,
|
|
17
|
+
...(run.input.voice !== undefined ? { voice: run.input.voice } : {}) } : {} : { prompt: run.input.prompt }), modelRef: run.input.model_ref,
|
|
18
|
+
...(run.input.size !== undefined ? { size: run.input.size } : {}),
|
|
19
|
+
...(run.input.duration !== undefined ? { duration: run.input.duration } : {}),
|
|
20
|
+
...(run.input.workflow_parameters !== undefined ? { workflowParameters: run.input.workflow_parameters } : {}) },
|
|
16
21
|
source: ref(run.source), output: ref(run.output), comparison: ref(run.comparison), selection: ref(run.selection),
|
|
17
22
|
task: task ? { id: task.id, acceptance: task.acceptance, outcome: task.outcome,
|
|
18
|
-
deliveryState: task.deliveryState, attemptCount: task.attempts.length
|
|
23
|
+
deliveryState: task.deliveryState, attemptCount: task.attempts.length,
|
|
24
|
+
...(task.networkRoutes ? { networkRoutes: task.networkRoutes.map(route => ({ route: route.route, operation: route.operation, lastAt: route.lastAt,
|
|
25
|
+
...(route.poolId ? { poolId: route.poolId } : {}), ...(route.nodeId ? { nodeId: route.nodeId } : {}) })) } : {}) } : null,
|
|
19
26
|
metrics: facts.metrics ? Object.fromEntries(['ratio', 'diffPixels', 'totalPixels', 'width', 'height']
|
|
20
27
|
.map(key => [key, facts.metrics[key]])) : null
|
|
21
28
|
};
|
|
@@ -23,5 +30,7 @@ export function projectLocalApiFlow(facts) {
|
|
|
23
30
|
|
|
24
31
|
export function projectLocalApiRunSummary(run) {
|
|
25
32
|
return { id: run.id, recipe: run.recipe, createdAt: run.createdAt,
|
|
26
|
-
|
|
33
|
+
updatedAt: run.updatedAt, revision: run.revision, modelRef: run.input.model_ref,
|
|
34
|
+
source: run.source ? { artifactId: run.source.artifactId } : null, output: run.output ? { artifactId: run.output.artifactId } : null,
|
|
35
|
+
selection: run.selection ? { artifactId: run.selection.artifactId } : null };
|
|
27
36
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { writeLocalApiTokenFile } from './local-api.js';
|
|
5
|
+
|
|
6
|
+
const invalid = message => { throw Object.assign(new Error(message), { code: 'IRIS_API_TOKEN_DIRECTORY_INVALID' }); };
|
|
7
|
+
|
|
8
|
+
/** The explicitly chosen directory may be new; its parent must exist. No chmod or cleanup. */
|
|
9
|
+
export function checkLocalApiTokenDirectory(directory, dataRoot) {
|
|
10
|
+
if (typeof directory !== 'string' || !path.isAbsolute(directory)) invalid('token-dir 必须是根外私有目录的绝对路径');
|
|
11
|
+
const supplied = path.resolve(directory);
|
|
12
|
+
let info;
|
|
13
|
+
try { info = fs.lstatSync(supplied); }
|
|
14
|
+
catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
15
|
+
if (info && (!info.isDirectory() || info.isSymbolicLink() || process.platform !== 'win32' && info.mode & 0o077)) {
|
|
16
|
+
invalid('token-dir 必须是私有真实目录(POSIX 0700);不会修改已有目录权限');
|
|
17
|
+
}
|
|
18
|
+
const resolved = info ? fs.realpathSync(supplied) : path.join(fs.realpathSync(path.dirname(supplied)), path.basename(supplied));
|
|
19
|
+
const physical = info ? fs.realpathSync.native(supplied) : path.join(fs.realpathSync.native(path.dirname(supplied)), path.basename(supplied));
|
|
20
|
+
const relative = path.relative(fs.realpathSync.native(dataRoot), physical);
|
|
21
|
+
if (!relative || relative !== '..' && !relative.startsWith('..' + path.sep) && !path.isAbsolute(relative)) invalid('token-dir 必须位于 Core 数据根之外');
|
|
22
|
+
return resolved;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function writeLocalApiTokenDirectory(directory, token, dataRoot) {
|
|
26
|
+
const target = checkLocalApiTokenDirectory(directory, dataRoot);
|
|
27
|
+
if (!fs.existsSync(target)) fs.mkdirSync(target, { mode: 0o700 });
|
|
28
|
+
const file = path.join(target, 'session-' + crypto.randomUUID() + '.token');
|
|
29
|
+
writeLocalApiTokenFile(file, token, dataRoot);
|
|
30
|
+
return file;
|
|
31
|
+
}
|