@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/local-api.js CHANGED
@@ -8,7 +8,8 @@ import { createCoreRuntime } from './core-runtime.js';
8
8
  import { inspectFlowRoot, listFlowRuns, readFlowRun } from './flow-store.js';
9
9
  import { inspectCoreArtifactMetadata, readCoreArtifactBytes, listCoreArtifacts } from './core-artifacts.js';
10
10
  import { isPluginSourceImage } from './plugin-source-image.js';
11
- import { imageCandidatesFromCatalog, ttsCandidatesFromCatalog, transcribeCandidatesFromCatalog, loadProviderCatalog, providerTaskBinding } from './provider-catalog.js';
11
+ import { imageCandidatesFromCatalog, videoCandidatesFromCatalog, ttsCandidatesFromCatalog, transcribeCandidatesFromCatalog, loadProviderCatalog, providerTaskBinding } from './provider-catalog.js';
12
+ import { videoModelSupportsMode } from './video-input.js';
12
13
  import { AUDIO_TYPES, MAX_AUDIO_INPUT_BYTES, isAudioRecipe } from './audio-flow-input.js';
13
14
  import { CREATION_KINDS } from './creation-request-store.js';
14
15
  import { supportsImageEdit } from './provider-protocol.js';
@@ -33,9 +34,15 @@ import { CONNECTION_TEMPLATE_EXAMPLE } from './connection-template.js';
33
34
  import { checkedBridgeCapabilities, DEFAULT_BRIDGE_CAPABILITIES } from './core-bridge-selection.js';
34
35
  import { createCoreCapabilityCatalog, projectCoreSessionOperations, CORE_CAPABILITY_GROUPS } from './core-capability-catalog.js';
35
36
  import { MCP_CONNECTION_PRESETS, MCP_CONFIG_FORMATS } from './mcp-delivery.js';
37
+ import { VIDEO_RECIPE, VIDEO_TYPES, MAX_VIDEO_BYTES } from './video-flow-input.js';
36
38
  import { checkedPluginSelection, hasPluginSelection } from './plugin-selection.js';
37
39
  import { readAllOperations, projectOperation } from './operation-store.js';
38
40
  import { createWorkbenchAuth, WorkbenchAuthError } from './workbench-auth.js';
41
+ import { createProviderNetworkManager } from './network-routing.js';
42
+ import { providerNetworkRequirement } from './network-policy.js';
43
+ import { networkManagementSettings, manageNetworkResources, NETWORK_MANAGEMENT_ACTIONS } from './network-management.js';
44
+ import { networkDoctor, probeNetworkRoute } from './network-doctor.js';
45
+ import { createMediaScheduler } from './media-scheduler.js';
39
46
 
40
47
  export const MAX_LOCAL_API_IMAGE_BYTES = 64 * 1024 * 1024;
41
48
  const RUN = /^run_[a-f0-9]{24}$/;
