@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/bin/dsh-iris.js CHANGED
@@ -18,28 +18,46 @@ import { executeVisionCommand, normalizeVisionInput } from '../lib/headless-visi
18
18
  import { videoTaskCandidates } from '../lib/video-input.js';
19
19
  import { createChromiumBrowser } from '../lib/chromium-browser.js';
20
20
  import { executeConfigCommand, recordCatalogRateLimit, recordCatalogAvailability, refreshCatalogDirectories } from '../lib/provider-config-service.js';
21
+ import { executeNetworkConfigCommand } from '../lib/network-config-service.js';
21
22
  import { probeVisionModel } from '../lib/vision-model-routing.js';
22
23
  import { RED_TEST_IMAGE } from '../lib/vision.js';
23
24
  import { parseModelRef, modelRef as qualifiedModelRef } from '../lib/models.js';
24
25
  import { coreTaskWaitFinished } from '../lib/task-wait.js';
25
26
  import { createLocalReadApi, writeLocalApiTokenFile } from '../lib/local-api.js';
26
27
  import { manageCoreSkill } from '../lib/skill-installation.js';
27
- import { createTrackedProviderAdapter } from '../lib/configured-image-runtime.js';
28
+ import { createTrackedProviderAdapter, configuredImagePorts } from '../lib/configured-image-runtime.js';
28
29
  import { inspectExecutionLease, recoverExecutionLease } from '../lib/operation-runner.js';
29
30
  import { createConnectionPair } from '../lib/core-connections.js';
30
31
  import { builtinLaunchPlugins, loadPluginLaunch } from '../lib/plugin-launch.js';
31
32
  import { checkLocalApiTokenDirectory, writeLocalApiTokenDirectory } from '../lib/local-api-token-directory.js';
32
33
  import { resolveWorkbenchAuthDirectory } from '../lib/workbench-auth.js';
33
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
+ }
34
51
 
35
52
  function trackedProviderAdapter(provider, options, task) {
36
- return createTrackedProviderAdapter(provider, options['provider-config'], task, options.providerRegistry);
53
+ return createTrackedProviderAdapter(provider, options['provider-config'], task, options.providerRegistry, {
54
+ network: cliResolveNetwork({ provider, providerId: provider.id, taskId: task?.id }) });
37
55
  }
38
56
 
39
57
  async function generationCatalog(options, capability, modelRef, signal = cliAbortController.signal) {
40
58
  const file = required(options['provider-config'], 'provider-config');
41
59
  const catalog = loadProviderCatalog(file);
42
- if (!modelRef) await refreshCatalogDirectories(file, catalog, capability, { signal, registry: options.providerRegistry });
60
+ if (!modelRef) await refreshCatalogDirectories(file, catalog, capability, { signal, registry: options.providerRegistry, resolveProviderNetwork: cliResolveNetwork });
43
61
  return catalog;
44
62
  }
45
63
 
@@ -68,13 +86,13 @@ function usage() {
68
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.',
69
87
  ' dsh-iris doctor [--data-root <absolute-path>] [--json]',
70
88
  ' dsh-iris api init --data-root <empty-existing-flow-root>',
71
- ' dsh-iris execution init --data-root <empty-existing-flow-root> [--image-generation|--audio]',
72
- ' Initialize a NEW v1 request container, v2 with --image-generation, or v3 with --audio (images + speech/transcription). Existing roots are never migrated.',
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.',
73
91
  ' dsh-iris execution inspect --data-root <v1-flow-root>',
74
92
  ' dsh-iris execution recover-lease --data-root <v1-flow-root> --confirm-stale-pid <pid>',
75
- ' 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|none>] [--tools <comma-separated-names|none>]',
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>]',
76
94
  ' dsh-iris api serve ... --allow-execution --provider-config <absolute-path> [--connections-file <private-grants-file>]',
77
- ' dsh-iris mcp serve --connection-file <private-client-json> [--scopes <read,create,execute,stop>] [--capabilities <image-edit,image-generate,tts,transcribe|none>] [--tools <comma-separated-names|none>]',
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>]',
78
96
  ' Official SDK stdio bridge to an already running Core service. Read-only by default; stdout contains MCP messages only.',
79
97
  ' dsh-iris mcp capabilities',
80
98
  ' dsh-iris mcp presets',
@@ -84,6 +102,8 @@ function usage() {
84
102
  ' dsh-iris api serve ... --connections-file <private-grants-file> --allow-mcp',
85
103
  ' dsh-iris web serve ... --connection-store <private-directory-outside-core> --allow-connection-management [--allow-mcp] [--allow-execution --provider-config <absolute-path>]',
86
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).',
87
107
  ' Explicit standard Streamable HTTP endpoint at /mcp; default remains disabled. Uses the existing Core service and user-selected connection tools.',
88
108
  ' dsh-iris operation list --data-root <execution-root> [--run-id <run-id>] [--limit <1..30>] [--cursor <operation-id>]',
89
109
  ' dsh-iris operation inspect <operation-id> --data-root <execution-root>',
@@ -103,6 +123,14 @@ function usage() {
103
123
  ' dsh-iris providers list --provider-config <absolute-path>',
104
124
  ' dsh-iris capabilities list --provider-config <absolute-path>',
105
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.',
106
134
  ' dsh-iris providers <add|set> --provider-config <absolute-path> --input <provider-json>',
107
135
  ' dsh-iris providers remove <provider-id> --provider-config <absolute-path>',
108
136
  ' dsh-iris models list --provider-config <absolute-path>',
@@ -150,7 +178,7 @@ function usage() {
150
178
  ' dsh-iris core transactions --data-root <absolute-path>',
151
179
  ' dsh-iris core restore <transaction-id> --data-root <absolute-path>',
152
180
  '',
153
- '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.'
154
182
  ].join('\n');
155
183
  }
156
184
 
