@mokuyoaxis/dsh-iris 0.3.0-alpha.1 → 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 +63 -1
- package/README.en.md +20 -13
- package/README.md +20 -13
- package/bin/dsh-iris.js +143 -51
- package/docs/AGENT_CORE_QUICKSTART.md +2 -2
- package/docs/AGENT_QUICKSTART.md +1 -1
- package/docs/ARCHITECTURE.md +5 -5
- package/docs/ARTIFACT_MANIFEST.md +4 -0
- package/docs/AUDIO_FLOWS.md +20 -4
- package/docs/BETA_SCOPE.md +42 -0
- package/docs/CONTACT_SHEET.md +1 -1
- package/docs/CORE_CONNECTIONS.md +3 -1
- package/docs/CORE_QUICKSTART.md +7 -5
- package/docs/CORE_RUNTIME_CONTRACT.md +3 -3
- package/docs/CORE_SERVICE.md +9 -5
- package/docs/CORE_SKILLS.md +2 -2
- package/docs/DSH_COMPATIBILITY.md +11 -1
- package/docs/DSH_CORE_MIGRATION.md +6 -0
- package/docs/DSH_RUNTIME.md +2 -2
- package/docs/HEADLESS_CLI.md +29 -4
- package/docs/HOST_ADAPTER_CONTRACT.md +5 -3
- package/docs/IMAGE_FLOW.md +2 -2
- package/docs/IMAGE_GENERATION.md +1 -1
- package/docs/LOCAL_API.md +2 -2
- package/docs/LOCAL_API_CONTRACT.md +17 -15
- package/docs/LOCAL_CREATION.md +2 -2
- package/docs/LOCAL_OPERATIONS.md +2 -2
- package/docs/LOCAL_SELECTION.md +1 -1
- package/docs/LOCAL_WEB.md +11 -5
- package/docs/MCP.md +30 -9
- package/docs/MEDIA_SCHEDULING.md +72 -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 +1 -1
- package/docs/PLUGINS.md +9 -5
- package/docs/PLUGIN_DEVELOPMENT.md +39 -0
- package/docs/PLUGIN_STARTUP.md +15 -2
- package/docs/PROVIDER_ADAPTER_CONTRACT.md +8 -2
- package/docs/PROVIDER_CREDENTIALS.md +67 -0
- package/docs/PROVIDER_EXTENSIONS.md +8 -4
- package/docs/PROVIDER_HEALTH.md +4 -2
- package/docs/PROVIDER_NETWORK.md +45 -0
- package/docs/PROVIDER_PLUGIN_CONTRACT.md +24 -0
- package/docs/PROVIDER_SETTINGS.md +13 -7
- package/docs/PROVIDER_SUBMISSION_CONTRACT.md +3 -1
- package/docs/ROADMAP.md +6 -6
- package/docs/SECURITY.md +1 -1
- package/docs/SYNC_DELIVERY_RECOVERY.md +5 -1
- package/docs/VIDEO_FLOWS.md +89 -0
- package/docs/VISION_MODEL.md +2 -0
- package/docs/WEB_PROTOTYPE.md +3 -3
- package/docs/WEB_RECOVERY.md +2 -2
- package/docs/WORKBENCH_ARTIFACTS.md +6 -0
- package/docs/WORKBENCH_LOGIN.md +2 -2
- package/docs/releases/0.3.0-beta.1.md +34 -0
- package/docs/screenshots.md +2 -2
- package/lib/actions.js +5 -4
- package/lib/adapters.js +58 -41
- package/lib/api.js +23 -13
- package/lib/application-runtime.js +44 -15
- package/lib/application-writer.js +193 -0
- package/lib/client.d.ts +3 -3
- package/lib/client.js +62 -25
- package/lib/comfyui-adapter.js +3 -2
- package/lib/comfyui-client.js +10 -1
- package/lib/command-service.js +18 -5
- package/lib/config.js +13 -9
- package/lib/configured-image-runtime.js +28 -9
- package/lib/connection-skill.js +9 -0
- package/lib/core-artifact-store.js +68 -26
- package/lib/core-bridge-tools.js +5 -3
- package/lib/core-capability-catalog.js +21 -11
- package/lib/core-delivery-cache.js +8 -3
- package/lib/core-service-client.js +6 -2
- package/lib/core-tasks.js +86 -11
- package/lib/creation-request-store.js +8 -5
- package/lib/doctor.js +2 -2
- package/lib/dsh-core-adapter.js +61 -9
- package/lib/dsh-provider-config.js +1 -1
- package/lib/dsh-service-bridge.js +1 -0
- package/lib/flow-store.js +64 -17
- package/lib/image-flow.js +39 -17
- package/lib/index.js +10 -5
- package/lib/local-api-projection.js +6 -2
- package/lib/local-api.js +137 -49
- package/lib/local-creation.js +12 -5
- package/lib/local-operations.js +2 -2
- package/lib/local-request-root.js +2 -2
- package/lib/local-web-assets.js +2 -0
- package/lib/mcp-http.js +82 -9
- package/lib/mcp-server.js +23 -1
- package/lib/media-scheduler.js +194 -0
- package/lib/model-directory-service.js +25 -8
- package/lib/music-transport.js +4 -2
- 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 +83 -38
- package/lib/operation-store.js +6 -6
- package/lib/plugins.d.ts +25 -6
- package/lib/plugins.js +2 -1
- package/lib/provider-adapter.js +2 -2
- package/lib/provider-adapters.js +37 -5
- package/lib/provider-catalog.js +33 -16
- package/lib/provider-config-format.js +51 -0
- package/lib/provider-config-service.js +52 -22
- 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 +5 -5
- package/lib/provider-health.js +10 -5
- package/lib/provider-monitor.js +5 -3
- package/lib/provider-network.js +52 -0
- package/lib/provider-protocol.js +3 -2
- package/lib/provider-registry.js +15 -5
- package/lib/provider-response.js +10 -0
- package/lib/provider-settings.js +14 -12
- package/lib/provider-task-runner.js +113 -37
- package/lib/provider-transport.js +16 -0
- package/lib/render.js +11 -1
- package/lib/scheduling-policy.js +66 -0
- 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 +2 -0
- package/lib/web/app.js +107 -46
- package/lib/web/connections.js +151 -12
- package/lib/web/creation.js +29 -25
- package/lib/web/flow-view.js +4 -3
- package/lib/web/gallery.js +17 -16
- package/lib/web/index.html +58 -5
- package/lib/web/media-client.js +18 -4
- package/lib/web/network.js +232 -0
- package/lib/web/plugin-panel.js +4 -2
- package/lib/web/plugin-status.js +140 -0
- package/lib/web/providers.js +34 -13
- package/lib/web/recovery.js +2 -2
- package/lib/web/styles.css +54 -1
- package/lib/web/transport.js +6 -1
- package/lib/workbench-artifacts.js +3 -2
- package/package.json +5 -2
- package/skills/iris-core/references/comfyui.md +1 -1
- 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 +3 -3
package/lib/flow-store.js
CHANGED
|
@@ -10,6 +10,8 @@ import { parseModelRef } from './models.js';
|
|
|
10
10
|
import { inspectLocalRequestRoot } from './local-request-root.js';
|
|
11
11
|
import { normalizeComfyParameters } from './comfyui-workflow.js';
|
|
12
12
|
import { AUDIO_RECIPES, isAudioRecipe, normalizeAudioFlowInput } from './audio-flow-input.js';
|
|
13
|
+
import { VIDEO_RECIPE, normalizeVideoFlowInput } from './video-flow-input.js';
|
|
14
|
+
import { checkedNetworkRequirement } from './network-policy.js';
|
|
13
15
|
|
|
14
16
|
const RUN = /^run_[a-f0-9]{24}$/;
|
|
15
17
|
const TASK = /^task_[a-f0-9]{24}$/;
|
|
@@ -32,27 +34,34 @@ function iso(value) { return typeof value === 'string' && Number.isFinite(Date.p
|
|
|
32
34
|
const copy = value => Object.freeze(structuredClone(value));
|
|
33
35
|
export const flowId = prefix => prefix + '_' + crypto.randomBytes(12).toString('hex');
|
|
34
36
|
export const flowSha256 = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
35
|
-
export const flowStepId = run => isAudioRecipe(run.recipe) ? AUDIO_RECIPES[run.recipe].step : run.recipe === IMAGE_GENERATE_RECIPE ? 'generate' : 'edit';
|
|
36
|
-
export const flowCapability = run => isAudioRecipe(run.recipe) ? AUDIO_RECIPES[run.recipe].capability : 'image';
|
|
37
|
+
export const flowStepId = run => run.recipe === VIDEO_RECIPE ? 'video' : isAudioRecipe(run.recipe) ? AUDIO_RECIPES[run.recipe].step : run.recipe === IMAGE_GENERATE_RECIPE ? 'generate' : 'edit';
|
|
38
|
+
export const flowCapability = run => run.recipe === VIDEO_RECIPE ? 'video' : isAudioRecipe(run.recipe) ? AUDIO_RECIPES[run.recipe].capability : 'image';
|
|
37
39
|
export const flowStep = run => run[flowStepId(run)];
|
|
38
40
|
export function requireImageGenerationRoot(root) {
|
|
39
|
-
if (![2, 3].includes(inspectLocalRequestRoot(root, { allowAbsent: true })?.schemaVersion)) {
|
|
41
|
+
if (![2, 3, 4].includes(inspectLocalRequestRoot(root, { allowAbsent: true })?.schemaVersion)) {
|
|
40
42
|
flowFail('IRIS_FLOW_GENERATION_ROOT_REQUIRED', '文生图需要新的专用执行根:execution init --image-generation;不迁移已有根');
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
45
|
export function requireAudioRoot(root) {
|
|
44
|
-
if (inspectLocalRequestRoot(root, { allowAbsent: true })?.schemaVersion
|
|
46
|
+
if (![3, 4].includes(inspectLocalRequestRoot(root, { allowAbsent: true })?.schemaVersion)) {
|
|
45
47
|
flowFail('IRIS_FLOW_AUDIO_ROOT_REQUIRED', '音频需要新的专用执行根:execution init --audio;不迁移已有根');
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
export function requireVideoRoot(root) {
|
|
52
|
+
if (inspectLocalRequestRoot(root, { allowAbsent: true })?.schemaVersion !== 4) {
|
|
53
|
+
flowFail('IRIS_FLOW_VIDEO_ROOT_REQUIRED', '文生视频需要新的专用 execution init --video 根;原根不迁移');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
49
57
|
export function validFlowBinding(value) {
|
|
50
58
|
return keys(value, ['runId', 'stepId', 'inputDigest']) && RUN.test(value.runId)
|
|
51
|
-
&& ['edit', 'generate', 'speak', 'transcribe'].includes(value.stepId) && BINDING.test(value.inputDigest);
|
|
59
|
+
&& ['edit', 'generate', 'speak', 'transcribe', 'video'].includes(value.stepId) && BINDING.test(value.inputDigest);
|
|
52
60
|
}
|
|
53
61
|
export function flowTaskBinding(run) { return { runId: run.id, stepId: flowStepId(run), inputDigest: run.inputDigest }; }
|
|
54
62
|
|
|
55
|
-
function
|
|
63
|
+
function legacyInputDigest(run) {
|
|
64
|
+
if (run.recipe === VIDEO_RECIPE) return 'sha256:' + flowSha256(JSON.stringify([VIDEO_RECIPE, run.providerBinding, normalizeVideoFlowInput(run.input)]));
|
|
56
65
|
if (isAudioRecipe(run.recipe)) return 'sha256:' + flowSha256(JSON.stringify([run.recipe, run.providerBinding,
|
|
57
66
|
normalizeAudioFlowInput(run.recipe, run.input), run.source?.sha256 ?? null]));
|
|
58
67
|
if (run.recipe === IMAGE_GENERATE_RECIPE) return 'sha256:' + flowSha256(JSON.stringify([IMAGE_GENERATE_RECIPE,
|
|
@@ -62,13 +71,24 @@ function inputDigest(run) {
|
|
|
62
71
|
run.input.model_ref, run.providerBinding, run.input.prompt, run.input.size ?? null, 1]));
|
|
63
72
|
}
|
|
64
73
|
|
|
74
|
+
function networkPart(value) {
|
|
75
|
+
if (value?.networkRequirement === undefined) return {};
|
|
76
|
+
try { return { networkRequirement: checkedNetworkRequirement(value.networkRequirement) }; }
|
|
77
|
+
catch (_) { flowFail('IRIS_FLOW_RECORD_INVALID', '流程网络要求身份或版本无效'); }
|
|
78
|
+
}
|
|
79
|
+
function inputDigest(run) {
|
|
80
|
+
const base = legacyInputDigest(run);
|
|
81
|
+
return run.networkRequirement === undefined ? base
|
|
82
|
+
: 'sha256:' + flowSha256(JSON.stringify(['network-flow/v1', base, networkPart(run).networkRequirement]));
|
|
83
|
+
}
|
|
84
|
+
|
|
65
85
|
function reference(value) { return keys(value, ['artifactId', 'sha256']) && ARTIFACT.test(value.artifactId) && SHA.test(value.sha256); }
|
|
66
86
|
function validateAudio(run, expectedId) {
|
|
67
87
|
let normalized;
|
|
68
88
|
try { normalized = normalizeAudioFlowInput(run.recipe, run.input); }
|
|
69
89
|
catch (_) { flowFail('IRIS_FLOW_RECORD_INVALID', '音频流程输入无效'); }
|
|
70
90
|
const step = flowStepId(run), transcribe = run.recipe === 'transcribe/v0';
|
|
71
|
-
if (!keys(run, ['schemaVersion', 'recipe', 'id', 'revision', 'createdAt', 'updatedAt', 'input', 'providerBinding', 'inputDigest', step, 'source', 'output'],
|
|
91
|
+
if (!keys(run, ['schemaVersion', 'recipe', 'id', 'revision', 'createdAt', 'updatedAt', 'input', 'providerBinding', 'inputDigest', step, 'source', 'output', 'networkRequirement'],
|
|
72
92
|
['schemaVersion', 'recipe', 'id', 'revision', 'createdAt', 'updatedAt', 'input', 'providerBinding', 'inputDigest', step, ...(transcribe ? ['source'] : [])])
|
|
73
93
|
|| run.schemaVersion !== 3 || !RUN.test(run.id) || run.id !== expectedId || !Number.isSafeInteger(run.revision) || run.revision < 1
|
|
74
94
|
|| !iso(run.createdAt) || !iso(run.updatedAt) || !BINDING.test(run.providerBinding)
|
|
@@ -83,7 +103,7 @@ function validateAudio(run, expectedId) {
|
|
|
83
103
|
}
|
|
84
104
|
function validateGeneration(run, expectedId, normalized) {
|
|
85
105
|
if (!keys(run, ['schemaVersion', 'recipe', 'id', 'revision', 'createdAt', 'updatedAt', 'input', 'providerBinding',
|
|
86
|
-
'inputDigest', 'generate', 'output', 'selection'],
|
|
106
|
+
'inputDigest', 'generate', 'output', 'selection', 'networkRequirement'],
|
|
87
107
|
['schemaVersion', 'recipe', 'id', 'revision', 'createdAt', 'updatedAt', 'input', 'providerBinding', 'inputDigest', 'generate'])
|
|
88
108
|
|| run.schemaVersion !== (run.input?.workflow_parameters === undefined ? 1 : 2) || run.recipe !== IMAGE_GENERATE_RECIPE || !RUN.test(run.id) || run.id !== expectedId
|
|
89
109
|
|| !Number.isSafeInteger(run.revision) || run.revision < 1 || !iso(run.createdAt) || !iso(run.updatedAt)
|
|
@@ -102,13 +122,26 @@ function validateGeneration(run, expectedId, normalized) {
|
|
|
102
122
|
return run;
|
|
103
123
|
}
|
|
104
124
|
function validate(run, expectedId) {
|
|
125
|
+
networkPart(run);
|
|
126
|
+
if (run?.recipe === VIDEO_RECIPE) {
|
|
127
|
+
let normalized;
|
|
128
|
+
try { normalized = normalizeVideoFlowInput(run.input); } catch (_) { flowFail('IRIS_FLOW_RECORD_INVALID', '视频流程输入无效'); }
|
|
129
|
+
if (!keys(run, ['schemaVersion', 'recipe', 'id', 'revision', 'createdAt', 'updatedAt', 'input', 'providerBinding', 'inputDigest', 'video', 'output', 'networkRequirement'],
|
|
130
|
+
['schemaVersion', 'recipe', 'id', 'revision', 'createdAt', 'updatedAt', 'input', 'providerBinding', 'inputDigest', 'video'])
|
|
131
|
+
|| run.schemaVersion !== 4 || run.id !== expectedId || !RUN.test(run.id) || !Number.isSafeInteger(run.revision) || run.revision < 1
|
|
132
|
+
|| !iso(run.createdAt) || !iso(run.updatedAt) || !BINDING.test(run.providerBinding)
|
|
133
|
+
|| JSON.stringify(run.input) !== JSON.stringify(normalized) || run.inputDigest !== inputDigest(run)
|
|
134
|
+
|| !keys(run.video, ['taskId', 'claimed']) || !TASK.test(run.video.taskId) || typeof run.video.claimed !== 'boolean'
|
|
135
|
+
|| run.output !== undefined && (!reference(run.output) || !run.video.claimed)) flowFail('IRIS_FLOW_RECORD_INVALID', '视频流程记录或绑定摘要冲突');
|
|
136
|
+
return run;
|
|
137
|
+
}
|
|
105
138
|
if (isAudioRecipe(run?.recipe)) return validateAudio(run, expectedId);
|
|
106
139
|
let normalized;
|
|
107
140
|
try { normalized = normalizeGenerationInput('image', run?.input); }
|
|
108
141
|
catch (_) { flowFail('IRIS_FLOW_RECORD_INVALID', '流程创作输入无效'); }
|
|
109
142
|
if (run.recipe === IMAGE_GENERATE_RECIPE) return validateGeneration(run, expectedId, normalized);
|
|
110
143
|
if (!keys(run, ['schemaVersion', 'recipe', 'id', 'revision', 'createdAt', 'updatedAt', 'source', 'input', 'providerBinding',
|
|
111
|
-
'inputDigest', 'edit', 'compare', 'output', 'comparison', 'selection'],
|
|
144
|
+
'inputDigest', 'edit', 'compare', 'output', 'comparison', 'selection', 'networkRequirement'],
|
|
112
145
|
['schemaVersion', 'recipe', 'id', 'revision', 'createdAt', 'updatedAt', 'source', 'input', 'providerBinding', 'inputDigest', 'edit', 'compare'])
|
|
113
146
|
|| run.schemaVersion !== 0 || run.recipe !== RECIPE || !RUN.test(run.id) || run.id !== expectedId
|
|
114
147
|
|| !Number.isSafeInteger(run.revision) || run.revision < 1 || !iso(run.createdAt) || !iso(run.updatedAt)
|
|
@@ -185,28 +218,41 @@ export function readFlowRun(root, id) {
|
|
|
185
218
|
throw error;
|
|
186
219
|
}
|
|
187
220
|
validate(record, id);
|
|
221
|
+
if (record.recipe === VIDEO_RECIPE) requireVideoRoot(root);
|
|
188
222
|
if (isAudioRecipe(record.recipe)) requireAudioRoot(root);
|
|
189
223
|
if (record.recipe === IMAGE_GENERATE_RECIPE) requireImageGenerationRoot(root);
|
|
190
224
|
return copy(record);
|
|
191
225
|
}
|
|
192
226
|
|
|
193
227
|
/** Internal immutable creation intent; HTTP callers cannot choose these identities. */
|
|
194
|
-
export function prepareFlowCreation({ source, input, providerBinding, recipe }) {
|
|
195
|
-
|
|
228
|
+
export function prepareFlowCreation({ source, input, providerBinding, recipe, networkRequirement }) {
|
|
229
|
+
const network = networkPart({ networkRequirement });
|
|
230
|
+
if (recipe === VIDEO_RECIPE) return { ...network, recipe, runId: flowId('run'), taskId: flowId('task'), createdAt: new Date().toISOString(), input, providerBinding };
|
|
231
|
+
if (isAudioRecipe(recipe)) return { ...network, recipe, runId: flowId('run'), taskId: flowId('task'), createdAt: new Date().toISOString(),
|
|
196
232
|
...(source ? { source } : {}), input, providerBinding };
|
|
197
|
-
if (!source) return { recipe: IMAGE_GENERATE_RECIPE, runId: flowId('run'), taskId: flowId('task'),
|
|
233
|
+
if (!source) return { ...network, recipe: IMAGE_GENERATE_RECIPE, runId: flowId('run'), taskId: flowId('task'),
|
|
198
234
|
createdAt: new Date().toISOString(), input, providerBinding };
|
|
199
|
-
return { runId: flowId('run'), taskId: flowId('task'), comparisonArtifactId: flowId('artifact'),
|
|
235
|
+
return { ...network, runId: flowId('run'), taskId: flowId('task'), comparisonArtifactId: flowId('artifact'),
|
|
200
236
|
createdAt: new Date().toISOString(), source, input, providerBinding };
|
|
201
237
|
}
|
|
202
238
|
|
|
203
239
|
export function validateFlowCreation(intent) {
|
|
240
|
+
if (!intent || typeof intent !== 'object' || Array.isArray(intent)) flowFail('IRIS_FLOW_RECORD_INVALID', '流程创建意图无效');
|
|
241
|
+
const network = networkPart(intent);
|
|
242
|
+
const { networkRequirement, ...payload } = intent;
|
|
243
|
+
intent = payload;
|
|
244
|
+
if (intent?.recipe === VIDEO_RECIPE) {
|
|
245
|
+
if (!keys(intent, ['recipe', 'runId', 'taskId', 'createdAt', 'input', 'providerBinding'])) flowFail('IRIS_FLOW_RECORD_INVALID', '视频创建意图无效');
|
|
246
|
+
const record = { ...network, schemaVersion: 4, recipe: VIDEO_RECIPE, id: intent.runId, revision: 1, createdAt: intent.createdAt, updatedAt: intent.createdAt,
|
|
247
|
+
input: intent.input, providerBinding: intent.providerBinding, video: { taskId: intent.taskId, claimed: false } };
|
|
248
|
+
record.inputDigest = inputDigest(record); return validate(record, record.id);
|
|
249
|
+
}
|
|
204
250
|
if (isAudioRecipe(intent?.recipe)) {
|
|
205
251
|
if (!keys(intent, ['recipe', 'runId', 'taskId', 'createdAt', 'input', 'providerBinding', 'source'],
|
|
206
252
|
['recipe', 'runId', 'taskId', 'createdAt', 'input', 'providerBinding', ...(intent.recipe === 'transcribe/v0' ? ['source'] : [])])) {
|
|
207
253
|
flowFail('IRIS_FLOW_RECORD_INVALID', '音频创建意图无效');
|
|
208
254
|
}
|
|
209
|
-
const record = { schemaVersion: 3, recipe: intent.recipe, id: intent.runId, revision: 1,
|
|
255
|
+
const record = { ...network, schemaVersion: 3, recipe: intent.recipe, id: intent.runId, revision: 1,
|
|
210
256
|
createdAt: intent.createdAt, updatedAt: intent.createdAt, input: intent.input, providerBinding: intent.providerBinding,
|
|
211
257
|
...(intent.source ? { source: intent.source } : {}), [AUDIO_RECIPES[intent.recipe].step]: { taskId: intent.taskId, claimed: false } };
|
|
212
258
|
record.inputDigest = inputDigest(record);
|
|
@@ -216,7 +262,7 @@ export function validateFlowCreation(intent) {
|
|
|
216
262
|
if (!keys(intent, ['recipe', 'runId', 'taskId', 'createdAt', 'input', 'providerBinding'])) {
|
|
217
263
|
flowFail('IRIS_FLOW_RECORD_INVALID', '文生图创建意图无效');
|
|
218
264
|
}
|
|
219
|
-
const record = { schemaVersion: intent.input?.workflow_parameters === undefined ? 1 : 2, recipe: IMAGE_GENERATE_RECIPE, id: intent.runId, revision: 1,
|
|
265
|
+
const record = { ...network, schemaVersion: intent.input?.workflow_parameters === undefined ? 1 : 2, recipe: IMAGE_GENERATE_RECIPE, id: intent.runId, revision: 1,
|
|
220
266
|
createdAt: intent.createdAt, updatedAt: intent.createdAt, input: intent.input,
|
|
221
267
|
providerBinding: intent.providerBinding, generate: { taskId: intent.taskId, claimed: false } };
|
|
222
268
|
record.inputDigest = inputDigest(record);
|
|
@@ -225,7 +271,7 @@ export function validateFlowCreation(intent) {
|
|
|
225
271
|
if (!keys(intent, ['runId', 'taskId', 'comparisonArtifactId', 'createdAt', 'source', 'input', 'providerBinding'])) {
|
|
226
272
|
flowFail('IRIS_FLOW_RECORD_INVALID', '流程创建意图无效');
|
|
227
273
|
}
|
|
228
|
-
const record = { schemaVersion: 0, recipe: RECIPE, id: intent.runId, revision: 1,
|
|
274
|
+
const record = { ...network, schemaVersion: 0, recipe: RECIPE, id: intent.runId, revision: 1,
|
|
229
275
|
createdAt: intent.createdAt, updatedAt: intent.createdAt, source: intent.source, input: intent.input,
|
|
230
276
|
providerBinding: intent.providerBinding, edit: { taskId: intent.taskId, claimed: false },
|
|
231
277
|
compare: { artifactId: intent.comparisonArtifactId } };
|
|
@@ -236,6 +282,7 @@ export function validateFlowCreation(intent) {
|
|
|
236
282
|
export function commitFlowCreation(root, intent, { allowCreate = true } = {}) {
|
|
237
283
|
inspectFlowRoot(root);
|
|
238
284
|
const record = validateFlowCreation(intent);
|
|
285
|
+
if (record.recipe === VIDEO_RECIPE) requireVideoRoot(root);
|
|
239
286
|
if (isAudioRecipe(record.recipe)) requireAudioRoot(root);
|
|
240
287
|
if (record.recipe === IMAGE_GENERATE_RECIPE) requireImageGenerationRoot(root);
|
|
241
288
|
let current;
|
|
@@ -261,7 +308,7 @@ export function createFlowRun(root, input) {
|
|
|
261
308
|
export function updateFlowRun(root, previous, patch) {
|
|
262
309
|
const current = readFlowRun(root, previous.id);
|
|
263
310
|
if (current.revision !== previous.revision) flowFail('IRIS_FLOW_REVISION_CONFLICT', '流程在本次操作期间已改变');
|
|
264
|
-
const allowed = isAudioRecipe(current.recipe) ? [flowStepId(current), 'output']
|
|
311
|
+
const allowed = isAudioRecipe(current.recipe) || current.recipe === VIDEO_RECIPE ? [flowStepId(current), 'output']
|
|
265
312
|
: current.recipe === IMAGE_GENERATE_RECIPE ? ['generate', 'output', 'selection'] : ['edit', 'output', 'comparison', 'selection'];
|
|
266
313
|
if (!keys(patch, allowed, [])) throw new TypeError('未知流程更新字段');
|
|
267
314
|
const record = { ...current, ...patch, revision: current.revision + 1, updatedAt: new Date().toISOString() };
|
package/lib/image-flow.js
CHANGED
|
@@ -1,17 +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
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';
|
|
12
15
|
import { AUDIO_RECIPES, AUDIO_TYPES, isAudioRecipe, normalizeAudioFlowInput, readImportAudio } from './audio-flow-input.js';
|
|
13
16
|
import { commitFlowCreation, flowFail, flowSha256, flowStep, flowStepId, initializeFlowRoot, inspectFlowRoot,
|
|
14
|
-
IMAGE_GENERATE_RECIPE, requireImageGenerationRoot, requireAudioRoot, listFlowRuns, prepareFlowCreation, readFlowRun, updateFlowRun } from './flow-store.js';
|
|
17
|
+
IMAGE_GENERATE_RECIPE, requireImageGenerationRoot, requireAudioRoot, requireVideoRoot, listFlowRuns, prepareFlowCreation, readFlowRun, updateFlowRun } from './flow-store.js';
|
|
15
18
|
|
|
16
19
|
function exact(input, allowed) {
|
|
17
20
|
if (!input || typeof input !== 'object' || Array.isArray(input) || Object.keys(input).some(key => !allowed.includes(key))) {
|
|
@@ -49,7 +52,7 @@ function facts(root, run) {
|
|
|
49
52
|
}
|
|
50
53
|
let status = 'not_started';
|
|
51
54
|
if (task?.acceptance === 'unknown' || ['unknown', 'failed', 'canceled'].includes(task?.outcome)) status = 'needs_attention';
|
|
52
|
-
else if (task?.deliveryState === 'ready') status = isAudioRecipe(run.recipe) ? run.output ? 'ready' : 'needs_result'
|
|
55
|
+
else if (task?.deliveryState === 'ready') status = isAudioRecipe(run.recipe) || run.recipe === VIDEO_RECIPE ? run.output ? 'ready' : 'needs_result'
|
|
53
56
|
: run.output && (run.recipe === IMAGE_GENERATE_RECIPE || run.comparison)
|
|
54
57
|
? run.selection ? 'selected' : 'awaiting_selection' : run.recipe === IMAGE_GENERATE_RECIPE ? 'needs_result' : 'needs_comparison';
|
|
55
58
|
else if (task?.deliveryState === 'failed') status = 'delivery_failed';
|
|
@@ -57,9 +60,10 @@ function facts(root, run) {
|
|
|
57
60
|
return { contractVersion: 0, run, task, status, ...(comparison ? { metrics: comparison.metadata } : {}) };
|
|
58
61
|
}
|
|
59
62
|
|
|
60
|
-
/** Fixed
|
|
61
|
-
export function createImageFlowService(runtime, { resolveImageCandidates, resolveAudioCandidates, resolveTaskAdapter } = {}) {
|
|
63
|
+
/** Fixed media recipes, sharing operation-scoped leases and the original Task. */
|
|
64
|
+
export function createImageFlowService(runtime, { resolveImageCandidates, resolveAudioCandidates, resolveVideoCandidates, resolveTaskAdapter } = {}) {
|
|
62
65
|
function creationInput(input) {
|
|
66
|
+
if (input?.recipe === VIDEO_RECIPE) { const { recipe, ...value } = input; return { recipe, ...normalizeVideoFlowInput(value) }; }
|
|
63
67
|
if (isAudioRecipe(input?.recipe)) {
|
|
64
68
|
const { recipe, ...value } = input;
|
|
65
69
|
return { recipe, ...normalizeAudioFlowInput(recipe, value) };
|
|
@@ -77,13 +81,16 @@ export function createImageFlowService(runtime, { resolveImageCandidates, resolv
|
|
|
77
81
|
const owned = creationInput(input);
|
|
78
82
|
inspectFlowRoot(root);
|
|
79
83
|
const audio = isAudioRecipe(owned.recipe);
|
|
80
|
-
|
|
84
|
+
const video = owned.recipe === VIDEO_RECIPE;
|
|
85
|
+
if (video) requireVideoRoot(root);
|
|
86
|
+
else if (audio) requireAudioRoot(root);
|
|
81
87
|
else if (!owned.source_artifact_id) requireImageGenerationRoot(root);
|
|
82
88
|
const source = owned.source_artifact_id ? audio ? readAudioSource(root, owned.source_artifact_id) : await readImageEditArtifact(root, owned.source_artifact_id, signal) : null;
|
|
83
89
|
const candidates = await candidatesFor(owned, signal);
|
|
84
90
|
const { recipe, ...audioInput } = owned;
|
|
85
91
|
return prepareFlowCreation({ ...(source ? { source: { artifactId: owned.source_artifact_id, sha256: flowSha256(source.bytes) } } : {}),
|
|
86
|
-
input: audio ? audioInput : owned, providerBinding: candidates[0].providerBinding,
|
|
92
|
+
input: audio || video ? audioInput : owned, providerBinding: candidates[0].providerBinding,
|
|
93
|
+
...(candidates[0].adapter.networkRequirement ? { networkRequirement: candidates[0].adapter.networkRequirement } : {}), ...(audio || video ? { recipe } : {}) });
|
|
87
94
|
}
|
|
88
95
|
function readAudioSource(root, id) {
|
|
89
96
|
const source = readCoreArtifactBytes(root, id);
|
|
@@ -91,11 +98,16 @@ export function createImageFlowService(runtime, { resolveImageCandidates, resolv
|
|
|
91
98
|
return source;
|
|
92
99
|
}
|
|
93
100
|
async function candidatesFor(runInput, signal, binding) {
|
|
94
|
-
const audio = isAudioRecipe(runInput.recipe), resolver = audio ? resolveAudioCandidates : resolveImageCandidates;
|
|
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
|
+
}
|
|
95
106
|
if (!resolver) flowFail('IRIS_FLOW_PROVIDER_REQUIRED', '创建或首次提交流程需要显式 Provider 配置');
|
|
96
107
|
signal.throwIfAborted();
|
|
97
|
-
const
|
|
108
|
+
const resolve = () => resolver({ modelRef: runInput.model_ref, signal, input: runInput,
|
|
98
109
|
...(audio ? { capability: AUDIO_RECIPES[runInput.recipe].capability } : {}) });
|
|
110
|
+
const candidates = await (runtime.networkWait ? runtime.networkWait(resolve, 'candidates') : resolve());
|
|
99
111
|
signal.throwIfAborted();
|
|
100
112
|
const parsed = parseModelRef(runInput.model_ref);
|
|
101
113
|
if (!Array.isArray(candidates) || candidates.length !== 1 || candidates[0].model !== runInput.model_ref
|
|
@@ -109,7 +121,8 @@ export function createImageFlowService(runtime, { resolveImageCandidates, resolv
|
|
|
109
121
|
|
|
110
122
|
async function adapterFor(task, signal) {
|
|
111
123
|
if (!resolveTaskAdapter) flowFail('IRIS_FLOW_PROVIDER_REQUIRED', '观察或远端补交付需要原 Task 的 Provider 配置');
|
|
112
|
-
const
|
|
124
|
+
const resolve = () => resolveTaskAdapter(task, { signal });
|
|
125
|
+
const adapter = await (runtime.networkWait ? runtime.networkWait(resolve, 'candidates') : resolve());
|
|
113
126
|
signal.throwIfAborted();
|
|
114
127
|
return adapter;
|
|
115
128
|
}
|
|
@@ -119,23 +132,26 @@ export function createImageFlowService(runtime, { resolveImageCandidates, resolv
|
|
|
119
132
|
let task = facts(root, run).task;
|
|
120
133
|
if (task?.attempts.length && !flowStep(run).claimed) flowFail('IRIS_FLOW_TASK_CONFLICT', '步骤检查点与已开始的 Task 冲突');
|
|
121
134
|
const runner = createProviderTaskRunner(runtime);
|
|
135
|
+
if (task) runner.assertIdle(task.id);
|
|
122
136
|
if (!task || !task.attempts.length) {
|
|
123
137
|
const audio = isAudioRecipe(run.recipe);
|
|
124
|
-
|
|
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', '流程网络要求已改变,不会隐式转换或解除');
|
|
125
141
|
if (run.recipe === 'transcribe/v0') {
|
|
126
142
|
const source = readAudioSource(root, run.source.artifactId);
|
|
127
|
-
candidates = candidates.map(candidate => ({ ...candidate, prepareInput: async ({ signal }) => {
|
|
143
|
+
candidates = candidates.map(candidate => ({ ...candidate, prepareInput: async ({ signal, taskId, attemptId, networkBudget, recordNetworkRoute, modelRef, providerId }) => {
|
|
128
144
|
const prepared = await prepareProviderInput(candidate.adapter, { model: candidate.providerModel, bytes: source.bytes,
|
|
129
|
-
filename: source.artifact.id + '.' + AUDIO_TYPES[source.artifact.mediaType], signal });
|
|
145
|
+
filename: source.artifact.id + '.' + AUDIO_TYPES[source.artifact.mediaType], signal }, { taskId, attemptId, networkBudget, recordNetworkRoute, modelRef, providerId });
|
|
130
146
|
return { audioUrl: prepared.url };
|
|
131
147
|
} }));
|
|
132
148
|
}
|
|
133
149
|
task = claimFlowTask(root, run.id);
|
|
134
150
|
if (!flowStep(run).claimed) run = updateFlowRun(root, run, { [flowStepId(run)]: { ...flowStep(run), claimed: true } });
|
|
135
151
|
signal.throwIfAborted();
|
|
136
|
-
const normalized = audio ? Object.fromEntries(Object.entries(run.input).filter(([key]) => key !== 'model_ref'))
|
|
152
|
+
const normalized = audio || video ? Object.fromEntries(Object.entries(run.input).filter(([key]) => key !== 'model_ref'))
|
|
137
153
|
: normalizeGenerationInput('image', run.input).providerInput;
|
|
138
|
-
task = (await runner.submit({ capability: audio ? AUDIO_RECIPES[run.recipe].capability : 'image', candidates, providerInput: normalized,
|
|
154
|
+
task = (await runner.submit({ capability: video ? 'video' : audio ? AUDIO_RECIPES[run.recipe].capability : 'image', candidates, providerInput: normalized,
|
|
139
155
|
sourceArtifactId: run.source?.artifactId, flowRunId: run.id })).task;
|
|
140
156
|
} else {
|
|
141
157
|
task = recoverCoreTask(root, task.id);
|
|
@@ -150,6 +166,12 @@ export function createImageFlowService(runtime, { resolveImageCandidates, resolv
|
|
|
150
166
|
signal.throwIfAborted();
|
|
151
167
|
if (task.deliveryState !== 'ready') return facts(root, run);
|
|
152
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
|
+
}
|
|
153
175
|
const output = readCoreArtifactBytes(root, task.artifactIds[0]);
|
|
154
176
|
if (!run.output) run = updateFlowRun(root, run, { output: reference(output.artifact) });
|
|
155
177
|
facts(root, run);
|
|
@@ -207,8 +229,8 @@ export function createImageFlowService(runtime, { resolveImageCandidates, resolv
|
|
|
207
229
|
flowFail('IRIS_FLOW_REVISION_CONFLICT', '流程已改变,请读取当前版本后明确选择');
|
|
208
230
|
}
|
|
209
231
|
facts(dataRoot, run);
|
|
210
|
-
if (isAudioRecipe(run.recipe)) {
|
|
211
|
-
if (name === 'flow.select') flowFail('IRIS_FLOW_SELECTION_INVALID', '
|
|
232
|
+
if (isAudioRecipe(run.recipe) || run.recipe === VIDEO_RECIPE) {
|
|
233
|
+
if (name === 'flow.select') flowFail('IRIS_FLOW_SELECTION_INVALID', '视频、音频和转写为单一结果,无需选择图片版本');
|
|
212
234
|
if (!run.output) flowFail('IRIS_FLOW_NOT_READY', '请先接回原任务的结果');
|
|
213
235
|
return { ...facts(dataRoot, run), export: exportCoreArtifact(dataRoot, run.output.artifactId, input.output_path) };
|
|
214
236
|
}
|
package/lib/index.js
CHANGED
|
@@ -262,13 +262,18 @@ export async function apply(ctx) {
|
|
|
262
262
|
else console.log('[iris] 工作台导入:' + imported.reason);
|
|
263
263
|
});
|
|
264
264
|
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
const
|
|
269
|
-
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;
|
|
270
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 => {
|
|
271
274
|
if (resumed.length) console.log(`[iris] 已恢复接管 ${resumed.length} 个后台任务`);
|
|
275
|
+
}, error => {
|
|
276
|
+
if (core.snapshot().state === 'open') console.error('[iris] 恢复后台任务失败(已隔离,不影响宿主):', error?.code || 'unknown');
|
|
272
277
|
});
|
|
273
278
|
|
|
274
279
|
// Owned background setup lets unload cancel a slow connection resolver during takeover.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// HTTP DTOs whitelist existing Core facts; never expose records or configuration wholesale.
|
|
2
2
|
import { isAudioRecipe } from './audio-flow-input.js';
|
|
3
|
+
import { VIDEO_RECIPE } from './video-flow-input.js';
|
|
3
4
|
const STATUSES = new Set(['not_started', 'running', 'needs_attention', 'delivery_failed',
|
|
4
5
|
'needs_comparison', 'needs_result', 'awaiting_selection', 'selected', 'ready']);
|
|
5
6
|
|
|
6
7
|
export function projectLocalApiFlow(facts) {
|
|
7
|
-
if (facts.contractVersion !== 0 || !['image-edit/v0', 'image-generate/v0'].includes(facts.run.recipe) && !isAudioRecipe(facts.run.recipe) || !STATUSES.has(facts.status)) {
|
|
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)) {
|
|
8
9
|
throw new TypeError('不支持的流程契约或状态');
|
|
9
10
|
}
|
|
10
11
|
const { run, task } = facts;
|
|
@@ -15,10 +16,13 @@ export function projectLocalApiFlow(facts) {
|
|
|
15
16
|
input: { ...(isAudioRecipe(run.recipe) ? run.recipe === 'speech/v0' ? { text: run.input.text,
|
|
16
17
|
...(run.input.voice !== undefined ? { voice: run.input.voice } : {}) } : {} : { prompt: run.input.prompt }), modelRef: run.input.model_ref,
|
|
17
18
|
...(run.input.size !== undefined ? { size: run.input.size } : {}),
|
|
19
|
+
...(run.input.duration !== undefined ? { duration: run.input.duration } : {}),
|
|
18
20
|
...(run.input.workflow_parameters !== undefined ? { workflowParameters: run.input.workflow_parameters } : {}) },
|
|
19
21
|
source: ref(run.source), output: ref(run.output), comparison: ref(run.comparison), selection: ref(run.selection),
|
|
20
22
|
task: task ? { id: task.id, acceptance: task.acceptance, outcome: task.outcome,
|
|
21
|
-
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,
|
|
22
26
|
metrics: facts.metrics ? Object.fromEntries(['ratio', 'diffPixels', 'totalPixels', 'width', 'height']
|
|
23
27
|
.map(key => [key, facts.metrics[key]])) : null
|
|
24
28
|
};
|