@@ -54,16 +61,25 @@ function fail(code, message, status) { throw new LocalApiError(code, message, st
54
61
  function httpError(error) {
55
62
  if (error instanceof LocalApiError) return error;
56
63
  if (error instanceof WorkbenchAuthError) return new LocalApiError(error.code, error.message, error.status);
64
+ if (error instanceof ProviderContractError && error.category === 'invalid_request') return new LocalApiError('IRIS_CONFIG_INPUT_INVALID',
65
+ '供应商配置或参数无效,请核对所选协议要求的 API 根地址、模型和参数', 400);
66
+ if (/^IRIS_NETWORK_/.test(error?.code || '')) return new LocalApiError(error.code, error.message, 409);
57
67
  if (/^IRIS_PLUGIN_/.test(error?.code || '')) return new LocalApiError(error.code,
58
68
  error.code === 'IRIS_PLUGIN_INPUT_INVALID' ? '业务输入不符合当前插件参数要求' : '插件业务未授权、缺失或版本不一致;请读取原 Operation 并核对准确插件版本',
59
69
  error.code === 'IRIS_PLUGIN_INPUT_INVALID' ? 400 : error.code === 'IRIS_PLUGIN_OPERATION_DENIED' ? 403 : 409);
60
70
  if (['IRIS_CONFIG_CHANGED', 'IRIS_CONFIG_BUSY'].includes(error?.code)) return new LocalApiError(error.code,
61
71
  '配置已变化或有其他写入,请保留草稿并重新读取后核对', 409);
72
+ if (error?.code === 'IRIS_PROVIDER_CREDENTIAL_UNAVAILABLE') return new LocalApiError(error.code,
73
+ '环境凭据未就绪,请由所有者检查 Core 进程中的指定变量;不会改绑或重新提交任务', 409);
74
+ if (error?.providerCode === 'IRIS_PROVIDER_DIRECTORY_CREDENTIALS') return new LocalApiError(error.providerCode,
75
+ '上游模型目录包含认证内容,已拒绝读取与保存;请检查该服务的目录响应', 502);
62
76
  if (error?.code === 'IRIS_CONFIG_INPUT_INVALID' || error?.code === 'IRIS_FUNCTION_POLICY_INVALID') return new LocalApiError('IRIS_CONFIG_INPUT_INVALID',
63
77
  '设置无效,请检查地址、协议、模型用途和密钥动作', 400);
64
78
  if (error?.code === 'IRIS_OPERATION_NOT_FOUND') return new LocalApiError(error.code, 'Operation 不存在', 404);
65
79
  if (error?.code === 'IRIS_OPERATION_KEY_CONFLICT') return new LocalApiError(error.code, '该 UUID 已绑定其他业务、输入或依赖,请先查询原操作', 409);
66
80
  if (error?.code === 'IRIS_OPERATION_QUEUE_FULL') return new LocalApiError(error.code, '活动操作已达上限,本次未接收', 429);
81
+ if (/^IRIS_SCHEDULER_(?:QUEUE_FULL|QUEUE_TIMEOUT)$/.test(error?.code || '')) return new LocalApiError(error.code, error.message, 429);
82
+ if (['IRIS_APP_OPERATION_BUSY', 'IRIS_PROVIDER_TASK_BUSY'].includes(error?.code)) return new LocalApiError(error.code, '原 Run 或 Task 正在执行,请查询原操作', 409);
67
83
  if (error?.code === 'IRIS_OPERATION_INPUT_INVALID' || error?.code === 'IRIS_OPERATION_KEY_INVALID') return new LocalApiError(error.code, 'Operation 输入无效', 400);
68
84
  if (/^IRIS_CONNECTION_/.test(error?.code || '')) {
69
85
  const status = error.code === 'IRIS_CONNECTION_AUTH_REQUIRED' ? 401 : error.code === 'IRIS_CONNECTION_CAPABILITY_DENIED' ? 403
@@ -84,7 +100,7 @@ function httpError(error) {
84
100
  if (error?.code === 'IRIS_CREATION_KEY_CONFLICT') return new LocalApiError('IRIS_API_KEY_CONFLICT', '该 Key 已绑定其他输入', 409);
85
101
  if (error?.code === 'IRIS_CREATION_NOT_FOUND') return new LocalApiError('IRIS_API_NOT_FOUND', '该实例中没有此创建请求', 404);
86
102
  if (['IRIS_CREATION_KEY_INVALID', 'IRIS_CREATION_INPUT_INVALID', 'IRIS_GENERATION_INPUT_INVALID',
87
- 'IRIS_IMAGE_IMPORT_INVALID', 'IRIS_IMAGE_EDIT_INPUT_INVALID', 'IRIS_FLOW_INPUT_INVALID', 'IRIS_COMFYUI_PARAMETERS_INVALID'].includes(error?.code)) {
103
+ 'IRIS_IMAGE_IMPORT_INVALID', 'IRIS_IMAGE_EDIT_INPUT_INVALID', 'IRIS_FLOW_INPUT_INVALID', 'IRIS_VIDEO_FLOW_INPUT_INVALID', 'IRIS_COMFYUI_PARAMETERS_INVALID'].includes(error?.code)) {
88
104
  return new LocalApiError('IRIS_API_INPUT_INVALID', '创作输入或静态图片无效,请检查来源、模型和参数', 400);
89
105
  }
90
106
  if (error?.code === 'IRIS_CREATION_MEDIA_MISMATCH') return new LocalApiError('IRIS_API_MEDIA_UNSUPPORTED', '图片真实格式与 Content-Type 不一致', 415);
@@ -114,7 +130,8 @@ function reportUnexpected(error, requestId) {
114
130
  // Keep call sites for diagnosis, without the potentially private exception message or directory names.
115
131
  const frames = String(error?.stack || '').split('\n').slice(1, 7)
116
132
  .map(line => line.replace(/(?:file:\/\/)?(?:\/[^\s()]+)+(?=:\d+:\d+)/g, value => path.basename(value)));
117
- console.error(JSON.stringify({ event: 'iris_local_api_error', requestId, frames }));
133
+ const code = ['IRIS_SCHEDULER_QUEUE_TIMEOUT', 'IRIS_SCHEDULER_QUEUE_FULL', 'IRIS_SCHEDULER_CLOSED'].includes(error?.code) ? error.code : undefined;
134
+ console.error(JSON.stringify({ event: 'iris_local_api_error', requestId, ...(code ? { code } : {}), frames }));
118
135
  }
119
136
 
120
137
  function headerCount(request, name) {
@@ -160,7 +177,8 @@ const json = (request, response, value, status = 200) => send(request, response,
160
177
  /** Internal Host, read-only by default. Construction performs no I/O and never binds a port. */
161
178
  export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWrites = false, serveWorkbench = false,
162
179
  allowExecution = false, allowSelection = false, allowMcp = false, connectionsFile, executionPorts, allowConnectionManagement = false, connectionStore, providerRegistry, pluginRegistry,
163
- allowProviderManagement = false, panelManifestPath, panelContributions, allowedOperations, workbenchAuth } = {}) {
180
+ allowProviderManagement = false, allowNetworkManagement = false, panelManifestPath, panelContributions, allowedOperations, workbenchAuth, networkConfigPath, networkManager,
181
+ scheduling, scheduler: suppliedScheduler } = {}) {
164
182
  if (typeof allowExecution !== 'boolean') throw new TypeError('allowExecution 必须是布尔值');
165
183
  if (typeof allowSelection !== 'boolean') throw new TypeError('allowSelection 必须是布尔值');
166
184
  if (typeof allowMcp !== 'boolean') throw new TypeError('allowMcp 必须是布尔值');
@@ -175,8 +193,13 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
175
193
  fail('IRIS_API_CONFIG_INVALID', '工作台口令认证只接受明确的私有 directory,且仅用于 web serve');
176
194
  }
177
195
  if (typeof allowProviderManagement !== 'boolean') throw new TypeError('allowProviderManagement 必须是布尔值');
196
+ if (typeof allowNetworkManagement !== 'boolean') throw new TypeError('allowNetworkManagement 必须是布尔值');
197
+ if (allowNetworkManagement && !networkConfigPath) fail('IRIS_API_CONFIG_INVALID', '网络管理需要显式network-config路径');
178
198
  if (pluginRegistry && providerRegistry && providerRegistry !== pluginRegistry.providers) fail('IRIS_API_CONFIG_INVALID', '插件与 Provider 注册器须来自同一装配实例');
179
199
  const suppliedRegistry = pluginRegistry?.providers ?? providerRegistry;
200
+ if (networkConfigPath !== undefined && (typeof networkConfigPath !== 'string' || !path.isAbsolute(networkConfigPath))) fail('IRIS_API_CONFIG_INVALID', '网络文件必须是显式绝对路径');
201
+ if (networkManager && (typeof networkManager.resolveProviderNetwork !== 'function' || typeof networkManager.dispose !== 'function')) throw new TypeError('networkManager 需要实例解析与释放方法');
202
+ if ((networkConfigPath || scheduling || suppliedScheduler) && suppliedRegistry && !networkManager) fail('IRIS_API_CONFIG_INVALID', '借用 Provider 注册器并使用网络或调度配置时,请同时传入由调用者持有的网络管理实例,按顺序统一释放');
180
203
  const capabilityCatalog = createCoreCapabilityCatalog({ registry: pluginRegistry });
181
204
  const pluginExecution = allowedOperations !== undefined;
182
205
  const selectedOperations = pluginExecution ? capabilityCatalog.selectOperations(allowedOperations) : [];
@@ -191,11 +214,18 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
191
214
  if (allowExecution && !providerConfigPath && !executionPorts) fail('IRIS_API_CONFIG_INVALID', '执行模式需要显式 Provider 配置');
192
215
  if (connectionsFile !== undefined && (typeof connectionsFile !== 'string' || !path.isAbsolute(connectionsFile))) fail('IRIS_API_CONFIG_INVALID', '连接授权须为显式根外文件');
193
216
  let state = 'new', origin = null, tokenDigest = null, application, server, physicalRoot;
217
+ const scheduler = suppliedScheduler || networkManager?.scheduler || (scheduling ? createMediaScheduler(scheduling) : undefined);
218
+ if (scheduler && networkManager && networkManager.scheduler !== scheduler) fail('IRIS_API_CONFIG_INVALID', '应用与网络管理必须共用同一调度实例');
219
+ const ownedScheduler = Boolean(scheduler && !suppliedScheduler && !networkManager?.scheduler);
220
+ const ownedNetwork = !networkManager && (networkConfigPath !== undefined || Boolean(scheduler));
221
+ const networks = networkManager || (ownedNetwork ? createProviderNetworkManager({ networkConfigPath, scheduler }) : undefined);
222
+ const resolveProviderNetwork = ({ provider, ...context }) => networks?.resolveProviderNetwork({ ...context,
223
+ networkRequirement: context.networkRequirement || providerNetworkRequirement(provider) });
194
224
  let browserAuth;
195
225
  let providerMonitor;
196
- let runner, runnerStarted = false, connections, mcp, managed, imageGenerationEnabled = false, audioEnabled = false;
226
+ let runner, runnerStarted = false, connections, mcp, managed, imageGenerationEnabled = false, audioEnabled = false, videoEnabled = false;
197
227
  let registry = suppliedRegistry, panel = DEFAULT_PANEL, pluginPanel, providerManagementBusy = false;
198
- const capabilityFacts = () => capabilityCatalog.snapshot({ audioEnabled, panelContributions: pluginPanel?.selection, allowedOperations: allowedPluginIds });
228
+ const capabilityFacts = () => capabilityCatalog.snapshot({ audioEnabled, videoEnabled, panelContributions: pluginPanel?.selection, allowedOperations: allowedPluginIds });
199
229
  const changedAuthorizations = new Set();
200
230
  let loginTicket = null;
201
231
  const browserSessions = new Set();
@@ -214,7 +244,7 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
214
244
  return [...facts.tools, ...facts.pluginTools].filter(tool => tool.scope !== 'execute'
215
245
  || (tool.name === 'iris_core_plugin_call' ? pluginExecution : !pluginExecution || allowExecution)).map(tool => tool.name);
216
246
  };
217
- const availableCapabilities = () => capabilityCatalog.snapshot({ audioEnabled }).groups;
247
+ const availableCapabilities = () => capabilityCatalog.snapshot({ audioEnabled, videoEnabled }).groups;
218
248
 
219
249
  function issueLoginLink() {
220
250
  const ticket = crypto.randomBytes(32).toString('base64url');
@@ -287,6 +317,7 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
287
317
  async function requireArtifactCapability(caller, artifactId) {
288
318
  if (caller.owner) return;
289
319
  const metadata = await application.run('reader', runtime => runtime.run('inspect', ({ dataRoot }) => inspectCoreArtifactMetadata(dataRoot, artifactId)));
320
+ scope(caller, 'read'); // A revocation during the metadata read invalidates the original handshake too.
290
321
  const selection = connections.selection(caller.connectionId, caller.runtimeInstanceId);
291
322
  if (isPluginSourceImage(metadata) && (selection.pluginOperations || []).some(pin => selectedOperations.some(item => hasPluginSelection([pin], item.dependency) && item.artifactInputs?.length))) return;
292
323
  if (metadata.metadata?.pluginOperationId) {
@@ -308,7 +339,7 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
308
339
  // alone only grant the declared eligible source images handled above.
309
340
  if (CORE_CAPABILITY_GROUPS.some(group => selection.capabilities.includes(group.id)
310
341
  && (metadata.mediaType.startsWith('image/') ? group.media === 'image'
311
- : metadata.mediaType.startsWith('audio/') ? group.requiresAudio
342
+ : metadata.mediaType.startsWith('video/') ? group.media === 'video' : metadata.mediaType.startsWith('audio/') ? group.requiresAudio
312
343
  : metadata.mediaType === 'text/plain' && group.media === 'transcribe'))) return;
313
344
  fail('IRIS_CONNECTION_CAPABILITY_DENIED', '此连接没有选择该作品的媒体功能或图片来源用途', 403);
314
345
  }
@@ -344,6 +375,7 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
344
375
  }
345
376
  if (url.pathname.startsWith('/api/v0/connection-management')) return manage(request, response, url, caller);
346
377
  if (url.pathname.startsWith('/api/v0/provider-management')) return manageProviders(request, response, url, caller);
378
+ if (url.pathname.startsWith('/api/v0/network-management')) return manageNetworks(request, response, url, caller);
347
379
  const selecting = /^\/api\/v0\/runs\/([^/]+)\/selection$/.exec(url.pathname);
348
380
  if (selecting) return selectRun(request, response, selecting[1], caller);
349
381
  const connection = /^\/api\/v0\/connections\/([a-f0-9-]{36})\/(handshake|disconnect|client-info)$/.exec(url.pathname);
@@ -380,10 +412,11 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
380
412
  const result = await runner.submit({ key: request.headers['idempotency-key'], runId: continuing[1], expectedRunRevision: value.expectedRevision });
381
413
  return json(request, response, result, result.idempotentReplay ? 200 : 202);
382
414
  }
383
- scope(caller, 'create', ({ '/api/v0/images': 'iris_core_import_image', '/api/v0/runs': 'iris_core_create_run',
415
+ const creationTool = ({ '/api/v0/images': 'iris_core_import_image', '/api/v0/runs': 'iris_core_create_run',
384
416
  '/api/v0/generations': 'iris_core_create_generation', '/api/v0/audio': 'iris_core_import_audio',
385
- '/api/v0/speeches': 'iris_core_create_speech', '/api/v0/transcriptions': 'iris_core_create_transcription' })[url.pathname]);
386
- return create(request, response, url);
417
+ '/api/v0/speeches': 'iris_core_create_speech', '/api/v0/transcriptions': 'iris_core_create_transcription', '/api/v0/videos': 'iris_core_create_video' })[url.pathname];
418
+ scope(caller, 'create', creationTool);
419
+ return create(request, response, url, caller, creationTool);
387
420
  }
388
421
 
389
422
  async function selectRun(request, response, runId, caller) {
@@ -429,11 +462,11 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
429
462
  if (['GET', 'HEAD'].includes(request.method) && url.pathname === '/api/v0/provider-management/settings') {
430
463
  return json(request, response, await executeConfigCommand(providerConfigPath, 'settings.show', {}, { registry }));
431
464
  }
432
- const monitor = () => providerMonitor ||= createProviderMonitor({ configPath: providerConfigPath, registry });
465
+ const monitor = () => providerMonitor ||= createProviderMonitor({ configPath: providerConfigPath, registry, resolveProviderNetwork });
433
466
  if (['GET', 'HEAD'].includes(request.method) && url.pathname === '/api/v0/provider-management/diagnostics') {
434
467
  const settings = await executeConfigCommand(providerConfigPath, 'settings.show', {}, { registry });
435
468
  return json(request, response, { ...projectProviderDiagnostics(loadProviderCatalog(providerConfigPath), registry), revision: settings.revision,
436
- monitors: monitor().snapshot(), execution: allowExecution, imageGeneration: imageGenerationEnabled, audio: audioEnabled });
469
+ monitors: monitor().snapshot(), execution: allowExecution, imageGeneration: imageGenerationEnabled, audio: audioEnabled, video: videoEnabled });
437
470
  }
