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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/CHANGELOG.md +63 -1
  2. package/README.en.md +20 -13
  3. package/README.md +20 -13
  4. package/bin/dsh-iris.js +143 -51
  5. package/docs/AGENT_CORE_QUICKSTART.md +2 -2
  6. package/docs/AGENT_QUICKSTART.md +1 -1
  7. package/docs/ARCHITECTURE.md +5 -5
  8. package/docs/ARTIFACT_MANIFEST.md +4 -0
  9. package/docs/AUDIO_FLOWS.md +20 -4
  10. package/docs/BETA_SCOPE.md +42 -0
  11. package/docs/CONTACT_SHEET.md +1 -1
  12. package/docs/CORE_CONNECTIONS.md +3 -1
  13. package/docs/CORE_QUICKSTART.md +7 -5
  14. package/docs/CORE_RUNTIME_CONTRACT.md +3 -3
  15. package/docs/CORE_SERVICE.md +9 -5
  16. package/docs/CORE_SKILLS.md +2 -2
  17. package/docs/DSH_COMPATIBILITY.md +11 -1
  18. package/docs/DSH_CORE_MIGRATION.md +6 -0
  19. package/docs/DSH_RUNTIME.md +2 -2
  20. package/docs/HEADLESS_CLI.md +29 -4
  21. package/docs/HOST_ADAPTER_CONTRACT.md +5 -3
  22. package/docs/IMAGE_FLOW.md +2 -2
  23. package/docs/IMAGE_GENERATION.md +1 -1
  24. package/docs/LOCAL_API.md +2 -2
  25. package/docs/LOCAL_API_CONTRACT.md +17 -15
  26. package/docs/LOCAL_CREATION.md +2 -2
  27. package/docs/LOCAL_OPERATIONS.md +2 -2
  28. package/docs/LOCAL_SELECTION.md +1 -1
  29. package/docs/LOCAL_WEB.md +11 -5
  30. package/docs/MCP.md +30 -9
  31. package/docs/MEDIA_SCHEDULING.md +72 -0
  32. package/docs/NETWORK_RESOURCES.md +106 -0
  33. package/docs/NETWORK_ROUTING.md +103 -0
  34. package/docs/NETWORK_WORKBENCH.md +65 -0
  35. package/docs/PANEL_EXTENSIONS.md +1 -1
  36. package/docs/PLUGINS.md +9 -5
  37. package/docs/PLUGIN_DEVELOPMENT.md +39 -0
  38. package/docs/PLUGIN_STARTUP.md +15 -2
  39. package/docs/PROVIDER_ADAPTER_CONTRACT.md +8 -2
  40. package/docs/PROVIDER_CREDENTIALS.md +67 -0
  41. package/docs/PROVIDER_EXTENSIONS.md +8 -4
  42. package/docs/PROVIDER_HEALTH.md +4 -2
  43. package/docs/PROVIDER_NETWORK.md +45 -0
  44. package/docs/PROVIDER_PLUGIN_CONTRACT.md +24 -0
  45. package/docs/PROVIDER_SETTINGS.md +13 -7
  46. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +3 -1
  47. package/docs/ROADMAP.md +6 -6
  48. package/docs/SECURITY.md +1 -1
  49. package/docs/SYNC_DELIVERY_RECOVERY.md +5 -1
  50. package/docs/VIDEO_FLOWS.md +89 -0
  51. package/docs/VISION_MODEL.md +2 -0
  52. package/docs/WEB_PROTOTYPE.md +3 -3
  53. package/docs/WEB_RECOVERY.md +2 -2
  54. package/docs/WORKBENCH_ARTIFACTS.md +6 -0
  55. package/docs/WORKBENCH_LOGIN.md +2 -2
  56. package/docs/releases/0.3.0-beta.1.md +34 -0
  57. package/docs/screenshots.md +2 -2
  58. package/lib/actions.js +5 -4
  59. package/lib/adapters.js +58 -41
  60. package/lib/api.js +23 -13
  61. package/lib/application-runtime.js +44 -15
  62. package/lib/application-writer.js +193 -0
  63. package/lib/client.d.ts +3 -3
  64. package/lib/client.js +62 -25
  65. package/lib/comfyui-adapter.js +3 -2
  66. package/lib/comfyui-client.js +10 -1
  67. package/lib/command-service.js +18 -5
  68. package/lib/config.js +13 -9
  69. package/lib/configured-image-runtime.js +28 -9
  70. package/lib/connection-skill.js +9 -0
  71. package/lib/core-artifact-store.js +68 -26
  72. package/lib/core-bridge-tools.js +5 -3
  73. package/lib/core-capability-catalog.js +21 -11
  74. package/lib/core-delivery-cache.js +8 -3
  75. package/lib/core-service-client.js +6 -2
  76. package/lib/core-tasks.js +86 -11
  77. package/lib/creation-request-store.js +8 -5
  78. package/lib/doctor.js +2 -2
  79. package/lib/dsh-core-adapter.js +61 -9
  80. package/lib/dsh-provider-config.js +1 -1
  81. package/lib/dsh-service-bridge.js +1 -0
  82. package/lib/flow-store.js +64 -17
  83. package/lib/image-flow.js +39 -17
  84. package/lib/index.js +10 -5
  85. package/lib/local-api-projection.js +6 -2
  86. package/lib/local-api.js +137 -49
  87. package/lib/local-creation.js +12 -5
  88. package/lib/local-operations.js +2 -2
  89. package/lib/local-request-root.js +2 -2
  90. package/lib/local-web-assets.js +2 -0
  91. package/lib/mcp-http.js +82 -9
  92. package/lib/mcp-server.js +23 -1
  93. package/lib/media-scheduler.js +194 -0
  94. package/lib/model-directory-service.js +25 -8
  95. package/lib/music-transport.js +4 -2
  96. package/lib/network-config-service.js +123 -0
  97. package/lib/network-config.js +127 -0
  98. package/lib/network-doctor.js +94 -0
  99. package/lib/network-management.js +51 -0
  100. package/lib/network-policy.js +19 -0
  101. package/lib/network-route-evidence.js +38 -0
  102. package/lib/network-routing.js +283 -0
  103. package/lib/operation-runner.js +83 -38
  104. package/lib/operation-store.js +6 -6
  105. package/lib/plugins.d.ts +25 -6
  106. package/lib/plugins.js +2 -1
  107. package/lib/provider-adapter.js +2 -2
  108. package/lib/provider-adapters.js +37 -5
  109. package/lib/provider-catalog.js +33 -16
  110. package/lib/provider-config-format.js +51 -0
  111. package/lib/provider-config-service.js +52 -22
  112. package/lib/provider-context.d.ts +90 -0
  113. package/lib/provider-context.js +42 -0
  114. package/lib/provider-credentials.js +97 -0
  115. package/lib/provider-diagnostics.js +5 -5
  116. package/lib/provider-health.js +10 -5
  117. package/lib/provider-monitor.js +5 -3
  118. package/lib/provider-network.js +52 -0
  119. package/lib/provider-protocol.js +3 -2
  120. package/lib/provider-registry.js +15 -5
  121. package/lib/provider-response.js +10 -0
  122. package/lib/provider-settings.js +14 -12
  123. package/lib/provider-task-runner.js +113 -37
  124. package/lib/provider-transport.js +16 -0
  125. package/lib/render.js +11 -1
  126. package/lib/scheduling-policy.js +66 -0
  127. package/lib/tasks.js +3 -2
  128. package/lib/video-flow-input.js +16 -0
  129. package/lib/video-input.js +12 -5
  130. package/lib/vision-model-routing.js +2 -0
  131. package/lib/web/app.js +107 -46
  132. package/lib/web/connections.js +151 -12
  133. package/lib/web/creation.js +29 -25
  134. package/lib/web/flow-view.js +4 -3
  135. package/lib/web/gallery.js +17 -16
  136. package/lib/web/index.html +58 -5
  137. package/lib/web/media-client.js +18 -4
  138. package/lib/web/network.js +232 -0
  139. package/lib/web/plugin-panel.js +4 -2
  140. package/lib/web/plugin-status.js +140 -0
  141. package/lib/web/providers.js +34 -13
  142. package/lib/web/recovery.js +2 -2
  143. package/lib/web/styles.css +54 -1
  144. package/lib/web/transport.js +6 -1
  145. package/lib/workbench-artifacts.js +3 -2
  146. package/package.json +5 -2
  147. package/skills/iris-core/references/comfyui.md +1 -1
  148. package/templates/plugin-starter/LICENSE +21 -0
  149. package/templates/plugin-starter/README.md +74 -0
  150. package/templates/plugin-starter/assets/peach.png +0 -0
  151. package/templates/plugin-starter/assets/violet.png +0 -0
  152. package/templates/plugin-starter/index.mjs +27 -0
  153. package/templates/plugin-starter/package.json +18 -0
  154. package/templates/plugin-starter/scripts/core-cli.mjs +20 -0
  155. package/templates/plugin-starter/scripts/environment.mjs +65 -0
  156. package/templates/plugin-starter/scripts/mcp-probe.mjs +22 -0
  157. package/templates/plugin-starter/scripts/setup.mjs +42 -0
  158. package/templates/plugin-starter/scripts/smoke.mjs +76 -0
  159. package/templates/plugin-starter/web/board.css +27 -0
  160. package/templates/plugin-starter/web/board.mjs +160 -0
  161. package/templates/plugin-starter/web/png.mjs +23 -0
  162. package/templates/provider-starter/LICENSE +21 -0
  163. package/templates/provider-starter/README.md +50 -0
  164. package/templates/provider-starter/index.d.mts +5 -0
  165. package/templates/provider-starter/index.mjs +165 -0
  166. package/templates/provider-starter/package.json +13 -0
  167. package/templates/provider-starter/scripts/core-cli.mjs +22 -0
  168. package/templates/provider-starter/scripts/environment.mjs +122 -0
  169. package/templates/provider-starter/scripts/fixture.mjs +54 -0
  170. package/templates/provider-starter/scripts/mcp-probe.mjs +21 -0
  171. package/templates/provider-starter/scripts/protocol-smoke.mjs +48 -0
  172. package/templates/provider-starter/scripts/setup.mjs +44 -0
  173. package/templates/provider-starter/scripts/smoke.mjs +145 -0
  174. package/user_guide.md +3 -3
