@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.
Files changed (174) hide show
  1. package/CHANGELOG.md +63 -1
  2. package/README.en.md +20 -13
  3. package/README.md +20 -13
  4. package/bin/dsh-iris.js +143 -51
  5. package/docs/AGENT_CORE_QUICKSTART.md +2 -2
  6. package/docs/AGENT_QUICKSTART.md +1 -1
  7. package/docs/ARCHITECTURE.md +5 -5
  8. package/docs/ARTIFACT_MANIFEST.md +4 -0
  9. package/docs/AUDIO_FLOWS.md +20 -4
  10. package/docs/BETA_SCOPE.md +42 -0
  11. package/docs/CONTACT_SHEET.md +1 -1
  12. package/docs/CORE_CONNECTIONS.md +3 -1
  13. package/docs/CORE_QUICKSTART.md +7 -5
  14. package/docs/CORE_RUNTIME_CONTRACT.md +3 -3
  15. package/docs/CORE_SERVICE.md +9 -5
  16. package/docs/CORE_SKILLS.md +2 -2
  17. package/docs/DSH_COMPATIBILITY.md +11 -1
  18. package/docs/DSH_CORE_MIGRATION.md +6 -0
  19. package/docs/DSH_RUNTIME.md +2 -2
  20. package/docs/HEADLESS_CLI.md +29 -4
  21. package/docs/HOST_ADAPTER_CONTRACT.md +5 -3
  22. package/docs/IMAGE_FLOW.md +2 -2
  23. package/docs/IMAGE_GENERATION.md +1 -1
  24. package/docs/LOCAL_API.md +2 -2
  25. package/docs/LOCAL_API_CONTRACT.md +17 -15
  26. package/docs/LOCAL_CREATION.md +2 -2
  27. package/docs/LOCAL_OPERATIONS.md +2 -2
  28. package/docs/LOCAL_SELECTION.md +1 -1
  29. package/docs/LOCAL_WEB.md +11 -5
  30. package/docs/MCP.md +30 -9
  31. package/docs/MEDIA_SCHEDULING.md +72 -0
  32. package/docs/NETWORK_RESOURCES.md +106 -0
  33. package/docs/NETWORK_ROUTING.md +103 -0
  34. package/docs/NETWORK_WORKBENCH.md +65 -0
  35. package/docs/PANEL_EXTENSIONS.md +1 -1
  36. package/docs/PLUGINS.md +9 -5
  37. package/docs/PLUGIN_DEVELOPMENT.md +39 -0
  38. package/docs/PLUGIN_STARTUP.md +15 -2
  39. package/docs/PROVIDER_ADAPTER_CONTRACT.md +8 -2
  40. package/docs/PROVIDER_CREDENTIALS.md +67 -0
  41. package/docs/PROVIDER_EXTENSIONS.md +8 -4
  42. package/docs/PROVIDER_HEALTH.md +4 -2
  43. package/docs/PROVIDER_NETWORK.md +45 -0
  44. package/docs/PROVIDER_PLUGIN_CONTRACT.md +24 -0
  45. package/docs/PROVIDER_SETTINGS.md +13 -7
  46. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +3 -1
  47. package/docs/ROADMAP.md +6 -6
  48. package/docs/SECURITY.md +1 -1
  49. package/docs/SYNC_DELIVERY_RECOVERY.md +5 -1
  50. package/docs/VIDEO_FLOWS.md +89 -0
  51. package/docs/VISION_MODEL.md +2 -0
  52. package/docs/WEB_PROTOTYPE.md +3 -3
  53. package/docs/WEB_RECOVERY.md +2 -2
  54. package/docs/WORKBENCH_ARTIFACTS.md +6 -0
  55. package/docs/WORKBENCH_LOGIN.md +2 -2
  56. package/docs/releases/0.3.0-beta.1.md +34 -0
  57. package/docs/screenshots.md +2 -2
  58. package/lib/actions.js +5 -4
  59. package/lib/adapters.js +58 -41
  60. package/lib/api.js +23 -13
  61. package/lib/application-runtime.js +44 -15
  62. package/lib/application-writer.js +193 -0
  63. package/lib/client.d.ts +3 -3
  64. package/lib/client.js +62 -25
  65. package/lib/comfyui-adapter.js +3 -2
  66. package/lib/comfyui-client.js +10 -1
  67. package/lib/command-service.js +18 -5
  68. package/lib/config.js +13 -9
  69. package/lib/configured-image-runtime.js +28 -9
  70. package/lib/connection-skill.js +9 -0
  71. package/lib/core-artifact-store.js +68 -26
  72. package/lib/core-bridge-tools.js +5 -3
  73. package/lib/core-capability-catalog.js +21 -11
  74. package/lib/core-delivery-cache.js +8 -3
  75. package/lib/core-service-client.js +6 -2
  76. package/lib/core-tasks.js +86 -11
  77. package/lib/creation-request-store.js +8 -5
  78. package/lib/doctor.js +2 -2
  79. package/lib/dsh-core-adapter.js +61 -9
  80. package/lib/dsh-provider-config.js +1 -1
  81. package/lib/dsh-service-bridge.js +1 -0
  82. package/lib/flow-store.js +64 -17
  83. package/lib/image-flow.js +39 -17
  84. package/lib/index.js +10 -5
  85. package/lib/local-api-projection.js +6 -2
  86. package/lib/local-api.js +137 -49
  87. package/lib/local-creation.js +12 -5
  88. package/lib/local-operations.js +2 -2
  89. package/lib/local-request-root.js +2 -2
  90. package/lib/local-web-assets.js +2 -0
  91. package/lib/mcp-http.js +82 -9
  92. package/lib/mcp-server.js +23 -1
  93. package/lib/media-scheduler.js +194 -0
  94. package/lib/model-directory-service.js +25 -8
  95. package/lib/music-transport.js +4 -2
  96. package/lib/network-config-service.js +123 -0
  97. package/lib/network-config.js +127 -0
  98. package/lib/network-doctor.js +94 -0
  99. package/lib/network-management.js +51 -0
  100. package/lib/network-policy.js +19 -0
  101. package/lib/network-route-evidence.js +38 -0
  102. package/lib/network-routing.js +283 -0
  103. package/lib/operation-runner.js +83 -38
  104. package/lib/operation-store.js +6 -6
  105. package/lib/plugins.d.ts +25 -6
  106. package/lib/plugins.js +2 -1
  107. package/lib/provider-adapter.js +2 -2
  108. package/lib/provider-adapters.js +37 -5
  109. package/lib/provider-catalog.js +33 -16
  110. package/lib/provider-config-format.js +51 -0
  111. package/lib/provider-config-service.js +52 -22
  112. package/lib/provider-context.d.ts +90 -0
  113. package/lib/provider-context.js +42 -0
  114. package/lib/provider-credentials.js +97 -0
  115. package/lib/provider-diagnostics.js +5 -5
  116. package/lib/provider-health.js +10 -5
  117. package/lib/provider-monitor.js +5 -3
  118. package/lib/provider-network.js +52 -0
  119. package/lib/provider-protocol.js +3 -2
  120. package/lib/provider-registry.js +15 -5
  121. package/lib/provider-response.js +10 -0
  122. package/lib/provider-settings.js +14 -12
  123. package/lib/provider-task-runner.js +113 -37
  124. package/lib/provider-transport.js +16 -0
  125. package/lib/render.js +11 -1
  126. package/lib/scheduling-policy.js +66 -0
  127. package/lib/tasks.js +3 -2
  128. package/lib/video-flow-input.js +16 -0
  129. package/lib/video-input.js +12 -5
  130. package/lib/vision-model-routing.js +2 -0
  131. package/lib/web/app.js +107 -46
  132. package/lib/web/connections.js +151 -12
  133. package/lib/web/creation.js +29 -25
  134. package/lib/web/flow-view.js +4 -3
  135. package/lib/web/gallery.js +17 -16
  136. package/lib/web/index.html +58 -5
  137. package/lib/web/media-client.js +18 -4
  138. package/lib/web/network.js +232 -0
  139. package/lib/web/plugin-panel.js +4 -2
  140. package/lib/web/plugin-status.js +140 -0
  141. package/lib/web/providers.js +34 -13
  142. package/lib/web/recovery.js +2 -2
  143. package/lib/web/styles.css +54 -1
  144. package/lib/web/transport.js +6 -1
  145. package/lib/workbench-artifacts.js +3 -2
  146. package/package.json +5 -2
  147. package/skills/iris-core/references/comfyui.md +1 -1
  148. package/templates/plugin-starter/LICENSE +21 -0
  149. package/templates/plugin-starter/README.md +74 -0
  150. package/templates/plugin-starter/assets/peach.png +0 -0
  151. package/templates/plugin-starter/assets/violet.png +0 -0
  152. package/templates/plugin-starter/index.mjs +27 -0
  153. package/templates/plugin-starter/package.json +18 -0
  154. package/templates/plugin-starter/scripts/core-cli.mjs +20 -0
  155. package/templates/plugin-starter/scripts/environment.mjs +65 -0
  156. package/templates/plugin-starter/scripts/mcp-probe.mjs +22 -0
  157. package/templates/plugin-starter/scripts/setup.mjs +42 -0
  158. package/templates/plugin-starter/scripts/smoke.mjs +76 -0
  159. package/templates/plugin-starter/web/board.css +27 -0
  160. package/templates/plugin-starter/web/board.mjs +160 -0
  161. package/templates/plugin-starter/web/png.mjs +23 -0
  162. package/templates/provider-starter/LICENSE +21 -0
  163. package/templates/provider-starter/README.md +50 -0
  164. package/templates/provider-starter/index.d.mts +5 -0
  165. package/templates/provider-starter/index.mjs +165 -0
  166. package/templates/provider-starter/package.json +13 -0
  167. package/templates/provider-starter/scripts/core-cli.mjs +22 -0
  168. package/templates/provider-starter/scripts/environment.mjs +122 -0
  169. package/templates/provider-starter/scripts/fixture.mjs +54 -0
  170. package/templates/provider-starter/scripts/mcp-probe.mjs +21 -0
  171. package/templates/provider-starter/scripts/protocol-smoke.mjs +48 -0
  172. package/templates/provider-starter/scripts/setup.mjs +44 -0
  173. package/templates/provider-starter/scripts/smoke.mjs +145 -0
  174. 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 !== 3) {
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 inputDigest(run) {
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
- if (isAudioRecipe(recipe)) return { recipe, runId: flowId('run'), taskId: flowId('task'), createdAt: new Date().toISOString(),
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 image/audio recipes, sharing operation-scoped leases and the original Task. */
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
- if (audio) requireAudioRoot(root);
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, ...(audio ? { recipe } : {}) });
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 candidates = await resolver({ modelRef: runInput.model_ref, signal, input: runInput,
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 adapter = await resolveTaskAdapter(task, { signal });
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
- let candidates = await candidatesFor({ ...run.input, ...(audio ? { recipe: run.recipe } : {}) }, signal, run.providerBinding);
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
- // 接管上次进程未跑完的异步任务(恢复循环本身在 tasks.js 里逐任务防御)
266
- guard('恢复后台任务', () => {
267
- const resumed = tasks.resumePending((t) => {
268
- const p = store.providerById(t.providerId);
269
- return p ? pollDeps(p, t.cap) : null;
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 } : null,
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
  };