438
471
  const command = { '/api/v0/provider-management/save': 'settings.save', '/api/v0/provider-management/discover': 'settings.discover',
439
472
  '/api/v0/provider-management/policies': 'settings.policies', '/api/v0/provider-management/enabled': 'settings.enabled',
@@ -466,11 +499,11 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
466
499
  const result = await application.run('writer', runtime => runtime.run('execute', async ({ dataRoot }) => {
467
500
  const dependencies = providerDependencies(dataRoot, value.providerId);
468
501
  if (dependencies.blockingTasks.length || dependencies.blockingRuns.length) fail('IRIS_PROVIDER_REFERENCED', '当前工作区仍有未完成或可恢复流程依赖此供应商,请先处理原任务', 409);
469
- return executeConfigCommand(providerConfigPath, command, value, { registry, signal: controller.signal });
502
+ return executeConfigCommand(providerConfigPath, command, value, { registry, signal: controller.signal, resolveProviderNetwork });
470
503
  }), { signal: controller.signal });
471
504
  providerMonitor?.snapshot(); return json(request, response, result);
472
505
  }
473
- const result = await executeConfigCommand(providerConfigPath, command, value, { registry, signal: controller.signal });
506
+ const result = await executeConfigCommand(providerConfigPath, command, value, { registry, signal: controller.signal, resolveProviderNetwork });
474
507
  if (command === 'settings.enabled' || command === 'settings.save') providerMonitor?.snapshot();
475
508
  return await json(request, response, result);
476
509
  } catch (error) {
@@ -481,6 +514,28 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
481
514
  throw error;
482
515
  } finally { response.off('close', aborted); providerManagementBusy = false; }
483
516
  }
