@mokuyoaxis/dsh-iris 0.2.7 → 0.3.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/CHANGELOG.md +143 -1
  2. package/README.en.md +109 -311
  3. package/README.md +106 -309
  4. package/bin/dsh-iris.js +452 -92
  5. package/docs/AGENT_CORE_QUICKSTART.md +64 -0
  6. package/docs/AGENT_QUICKSTART.md +2 -0
  7. package/docs/ARCHITECTURE.md +13 -3
  8. package/docs/ARTIFACT_MANIFEST.md +4 -0
  9. package/docs/AUDIO_FLOWS.md +87 -0
  10. package/docs/BETA_SCOPE.md +42 -0
  11. package/docs/CONTACT_SHEET.md +62 -0
  12. package/docs/CORE_CONNECTIONS.md +101 -0
  13. package/docs/CORE_QUICKSTART.md +110 -0
  14. package/docs/CORE_RUNTIME_CONTRACT.md +7 -2
  15. package/docs/CORE_SERVICE.md +101 -0
  16. package/docs/CORE_SKILLS.md +35 -3
  17. package/docs/DSH_COMPATIBILITY.md +21 -1
  18. package/docs/DSH_CORE_MIGRATION.md +6 -0
  19. package/docs/DSH_RC2_ADAPTATION.md +1 -1
  20. package/docs/DSH_RUNTIME.md +6 -2
  21. package/docs/HEADLESS_CLI.md +30 -5
  22. package/docs/HOST_ADAPTER_CONTRACT.md +5 -3
  23. package/docs/IMAGE_FLOW.md +4 -0
  24. package/docs/IMAGE_GENERATION.md +127 -0
  25. package/docs/LOCAL_API.md +4 -2
  26. package/docs/LOCAL_API_CONTRACT.md +42 -15
  27. package/docs/LOCAL_CREATION.md +7 -1
  28. package/docs/LOCAL_OPERATIONS.md +46 -0
  29. package/docs/LOCAL_SELECTION.md +57 -0
  30. package/docs/LOCAL_WEB.md +207 -0
  31. package/docs/MCP.md +212 -0
  32. package/docs/MEDIA_SCHEDULING.md +72 -0
  33. package/docs/MUSIC.md +2 -0
  34. package/docs/NETWORK_RESOURCES.md +106 -0
  35. package/docs/NETWORK_ROUTING.md +103 -0
  36. package/docs/NETWORK_WORKBENCH.md +65 -0
  37. package/docs/PANEL_EXTENSIONS.md +64 -0
  38. package/docs/PLUGINS.md +193 -0
  39. package/docs/PLUGIN_DEVELOPMENT.md +39 -0
  40. package/docs/PLUGIN_STARTUP.md +113 -0
  41. package/docs/PROVIDER_ADAPTER_CONTRACT.md +8 -2
  42. package/docs/PROVIDER_CREDENTIALS.md +67 -0
  43. package/docs/PROVIDER_EXTENSIONS.md +82 -0
  44. package/docs/PROVIDER_HEALTH.md +5 -1
  45. package/docs/PROVIDER_NETWORK.md +45 -0
  46. package/docs/PROVIDER_PLUGIN_CONTRACT.md +24 -0
  47. package/docs/PROVIDER_SETTINGS.md +91 -0
  48. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +8 -0
  49. package/docs/ROADMAP.md +18 -8
  50. package/docs/SECURITY.md +1 -1
  51. package/docs/SYNC_DELIVERY_RECOVERY.md +5 -1
  52. package/docs/VIDEO_FLOWS.md +89 -0
  53. package/docs/VISION_CLI.md +1 -1
  54. package/docs/VISION_MODEL.md +2 -0
  55. package/docs/WEB_PROTOTYPE.md +21 -6
  56. package/docs/WEB_RECOVERY.md +46 -0
  57. package/docs/WORKBENCH_ARTIFACTS.md +6 -0
  58. package/docs/WORKBENCH_LOGIN.md +63 -0
  59. package/docs/assets/screenshots/core-connections-preview.webp +0 -0
  60. package/docs/assets/screenshots/core-model-pools-preview.webp +0 -0
  61. package/docs/assets/screenshots/core-overview-preview.webp +0 -0
  62. package/docs/assets/screenshots/core-spectrum-preview.webp +0 -0
  63. package/docs/assets/screenshots/core-studio-dark-preview.webp +0 -0
  64. package/docs/assets/screenshots/core-studio-mobile-preview.webp +0 -0
  65. package/docs/assets/screenshots/core-studio-preview.webp +0 -0
  66. package/docs/releases/0.3.0-alpha.1.md +51 -0
  67. package/docs/releases/0.3.0-beta.1.md +34 -0
  68. package/docs/screenshots.md +41 -1
  69. package/lib/actions.js +56 -4
  70. package/lib/adapters.js +75 -49
  71. package/lib/api.js +23 -13
  72. package/lib/application-runtime.js +73 -17
  73. package/lib/application-writer.js +193 -0
  74. package/lib/audio-flow-input.js +37 -0
  75. package/lib/client.d.ts +3 -3
  76. package/lib/client.js +402 -72
  77. package/lib/comfyui-adapter.js +95 -0
  78. package/lib/comfyui-client.js +60 -0
  79. package/lib/comfyui-inspection.js +73 -0
  80. package/lib/comfyui-workflow.js +94 -0
  81. package/lib/command-service.js +18 -5
  82. package/lib/config.js +49 -10
  83. package/lib/configured-image-runtime.js +69 -0
  84. package/lib/connection-skill.js +98 -0
  85. package/lib/connection-template.js +23 -0
  86. package/lib/core-artifact-store.js +84 -28
  87. package/lib/core-bridge-selection.js +31 -0
  88. package/lib/core-bridge-tools.js +85 -0
  89. package/lib/core-capability-catalog.js +192 -0
  90. package/lib/core-connection-diagnostics.js +49 -0
  91. package/lib/core-connection-store.js +277 -0
  92. package/lib/core-connections.js +225 -0
  93. package/lib/core-delivery-cache.js +8 -3
  94. package/lib/core-maintenance.js +9 -3
  95. package/lib/core-runtime.js +2 -1
  96. package/lib/core-service-client.js +174 -0
  97. package/lib/core-tasks.js +113 -21
  98. package/lib/core-writer-lease.js +16 -0
  99. package/lib/creation-request-store.js +41 -64
  100. package/lib/doctor.js +2 -2
  101. package/lib/dsh-core-adapter.js +61 -9
  102. package/lib/dsh-host-adapter.js +4 -1
  103. package/lib/dsh-provider-config.js +1 -1
  104. package/lib/dsh-service-bridge.js +67 -0
  105. package/lib/execution-ownership.js +43 -0
  106. package/lib/flow-store.js +156 -14
  107. package/lib/generation-input.js +10 -1
  108. package/lib/headless-vision.js +7 -7
  109. package/lib/image-edit-input.js +1 -1
  110. package/lib/image-flow.js +97 -31
  111. package/lib/index.js +47 -22
  112. package/lib/local-api-projection.js +15 -6
  113. package/lib/local-api-token-directory.js +31 -0
  114. package/lib/local-api.js +861 -71
  115. package/lib/local-creation.js +96 -24
  116. package/lib/local-operations.js +36 -0
  117. package/lib/local-request-root.js +86 -0
  118. package/lib/local-web-assets.js +39 -0
  119. package/lib/mcp-bridge.js +58 -0
  120. package/lib/mcp-check.js +26 -0
  121. package/lib/mcp-config.js +42 -0
  122. package/lib/mcp-delivery.js +46 -0
  123. package/lib/mcp-http.js +174 -0
  124. package/lib/mcp-server.js +69 -0
  125. package/lib/media-scheduler.js +194 -0
  126. package/lib/model-directory-service.js +37 -10
  127. package/lib/model-directory.js +3 -3
  128. package/lib/models.js +2 -2
  129. package/lib/music-transport.js +6 -3
  130. package/lib/network-config-service.js +123 -0
  131. package/lib/network-config.js +127 -0
  132. package/lib/network-doctor.js +94 -0
  133. package/lib/network-management.js +51 -0
  134. package/lib/network-policy.js +19 -0
  135. package/lib/network-route-evidence.js +38 -0
  136. package/lib/network-routing.js +283 -0
  137. package/lib/operation-runner.js +281 -0
  138. package/lib/operation-store.js +216 -0
  139. package/lib/panel-extension.js +36 -0
  140. package/lib/plugin-contract.js +194 -0
  141. package/lib/plugin-execution.js +51 -0
  142. package/lib/plugin-launch.js +86 -0
  143. package/lib/plugin-operation-store.js +101 -0
  144. package/lib/plugin-operations.js +28 -0
  145. package/lib/plugin-registry.js +109 -0
  146. package/lib/plugin-selection.js +15 -0
  147. package/lib/plugin-source-image.js +5 -0
  148. package/lib/plugin-web-assets.js +54 -0
  149. package/lib/plugins.d.ts +307 -0
  150. package/lib/plugins.js +59 -0
  151. package/lib/provider-adapter.js +7 -3
  152. package/lib/provider-adapters.js +92 -22
  153. package/lib/provider-catalog.js +64 -36
  154. package/lib/provider-config-format.js +51 -0
  155. package/lib/provider-config-service.js +143 -31
  156. package/lib/provider-context.d.ts +90 -0
  157. package/lib/provider-context.js +42 -0
  158. package/lib/provider-credentials.js +97 -0
  159. package/lib/provider-diagnostics.js +56 -0
  160. package/lib/provider-extensions.js +29 -0
  161. package/lib/provider-health.js +18 -6
  162. package/lib/provider-monitor.js +81 -0
  163. package/lib/provider-network.js +52 -0
  164. package/lib/provider-protocol.js +73 -22
  165. package/lib/provider-registry.js +128 -0
  166. package/lib/provider-response.js +38 -0
  167. package/lib/provider-settings.js +136 -0
  168. package/lib/provider-task-runner.js +132 -43
  169. package/lib/provider-transport.js +16 -0
  170. package/lib/render.js +11 -1
  171. package/lib/scheduling-policy.js +66 -0
  172. package/lib/skill-installation.js +23 -11
  173. package/lib/tasks.js +3 -2
  174. package/lib/video-flow-input.js +16 -0
  175. package/lib/video-input.js +12 -5
  176. package/lib/vision-model-routing.js +5 -0
  177. package/lib/web/app.js +703 -0
  178. package/lib/web/appearance.js +111 -0
  179. package/lib/web/comfyui.js +146 -0
  180. package/lib/web/connections.js +516 -0
  181. package/lib/web/creation.js +359 -0
  182. package/lib/web/flow-view.js +34 -0
  183. package/lib/web/gallery.js +133 -0
  184. package/lib/web/index.html +344 -0
  185. package/lib/web/media-client.js +218 -0
  186. package/lib/web/network.js +232 -0
  187. package/lib/web/plugin-panel.js +223 -0
  188. package/lib/web/plugin-status.js +140 -0
  189. package/lib/web/plugin-surface.css +17 -0
  190. package/lib/web/providers.js +513 -0
  191. package/lib/web/recovery.js +105 -0
  192. package/lib/web/startup.js +18 -0
  193. package/lib/web/styles.css +571 -0
  194. package/lib/web/transport.js +101 -0
  195. package/lib/web/workbench-login.js +165 -0
  196. package/lib/workbench-artifacts.js +3 -2
  197. package/lib/workbench-auth-cli.js +47 -0
  198. package/lib/workbench-auth.js +200 -0
  199. package/package.json +24 -6
  200. package/plugins/contact-sheet/index.d.mts +4 -0
  201. package/plugins/contact-sheet/index.mjs +33 -0
  202. package/plugins/contact-sheet/web/board.css +14 -0
  203. package/plugins/contact-sheet/web/board.mjs +93 -0
  204. package/skills/iris-core/SKILL.md +8 -4
  205. package/skills/iris-core/references/cli.md +2 -2
  206. package/skills/iris-core/references/comfyui.md +69 -0
  207. package/skills/iris-core/references/tasks.md +2 -0
  208. package/templates/plugin-starter/LICENSE +21 -0
  209. package/templates/plugin-starter/README.md +74 -0
  210. package/templates/plugin-starter/assets/peach.png +0 -0
  211. package/templates/plugin-starter/assets/violet.png +0 -0
  212. package/templates/plugin-starter/index.mjs +27 -0
  213. package/templates/plugin-starter/package.json +18 -0
  214. package/templates/plugin-starter/scripts/core-cli.mjs +20 -0
  215. package/templates/plugin-starter/scripts/environment.mjs +65 -0
  216. package/templates/plugin-starter/scripts/mcp-probe.mjs +22 -0
  217. package/templates/plugin-starter/scripts/setup.mjs +42 -0
  218. package/templates/plugin-starter/scripts/smoke.mjs +76 -0
  219. package/templates/plugin-starter/web/board.css +27 -0
  220. package/templates/plugin-starter/web/board.mjs +160 -0
  221. package/templates/plugin-starter/web/png.mjs +23 -0
  222. package/templates/provider-starter/LICENSE +21 -0
  223. package/templates/provider-starter/README.md +50 -0
  224. package/templates/provider-starter/index.d.mts +5 -0
  225. package/templates/provider-starter/index.mjs +165 -0
  226. package/templates/provider-starter/package.json +13 -0
  227. package/templates/provider-starter/scripts/core-cli.mjs +22 -0
  228. package/templates/provider-starter/scripts/environment.mjs +122 -0
  229. package/templates/provider-starter/scripts/fixture.mjs +54 -0
  230. package/templates/provider-starter/scripts/mcp-probe.mjs +21 -0
  231. package/templates/provider-starter/scripts/protocol-smoke.mjs +48 -0
  232. package/templates/provider-starter/scripts/setup.mjs +44 -0
  233. package/templates/provider-starter/scripts/smoke.mjs +145 -0
  234. package/user_guide.md +5 -1