package/lib/api.js CHANGED
@@ -13,6 +13,7 @@
13
13
  import fs from 'node:fs';
14
14
  import path from 'node:path';
15
15
  import crypto from 'node:crypto';
16
+ import { pipeline } from 'node:stream/promises';
16
17
  import { irisHome, capabilitiesOf, onChange as onConfigChange, providerHealthSnapshot } from './config.js';
17
18
  import { mimeOf, mediaUrl } from './media.js';
18
19
  import * as tasks from './tasks.js';
@@ -65,7 +66,7 @@ function providerPublic(p) {
65
66
  mediaBaseUrl: p.mediaBaseUrl || '',
66
67
  enabled: p.enabled !== false,
67
68
  ...selectMediaProtocol(p),
68
- apiKeyHint: p.dshProvider ? '由 DSH 管理' : keyHint(p.apiKey),
69
+ apiKeyHint: p.dshProvider ? '由 DSH 管理' : p.apiKeyEnv ? '由环境变量提供' : keyHint(p.apiKey),
69
70
  ...(p.dshProvider ? { dshProvider: p.dshProvider } : {}),
70
71
  capabilities: capabilitiesOf(p),
71
72
  // 迁移/推断结果是否与原始声明不同(UI 提示旧配置已推断)
@@ -445,19 +446,22 @@ function handleWorkbenchPost(req, res, execute, download = false, core) {
445
446
 
446
447
  async function serveCoreArtifactMedia(artifactId, method, res, core) {
447
448
  try {
448
- const result = await core.readCoreArtifactMediaForDsh(artifactId);
449
- if (res.writableEnded || res.destroyed) return;
450
- res.writeHead(200, {
451
- 'Content-Type': result.artifact.mediaType,
452
- 'Content-Length': result.bytes.length,
453
- 'Content-Disposition': 'inline',
454
- 'Cache-Control': 'private, max-age=3600',
455
- 'X-Content-Type-Options': 'nosniff',
456
- 'Cross-Origin-Resource-Policy': 'same-origin',
457
- 'Referrer-Policy': 'no-referrer'
458
- });
459
- res.end(method === 'HEAD' ? undefined : result.bytes);
449
+ await core.streamCoreArtifactMediaForDsh(artifactId, async result => {
450
+ if (res.writableEnded || res.destroyed) return;
451
+ res.writeHead(200, {
452
+ 'Content-Type': result.artifact.mediaType,
453
+ 'Content-Length': result.artifact.size,
454
+ 'Content-Disposition': 'inline',
455
+ 'Cache-Control': 'private, max-age=3600',
456
+ 'X-Content-Type-Options': 'nosniff',
457
+ 'Cross-Origin-Resource-Policy': 'same-origin',
458
+ 'Referrer-Policy': 'no-referrer'
459
+ });
460
+ if (method === 'HEAD') res.end();
461
+ else await pipeline(result.stream, res);
462
+ }, { headOnly: method === 'HEAD' });
460
463
  } catch (_) {
464
+ if (res.headersSent || res.destroyed) { res.destroy(); return; }
461
465
  return sendJson(res, 404, { error: 'not found' });
462
466
  }
463
467
  }
@@ -567,10 +571,16 @@ function handleCoreTaskRetry(taskId, req, res, core) {
567
571
  error.statusCode = 400;
568
572
  throw error;
569
573
  }
574
+ if (body.video_mode !== undefined && ['img_data_url', 'first_frame_path', 'audio_path'].some(field => Object.hasOwn(body, field))) {
575
+ throw Object.assign(new Error('DSH 视频重试仅提供明确的文生入口;图生/数字人视频请通过视频工具或 CLI 重新提供首帧和音频'), {
576
+ code: 'IRIS_COMMAND_INPUT_INVALID'
577
+ });
578
+ }
570
579
  const prompt = String(body.prompt || '');
571
580
  const result = await core.retryProviderTaskForDsh({
572
581
  taskId,
573
582
  prompt,
583
+ ...(body.video_mode !== undefined ? { videoMode: body.video_mode } : {}),
574
584
  ...(body.source_artifact_id !== undefined ? { sourceArtifactId: body.source_artifact_id } : {}),
575
585
  ...(body.text !== undefined ? { text: String(body.text) } : {}),
576
586
  ...(body.voice !== undefined ? { voice: String(body.voice) } : {}),
@@ -10,6 +10,9 @@ import { createLocalCreationService } from './local-creation.js';
10
10
  import { createLocalOperationService } from './local-operations.js';
11
11
  import { reservePluginOperation } from './plugin-operations.js';
12
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';
13
16
 
14
17
  // Internal first-slice execution surface, not a public API or an HTTP allowlist.
15
18
  const COMMAND_MODES = Object.freeze({
@@ -19,7 +22,7 @@ const COMMAND_MODES = Object.freeze({
19
22
  'flow.init': 'writer', 'flow.create': 'writer', 'flow.continue': 'writer', 'flow.select': 'writer', 'flow.export': 'writer',
20
23
  'flow.inspect': 'reader', 'flow.list': 'reader',
21
24
  'creation.init': 'writer', 'creation.image': 'writer', 'creation.flow': 'writer', 'creation.generate': 'writer',
22
- 'creation.audio': 'writer', 'creation.speech': 'writer', 'creation.transcribe': 'writer',
25
+ 'creation.audio': 'writer', 'creation.speech': 'writer', 'creation.transcribe': 'writer', 'creation.video': 'writer',
23
26
  'creation.root': 'reader', 'creation.lookup': 'reader',
24
27
  'execution.init': 'writer', 'operation.accept': 'writer', 'operation.transition': 'writer',
25
28
  'operation.inspect': 'reader', 'operation.lookup': 'reader', 'operation.list': 'reader'
@@ -38,7 +41,8 @@ function closed() {
38
41
  }
39
42
 
40
43
  /** Instance-owned FIFO and operation-scoped Core leases. No I/O until an explicit operation. */
41
- export function createApplicationRuntime({ dataRoot, resolveImageCandidates, resolveAudioCandidates, resolveTaskAdapter } = {}) {
44
+ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, resolveAudioCandidates, resolveVideoCandidates, resolveTaskAdapter,
45
+ scheduling, scheduler: suppliedScheduler } = {}) {
42
46
  const root = normalizeCoreOptions({ dataRoot }).dataRoot;
43
47
  if (resolveImageCandidates !== undefined && typeof resolveImageCandidates !== 'function') {
44
48
  throw new TypeError('resolveImageCandidates 必须是显式候选解析函数');
@@ -47,22 +51,29 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
47
51
  throw new TypeError('resolveTaskAdapter 必须是显式 Task Provider 解析函数');
48
52
  }
49
53
  if (resolveAudioCandidates !== undefined && typeof resolveAudioCandidates !== 'function') throw new TypeError('音频候选解析须为函数');
54
+ if (resolveVideoCandidates !== undefined && typeof resolveVideoCandidates !== 'function') throw new TypeError('视频候选解析须为函数');
50
55
  let state = 'open';
51
56
  const queue = [];
52
57
  const active = new Set();
53
58
  let writer = null;
54
59
  let disposePromise;
55
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;
56
65
 
57
66
  function snapshot() {
58
67
  return Object.freeze({ state, queuedWrites: queue.length, activeWrites: Number(Boolean(writer)),
59
- 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 } : {}) });
60
70
  }
61
71
 
62
72
  function detach(job) { job.signal?.removeEventListener('abort', job.onAbort); }
63
73
 
64
74
  function rejectQueued(job, error) {
65
75
  detach(job);
76
+ job.resource.emitDestroy();
66
77
  job.reject(error);
67
78
  }
68
79
 
@@ -114,7 +125,7 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
114
125
 
115
126
  function start(job) {
116
127
  active.add(job);
117
- job.done = Promise.resolve().then(async () => {
128
+ job.done = job.resource.runInAsyncScope(() => Promise.resolve().then(async () => {
118
129
  let result, error, failed = false;
119
130
  try { result = await perform(job); }
120
131
  catch (caught) { failed = true; error = caught; }
@@ -122,8 +133,9 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
122
133
  active.delete(job);
123
134
  if (writer === job) writer = null;
124
135
  if (failed) job.reject(error); else job.resolve(result);
136
+ job.resource.emitDestroy();
125
137
  dispatch();
126
- });
138
+ }));
127
139
  }
128
140
 
129
141
  function dispatch() {
@@ -132,12 +144,19 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
132
144
  start(writer);
133
145
  }
134
146
 
135
- function schedule(mode, action, { signal } = {}) {
147
+ function schedule(mode, action, { signal, managed = false, transaction = false, finalize = false, key } = {}) {
136
148
  if (signal !== undefined && !(signal instanceof AbortSignal)) throw new TypeError('signal 必须是 AbortSignal');
137
149
  if (state !== 'open') return Promise.reject(closed());
138
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
+ }
139
157
  let job;
140
- 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') }; });
141
160
  job.onAbort = () => {
142
161
  const index = queue.indexOf(job);
143
162
  if (index >= 0) { queue.splice(index, 1); rejectQueued(job, aborted()); }
@@ -163,16 +182,19 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
163
182
  const ownedInput = structuredClone(input);
164
183
  return schedule(COMMAND_MODES[name], runtime => {
165
184
  const ports = resolveTaskAdapter ? { resolveTaskAdapter: async task => {
166
- 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());
167
187
  if (runtime.signal.aborted) throw aborted();
168
188
  return adapter;
169
189
  } } : {};
170
190
  if (name === 'execution.init' || name.startsWith('operation.')) return createLocalOperationService(runtime).execute(name, ownedInput);
171
- if (name.startsWith('creation.')) return createLocalCreationService(runtime, { resolveImageCandidates, resolveAudioCandidates,
191
+ if (name.startsWith('creation.')) return createLocalCreationService(runtime, { resolveImageCandidates, resolveAudioCandidates, resolveVideoCandidates,
172
192
  onPrepared: options?.onPrepared }).execute(name, ownedInput);
173
- if (name.startsWith('flow.')) return createImageFlowService(runtime, { ...ports, resolveImageCandidates, resolveAudioCandidates }).execute(name, ownedInput);
193
+ if (name.startsWith('flow.')) return createImageFlowService(runtime, { ...ports, resolveImageCandidates, resolveAudioCandidates, resolveVideoCandidates }).execute(name, ownedInput);
174
194
  return createCommandService(runtime, ports).execute(name, ownedInput);
175
- }, 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 });
176
198
  }
177
199
 
178
200
  async function generateImage(input, options) {
@@ -181,11 +203,12 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
181
203
  throw new ApplicationRuntimeError('IRIS_APP_IMAGE_RESOLVER_MISSING', '图片生成需要显式提供候选解析函数');
182
204
  }
183
205
  return schedule('writer', async runtime => {
184
- 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());
185
208
  if (runtime.signal.aborted) throw aborted();
186
209
  return createProviderTaskRunner(runtime).submit({ capability: 'image', candidates,
187
210
  providerInput: normalized.providerInput, sourceArtifactId: normalized.sourceArtifactId });
188
- }, options);
211
+ }, { ...options, managed: true });
189
212
  }
190
213
 
191
214
  /** Explicit trusted assembly only. No HTTP/MCP route or persistent Operation action is added. */
@@ -201,7 +224,10 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
201
224
  if (disposePromise) return disposePromise;
202
225
  state = 'closing';
203
226
  stopAll();
204
- 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();
205
231
  if (cleanupFailure) throw cleanupFailure;
206
232
  state = 'closed';
207
233
  return snapshot();
@@ -209,5 +235,8 @@ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, res
209
235
  return disposePromise;
210
236
  }
211
237
 
212
- return Object.freeze({ execute, generateImage, executePluginOperation, 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 }); } });
213
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
+ }
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
+ }
package/lib/client.js CHANGED
@@ -21,7 +21,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
21
21
  var exports = module.exports;