517
+ async function manageNetworks(request, response, url, caller) {
518
+ if (!caller.owner) fail('IRIS_CONNECTION_CAPABILITY_DENIED', '只有工作台所有者可以管理和检测网络出口', 403);
519
+ if (!allowNetworkManagement) fail('IRIS_API_READ_ONLY', '此服务未开启网络出口管理', 405);
520
+ query(url);
521
+ const options = { networkConfigPath, providerConfigPath, registry, resolveProviderNetwork };
522
+ const action = url.pathname.slice('/api/v0/network-management/'.length);
523
+ if (['GET', 'HEAD'].includes(request.method) && ['settings', 'doctor'].includes(action)) {
524
+ return json(request, response, action === 'settings' ? networkManagementSettings(options) : { ...networkDoctor(options), instanceId });
525
+ }
526
+ if (request.method !== 'POST' || ![...NETWORK_MANAGEMENT_ACTIONS, 'probe'].includes(action)) fail('IRIS_API_NOT_FOUND', '没有此网络管理操作', 404);
527
+ if (providerManagementBusy) fail('IRIS_API_BUSY', '供应商或出口设置正在处理,请等待完成', 429);
528
+ providerManagementBusy = true;
529
+ const controller = new AbortController(), aborted = () => { if (!response.writableFinished) controller.abort(); };
530
+ response.once('close', aborted);
531
+ try {
532
+ const input = await smallJson(request, 64 * 1024);
533
+ controller.signal.throwIfAborted();
534
+ const result = action === 'probe' ? await probeNetworkRoute(options, input, { signal: controller.signal })
535
+ : manageNetworkResources(options, action, input, { signal: controller.signal });
536
+ return await json(request, response, { ...result, instanceId });
537
+ } finally { response.off('close', aborted); providerManagementBusy = false; }
538
+ }
484
539
  async function manage(request, response, url, caller) {
485
540
  if (!caller.owner) fail('IRIS_CONNECTION_CAPABILITY_DENIED', '只有工作台所有者可以管理连接', 403);
486
541
  if (!allowConnectionManagement) fail('IRIS_API_READ_ONLY', '此服务未开启连接管理', 405);
@@ -536,12 +591,12 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
536
591
  reader.start();
537
592
  return await reader.run('inspect', ({ dataRoot: physicalRoot }) => {
538
593
  const metadata = inspectCoreArtifactMetadata(physicalRoot, artifactId);
539
- if (!(kind === 'image' ? IMAGE_TYPES.has(metadata.mediaType) : kind === 'audio' ? AUDIO_MEDIA_TYPES.has(metadata.mediaType) : metadata.mediaType === 'text/plain')) {
594
+ if (!(kind === 'image' ? IMAGE_TYPES.has(metadata.mediaType) : kind === 'video' ? Object.hasOwn(VIDEO_TYPES, metadata.mediaType) : kind === 'audio' ? AUDIO_MEDIA_TYPES.has(metadata.mediaType) : metadata.mediaType === 'text/plain')) {
540
595
  fail('IRIS_API_MEDIA_UNSUPPORTED', '此入口不支持该作品类型', 415);
541
596
  }
542
- if (metadata.size > (kind === 'text' ? 1024 * 1024 : MAX_LOCAL_API_IMAGE_BYTES)) fail('IRIS_API_MEDIA_TOO_LARGE', '作品超过此读取入口的大小上限', 413);
597
+ if (metadata.size > (kind === 'text' ? 1024 * 1024 : kind === 'video' ? MAX_VIDEO_BYTES : MAX_LOCAL_API_IMAGE_BYTES)) fail('IRIS_API_MEDIA_TOO_LARGE', '作品超过此读取入口的大小上限', 413);
543
598
  const result = readCoreArtifactBytes(physicalRoot, artifactId);
544
- if (result.bytes.length > MAX_LOCAL_API_IMAGE_BYTES || result.bytes.length !== result.artifact.size
599
+ if (result.bytes.length > (kind === 'video' ? MAX_VIDEO_BYTES : MAX_LOCAL_API_IMAGE_BYTES) || result.bytes.length !== result.artifact.size
545
600
  || digest(result.bytes).toString('hex') !== result.artifact.digest.value) {
546
601
  fail('IRIS_API_RECORD_CONFLICT', '图片字节与已保存记录不一致', 409);
547
602
  }
@@ -559,6 +614,7 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
559
614
  if (error.code === 'IRIS_PROVIDER_IMAGE_PROTOCOL_INVALID') {
560
615
  return { contractVersion: 0, configured: true, models: [], total: 0, gap: 'protocol_unavailable' };
561
616
  }
617
+ if (error.code === 'IRIS_PROVIDER_CREDENTIAL_UNAVAILABLE') return { contractVersion: 0, configured: true, models: [], total: 0, gap: 'credential_unavailable' };
562
618
  if (error.code !== 'IRIS_PROVIDER_MODEL_UNAVAILABLE') throw error;
563
619
  candidates = [];
564
620
  }
@@ -576,15 +632,25 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
576
632
  if (!capabilities.includes(capability)) continue;
577
633
  let candidates;
578
634
  try { candidates = select(catalog, '', registry); }
579
- catch (error) { if (error.code !== 'IRIS_PROVIDER_MODEL_UNAVAILABLE') throw error; candidates = []; }
635
+ catch (error) { if (!['IRIS_PROVIDER_MODEL_UNAVAILABLE', 'IRIS_PROVIDER_CREDENTIAL_UNAVAILABLE'].includes(error.code)) throw error; candidates = []; }
580
636
  entries.push(...candidates.map(route => ({ capability, modelRef: route.modelRef, displayName: route.model })));
581
637
  }
582
638
  return { contractVersion: 0, configured: true, models: entries, total: entries.length };
583
639
  }
584
640
 
585
- async function create(request, response, url) {
641
+ function videoModels() {
642
+ if (!providerConfigPath) return { contractVersion: 0, configured: false, models: [], total: 0 };
643
+ let candidates;
644
+ try { candidates = videoCandidatesFromCatalog(loadProviderCatalog(providerConfigPath), '', registry); }
645
+ catch (error) { if (!['IRIS_PROVIDER_MODEL_UNAVAILABLE', 'IRIS_PROVIDER_CREDENTIAL_UNAVAILABLE'].includes(error.code)) throw error; candidates = []; }
646
+ const entries = candidates.filter(route => videoModelSupportsMode(route.model, 't2v')).map(route => ({ modelRef: route.modelRef, displayName: route.model }));
647
+ return { contractVersion: 0, configured: true, models: entries, total: entries.length };
648
+ }
649
+
650
+ async function create(request, response, url, caller, creationTool) {
586
651
  query(url);
587
- if (!['/api/v0/images', '/api/v0/runs', '/api/v0/generations', '/api/v0/audio', '/api/v0/speeches', '/api/v0/transcriptions'].includes(url.pathname)) fail('IRIS_API_NOT_FOUND', '没有此本地创建接口', 404);
652
+ if (!['/api/v0/images', '/api/v0/runs', '/api/v0/generations', '/api/v0/audio', '/api/v0/speeches', '/api/v0/transcriptions', '/api/v0/videos'].includes(url.pathname)) fail('IRIS_API_NOT_FOUND', '没有此本地创建接口', 404);
653
+ if (url.pathname === '/api/v0/videos' && !videoEnabled) fail('IRIS_FLOW_VIDEO_ROOT_REQUIRED', '需要新的 execution init --video 根;原根不迁移', 409);
588
654
  const isImage = url.pathname === '/api/v0/images', isAudio = url.pathname === '/api/v0/audio', upload = isImage || isAudio;
589
655
  if (!audioEnabled && ['/api/v0/audio', '/api/v0/speeches', '/api/v0/transcriptions'].includes(url.pathname)) fail('IRIS_FLOW_AUDIO_ROOT_REQUIRED', '请使用新的音频执行根;原根不迁移', 409);
590
656
  if (headerCount(request, 'idempotency-key') !== 1 || !KEY.test(request.headers['idempotency-key'] || '')) {
@@ -624,8 +690,18 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
624
690
  try { value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes)); }
625
691
  catch (_) { fail('IRIS_API_INPUT_INVALID', '需要有效的 UTF-8 JSON'); }
626
692
  }
693
+ if (url.pathname === '/api/v0/runs' || url.pathname === '/api/v0/transcriptions') {
694
+ if (!value || typeof value !== 'object' || Array.isArray(value)
695
+ || typeof value.sourceArtifactId !== 'string' || !ARTIFACT.test(value.sourceArtifactId)) {
696
+ fail('IRIS_API_INPUT_INVALID', '需要当前数据根中的有效来源 Artifact ID');
697
+ }
698
+ // Input reuse follows the existing media/shared-source grant. Knowing an ID
699
+ // does not authorize a new Run to read or upload another capability's media.
700
+ await requireArtifactCapability(caller, value.sourceArtifactId);
701
+ scope(caller, 'create', creationTool);
702
+ }
627
703
  const command = { '/api/v0/images': 'creation.image', '/api/v0/audio': 'creation.audio', '/api/v0/runs': 'creation.flow',
628
- '/api/v0/generations': 'creation.generate', '/api/v0/speeches': 'creation.speech', '/api/v0/transcriptions': 'creation.transcribe' }[url.pathname];
704
+ '/api/v0/generations': 'creation.generate', '/api/v0/speeches': 'creation.speech', '/api/v0/transcriptions': 'creation.transcribe', '/api/v0/videos': 'creation.video' }[url.pathname];
629
705
  const result = await application.execute(command, {
630
706
  key: request.headers['idempotency-key'], ...(upload ? { bytes, mediaType } : { value })
631
707
  }, { signal: controller.signal, onPrepared: () => { prepared = true; } });
@@ -720,8 +796,9 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
720
796
  const connectionPost = connections && /^\/api\/v0\/connections\/[a-f0-9-]{36}\/(?:handshake|disconnect|client-info)$/.test(request.url.split('?')[0]);
721
797
  const managementPost = allowConnectionManagement && request.url.split('?')[0].startsWith('/api/v0/connection-management/');
722
798
  const providerPost = allowProviderManagement && request.url.split('?')[0].startsWith('/api/v0/provider-management/');
799
+ const networkPost = allowNetworkManagement && request.url.split('?')[0].startsWith('/api/v0/network-management/');
723
800
  const pluginPost = pluginExecution && (request.url.split('?')[0] === '/api/v0/plugin-operations' || /^\/api\/v0\/operations\/op_[a-f0-9]{64}\/cancel$/.test(request.url.split('?')[0]));
724
- if (!['GET', 'HEAD'].includes(request.method) && !((allowLocalWrites || allowSelection || connectionPost || managementPost || providerPost || pluginPost) && request.method === 'POST')) {
801
+ if (!['GET', 'HEAD'].includes(request.method) && !((allowLocalWrites || allowSelection || connectionPost || managementPost || providerPost || networkPost || pluginPost) && request.method === 'POST')) {
725
802
  fail('IRIS_API_READ_ONLY', allowLocalWrites || allowSelection ? '仅支持 GET/HEAD 与明确开启的 POST 操作' : '当前接口仅支持只读 GET/HEAD', 405);
726
803
  }
727
804
  if (request.method !== 'POST' && (request.headers['transfer-encoding'] !== undefined || (request.headers['content-length'] !== undefined && request.headers['content-length'] !== '0'))) {
@@ -733,10 +810,12 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
733
810
  if (request.method === 'POST') return specialPost(request, response, url, caller);
734
811
  if (url.pathname.startsWith('/api/v0/connection-management')) return manage(request, response, url, caller);
735
812
  if (url.pathname.startsWith('/api/v0/provider-management')) return manageProviders(request, response, url, caller);
813
+ if (url.pathname.startsWith('/api/v0/network-management')) return manageNetworks(request, response, url, caller);
736
814
  const callerScopes = scope(caller, 'read');
737
815
  const matchRun = /^\/api\/v0\/runs\/([^/]+)(\/download)?$/.exec(url.pathname);
738
816
  const matchImage = /^\/api\/v0\/artifacts\/([^/]+)\/content$/.exec(url.pathname);
739
- const matchAudio = audioEnabled && /^\/api\/v0\/artifacts\/([^/]+)\/(audio|text)$/.exec(url.pathname);
817
+ const matchMedia = audioEnabled && /^\/api\/v0\/artifacts\/([^/]+)\/(audio|text)$/.exec(url.pathname)
818
+ || videoEnabled && /^\/api\/v0\/artifacts\/([^/]+)\/(video)$/.exec(url.pathname);
740
819
  const operation = (allowExecution || pluginExecution) && /^\/api\/v0\/operations\/(op_[a-f0-9]{64})$/.exec(url.pathname);
741
820
  const runOperations = allowExecution && /^\/api\/v0\/runs\/(run_[a-f0-9]{24})\/operations$/.exec(url.pathname);
742
821
  const operationLookup = (allowExecution || pluginExecution) && url.pathname === '/api/v0/operations';
@@ -746,16 +825,16 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
746
825
  if (creationLookup && !caller.owner) fail('IRIS_CONNECTION_CAPABILITY_DENIED', '只有工作台所有者可以定位原创建请求', 403);
747
826
  query(url, pluginCatalog ? ['limit', 'offset'] : pluginHistory ? ['limit', 'cursor'] : url.pathname === '/api/v0/runs' ? ['limit', 'cursor', 'selection', 'kind'] : runOperations ? ['limit', 'cursor']
748
827
  : creationLookup ? ['kind', 'key'] : operationLookup ? ['key'] : []);
749
- if (matchRun && !RUN.test(matchRun[1]) || matchImage && !ARTIFACT.test(matchImage[1]) || matchAudio && !ARTIFACT.test(matchAudio[1])) {
828
+ if (matchRun && !RUN.test(matchRun[1]) || matchImage && !ARTIFACT.test(matchImage[1]) || matchMedia && !ARTIFACT.test(matchMedia[1])) {
750
829
  fail('IRIS_API_INPUT_INVALID', '流程或图片 ID 格式无效');
751
830
  }
752
831
  const readTool = pluginCatalog ? 'iris_core_plugin_catalog' : pluginHistory ? 'iris_core_operation' : operation ? 'iris_core_operation' : runOperations ? 'iris_core_operation' : operationLookup ? 'iris_core_lookup'
753
- : matchAudio ? matchAudio[2] === 'audio' ? 'iris_core_audio' : 'iris_core_transcript'
832
+ : matchMedia ? matchMedia[2] === 'video' ? 'iris_core_video' : matchMedia[2] === 'audio' ? 'iris_core_audio' : 'iris_core_transcript'
754
833
  : matchImage ? 'iris_core_image' : matchRun ? matchRun[2] ? null : 'iris_core_run'
755
- : ({ '/api/v0/models': 'iris_core_models', '/api/v0/audio-models': 'iris_core_audio_models', '/api/v0/runs': 'iris_core_runs', '/api/v0/connections': 'iris_core_connections', '/api/v0/adapters': 'iris_core_connection' })[url.pathname];
834
+ : ({ '/api/v0/models': 'iris_core_models', '/api/v0/video-models': 'iris_core_video_models', '/api/v0/audio-models': 'iris_core_audio_models', '/api/v0/runs': 'iris_core_runs', '/api/v0/connections': 'iris_core_connections', '/api/v0/adapters': 'iris_core_connection' })[url.pathname];
756
835
  if (readTool) scope(caller, 'read', readTool);
757
836
  if (matchRun?.[2] && !caller.owner && !connections.selection(caller.connectionId, caller.runtimeInstanceId).tools
758
- .some(name => ['iris_core_image', 'iris_core_audio', 'iris_core_transcript'].includes(name))) fail('IRIS_CONNECTION_CAPABILITY_DENIED', '未选择作品交付工具', 403);
837
+ .some(name => ['iris_core_image', 'iris_core_audio', 'iris_core_transcript', 'iris_core_video'].includes(name))) fail('IRIS_CONNECTION_CAPABILITY_DENIED', '未选择作品交付工具', 403);
759
838
  if (activeReads >= 2) fail('IRIS_API_BUSY', '读取繁忙,请稍后再试', 429);
760
839
  activeReads++;
761
840
  try {
@@ -800,15 +879,17 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
800
879
  return await json(request, response, { contractVersion: 0, instanceId, readOnly: mediaReadOnly,
801
880
  ...(identity ? { rootId: identity.rootId } : {}),
802
881
  connectionManagement: Boolean(caller.owner && allowConnectionManagement), workbenchMediaReadOnly: serveWorkbench && mediaReadOnly,
803
- providerManagement: Boolean(caller.owner && allowProviderManagement), ...(serveWorkbench && caller.owner ? { panel: panel.identity } : {}),
882
+ providerManagement: Boolean(caller.owner && allowProviderManagement), networkManagement: Boolean(caller.owner && allowNetworkManagement),
883
+ ...(serveWorkbench && caller.owner ? { panel: panel.identity } : {}),
804
884
  ...(pluginPanel && caller.owner ? { panelContributions: pluginPanel.manifest } : {}),
805
885
  ...(pluginExecution ? { pluginExecution: true, pluginOperations: caller.owner ? selectedOperations.map(item => item.dependency) : selection?.pluginOperations || [] } : {}),
806
- ...(audioEnabled ? { audio: true } : {}),
886
+ ...(audioEnabled ? { audio: true } : {}), ...(videoEnabled ? { video: true } : {}),
807
887
  operations: projectCoreSessionOperations({ scopes: callerScopes, ...(selection ? { tools: selection.tools } : {}),
808
- imageGeneration: imageGenerationEnabled, audio: audioEnabled, execution: allowExecution, pluginExecution,
888
+ imageGeneration: imageGenerationEnabled, audio: audioEnabled, video: videoEnabled, execution: allowExecution, pluginExecution,
809
889
  creationLookup: Boolean(identity?.rootId), selection: canSelect }),
810
890
  ...(selection || {}),
811
891
  ...(allowExecution || pluginExecution ? { executionEnabled: runner.snapshot().executionEnabled } : {}),
892
+ ...(caller.owner && scheduler ? { scheduling: scheduler.snapshot(physicalRoot) } : {}),
812
893
  limits: { runPageSize: 30, imageBytes: MAX_LOCAL_API_IMAGE_BYTES, activeReads: 2,
813
894
  ...(allowLocalWrites ? { uploadBytes: MAX_IMPORT_IMAGE_BYTES, jsonBytes: 64 * 1024, activeUploads: 1, localWrites: 9 } : {}) } });
814
895
  }
@@ -819,6 +900,7 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
819
900
  if (operation) {
820
901
  const result = await application.execute('operation.inspect', { id: operation[1] });
821
902
  await requireOperationCapability(caller, result.operation);
903
+ if (runner?.scheduling(operation[1])) result.operation.scheduling = runner.scheduling(operation[1]);
822
904
  return await json(request, response, result);
823
905
  }
824
906
  if (creationLookup) {
@@ -830,6 +912,7 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
830
912
  if (!KEY.test(url.searchParams.get('key') || '')) fail('IRIS_API_INPUT_INVALID', '需要原 Operation Key');
831
913
  const result = await application.execute('operation.lookup', { key: url.searchParams.get('key') });
832
914
  await requireOperationCapability(caller, result.operation);
915
+ if (runner?.scheduling(result.operation.id)) result.operation.scheduling = runner.scheduling(result.operation.id);
833
916
  return await json(request, response, result);
834
917
  }
835
918
  if (runOperations) {
@@ -840,6 +923,7 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
840
923
  ...(limit !== null ? { limit: Number(limit) } : {}), ...(cursor !== null ? { cursor } : {}) }));
841
924
  }