package/bin/dsh-iris.js CHANGED
@@ -8,7 +8,8 @@ import { recoverCoreWriterLease } from '../lib/core-lease-recovery.js';
8
8
  import { doctor, formatDoctorReport } from '../lib/doctor.js';
9
9
  import { loadProviderCatalog, providerCatalogSnapshot, catalogCapabilitySnapshot, imageCandidatesFromCatalog, musicCandidatesFromCatalog, providerForTaskFromCatalog, providerTaskBinding, transcribeCandidatesFromCatalog, ttsCandidatesFromCatalog, videoCandidatesFromCatalog } from '../lib/provider-catalog.js';
10
10
  import { createConfiguredProviderAdapter } from '../lib/provider-adapters.js';
11
- import { providerForImageModel, selectMediaProtocol, dashscopeTtsInputIssue } from '../lib/provider-protocol.js';
11
+ import { providerForImageModel, selectMediaProtocol, dashscopeTtsInputIssue, supportsImageEdit } from '../lib/provider-protocol.js';
12
+ import { loadProviderRegistry } from '../lib/provider-extensions.js';
12
13
  import { prepareProviderInput } from '../lib/provider-adapter.js';
13
14
  import { createProviderTaskRunner } from '../lib/provider-task-runner.js';
14
15
  import { generateMusicTask } from '../lib/music-generation.js';
@@ -17,25 +18,46 @@ import { executeVisionCommand, normalizeVisionInput } from '../lib/headless-visi
17
18
  import { videoTaskCandidates } from '../lib/video-input.js';
18
19
  import { createChromiumBrowser } from '../lib/chromium-browser.js';
19
20
  import { executeConfigCommand, recordCatalogRateLimit, recordCatalogAvailability, refreshCatalogDirectories } from '../lib/provider-config-service.js';
21
+ import { executeNetworkConfigCommand } from '../lib/network-config-service.js';
20
22
  import { probeVisionModel } from '../lib/vision-model-routing.js';
21
23
  import { RED_TEST_IMAGE } from '../lib/vision.js';
22
24
  import { parseModelRef, modelRef as qualifiedModelRef } from '../lib/models.js';
23
25
  import { coreTaskWaitFinished } from '../lib/task-wait.js';
24
26
  import { createLocalReadApi, writeLocalApiTokenFile } from '../lib/local-api.js';
25
27
  import { manageCoreSkill } from '../lib/skill-installation.js';
28
+ import { createTrackedProviderAdapter, configuredImagePorts } from '../lib/configured-image-runtime.js';
29
+ import { inspectExecutionLease, recoverExecutionLease } from '../lib/operation-runner.js';
30
+ import { createConnectionPair } from '../lib/core-connections.js';
31
+ import { builtinLaunchPlugins, loadPluginLaunch } from '../lib/plugin-launch.js';
32
+ import { checkLocalApiTokenDirectory, writeLocalApiTokenDirectory } from '../lib/local-api-token-directory.js';
33
+ import { resolveWorkbenchAuthDirectory } from '../lib/workbench-auth.js';
34
+ import { manageWorkbenchPassword } from '../lib/workbench-auth-cli.js';
35
+ import crypto from 'node:crypto';
36
+ import { atomicCreatePrivate } from '../lib/private-storage.js';
37
+ import { providerNetworkRequirement, networkPolicyError } from '../lib/network-policy.js';
38
+ import { createProviderNetworkManager } from '../lib/network-routing.js';
39
+ import { encodeProviderConfig, wrapProviderCatalog } from '../lib/provider-config-format.js';
40
+ import { readNetworkConfig } from '../lib/network-config-service.js';
41
+ import { readSchedulingPolicy } from '../lib/scheduling-policy.js';
42
+ import { createMediaScheduler } from '../lib/media-scheduler.js';
43
+
44
+ let cliNetworkConfigPath, cliNetworkManager, cliScheduler;
45
+ function cliResolveNetwork({ provider, ...context }) {
46
+ const requirement = context.networkRequirement || providerNetworkRequirement(provider);
47
+ if (requirement === undefined && !cliScheduler) return undefined;
48
+ cliNetworkManager ||= createProviderNetworkManager({ networkConfigPath: cliNetworkConfigPath, scheduler: cliScheduler });
49
+ return cliNetworkManager.resolveProviderNetwork({ ...context, networkRequirement: requirement });
50
+ }
26
51
 
27
52
  function trackedProviderAdapter(provider, options, task) {
28
- return createConfiguredProviderAdapter(provider, {
29
- modelId: parseModelRef(task?.modelRef)?.modelId,
30
- onModelAvailability: event => recordCatalogAvailability(options['provider-config'], event),
31
- onRateLimit: event => recordCatalogRateLimit(options['provider-config'], event)
32
- });
53
+ return createTrackedProviderAdapter(provider, options['provider-config'], task, options.providerRegistry, {
54
+ network: cliResolveNetwork({ provider, providerId: provider.id, taskId: task?.id }) });
33
55
  }
34
56
 
35
57
  async function generationCatalog(options, capability, modelRef, signal = cliAbortController.signal) {
36
58
  const file = required(options['provider-config'], 'provider-config');
37
59
  const catalog = loadProviderCatalog(file);
38
- if (!modelRef) await refreshCatalogDirectories(file, catalog, capability, { signal });
60
+ if (!modelRef) await refreshCatalogDirectories(file, catalog, capability, { signal, registry: options.providerRegistry, resolveProviderNetwork: cliResolveNetwork });
39
61
  return catalog;
40
62
  }
41
63
 
