@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
@@ -1,12 +1,18 @@
1
1
  'use strict';
2
2
 
3
3
  import { normalizeCoreOptions } from './core-contract.js';
4
- import { createCoreRuntime } from './core-runtime.js';
4
+ import { createCoreRuntime, inspectCoreWriterLease } from './core-runtime.js';
5
5
  import { createCommandService } from './command-service.js';
6
6
  import { normalizeGenerationInput } from './generation-input.js';
7
7
  import { createProviderTaskRunner } from './provider-task-runner.js';
8
8
  import { createImageFlowService } from './image-flow.js';
9
9
  import { createLocalCreationService } from './local-creation.js';
10
+ import { createLocalOperationService } from './local-operations.js';
11
+ import { reservePluginOperation } from './plugin-operations.js';
12
+ import { invokeReservedPlugin } from './plugin-execution.js';
13
+ import { AsyncResource } from 'node:async_hooks';
14
+ import { createApplicationWriter } from './application-writer.js';
15
+ import { createMediaScheduler } from './media-scheduler.js';
10
16
 
11
17
  // Internal first-slice execution surface, not a public API or an HTTP allowlist.
12
18
  const COMMAND_MODES = Object.freeze({
@@ -15,7 +21,11 @@ const COMMAND_MODES = Object.freeze({
15
21
  'artifact.inspect': 'reader', 'artifact.list': 'reader', 'task.inspect': 'reader', 'task.list': 'reader',
16
22
  'flow.init': 'writer', 'flow.create': 'writer', 'flow.continue': 'writer', 'flow.select': 'writer', 'flow.export': 'writer',
17
23
  'flow.inspect': 'reader', 'flow.list': 'reader',
18
- 'creation.init': 'writer', 'creation.image': 'writer', 'creation.flow': 'writer'
24
+ 'creation.init': 'writer', 'creation.image': 'writer', 'creation.flow': 'writer', 'creation.generate': 'writer',
25
+ 'creation.audio': 'writer', 'creation.speech': 'writer', 'creation.transcribe': 'writer', 'creation.video': 'writer',
26
+ 'creation.root': 'reader', 'creation.lookup': 'reader',
27
+ 'execution.init': 'writer', 'operation.accept': 'writer', 'operation.transition': 'writer',
28
+ 'operation.inspect': 'reader', 'operation.lookup': 'reader', 'operation.list': 'reader'
19
29
  });
20
30
 
21
31
  export class ApplicationRuntimeError extends Error {
@@ -31,7 +41,8 @@ function closed() {
31
41
  }
32
42
 
33
43
  /** Instance-owned FIFO and operation-scoped Core leases. No I/O until an explicit operation. */
34
- export function createApplicationRuntime({ dataRoot, resolveImageCandidates, resolveTaskAdapter } = {}) {
44
+ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, resolveAudioCandidates, resolveVideoCandidates, resolveTaskAdapter,
45
+ scheduling, scheduler: suppliedScheduler } = {}) {
35
46
  const root = normalizeCoreOptions({ dataRoot }).dataRoot;
36
47
  if (resolveImageCandidates !== undefined && typeof resolveImageCandidates !== 'function') {
37
48
  throw new TypeError('resolveImageCandidates 必须是显式候选解析函数');
@@ -39,22 +50,30 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
39
50
  if (resolveTaskAdapter !== undefined && typeof resolveTaskAdapter !== 'function') {
40
51
  throw new TypeError('resolveTaskAdapter 必须是显式 Task Provider 解析函数');
41
52
  }
53
+ if (resolveAudioCandidates !== undefined && typeof resolveAudioCandidates !== 'function') throw new TypeError('音频候选解析须为函数');
54
+ if (resolveVideoCandidates !== undefined && typeof resolveVideoCandidates !== 'function') throw new TypeError('视频候选解析须为函数');
42
55
  let state = 'open';
43
56
  const queue = [];
44
57
  const active = new Set();
45
58
  let writer = null;
46
59
  let disposePromise;
47
60
  let cleanupFailure;
61
+ const scheduler = suppliedScheduler || (scheduling ? createMediaScheduler(scheduling) : undefined);
62
+ const managedJobs = new Set();
63
+ const cooperative = scheduler ? createApplicationWriter({ dataRoot: root, policy: scheduler.policy, scheduler,
64
+ onFailure(error) { cleanupFailure ||= error; state = 'closing'; cooperative.abort(); stopAll(); } }) : undefined;
48
65
 
49
66
  function snapshot() {
50
67
  return Object.freeze({ state, queuedWrites: queue.length, activeWrites: Number(Boolean(writer)),
51
- activeReaders: active.size - Number(Boolean(writer)), cleanupFailed: Boolean(cleanupFailure) });
68
+ activeReaders: active.size - Number(Boolean(writer)), cleanupFailed: Boolean(cleanupFailure),
69
+ ...(cooperative ? { ...cooperative.snapshot(), schedulingEnabled: true } : {}) });
52
70
  }
53
71
 
54
72
  function detach(job) { job.signal?.removeEventListener('abort', job.onAbort); }
55
73
 
56
74
  function rejectQueued(job, error) {
57
75
  detach(job);
76
+ job.resource.emitDestroy();
58
77
  job.reject(error);
59
78
  }
60
79
 
@@ -80,7 +99,16 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
80
99
  try {
81
100
  runtime.start();
82
101
  result = await job.action(runtime);
83
- } catch (error) { failed = true; operationError = error; }
102
+ } catch (error) {
103
+ failed = true; operationError = error;
104
+ if (job.mode === 'reader' && error?.code === 'IRIS_ARTIFACT_INDEX_INVALID' && error.missingFile === true) {
105
+ const lease = inspectCoreWriterLease(root);
106
+ if (lease.status === 'present' && lease.ownerStatus === 'alive') {
107
+ operationError = new ApplicationRuntimeError('IRIS_CORE_DATA_ROOT_BUSY', '作品读取遇到活动写入,请在写入结束后显式重试');
108
+ Object.defineProperty(operationError, 'cause', { value: error });
109
+ }
110
+ }
111
+ }
84
112
  try { await (job.disposal || runtime.dispose()); }
85
113
  catch (error) {
86
114
  cleanupFailure ||= error;
@@ -97,7 +125,7 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
97
125
 
98
126
  function start(job) {
99
127
  active.add(job);
100
- job.done = Promise.resolve().then(async () => {
128
+ job.done = job.resource.runInAsyncScope(() => Promise.resolve().then(async () => {
101
129
  let result, error, failed = false;
102
130
  try { result = await perform(job); }
103
131
  catch (caught) { failed = true; error = caught; }
@@ -105,8 +133,9 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
105
133
  active.delete(job);
106
134
  if (writer === job) writer = null;
107
135
  if (failed) job.reject(error); else job.resolve(result);
136
+ job.resource.emitDestroy();
108
137
  dispatch();
109
- });
138
+ }));
110
139
  }
111
140
 
112
141
  function dispatch() {
@@ -115,12 +144,19 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
115
144
  start(writer);
116
145
  }
117
146
 
118
- function schedule(mode, action, { signal } = {}) {
147
+ function schedule(mode, action, { signal, managed = false, transaction = false, finalize = false, key } = {}) {
119
148
  if (signal !== undefined && !(signal instanceof AbortSignal)) throw new TypeError('signal 必须是 AbortSignal');
120
149
  if (state !== 'open') return Promise.reject(closed());
121
150
  if (signal?.aborted) return Promise.reject(aborted());
151
+ if (mode === 'writer' && cooperative) {
152
+ const promise = cooperative.execute(action, { signal, managed: managed || transaction, transaction, finalize, key });
153
+ managedJobs.add(promise);
154
+ void promise.then(() => managedJobs.delete(promise), () => managedJobs.delete(promise));
155
+ return promise;
156
+ }
122
157
  let job;
123
- const result = new Promise((resolve, reject) => { job = { mode, action, signal, resolve, reject, aborted: false }; });
158
+ const result = new Promise((resolve, reject) => { job = { mode, action, signal, resolve, reject, aborted: false,
159
+ resource: new AsyncResource('IrisApplicationJob') }; });
124
160
  job.onAbort = () => {
125
161
  const index = queue.indexOf(job);
126
162
  if (index >= 0) { queue.splice(index, 1); rejectQueued(job, aborted()); }
@@ -146,15 +182,19 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
146
182
  const ownedInput = structuredClone(input);
147
183
  return schedule(COMMAND_MODES[name], runtime => {
148
184
  const ports = resolveTaskAdapter ? { resolveTaskAdapter: async task => {
149
- const adapter = await resolveTaskAdapter(task, { signal: runtime.signal });
185
+ const resolve = () => resolveTaskAdapter(task, { signal: runtime.signal });
186
+ const adapter = await (runtime.networkWait ? runtime.networkWait(resolve, 'candidates') : resolve());
150
187
  if (runtime.signal.aborted) throw aborted();
151
188
  return adapter;
152
189
  } } : {};
153
- if (name.startsWith('creation.')) return createLocalCreationService(runtime, { resolveImageCandidates,
190
+ if (name === 'execution.init' || name.startsWith('operation.')) return createLocalOperationService(runtime).execute(name, ownedInput);
191
+ if (name.startsWith('creation.')) return createLocalCreationService(runtime, { resolveImageCandidates, resolveAudioCandidates, resolveVideoCandidates,
154
192
  onPrepared: options?.onPrepared }).execute(name, ownedInput);
155
- if (name.startsWith('flow.')) return createImageFlowService(runtime, { ...ports, resolveImageCandidates }).execute(name, ownedInput);
193
+ if (name.startsWith('flow.')) return createImageFlowService(runtime, { ...ports, resolveImageCandidates, resolveAudioCandidates, resolveVideoCandidates }).execute(name, ownedInput);
156
194
  return createCommandService(runtime, ports).execute(name, ownedInput);
157
- }, options);
195
+ }, { ...options, managed: true,
196
+ key: ownedInput?.run_id ? `run:${ownedInput.run_id}` : ownedInput?.task_id ? `task:${ownedInput.task_id}`
197
+ : name.startsWith('creation.') && ownedInput?.key ? `creation:${name}:${ownedInput.key}` : undefined });
158
198
  }
159
199
 
160
200
  async function generateImage(input, options) {
@@ -163,18 +203,31 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
163
203
  throw new ApplicationRuntimeError('IRIS_APP_IMAGE_RESOLVER_MISSING', '图片生成需要显式提供候选解析函数');
164
204
  }
165
205
  return schedule('writer', async runtime => {
166
- const candidates = await resolveImageCandidates({ modelRef: normalized.modelRef, signal: runtime.signal });
206
+ const resolve = () => resolveImageCandidates({ modelRef: normalized.modelRef, signal: runtime.signal });
207
+ const candidates = await (runtime.networkWait ? runtime.networkWait(resolve, 'candidates') : resolve());
167
208
  if (runtime.signal.aborted) throw aborted();
168
209
  return createProviderTaskRunner(runtime).submit({ capability: 'image', candidates,
169
210
  providerInput: normalized.providerInput, sourceArtifactId: normalized.sourceArtifactId });
170
- }, options);
211
+ }, { ...options, managed: true });
212
+ }
213
+
214
+ /** Explicit trusted assembly only. No HTTP/MCP route or persistent Operation action is added. */
215
+ async function executePluginOperation(registry, id, input, { allowedOperations = [], dependency, signal } = {}) {
216
+ const reserved = reservePluginOperation(registry, id, input, allowedOperations, dependency);
217
+ try {
218
+ return await schedule(reserved.mode, runtime => runtime.run(reserved.mode === 'reader' ? 'inspect' : 'execute',
219
+ () => invokeReservedPlugin(runtime, reserved)), { signal });
220
+ } finally { reserved.release(); }
171
221
  }
172
222
 
173
223
  function dispose() {
174
224
  if (disposePromise) return disposePromise;
175
225
  state = 'closing';
176
226
  stopAll();
177
- disposePromise = Promise.all([...active].map(job => job.done)).then(() => {
227
+ cooperative?.abort();
228
+ disposePromise = Promise.allSettled([...active].map(job => job.done).concat([...managedJobs])).then(async () => {
229
+ await cooperative?.dispose();
230
+ if (!suppliedScheduler) scheduler?.dispose();
178
231
  if (cleanupFailure) throw cleanupFailure;
179
232
  state = 'closed';
180
233
  return snapshot();
@@ -182,5 +235,8 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
182
235
  return disposePromise;
183
236
  }
184
237
 
185
- return Object.freeze({ execute, generateImage, run, dispose, snapshot });
238
+ return Object.freeze({ execute, generateImage, executePluginOperation, run, dispose, snapshot, scheduler,
239
+ schedulingPhase(key) { return cooperative?.phase(key) || null; },
240
+ runMedia(action, options) { return schedule('writer', action, { ...options, managed: true }); },
241
+ transaction(action, { finalize = false } = {}) { return schedule('writer', action, { transaction: true, finalize }); } });
186
242
  }
@@ -0,0 +1,193 @@
1
+ import { AsyncResource } from 'node:async_hooks';
2
+ import { createCoreRuntime } from './core-runtime.js';
3
+ import { schedulingError } from './scheduling-policy.js';
4
+ import { performance } from 'node:perf_hooks';
5
+
6
+ const aborted = () => Object.assign(schedulingError('IRIS_APP_ABORTED', 'Iris 操作已取消'), { name: 'AbortError' });
7
+ const closed = () => schedulingError('IRIS_APP_CLOSED', 'Iris 应用运行时正在关闭或已关闭');
8
+
9
+ /** Internal cooperative writer. Only known media code may loan its write turn. */
10
+ export function createApplicationWriter({ dataRoot, policy, scheduler, onFailure }) {
11
+ const pending = [], active = new Set(), taskGate = {};
12
+ let rootRuntime, owner, closing = false, failure, retiring, disposal;
13
+ const snapshot = () => ({ queuedWrites: pending.filter(entry => entry.kind === 'start').length,
14
+ activeWrites: Number(Boolean(rootRuntime)), writeTurn: Number(Boolean(owner)),
15
+ managedWrites: active.size, waitingWrites: [...active].filter(job => job.parked).length, cleanupFailed: Boolean(failure) });
16
+
17
+ function remove(entry) {
18
+ const index = pending.indexOf(entry);
19
+ if (index >= 0) pending.splice(index, 1);
20
+ clearTimeout(entry.timer); entry.signal?.removeEventListener('abort', entry.abort);
21
+ }
22
+ function enqueue(entry) {
23
+ return new Promise((resolve, reject) => {
24
+ entry.resolve = resolve; entry.reject = reject;
25
+ pending.push(entry); pump();
26
+ });
27
+ }
28
+ async function retire() {
29
+ const runtime = rootRuntime;
30
+ try { await runtime.dispose(); }
31
+ catch (error) {
32
+ failure = error; closing = true; onFailure?.(error);
33
+ for (const entry of [...pending]) { remove(entry); entry.reject(error); }
34
+ throw error;
35
+ } finally { rootRuntime = null; retiring = null; pump(); }
36
+ }
37
+ function pump() {
38
+ if (owner || retiring) return;
39
+ // A waiting exclusive callback cannot block the resumes that let earlier media jobs finish.
40
+ let index = pending.findIndex(entry => entry.kind !== 'start' || entry.transaction);
41
+ if (index < 0) index = 0;
42
+ let entry = pending[index];
43
+ if (!entry) {
44
+ if (rootRuntime && !active.size) {
45
+ retiring = Promise.resolve().then(retire); void retiring.catch(() => {});
46
+ }
47
+ return;
48
+ }
49
+ if (entry.kind === 'start' && performance.now() >= entry.deadline) {
50
+ remove(entry); entry.reject(schedulingError('IRIS_SCHEDULER_QUEUE_TIMEOUT', '本地写入等候超时;未开始本次操作')); pump(); return;
51
+ }
52
+ if (entry.kind === 'start' && (!entry.managed && active.size || [...active].some(job => !job.managed))) return;
53
+ remove(entry);
54
+ if (entry.kind === 'start') {
55
+ try {
56
+ if (!rootRuntime) { rootRuntime = createCoreRuntime({ dataRoot, mode: 'writer' }); rootRuntime.start(); }
57
+ } catch (error) {
58
+ entry.reject(error);
59
+ // start may have retained a failed lease; dispose must report that failure.
60
+ retiring = Promise.resolve().then(retire); void retiring.catch(() => {}); return;
61
+ }
62
+ const job = entry.job;
63
+ active.add(job); owner = job;
64
+ entry.resolve();
65
+ } else { owner = entry.job; entry.resolve(); }
66
+ }
67
+ function release(job) {
68
+ if (owner !== job) throw new Error('Iris writer turn ownership mismatch');
69
+ owner = null;
70
+ let error;
71
+ try { scheduler?.changed(dataRoot); } catch (caught) { error = caught; }
72
+ pump(); return error;
73
+ }
74
+ async function networkWait(job, action, reason = 'network') {
75
+ if (!job.managed || job.parked) throw new Error('Iris write turn can only be loaned once by managed media');
76
+ job.parked = reason; const releaseError = release(job);
77
+ let result, error;
78
+ try { if (releaseError) throw releaseError; result = await action(); } catch (caught) { error = caught; }
79
+ // Even an aborted operation reacquires its turn before recording acceptance/terminal facts.
80
+ await enqueue({ kind: 'resume', job }); job.parked = null; job.phase = null;
81
+ if (error) throw error;
82
+ return result;
83
+ }
84
+ async function checkpoint(job, action) {
85
+ if (job.finished) throw closed();
86
+ const parked = job.parked;
87
+ if (parked) await enqueue({ kind: 'checkpoint', job });
88
+ if (owner !== job) throw new Error('Core checkpoint requires the current write turn');
89
+ try {
90
+ const value = action();
91
+ if (value && typeof value.then === 'function') throw new TypeError('Core checkpoint 必须同步完成');
92
+ return value;
93
+ } finally { if (parked) { const error = release(job); if (error) throw error; } }
94
+ }
95
+ async function execute(action, { signal, managed = false, transaction = false, finalize = false, key } = {}) {
96
+ if (closing) throw closed();
97
+ signal?.throwIfAborted();
98
+ if (!finalize && active.size + pending.filter(entry => entry.kind === 'start').length >= policy.queueLimit) {
99
+ throw schedulingError('IRIS_SCHEDULER_QUEUE_FULL', '本地写入队列已满;请查询原操作后再明确重试');
100
+ }
101
+ const resource = new AsyncResource('IrisApplicationWriter'), controller = new AbortController();
102
+ const job = { managed, parked: null, key, controller, cleanup: [], inFlight: new Set(), finished: false };
103
+ // Only the runner uses finalize for short terminal writes of already accepted Operations.
104
+ // They must drain under the writer even when new work has reached its admission budget.
105
+ const entry = { kind: 'start', job, signal, managed, transaction, deadline: finalize ? Infinity : performance.now() + policy.queueTimeoutMs };
106
+ // Same durable Run/creation key must not overlap even if its candidate resolver waits.
107
+ if (key && ([...active].some(other => other.key === key) || pending.some(other => other.job.key === key))) {
108
+ resource.emitDestroy(); throw schedulingError('IRIS_APP_OPERATION_BUSY', '原 Run 或创建 Key 正在执行;请查询原操作');
109
+ }
110
+ entry.abort = () => {
111
+ controller.abort();
112
+ if (!active.has(job)) { remove(entry); entry.reject(aborted()); pump(); }
113
+ };
114
+ signal?.addEventListener('abort', entry.abort, { once: true });
115
+ const start = enqueue(entry);
116
+ // Queued starts are bounded; resumes/terminal persistence never expire behind this timer.
117
+ if (pending.includes(entry) && !finalize) entry.timer = setTimeout(() => {
118
+ remove(entry); entry.reject(schedulingError('IRIS_SCHEDULER_QUEUE_TIMEOUT', '本地写入等候超时;未开始本次操作')); pump();
119
+ }, Math.max(1, entry.deadline - performance.now()));
120
+ let result, operationError;
121
+ try {
122
+ await start;
123
+ // The queue entry's abort listener was removed at admission; active jobs keep their own signal.
124
+ signal?.addEventListener('abort', entry.abort, { once: true });
125
+ if (signal?.aborted) controller.abort();
126
+ controller.signal.throwIfAborted();
127
+ const runtime = Object.freeze({
128
+ get signal() { return controller.signal; }, taskGate,
129
+ run(operation, callback) {
130
+ if (job.finished) throw closed();
131
+ if (owner !== job) throw new Error('Core operation requires the current write turn');
132
+ const promise = rootRuntime.run(operation, context => callback(Object.freeze({ ...context, signal: controller.signal })));
133
+ job.inFlight.add(promise);
134
+ void promise.then(() => job.inFlight.delete(promise), () => job.inFlight.delete(promise));
135
+ return promise;
136
+ },
137
+ snapshot: () => rootRuntime.snapshot(),
138
+ onDispose(callback) {
139
+ if (job.finished) throw closed();
140
+ if (typeof callback !== 'function') throw new TypeError('Core cleanup 必须是函数');
141
+ job.cleanup.push(callback); return () => { const i = job.cleanup.indexOf(callback); if (i < 0) return false; job.cleanup.splice(i, 1); return true; };
142
+ },
143
+ networkWait: (callback, reason) => networkWait(job, callback, reason),
144
+ checkpoint: callback => checkpoint(job, callback),
145
+ reportScheduling(phase) { if (job.parked) job.phase = phase; },
146
+ scheduler,
147
+ schedulingState: () => job.parked || 'writing'
148
+ });
149
+ result = await resource.runInAsyncScope(action, undefined, runtime);
150
+ } catch (error) { operationError = error; }
151
+ finally {
152
+ signal?.removeEventListener('abort', entry.abort);
153
+ if (active.has(job)) {
154
+ while (job.inFlight.size) await Promise.allSettled([...job.inFlight]);
155
+ job.finished = true; controller.abort();
156
+ try { for (const callback of job.cleanup.reverse()) await callback(); }
157
+ catch (error) {
158
+ failure = error; closing = true;
159
+ // A failed per-job cleanup keeps the same retained-lease semantics as CoreRuntime.
160
+ rootRuntime.onDispose(() => { throw error; });
161
+ onFailure?.(error); operationError ||= error;
162
+ }
163
+ active.delete(job);
164
+ if (owner === job) { const error = release(job); operationError ||= error; } else pump();
165
+ if (retiring) {
166
+ try { await retiring; }
167
+ catch (error) { operationError = operationError ? Object.assign(new AggregateError([operationError, error], '操作和写者释放均失败'), { code: error.code }) : error; }
168
+ }
169
+ }
170
+ resource.emitDestroy();
171
+ }
172
+ if (operationError) throw operationError;
173
+ return result;
174
+ }
175
+ function dispose() {
176
+ if (disposal) return disposal;
177
+ closing = true;
178
+ for (const entry of [...pending]) if (entry.kind === 'start') { remove(entry); entry.reject(closed()); }
179
+ for (const job of active) job.controller.abort();
180
+ disposal = (async () => {
181
+ // The application awaits admitted jobs before invoking this final drain.
182
+ if (active.size) throw new Error('Iris writer disposed before jobs drained');
183
+ pump(); if (retiring) await retiring;
184
+ if (failure) throw failure;
185
+ })();
186
+ return disposal;
187
+ }
188
+ return Object.freeze({ execute, snapshot, dispose, phase(key) { const job = [...active].find(job => job.key === key); return job?.phase || job?.parked || null; }, abort() {
189
+ closing = true;
190
+ for (const entry of [...pending]) if (entry.kind === 'start') { remove(entry); entry.reject(closed()); }
191
+ for (const job of active) job.controller.abort();
192
+ } });
193
+ }
@@ -0,0 +1,37 @@
1
+ import { normalizeGenerationInput } from './generation-input.js';
2
+ import { parseModelRef } from './models.js';
3
+
4
+ export const AUDIO_RECIPES = Object.freeze({
5
+ 'speech/v0': Object.freeze({ capability: 'tts', step: 'speak' }),
6
+ 'transcribe/v0': Object.freeze({ capability: 'transcribe', step: 'transcribe' })
7
+ });
8
+ export const MAX_AUDIO_INPUT_BYTES = 20 * 1024 * 1024;
9
+ export const AUDIO_TYPES = Object.freeze({ 'audio/wav': 'wav', 'audio/mpeg': 'mp3', 'audio/ogg': 'ogg', 'audio/flac': 'flac' });
10
+ export const isAudioRecipe = recipe => Object.hasOwn(AUDIO_RECIPES, recipe);
11
+ const invalid = message => { throw Object.assign(new Error(message), { code: 'IRIS_AUDIO_INPUT_INVALID' }); };
12
+
13
+ export function normalizeAudioFlowInput(recipe, input) {
14
+ if (!isAudioRecipe(recipe) || !input || typeof input !== 'object' || Array.isArray(input)) invalid('不支持的音频流程输入');
15
+ const speech = recipe === 'speech/v0', required = speech ? ['model_ref', 'text'] : ['model_ref', 'source_artifact_id'];
16
+ if (Object.keys(input).some(key => ![...required, ...(speech ? ['voice'] : [])].includes(key))
17
+ || required.some(key => typeof input[key] !== 'string') || !parseModelRef(input.model_ref)
18
+ || input.voice !== undefined && typeof input.voice !== 'string') invalid('请提供明确模型和文字或当前工作区的音频作品');
19
+ if (speech) {
20
+ const normalized = normalizeGenerationInput('tts', input);
21
+ return { model_ref: normalized.modelRef, ...normalized.providerInput };
22
+ }
23
+ if (!/^artifact_[a-f0-9]{24}$/.test(input.source_artifact_id)) invalid('转写来源须为当前工作区的音频作品');
24
+ return { model_ref: input.model_ref, source_artifact_id: input.source_artifact_id };
25
+ }
26
+
27
+ /** Bounded container sniffing, not a decoder or a claim of provider compatibility. */
28
+ export function readImportAudio(bytes, mediaType) {
29
+ if (!(bytes instanceof Uint8Array) || !bytes.length || bytes.length > MAX_AUDIO_INPUT_BYTES) invalid('音频须为最多20 MiB的WAV、MP3、OGG或FLAC');
30
+ const value = Buffer.from(bytes);
31
+ const actual = value.length >= 12 && value.toString('ascii', 0, 4) === 'RIFF' && value.toString('ascii', 8, 12) === 'WAVE' ? 'audio/wav'
32
+ : value.length >= 10 && (value.toString('ascii', 0, 3) === 'ID3' || value[0] === 0xff && (value[1] & 0xe0) === 0xe0) ? 'audio/mpeg'
33
+ : value.length >= 27 && value.toString('ascii', 0, 4) === 'OggS' ? 'audio/ogg'
34
+ : value.length >= 8 && value.toString('ascii', 0, 4) === 'fLaC' ? 'audio/flac' : null;
35
+ if (!actual || actual !== mediaType) invalid('音频容器与Content-Type不一致,或不是支持的格式');
36
+ return { bytes: value, mediaType: actual, metadata: {} };
37
+ }
package/lib/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- declare const _default: {
1
+ /** The factory registered with DSH's browser ModuleLoader, not a Node module export. */
2
+ export interface DshClientPlugin {
2
3
  inject: string[];
3
4
  apply(ctx: any): void;
4
- };
5
- export = _default;
5
+ }