842
925
  if (url.pathname === '/api/v0/models') return await json(request, response, models());
926
+ if (url.pathname === '/api/v0/video-models' && videoEnabled) return await json(request, response, videoModels());
843
927
  if (url.pathname === '/api/v0/audio-models' && audioEnabled) return await json(request, response, audioModels(caller));
844
928
  if (url.pathname === '/api/v0/runs') {
845
929
  const limitText = url.searchParams.get('limit') ?? '20';
@@ -849,10 +933,10 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
849
933
  const selection = url.searchParams.get('selection') ?? 'all';
850
934
  if (!['all', 'selected', 'unselected'].includes(selection)) fail('IRIS_API_INPUT_INVALID', '选择筛选无效');
851
935
  const kind = url.searchParams.get('kind') ?? 'all';
852
- if (!['all', 'image', 'speech', 'transcribe'].includes(kind)) fail('IRIS_API_INPUT_INVALID', '作品类别无效');
936
+ if (!['all', 'image', 'speech', 'transcribe', 'video'].includes(kind)) fail('IRIS_API_INPUT_INVALID', '作品类别无效');
853
937
  const records = (await application.run('reader', runtime => runtime.run('inspect', ({ dataRoot }) => listFlowRuns(dataRoot))))
854
- .filter(run => allowsRun(caller, run) && (kind === 'all' || kind === 'image' && !isAudioRecipe(run.recipe)
855
- || kind === 'speech' && run.recipe === 'speech/v0' || kind === 'transcribe' && run.recipe === 'transcribe/v0'));
938
+ .filter(run => allowsRun(caller, run) && (kind === 'all' || kind === 'image' && !isAudioRecipe(run.recipe) && run.recipe !== VIDEO_RECIPE
939
+ || kind === 'video' && run.recipe === VIDEO_RECIPE || kind === 'speech' && run.recipe === 'speech/v0' || kind === 'transcribe' && run.recipe === 'transcribe/v0'));
856
940
  const selected = records.filter(run => Boolean(run.selection)).length;
857
941
  const counts = { all: records.length, selected, unselected: records.length - selected };
858
942
  const ordered = records.filter(run => selection === 'all' || Boolean(run.selection) === (selection === 'selected'))
@@ -863,15 +947,15 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
863
947
  return await json(request, response, { contractVersion: 0, total: ordered.length, counts,
864
948
  runs: page.map(projectLocalApiRunSummary), nextCursor: index + page.length < ordered.length ? page.at(-1).id : null });
865
949
  }