@@ -51,17 +73,64 @@ function usage() {
51
73
  return [
52
74
  'Usage:',
53
75
  ' JSON input: --input <json>, --input-file <path>, or --input - (stdin).',
76
+ ' --provider-extension <absolute-js-or-mjs-entry> explicitly loads trusted Provider code; repeat for multiple extensions. Experimental v0, no installation or directory discovery.',
77
+ ' dsh-iris providers protocols [--provider-extension <absolute-entry>] lists protocol/configuration descriptions without credentials or model calls.',
78
+ ' dsh-iris plugins list',
79
+ ' dsh-iris plugins inspect --plugin-config <absolute-startup.json>',
80
+ ' Explicit trusted plugin entries, versions, contributions and selections; inspect imports/activates trusted code but does not start Core or grant connections.',
81
+ ' dsh-iris web serve ... --plugin-config <absolute-startup.json> [--auth-store <absolute-private-directory>]',
82
+ ' entries load plugins; operations enables exact business IDs; panel selects pages/settings/renderers. MCP grants remain separate. Do not combine with --provider-extension.',
54
83
  ' dsh-iris skills <install|status|uninstall> --target kimi [--directory <absolute-skills-directory>]',
55
84
  ' dsh-iris skills <install|status|uninstall> --directory <absolute-skills-directory>',
85
+ ' dsh-iris skills <install|status|uninstall> --bundle <absolute-connection-skill.json> --directory <absolute-skills-directory> [--dry-run]',
56
86
  ' install/uninstall accept --dry-run. Copy only iris-core; preserve local changes and archive managed updates/removals outside the scan directory. status is read-only and does not verify host discovery.',
57
87
  ' dsh-iris doctor [--data-root <absolute-path>] [--json]',
58
88
  ' dsh-iris api init --data-root <empty-existing-flow-root>',
59
- ' dsh-iris api serve --data-root <existing-flow-root> --token-file <new-absolute-path-outside-root> [--provider-config <absolute-path>] [--port <0..65535>] [--allow-local-writes]',
60
- ' Explicit loopback-only API, read-only by default. Local writes only upload images/create Runs, without generation. The session token is saved privately; stdout contains no token.',
89
+ ' dsh-iris execution init --data-root <empty-existing-flow-root> [--image-generation|--audio|--video]',
90
+ ' Initialize a NEW v1 request container, v2 with --image-generation, v3 with --audio, or v4 with --video (images + audio + text-to-video). Existing roots are never migrated.',
91
+ ' dsh-iris execution inspect --data-root <v1-flow-root>',
92
+ ' dsh-iris execution recover-lease --data-root <v1-flow-root> --confirm-stale-pid <pid>',
93
+ ' dsh-iris connection create --data-root <execution-flow-root> --directory <new-private-directory> --origin <http://127.0.0.1:port> --label <name> [--harness <generic|kimi|dsh>] [--scopes <read,create,execute,stop>] [--capabilities <image-edit,image-generate,tts,transcribe,video-generate|none>] [--tools <comma-separated-names|none>]',
94
+ ' dsh-iris api serve ... --allow-execution --provider-config <absolute-path> [--connections-file <private-grants-file>]',
95
+ ' dsh-iris mcp serve --connection-file <private-client-json> [--scopes <read,create,execute,stop>] [--capabilities <image-edit,image-generate,tts,transcribe,video-generate|none>] [--tools <comma-separated-names|none>]',
96
+ ' Official SDK stdio bridge to an already running Core service. Read-only by default; stdout contains MCP messages only.',
97
+ ' dsh-iris mcp capabilities',
98
+ ' dsh-iris mcp presets',
99
+ ' dsh-iris mcp config --connection-file <private-client-json> [--preset <generic|claude|codex|kimi>-<stdio|http> | --transport <stdio|http> --format <mcpServers|entry|claude-json|codex-toml>] [--output <new-absolute-path>]',
100
+ ' dsh-iris mcp check --connection-file <private-client-json> [--scopes <read,create,execute,stop>]',
101
+ ' stdio config also accepts scopes/capabilities/tools. HTTP config includes credentials and requires a new file inside the connection private directory.',
102
+ ' dsh-iris api serve ... --connections-file <private-grants-file> --allow-mcp',
103
+ ' dsh-iris web serve ... --connection-store <private-directory-outside-core> --allow-connection-management [--allow-mcp] [--allow-execution --provider-config <absolute-path>]',
104
+ ' dsh-iris web serve ... --provider-config <private-config> --allow-provider-management [--panel <trusted-panel-manifest.json>]',
105
+ ' dsh-iris api|web serve ... --network-config <private-config> --allow-network-management',
106
+ ' dsh-iris api|web serve ... --scheduler-config <absolute-private-json> enables bounded media scheduling (HTTP concurrency <=4, rolling RPS and remote outstanding limits).',
107
+ ' Explicit standard Streamable HTTP endpoint at /mcp; default remains disabled. Uses the existing Core service and user-selected connection tools.',
108
+ ' dsh-iris operation list --data-root <execution-root> [--run-id <run-id>] [--limit <1..30>] [--cursor <operation-id>]',
109
+ ' dsh-iris operation inspect <operation-id> --data-root <execution-root>',
110
+ ' dsh-iris api serve --data-root <existing-flow-root> --token-file <new-absolute-path-outside-root> [--provider-config <absolute-path>] [--port <0..65535>] [--allow-local-writes] [--allow-selection]',
111
+ ' Explicit loopback-only API, read-only by default. Local writes import images/audio and create Runs within the root capabilities, without generation. The session token is saved privately; stdout contains no token.',
112
+ ' dsh-iris web serve --data-root <existing-flow-root> [--auth-store <absolute-private-directory>] [--provider-config <absolute-path>] [--port <0..65535>] [--allow-local-writes] [--allow-execution] [--allow-selection]',
113
+ ' Default: set one access password, then sign in at origin. Remembered browser sessions survive normal restarts. No Token file is required.',
114
+ ' Auth state defaults to $IRIS_HOME/web-auth/<root-key> ($IRIS_HOME defaults to ~/.iris). Open the initial loginUrl once to set your password.',
115
+ ' dsh-iris web login --data-root <existing-flow-root> [--auth-store <absolute-private-directory>]',
116
+ ' dsh-iris web password reset --data-root <existing-flow-root> [--auth-store <absolute-private-directory>]',
117
+ ' Reset only the workbench password/browser sessions; Core work and MCP grants remain intact. Prints a setup link while the service is running.',
118
+ ' Explicit --token-file or --token-dir keeps the legacy Token/link login mode. Do not combine them with --auth-store. api serve still requires one of these Token options.',
119
+ ' dsh-iris web login --origin <http://127.0.0.1:port> --token-file <current-session-token-file>',
120
+ ' Get a fresh one-time login link without restarting the workbench.',
121
+ ' Workbench media APIs are read-only by default; explicit write flags do not require connection management. --allow-selection permits owner-only, revision-checked image selection. Execution enables supported image/speech/transcription flows; Provider management uses its own owner-only flag.',
61
122
  ' dsh-iris runtime recover --data-root <absolute-path> --confirm-stale-pid <pid>',
62
123
  ' dsh-iris providers list --provider-config <absolute-path>',
63
124
  ' dsh-iris capabilities list --provider-config <absolute-path>',
64
125
  ' dsh-iris config <init|show|check> --provider-config <absolute-path>',
126
+ ' dsh-iris network <init|show|check> --network-config <absolute-private-path>',
127
+ ' dsh-iris network routing <init|copy> --network-config <new-private-path> [--input <json with source>]',
128
+ ' dsh-iris network catalog create --network-config <v2-path> --provider-config <source-path> --output <new-private-path>',
129
+ ' dsh-iris network bindings <list|set|remove> --network-config <v2-path> [--provider-config <wrapped-path> --input <json>]',
130
+ ' Media/flow/tasks/web accept --network-config with wrapped Provider catalogs. Explicit policy failures never silently inherit or replay generation.',
131
+ ' dsh-iris network <nodes|pools> list --network-config <absolute-private-path>',
132
+ ' dsh-iris network <nodes|pools> <add|set|remove> --network-config <absolute-private-path> --input <json>',
133
+ ' Owner-only resources use environment credential references and revision-checked mutations. Resources alone do not change routes; wrapped catalogs and explicit --network-config enable bound policies.',
65
134
  ' dsh-iris providers <add|set> --provider-config <absolute-path> --input <provider-json>',
66
135
  ' dsh-iris providers remove <provider-id> --provider-config <absolute-path>',
67
136
  ' dsh-iris models list --provider-config <absolute-path>',
@@ -97,8 +166,9 @@ function usage() {
97
166
  ' dsh-iris artifact export <id> --data-root <absolute-path> --output <absolute-path>',
98
167
  ' dsh-iris artifact rebuild --data-root <absolute-path>',
99
168
  ' dsh-iris flow init --data-root <new-absolute-path>',
100
- ' dsh-iris flow create --data-root <absolute-path> --provider-config <absolute-path> --input <image-edit-json>',
101
- ' Import the source image after flow init; use explicit model_ref and n=1. create saves private inputs but never submits.',
169
+ ' dsh-iris flow create --data-root <absolute-path> --provider-config <absolute-path> --input <image-json>',
170
+ ' dsh-iris models workflow-check <provider::workflow> --provider-config <absolute-path> [--input <workflow_parameters-json>] (read-only)',
171
+ ' Use explicit model_ref and n=1. Edits require an imported source; text-to-image requires a new execution init --image-generation root. create saves private inputs but never submits.',
102
172
  ' dsh-iris flow <list|inspect> [run-id] --data-root <absolute-path>',
103
173
  ' dsh-iris flow continue <run-id> --data-root <absolute-path> [--provider-config <absolute-path>]',
104
174
  ' dsh-iris flow select <run-id> --data-root <absolute-path> --input <json with artifact_id>',
@@ -108,11 +178,11 @@ function usage() {
108
178
  ' dsh-iris core transactions --data-root <absolute-path>',
109
179
  ' dsh-iris core restore <transaction-id> --data-root <absolute-path>',
110
180
  '',
111
- 'Commands never start DSH. run image and the task observe/redeliver/cancel/retry verbs contact a provider only when explicitly requested; credentials come from a private file. observe polls once, redeliver only re-downloads, and cancel is recorded as canceled only when the provider explicitly confirms; none of them ever submits an existing task again. task retry creates a NEW task that may incur duplicate generation billing and requires --confirm-billing; the prompt must be supplied again because Task records never persist prompts. Flow inputs do retain the prompt privately. flow continue may perform the first submission or observe/redeliver the original task, but never resubmits a task with an Attempt.'
181
+ 'Commands never start DSH. run image and the task observe/redeliver/cancel/retry verbs contact a provider only when explicitly requested; provider configuration comes from a private file; explicit environment key references are resolved in this process. observe polls once, redeliver only re-downloads, and cancel is recorded as canceled only when the provider explicitly confirms; none of them ever submits an existing task again. task retry creates a NEW task that may incur duplicate generation billing and requires --confirm-billing; the prompt must be supplied again because Task records never persist prompts. Flow inputs do retain the prompt privately. flow continue may perform the first submission or observe/redeliver the original task, but never resubmits a task with an Attempt.'
112
182
  ].join('\n');
113
183
  }
114
184
 
115
- function flags(args, allowed) {
185
+ function parseFlags(args, allowed) {
116
186
  const parsed = {};
117
187
  for (let index = 0; index < args.length; index += 2) {
118
188
  const flag = args[index];
@@ -170,6 +240,7 @@ function generationInput(capability, input, options) {
170
240
  /** C-8: catchable signals dispose active runtimes before the CLI exits. */
171
241
  const activeRuntimes = new Set();
172
242
  const cliAbortController = new AbortController();
243
+ let cliProviderRegistry;
173
244
  let shuttingDown = false;
174
245
 
175
246
  function registerRuntime(runtime) {
@@ -180,6 +251,8 @@ function registerRuntime(runtime) {
180
251
  async function disposeActiveRuntimes() {
181
252
  await Promise.allSettled([...activeRuntimes].map((runtime) => runtime.dispose().catch(() => {})));
182
253
  activeRuntimes.clear();
254
+ try { await cliProviderRegistry?.dispose(); }
255
+ finally { try { await cliNetworkManager?.dispose(); } finally { cliScheduler?.dispose(); } }
183
256
  }
184
257
 
185
258
  for (const signal of ['SIGINT', 'SIGTERM']) {
@@ -209,26 +282,78 @@ async function withRuntime(mode, dataRoot, callback, ports = {}) {
209
282
  return withRawRuntime(mode, dataRoot, (runtime) => callback(createCommandService(runtime, ports)));
210
283
  }
211
284
 
212
- async function withApplication(dataRoot, callback, resolveImageCandidates, resolveTaskAdapter) {
213
- const application = createApplicationRuntime({ dataRoot: required(dataRoot, 'data-root'), resolveImageCandidates, resolveTaskAdapter });
285
+ async function withApplication(dataRoot, callback, ports = {}) {
286
+ const application = createApplicationRuntime({ dataRoot: required(dataRoot, 'data-root'), ...ports });
214
287
  const unregister = registerRuntime(application);
215
288
  try { return await callback(application); }
216
289
  finally { try { await application.dispose(); } finally { unregister(); } }
217
290
  }
218
291
 
219
- async function main(args) {
292
+ /** Own the service and its plugin registry in order, including cancellation during import/start. */
293
+ async function withCliLaunch(pluginConfig, launchOptions, callback) {
294
+ let loading, launch, api, disposal, startupFailure;
295
+ const detail = error => ({ code: error.code || 'IRIS_CLI_FAILED', message: error.message || String(error),
296
+ ...(error instanceof AggregateError ? { errors: error.errors.map(detail) } : {}) });
297
+ const lifetime = { dispose() {
298
+ if (!disposal) disposal = (async () => {
299
+ if (loading) await Promise.allSettled([loading]); // main retains the original startup error.
300
+ const failures = [];
301
+ try { await api?.dispose(); } catch (error) { failures.push({ stage: 'core-stop', error }); }
302
+ try { await launch?.dispose(); } catch (error) { failures.push({ stage: 'plugin-stop', error }); }
303
+ if (failures.length) {
304
+ const error = Object.assign(new AggregateError(failures.map(item => item.error), 'Core 服务或插件释放失败;已尝试两阶段收尾'), { code: 'IRIS_CLI_SERVICE_CLEANUP_FAILED' });
305
+ error.diagnostic = { code: error.code, stage: 'shutdown', message: error.message, ...(pluginConfig ? { file: pluginConfig } : {}),
306
+ errors: failures.map(item => ({ stage: item.stage, ...detail(item.error) })) };
307
+ if (cliAbortController.signal.aborted) console.error(JSON.stringify(error.diagnostic));
308
+ throw error;
309
+ }
310
+ })();
311
+ return disposal;
312
+ } };
313
+ const unregister = registerRuntime(lifetime);
314
+ try {
315
+ loading = pluginConfig ? loadPluginLaunch(pluginConfig, launchOptions).then(value => (launch = value)) : Promise.resolve();
316
+ await loading; cliAbortController.signal.throwIfAborted();
317
+ return await callback(launch, value => { api = value; });
318
+ } catch (error) { startupFailure = error; throw error; }
319
+ finally {
320
+ try { await lifetime.dispose(); }
321
+ catch (error) { if (startupFailure) error.diagnostic.startup = detail(startupFailure); throw error; }
322
+ finally { unregister(); }
323
+ }
324
+ }
325
+
326
+ async function main(args, providerRegistry) {
327
+ const flags = (values, allowed) => Object.defineProperty(parseFlags(values, allowed), 'providerRegistry', { value: providerRegistry });
220
328
  if (!args.length || args.includes('--help') || args.includes('-h')) {
221
329
  console.log(usage());
222
330
  return args.length ? 0 : 2;
223
331
  }
224
332
 
333
+ if (args[0] === 'providers' && args[1] === 'protocols') {
334
+ flags(args.slice(2), []);
335
+ console.log(JSON.stringify(providerRegistry.snapshot(), null, 2)); return 0;
336
+ }
337
+
338
+ if (args[0] === 'plugins') {
339
+ if (args[1] === 'list') {
340
+ flags(args.slice(2), []);
341
+ console.log(JSON.stringify({ contractVersion: 0, builtins: builtinLaunchPlugins(), automaticDiscovery: false })); return 0;
342
+ }
343
+ if (args[1] !== 'inspect') throw new CliUsageError('plugins 仅支持 list/inspect');
344
+ const options = flags(args.slice(2), ['plugin-config']);
345
+ return withCliLaunch(required(options['plugin-config'], 'plugin-config'), {}, async launch => {
346
+ console.log(JSON.stringify(launch.summary, null, 2)); return 0;
347
+ });
348
+ }
349
+
225
350
  if (args[0] === 'skills') {
226
351
  const rest = args.slice(2), dryRunFlags = rest.filter(value => value === '--dry-run');
227
352
  if (dryRunFlags.length > 1) throw new CliUsageError('参数重复:--dry-run');
228
- const options = flags(rest.filter(value => value !== '--dry-run'), ['target', 'directory']);
353
+ const options = flags(rest.filter(value => value !== '--dry-run'), ['target', 'directory', 'bundle']);
229
354
  let result;
230
355
  try {
231
- result = manageCoreSkill({ action: args[1], target: options.target, directory: options.directory, dryRun: dryRunFlags.length === 1 });
356
+ result = manageCoreSkill({ action: args[1], target: options.target, directory: options.directory, bundle: options.bundle, dryRun: dryRunFlags.length === 1 });
232
357
  } catch (error) {
233
358
  if (error.code === 'IRIS_SKILL_OPTIONS_INVALID') throw new CliUsageError(error.message);
234
359
  throw error;
@@ -237,8 +362,133 @@ async function main(args) {
237
362
  return args[1] === 'status' && result.status !== 'current' ? 1 : 0;
238
363
  }
239
364
 
240
- if (args[0] === 'api') {
241
- if (args[1] === 'init') {
365
+ if (args[0] === 'mcp') {
366
+ if (args[1] === 'presets') {
367
+ flags(args.slice(2), []);
368
+ const { MCP_CONNECTION_PRESETS } = await import('../lib/mcp-delivery.js');
369
+ console.log(JSON.stringify({ contractVersion: 0, presets: MCP_CONNECTION_PRESETS, installationDetected: false }, null, 2)); return 0;
370
+ }
371
+ if (args[1] === 'capabilities') {
372
+ flags(args.slice(2), []);
373
+ const { BRIDGE_CAPABILITIES, BRIDGE_CONTROL_TOOLS } = await import('../lib/core-bridge-selection.js');
374
+ console.log(JSON.stringify({ contractVersion: 0, capabilities: BRIDGE_CAPABILITIES, controlTools: BRIDGE_CONTROL_TOOLS,
375
+ implementedMedia: BRIDGE_CAPABILITIES.map(group => group.recipe), selectionDoesNotGenerate: true }, null, 2)); return 0;
376
+ }
377
+ if (!['serve', 'config', 'check'].includes(args[1])) throw new CliUsageError('mcp 支持 serve/config/check/capabilities/presets');
378
+ const options = flags(args.slice(2), ['connection-file', 'scopes', 'capabilities', 'tools', ...(args[1] === 'config' ? ['transport', 'format', 'preset', 'output'] : [])]);
379
+ const list = value => value === 'none' ? [] : value.split(',');
380
+ const selected = { ...(options.capabilities !== undefined ? { capabilities: list(options.capabilities) } : {}),
381
+ ...(options.tools !== undefined ? { tools: list(options.tools) } : {}) };
382
+ if (args[1] === 'check') {
383
+ const { checkMcpConnection } = await import('../lib/mcp-check.js');
384
+ const result = await checkMcpConnection({ connectionFile: required(options['connection-file'], 'connection-file'),
385
+ ...(options.scopes !== undefined ? { scopes: options.scopes.split(',') } : {}), ...selected });
386
+ console.log(JSON.stringify(result, null, 2)); return result.ok ? 0 : 1;
387
+ }
388
+ if (args[1] === 'config') {
389
+ const { exportMcpConfiguration } = await import('../lib/mcp-config.js');
390
+ const result = exportMcpConfiguration({ connectionFile: required(options['connection-file'], 'connection-file'), ...selected,
391
+ ...(options.scopes !== undefined ? { scopes: options.scopes.split(',') } : {}), transport: options.transport, format: options.format, presetId: options.preset, output: options.output });
392
+ process.stdout.write(typeof result === 'string' ? result : JSON.stringify(result, null, 2) + '\n');
393
+ return 0;
394
+ }
395
+ const { createMcpBridge } = await import('../lib/mcp-bridge.js');
396
+ const manifest = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
397
+ const bridge = createMcpBridge({ connectionFile: required(options['connection-file'], 'connection-file'),
398
+ requestedScopes: (options.scopes === undefined ? 'read' : options.scopes).split(','), ...selected, version: manifest.version });
399
+ const unregister = registerRuntime(bridge);
400
+ try { await bridge.start(); await bridge.closed; return 0; }
401
+ finally { try { await bridge.dispose(); } finally { unregister(); } }
402
+ }
403
+ if (args[0] === 'connection') {
404
+ if (args[1] !== 'create') throw new CliUsageError('connection 只支持显式 create');
405
+ const options = flags(args.slice(2), ['data-root', 'directory', 'origin', 'harness', 'label', 'scopes', 'capabilities', 'tools']);
406
+ const result = createConnectionPair({ dataRoot: required(options['data-root'], 'data-root'), directory: required(options.directory, 'directory'), origin: required(options.origin, 'origin'),
407
+ harnessKind: options.harness === undefined ? 'generic' : options.harness, label: required(options.label, 'label'),
408
+ ...(options.scopes !== undefined ? { scopes: options.scopes.split(',') } : {}),
409
+ ...(options.capabilities !== undefined ? { capabilities: options.capabilities === 'none' ? [] : options.capabilities.split(',') } : {}),
410
+ ...(options.tools !== undefined ? { tools: options.tools === 'none' ? [] : options.tools.split(',') } : {}) });
411
+ console.log(JSON.stringify(result, null, 2)); return 0;
412
+ }
413
+ if (args[0] === 'execution' || args[0] === 'operation') {
414
+ if (args[0] === 'execution') {
415
+ if (['inspect', 'recover-lease'].includes(args[1])) {
416
+ const options = flags(args.slice(2), ['data-root', ...(args[1] === 'recover-lease' ? ['confirm-stale-pid'] : [])]);
417
+ const dataRoot = required(options['data-root'], 'data-root');
418
+ if (args[1] === 'recover-lease' && !/^[1-9][0-9]*$/.test(options['confirm-stale-pid'] || '')) throw new CliUsageError('需要明确回显 stale owner PID');
419
+ const result = args[1] === 'inspect' ? inspectExecutionLease(dataRoot)
420
+ : recoverExecutionLease(dataRoot, { confirmStalePid: Number(options['confirm-stale-pid']) });
421
+ console.log(JSON.stringify(result, null, 2)); return 0;
422
+ }
423
+ if (args[1] !== 'init') throw new CliUsageError('不支持的 execution 子命令');
424
+ const initArgs = args.slice(2), generationFlags = initArgs.filter(value => value === '--image-generation'), audioFlags = initArgs.filter(value => value === '--audio'), videoFlags = initArgs.filter(value => value === '--video');
425
+ if (generationFlags.length + audioFlags.length + videoFlags.length > 1) throw new CliUsageError('初始化开关重复或冲突;video包含音频和图片,audio包含图片');
426
+ const options = flags(initArgs.filter(value => !['--image-generation', '--audio', '--video'].includes(value)), ['data-root']);
427
+ const suppliedRoot = required(options['data-root'], 'data-root');
428
+ if (!path.isAbsolute(suppliedRoot)) throw new CliUsageError('data-root 必须是已初始化的空 Flow 根绝对路径');
429
+ let dataRoot;
430
+ try { dataRoot = fs.realpathSync(suppliedRoot); }
431
+ catch (_) { throw new CliUsageError('请先在新的专用数据根执行 flow init'); }
432
+ const result = await withApplication(dataRoot, application =>
433
+ application.execute('execution.init', videoFlags.length ? { video: true } : audioFlags.length ? { audio: true } : generationFlags.length ? { imageGeneration: true } : {}, { signal: cliAbortController.signal }));
434
+ console.log(JSON.stringify(result, null, 2)); return 0;
435
+ }
436
+ if (!['list', 'inspect'].includes(args[1])) throw new CliUsageError('operation 只支持只读 list/inspect');
437
+ const inspecting = args[1] === 'inspect';
438
+ if (inspecting && (!args[2] || args[2].startsWith('--'))) throw new CliUsageError('缺少 Operation ID');
439
+ const options = flags(args.slice(inspecting ? 3 : 2), ['data-root', ...(inspecting ? [] : ['run-id', 'limit', 'cursor'])]);
440
+ if (options.limit !== undefined && !/^(?:[1-9]|[12][0-9]|30)$/.test(options.limit)) throw new CliUsageError('limit 必须是 1–30 的整数');
441
+ const input = inspecting ? { id: args[2] } : { ...(options['run-id'] ? { runId: options['run-id'] } : {}),
442
+ ...(options.limit !== undefined ? { limit: Number(options.limit) } : {}), ...(options.cursor ? { cursor: options.cursor } : {}) };
443
+ const result = await withApplication(required(options['data-root'], 'data-root'), application =>
444
+ application.execute('operation.' + args[1], input, { signal: cliAbortController.signal }));
445
+ console.log(JSON.stringify(result, null, 2)); return 0;
446
+ }
447
+
448
+ if (args[0] === 'api' || args[0] === 'web') {
449
+ const serveWorkbench = args[0] === 'web';
450
+ if (serveWorkbench && args[1] === 'password') {
451
+ if (args[2] !== 'reset') throw new CliUsageError('web password 仅支持 reset');
452
+ const options = flags(args.slice(3), ['data-root', 'auth-store']);
453
+ const dataRoot = required(options['data-root'], 'data-root');
454
+ if (!path.isAbsolute(dataRoot)) throw new CliUsageError('data-root 必须是工作台数据根的绝对路径');
455
+ console.log(JSON.stringify(await manageWorkbenchPassword({ dataRoot, directory: options['auth-store'], reset: true, signal: cliAbortController.signal })));
456
+ return 0;
457
+ }
458
+ if (serveWorkbench && args[1] === 'login') {
459
+ const options = flags(args.slice(2), ['origin', 'token-file', 'data-root', 'auth-store']);
460
+ if (options['data-root'] !== undefined) {
461
+ if (options.origin !== undefined || options['token-file'] !== undefined) throw new CliUsageError('口令登录用 data-root,旧 Token 登录用 origin/token-file,不能混用');
462
+ if (!path.isAbsolute(options['data-root'])) throw new CliUsageError('data-root 必须是工作台数据根的绝对路径');
463
+ console.log(JSON.stringify(await manageWorkbenchPassword({ dataRoot: options['data-root'], directory: options['auth-store'], signal: cliAbortController.signal })));
464
+ return 0;
465
+ }
466
+ if (options['auth-store'] !== undefined) throw new CliUsageError('auth-store 需要同时明确 data-root');
467
+ const origin = required(options.origin, 'origin'), tokenFile = required(options['token-file'], 'token-file');
468
+ if (!/^http:\/\/127\.0\.0\.1:[1-9]\d{0,4}$/.test(origin) || Number(origin.split(':').at(-1)) > 65535) throw new CliUsageError('origin 必须是启动时输出的 http://127.0.0.1:端口');
469
+ if (!path.isAbsolute(tokenFile)) throw new CliUsageError('token-file 必须是当前启动文件的绝对路径');
470
+ let token;
471
+ const fd = fs.openSync(tokenFile, 'r');
472
+ try {
473
+ const info = fs.fstatSync(fd);
474
+ if (!info.isFile() || info.size > 256) throw new CliUsageError('请选择当前启动生成的小型 Token 文本文件');
475
+ token = fs.readFileSync(fd, 'utf8').trim();
476
+ } finally { fs.closeSync(fd); }
477
+ if (!/^[A-Za-z0-9_-]{43}$/.test(token)) throw new CliUsageError('Token 文件内容无效,请使用当前启动生成的文件');
478
+ const response = await fetch(origin + '/api/v0/workbench/login-link', { method: 'POST',
479
+ headers: { Authorization: 'Bearer ' + token, 'Content-Type': 'application/json' }, body: '{}',
480
+ redirect: 'error', signal: AbortSignal.any([cliAbortController.signal, AbortSignal.timeout(15000)]) });
481
+ if (!response.ok) throw new CliUsageError(response.status === 401 ? 'Token 已失效,请使用当前服务启动时生成的 Token 文件'
482
+ : response.status === 404 || response.status === 405 ? '此服务不支持登录链接,请确认地址属于新版 web serve 工作台' : '无法获取登录链接,请检查本机工作台状态');
483
+ const result = await response.json();
484
+ if (result.contractVersion !== 0 || typeof result.loginUrl !== 'string' || !result.loginUrl.startsWith(origin + '/#login=')
485
+ || !/^[A-Za-z0-9_-]{43}$/.test(result.loginUrl.slice((origin + '/#login=').length)) || !Number.isFinite(Date.parse(result.loginExpiresAt))) {
486
+ throw new CliUsageError('工作台返回的登录链接无效');
487
+ }
488
+ console.log(JSON.stringify({ ...result, message: '打开 loginUrl 即可进入;链接 10 分钟内有效、仅用一次,刷新无需重新登录。' }));
489
+ return 0;
490
+ }
491
+ if (!serveWorkbench && args[1] === 'init') {
242
492
  const options = flags(args.slice(2), ['data-root']);
243
493
  const suppliedRoot = required(options['data-root'], 'data-root');
244
494
  if (!path.isAbsolute(suppliedRoot)) throw new CliUsageError('data-root 必须是已初始化的空 Flow 根绝对路径');
@@ -249,30 +499,64 @@ async function main(args) {
249
499
  console.log(JSON.stringify(initialized));
250
500
  return 0;
251
501
  }
252
- if (args[1] !== 'serve') throw new CliUsageError('不支持的 api 子命令');
253
- const rest = args.slice(2), localWriteFlags = rest.filter(value => value === '--allow-local-writes');
254
- if (localWriteFlags.length > 1) throw new CliUsageError('参数重复:--allow-local-writes');
255
- const allowLocalWrites = localWriteFlags.length === 1;
256
- const options = flags(rest.filter(value => value !== '--allow-local-writes'), ['data-root', 'token-file', 'provider-config', 'port']);
502
+ if (args[1] !== 'serve') throw new CliUsageError(`不支持的 ${args[0]} 子命令`);
503
+ const rest = args.slice(2), localWriteFlags = rest.filter(value => value === '--allow-local-writes'), executionFlags = rest.filter(value => value === '--allow-execution'), selectionFlags = rest.filter(value => value === '--allow-selection'), mcpFlags = rest.filter(value => value === '--allow-mcp'), managementFlags = rest.filter(value => value === '--allow-connection-management'), providerFlags = rest.filter(value => value === '--allow-provider-management'), networkFlags = rest.filter(value => value === '--allow-network-management');
504
+ if (localWriteFlags.length > 1 || executionFlags.length > 1 || selectionFlags.length > 1 || mcpFlags.length > 1 || managementFlags.length > 1 || providerFlags.length > 1 || networkFlags.length > 1) throw new CliUsageError('写入/执行/选择/MCP/管理参数重复');
505
+ const allowExecution = executionFlags.length === 1, allowLocalWrites = localWriteFlags.length === 1 || allowExecution;
506
+ const allowSelection = selectionFlags.length === 1;
507
+ const allowConnectionManagement = managementFlags.length === 1;
508
+ const options = flags(rest.filter(value => !['--allow-local-writes', '--allow-execution', '--allow-selection', '--allow-mcp', '--allow-connection-management', '--allow-provider-management', '--allow-network-management'].includes(value)), ['data-root', 'token-file', 'token-dir', 'auth-store', 'provider-config', 'port', 'connections-file', 'connection-store', 'panel', 'plugin-config']);
257
509
  const suppliedRoot = required(options['data-root'], 'data-root');
258
- const tokenFile = required(options['token-file'], 'token-file');
259
- if (!path.isAbsolute(suppliedRoot) || !path.isAbsolute(tokenFile)) throw new CliUsageError('data-root 与 token-file 必须是绝对路径');
510
+ const passwordMode = serveWorkbench && options['token-file'] === undefined && options['token-dir'] === undefined;
511
+ if (!passwordMode && Boolean(options['token-file']) === Boolean(options['token-dir'])) throw new CliUsageError('请选择且只选择 --token-file 或 --token-dir');
512
+ if (options['auth-store'] !== undefined && !passwordMode) throw new CliUsageError('auth-store 仅用于默认口令工作台;不能与 Token 参数或 api serve 混用');
513
+ if (!path.isAbsolute(suppliedRoot) || !passwordMode && !path.isAbsolute(options['token-file'] || options['token-dir'])) throw new CliUsageError('data-root 与 token-file/token-dir 必须是绝对路径');
514
+ let tokenFile = options['token-file'];
260
515
  if (options.port !== undefined && (!/^\d+$/.test(options.port) || Number(options.port) > 65535)) throw new CliUsageError('port 必须是 0–65535 的整数');
261
516
  // Pin the physical root for both the server and the outside-root credential check.
262
517
  let dataRoot;
263
518
  try { dataRoot = fs.realpathSync(suppliedRoot); }
264
519
  catch (_) { throw new CliUsageError('data-root 必须是已初始化且可读取的 Flow 根'); }
265
- const api = createLocalReadApi({ dataRoot, providerConfigPath: options['provider-config'], allowLocalWrites });
266
- const unregister = registerRuntime(api);
267
- try {
520
+ const authDirectory = passwordMode ? resolveWorkbenchAuthDirectory(dataRoot, options['auth-store']) : undefined;
521
+ if (options['token-dir']) checkLocalApiTokenDirectory(options['token-dir'], dataRoot);
522
+ return withCliLaunch(options['plugin-config'], { serveWorkbench, panelManifestPath: options.panel }, async (launch, ownApi) => {
523
+ const api = createLocalReadApi({ dataRoot, providerConfigPath: options['provider-config'], allowLocalWrites, serveWorkbench,
524
+ ...(cliNetworkConfigPath || cliScheduler ? { networkConfigPath: cliNetworkConfigPath,
525
+ networkManager: cliNetworkManager ||= createProviderNetworkManager({ networkConfigPath: cliNetworkConfigPath, scheduler: cliScheduler }), scheduler: cliScheduler } : {}),
526
+ ...(passwordMode ? { workbenchAuth: { directory: authDirectory } } : {}),
527
+ allowExecution, allowSelection, allowMcp: mcpFlags.length === 1, connectionsFile: options['connections-file'], allowConnectionManagement, connectionStore: options['connection-store'], providerRegistry,
528
+ ...(launch ? { pluginRegistry: launch.registry, providerRegistry: launch.registry.providers, allowedOperations: launch.allowedOperations,
529
+ panelContributions: launch.panelContributions } : {}),
530
+ allowProviderManagement: providerFlags.length === 1, allowNetworkManagement: networkFlags.length === 1, panelManifestPath: options.panel });
531
+ ownApi(api);
268
532
  const started = await api.start({ port: options.port === undefined ? 0 : Number(options.port) });
269
- writeLocalApiTokenFile(tokenFile, started.accessToken, dataRoot);
270
- console.log(JSON.stringify({ contractVersion: 0, readOnly: !allowLocalWrites, origin: started.origin, tokenFile,
271
- message: allowLocalWrites ? '本地创建 API 已启动;只上传/创建,不执行生成。会话 Token 在指定私有文件中,退出后失效。'
533
+ if (!passwordMode) {
534
+ if (options['token-dir']) tokenFile = writeLocalApiTokenDirectory(options['token-dir'], started.accessToken, dataRoot);
535
+ else writeLocalApiTokenFile(tokenFile, started.accessToken, dataRoot);
536
+ }
537
+ console.log(JSON.stringify({ contractVersion: 0, readOnly: !allowLocalWrites && !allowSelection, origin: started.origin,
538
+ ...(passwordMode ? { authentication: started.authentication, authStore: authDirectory } : { tokenFile }),
539
+ ...(launch ? { pluginLaunch: launch.summary } : {}),
540
+ ...(serveWorkbench ? { loginUrl: started.loginUrl, loginExpiresAt: started.loginExpiresAt } : {}),
541
+ ...(allowConnectionManagement ? { connectionManagement: true } : {}),
542
+ ...(providerFlags.length ? { providerManagement: true } : {}),
543
+ ...(networkFlags.length ? { networkManagement: true } : {}),
544
+ ...(started.mcpEndpoint ? { mcpEndpoint: started.mcpEndpoint } : {}),
545
+ message: passwordMode ? (started.authentication.configured ? '工作台已启动,打开 origin 用访问口令登录;记住设备后可跨正常重启继续使用。'
546
+ : '首次使用请打开 loginUrl 设置访问口令;之后打开普通地址即可登录,无需 Token 文件。')
547
+ : serveWorkbench ? (providerFlags.length ? 'Core 工作台与供应商管理已启动;打开 loginUrl 即可进入。配置保存不生成媒体。'
548
+ : allowConnectionManagement ? 'Core 工作台与连接管理已启动;打开 loginUrl 即可进入。配置连接不执行媒体。'
549
+ : allowLocalWrites || allowSelection ? 'Core 工作台已启动;打开 loginUrl 即可进入。媒体 API 按显式开关开放。'
550
+ : launch?.allowedOperations !== undefined ? 'Core 工作台已启动;打开 loginUrl 即可进入。插件业务按启动选择开放,媒体 API 保持只读。'
551
+ : '只读工作台已启动;打开 loginUrl 即可进入。')
552
+ : allowExecution ? '有限执行 API 已启动;显式继续才执行,停止只中止本地等待,重启不自动重提。'
553
+ : allowLocalWrites ? '本地创建 API 已启动;只上传/创建,不执行生成。会话 Token 在指定私有文件中,退出后失效。'
554
+ : allowSelection ? '本地选择 API 已启动;只更新已完成流程的选择,不执行生成。会话 Token 在指定私有文件中,退出后失效。'
555
+ : launch?.allowedOperations !== undefined ? '插件业务 API 已启动;按启动选择执行,媒体 API 保持只读。会话 Token 在指定私有文件中,退出后失效。'
272
556
  : '只读本地 API 已启动;会话 Token 在指定私有文件中,退出后失效。' }));
273
557
  await api.closed;
274
558
  return 0;
275
- } finally { try { await api.dispose(); } finally { unregister(); } }
559
+ });
276
560
  }
277
561
 
278
562
  if (args[0] === 'flow') {
@@ -287,19 +571,12 @@ async function main(args) {
287
571
  if (verb === 'select' && Object.keys(input).some(key => key !== 'artifact_id')) throw new CliUsageError('flow select 只接受 artifact_id');
288
572
  if (needsId) input.run_id = args[2];
289
573
  if (verb === 'export') input.output_path = required(options.output, 'output');
290
- const resolveImageCandidates = options['provider-config'] ? async ({ modelRef, signal }) => {
291
- const catalog = await generationCatalog(options, 'image-gen', modelRef, signal);
292
- return imageCandidatesFromCatalog(catalog, modelRef).map(route => ({ adapter: trackedProviderAdapter(route.provider, options),
293
- model: route.modelRef, selectionReason: route.selectionReason, providerBinding: providerTaskBinding(route.provider) }));
294
- } : undefined;
295
- const resolveTaskAdapter = options['provider-config'] ? task => {
296
- const catalog = loadProviderCatalog(options['provider-config']);
297
- return trackedProviderAdapter(providerForTaskFromCatalog(catalog, task), options, task);
298
- } : undefined;
574
+ const ports = options['provider-config'] ? configuredImagePorts(options['provider-config'], providerRegistry,
575
+ { resolveProviderNetwork: cliResolveNetwork }) : {};
299
576
  let result;
300
577
  try {
301
578
  result = await withApplication(options['data-root'], application =>
302
- application.execute('flow.' + verb, input, { signal: cliAbortController.signal }), resolveImageCandidates, resolveTaskAdapter);
579
+ application.execute('flow.' + verb, input, { signal: cliAbortController.signal }), ports);
303
580
  } catch (error) {
304
581
  if (error instanceof GenerationInputError) throw new CliUsageError(error.message);
305
582
  throw error;
@@ -344,14 +621,58 @@ async function main(args) {
344
621
  if (['providers', 'capabilities'].includes(args[0]) && args[1] === 'list') {
345
622
  const options = flags(args.slice(2), ['provider-config']);
346
623
  const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
347
- const report = args[0] === 'providers' ? providerCatalogSnapshot(catalog) : catalogCapabilitySnapshot(catalog);
624
+ const report = args[0] === 'providers' ? providerCatalogSnapshot(catalog) : catalogCapabilitySnapshot(catalog, providerRegistry);
348
625
  console.log(JSON.stringify(report, null, 2));
349
626
  return 0;
350
627
  }
351
628
 
629
+ if (args[0] === 'network') {
630
+ const group = args[1];
631
+ const collection = ['nodes', 'pools', 'bindings', 'routing', 'catalog'].includes(group);
632
+ const verb = collection ? args[2] : group;
633
+ const verbs = group === 'routing' ? ['init', 'copy'] : group === 'catalog' ? ['create'] : group === 'bindings' ? ['list', 'set', 'remove'] : collection ? ['list', 'add', 'set', 'remove'] : ['init', 'show', 'check'];
634
+ if (!verbs.includes(verb)) {
635
+ throw new CliUsageError('未知网络资源命令');
636
+ }
637
+ const mutation = collection && verb !== 'list' && !(group === 'routing' && verb === 'init') && group !== 'catalog';
638
+ let options;
639
+ try { options = flags(args.slice(collection ? 3 : 2), ['network-config', ...(mutation ? ['input'] : []),
640
+ ...(['catalog', 'bindings'].includes(group) ? ['provider-config'] : []), ...(group === 'catalog' ? ['output'] : [])]); }
641
+ catch (error) {
642
+ if (!(error instanceof CliUsageError)) throw error;
643
+ throw new CliUsageError('网络参数无效:请使用 --network-config;写入内容通过 --input、--input-file 或 --input - 提供。代理地址请在节点 JSON 中分别填写 host/port,认证使用环境变量引用');
644
+ }
645
+ const file = required(options['network-config'], 'network-config');
646
+ if (group === 'catalog') {
647
+ const network = readNetworkConfig(file).config;
648
+ if (network.schemaVersion !== 2) throw networkPolicyError('IRIS_NETWORK_CONFIG_VERSION', '网络目录需使用显式新建或另存的 v2 网络文件');
649
+ const source = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
650
+ const requirement = { schemaVersion: 1, configId: network.configId, catalogId: crypto.randomUUID() };
651
+ const output = required(options.output, 'output');
652
+ if (!path.isAbsolute(output)) throw new CliUsageError('output 必须是新私有文件的绝对路径');
653
+ atomicCreatePrivate(output, JSON.stringify(encodeProviderConfig(wrapProviderCatalog(source, requirement)), null, 2) + '\n');
654
+ console.log(JSON.stringify({ created: true, networkRequirement: requirement }));
655
+ return 0;
656
+ }
657
+ let input = mutation ? jsonInput(options.input) : {};
658
+ if (group === 'bindings' && verb === 'remove' && !options['provider-config']) {
659
+ if (Object.keys(input).some(key => !['expectedRevision', 'catalogId', 'providerId', 'confirm'].includes(key))) throw new CliUsageError('解除遗留绑定需要明确目录身份、供应商、修订与确认');
660
+ } else if (group === 'bindings' && verb !== 'list') {
661
+ const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
662
+ const requirement = providerNetworkRequirement(catalog), network = readNetworkConfig(file).config;
663
+ if (!requirement || requirement.configId !== network.configId) throw networkPolicyError('IRIS_NETWORK_CONFIG_MISMATCH', '绑定需要匹配的新版供应商目录与网络文件');
664
+ const allowed = verb === 'set' ? ['expectedRevision', 'providerId', 'poolId'] : ['expectedRevision', 'providerId', 'confirm'];
665
+ if (Object.keys(input).some(key => !allowed.includes(key)) || catalog.providers.filter(provider => provider.id === input.providerId).length !== 1) throw new CliUsageError('绑定输入需要该目录中明确且唯一的供应商身份');
666
+ input = { ...input, catalogId: requirement.catalogId };
667
+ }
668
+ const result = executeNetworkConfigCommand(file, collection ? group + '.' + verb : verb, input, { signal: cliAbortController.signal });
669
+ console.log(JSON.stringify(result, null, 2));
670
+ return result.valid === false ? 1 : 0;
671
+ }
672
+
352
673
  if (['config', 'providers', 'models', 'assignments'].includes(args[0])) {
353
674
  const [group, verb] = args;
354
- const verbs = { config: ['init', 'show', 'check'], providers: ['add', 'set', 'remove'], models: ['list', 'add', 'remove', 'caps', 'protocol', 'music-protocol', 'vision-input', 'discover', 'test'], assignments: ['list', 'set', 'clear'] };
675
+ const verbs = { config: ['init', 'show', 'check'], providers: ['add', 'set', 'remove'], models: ['list', 'add', 'remove', 'caps', 'protocol', 'music-protocol', 'vision-input', 'discover', 'test', 'workflow-check'], assignments: ['list', 'set', 'clear'] };
355
676
  if (!verbs[group].includes(verb)) throw new CliUsageError('未知配置命令');
356
677
  const positional = (group === 'providers' && verb === 'remove') || (group === 'models' && verb !== 'list');
357
678
  const id = positional ? required(args[2], group === 'providers' || verb === 'discover' ? 'provider-id' : 'model-ref') : undefined;
@@ -368,7 +689,7 @@ async function main(args) {
368
689
  }
369
690
  }
370
691
  const result = await executeConfigCommand(required(options['provider-config'], 'provider-config'), group === 'config' ? verb : group + '.' + verb, input, {
371
- signal: cliAbortController.signal,
692
+ signal: cliAbortController.signal, registry: providerRegistry, resolveProviderNetwork: cliResolveNetwork,
372
693
  probe: async ({ provider, modelId, capability, signal }) => {
373
694
  if (capability === 'music-gen') return { skipped: true, ok: false, reason: 'Music 不发起小样本探测;请使用 run music 提交创作要求' };
374
695
  if (capability === 'vision') return probeVisionModel(provider, modelId, { bytes: Buffer.from(RED_TEST_IMAGE.split(',')[1], 'base64'), mediaType: 'image/png' }, { signal });
@@ -381,22 +702,29 @@ async function main(args) {
381
702
  }
382
703
  return withRawRuntime('writer', options['data-root'], async runtime => {
383
704
  const mediaCapability = capability === 'image-gen' ? 'image' : capability === 'video-gen' ? 'video' : capability;
384
- if (mediaCapability === 'image') provider = providerForImageModel(provider, modelId);
385
- const adapter = createConfiguredProviderAdapter(provider, { allowRateLimited: true, modelId });
705
+ if (mediaCapability === 'image') provider = providerForImageModel(provider, modelId, providerRegistry);
706
+ const adapter = createConfiguredProviderAdapter(provider, { allowRateLimited: true, modelId, registry: providerRegistry,
707
+ network: cliResolveNetwork({ provider, providerId: provider.id }) });
386
708
  let providerInput = mediaCapability === 'image'
387
- ? { prompt: 'red circle', n: 1, ...(['openai-chat-images', 'openai-responses-images'].includes(adapter.protocol) ? {} : {
709
+ ? { prompt: 'red circle', n: 1, ...(['openai-chat-images', 'openai-responses-images', 'comfyui'].includes(adapter.protocol) ? {} : {
388
710
  size: adapter.protocol === 'openai-images' ? '256x256' : '512*512'
389
711
  }) }
390
712
  : { text: '你好', voice: probeInput.voice || provider.ttsVoice || 'Cherry' };
391
- let candidates = [{ adapter, model: qualifiedModelRef(provider.id, modelId), selectionReason: 'explicit', providerBinding: providerTaskBinding(provider) }];
713
+ let candidates = [{ adapter, model: qualifiedModelRef(provider.id, modelId), selectionReason: 'explicit', providerBinding: providerTaskBinding(provider, providerRegistry) }];
392
714
  if (['video', 'transcribe'].includes(mediaCapability)) {
393
715
  const normalized = generationInput(mediaCapability, { ...probeInput, model_ref: qualifiedModelRef(provider.id, modelId) },
394
716
  { allowVideoPaths: true, allowAudioPath: true });
395
717
  providerInput = normalized.providerInput;
396
718
  if (mediaCapability === 'video') candidates = videoTaskCandidates(candidates, normalized);
397
719
  else if (normalized.audioPath) {
398
- const prepared = await prepareProviderInput(adapter, { model: modelId, filePath: normalized.audioPath, signal });
399
- providerInput = generationInput('transcribe', { audio_url: prepared.url }).providerInput;
720
+ if (adapter.networkRequirement) candidates = candidates.map(candidate => ({ ...candidate, prepareInput: async ({ signal, taskId, attemptId, networkBudget, recordNetworkRoute }) => {
721
+ const prepared = await prepareProviderInput(adapter, { model: modelId, filePath: normalized.audioPath, signal }, { taskId, attemptId, networkBudget, recordNetworkRoute });
722
+ return { audioUrl: prepared.url };
723
+ } }));
724
+ else {
725
+ const prepared = await prepareProviderInput(adapter, { model: modelId, filePath: normalized.audioPath, signal });
726
+ providerInput = generationInput('transcribe', { audio_url: prepared.url }).providerInput;
727
+ }
400
728
  }
401
729
  }
402
730
  const submitted = await createProviderTaskRunner(runtime).submit({ capability: mediaCapability,
@@ -429,8 +757,8 @@ async function main(args) {
429
757
  } catch (_) { throw new CliUsageError('输出文件已存在或父目录不可写'); }
430
758
  }
431
759
  const catalog = await generationCatalog(options, 'vision', options['model-ref'] || input.model_ref);
432
- if (input.transcribe && !input.transcribe_model_ref) await refreshCatalogDirectories(options['provider-config'], catalog, 'transcribe', { signal: cliAbortController.signal });
433
- const execute = runtime => executeVisionCommand({ command, input, catalog, runtime,
760
+ if (input.transcribe && !input.transcribe_model_ref) await refreshCatalogDirectories(options['provider-config'], catalog, 'transcribe', { signal: cliAbortController.signal, registry: providerRegistry });
761
+ const execute = runtime => executeVisionCommand({ command, input, catalog, runtime, registry: providerRegistry,
434
762
  onRateLimit: event => recordCatalogRateLimit(options['provider-config'], event),
435
763
  onModelAvailability: event => recordCatalogAvailability(options['provider-config'], event),
436
764
  modelRef: options['model-ref'] || '', signal: cliAbortController.signal,
@@ -488,16 +816,17 @@ async function main(args) {
488
816
  const input = jsonInput(options.input);
489
817
  const resolveImageCandidates = async ({ modelRef, signal }) => {
490
818
  const catalog = await generationCatalog(options, 'image-gen', modelRef, signal);
491
- return imageCandidatesFromCatalog(catalog, modelRef).map(route => ({
819
+ return imageCandidatesFromCatalog(catalog, modelRef, providerRegistry).map(route => ({
492
820
  adapter: trackedProviderAdapter(route.provider, options), model: route.modelRef,
821
+ supportsImageEdit: supportsImageEdit(route.provider.mediaProtocol, route.model, providerRegistry),
493
822
  selectionReason: route.selectionReason,
494
- providerBinding: providerTaskBinding(route.provider)
823
+ providerBinding: providerTaskBinding(route.provider, providerRegistry)
495
824
  }));
496
825
  };
497
826
  let result;
498
827
  try {
499
828
  result = await withApplication(options['data-root'], application =>
500
- application.generateImage(input, { signal: cliAbortController.signal }), resolveImageCandidates);
829
+ application.generateImage(input, { signal: cliAbortController.signal }), { resolveImageCandidates });
501
830
  } catch (error) {
502
831
  if (error instanceof GenerationInputError) throw new CliUsageError(error.message);
503
832
  throw error;
@@ -512,12 +841,12 @@ async function main(args) {
512
841
  const normalized = generationInput('video', jsonInput(options.input), { allowVideoPaths: true });
513
842
  const { providerInput, modelRef } = normalized;
514
843
  const catalog = await generationCatalog(options, 'video-gen', modelRef);
515
- const routes = videoCandidatesFromCatalog(catalog, modelRef);
844
+ const routes = videoCandidatesFromCatalog(catalog, modelRef, providerRegistry);
516
845
  await withRawRuntime('writer', options['data-root'], async (runtime) => {
517
846
  const candidates = videoTaskCandidates(routes.map((route) => ({
518
847
  adapter: trackedProviderAdapter(route.provider, options), model: route.modelRef,
519
848
  selectionReason: route.selectionReason,
520
- providerBinding: providerTaskBinding(route.provider)
849
+ providerBinding: providerTaskBinding(route.provider, providerRegistry)
521
850
  })), normalized);
522
851
  const result = await createProviderTaskRunner(runtime).submit({
523
852
  capability: 'video', candidates,
@@ -533,12 +862,12 @@ async function main(args) {
533
862
  const options = flags(args.slice(2), ['data-root', 'provider-config', 'input']);
534
863
  const normalized = generationInput('music', jsonInput(options.input));
535
864
  const catalog = await generationCatalog(options, 'music-gen', normalized.modelRef);
536
- const routes = musicCandidatesFromCatalog(catalog, normalized.modelRef);
865
+ const routes = musicCandidatesFromCatalog(catalog, normalized.modelRef, providerRegistry);
537
866
  let result;
538
867
  await withRawRuntime('writer', options['data-root'], async runtime => {
539
868
  result = await generateMusicTask({ runner: createProviderTaskRunner(runtime),
540
869
  candidates: routes.map(route => ({ adapter: trackedProviderAdapter(route.provider, options), model: route.modelRef,
541
- selectionReason: route.selectionReason, providerBinding: providerTaskBinding(route.provider) })),
870
+ selectionReason: route.selectionReason, providerBinding: providerTaskBinding(route.provider, providerRegistry) })),
542
871
  providerInput: normalized.providerInput, options: normalized.musicOptions, signal: cliAbortController.signal });
543
872
  console.log(JSON.stringify(result, null, 2));
544
873
  });
@@ -549,12 +878,12 @@ async function main(args) {
549
878
  const options = flags(args.slice(2), ['data-root', 'provider-config', 'input']);
550
879
  const { providerInput, modelRef } = generationInput('tts', jsonInput(options.input));
551
880
  const catalog = await generationCatalog(options, 'tts', modelRef);
552
- const routes = ttsCandidatesFromCatalog(catalog, modelRef);
881
+ const routes = ttsCandidatesFromCatalog(catalog, modelRef, providerRegistry);
553
882
  await withRawRuntime('writer', options['data-root'], async (runtime) => {
554
883
  const candidates = routes.map((route) => ({
555
884
  adapter: trackedProviderAdapter(route.provider, options), model: route.modelRef,
556
885
  selectionReason: route.selectionReason,
557
- providerBinding: providerTaskBinding(route.provider)
886
+ providerBinding: providerTaskBinding(route.provider, providerRegistry)
558
887
  }));
559
888
  const result = await createProviderTaskRunner(runtime).submit({
560
889
  capability: 'tts', candidates,
@@ -566,29 +895,33 @@ async function main(args) {
566
895
  }
567
896
 
568
897
  // 转写输入冻结(E3 Profile):上传型异步——audio_url(公网/oss://)或
569
- // audio_path(本地文件,经首选候选 Provider 的临时存储上传,签名 URL 不落 Core)。
898
+ // audio_path(本地文件,在写者与原 Attempt 建立后按实际候选上传,签名 URL 不落 Core)。
570
899
  if (args[0] === 'run' && args[1] === 'transcribe') {
571
900
  const options = flags(args.slice(2), ['data-root', 'provider-config', 'input']);
572
901
  const normalized = generationInput('transcribe', jsonInput(options.input), { allowAudioPath: true });
573
902
  const catalog = await generationCatalog(options, 'transcribe', normalized.modelRef);
574
- const routes = transcribeCandidatesFromCatalog(catalog, normalized.modelRef);
575
- let providerInput = normalized.providerInput;
576
- if (normalized.audioPath) {
577
- if (!fs.existsSync(normalized.audioPath)) throw new CliUsageError('音频文件不存在');
578
- const prepared = await prepareProviderInput(trackedProviderAdapter(routes[0].provider, options), {
579
- model: routes[0].model, filePath: normalized.audioPath
580
- });
581
- providerInput = generationInput('transcribe', { audio_url: prepared.url }).providerInput;
582
- }
903
+ const routes = transcribeCandidatesFromCatalog(catalog, normalized.modelRef, providerRegistry);
904
+ if (normalized.audioPath && !fs.existsSync(normalized.audioPath)) throw new CliUsageError('音频文件不存在');
583
905
  await withRawRuntime('writer', options['data-root'], async (runtime) => {
584
- const candidates = routes.map((route) => ({
585
- adapter: trackedProviderAdapter(route.provider, options), model: route.modelRef,
906
+ const candidates = routes.map(route => {
907
+ const adapter = trackedProviderAdapter(route.provider, options);
908
+ return { adapter, model: route.modelRef,
586
909
  selectionReason: route.selectionReason,
587
- providerBinding: providerTaskBinding(route.provider)
588
- }));
910
+ providerBinding: providerTaskBinding(route.provider, providerRegistry),
911
+ ...(normalized.audioPath ? { prepareInput: async ({ signal, taskId, attemptId, networkBudget, recordNetworkRoute, modelRef, providerId }) => {
912
+ const prepared = await prepareProviderInput(adapter, { model: route.model, filePath: normalized.audioPath, signal },
913
+ { taskId, attemptId, networkBudget, recordNetworkRoute, modelRef, providerId });
914
+ return { audioUrl: prepared.url };
915
+ } } : {}) };
916
+ });
917
+ if (normalized.audioPath && candidates.every(candidate => typeof candidate.adapter.prepareInput !== 'function')) {
918
+ throw Object.assign(new Error('所选协议未实现临时音频上传;请使用支持上传的转写协议或重新提供 audio_url'), {
919
+ code: 'IRIS_PROVIDER_INPUT_PREPARATION_UNSUPPORTED'
920
+ });
921
+ }
589
922
  const result = await createProviderTaskRunner(runtime).submit({
590
923
  capability: 'transcribe', candidates,
591
- providerInput
924
+ providerInput: normalized.providerInput
592
925
  });
593
926
  console.log(JSON.stringify(result, null, 2));
594
927
  });
@@ -602,7 +935,7 @@ async function main(args) {
602
935
  const inspected = await withRuntime('reader', options['data-root'], commands => commands.execute('task.inspect', { task_id: args[2] }));
603
936
  const catalog = coreTaskWaitFinished(inspected.task) ? null : loadProviderCatalog(required(options['provider-config'], 'provider-config'));
604
937
  const result = await withRuntime(catalog ? 'writer' : 'reader', options['data-root'], commands => commands.execute('task.wait', input), {
605
- ...(catalog ? { resolveTaskAdapter: task => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task), options, task) } : {}) });
938
+ ...(catalog ? { resolveTaskAdapter: task => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task, providerRegistry), options, task) } : {}) });
606
939
  console.log(JSON.stringify(result, null, 2));
607
940
  return result.timedOut ? 3 : result.ready ? 0 : 1;
608
941
  }
@@ -612,7 +945,7 @@ async function main(args) {
612
945
  const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
613
946
  await withRuntime('reader', options['data-root'], (commands) => commands.execute('task.inspect', { task_id: args[2] }));
614
947
  const result = await withRuntime('writer', options['data-root'], (commands) => commands.execute('task.observe', { task_id: args[2] }), {
615
- resolveTaskAdapter: (task) => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task), options, task)
948
+ resolveTaskAdapter: (task) => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task, providerRegistry), options, task)
616
949
  });
617
950
  console.log(JSON.stringify(result, null, 2));
618
951
  return 0;
@@ -623,7 +956,7 @@ async function main(args) {
623
956
  const catalog = options['provider-config'] ? loadProviderCatalog(options['provider-config']) : null;
624
957
  await withRuntime('reader', options['data-root'], (commands) => commands.execute('task.inspect', { task_id: args[2] }));
625
958
  const result = await withRuntime('writer', options['data-root'], (commands) => commands.execute('task.redeliver', { task_id: args[2] }), {
626
- ...(catalog ? { resolveTaskAdapter: (task) => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task), options, task) } : {})
959
+ ...(catalog ? { resolveTaskAdapter: (task) => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task, providerRegistry), options, task) } : {})
627
960
  });
628
961
  console.log(JSON.stringify(result, null, 2));
629
962
  return 0;
@@ -634,7 +967,7 @@ async function main(args) {
634
967
  const catalog = loadProviderCatalog(required(options['provider-config'], 'provider-config'));
635
968
  await withRuntime('reader', options['data-root'], (commands) => commands.execute('task.inspect', { task_id: args[2] }));
636
969
  const result = await withRuntime('writer', options['data-root'], (commands) => commands.execute('task.cancel', { task_id: args[2] }), {
637
- resolveTaskAdapter: (task) => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task), options, task)
970
+ resolveTaskAdapter: (task) => trackedProviderAdapter(providerForTaskFromCatalog(catalog, task, providerRegistry), options, task)
638
971
  });
639
972
  console.log(JSON.stringify(result, null, 2));
640
973
  return 0;
@@ -658,19 +991,19 @@ async function main(args) {
658
991
  resolveTaskCandidates: async ({ capability, modelRef }) => {
659
992
  const catalog = await generationCatalog(options, { image: 'image-gen', video: 'video-gen', music: 'music-gen' }[capability] || capability, modelRef);
660
993
  return (capability === 'video'
661
- ? videoCandidatesFromCatalog(catalog, modelRef || undefined)
994
+ ? videoCandidatesFromCatalog(catalog, modelRef || undefined, providerRegistry)
662
995
  : capability === 'tts'
663
- ? ttsCandidatesFromCatalog(catalog, modelRef || undefined)
996
+ ? ttsCandidatesFromCatalog(catalog, modelRef || undefined, providerRegistry)
664
997
  : capability === 'music'
665
- ? musicCandidatesFromCatalog(catalog, modelRef || undefined)
998
+ ? musicCandidatesFromCatalog(catalog, modelRef || undefined, providerRegistry)
666
999
  : capability === 'transcribe'
667
- ? transcribeCandidatesFromCatalog(catalog, modelRef || undefined)
668
- : imageCandidatesFromCatalog(catalog, modelRef || undefined)
1000
+ ? transcribeCandidatesFromCatalog(catalog, modelRef || undefined, providerRegistry)
1001
+ : imageCandidatesFromCatalog(catalog, modelRef || undefined, providerRegistry)
669
1002
  ).map((route) => ({
670
1003
  adapter: trackedProviderAdapter(route.provider, options),
671
1004
  model: route.modelRef,
672
1005
  selectionReason: route.selectionReason,
673
- providerBinding: providerTaskBinding(route.provider)
1006
+ providerBinding: providerTaskBinding(route.provider, providerRegistry)
674
1007
  }));
675
1008
  }
676
1009
  });
@@ -753,11 +1086,38 @@ async function main(args) {
753
1086
  }
754
1087
 
755
1088
  try {
756
- process.exitCode = await main(process.argv.slice(2));
1089
+ const args = [], entries = [], raw = process.argv.slice(2);
1090
+ for (let index = 0; index < raw.length; index++) {
1091
+ if (raw[index] !== '--provider-extension') args.push(raw[index]);
1092
+ else {
1093
+ const entry = raw[++index];
1094
+ if (!entry || entry.startsWith('--')) throw new CliUsageError('--provider-extension 缺少入口路径');
1095
+ entries.push(entry);
1096
+ }
1097
+ }
1098
+ if (args[0] !== 'network') for (let index = 0; index < args.length; index++) {
1099
+ if (args[index] !== '--network-config') continue;
1100
+ if (cliNetworkConfigPath !== undefined || !args[index + 1] || !path.isAbsolute(args[index + 1])) throw new CliUsageError('network-config 需要唯一、明确的绝对路径');
1101
+ cliNetworkConfigPath = args[index + 1]; args.splice(index, 2); index--;
1102
+ }
1103
+ let schedulerFile;
1104
+ for (let index = 0; index < args.length; index++) {
1105
+ if (args[index] !== '--scheduler-config') continue;
1106
+ if (!['api', 'web'].includes(args[0]) || args[1] !== 'serve') throw new CliUsageError('scheduler-config 首片仅用于 api/web serve;单次 CLI 和 DSH 独立路径保持原行为');
1107
+ if (schedulerFile !== undefined || !args[index + 1]) throw new CliUsageError('scheduler-config 需要唯一、明确的绝对路径');
1108
+ schedulerFile = args[index + 1]; args.splice(index, 2); index--;
1109
+ }
1110
+ if (schedulerFile !== undefined) cliScheduler = createMediaScheduler(readSchedulingPolicy(schedulerFile));
1111
+ try {
1112
+ if (entries.length && args.includes('--plugin-config')) throw new CliUsageError('--plugin-config 与 --provider-extension 不能混用;Provider 插件应通过同一插件装配实例加载');
1113
+ if (args.length && !args.includes('--help') && !args.includes('-h')) cliProviderRegistry = await loadProviderRegistry(entries);
1114
+ process.exitCode = await main(args, cliProviderRegistry);
1115
+ } finally { try { await cliProviderRegistry?.dispose(); } finally { try { await cliNetworkManager?.dispose(); } finally { cliScheduler?.dispose(); } } }
757
1116
  } catch (error) {
758
- const code = error && error.code || 'IRIS_CLI_FAILED';
1117
+ const code = error && error.code || (error?.providerCode === 'IRIS_PROVIDER_DIRECTORY_CREDENTIALS' ? error.providerCode : 'IRIS_CLI_FAILED');
759
1118
  const message = error && error.message || '命令执行失败';
760
- console.error(code + ': ' + message);
1119
+ if (error.diagnostic) console.error(JSON.stringify(error.diagnostic));
1120
+ else console.error(code + ': ' + message);
761
1121
  if (error instanceof CliUsageError) console.error('\n' + usage());
762
1122
  process.exitCode = error instanceof CliUsageError ? 2 : 1;
763
1123
  }