@mokuyoaxis/dsh-iris 0.2.6 → 0.3.0-alpha.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 (177) hide show
  1. package/CHANGELOG.md +103 -1
  2. package/README.en.md +104 -304
  3. package/README.md +101 -302
  4. package/bin/dsh-iris.js +446 -64
  5. package/docs/AGENT_CORE_QUICKSTART.md +64 -0
  6. package/docs/AGENT_QUICKSTART.md +7 -5
  7. package/docs/ARCHITECTURE.md +20 -4
  8. package/docs/AUDIO_FLOWS.md +71 -0
  9. package/docs/CLI_MANAGEMENT.md +1 -1
  10. package/docs/CONTACT_SHEET.md +62 -0
  11. package/docs/CORE_CONNECTIONS.md +99 -0
  12. package/docs/CORE_QUICKSTART.md +108 -0
  13. package/docs/CORE_RUNTIME_CONTRACT.md +41 -5
  14. package/docs/CORE_SERVICE.md +97 -0
  15. package/docs/CORE_SKILLS.md +94 -0
  16. package/docs/DSH_COMPATIBILITY.md +14 -4
  17. package/docs/DSH_CORE_MIGRATION.md +4 -2
  18. package/docs/DSH_PROVIDER_CONNECTIONS.md +1 -1
  19. package/docs/DSH_RC2_ADAPTATION.md +1 -1
  20. package/docs/DSH_RUNTIME.md +40 -0
  21. package/docs/FUNCTION_MODEL_POOLS.md +1 -1
  22. package/docs/HEADLESS_CLI.md +12 -4
  23. package/docs/HOST_DOCTOR.md +13 -1
  24. package/docs/IMAGE_FLOW.md +75 -0
  25. package/docs/IMAGE_GENERATION.md +127 -0
  26. package/docs/LOCAL_API.md +62 -0
  27. package/docs/LOCAL_API_CONTRACT.md +140 -0
  28. package/docs/LOCAL_CREATION.md +56 -0
  29. package/docs/LOCAL_OPERATIONS.md +46 -0
  30. package/docs/LOCAL_SELECTION.md +57 -0
  31. package/docs/LOCAL_WEB.md +201 -0
  32. package/docs/MCP.md +191 -0
  33. package/docs/MUSIC.md +2 -0
  34. package/docs/PANEL_EXTENSIONS.md +64 -0
  35. package/docs/PLUGINS.md +189 -0
  36. package/docs/PLUGIN_STARTUP.md +100 -0
  37. package/docs/PROVIDER_ADAPTER_CONTRACT.md +1 -1
  38. package/docs/PROVIDER_EXTENSIONS.md +78 -0
  39. package/docs/PROVIDER_HEALTH.md +4 -2
  40. package/docs/PROVIDER_SETTINGS.md +85 -0
  41. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +6 -0
  42. package/docs/ROADMAP.md +28 -6
  43. package/docs/SECURITY.md +9 -1
  44. package/docs/SYNC_DELIVERY_RECOVERY.md +2 -0
  45. package/docs/VISION_CLI.md +2 -2
  46. package/docs/WEB_PROTOTYPE.md +62 -0
  47. package/docs/WEB_RECOVERY.md +46 -0
  48. package/docs/WORKBENCH_LOGIN.md +63 -0
  49. package/docs/assets/screenshots/core-connections-preview.webp +0 -0
  50. package/docs/assets/screenshots/core-model-pools-preview.webp +0 -0
  51. package/docs/assets/screenshots/core-overview-preview.webp +0 -0
  52. package/docs/assets/screenshots/core-spectrum-preview.webp +0 -0
  53. package/docs/assets/screenshots/core-studio-dark-preview.webp +0 -0
  54. package/docs/assets/screenshots/core-studio-mobile-preview.webp +0 -0
  55. package/docs/assets/screenshots/core-studio-preview.webp +0 -0
  56. package/docs/releases/0.2.7.md +62 -0
  57. package/docs/releases/0.3.0-alpha.1.md +51 -0
  58. package/docs/screenshots.md +41 -1
  59. package/lib/actions.js +132 -70
  60. package/lib/adapters.js +20 -11
  61. package/lib/api.js +38 -38
  62. package/lib/application-runtime.js +213 -0
  63. package/lib/audio-flow-input.js +37 -0
  64. package/lib/client.js +434 -55
  65. package/lib/comfyui-adapter.js +94 -0
  66. package/lib/comfyui-client.js +51 -0
  67. package/lib/comfyui-inspection.js +73 -0
  68. package/lib/comfyui-workflow.js +94 -0
  69. package/lib/config.js +36 -1
  70. package/lib/configured-image-runtime.js +50 -0
  71. package/lib/connection-skill.js +89 -0
  72. package/lib/connection-template.js +23 -0
  73. package/lib/core-artifact-store.js +21 -2
  74. package/lib/core-bridge-selection.js +31 -0
  75. package/lib/core-bridge-tools.js +83 -0
  76. package/lib/core-capability-catalog.js +182 -0
  77. package/lib/core-connection-diagnostics.js +49 -0
  78. package/lib/core-connection-store.js +277 -0
  79. package/lib/core-connections.js +225 -0
  80. package/lib/core-maintenance.js +31 -2
  81. package/lib/core-runtime.js +20 -0
  82. package/lib/core-service-client.js +170 -0
  83. package/lib/core-tasks.js +84 -13
  84. package/lib/core-writer-lease.js +16 -0
  85. package/lib/creation-request-store.js +133 -0
  86. package/lib/doctor.js +12 -1
  87. package/lib/dsh-core-adapter.js +666 -572
  88. package/lib/dsh-host-adapter.js +8 -2
  89. package/lib/dsh-service-bridge.js +66 -0
  90. package/lib/execution-ownership.js +43 -0
  91. package/lib/flow-store.js +290 -0
  92. package/lib/generation-input.js +10 -1
  93. package/lib/headless-vision.js +7 -7
  94. package/lib/image-edit-input.js +1 -1
  95. package/lib/image-flow.js +232 -0
  96. package/lib/index.js +94 -68
  97. package/lib/local-api-projection.js +32 -0
  98. package/lib/local-api-token-directory.js +31 -0
  99. package/lib/local-api.js +1083 -0
  100. package/lib/local-creation.js +150 -0
  101. package/lib/local-operations.js +36 -0
  102. package/lib/local-request-root.js +86 -0
  103. package/lib/local-web-assets.js +37 -0
  104. package/lib/mcp-bridge.js +58 -0
  105. package/lib/mcp-check.js +26 -0
  106. package/lib/mcp-config.js +42 -0
  107. package/lib/mcp-delivery.js +46 -0
  108. package/lib/mcp-http.js +101 -0
  109. package/lib/mcp-server.js +47 -0
  110. package/lib/model-directory-service.js +16 -6
  111. package/lib/model-directory.js +3 -3
  112. package/lib/models.js +2 -2
  113. package/lib/music-transport.js +2 -1
  114. package/lib/operation-runner.js +236 -0
  115. package/lib/operation-store.js +216 -0
  116. package/lib/panel-extension.js +36 -0
  117. package/lib/plugin-contract.js +194 -0
  118. package/lib/plugin-execution.js +51 -0
  119. package/lib/plugin-launch.js +86 -0
  120. package/lib/plugin-operation-store.js +101 -0
  121. package/lib/plugin-operations.js +28 -0
  122. package/lib/plugin-registry.js +109 -0
  123. package/lib/plugin-selection.js +15 -0
  124. package/lib/plugin-source-image.js +5 -0
  125. package/lib/plugin-web-assets.js +54 -0
  126. package/lib/plugins.d.ts +288 -0
  127. package/lib/plugins.js +58 -0
  128. package/lib/private-storage.js +12 -0
  129. package/lib/provider-adapter.js +5 -1
  130. package/lib/provider-adapters.js +56 -18
  131. package/lib/provider-catalog.js +40 -29
  132. package/lib/provider-config-service.js +105 -23
  133. package/lib/provider-diagnostics.js +56 -0
  134. package/lib/provider-extensions.js +29 -0
  135. package/lib/provider-health.js +8 -1
  136. package/lib/provider-monitor.js +79 -0
  137. package/lib/provider-protocol.js +70 -20
  138. package/lib/provider-registry.js +118 -0
  139. package/lib/provider-response.js +28 -0
  140. package/lib/provider-settings.js +134 -0
  141. package/lib/provider-task-runner.js +40 -7
  142. package/lib/render-address.js +35 -0
  143. package/lib/render.js +52 -13
  144. package/lib/skill-installation.js +211 -0
  145. package/lib/vision-model-routing.js +3 -0
  146. package/lib/web/app.js +642 -0
  147. package/lib/web/appearance.js +111 -0
  148. package/lib/web/comfyui.js +146 -0
  149. package/lib/web/connections.js +377 -0
  150. package/lib/web/creation.js +355 -0
  151. package/lib/web/flow-view.js +33 -0
  152. package/lib/web/gallery.js +132 -0
  153. package/lib/web/index.html +291 -0
  154. package/lib/web/media-client.js +204 -0
  155. package/lib/web/plugin-panel.js +221 -0
  156. package/lib/web/plugin-surface.css +17 -0
  157. package/lib/web/providers.js +492 -0
  158. package/lib/web/recovery.js +105 -0
  159. package/lib/web/startup.js +18 -0
  160. package/lib/web/styles.css +518 -0
  161. package/lib/web/transport.js +96 -0
  162. package/lib/web/workbench-login.js +165 -0
  163. package/lib/workbench-artifacts.js +1 -2
  164. package/lib/workbench-auth-cli.js +47 -0
  165. package/lib/workbench-auth.js +200 -0
  166. package/lib/workbench-management.js +8 -9
  167. package/package.json +22 -6
  168. package/plugins/contact-sheet/index.d.mts +4 -0
  169. package/plugins/contact-sheet/index.mjs +33 -0
  170. package/plugins/contact-sheet/web/board.css +14 -0
  171. package/plugins/contact-sheet/web/board.mjs +93 -0
  172. package/skills/iris-core/SKILL.md +44 -0
  173. package/skills/iris-core/agents/openai.yaml +4 -0
  174. package/skills/iris-core/references/cli.md +95 -0
  175. package/skills/iris-core/references/comfyui.md +69 -0
  176. package/skills/iris-core/references/tasks.md +60 -0
  177. package/user_guide.md +19 -9