866
- let artifactId, mediaKind = matchAudio ? matchAudio[2] : 'image';
950
+ let artifactId, mediaKind = matchMedia ? matchMedia[2] : 'image';
867
951
  if (matchRun) {
868
952
  await requireFlowCapability(caller, matchRun[1]);
869
953
  const view = projectLocalApiFlow(await application.execute('flow.inspect', { run_id: matchRun[1] }));
870
954
  if (!matchRun[2]) return await json(request, response, view);
871
- const audio = isAudioRecipe(view.recipe);
872
- if (audio) {
873
- mediaKind = view.recipe === 'speech/v0' ? 'audio' : 'text';
874
- scope(caller, 'read', mediaKind === 'audio' ? 'iris_core_audio' : 'iris_core_transcript');
955
+ const audio = isAudioRecipe(view.recipe), video = view.recipe === VIDEO_RECIPE;
956
+ if (audio || video) {
957
+ mediaKind = video ? 'video' : view.recipe === 'speech/v0' ? 'audio' : 'text';
958
+ scope(caller, 'read', mediaKind === 'video' ? 'iris_core_video' : mediaKind === 'audio' ? 'iris_core_audio' : 'iris_core_transcript');
875
959
  if (!view.output) fail('IRIS_API_SELECTION_REQUIRED', '请先接回原任务结果', 409);
876
960
  artifactId = view.output.artifactId;
877
961
  } else {
@@ -880,12 +964,12 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
880
964
  artifactId = view.selection.artifactId;
881
965
  }
882
966
  } else if (matchImage) artifactId = matchImage[1];
883
- else if (matchAudio) artifactId = matchAudio[1];
967
+ else if (matchMedia) artifactId = matchMedia[1];
884
968
  else fail('IRIS_API_NOT_FOUND', '没有此只读接口', 404);
885
969
  await requireArtifactCapability(caller, artifactId);
886
970
  const result = await image(artifactId, mediaKind);
887
971
  return await send(request, response, 200, result.bytes, { 'Content-Type': result.artifact.mediaType,
888
- ...(matchRun ? { 'Content-Disposition': `attachment; filename="iris-${artifactId}.${IMAGE_TYPES.get(result.artifact.mediaType) || AUDIO_TYPES[result.artifact.mediaType] || 'txt'}"` } : {}) });
972
+ ...(matchRun ? { 'Content-Disposition': `attachment; filename="iris-${artifactId}.${IMAGE_TYPES.get(result.artifact.mediaType) || AUDIO_TYPES[result.artifact.mediaType] || VIDEO_TYPES[result.artifact.mediaType] || 'txt'}"` } : {}) });
889
973
  } finally { activeReads--; }