22
22
 
23
23
  var React = require('react');
24
- var IRIS_CLIENT_VERSION = '0.3.0-alpha.1';
24
+ var IRIS_CLIENT_VERSION = '0.3.0-beta.1';
25
25
  var HOST_CLIENT_PROTOCOL_VERSION = 0;
26
26
  var reportedSeats = [];
27
27
 
@@ -567,6 +567,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
567
567
  for (var i = 0; i < sharedListeners.length; i++) {
568
568
  sharedListeners[i](data);
569
569
  }
570
+ window.dispatchEvent(new CustomEvent('iris-core-refresh-tick'));
570
571
  }
571
572
  function load() {
572
573
  fetch('/iris/api/state')
@@ -574,7 +575,6 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
574
575
  .then(function (data) {
575
576
  if (data) {
576
577
  apply(data);
577
- window.dispatchEvent(new CustomEvent('iris-core-refresh-tick'));
578
578
  }
579
579
  })
580
580
  .catch(function () { /* 网络抖动静默 */ });
@@ -1623,9 +1623,9 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1623
1623
  p.protocolInferred ? React.createElement('div', { className: 'iris-pm-note' }, '协议为推断值,请确认') : null,
1624
1624
  p.mediaProtocol === 'comfyui' ? React.createElement('details', {}, React.createElement('summary', {}, '连接设置'),
1625
1625
  React.createElement('div', { className: 'iris-pm-note' }, '服务地址:' + (p.mediaBaseUrl || p.baseUrl)),
1626
- React.createElement(ProviderEndpointEditor, { provider: p, onNote: setNote, onDone: refresh }),
1626
+ !p.dshProvider ? React.createElement(ProviderEndpointEditor, { provider: p, onNote: setNote, onDone: refresh }) : null,
1627
1627
  !p.dshProvider ? React.createElement(ProviderAuthenticationEditor, { provider: p, onDone: refresh }) : null)
1628
- : React.createElement(React.Fragment, {}, React.createElement(ProviderEndpointEditor, { provider: p, onNote: setNote, onDone: refresh }),
1628
+ : React.createElement(React.Fragment, {}, !p.dshProvider ? React.createElement(ProviderEndpointEditor, { provider: p, onNote: setNote, onDone: refresh }) : null,
1629
1629
  !p.dshProvider ? React.createElement(ProviderAuthenticationEditor, { provider: p, onDone: refresh }) : null,
1630
1630
  React.createElement(VisionInputEditor, { provider: p, onNote: setNote, onDone: refresh })),
1631
1631
  React.createElement('div', { className: 'iris-pm-field' }, React.createElement('label', {}, '媒体协议'), React.createElement('select', { value: p.protocolInferred ? 'auto' : (p.mediaProtocol || 'auto'), onChange: function (e) { setProtocol(p, e.target.value); } },
@@ -2676,8 +2676,11 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
2676
2676
  .then(function (value) {
2677
2677
  if (request.current !== controller || controller.signal.aborted) return;
2678
2678
  setData(value);
2679
- setNote(value.coreError ? 'Core 作品暂时不可用,当前计数仅含可读取作品。'
2680
- : value.droppedCore ? '已跳过 ' + value.droppedCore + ' 条不可读取的 Core 作品。' : '');
2679
+ var notes = [];
2680
+ if (value.coreError) notes.push('Core 作品暂时不可用,当前计数仅含可读取作品。');
2681
+ if (value.droppedCore) notes.push('已跳过 ' + value.droppedCore + ' 条损坏、缺失或不支持格式的 Core 作品。');
2682
+ if (value.legacyCoreSkipped) notes.push(value.legacyCoreSkipped + ' 条早期 Core 作品尚未编入当前目录;可按 ID 读取,或显式重建索引后显示。');
2683
+ setNote(notes.join(' '));
2681
2684
  })
2682
2685
  .catch(function (error) { if (request.current === controller && !controller.signal.aborted) setNote(error.message); })
2683
2686
  .finally(function () { if (request.current === controller) { request.current = null; setBusy(false); } });
@@ -2868,33 +2871,46 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
2868
2871
  var EMPTY_CORE_USER = Object.freeze({ rows: [], available: false, degraded: false, droppedTasks: 0 });
2869
2872
 
2870
2873
  function useCoreUserTasks() {
2871
- var pair = React.useState(EMPTY_CORE_USER);
2874
+ var store = useCoreUserTasks.shared;
2875
+ if (!store) store = useCoreUserTasks.shared = { data: EMPTY_CORE_USER, listeners: [], sequence: 0, refresh: null };
2876
+ var pair = React.useState(store.data);
2872
2877
  var setData = pair[1];
2873
2878
  React.useEffect(function () {
2874
- var alive = true, sequence = 0;
2879
+ function listener(data) { setData(data); }
2880
+ store.listeners.push(listener);
2881
+ setData(store.data);
2875
2882
  function load() {
2876
- var current = ++sequence;
2883
+ var current = ++store.sequence;
2877
2884
  fetch('/iris/api/core/snapshot?limit=200')
2878
2885
  .then(function (res) { return res.ok ? res.json() : null; })
2879
2886
  .then(function (data) {
2880
- if (!alive || current !== sequence || !data || !Array.isArray(data.userTasks)) return;
2881
- setData({
2887
+ if (!store.listeners.length || current !== store.sequence || !data || !Array.isArray(data.userTasks)) return;
2888
+ store.data = {
2882
2889
  rows: data.userTasks,
2890
+ activity: data.activity,
2883
2891
  available: !!data.available,
2884
2892
  degraded: !!data.degraded,
2885
2893
  droppedTasks: Number(data.droppedTasks || 0)
2886
- });
2894
+ };
2895
+ store.listeners.forEach(function (notify) { notify(store.data); });
2887
2896
  })
2888
2897
  .catch(function () { /* Core 不可用不阻塞 legacy 任务区 */ });
2889
2898
  }
2890
- function changed() { load(); }
2891
- load();
2892
- window.addEventListener('iris-core-artifacts-changed', changed);
2893
- window.addEventListener('iris-core-refresh-tick', changed);
2899
+ if (store.listeners.length === 1) {
2900
+ store.refresh = load;
2901
+ load();
2902
+ window.addEventListener('iris-core-artifacts-changed', load);
2903
+ window.addEventListener('iris-core-refresh-tick', load);
2904
+ }
2894
2905
  return function () {
2895
- alive = false;
2896
- window.removeEventListener('iris-core-artifacts-changed', changed);
2897
- window.removeEventListener('iris-core-refresh-tick', changed);
2906
+ var index = store.listeners.indexOf(listener);
2907
+ if (index >= 0) store.listeners.splice(index, 1);
2908
+ if (!store.listeners.length) {
2909
+ store.sequence++;
2910
+ window.removeEventListener('iris-core-artifacts-changed', store.refresh);
2911
+ window.removeEventListener('iris-core-refresh-tick', store.refresh);
2912
+ store.refresh = null; store.data = EMPTY_CORE_USER;
2913
+ }
2898
2914
  };
2899
2915
  }, []);
2900
2916
  return pair[0];
@@ -3018,11 +3034,16 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
3018
3034
  if (!props.row || props.row.retryable !== true) return null;
3019
3035
  return React.createElement(CoreTaskManualButton, {
3020
3036
  id: props.row.id, action: 'retry',
3021
- idleLabel: '重试为新任务', busyLabel: '创建中…', failPrefix: '重试',
3037
+ idleLabel: props.row.capability === 'video' ? '重新创建文生视频' : '重试为新任务', busyLabel: '创建中…', failPrefix: '重试',
3022
3038
  title: '创建一个新任务重新生成,可能产生重复费用;生成指令需要重新输入',
3023
3039
  confirm: '将创建一个新任务并可能产生重复生成费用,确定继续?',
3024
3040
  createdPrefix: '已创建新任务:',
3025
3041
  buildBody: function () {
3042
+ var videoMode;
3043
+ if (props.row.capability === 'video') {
3044
+ videoMode = window.prompt('旧视频 Task 不保存模式、首帧和音频。本按钮仅创建新的文生视频;图生视频或数字人视频请取消,并通过视频工具或 CLI 重新提供首帧/音频。\n如明确选择文生视频,请输入 t2v:');
3045
+ if (videoMode === null || String(videoMode).trim() !== 't2v') return null;
3046
+ }
3026
3047
  var isTts = props.row.capability === 'tts';
3027
3048
  var isTranscribe = props.row.capability === 'transcribe';
3028
3049
  var draft = window.prompt(isTts
@@ -3040,7 +3061,8 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
3040
3061
  }
3041
3062
  return isTts ? { text: draft, confirmBilling: true }
3042
3063
  : isTranscribe ? { audio_url: draft, confirmBilling: true }
3043
- : { prompt: draft, confirmBilling: true, ...(sourceArtifactId ? { source_artifact_id: String(sourceArtifactId).trim() } : {}) };
3064
+ : { prompt: draft, confirmBilling: true, ...(videoMode ? { video_mode: 't2v' } : {}),
3065
+ ...(sourceArtifactId ? { source_artifact_id: String(sourceArtifactId).trim() } : {}) };
3044
3066
  }
3045
3067
  });
3046
3068
  }
@@ -4245,13 +4267,21 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
4245
4267
  function ProgressDock() {
4246
4268
  var state = useIrisState(5000);
4247
4269
  var running = (state && state.tasks && state.tasks.running) || [];
4248
- if (!running.length) return null;
4270
+ var coreUser = useCoreUserTasks();
4271
+ var coreRows = coreUser.activity && coreUser.activity.recent || [];
4272
+ if (!running.length && !coreRows.length) return null;
4249
4273
  return React.createElement('div', { className: 'iris-progress-dock' },
4250
4274
  running.map(function (t) {
4251
4275
  return React.createElement('div', { key: t.id, className: 'iris-progress-row' },
4252
4276
  React.createElement('span', {}, '🫧 ' + t.cap),
4253
4277
  React.createElement('span', {}, t.model || ''),
4254
4278
  React.createElement('span', {}, String(t.progress || '')));
4279
+ }),
4280
+ coreRows.map(function (row) {
4281
+ return React.createElement('div', { key: 'core:' + row.id, className: 'iris-progress-row' },
4282
+ React.createElement('span', {}, 'Core · ' + row.capability),
4283
+ React.createElement('span', {}, row.model),
4284
+ React.createElement('span', {}, row.label));
4255
4285
  }));
4256
4286
  }
4257
4287
 
@@ -4280,6 +4310,8 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
4280
4310
 
4281
4311
  function FloatingBubble() {
4282
4312
  var state = useIrisState(5000);
4313
+ var coreUser = useCoreUserTasks();
4314
+ var activity = coreUser.activity;
4283
4315
  var providers = (state && state.providers) || [];
4284
4316
  var fallbackConfigured = providers.some(function (p) { return p.enabled && p.apiKeyHint; });
4285
4317
  var overallState = state && state.health && HEALTH_META[state.health.overall]
@@ -4386,9 +4418,14 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
4386
4418
  style = { right: DEFAULT_X + 'px', bottom: DEFAULT_Y + 'px' };
4387
4419
  }
4388
4420
 
4389
- var badgeCount = running.length + attention.length;
4390
- var badge = badgeCount
4391
- ? React.createElement('span', { className: 'iris-bubble-badge', title: attention.length ? attention.length + ' 个任务需要处理' : '' }, badgeCount)
4421
+ var coreCount = activity ? activity.running + activity.paused + activity.attention : 0;
4422
+ var badgeCount = running.length + attention.length + coreCount;
4423
+ var badgeTitle = '旧任务:' + running.length + ' 运行中,' + attention.length + ' 待处理';
4424
+ if (activity) badgeTitle += ';Core:' + activity.running + ' 运行中,' + activity.paused + ' 观察暂停,' + activity.attention + ' 待处理'
4425
+ + (activity.truncated ? '(部分较早的 Core 记录尚未统计)' : '');
4426
+ var badge = badgeCount || activity && activity.truncated
4427
+ ? React.createElement('span', { className: 'iris-bubble-badge', title: badgeTitle, 'aria-label': badgeTitle },
4428
+ badgeCount ? String(badgeCount) + (activity && activity.truncated ? '+' : '') : '…')
4392
4429
  : null;
4393
4430
 
4394
4431
  var panel = null;
@@ -6,6 +6,7 @@ import { parseImageSize } from './provider-protocol.js';
6
6
  import { createComfyUiClient, comfyFail as fail } from './comfyui-client.js';
7
7
  import { inspectComfyWorkflow } from './comfyui-inspection.js';
8
8
  import { writePrivateFile } from './private-storage.js';
9
+ import { inheritedProviderFetch } from './provider-network.js';
9
10
 
10
11
  const TASK = /^[A-Za-z0-9_-]{1,200}$/;
11
12
  function imageLocation(value) {
@@ -16,8 +17,8 @@ function imageLocation(value) {
16
17
  }
17
18
  return { filename: value.filename, subfolder: value.subfolder, type: value.type };
18
19
  }
19
- export function createComfyUiProviderAdapter(provider, { fetchPort = globalThis.fetch } = {}) {
20
- const client = createComfyUiClient(provider, { fetchPort }), { route, request } = client;
20
+ export function createComfyUiProviderAdapter(provider, { fetchPort = inheritedProviderFetch, network } = {}) {
21
+ const client = createComfyUiClient(provider, { fetchPort, network }), { route, request } = client;
21
22
  function workflow(model) {
22
23
  const value = model ? provider.models?.find(entry => entry.id === model)?.comfyWorkflow : provider.comfyWorkflow;
23
24
  try { return checkedComfyWorkflow(value); }