package/lib/adapters.js CHANGED
@@ -15,6 +15,7 @@ import { dashscopeApiBase, imageSizeViolation, isDashScopeBaseUrl, normalizeImag
15
15
  import { chmodPrivateFile, ensurePrivateDir, privateSibling } from './private-storage.js';
16
16
  import { isFreeQuotaExhausted, retryAfterDelay } from './provider-health.js';
17
17
  import { ProviderContractError } from './provider-contract.js';
18
+ import { boundedProviderBody, readProviderText, PROVIDER_RESPONSE_LIMITS } from './provider-response.js';
18
19
  export { generateMusic } from './music-transport.js';
19
20
 
20
21
  /**
@@ -42,8 +43,8 @@ function authHeaders(key) {
42
43
  };
43
44
  }
44
45
 
45
- async function readJson(res) {
46
- const text = await res.text();
46
+ async function readJson(res, maxBytes = PROVIDER_RESPONSE_LIMITS.json) {
47
+ const text = await readProviderText(res, { maxBytes });
47
48
  try {
48
49
  return text ? JSON.parse(text) : null;
49
50
  } catch (_) {
@@ -149,8 +150,9 @@ async function fetchImageJson(url, options) {
149
150
  let response;
150
151
  try {
151
152
  response = await fetch(url, options);
152
- return { res: response, body: await readJson(response) };
153
+ return { res: response, body: await readJson(response, response.ok ? PROVIDER_RESPONSE_LIMITS.mediaJson : PROVIDER_RESPONSE_LIMITS.json) };
153
154
  } catch (err) {
155
+ if (err instanceof ProviderContractError) throw err;
154
156
  throw transportFetchError(err, response);
155
157
  }
156
158
  }
@@ -287,7 +289,11 @@ export async function submitVideo({ key, baseUrl, model, prompt, imgDataUrl, siz
287
289
  * 返回 oss:// 形式 URL;调用模型时请求头须带 X-DashScope-OssResourceResolve: enable
288
290
  * (submitVideo 的 s2v 分支已自动处理)。
289
291
  */
290
- export async function uploadTempFile({ key, baseUrl, model, filePath, timeoutMs = T_UPLOAD, signal }) {
292
+ export async function uploadTempFile({ key, baseUrl, model, filePath, bytes, filename, timeoutMs = T_UPLOAD, signal }) {
293
+ if ((filePath === undefined) === (bytes === undefined)) throw new TypeError('上传须且只能提供本地路径或明确字节');
294
+ const name = filePath !== undefined ? path.basename(filePath) : filename;
295
+ if (typeof name !== 'string' || !name || name.length > 128 || /[\/\\\0]/.test(name)
296
+ || bytes !== undefined && (!(bytes instanceof Uint8Array) || !bytes.length)) throw new TypeError('上传字节或文件名无效');
291
297
  const apiBase = dashscopeApiBase(baseUrl);
292
298
  const u = new URL(`${apiBase}/uploads`);
293
299
  u.searchParams.set('action', 'getPolicy');
@@ -296,7 +302,6 @@ export async function uploadTempFile({ key, baseUrl, model, filePath, timeoutMs
296
302
  const pbody = await readJson(pres);
297
303
  if (!pres.ok || !pbody || !pbody.data) throw providerError(pres.status, pbody, '获取上传凭证失败', pres.headers);
298
304
  const pol = pbody.data;
299
- const name = path.basename(filePath);
300
305
  const keyPath = `${pol.upload_dir}/${Date.now()}-${name}`;
301
306
  const fd = new FormData();
302
307
  fd.append('OSSAccessKeyId', pol.oss_access_key_id);
@@ -306,7 +311,7 @@ export async function uploadTempFile({ key, baseUrl, model, filePath, timeoutMs
306
311
  fd.append('x-oss-forbid-overwrite', pol.x_oss_forbid_overwrite);
307
312
  fd.append('key', keyPath);
308
313
  fd.append('success_action_status', '200');
309
- fd.append('file', new Blob([fs.readFileSync(filePath)]), name);
314
+ fd.append('file', new Blob([bytes ?? fs.readFileSync(filePath)]), name);
310
315
  const up = await fetch(pol.upload_host, { method: 'POST', body: fd, signal: timeoutSignal(timeoutMs, signal) });
311
316
  if (up.status !== 200) throw new Error(`临时上传失败:HTTP ${up.status}`);
312
317
  return 'oss://' + keyPath;
@@ -366,7 +371,7 @@ export async function synthesizeTts({ key, baseUrl, model = 'qwen-tts-latest', t
366
371
  signal: timeoutSignal(timeoutMs, signal),
367
372
  body: JSON.stringify({ model, input: { text, voice } })
368
373
  });
369
- const body = await readJson(res);
374
+ const body = await readJson(res, res.ok ? PROVIDER_RESPONSE_LIMITS.mediaJson : PROVIDER_RESPONSE_LIMITS.json);
370
375
  if (!res.ok) {
371
376
  const error = providerError(res.status, body, '合成失败', res.headers);
372
377
  if (res.status === 404 && (/NotFound\.Model|model_not_found/i.test(error.providerCode)
@@ -614,7 +619,7 @@ export async function visionStream({ key, baseUrl, model = 'qwen-vl-plus', promp
614
619
  throw networkError(err); // 网络层失败归类为 network
615
620
  }
616
621
  if (!res.ok) {
617
- const text = await res.text().catch(() => '');
622
+ const text = await readProviderText(res, { maxBytes: PROVIDER_RESPONSE_LIMITS.json, stage: 'response', acceptance: 'unknown' });
618
623
  let body = null;
619
624
  try { body = JSON.parse(text); } catch (_) { /* keep null */ }
620
625
  throw providerError(res.status, body, '视觉理解失败', res.headers);
@@ -655,15 +660,19 @@ export function extFromUrl(url, fallback = 'bin') {
655
660
  return fallback;
656
661
  }
657
662
 
658
- export async function downloadTo(url, filePath, { timeoutMs = T_DOWNLOAD, signal } = {}) {
663
+ export async function downloadTo(url, filePath, { timeoutMs = T_DOWNLOAD, signal, maxBytes = PROVIDER_RESPONSE_LIMITS.video } = {}) {
659
664
  const res = await fetch(url, { signal: timeoutSignal(timeoutMs, signal) });
660
- if (!res.ok) throw new Error(`下载结果失败:HTTP ${res.status}`);
665
+ if (!res.ok) {
666
+ await res.body?.cancel();
667
+ throw Object.assign(new Error(`下载结果失败:HTTP ${res.status}`), { stage: 'download', category: 'provider', httpStatus: res.status, acceptance: 'accepted' });
668
+ }
661
669
  if (!res.body) throw new Error('下载结果失败:响应没有内容');
662
670
  ensurePrivateDir(path.dirname(filePath));
663
671
  const tmp = privateSibling(filePath, 'download');
664
672
  try {
665
673
  const output = fs.createWriteStream(tmp, { flags: 'wx', mode: 0o600 });
666
- await pipeline(Readable.fromWeb(res.body), output);
674
+ const closed = new Promise(resolve => output.once('close', resolve));
675
+ await pipeline(Readable.from(boundedProviderBody(res, { maxBytes, stage: 'download', acceptance: 'accepted' })), output).finally(() => closed);
667
676
  const bytes = fs.statSync(tmp).size;
668
677
  fs.renameSync(tmp, filePath);
669
678
  chmodPrivateFile(filePath);
package/lib/api.js CHANGED
@@ -26,7 +26,6 @@ import { selectMediaProtocol } from './provider-protocol.js';
26
26
  import { deriveUserState } from './task-semantics.js';
27
27
  import { hostDoctor } from './doctor.js';
28
28
  import { hostRuntimeEvidence, recordHostClient } from './host-runtime.js';
29
- import { cancelProviderTaskForDsh, coreSnapshotForDsh, disposeCoreTaskAttention, readCoreArtifactMediaForDsh, redeliverProviderTaskForDsh, reobserveProviderTaskForDsh, retryProviderTaskForDsh } from './dsh-core-adapter.js';
30
29
  import { optimizePrompt } from './prompt-optimizer.js';
31
30
  import { PROMPT_OPTIMIZER_CAPABILITIES } from './prompt-optimizer-core.js';
32
31
  import {
@@ -332,16 +331,17 @@ function serveSse(res) {
332
331
  }
333
332
 
334
333
  /** webServer 前缀路由 handler:状态、任务、动作与提示词优化器 API。 */
335
- export function serveApi(req, res, host) {
334
+ export function serveApi(req, res, host, core) {
335
+ if (!core || core.snapshot().state !== 'open') return sendJson(res, 503, { error: 'Iris DSH 实例正在关闭或不可用', code: 'IRIS_DSH_RUNTIME_CLOSED' });
336
336
  try {
337
337
  const method = String(req.method || 'GET').toUpperCase();
338
338
  const urlPath = String(req.url || '').split('?')[0];
339
339
 
340
340
  // POST /iris/api/actions/:name —— GUI 直连触发工具执行
341
341
  if (method === 'POST') {
342
- if (urlPath === '/iris/api/works/download') return handleWorkbenchPost(req, res, downloadWorkbenchSelection, true);
343
- if (urlPath === '/iris/api/works/delete') return handleWorkbenchPost(req, res, deleteWorkbenchArtifacts);
344
- if (urlPath === '/iris/api/works/restore') return handleWorkbenchPost(req, res, restoreWorkbenchTransaction);
342
+ if (urlPath === '/iris/api/works/download') return handleWorkbenchPost(req, res, downloadWorkbenchSelection, true, core);
343
+ if (urlPath === '/iris/api/works/delete') return handleWorkbenchPost(req, res, deleteWorkbenchArtifacts, false, core);
344
+ if (urlPath === '/iris/api/works/restore') return handleWorkbenchPost(req, res, restoreWorkbenchTransaction, false, core);
345
345
  if (urlPath === '/iris/api/host-client') {
346
346
  if (!host || typeof host !== 'object') return sendJson(res, 503, { error: 'host unavailable' });
347
347
  return handleJsonPost(req, res, (body) => recordHostClient(body));
@@ -352,16 +352,16 @@ export function serveApi(req, res, host) {
352
352
  }
353
353
  // D4 人工 retry as new task:产生新的真实计费,请求体必须显式 confirmBilling:true
354
354
  const coreTaskRetry = urlPath.match(/^\/iris\/api\/core\/task\/([^/]+)\/retry\/?$/);
355
- if (coreTaskRetry) return handleCoreTaskRetry(coreTaskRetry[1], req, res);
355
+ if (coreTaskRetry) return handleCoreTaskRetry(coreTaskRetry[1], req, res, core);
356
356
  // D1/D2/D3 人工动作:reobserve 显式单步 poll 一次;redeliver 只对 succeeded+failed
357
357
  // 重新取回产物;cancel 只有 Provider 明确确认才写已取消。三者绝不 submit。
358
358
  const coreTaskAction = urlPath.match(/^\/iris\/api\/core\/task\/([^/]+)\/(reobserve|redeliver|cancel|acknowledge|restore|hide|unhide)\/?$/);
359
- if (coreTaskAction) return handleCoreTaskManual(coreTaskAction[2], coreTaskAction[1], req, res);
359
+ if (coreTaskAction) return handleCoreTaskManual(coreTaskAction[2], coreTaskAction[1], req, res, core);
360
360
  const promptMatch = typeof host === 'object' && urlPath.match(/^\/iris\/api\/prompt-optimizer\/(optimize|import|reset|enabled)\/?$/);
361
- if (promptMatch) return handlePromptOptimizer(host, promptMatch[1], req, res);
361
+ if (promptMatch) return handlePromptOptimizer(host, promptMatch[1], req, res, core);
362
362
  const actionMatch = typeof host === 'object' && urlPath.match(/^\/iris\/api\/actions\/([A-Za-z0-9_-]+)\/?$/);
363
363
  if (!actionMatch) { return sendJson(res, 405, { error: 'method not allowed' }); }
364
- return handleAction(host, actionMatch[1], req, res);
364
+ return handleAction(host, actionMatch[1], req, res, core);
365
365
  }
366
366
  // GET/HEAD 原逻辑
367
367
  if (method !== 'GET' && method !== 'HEAD') {
@@ -382,16 +382,16 @@ export function serveApi(req, res, host) {
382
382
  if (urlPath === '/iris/api/core/snapshot') {
383
383
  if (method === 'HEAD') return sendJson(res, 200, {});
384
384
  const query = new URL(req.url || '', 'http://iris.local').searchParams;
385
- return serveCoreSnapshot(res, { limit: Number(query.get('limit') || 12) });
385
+ return serveCoreSnapshot(res, { limit: Number(query.get('limit') || 12) }, core);
386
386
  }
387
387
  if (urlPath === '/iris/api/works') {
388
- return serveWorkbenchArtifacts(req, method, res);
388
+ return serveWorkbenchArtifacts(req, method, res, core);
389
389
  }
390
- if (urlPath === '/iris/api/works/transactions') return serveWorkbenchRead(res, method, () => workbenchTransactions());
390
+ if (urlPath === '/iris/api/works/transactions') return serveWorkbenchRead(res, method, () => workbenchTransactions({ core }));
391
391
  const workDetailMatch = urlPath.match(/^\/iris\/api\/works\/(core|legacy)\/([^/]+)$/);
392
- if (workDetailMatch) return serveWorkbenchRead(res, method, () => workbenchDetail(workDetailMatch[1], workDetailMatch[2]));
392
+ if (workDetailMatch) return serveWorkbenchRead(res, method, () => workbenchDetail(workDetailMatch[1], workDetailMatch[2], { core }));
393
393
  const coreMediaMatch = urlPath.match(/^\/iris\/api\/core\/artifact\/(artifact_[a-f0-9]{24})\/media$/);
394
- if (coreMediaMatch) return serveCoreArtifactMedia(coreMediaMatch[1], method, res);
394
+ if (coreMediaMatch) return serveCoreArtifactMedia(coreMediaMatch[1], method, res, core);
395
395
  if (urlPath === '/iris/api/artifacts') {
396
396
  const query = new URL(req.url || '', 'http://iris.local').searchParams;
397
397
  const page = artifacts.page({ offset: Number(query.get('offset') || 0), limit: Number(query.get('limit') || 24) });
@@ -417,11 +417,11 @@ export function serveApi(req, res, host) {
417
417
  }
418
418
  }
419
419
 
420
- async function serveWorkbenchArtifacts(req, method, res) {
420
+ async function serveWorkbenchArtifacts(req, method, res, core) {
421
421
  const params = new URL(req.url || '', 'http://iris.local').searchParams;
422
422
  const input = Object.fromEntries(params);
423
423
  for (const key of ['offset', 'limit']) if (input[key] !== undefined) input[key] = Number(input[key]);
424
- try { return sendJson(res, 200, method === 'HEAD' ? {} : await workbenchArtifacts(input)); }
424
+ try { return sendJson(res, 200, method === 'HEAD' ? {} : await workbenchArtifacts(input, { core })); }
425
425
  catch (error) { return sendJson(res, error.statusCode || 500, { error: '作品读取失败', code: error.code || 'IRIS_GALLERY_FAILED' }); }
426
426
  }
427
427
 
@@ -430,9 +430,9 @@ async function serveWorkbenchRead(res, method, execute) {
430
430
  catch (error) { const safe = workbenchError(error); return sendJson(res, safe.statusCode || 400, { error: safe.message, code: safe.code }); }
431
431
  }
432
432
 
433
- function handleWorkbenchPost(req, res, execute, download = false) {
433
+ function handleWorkbenchPost(req, res, execute, download = false, core) {
434
434
  return handleJsonPost(req, res, async (body, signal) => {
435
- try { return await execute(body, { signal }); }
435
+ try { return await core.runHostOperation(operationSignal => execute(body, { signal: operationSignal, core }), { signal }); }
436
436
  catch (error) { throw workbenchError(error); }
437
437
  }, download ? (response, _status, bytes) => {
438
438
  if (response.writableEnded || response.destroyed) return;
@@ -443,9 +443,9 @@ function handleWorkbenchPost(req, res, execute, download = false) {
443
443
  } : sendJson);
444
444
  }
445
445
 
446
- async function serveCoreArtifactMedia(artifactId, method, res) {
446
+ async function serveCoreArtifactMedia(artifactId, method, res, core) {
447
447
  try {
448
- const result = await readCoreArtifactMediaForDsh(artifactId);
448
+ const result = await core.readCoreArtifactMediaForDsh(artifactId);
449
449
  if (res.writableEnded || res.destroyed) return;
450
450
  res.writeHead(200, {
451
451
  'Content-Type': result.artifact.mediaType,
@@ -462,9 +462,9 @@ async function serveCoreArtifactMedia(artifactId, method, res) {
462
462
  }
463
463
  }
464
464
 
465
- async function serveCoreSnapshot(res, options = {}) {
465
+ async function serveCoreSnapshot(res, options = {}, core) {
466
466
  try {
467
- return sendJson(res, 200, await coreSnapshotForDsh({ limit: options.limit || 12 }));
467
+ return sendJson(res, 200, await core.coreSnapshotForDsh({ limit: options.limit || 12 }));
468
468
  } catch (error) {
469
469
  console.error('[iris] Core 只读快照失败:', error && error.code || 'unknown');
470
470
  return sendJson(res, 409, {
@@ -527,8 +527,8 @@ function handleJsonPost(req, res, execute, respond = sendJson) {
527
527
  req.on('error', () => finish(400, { error: 'bad request' }));
528
528
  }
529
529
 
530
- function handleAction(host, name, req, res) {
531
- return handleJsonPost(req, res, (args, signal) => runAction(host, name, args, { signal }));
530
+ function handleAction(host, name, req, res, core) {
531
+ return handleJsonPost(req, res, (args, signal) => runAction(host, name, args, { signal, core }));
532
532
  }
533
533
 
534
534
  /**
@@ -539,14 +539,14 @@ function handleAction(host, name, req, res) {
539
539
  * 经 AbortSignal 传入在途操作,与 GUI actions 同一约定。
540
540
  */
541
541
  const CORE_TASK_MANUAL_ACTIONS = Object.freeze({
542
- reobserve: { command: 'task.reobserve', run: reobserveProviderTaskForDsh },
543
- redeliver: { command: 'task.redeliver', run: redeliverProviderTaskForDsh },
544
- cancel: { command: 'task.cancel', run: cancelProviderTaskForDsh },
542
+ reobserve: { command: 'task.reobserve', run: (core, input) => core.reobserveProviderTaskForDsh(input) },
543
+ redeliver: { command: 'task.redeliver', run: (core, input) => core.redeliverProviderTaskForDsh(input) },
544
+ cancel: { command: 'task.cancel', run: (core, input) => core.cancelProviderTaskForDsh(input) },
545
545
  // 注意力处置是 Host 偏好(不写 Core 事实):acknowledge/restore/hide/unhide。
546
- acknowledge: { command: 'attention.acknowledge', run: (input) => disposeCoreTaskAttention({ ...input, action: 'acknowledge' }) },
547
- restore: { command: 'attention.restore', run: (input) => disposeCoreTaskAttention({ ...input, action: 'restore' }) },
548
- hide: { command: 'attention.hide', run: (input) => disposeCoreTaskAttention({ ...input, action: 'hide' }) },
549
- unhide: { command: 'attention.unhide', run: (input) => disposeCoreTaskAttention({ ...input, action: 'unhide' }) }
546
+ acknowledge: { command: 'attention.acknowledge', run: (core, input) => core.disposeCoreTaskAttention({ ...input, action: 'acknowledge' }) },
547
+ restore: { command: 'attention.restore', run: (core, input) => core.disposeCoreTaskAttention({ ...input, action: 'restore' }) },
548
+ hide: { command: 'attention.hide', run: (core, input) => core.disposeCoreTaskAttention({ ...input, action: 'hide' }) },
549
+ unhide: { command: 'attention.unhide', run: (core, input) => core.disposeCoreTaskAttention({ ...input, action: 'unhide' }) }
550
550
  });
551
551
 
552
552
  /**
@@ -555,7 +555,7 @@ const CORE_TASK_MANUAL_ACTIONS = Object.freeze({
555
555
  * Prompt,只能由调用方重新提供;缺显式计费确认在读取旧 Task 之前拒绝。
556
556
  * 成功返回 200 + 新任务的五类用户投影行与 retriedFrom 关系。
557
557
  */
558
- function handleCoreTaskRetry(taskId, req, res) {
558
+ function handleCoreTaskRetry(taskId, req, res, core) {
559
559
  if (!/^task_[a-f0-9]{24}$/.test(taskId)) {
560
560
  return sendJson(res, 400, { error: { code: 'IRIS_DSH_TASK_INVALID', message: 'Core Task ID 无效' } });
561
561
  }
@@ -568,7 +568,7 @@ function handleCoreTaskRetry(taskId, req, res) {
568
568
  throw error;
569
569
  }
570
570
  const prompt = String(body.prompt || '');
571
- const result = await retryProviderTaskForDsh({
571
+ const result = await core.retryProviderTaskForDsh({
572
572
  taskId,
573
573
  prompt,
574
574
  ...(body.source_artifact_id !== undefined ? { sourceArtifactId: body.source_artifact_id } : {}),
@@ -597,7 +597,7 @@ function handleCoreTaskRetry(taskId, req, res) {
597
597
  });
598
598
  }
599
599
 
600
- function handleCoreTaskManual(action, taskId, req, res) {
600
+ function handleCoreTaskManual(action, taskId, req, res, core) {
601
601
  if (!/^task_[a-f0-9]{24}$/.test(taskId)) {
602
602
  return sendJson(res, 400, { error: { code: 'IRIS_DSH_TASK_INVALID', message: 'Core Task ID 无效' } });
603
603
  }
@@ -614,7 +614,7 @@ function handleCoreTaskManual(action, taskId, req, res) {
614
614
  if (typeof req.on === 'function') req.on('aborted', disconnect);
615
615
  if (typeof res.on === 'function') res.on('close', disconnect);
616
616
  if (typeof req.resume === 'function') req.resume(); // 无 body 入参;避免小请求体滞留
617
- entry.run({ taskId, signal: controller.signal })
617
+ entry.run(core, { taskId, signal: controller.signal })
618
618
  .then((result) => finish(200, {
619
619
  ok: true,
620
620
  contractVersion: 0,
@@ -631,8 +631,8 @@ function handleCoreTaskManual(action, taskId, req, res) {
631
631
  });
632
632
  }
633
633
 
634
- function handlePromptOptimizer(host, operation, req, res) {
635
- return handleJsonPost(req, res, async (body, signal) => {
634
+ function handlePromptOptimizer(host, operation, req, res, core) {
635
+ return handleJsonPost(req, res, (body, requestSignal) => core.runHostOperation(async signal => {
636
636
  if (operation === 'optimize') return optimizePrompt(host, body, { signal });
637
637
  if (operation === 'import') {
638
638
  const imported = body && body.config !== undefined ? body.config : body;
@@ -645,7 +645,7 @@ function handlePromptOptimizer(host, operation, req, res) {
645
645
  }
646
646
  if (operation === 'enabled') return { ok: true, ...setPromptOptimizerEnabled(body && body.enabled), capabilities: PROMPT_CONFIG_CAPABILITIES };
647
647
  throw new Error('unknown prompt optimizer operation');
648
- });
648
+ }, { signal: requestSignal }));
649
649
  }
650
650
 
651
651
  /* ---------- 文件选择器 L2:本地文件上传(阶段 10) ---------- */
@@ -0,0 +1,213 @@
1
+ 'use strict';
2
+
3
+ import { normalizeCoreOptions } from './core-contract.js';
4
+ import { createCoreRuntime, inspectCoreWriterLease } from './core-runtime.js';
5
+ import { createCommandService } from './command-service.js';
6
+ import { normalizeGenerationInput } from './generation-input.js';
7
+ import { createProviderTaskRunner } from './provider-task-runner.js';
8
+ import { createImageFlowService } from './image-flow.js';
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
+
14
+ // Internal first-slice execution surface, not a public API or an HTTP allowlist.
15
+ const COMMAND_MODES = Object.freeze({
16
+ 'artifact.import': 'writer', 'artifact.export': 'writer', 'media.diff': 'writer',
17
+ 'task.observe': 'writer', 'task.reobserve': 'writer', 'task.redeliver': 'writer', 'task.cancel': 'writer',
18
+ 'artifact.inspect': 'reader', 'artifact.list': 'reader', 'task.inspect': 'reader', 'task.list': 'reader',
19
+ 'flow.init': 'writer', 'flow.create': 'writer', 'flow.continue': 'writer', 'flow.select': 'writer', 'flow.export': 'writer',
20
+ 'flow.inspect': 'reader', 'flow.list': 'reader',
21
+ 'creation.init': 'writer', 'creation.image': 'writer', 'creation.flow': 'writer', 'creation.generate': 'writer',
22
+ 'creation.audio': 'writer', 'creation.speech': 'writer', 'creation.transcribe': 'writer',
23
+ 'creation.root': 'reader', 'creation.lookup': 'reader',
24
+ 'execution.init': 'writer', 'operation.accept': 'writer', 'operation.transition': 'writer',
25
+ 'operation.inspect': 'reader', 'operation.lookup': 'reader', 'operation.list': 'reader'
26
+ });
27
+
28
+ export class ApplicationRuntimeError extends Error {
29
+ constructor(code, message) { super(message); this.name = 'ApplicationRuntimeError'; this.code = code; }
30
+ }
31
+
32
+ function aborted() {
33
+ return Object.assign(new ApplicationRuntimeError('IRIS_APP_ABORTED', 'Iris 操作已取消'), { name: 'AbortError' });
34
+ }
35
+
36
+ function closed() {
37
+ return new ApplicationRuntimeError('IRIS_APP_CLOSED', 'Iris 应用运行时正在关闭或已关闭');
38
+ }
39
+
40
+ /** Instance-owned FIFO and operation-scoped Core leases. No I/O until an explicit operation. */
41
+ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, resolveAudioCandidates, resolveTaskAdapter } = {}) {
42
+ const root = normalizeCoreOptions({ dataRoot }).dataRoot;
43
+ if (resolveImageCandidates !== undefined && typeof resolveImageCandidates !== 'function') {
44
+ throw new TypeError('resolveImageCandidates 必须是显式候选解析函数');
45
+ }
46
+ if (resolveTaskAdapter !== undefined && typeof resolveTaskAdapter !== 'function') {
47
+ throw new TypeError('resolveTaskAdapter 必须是显式 Task Provider 解析函数');
48
+ }
49
+ if (resolveAudioCandidates !== undefined && typeof resolveAudioCandidates !== 'function') throw new TypeError('音频候选解析须为函数');
50
+ let state = 'open';
51
+ const queue = [];
52
+ const active = new Set();
53
+ let writer = null;
54
+ let disposePromise;
55
+ let cleanupFailure;
56
+
57
+ function snapshot() {
58
+ return Object.freeze({ state, queuedWrites: queue.length, activeWrites: Number(Boolean(writer)),
59
+ activeReaders: active.size - Number(Boolean(writer)), cleanupFailed: Boolean(cleanupFailure) });
60
+ }
61
+
62
+ function detach(job) { job.signal?.removeEventListener('abort', job.onAbort); }
63
+
64
+ function rejectQueued(job, error) {
65
+ detach(job);
66
+ job.reject(error);
67
+ }
68
+
69
+ function stop(job) {
70
+ job.aborted = true;
71
+ if (job.runtime && !job.disposal) {
72
+ job.disposal = job.runtime.dispose();
73
+ // The operation awaits this same promise below and reports any cleanup error.
74
+ job.disposal.catch(() => {});
75
+ }
76
+ }
77
+
78
+ function stopAll() {
79
+ for (const job of queue.splice(0)) rejectQueued(job, closed());
80
+ for (const job of active) stop(job);
81
+ }
82
+
83
+ async function perform(job) {
84
+ if (job.aborted) throw aborted();
85
+ const runtime = createCoreRuntime({ dataRoot: root, mode: job.mode });
86
+ job.runtime = runtime;
87
+ let result, operationError, failed = false;
88
+ try {
89
+ runtime.start();
90
+ result = await job.action(runtime);
91
+ } catch (error) {
92
+ failed = true; operationError = error;
93
+ if (job.mode === 'reader' && error?.code === 'IRIS_ARTIFACT_INDEX_INVALID' && error.missingFile === true) {
94
+ const lease = inspectCoreWriterLease(root);
95
+ if (lease.status === 'present' && lease.ownerStatus === 'alive') {
96
+ operationError = new ApplicationRuntimeError('IRIS_CORE_DATA_ROOT_BUSY', '作品读取遇到活动写入,请在写入结束后显式重试');
97
+ Object.defineProperty(operationError, 'cause', { value: error });
98
+ }
99
+ }
100
+ }
101
+ try { await (job.disposal || runtime.dispose()); }
102
+ catch (error) {
103
+ cleanupFailure ||= error;
104
+ state = 'closing';
105
+ stopAll();
106
+ if (failed) {
107
+ throw Object.assign(new AggregateError([operationError, error], 'Iris 操作失败且运行时未能安全释放'), { code: error.code });
108
+ }
109
+ throw error;
110
+ }
111
+ if (failed) throw operationError;
112
+ return result;
113
+ }
114
+
115
+ function start(job) {
116
+ active.add(job);
117
+ job.done = Promise.resolve().then(async () => {
118
+ let result, error, failed = false;
119
+ try { result = await perform(job); }
120
+ catch (caught) { failed = true; error = caught; }
121
+ detach(job);
122
+ active.delete(job);
123
+ if (writer === job) writer = null;
124
+ if (failed) job.reject(error); else job.resolve(result);
125
+ dispatch();
126
+ });
127
+ }
128
+
129
+ function dispatch() {
130
+ if (state !== 'open' || writer || !queue.length) return;
131
+ writer = queue.shift();
132
+ start(writer);
133
+ }
134
+
135
+ function schedule(mode, action, { signal } = {}) {
136
+ if (signal !== undefined && !(signal instanceof AbortSignal)) throw new TypeError('signal 必须是 AbortSignal');
137
+ if (state !== 'open') return Promise.reject(closed());
138
+ if (signal?.aborted) return Promise.reject(aborted());
139
+ let job;
140
+ const result = new Promise((resolve, reject) => { job = { mode, action, signal, resolve, reject, aborted: false }; });
141
+ job.onAbort = () => {
142
+ const index = queue.indexOf(job);
143
+ if (index >= 0) { queue.splice(index, 1); rejectQueued(job, aborted()); }
144
+ else stop(job);
145
+ };
146
+ signal?.addEventListener('abort', job.onAbort, { once: true });
147
+ if (mode === 'writer') { queue.push(job); dispatch(); }
148
+ else start(job);
149
+ return result;
150
+ }
151
+
152
+ /** Trusted in-process Host composition; never an external command/HTTP dispatch surface. */
153
+ function run(mode, action, options) {
154
+ if (!['reader', 'writer'].includes(mode)) throw new TypeError('Host 操作需要明确 reader/writer 模式');
155
+ if (typeof action !== 'function') throw new TypeError('Host 操作必须是函数');
156
+ return schedule(mode, action, options);
157
+ }
158
+
159
+ async function execute(name, input, options) {
160
+ if (!Object.hasOwn(COMMAND_MODES, name)) {
161
+ throw new ApplicationRuntimeError('IRIS_APP_COMMAND_UNSUPPORTED', '应用运行时尚未装配该 Command');
162
+ }
163
+ const ownedInput = structuredClone(input);
164
+ return schedule(COMMAND_MODES[name], runtime => {
165
+ const ports = resolveTaskAdapter ? { resolveTaskAdapter: async task => {
166
+ const adapter = await resolveTaskAdapter(task, { signal: runtime.signal });
167
+ if (runtime.signal.aborted) throw aborted();
168
+ return adapter;
169
+ } } : {};
170
+ if (name === 'execution.init' || name.startsWith('operation.')) return createLocalOperationService(runtime).execute(name, ownedInput);
171
+ if (name.startsWith('creation.')) return createLocalCreationService(runtime, { resolveImageCandidates, resolveAudioCandidates,
172
+ onPrepared: options?.onPrepared }).execute(name, ownedInput);
173
+ if (name.startsWith('flow.')) return createImageFlowService(runtime, { ...ports, resolveImageCandidates, resolveAudioCandidates }).execute(name, ownedInput);
174
+ return createCommandService(runtime, ports).execute(name, ownedInput);
175
+ }, options);
176
+ }
177
+
178
+ async function generateImage(input, options) {
179
+ const normalized = normalizeGenerationInput('image', input);
180
+ if (!resolveImageCandidates) {
181
+ throw new ApplicationRuntimeError('IRIS_APP_IMAGE_RESOLVER_MISSING', '图片生成需要显式提供候选解析函数');
182
+ }
183
+ return schedule('writer', async runtime => {
184
+ const candidates = await resolveImageCandidates({ modelRef: normalized.modelRef, signal: runtime.signal });
185
+ if (runtime.signal.aborted) throw aborted();
186
+ return createProviderTaskRunner(runtime).submit({ capability: 'image', candidates,
187
+ providerInput: normalized.providerInput, sourceArtifactId: normalized.sourceArtifactId });
188
+ }, options);
189
+ }
190
+
191
+ /** Explicit trusted assembly only. No HTTP/MCP route or persistent Operation action is added. */
192
+ async function executePluginOperation(registry, id, input, { allowedOperations = [], dependency, signal } = {}) {
193
+ const reserved = reservePluginOperation(registry, id, input, allowedOperations, dependency);
194
+ try {
195
+ return await schedule(reserved.mode, runtime => runtime.run(reserved.mode === 'reader' ? 'inspect' : 'execute',
196
+ () => invokeReservedPlugin(runtime, reserved)), { signal });
197
+ } finally { reserved.release(); }
198
+ }
199
+
200
+ function dispose() {
201
+ if (disposePromise) return disposePromise;
202
+ state = 'closing';
203
+ stopAll();
204
+ disposePromise = Promise.all([...active].map(job => job.done)).then(() => {
205
+ if (cleanupFailure) throw cleanupFailure;
206
+ state = 'closed';
207
+ return snapshot();
208
+ });
209
+ return disposePromise;
210
+ }
211
+
212
+ return Object.freeze({ execute, generateImage, executePluginOperation, run, dispose, snapshot });
213
+ }
@@ -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
+ }