890
974
  }
891
975
 
@@ -899,7 +983,7 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
899
983
  error: { code: safe.code, message: safe.message, requestId } })), {
900
984
  Connection: 'close', ...(safe.status === 401 ? { 'WWW-Authenticate': 'Bearer realm="iris-local"' } : {}),
901
985
  ...(safe.status === 405 ? { Allow: safe.allow || (safe.code === 'IRIS_API_METHOD_DENIED' ? 'POST'
902
- : allowLocalWrites || allowSelection || allowConnectionManagement || allowProviderManagement ? 'GET, HEAD, POST' : 'GET, HEAD') } : {}) });
986
+ : allowLocalWrites || allowSelection || allowConnectionManagement || allowProviderManagement || allowNetworkManagement ? 'GET, HEAD, POST' : 'GET, HEAD') } : {}) });
903
987
  }
904
988
  }
905
989
 
@@ -918,7 +1002,9 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
918
1002
  ['application', () => application?.dispose()], ['requests', () => Promise.all([...jobs])],
919
1003
  ['connections-store', () => managed?.dispose()], ['connections', () => connections?.dispose()],
920
1004
  ['workbench-auth', () => browserAuth?.dispose()],
921
- ['providers', () => suppliedRegistry ? undefined : registry?.dispose()]
1005
+ ['providers', () => suppliedRegistry ? undefined : registry?.dispose()],
1006
+ ['network', () => ownedNetwork ? networks.dispose() : undefined],
1007
+ ['scheduler', () => ownedScheduler ? scheduler.dispose() : undefined]
922
1008
  ]) {
923
1009
  try { await close(); } catch (error) { failures.push({ stage, error }); }
924
1010
  }
@@ -946,7 +1032,7 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
946
1032
  physicalRoot = await reader.run('inspect', ({ dataRoot: root }) => {
947
1033
  inspectFlowRoot(root);
948
1034
  const version = inspectCreationRoot(root, { allowAbsent: !allowLocalWrites })?.schemaVersion;
949
- imageGenerationEnabled = [2, 3].includes(version); audioEnabled = version === 3;
1035
+ imageGenerationEnabled = [2, 3, 4].includes(version); audioEnabled = [3, 4].includes(version); videoEnabled = version === 4;
950
1036
  return root;
951
1037
  });
952
1038
  } finally { await reader.dispose(); }
@@ -955,7 +1041,7 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
955
1041
  let ports = {};
956
1042
  if (allowExecution) {
957
1043
  if (!executionPorts) loadProviderCatalog(providerConfigPath); // Static validation only; no discovery or model call.
958
- ports = executionPorts || configuredImagePorts(providerConfigPath, registry);
1044
+ ports = executionPorts || configuredImagePorts(providerConfigPath, registry, { resolveProviderNetwork, deferCooling: Boolean(scheduler) });
959
1045
  }
960
1046
  let grants = [];
961
1047
  if (connectionsFile) {
@@ -983,11 +1069,13 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
983
1069
  }
984
1070
  for (const grant of dynamic) connections.setGrant(grant);
985
1071
  }