@@ -223,7 +251,8 @@ function registerRuntime(runtime) {
223
251
  async function disposeActiveRuntimes() {
224
252
  await Promise.allSettled([...activeRuntimes].map((runtime) => runtime.dispose().catch(() => {})));
225
253
  activeRuntimes.clear();
226
- await cliProviderRegistry?.dispose();
254
+ try { await cliProviderRegistry?.dispose(); }
255
+ finally { try { await cliNetworkManager?.dispose(); } finally { cliScheduler?.dispose(); } }
227
256
  }
228
257
 
229
258
  for (const signal of ['SIGINT', 'SIGTERM']) {
@@ -253,8 +282,8 @@ async function withRuntime(mode, dataRoot, callback, ports = {}) {
253
282
  return withRawRuntime(mode, dataRoot, (runtime) => callback(createCommandService(runtime, ports)));
254
283
  }
255
284
 
256
- async function withApplication(dataRoot, callback, resolveImageCandidates, resolveTaskAdapter) {
257
- 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 });
258
287
  const unregister = registerRuntime(application);
259
288
  try { return await callback(application); }
260
289
  finally { try { await application.dispose(); } finally { unregister(); } }
@@ -392,16 +421,16 @@ async function main(args, providerRegistry) {
392
421
  console.log(JSON.stringify(result, null, 2)); return 0;
393
422
  }
394
423
  if (args[1] !== 'init') throw new CliUsageError('不支持的 execution 子命令');
395
- const initArgs = args.slice(2), generationFlags = initArgs.filter(value => value === '--image-generation'), audioFlags = initArgs.filter(value => value === '--audio');
396
- if (generationFlags.length > 1 || audioFlags.length > 1 || generationFlags.length && audioFlags.length) throw new CliUsageError('初始化开关重复或冲突;audio已包含文生图');
397
- const options = flags(initArgs.filter(value => !['--image-generation', '--audio'].includes(value)), ['data-root']);
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']);
398
427
  const suppliedRoot = required(options['data-root'], 'data-root');
399
428
  if (!path.isAbsolute(suppliedRoot)) throw new CliUsageError('data-root 必须是已初始化的空 Flow 根绝对路径');
400
429
  let dataRoot;
401
430
  try { dataRoot = fs.realpathSync(suppliedRoot); }
402
431
  catch (_) { throw new CliUsageError('请先在新的专用数据根执行 flow init'); }
403
432
  const result = await withApplication(dataRoot, application =>
404
- application.execute('execution.init', audioFlags.length ? { audio: true } : generationFlags.length ? { imageGeneration: true } : {}, { signal: cliAbortController.signal }));
433
+ application.execute('execution.init', videoFlags.length ? { video: true } : audioFlags.length ? { audio: true } : generationFlags.length ? { imageGeneration: true } : {}, { signal: cliAbortController.signal }));
405
434
  console.log(JSON.stringify(result, null, 2)); return 0;
406
435
  }
407
436
  if (!['list', 'inspect'].includes(args[1])) throw new CliUsageError('operation 只支持只读 list/inspect');
@@ -471,12 +500,12 @@ async function main(args, providerRegistry) {
471
500
  return 0;
472
501
  }
473
502
  if (args[1] !== 'serve') throw new CliUsageError(`不支持的 ${args[0]} 子命令`);
474
- 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');
475
- if (localWriteFlags.length > 1 || executionFlags.length > 1 || selectionFlags.length > 1 || mcpFlags.length > 1 || managementFlags.length > 1 || providerFlags.length > 1) throw new CliUsageError('写入/执行/选择/MCP/管理参数重复');
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/管理参数重复');
476
505
  const allowExecution = executionFlags.length === 1, allowLocalWrites = localWriteFlags.length === 1 || allowExecution;
477
506
  const allowSelection = selectionFlags.length === 1;
478
507
  const allowConnectionManagement = managementFlags.length === 1;
479
- const options = flags(rest.filter(value => !['--allow-local-writes', '--allow-execution', '--allow-selection', '--allow-mcp', '--allow-connection-management', '--allow-provider-management'].includes(value)), ['data-root', 'token-file', 'token-dir', 'auth-store', 'provider-config', 'port', 'connections-file', 'connection-store', 'panel', 'plugin-config']);
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']);
480
509
  const suppliedRoot = required(options['data-root'], 'data-root');
481
510
  const passwordMode = serveWorkbench && options['token-file'] === undefined && options['token-dir'] === undefined;
482
511
  if (!passwordMode && Boolean(options['token-file']) === Boolean(options['token-dir'])) throw new CliUsageError('请选择且只选择 --token-file 或 --token-dir');
@@ -492,11 +521,13 @@ async function main(args, providerRegistry) {
492
521
  if (options['token-dir']) checkLocalApiTokenDirectory(options['token-dir'], dataRoot);
493
522
  return withCliLaunch(options['plugin-config'], { serveWorkbench, panelManifestPath: options.panel }, async (launch, ownApi) => {
494
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 } : {}),
495
526
  ...(passwordMode ? { workbenchAuth: { directory: authDirectory } } : {}),
496
527
  allowExecution, allowSelection, allowMcp: mcpFlags.length === 1, connectionsFile: options['connections-file'], allowConnectionManagement, connectionStore: options['connection-store'], providerRegistry,
497
528
  ...(launch ? { pluginRegistry: launch.registry, providerRegistry: launch.registry.providers, allowedOperations: launch.allowedOperations,
498
529
  panelContributions: launch.panelContributions } : {}),
499
- allowProviderManagement: providerFlags.length === 1, panelManifestPath: options.panel });
530
+ allowProviderManagement: providerFlags.length === 1, allowNetworkManagement: networkFlags.length === 1, panelManifestPath: options.panel });
500
531
  ownApi(api);
501
532
  const started = await api.start({ port: options.port === undefined ? 0 : Number(options.port) });