986
- application = createApplicationRuntime({ dataRoot: physicalRoot, ...ports,
987
- ...(!allowExecution && allowLocalWrites && providerConfigPath ? { resolveAudioCandidates: ({ capability, modelRef }) => {
1072
+ application = createApplicationRuntime({ dataRoot: physicalRoot, ...ports, scheduler,
1073
+ ...(!allowExecution && allowLocalWrites && providerConfigPath ? { resolveVideoCandidates: ({ modelRef }) => videoCandidatesFromCatalog(loadProviderCatalog(providerConfigPath), modelRef, registry).map(route => ({
1074
+ adapter: { id: route.provider.id, protocol: route.provider.mediaProtocol, networkRequirement: providerNetworkRequirement(route.provider) }, model: route.modelRef,
1075
+ providerBinding: providerTaskBinding(route.provider, registry), selectionReason: 'explicit' })), resolveAudioCandidates: ({ capability, modelRef }) => {
988
1076
  const select = capability === 'tts' ? ttsCandidatesFromCatalog : transcribeCandidatesFromCatalog;
989
1077
  return select(loadProviderCatalog(providerConfigPath), modelRef, registry).map(route => ({
990
- adapter: { id: route.provider.id, protocol: route.provider.mediaProtocol }, model: route.modelRef,
1078
+ adapter: { id: route.provider.id, protocol: route.provider.mediaProtocol, networkRequirement: providerNetworkRequirement(route.provider) }, model: route.modelRef,
991
1079
  providerBinding: providerTaskBinding(route.provider, registry), selectionReason: 'explicit'
992
1080
  }));
993
1081
  }, resolveImageCandidates: ({ modelRef, input }) =>
@@ -997,7 +1085,7 @@ export function createLocalReadApi({ dataRoot, providerConfigPath, allowLocalWri
997
1085
  compileComfyWorkflow(route.provider.comfyWorkflow, input);
998
1086
  }
999
1087
  return {
1000
- adapter: { id: route.provider.id, protocol: route.provider.mediaProtocol }, model: route.modelRef,
1088
+ adapter: { id: route.provider.id, protocol: route.provider.mediaProtocol, networkRequirement: providerNetworkRequirement(route.provider) }, model: route.modelRef,
1001
1089
  supportsImageEdit: supportsImageEdit(route.provider.mediaProtocol, route.model, registry),
1002
1090
  providerBinding: providerTaskBinding(route.provider, registry), selectionReason: 'explicit'
1003
1091
  };
@@ -6,6 +6,7 @@ import { readImportImage } from './image-import.js';
6
6
  import { normalizeGenerationInput } from './generation-input.js';
7
7
  import { readFlowRun, requireAudioRoot } from './flow-store.js';
8
8
  import { readImportAudio, normalizeAudioFlowInput } from './audio-flow-input.js';
9
+ import { VIDEO_RECIPE, normalizeVideoFlowInput } from './video-flow-input.js';
9
10
  import { initializeCreationRoot, inspectCreationRoot, creationRequestId, creationInputDigest,
10
11
  readCreationRequest, publishCreationRequest, completeCreationRequest, creationFail, CREATION_KINDS, isImportCreation } from './creation-request-store.js';
11
12
 
@@ -32,8 +33,8 @@ function flowInput(input, generate = false) {
32
33
  return { ...(normalized.sourceArtifactId ? { source_artifact_id: normalized.sourceArtifactId } : {}), model_ref: normalized.modelRef,
33
34
  ...normalized.providerInput };
34
35
  }
35
- export function createLocalCreationService(runtime, { resolveImageCandidates, resolveAudioCandidates, onPrepared } = {}) {
36
- const flows = createImageFlowService(runtime, { resolveImageCandidates, resolveAudioCandidates });
36
+ export function createLocalCreationService(runtime, { resolveImageCandidates, resolveAudioCandidates, resolveVideoCandidates, onPrepared } = {}) {
37
+ const flows = createImageFlowService(runtime, { resolveImageCandidates, resolveAudioCandidates, resolveVideoCandidates });
37
38
  function execute(name, input) {
38
39
  if (name === 'creation.root' || name === 'creation.lookup') {
39
40
  return runtime.run('inspect', ({ dataRoot, signal }) => {
@@ -46,7 +47,8 @@ export function createLocalCreationService(runtime, { resolveImageCandidates, re
46
47
  }
47
48
  const marker = inspectCreationRoot(dataRoot, { allowAbsent: true });
48
49
  if (name === 'creation.root') return { contractVersion: 0, rootId: marker?.rootId ?? null,
49
- ...([2, 3].includes(marker?.schemaVersion) ? { imageGeneration: true } : {}), ...(marker?.schemaVersion === 3 ? { audio: true } : {}) };
50
+ ...([2, 3, 4].includes(marker?.schemaVersion) ? { imageGeneration: true } : {}), ...([3, 4].includes(marker?.schemaVersion) ? { audio: true } : {}),
51
+ ...(marker?.schemaVersion === 4 ? { video: true } : {}) };
50
52
  if (!marker) creationFail('IRIS_CREATION_NOT_FOUND', '此数据根尚无创建请求区');
51
53
  const id = creationRequestId(marker, input.kind, input.key);
52
54
  const request = readCreationRequest(dataRoot, id, marker);
@@ -86,7 +88,7 @@ export function createLocalCreationService(runtime, { resolveImageCandidates, re
86
88
  return runtime.run('execute', async ({ dataRoot, signal }) => {
87
89
  if (name === 'creation.init') return { contractVersion: 0, root: initializeCreationRoot(dataRoot) };
88
90
  const kinds = { 'creation.image': 'image-import', 'creation.flow': 'flow-create', 'creation.generate': 'generation-create',
89
- 'creation.audio': 'audio-import', 'creation.speech': 'speech-create', 'creation.transcribe': 'transcription-create' };
91
+ 'creation.audio': 'audio-import', 'creation.speech': 'speech-create', 'creation.transcribe': 'transcription-create', 'creation.video': 'video-create' };
90
92
  const kind = kinds[name];
91
93
  if (!kind) throw new TypeError('未知本地创建操作');
92
94
  if (['audio-import', 'speech-create', 'transcription-create'].includes(kind)) requireAudioRoot(dataRoot);
@@ -102,6 +104,11 @@ export function createLocalCreationService(runtime, { resolveImageCandidates, re
102
104
  if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(key => !allowed.includes(key))) creationFail('IRIS_CREATION_INPUT_INVALID', '音频创建输入无效');
103
105
  owned = normalizeAudioFlowInput(speech ? 'speech/v0' : 'transcribe/v0', { model_ref: value.modelRef,
104
106
  ...(speech ? { text: value.text, ...(value.voice !== undefined ? { voice: value.voice } : {}) } : { source_artifact_id: value.sourceArtifactId }) });
107
+ } else if (kind === 'video-create') {
108
+ const value = input.value;
109
+ if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).some(key => !['modelRef', 'prompt', 'size', 'duration'].includes(key))) creationFail('IRIS_CREATION_INPUT_INVALID', '文生视频输入无效');
110
+ owned = normalizeVideoFlowInput({ model_ref: value.modelRef, prompt: value.prompt,
111
+ ...(value.size !== undefined ? { size: value.size } : {}), ...(value.duration !== undefined ? { duration: value.duration } : {}) });
105
112
  } else owned = flowInput(input.value, kind === 'generation-create');
106
113
  const inputDigest = creationInputDigest(kind, owned);
107
114
  let request = readCreationRequest(dataRoot, id, marker);
@@ -112,7 +119,7 @@ export function createLocalCreationService(runtime, { resolveImageCandidates, re
112
119
  ? { artifactId: 'artifact_' + crypto.randomBytes(12).toString('hex'), ...owned,
113
120
  metadata: source.metadata, createdAt: new Date().toISOString() }
114
121
  : await flows.prepareCreation(dataRoot, signal, { ...owned, ...(kind === 'speech-create' ? { recipe: 'speech/v0' }
115
- : kind === 'transcription-create' ? { recipe: 'transcribe/v0' } : {}) });
122
+ : kind === 'transcription-create' ? { recipe: 'transcribe/v0' } : kind === 'video-create' ? { recipe: VIDEO_RECIPE } : {}) });
116
123
  signal.throwIfAborted();
117
124
  request = publishCreationRequest(dataRoot, { schemaVersion: 0, id, rootId: marker.rootId, kind,
118
125
  inputDigest, createdAt: intent.createdAt, intent, state: 'prepared' });
@@ -8,8 +8,8 @@ export function createLocalOperationService(runtime) {
8
8
  return runtime.run(readOnly ? 'inspect' : 'execute', ({ dataRoot, signal }) => {
9
9
  signal.throwIfAborted();
10
10
  if (name === 'execution.init') {
11
- if (!input || typeof input !== 'object' || Array.isArray(input) || Object.keys(input).some(key => !['imageGeneration', 'audio'].includes(key))
12
- || ['imageGeneration', 'audio'].some(key => input[key] !== undefined && typeof input[key] !== 'boolean')) operationFail('IRIS_OPERATION_INPUT_INVALID', 'execution.init 只接受可选 imageGeneration/audio 开关');
11
+ if (!input || typeof input !== 'object' || Array.isArray(input) || Object.keys(input).some(key => !['imageGeneration', 'audio', 'video'].includes(key))
12
+ || ['imageGeneration', 'audio', 'video'].some(key => input[key] !== undefined && typeof input[key] !== 'boolean')) operationFail('IRIS_OPERATION_INPUT_INVALID', 'execution.init 只接受可选 imageGeneration/audio/video 开关');
13
13
  return { contractVersion: 0, root: initializeExecutionRoot(dataRoot, input), executionEnabled: false };
14
14
  }
15
15
  if (name === 'operation.accept') {
@@ -36,7 +36,7 @@ export function inspectLocalRequestRoot(root, { allowAbsent = false } = {}) {
36
36
  }
37
37
  try {
38
38
  const versions = fs.readdirSync(base);
39
- if (versions.length !== 1 || !['v0', 'v1', 'v2', 'v3'].includes(versions[0])) invalid();
39
+ if (versions.length !== 1 || !['v0', 'v1', 'v2', 'v3', 'v4'].includes(versions[0])) invalid();
40
40
  const schemaVersion = Number(versions[0].slice(1)), version = directory(root, ['creation-requests', versions[0]]);
41
41
  const allowed = schemaVersion === 0 ? ['root.json', 'requests'] : ['root.json', 'requests', 'operations', 'runner'];
42
42
  if (fs.readdirSync(version).some(name => !allowed.includes(name) && !/^root\.json\.tmp-[1-9][0-9]*-[a-f0-9]{12}$/.test(name))) invalid();
@@ -62,7 +62,7 @@ export function inspectLocalRequestRoot(root, { allowAbsent = false } = {}) {
62
62
  }
63
63
 
64
64
  export function initializeLocalRequestRoot(root, schemaVersion) {
65
- if (![0, 1, 2, 3].includes(schemaVersion)) throw new TypeError('未知请求容器版本');
65
+ if (![0, 1, 2, 3, 4].includes(schemaVersion)) throw new TypeError('未知请求容器版本');
66
66
  const existing = inspectLocalRequestRoot(root, { allowAbsent: true });
67
67
  if (existing) {
68
68
  if (existing.schemaVersion !== schemaVersion) requestRootFail('IRIS_CREATION_ROOT_VERSION_CONFLICT', '请求根版本不同;不自动升级、降级或迁移,请使用新的专用根');