502
533
  if (!passwordMode) {
@@ -509,6 +540,7 @@ async function main(args, providerRegistry) {
509
540
  ...(serveWorkbench ? { loginUrl: started.loginUrl, loginExpiresAt: started.loginExpiresAt } : {}),
510
541
  ...(allowConnectionManagement ? { connectionManagement: true } : {}),
511
542
  ...(providerFlags.length ? { providerManagement: true } : {}),
543
+ ...(networkFlags.length ? { networkManagement: true } : {}),
512
544
  ...(started.mcpEndpoint ? { mcpEndpoint: started.mcpEndpoint } : {}),
513
545
  message: passwordMode ? (started.authentication.configured ? '工作台已启动,打开 origin 用访问口令登录;记住设备后可跨正常重启继续使用。'
514
546
  : '首次使用请打开 loginUrl 设置访问口令;之后打开普通地址即可登录,无需 Token 文件。')
@@ -539,20 +571,12 @@ async function main(args, providerRegistry) {
539
571
  if (verb === 'select' && Object.keys(input).some(key => key !== 'artifact_id')) throw new CliUsageError('flow select 只接受 artifact_id');
540
572
  if (needsId) input.run_id = args[2];
541
573
  if (verb === 'export') input.output_path = required(options.output, 'output');
542
- const resolveImageCandidates = options['provider-config'] ? async ({ modelRef, signal }) => {
543
- const catalog = await generationCatalog(options, 'image-gen', modelRef, signal);
544
- return imageCandidatesFromCatalog(catalog, modelRef, providerRegistry).map(route => ({ adapter: trackedProviderAdapter(route.provider, options),
545
- model: route.modelRef, supportsImageEdit: supportsImageEdit(route.provider.mediaProtocol, route.model, providerRegistry),
546
- selectionReason: route.selectionReason, providerBinding: providerTaskBinding(route.provider, providerRegistry) }));
547
- } : undefined;
548
- const resolveTaskAdapter = options['provider-config'] ? task => {
549
- const catalog = loadProviderCatalog(options['provider-config']);
550
- return trackedProviderAdapter(providerForTaskFromCatalog(catalog, task, providerRegistry), options, task);
551
- } : undefined;
574
+ const ports = options['provider-config'] ? configuredImagePorts(options['provider-config'], providerRegistry,
575
+ { resolveProviderNetwork: cliResolveNetwork }) : {};
552
576
  let result;
553
577
  try {
554
578
  result = await withApplication(options['data-root'], application =>
555
- application.execute('flow.' + verb, input, { signal: cliAbortController.signal }), resolveImageCandidates, resolveTaskAdapter);
579
+ application.execute('flow.' + verb, input, { signal: cliAbortController.signal }), ports);
556
580
  } catch (error) {
557
581
  if (error instanceof GenerationInputError) throw new CliUsageError(error.message);
558
582
  throw error;
@@ -602,6 +626,50 @@ async function main(args, providerRegistry) {
602
626
  return 0;
603
627
  }
604
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
+
605
673
  if (['config', 'providers', 'models', 'assignments'].includes(args[0])) {
606
674
  const [group, verb] = args;
607
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'] };
@@ -621,7 +689,7 @@ async function main(args, providerRegistry) {
621
689
  }
622
690
  }
623
691
  const result = await executeConfigCommand(required(options['provider-config'], 'provider-config'), group === 'config' ? verb : group + '.' + verb, input, {
624
- signal: cliAbortController.signal, registry: providerRegistry,
692
+ signal: cliAbortController.signal, registry: providerRegistry, resolveProviderNetwork: cliResolveNetwork,
625
693
  probe: async ({ provider, modelId, capability, signal }) => {
626
694
  if (capability === 'music-gen') return { skipped: true, ok: false, reason: 'Music 不发起小样本探测;请使用 run music 提交创作要求' };
627
695
  if (capability === 'vision') return probeVisionModel(provider, modelId, { bytes: Buffer.from(RED_TEST_IMAGE.split(',')[1], 'base64'), mediaType: 'image/png' }, { signal });
@@ -635,7 +703,8 @@ async function main(args, providerRegistry) {
635
703
  return withRawRuntime('writer', options['data-root'], async runtime => {
636
704
  const mediaCapability = capability === 'image-gen' ? 'image' : capability === 'video-gen' ? 'video' : capability;
637
705
  if (mediaCapability === 'image') provider = providerForImageModel(provider, modelId, providerRegistry);
638
- const adapter = createConfiguredProviderAdapter(provider, { allowRateLimited: true, modelId, registry: providerRegistry });
706
+ const adapter = createConfiguredProviderAdapter(provider, { allowRateLimited: true, modelId, registry: providerRegistry,
707
+ network: cliResolveNetwork({ provider, providerId: provider.id }) });
639
708
  let providerInput = mediaCapability === 'image'
640
709
  ? { prompt: 'red circle', n: 1, ...(['openai-chat-images', 'openai-responses-images', 'comfyui'].includes(adapter.protocol) ? {} : {
641
710
  size: adapter.protocol === 'openai-images' ? '256x256' : '512*512'
@@ -648,8 +717,14 @@ async function main(args, providerRegistry) {
648
717
  providerInput = normalized.providerInput;
649
718
  if (mediaCapability === 'video') candidates = videoTaskCandidates(candidates, normalized);
650
719
  else if (normalized.audioPath) {
651
- const prepared = await prepareProviderInput(adapter, { model: modelId, filePath: normalized.audioPath, signal });
652
- 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
+ }
653
728
  }
654
729
  }
655
730
  const submitted = await createProviderTaskRunner(runtime).submit({ capability: mediaCapability,
@@ -751,7 +826,7 @@ async function main(args, providerRegistry) {
751
826
  let result;
752
827
  try {
753
828
  result = await withApplication(options['data-root'], application =>
754
- application.generateImage(input, { signal: cliAbortController.signal }), resolveImageCandidates);
829
+ application.generateImage(input, { signal: cliAbortController.signal }), { resolveImageCandidates });
755
830
  } catch (error) {
756
831
  if (error instanceof GenerationInputError) throw new CliUsageError(error.message);
757
832
  throw error;
@@ -820,29 +895,33 @@ async function main(args, providerRegistry) {
820
895
  }
821
896
 
822
897
  // 转写输入冻结(E3 Profile):上传型异步——audio_url(公网/oss://)或
823
- // audio_path(本地文件,经首选候选 Provider 的临时存储上传,签名 URL 不落 Core)。
898
+ // audio_path(本地文件,在写者与原 Attempt 建立后按实际候选上传,签名 URL 不落 Core)。
824
899
  if (args[0] === 'run' && args[1] === 'transcribe') {
825
900
  const options = flags(args.slice(2), ['data-root', 'provider-config', 'input']);
826
901
  const normalized = generationInput('transcribe', jsonInput(options.input), { allowAudioPath: true });
827
902
  const catalog = await generationCatalog(options, 'transcribe', normalized.modelRef);
828
903
  const routes = transcribeCandidatesFromCatalog(catalog, normalized.modelRef, providerRegistry);
829
- let providerInput = normalized.providerInput;
830
- if (normalized.audioPath) {
831
- if (!fs.existsSync(normalized.audioPath)) throw new CliUsageError('音频文件不存在');
832
- const prepared = await prepareProviderInput(trackedProviderAdapter(routes[0].provider, options), {
833
- model: routes[0].model, filePath: normalized.audioPath
834
- });
835
- providerInput = generationInput('transcribe', { audio_url: prepared.url }).providerInput;
836
- }
904
+ if (normalized.audioPath && !fs.existsSync(normalized.audioPath)) throw new CliUsageError('音频文件不存在');
837
905
  await withRawRuntime('writer', options['data-root'], async (runtime) => {
838
- const candidates = routes.map((route) => ({
839
- 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,
840
909
  selectionReason: route.selectionReason,
841
- providerBinding: providerTaskBinding(route.provider, providerRegistry)
842
- }));
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
+ }
843
922
  const result = await createProviderTaskRunner(runtime).submit({
844
923
  capability: 'transcribe', candidates,
845
- providerInput
924
+ providerInput: normalized.providerInput
846
925
  });
847
926
  console.log(JSON.stringify(result, null, 2));
848
927
  });
@@ -1016,13 +1095,26 @@ try {
1016
1095
  entries.push(entry);
1017
1096
  }
1018
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));
1019
1111
  try {
1020
1112
  if (entries.length && args.includes('--plugin-config')) throw new CliUsageError('--plugin-config 与 --provider-extension 不能混用;Provider 插件应通过同一插件装配实例加载');
1021
1113
  if (args.length && !args.includes('--help') && !args.includes('-h')) cliProviderRegistry = await loadProviderRegistry(entries);
1022
1114
  process.exitCode = await main(args, cliProviderRegistry);
1023
- } finally { await cliProviderRegistry?.dispose(); }
1115
+ } finally { try { await cliProviderRegistry?.dispose(); } finally { try { await cliNetworkManager?.dispose(); } finally { cliScheduler?.dispose(); } } }
1024
1116
  } catch (error) {
1025
- 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');
1026
1118
  const message = error && error.message || '命令执行失败';
1027
1119
  if (error.diagnostic) console.error(JSON.stringify(error.diagnostic));
1028
1120
  else console.error(code + ': ' + message);
@@ -1,13 +1,13 @@
1
1
  # Agent Quickstart · Core 与 MCP 接入指导
2
2
 
3
- For **0.3.0-alpha.1 Core Web/MCP**, use the [Core quickstart](CORE_QUICKSTART.md). Stable npm **0.2.7 has CLI/DSH support, without this Web/MCP entry**; use [the stable Agent guide](AGENT_QUICKSTART.md) for it. Install the exact alpha or explicitly choose the alpha channel.
3
+ For **0.3.0-beta.1 Core Web/MCP**, use the [Core quickstart](CORE_QUICKSTART.md). Stable npm **0.2.7 has CLI/DSH support, without this Web/MCP entry**; use [the stable Agent guide](AGENT_QUICKSTART.md) for it. Install the exact beta or explicitly choose the beta channel.
4
4
 
5
5
  Give this instruction to an Agent helping with setup:
6
6
 
7
7
  ```text
8
8
  Help me use Iris (@mokuyoaxis/dsh-iris). First read README and check the actual
9
9
  installed package or source revision, Node version and available CLI commands.
10
- Distinguish stable 0.2.7 from the 0.3.0-alpha.1 Core workbench/MCP implementation.
10
+ Distinguish stable 0.2.7 from the 0.3.0-beta.1 Core workbench/MCP implementation.
11
11
  Installing latest does not opt into alpha; use the exact prerelease or alpha tag.
12
12
  Do not install a different package, upgrade the host or migrate a data root silently.
13
13
 
@@ -1,6 +1,6 @@
1
1
  # 给 AI Agent 的简要 Prompt
2
2
 
3
- 先选择入口:0.3.0-alpha.1 的 Core 工作台/MCP 使用 [Agent Core 指导](AGENT_CORE_QUICKSTART.md)与[从零启动](CORE_QUICKSTART.md);下面的安装提示保留面向**稳定 0.2.7 的 DSH / CLI**,不是新 MCP 配置步骤。
3
+ 先选择入口:0.3.0-beta.1 的 Core 工作台/MCP 使用 [Agent Core 指导](AGENT_CORE_QUICKSTART.md)与[从零启动](CORE_QUICKSTART.md);下面的安装提示保留面向**稳定 0.2.7 的 DSH / CLI**,不是新 MCP 配置步骤。
4
4
 
5
5
  把下面一段交给负责安装或使用 Iris 的 Agent。版本与发布状态以当前 [README](../README.md) 为准;Iris 和 DSH 的版本号独立。
6
6
 
@@ -1,12 +1,12 @@
1
1
  # Iris 架构与边界
2
2
 
3
- 状态:本文说明 **Iris 0.3.0-alpha.1** 的共享Core及Web、MCP与插件装配,并保留0.2.7入口图作历史对照。0.2.0建立实例化Core Runtime、Headless CLI及共享Task/Artifact事实;后续加入共享视觉、图片编辑、音乐和交付恢复。0.2.5恢复旧宿主支持,0.2.6增加连接引用、可选功能自动池和目录同步。准确版本及历史范围见 [兼容矩阵](DSH_COMPATIBILITY.md)。Node.js最低22.0.0;Core与插件契约仍为实验性。
3
+ 状态:本文说明 **Iris 0.3.0-beta.1** 的共享Core及Web、MCP与插件装配,并保留0.2.7入口图作历史对照。0.2.0建立实例化Core Runtime、Headless CLI及共享Task/Artifact事实;后续加入共享视觉、图片编辑、音乐和交付恢复。0.2.5恢复旧宿主支持,0.2.6增加连接引用、可选功能自动池和目录同步。准确版本及历史范围见 [兼容矩阵](DSH_COMPATIBILITY.md)。Node.js最低22.1.0;Core与插件契约仍为实验性。
4
4
 
5
5
  ## 当前形态
6
6
 
7
7
  当前源码的 [Core 工作台](LOCAL_WEB.md)已连接真实持久执行、停止、作品选择和供应商管理;[通用 MCP](MCP.md)及[显式插件贡献](PLUGINS.md)复用同一服务与授权边界。图片生成/编辑、语音/转写和插件业务按用户所选范围启用。下方示意图表示 0.2.7 已发布入口,完整当前覆盖见 [README](../README.md#当前入口覆盖)。
8
8
 
9
- 0.3 alpha源码另提供独立 [Panel/主题](PANEL_EXTENSIONS.md)和 [C3 供应商设置](PROVIDER_SETTINGS.md):页面可显式替换,配置管理复用应用层原子写入;默认媒体权限与 Core Task/Artifact 格式保持。它们不随已发布 0.2.7 提供,也不自动替换 DSH 内嵌 UI。
9
+ 0.3 beta另提供独立 [Panel/主题](PANEL_EXTENSIONS.md)和 [C3 供应商设置](PROVIDER_SETTINGS.md):页面可显式替换,配置管理复用应用层原子写入;默认媒体权限与 Core Task/Artifact 格式保持。它们不随已发布 0.2.7 提供,也不自动替换 DSH 内嵌 UI。
10
10
 
11
11
  Iris仍以一个npm包交付。DSH插件负责原生Agent工具、附件、浏览器和内嵌UI;Headless CLI与独立Core服务(Web/MCP)在没有DSH/Cordis的进程中接收明确数据根和私有Provider配置。各入口共用Core Runtime、Provider Task Runner、Task/Artifact Store和本地Command,不能在同一根同时充当写者;MCP桥连接已有Core服务,不另建业务写者。
12
12
 
@@ -39,7 +39,7 @@ DSH Host / Cordis Headless CLI
39
39
 
40
40
  `lib/index.js` 是 DSH 插件装配入口,`lib/dsh-host-adapter.js` 是原始 DSH/Cordis 对象进入 Iris 的服务端边界;`lib/dsh-core-adapter.js` 负责 DSH 的 Core 实例装配、Provider 连接解析和 Host DTO 投影。`bin/dsh-iris.js` 不启动 DSH,也不推断 profile。`lib/api.js` 与 `lib/client.js` 是 Host 投影,不是真相来源。
41
41
 
42
- 0.2.7 增加 `lib/application-runtime.js`,首批 CLI 图片命令和固定改图流程通过它装配 Core;[DSH 实例](DSH_RUNTIME.md)也已接入同一队列机制。写队列、操作取消和关闭归属于应用实例;每次写操作独立取得、释放 Core 租约,DSH 的 Core 观察器由插件实例持有并等待卸载。[固定流程](IMAGE_FLOW.md)使用专用根中的版本化 Run、稳定 Task/比较作品身份及引用保护,私有输入保存提示词,Task 仅保存可选流程绑定。[本地 API](LOCAL_API.md)已提供默认只读与主动启用的 [本地创建](LOCAL_CREATION.md),在 0.2.7 中,执行/取消和真实独立 Web 尚未提供,现已接入上文的0.3 alpha源码;通用 Recipe 仍待实现。详见 [应用运行时契约](CORE_RUNTIME_CONTRACT.md#应用运行时装配)。上图表示已发布入口的共同核心。
42
+ 0.2.7 增加 `lib/application-runtime.js`,首批 CLI 图片命令和固定改图流程通过它装配 Core;[DSH 实例](DSH_RUNTIME.md)也已接入同一队列机制。写队列、操作取消和关闭归属于应用实例;每次写操作独立取得、释放 Core 租约,DSH 的 Core 观察器由插件实例持有并等待卸载。[固定流程](IMAGE_FLOW.md)使用专用根中的版本化 Run、稳定 Task/比较作品身份及引用保护,私有输入保存提示词,Task 仅保存可选流程绑定。[本地 API](LOCAL_API.md)已提供默认只读与主动启用的 [本地创建](LOCAL_CREATION.md),在 0.2.7 中,执行/取消和真实独立 Web 尚未提供,现已接入上文的0.3 beta;通用 Recipe 仍待实现。详见 [应用运行时契约](CORE_RUNTIME_CONTRACT.md#应用运行时装配)。上图表示已发布入口的共同核心。
43
43
 
44
44
  0.2.1的 S2V 使用 Core Task/Attempt,HTML 使用 `media.html` 与显式 Browser Port;视觉和提示词预览通过共享 Model Ports 执行。纯视觉不创建 Task/Artifact,摘要主动转写仍使用 Core 任务。聊天改图属于媒体生成链,按来源 Artifact ID 创建新 Task 和关联产物。
45
45
 
@@ -67,9 +67,9 @@ DSH Host / Cordis Headless CLI
67
67
 
68
68
  完整规则见 [Core Runtime v0](CORE_RUNTIME_CONTRACT.md)、[Artifact Manifest v0](ARTIFACT_MANIFEST.md)、[Provider Adapter v0](PROVIDER_ADAPTER_CONTRACT.md) 与 [DSH → Core 渐进迁移](DSH_CORE_MIGRATION.md)。这些仍是内部契约,不是第三方 SDK 兼容承诺。
69
69
 
70
- 面向 0.3 的0.3 alpha源码增加[Provider 扩展首版](PROVIDER_EXTENSIONS.md):实例拥有协议描述、工厂、活动调用和异步释放;CLI/Core 服务显式传入注册实例,六类内置协议(含 ComfyUI)与外部协议共用候选/健康/恢复。实验性包入口仅开放媒体协议所需接口,尚未提供 DSH 外部模块加载设置、通用 Recipe 或完整 SDK。ComfyUI Task binding 另纳入模板摘要,见 [文生图与工作流入口](IMAGE_GENERATION.md);原五类协议的 binding 与既有记录保持。
70
+ 面向 0.3 的0.3 beta增加[Provider 扩展首版](PROVIDER_EXTENSIONS.md):实例拥有协议描述、工厂、活动调用和异步释放;CLI/Core 服务显式传入注册实例,六类内置协议(含 ComfyUI)与外部协议共用候选/健康/恢复。实验性包入口仅开放媒体协议所需接口,尚未提供 DSH 外部模块加载设置、通用 Recipe 或完整 SDK。ComfyUI Task binding 另纳入模板摘要,见 [文生图与工作流入口](IMAGE_GENERATION.md);原五类协议的 binding 与既有记录保持。
71
71
 
72
- 同属0.3 alpha源码的 [Core 插件贡献 v0](PLUGINS.md)统一五类贡献的身份、兼容与生命周期。新插件 Provider 的 Task/Attempt 在提交前记录准确依赖,旧绑定保持。默认 Panel 可显式组合页面/设置/预览,共享主题、认证客户端和会话;公开装配保持媒体只读,设置沿用 Core Provider 管理,整套替换仍可用。另由 `allowedOperations` 明确选择持久业务,复用同一单根 runner/FIFO/租约及有版本 `plugin.execute` 记录;Core绑定输入/来源、预留输出身份并管理实际作品存储。[多图对照板](CONTACT_SHEET.md)只引用公开 SDK 和现有 Sharp,已贯穿 Web、用户选择、通用 MCP 与原记录恢复。无稳定 SDK、市场或旧数据迁移。
72
+ 同属0.3 beta的 [Core 插件贡献 v0](PLUGINS.md)统一五类贡献的身份、兼容与生命周期。新插件 Provider 的 Task/Attempt 在提交前记录准确依赖,旧绑定保持。默认 Panel 可显式组合页面/设置/预览,共享主题、认证客户端和会话;公开装配保持媒体只读,设置沿用 Core Provider 管理,整套替换仍可用。另由 `allowedOperations` 明确选择持久业务,复用同一单根 runner/FIFO/租约及有版本 `plugin.execute` 记录;Core绑定输入/来源、预留输出身份并管理实际作品存储。[多图对照板](CONTACT_SHEET.md)只引用公开 SDK 和现有 Sharp,已贯穿 Web、用户选择、通用 MCP 与原记录恢复。无稳定 SDK、市场或旧数据迁移。
73
73
 
74
74
  Core能力目录派生MCP/DSH、Web选项/会话和Skill,投影五类贡献的真实边界。注册/发现不授权,旧工具名单不扩张。MCP用通用目录/执行工具投影连接明确保存的准确业务依赖,Core在接受和后续命令处检查有效权限;断开代理不停止Core。重启只记录interrupted,插件缺失或版本变化不重放,原作品/记录仍可读取。实例生命周期由装配者持有,先关服务再关注册器。
75
75
 
@@ -21,12 +21,16 @@ artifact-store/v0/
21
21
 
22
22
  Index 更新失败不会撤销已经提交的 Artifact,避免调用方因本地缓存错误重新生成或重复付费。显式 `artifact rebuild` 会从提交记录和旁车 Manifest 重建 Index。
23
23
 
24
+ beta.1修复在提交 record 前写入私有 `index.dirty` 标记,成功发布完整 Index 后移除。索引发布失败或进程在两者之间中断时,列表明确返回 `IRIS_ARTIFACT_INDEX_INVALID`,Doctor 报告索引异常;按 ID 读取原作品仍可用。下一次成功刷新或显式重建可消除标记,不需要迁移既有记录,也不自动删除媒体。
25
+
24
26
  ## 内容完整性
25
27
 
26
28
  Manifest 使用小写十六进制 SHA-256,并同时记录字节数。`inspect`、`export`、`list` 和重建都会核验对象;即使文件大小不变,内容被替换也返回 `IRIS_ARTIFACT_DIGEST_MISMATCH`。
27
29
 
28
30
  哈希通过固定大小分块读取,不把完整视频再次载入内存。Manifest、错误和 CLI 结果不保存输入路径、旧作品文件名、DSH URL 或供应商下载 URL。
29
31
 
32
+ beta.1中,新作品只核验自身内容和实际使用的关系目标;随后刷新 Index 时检查所有历史提交记录、Manifest 和普通文件大小,不重复重哈希历史媒体。历史结构/大小异常会阻止发布新索引,不静默跳过坏记录。已有摘要的正常提交仍需遍历元数据并原子重写 Index,尚非增量数据库;没有摘要的早期 v0 记录仍须计算真实哈希,可通过显式重建升级。媒体读取、CLI `inspect/list/export` 和显式重建继续验证完整内容哈希,目录刷新本身不证明历史媒体本次已重新验过内容。
33
+
30
34
  ## 关系边
31
35
 
32
36
  关系方向固定为“当前 Artifact 指向既有 Artifact”。v0 支持:
@@ -1,4 +1,4 @@
1
- # Core 语音合成与音频转写(0.3 alpha)
1
+ # Core 语音合成与音频转写(0.3 beta)
2
2
 
3
3
  0.3.0-alpha.1的 Web 与通用 MCP 接入 `speech/v0`、`transcribe/v0`,共用现有 Provider、Task/Attempt、持久 Run 和单根 Operation runner。**稳定 npm 0.2.7 不含这些新增入口。** 原独立 CLI/DSH 的语音和转写继续使用各自原入口;这里描述新的 Core 流程。
4
4
 
@@ -14,12 +14,26 @@ node bin/dsh-iris.js web serve --data-root /absolute/new-audio-core \
14
14
  --token-file /absolute/new-audio-owner-token --allow-execution
15
15
  ```
16
16
 
17
- `--audio` 建立 `creation-requests/v3`,同时支持原图片编辑/生成。不要与 `--image-generation` 混用,也不要在已有 v0/v1/v2 根上执行迁移;原根继续按原能力运行。省略两个开关的 `execution init` 仍建立 v1 图片编辑根,`--image-generation` 仍建立 v2。
17
+ `--audio` 建立 `creation-requests/v3`,同时支持原图片编辑/生成。不要与 `--image-generation` 混用,也不要在已有 v0/v1/v2 根上执行迁移;原根继续按原能力运行。beta.1视频流程另提供 `--video` 新 v4 根,保留这些音频流程;已有 v3 不迁移,三个开关不混用。省略这些开关的 `execution init` 仍建立 v1 图片编辑根,`--image-generation` 仍建立 v2。
18
18
 
19
19
  打开输出的 `loginUrl`;刷新保留当前标签页登录,过期后可用 `web login` 获取新链接,见 [工作台登录](LOCAL_WEB.md#启动与连接)。播放和下载已有结果无需 Provider;新建 Run 需要显式配置,执行另需 `--allow-execution`。音频不需要 `--allow-selection`。
20
20
 
21
21
  配置沿用[模型服务](PROVIDER_SETTINGS.md):为模型明确确认 TTS/转写用途和功能池,再从实际候选中选择。候选不是健康证明,打开页面不做实测。内置音频协议沿用现有 DashScope 实现;配置了 OpenAI 图片接口不等于接入 OpenAI TTS/ASR。这里没有新增语音协议、模型自动替换或音色发现接口。
22
22
 
23
+ ## CLI 保存与继续(beta.1修复)
24
+
25
+ 独立 `flow create/continue` 现装配与服务相同的图片、语音、转写和视频候选。先建立上述 v3/v4 根,并在功能池中允许所选模型:
26
+
27
+ ```bash
28
+ node bin/dsh-iris.js flow create --data-root /absolute/new-audio-core \
29
+ --provider-config /absolute/existing-providers.json \
30
+ --input '{"recipe":"speech/v0","model_ref":"provider::tts-model","text":"鸢尾花","voice":"Cherry"}'
31
+ node bin/dsh-iris.js flow continue run_0123456789abcdef01234567 \
32
+ --data-root /absolute/new-audio-core --provider-config /absolute/existing-providers.json
33
+ ```
34
+
35
+ Run ID 使用创建命令的实际返回值。转写使用 `recipe:"transcribe/v0"`、明确 `model_ref` 及 `source_artifact_id`,来源应先由当前根的 Web/MCP 音频导入保存;这里不新增 CLI 音频导入命令。创建不合成或上传;首次继续才执行,后续继续只观察或交付原 Task,受理未知不重提。已完成且本地结果完整时,无需 Provider 配置再次读取结果。
36
+
23
37
  ## 页面使用
24
38
 
25
39
  - **语音合成**:选择实际语音模型,填写朗读文字,必要时展开音色并填该模型支持的标识;留空使用现有协议默认值。点击合成后,保存输入、明确执行一次、保存原音频,可试听并下载。
@@ -28,9 +42,11 @@ node bin/dsh-iris.js web serve --data-root /absolute/new-audio-core \
28
42
 
29
43
  生成音频/转写是单一结果,保存后即可下载,无需图片版本选择。下载保留原字节;实际落盘以浏览器下载记录为准。离开创作页暂停播放,退出或切换流程释放相应媒体。当前不提供流式合成、音色目录、录音、长音频切片、字幕时间轴或本地 TTS。
30
44
 
45
+ beta.1交付新语音/音乐结果时,会读取有限文件头确认WAV或MP3容器,再保存真实MIME与后缀;不把供应商声明的MP3套在WAV字节上。不支持的音频响应保留已受理事实并报交付失败,不重新合成。此为容器识别,不是完整解码或格式迁移;已入库的旧作品不自动改写。
46
+
31
47
  ## 有限 HTTP 契约
32
48
 
33
- 以下均以 `/api/v0` 开头,保留 Bearer、Host/Origin、无 CORS 和有限权限。新路由只在 v3 根开放;创建使用原 UUIDv4 `Idempotency-Key`,首次 201、同 Key 同输入 200、不同输入 409。
49
+ 以下均以 `/api/v0` 开头,保留 Bearer、Host/Origin、无 CORS 和有限权限。新路由只在 v3 根(beta.1也支持 v4)开放;创建使用原 UUIDv4 `Idempotency-Key`,首次 201、同 Key 同输入 200、不同输入 409。
34
50
 
35
51
  | 路由 | 输入与结果 |
36
52
  |---|---|
@@ -68,4 +84,4 @@ DSH 可选的原生共享 Flow 桥仍限图片,未接入 Core 音频附件投
68
84
 
69
85
  原结果保存失败时,通过原 Run 明确接回;已有私有交付缓存时无需 Provider。浏览器恢复表只存有界身份,不存朗读文字、音色、音频或转写正文;查到创建成功不自动首次执行。查询类型另含 audio-import、speech-create、transcription-create,见 [刷新恢复](WEB_RECOVERY.md)。
70
86
 
71
- 定向测试为 `tests/audio-flow.mjs`、`tests/audio-api.mjs`、`tests/audio-recovery.mjs`、`tests/audio-upload.mjs`。真实 Chromium 可用[工作台验证脚本](LOCAL_WEB.md#验证与下一阶段)的独立 `--audio` 场景,覆盖原字节播放/下载、刷新、丢失创建回包、只读及窄屏/主题。SDK验证包含HTTP 2025/2026与真实stdio进程的2026路径。所有新音频验证使用受控 Provider 或上传响应;**未实测真实付费语音/转写服务,也不代表所有账号、编码或 harness 已通过验收**。
87
+ 定向测试为 `tests/audio-flow.mjs`、`tests/audio-api.mjs`、`tests/audio-recovery.mjs`、`tests/audio-upload.mjs`。真实 Chromium 可用[工作台验证脚本](LOCAL_WEB.md#验证与下一阶段)的独立 `--audio` 场景,覆盖原字节播放/下载、刷新、丢失创建回包、只读及窄屏/主题。SDK验证包含HTTP 2025/2026与真实stdio进程的2026路径。受控 Provider/上传回归与真实账号验收分别记录;**不能把回放、目录存在或单一账号成功推断为所有模型、编码或 harness 已通过验收**。
@@ -0,0 +1,42 @@
1
+ # 0.3.0-beta.1 范围与兼容约定
2
+
3
+ 本页定义 **0.3.0-beta.1** 的发行范围、最小插件契约与主要用户流程。使用准确版本或 npm `beta` 通道,稳定 `latest` 保留0.2.7;历史alpha.1没有新增网络、视频、调度与Provider工厂上下文v1。安装与升级入口见[发布说明](releases/0.3.0-beta.1.md)。
4
+
5
+ ## 纳入的主要流程
6
+
7
+ | 用户入口 | 候选约定 | 继续保留的边界 |
8
+ | --- | --- | --- |
9
+ | 启动/登录 | Core明确启动,首次设置访问口令、记住设备、退出/刷新恢复 | 不修改其他harness配置;Token模式继续作为显式选项 |
10
+ | 模型服务 | 添加/编辑/停用/删除供应商,显式凭据来源、完整目录确认、按功能选择模型池 | 目录和近期成功不是永久健康承诺;无跨请求模型轮转;不默认读取其他宿主或.env |
11
+ | 网络出口 | 独立HTTP/HTTPS节点、固定/有序池、供应商绑定、owner页面/CLI/API/Doctor | 无绑定继承实际进程环境;显式失效不悄悄直连;SOCKS、系统TUN控制、地域自动分流和Clash自动配置不在范围 |
12
+ | 创作/作品 | 图片生成/已支持的编辑协议、ComfyUI声明参数,语音/转写、固定文生视频,原件预览/下载 | 协议及账号能力以实际目录/实现和实测为准;没有新增Core音乐/视觉持久流程或远端视频编辑 |
13
+ | 执行/恢复 | 单物理根单写者,持久Run/Task/Attempt/Operation,显式有界调度、原UUID/原任务接回、本地停止 | 调度缺省串行;并发与HTTP速率/远端受理数分别限制;unknown与交付失败不自动新生成;停止不证明远端停费 |
14
+ | MCP连接 | 通用stdio和Streamable HTTP,显式客户端预设,用户选择功能/工具/scopes,导出/检查/握手/撤销/缩权 | 配置检查、协议握手与真实Agent调用分别验证;不扫描harness、不默认授权或聚合第三方MCP服务 |
15
+ | 插件/Panel | Provider、持久业务、页面/设置挂载、作品渲染,共享主题/API/登录;保留整套Panel替换 | 可信代码,无市场/自动安装/热加载/任意插件设置仓库;页面挂载不自动授权业务 |
16
+
17
+ 入口说明:[从零启动](CORE_QUICKSTART.md)、[模型服务](PROVIDER_SETTINGS.md)、[网络工作台](NETWORK_WORKBENCH.md)、[调度](MEDIA_SCHEDULING.md)、[MCP](MCP.md)、[插件开发](PLUGIN_DEVELOPMENT.md)。
18
+
19
+ ## 最小插件兼容面
20
+
21
+ 本轮冻结的是下列窄约定,内部模块与完整通用SDK不纳入冻结:
22
+
23
+ - Plugin/Adapter契约v0:稳定插件/贡献ID、各自独立版本、Core兼容范围、明确启用与授权、准确持久依赖及缺失/错版错误。
24
+ - Provider工厂上下文v1:Core提供当次解析的连接与借用fetch,管理完整方法的正文/多跳及关闭;插件不拥有Core队列、任务存储或网络资源管理。使用功能标记确认支持,详见[Provider最小契约](PROVIDER_PLUGIN_CONTRACT.md)。
25
+ - 持久业务通过公开有限Core命令和原Operation执行;页面、设置挂载与预览器使用公开浏览器端上下文。Node插件和Shadow DOM都不是安全沙箱。
26
+ - 变更上述可观察输入/结果、权限、生命周期或准确依赖语义时,先审查兼容性并更新相应契约/贡献版本,不把实现升级伪装成原版本恢复。
27
+
28
+ 两类[独立模板](PLUGIN_DEVELOPMENT.md)分别验证业务/页面/PNG预览与真实异步Provider协议。旧窄Provider扩展保留原用法。内部文件不因随包存在而成为公开导出;不要绕过package exports依赖私有路径。
29
+
30
+ ## 数据与平台
31
+
32
+ 旧根不自动迁移;新音频/视频根与网络任务有明确版本门禁,不能让旧CLI写入它们。已发布DSH原工具和原CLI仍按既有范围使用,Core新音视频不自动加入DSH原生共享Flow桥。
33
+
34
+ beta.1最低Node为22.1.0:隔离复核确认22.0.0原生fetch正文取消不会关闭连接,会占用调度名额;22.1.0与当前22.23.4通过同一验证。CI配置包含Ubuntu/Windows及最低/当前Node22;**工作流配置存在和历史CI成功都不能证明当前候选通过**。本机Termux/PRoot ARM64、真实浏览器、本地代理、实际供应商和原生Agent结果应分别记录。最终发布还需核对同一候选的完整测试、独立包消费者和标准平台矩阵,不能拿旧压测或一次握手替代。
35
+
36
+ 自动配额/GC、复杂模型/出口加权轮询、更多宿主凭据桥、legacy统一迁移、全部供应商/GPU/手机/浏览器/harness认证继续后置;不因本页列出能力就宣称这些覆盖已完成。
37
+
38
+ ## 源码验收与发行候选
39
+
40
+ 文档中的 `node scripts/...`、`tests/...`、`npm test` 和 `npm run lint` 从源码仓库根运行;开发脚本与测试不随npm包根目录分发。安装用户继续使用包内CLI/Web/MCP入口,不能把源码验证命令当作已安装包的使用要求。
41
+
42
+ CI除Ubuntu/Windows、最低/当前Node22完整测试外,另在Ubuntu当前Node22构建真实tarball,检查包外插件/Provider与凭据/调度消费者、strict NodeNext公共类型,并用真实Chromium验证打包后的总览、凭据、视频与调度页面。缺少必需编译器或浏览器会使该发行检查失败;其他可选运行时的SKIP不计作通过。具体运行结果以对应提交的CI和发行记录为准,历史B5记录不代替本轮结果。
@@ -1,4 +1,4 @@
1
- # 多图对照板:真实插件业务(0.3 alpha)
1
+ # 多图对照板:真实插件业务(0.3 beta)
2
2
 
3
3
  0.3.0-alpha.1随附可选插件 `@mokuyoaxis/dsh-iris/contact-sheet`。**稳定 npm 0.2.7 无此入口。** 它将现有 Core 图片离线排成一个新 PNG Artifact,不调用模型或供应商。安装不会启用或授权插件:Node 业务、Web 页和 MCP 连接分别显式选择。
4
4