@mokuyoaxis/dsh-iris 0.2.5 → 0.2.7

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 (83) hide show
  1. package/.dsh/skills/iris-use/SKILL.md +2 -2
  2. package/.dsh/skills/iris-use/references/media-contract.md +2 -2
  3. package/CHANGELOG.md +49 -1
  4. package/README.en.md +39 -22
  5. package/README.md +40 -21
  6. package/bin/dsh-iris.js +166 -35
  7. package/docs/AGENT_QUICKSTART.md +7 -7
  8. package/docs/ARCHITECTURE.md +9 -3
  9. package/docs/CLI_MANAGEMENT.md +5 -1
  10. package/docs/CORE_RUNTIME_CONTRACT.md +36 -5
  11. package/docs/CORE_SKILLS.md +62 -0
  12. package/docs/DSH_COMPATIBILITY.md +8 -4
  13. package/docs/DSH_CORE_MIGRATION.md +4 -2
  14. package/docs/DSH_PROVIDER_CONNECTIONS.md +34 -0
  15. package/docs/DSH_RUNTIME.md +36 -0
  16. package/docs/FUNCTION_MODEL_ASSIGNMENTS.md +2 -0
  17. package/docs/FUNCTION_MODEL_POOLS.md +84 -0
  18. package/docs/HEADLESS_CLI.md +16 -8
  19. package/docs/HOST_ADAPTER_CONTRACT.md +7 -2
  20. package/docs/HOST_DOCTOR.md +14 -2
  21. package/docs/IMAGE_FLOW.md +71 -0
  22. package/docs/LOCAL_API.md +60 -0
  23. package/docs/LOCAL_API_CONTRACT.md +115 -0
  24. package/docs/LOCAL_CREATION.md +50 -0
  25. package/docs/MUSIC.md +2 -2
  26. package/docs/PROVIDER_HEALTH.md +26 -2
  27. package/docs/ROADMAP.md +18 -4
  28. package/docs/SECURITY.md +20 -2
  29. package/docs/SYNC_DELIVERY_RECOVERY.md +2 -0
  30. package/docs/VISION_CLI.md +2 -2
  31. package/docs/WEB_PROTOTYPE.md +47 -0
  32. package/docs/releases/0.2.6.md +50 -0
  33. package/docs/releases/0.2.7.md +62 -0
  34. package/lib/actions.js +208 -90
  35. package/lib/adapters.js +36 -9
  36. package/lib/api.js +40 -39
  37. package/lib/application-runtime.js +186 -0
  38. package/lib/client.js +805 -182
  39. package/lib/composite-vision-routing.js +6 -6
  40. package/lib/config.js +140 -29
  41. package/lib/core-artifact-store.js +5 -0
  42. package/lib/core-maintenance.js +25 -2
  43. package/lib/core-runtime.js +19 -0
  44. package/lib/core-tasks.js +62 -8
  45. package/lib/creation-request-store.js +159 -0
  46. package/lib/doctor.js +20 -2
  47. package/lib/dsh-core-adapter.js +669 -564
  48. package/lib/dsh-host-adapter.js +14 -1
  49. package/lib/dsh-provider-config.js +35 -0
  50. package/lib/dsh-provider-source.js +102 -0
  51. package/lib/flow-store.js +195 -0
  52. package/lib/headless-vision.js +5 -5
  53. package/lib/host-contract.js +1 -0
  54. package/lib/http-vision-model-adapter.js +5 -2
  55. package/lib/image-flow.js +188 -0
  56. package/lib/index.js +103 -78
  57. package/lib/local-api-projection.js +27 -0
  58. package/lib/local-api.js +381 -0
  59. package/lib/local-creation.js +85 -0
  60. package/lib/model-directory-service.js +53 -0
  61. package/lib/model-directory.js +103 -0
  62. package/lib/models.js +71 -6
  63. package/lib/ocr-model-routing.js +2 -2
  64. package/lib/private-storage.js +12 -0
  65. package/lib/provider-adapters.js +42 -9
  66. package/lib/provider-catalog.js +14 -9
  67. package/lib/provider-config-scope.js +16 -0
  68. package/lib/provider-config-service.js +82 -13
  69. package/lib/provider-health.js +33 -0
  70. package/lib/provider-protocol.js +30 -0
  71. package/lib/provider-task-runner.js +22 -2
  72. package/lib/render-address.js +35 -0
  73. package/lib/render.js +52 -13
  74. package/lib/skill-installation.js +199 -0
  75. package/lib/vision-model-routing.js +25 -12
  76. package/lib/workbench-artifacts.js +1 -2
  77. package/lib/workbench-management.js +8 -9
  78. package/package.json +4 -3
  79. package/skills/iris-core/SKILL.md +40 -0
  80. package/skills/iris-core/agents/openai.yaml +4 -0
  81. package/skills/iris-core/references/cli.md +95 -0
  82. package/skills/iris-core/references/tasks.md +58 -0
  83. package/user_guide.md +32 -20
package/lib/adapters.js CHANGED
@@ -367,7 +367,15 @@ export async function synthesizeTts({ key, baseUrl, model = 'qwen-tts-latest', t
367
367
  body: JSON.stringify({ model, input: { text, voice } })
368
368
  });
369
369
  const body = await readJson(res);
370
- if (!res.ok) throw providerError(res.status, body, '合成失败', res.headers);
370
+ if (!res.ok) {
371
+ const error = providerError(res.status, body, '合成失败', res.headers);
372
+ if (res.status === 404 && (/NotFound\.Model|model_not_found/i.test(error.providerCode)
373
+ || /model (?:not exist|can not be found|.*does not exist)/i.test(String(error.rootCause)))) {
374
+ error.category = 'invalid_parameter';
375
+ error.message = `当前语音接口找不到模型 ${model}(${new URL(apiBase).hostname});请核对模型 ID、所属地域及调用协议。上游:${error.rootCause}`;
376
+ }
377
+ throw error;
378
+ }
371
379
  const audio = body && body.output && body.output.audio;
372
380
  const url = audio && (audio.url || audio.audio_url);
373
381
  const b64 = audio && typeof audio.data === 'string' ? audio.data : null;
@@ -510,6 +518,7 @@ export async function listModels({ key, baseUrl, timeoutMs = T_SUBMIT, signal })
510
518
  const found = new Map();
511
519
  const maxPages = dashscope ? 50 : 1;
512
520
  let loaded = 0;
521
+ let expectedTotal = null;
513
522
  for (let page = 1; page <= maxPages; page++) {
514
523
  const url = new URL(endpoint);
515
524
  if (dashscope) {
@@ -519,14 +528,24 @@ export async function listModels({ key, baseUrl, timeoutMs = T_SUBMIT, signal })
519
528
  const res = await fetch(url, { headers: authHeaders(key), signal: timeoutSignal(timeoutMs, signal) });
520
529
  const body = await readJson(res);
521
530
  if (!res.ok) throw providerError(res.status, body, '模型发现失败', res.headers);
531
+ if (!body || body.success === false) throw new ProviderContractError('模型目录响应无效,已保留上次完整目录', {
532
+ stage: 'validate', category: 'protocol', acceptance: 'not_accepted'
533
+ });
522
534
  const output = body && body.output;
523
535
  const items = (output && Array.isArray(output.models) && output.models)
524
536
  || (body && Array.isArray(body.data) && body.data)
525
- || (Array.isArray(body && body.models) ? body.models : []);
537
+ || (Array.isArray(body && body.models) ? body.models : null);
538
+ if (!items) throw new ProviderContractError('模型目录缺少模型数组,已保留上次完整目录', {
539
+ stage: 'validate', category: 'protocol', acceptance: 'not_accepted'
540
+ });
526
541
  loaded += items.length;
527
542
  for (const item of items) {
528
- const id = String(typeof item === 'string' ? item : item?.id || item?.model || '').trim();
529
- if (!id || found.has(id)) continue;
543
+ const rawId = typeof item === 'string' ? item : item?.id || item?.model;
544
+ if (typeof rawId !== 'string' || !rawId.trim()) throw new ProviderContractError('模型目录包含无效模型 ID,已保留上次完整目录', {
545
+ stage: 'validate', category: 'protocol', acceptance: 'not_accepted'
546
+ });
547
+ const id = rawId.trim();
548
+ if (found.has(id)) continue;
530
549
  const capabilities = Array.isArray(item?.capabilities)
531
550
  ? item.capabilities.filter((value) => typeof value === 'string' && value)
532
551
  : [];
@@ -538,17 +557,25 @@ export async function listModels({ key, baseUrl, timeoutMs = T_SUBMIT, signal })
538
557
  if (!Array.isArray(item?.capabilities)) {
539
558
  if (responseModalities.includes('image')) capabilities.push('IG');
540
559
  if (responseModalities.includes('video')) capabilities.push('VG');
541
- if (requestModalities.includes('audio') && responseModalities.includes('text')) capabilities.push('ASR');
542
- if (requestModalities.includes('text') && responseModalities.includes('audio')) capabilities.push('TTS');
560
+ // 音频理解不证明支持文件转写;混合视频/图像/文本输出也不证明独立TTS。
561
+ if (requestModalities.length === 1 && requestModalities[0] === 'text'
562
+ && responseModalities.length === 1 && responseModalities[0] === 'audio') capabilities.push('TTS');
543
563
  if (requestModalities.includes('image') && responseModalities.includes('text')) capabilities.push('VU');
544
564
  }
545
565
  const normalizedCapabilities = [...new Set(capabilities)];
546
566
  found.set(id, normalizedCapabilities.length ? { id, capabilities: normalizedCapabilities } : id);
547
567
  }
548
568
  if (!dashscope) break;
549
- const statedTotal = Number(output?.total);
550
- const total = Number.isFinite(statedTotal) && statedTotal >= 0 ? statedTotal : null;
551
- if (!items.length || (total !== null && loaded >= total) || items.length < 100) break;
569
+ const total = output?.total === undefined ? expectedTotal : Number(output.total);
570
+ if (total !== null && (!Number.isSafeInteger(total) || total < 0
571
+ || (expectedTotal !== null && total !== expectedTotal) || loaded > total || (!items.length && loaded < total)
572
+ || (loaded === total && found.size !== loaded))) {
573
+ throw new ProviderContractError('模型目录分页不完整或总数变化,已保留上次完整目录', {
574
+ stage: 'validate', category: 'protocol', acceptance: 'not_accepted'
575
+ });
576
+ }
577
+ if (total !== null) expectedTotal = total;
578
+ if ((total !== null && loaded === total) || (total === null && items.length < 100)) break;
552
579
  if (page === maxPages) throw new Error('iris: 模型目录超过安全分页上限,已保留原模型池');
553
580
  }
554
581
  return [...found.values()];
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 {
@@ -66,7 +65,8 @@ function providerPublic(p) {
66
65
  mediaBaseUrl: p.mediaBaseUrl || '',
67
66
  enabled: p.enabled !== false,
68
67
  ...selectMediaProtocol(p),
69
- apiKeyHint: keyHint(p.apiKey),
68
+ apiKeyHint: p.dshProvider ? '由 DSH 管理' : keyHint(p.apiKey),
69
+ ...(p.dshProvider ? { dshProvider: p.dshProvider } : {}),
70
70
  capabilities: capabilitiesOf(p),
71
71
  // 迁移/推断结果是否与原始声明不同(UI 提示旧配置已推断)
72
72
  capabilityInferred: !Array.isArray(p.capabilities) || p.capabilities.length === 0,
@@ -331,16 +331,17 @@ function serveSse(res) {
331
331
  }
332
332
 
333
333
  /** webServer 前缀路由 handler:状态、任务、动作与提示词优化器 API。 */
334
- 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' });
335
336
  try {
336
337
  const method = String(req.method || 'GET').toUpperCase();
337
338
  const urlPath = String(req.url || '').split('?')[0];
338
339
 
339
340
  // POST /iris/api/actions/:name —— GUI 直连触发工具执行
340
341
  if (method === 'POST') {
341
- if (urlPath === '/iris/api/works/download') return handleWorkbenchPost(req, res, downloadWorkbenchSelection, true);
342
- if (urlPath === '/iris/api/works/delete') return handleWorkbenchPost(req, res, deleteWorkbenchArtifacts);
343
- 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);
344
345
  if (urlPath === '/iris/api/host-client') {
345
346
  if (!host || typeof host !== 'object') return sendJson(res, 503, { error: 'host unavailable' });
346
347
  return handleJsonPost(req, res, (body) => recordHostClient(body));
@@ -351,16 +352,16 @@ export function serveApi(req, res, host) {
351
352
  }
352
353
  // D4 人工 retry as new task:产生新的真实计费,请求体必须显式 confirmBilling:true
353
354
  const coreTaskRetry = urlPath.match(/^\/iris\/api\/core\/task\/([^/]+)\/retry\/?$/);
354
- if (coreTaskRetry) return handleCoreTaskRetry(coreTaskRetry[1], req, res);
355
+ if (coreTaskRetry) return handleCoreTaskRetry(coreTaskRetry[1], req, res, core);
355
356
  // D1/D2/D3 人工动作:reobserve 显式单步 poll 一次;redeliver 只对 succeeded+failed
356
357
  // 重新取回产物;cancel 只有 Provider 明确确认才写已取消。三者绝不 submit。
357
358
  const coreTaskAction = urlPath.match(/^\/iris\/api\/core\/task\/([^/]+)\/(reobserve|redeliver|cancel|acknowledge|restore|hide|unhide)\/?$/);
358
- if (coreTaskAction) return handleCoreTaskManual(coreTaskAction[2], coreTaskAction[1], req, res);
359
+ if (coreTaskAction) return handleCoreTaskManual(coreTaskAction[2], coreTaskAction[1], req, res, core);
359
360
  const promptMatch = typeof host === 'object' && urlPath.match(/^\/iris\/api\/prompt-optimizer\/(optimize|import|reset|enabled)\/?$/);
360
- if (promptMatch) return handlePromptOptimizer(host, promptMatch[1], req, res);
361
+ if (promptMatch) return handlePromptOptimizer(host, promptMatch[1], req, res, core);
361
362
  const actionMatch = typeof host === 'object' && urlPath.match(/^\/iris\/api\/actions\/([A-Za-z0-9_-]+)\/?$/);
362
363
  if (!actionMatch) { return sendJson(res, 405, { error: 'method not allowed' }); }
363
- return handleAction(host, actionMatch[1], req, res);
364
+ return handleAction(host, actionMatch[1], req, res, core);
364
365
  }
365
366
  // GET/HEAD 原逻辑
366
367
  if (method !== 'GET' && method !== 'HEAD') {
@@ -381,16 +382,16 @@ export function serveApi(req, res, host) {
381
382
  if (urlPath === '/iris/api/core/snapshot') {
382
383
  if (method === 'HEAD') return sendJson(res, 200, {});
383
384
  const query = new URL(req.url || '', 'http://iris.local').searchParams;
384
- return serveCoreSnapshot(res, { limit: Number(query.get('limit') || 12) });
385
+ return serveCoreSnapshot(res, { limit: Number(query.get('limit') || 12) }, core);
385
386
  }
386
387
  if (urlPath === '/iris/api/works') {
387
- return serveWorkbenchArtifacts(req, method, res);
388
+ return serveWorkbenchArtifacts(req, method, res, core);
388
389
  }
389
- if (urlPath === '/iris/api/works/transactions') return serveWorkbenchRead(res, method, () => workbenchTransactions());
390
+ if (urlPath === '/iris/api/works/transactions') return serveWorkbenchRead(res, method, () => workbenchTransactions({ core }));
390
391
  const workDetailMatch = urlPath.match(/^\/iris\/api\/works\/(core|legacy)\/([^/]+)$/);
391
- if (workDetailMatch) return serveWorkbenchRead(res, method, () => workbenchDetail(workDetailMatch[1], workDetailMatch[2]));
392
+ if (workDetailMatch) return serveWorkbenchRead(res, method, () => workbenchDetail(workDetailMatch[1], workDetailMatch[2], { core }));
392
393
  const coreMediaMatch = urlPath.match(/^\/iris\/api\/core\/artifact\/(artifact_[a-f0-9]{24})\/media$/);
393
- if (coreMediaMatch) return serveCoreArtifactMedia(coreMediaMatch[1], method, res);
394
+ if (coreMediaMatch) return serveCoreArtifactMedia(coreMediaMatch[1], method, res, core);
394
395
  if (urlPath === '/iris/api/artifacts') {
395
396
  const query = new URL(req.url || '', 'http://iris.local').searchParams;
396
397
  const page = artifacts.page({ offset: Number(query.get('offset') || 0), limit: Number(query.get('limit') || 24) });
@@ -416,11 +417,11 @@ export function serveApi(req, res, host) {
416
417
  }
417
418
  }
418
419
 
419
- async function serveWorkbenchArtifacts(req, method, res) {
420
+ async function serveWorkbenchArtifacts(req, method, res, core) {
420
421
  const params = new URL(req.url || '', 'http://iris.local').searchParams;
421
422
  const input = Object.fromEntries(params);
422
423
  for (const key of ['offset', 'limit']) if (input[key] !== undefined) input[key] = Number(input[key]);
423
- try { return sendJson(res, 200, method === 'HEAD' ? {} : await workbenchArtifacts(input)); }
424
+ try { return sendJson(res, 200, method === 'HEAD' ? {} : await workbenchArtifacts(input, { core })); }
424
425
  catch (error) { return sendJson(res, error.statusCode || 500, { error: '作品读取失败', code: error.code || 'IRIS_GALLERY_FAILED' }); }
425
426
  }
426
427
 
@@ -429,9 +430,9 @@ async function serveWorkbenchRead(res, method, execute) {
429
430
  catch (error) { const safe = workbenchError(error); return sendJson(res, safe.statusCode || 400, { error: safe.message, code: safe.code }); }
430
431
  }
431
432
 
432
- function handleWorkbenchPost(req, res, execute, download = false) {
433
+ function handleWorkbenchPost(req, res, execute, download = false, core) {
433
434
  return handleJsonPost(req, res, async (body, signal) => {
434
- try { return await execute(body, { signal }); }
435
+ try { return await core.runHostOperation(operationSignal => execute(body, { signal: operationSignal, core }), { signal }); }
435
436
  catch (error) { throw workbenchError(error); }
436
437
  }, download ? (response, _status, bytes) => {
437
438
  if (response.writableEnded || response.destroyed) return;
@@ -442,9 +443,9 @@ function handleWorkbenchPost(req, res, execute, download = false) {
442
443
  } : sendJson);
443
444
  }
444
445
 
445
- async function serveCoreArtifactMedia(artifactId, method, res) {
446
+ async function serveCoreArtifactMedia(artifactId, method, res, core) {
446
447
  try {
447
- const result = await readCoreArtifactMediaForDsh(artifactId);
448
+ const result = await core.readCoreArtifactMediaForDsh(artifactId);
448
449
  if (res.writableEnded || res.destroyed) return;
449
450
  res.writeHead(200, {
450
451
  'Content-Type': result.artifact.mediaType,
@@ -461,9 +462,9 @@ async function serveCoreArtifactMedia(artifactId, method, res) {
461
462
  }
462
463
  }
463
464
 
464
- async function serveCoreSnapshot(res, options = {}) {
465
+ async function serveCoreSnapshot(res, options = {}, core) {
465
466
  try {
466
- return sendJson(res, 200, await coreSnapshotForDsh({ limit: options.limit || 12 }));
467
+ return sendJson(res, 200, await core.coreSnapshotForDsh({ limit: options.limit || 12 }));
467
468
  } catch (error) {
468
469
  console.error('[iris] Core 只读快照失败:', error && error.code || 'unknown');
469
470
  return sendJson(res, 409, {
@@ -526,8 +527,8 @@ function handleJsonPost(req, res, execute, respond = sendJson) {
526
527
  req.on('error', () => finish(400, { error: 'bad request' }));
527
528
  }
528
529
 
529
- function handleAction(host, name, req, res) {
530
- 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 }));
531
532
  }
532
533
 
533
534
  /**
@@ -538,14 +539,14 @@ function handleAction(host, name, req, res) {
538
539
  * 经 AbortSignal 传入在途操作,与 GUI actions 同一约定。
539
540
  */
540
541
  const CORE_TASK_MANUAL_ACTIONS = Object.freeze({
541
- reobserve: { command: 'task.reobserve', run: reobserveProviderTaskForDsh },
542
- redeliver: { command: 'task.redeliver', run: redeliverProviderTaskForDsh },
543
- 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) },
544
545
  // 注意力处置是 Host 偏好(不写 Core 事实):acknowledge/restore/hide/unhide。
545
- acknowledge: { command: 'attention.acknowledge', run: (input) => disposeCoreTaskAttention({ ...input, action: 'acknowledge' }) },
546
- restore: { command: 'attention.restore', run: (input) => disposeCoreTaskAttention({ ...input, action: 'restore' }) },
547
- hide: { command: 'attention.hide', run: (input) => disposeCoreTaskAttention({ ...input, action: 'hide' }) },
548
- 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' }) }
549
550
  });
550
551
 
551
552
  /**
@@ -554,7 +555,7 @@ const CORE_TASK_MANUAL_ACTIONS = Object.freeze({
554
555
  * Prompt,只能由调用方重新提供;缺显式计费确认在读取旧 Task 之前拒绝。
555
556
  * 成功返回 200 + 新任务的五类用户投影行与 retriedFrom 关系。
556
557
  */
557
- function handleCoreTaskRetry(taskId, req, res) {
558
+ function handleCoreTaskRetry(taskId, req, res, core) {
558
559
  if (!/^task_[a-f0-9]{24}$/.test(taskId)) {
559
560
  return sendJson(res, 400, { error: { code: 'IRIS_DSH_TASK_INVALID', message: 'Core Task ID 无效' } });
560
561
  }
@@ -567,7 +568,7 @@ function handleCoreTaskRetry(taskId, req, res) {
567
568
  throw error;
568
569
  }
569
570
  const prompt = String(body.prompt || '');
570
- const result = await retryProviderTaskForDsh({
571
+ const result = await core.retryProviderTaskForDsh({
571
572
  taskId,
572
573
  prompt,
573
574
  ...(body.source_artifact_id !== undefined ? { sourceArtifactId: body.source_artifact_id } : {}),
@@ -596,7 +597,7 @@ function handleCoreTaskRetry(taskId, req, res) {
596
597
  });
597
598
  }
598
599
 
599
- function handleCoreTaskManual(action, taskId, req, res) {
600
+ function handleCoreTaskManual(action, taskId, req, res, core) {
600
601
  if (!/^task_[a-f0-9]{24}$/.test(taskId)) {
601
602
  return sendJson(res, 400, { error: { code: 'IRIS_DSH_TASK_INVALID', message: 'Core Task ID 无效' } });
602
603
  }
@@ -613,7 +614,7 @@ function handleCoreTaskManual(action, taskId, req, res) {
613
614
  if (typeof req.on === 'function') req.on('aborted', disconnect);
614
615
  if (typeof res.on === 'function') res.on('close', disconnect);
615
616
  if (typeof req.resume === 'function') req.resume(); // 无 body 入参;避免小请求体滞留
616
- entry.run({ taskId, signal: controller.signal })
617
+ entry.run(core, { taskId, signal: controller.signal })
617
618
  .then((result) => finish(200, {
618
619
  ok: true,
619
620
  contractVersion: 0,
@@ -630,8 +631,8 @@ function handleCoreTaskManual(action, taskId, req, res) {
630
631
  });
631
632
  }
632
633
 
633
- function handlePromptOptimizer(host, operation, req, res) {
634
- 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 => {
635
636
  if (operation === 'optimize') return optimizePrompt(host, body, { signal });
636
637
  if (operation === 'import') {
637
638
  const imported = body && body.config !== undefined ? body.config : body;
@@ -644,7 +645,7 @@ function handlePromptOptimizer(host, operation, req, res) {
644
645
  }
645
646
  if (operation === 'enabled') return { ok: true, ...setPromptOptimizerEnabled(body && body.enabled), capabilities: PROMPT_CONFIG_CAPABILITIES };
646
647
  throw new Error('unknown prompt optimizer operation');
647
- });
648
+ }, { signal: requestSignal }));
648
649
  }
649
650
 
650
651
  /* ---------- 文件选择器 L2:本地文件上传(阶段 10) ---------- */
@@ -0,0 +1,186 @@
1
+ 'use strict';
2
+
3
+ import { normalizeCoreOptions } from './core-contract.js';
4
+ import { createCoreRuntime } 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
+
11
+ // Internal first-slice execution surface, not a public API or an HTTP allowlist.
12
+ const COMMAND_MODES = Object.freeze({
13
+ 'artifact.import': 'writer', 'artifact.export': 'writer', 'media.diff': 'writer',
14
+ 'task.observe': 'writer', 'task.reobserve': 'writer', 'task.redeliver': 'writer', 'task.cancel': 'writer',
15
+ 'artifact.inspect': 'reader', 'artifact.list': 'reader', 'task.inspect': 'reader', 'task.list': 'reader',
16
+ 'flow.init': 'writer', 'flow.create': 'writer', 'flow.continue': 'writer', 'flow.select': 'writer', 'flow.export': 'writer',
17
+ 'flow.inspect': 'reader', 'flow.list': 'reader',
18
+ 'creation.init': 'writer', 'creation.image': 'writer', 'creation.flow': 'writer'
19
+ });
20
+
21
+ export class ApplicationRuntimeError extends Error {
22
+ constructor(code, message) { super(message); this.name = 'ApplicationRuntimeError'; this.code = code; }
23
+ }
24
+
25
+ function aborted() {
26
+ return Object.assign(new ApplicationRuntimeError('IRIS_APP_ABORTED', 'Iris 操作已取消'), { name: 'AbortError' });
27
+ }
28
+
29
+ function closed() {
30
+ return new ApplicationRuntimeError('IRIS_APP_CLOSED', 'Iris 应用运行时正在关闭或已关闭');
31
+ }
32
+
33
+ /** Instance-owned FIFO and operation-scoped Core leases. No I/O until an explicit operation. */
34
+ export function createApplicationRuntime({ dataRoot, resolveImageCandidates, resolveTaskAdapter } = {}) {
35
+ const root = normalizeCoreOptions({ dataRoot }).dataRoot;
36
+ if (resolveImageCandidates !== undefined && typeof resolveImageCandidates !== 'function') {
37
+ throw new TypeError('resolveImageCandidates 必须是显式候选解析函数');
38
+ }
39
+ if (resolveTaskAdapter !== undefined && typeof resolveTaskAdapter !== 'function') {
40
+ throw new TypeError('resolveTaskAdapter 必须是显式 Task Provider 解析函数');
41
+ }
42
+ let state = 'open';
43
+ const queue = [];
44
+ const active = new Set();
45
+ let writer = null;
46
+ let disposePromise;
47
+ let cleanupFailure;
48
+
49
+ function snapshot() {
50
+ return Object.freeze({ state, queuedWrites: queue.length, activeWrites: Number(Boolean(writer)),
51
+ activeReaders: active.size - Number(Boolean(writer)), cleanupFailed: Boolean(cleanupFailure) });
52
+ }
53
+
54
+ function detach(job) { job.signal?.removeEventListener('abort', job.onAbort); }
55
+
56
+ function rejectQueued(job, error) {
57
+ detach(job);
58
+ job.reject(error);
59
+ }
60
+
61
+ function stop(job) {
62
+ job.aborted = true;
63
+ if (job.runtime && !job.disposal) {
64
+ job.disposal = job.runtime.dispose();
65
+ // The operation awaits this same promise below and reports any cleanup error.
66
+ job.disposal.catch(() => {});
67
+ }
68
+ }
69
+
70
+ function stopAll() {
71
+ for (const job of queue.splice(0)) rejectQueued(job, closed());
72
+ for (const job of active) stop(job);
73
+ }
74
+
75
+ async function perform(job) {
76
+ if (job.aborted) throw aborted();
77
+ const runtime = createCoreRuntime({ dataRoot: root, mode: job.mode });
78
+ job.runtime = runtime;
79
+ let result, operationError, failed = false;
80
+ try {
81
+ runtime.start();
82
+ result = await job.action(runtime);
83
+ } catch (error) { failed = true; operationError = error; }
84
+ try { await (job.disposal || runtime.dispose()); }
85
+ catch (error) {
86
+ cleanupFailure ||= error;
87
+ state = 'closing';
88
+ stopAll();
89
+ if (failed) {
90
+ throw Object.assign(new AggregateError([operationError, error], 'Iris 操作失败且运行时未能安全释放'), { code: error.code });
91
+ }
92
+ throw error;
93
+ }
94
+ if (failed) throw operationError;
95
+ return result;
96
+ }
97
+
98
+ function start(job) {
99
+ active.add(job);
100
+ job.done = Promise.resolve().then(async () => {
101
+ let result, error, failed = false;
102
+ try { result = await perform(job); }
103
+ catch (caught) { failed = true; error = caught; }
104
+ detach(job);
105
+ active.delete(job);
106
+ if (writer === job) writer = null;
107
+ if (failed) job.reject(error); else job.resolve(result);
108
+ dispatch();
109
+ });
110
+ }
111
+
112
+ function dispatch() {
113
+ if (state !== 'open' || writer || !queue.length) return;
114
+ writer = queue.shift();
115
+ start(writer);
116
+ }
117
+
118
+ function schedule(mode, action, { signal } = {}) {
119
+ if (signal !== undefined && !(signal instanceof AbortSignal)) throw new TypeError('signal 必须是 AbortSignal');
120
+ if (state !== 'open') return Promise.reject(closed());
121
+ if (signal?.aborted) return Promise.reject(aborted());
122
+ let job;
123
+ const result = new Promise((resolve, reject) => { job = { mode, action, signal, resolve, reject, aborted: false }; });
124
+ job.onAbort = () => {
125
+ const index = queue.indexOf(job);
126
+ if (index >= 0) { queue.splice(index, 1); rejectQueued(job, aborted()); }
127
+ else stop(job);
128
+ };
129
+ signal?.addEventListener('abort', job.onAbort, { once: true });
130
+ if (mode === 'writer') { queue.push(job); dispatch(); }
131
+ else start(job);
132
+ return result;
133
+ }
134
+
135
+ /** Trusted in-process Host composition; never an external command/HTTP dispatch surface. */
136
+ function run(mode, action, options) {
137
+ if (!['reader', 'writer'].includes(mode)) throw new TypeError('Host 操作需要明确 reader/writer 模式');
138
+ if (typeof action !== 'function') throw new TypeError('Host 操作必须是函数');
139
+ return schedule(mode, action, options);
140
+ }
141
+
142
+ async function execute(name, input, options) {
143
+ if (!Object.hasOwn(COMMAND_MODES, name)) {
144
+ throw new ApplicationRuntimeError('IRIS_APP_COMMAND_UNSUPPORTED', '应用运行时尚未装配该 Command');
145
+ }
146
+ const ownedInput = structuredClone(input);
147
+ return schedule(COMMAND_MODES[name], runtime => {
148
+ const ports = resolveTaskAdapter ? { resolveTaskAdapter: async task => {
149
+ const adapter = await resolveTaskAdapter(task, { signal: runtime.signal });
150
+ if (runtime.signal.aborted) throw aborted();
151
+ return adapter;
152
+ } } : {};
153
+ if (name.startsWith('creation.')) return createLocalCreationService(runtime, { resolveImageCandidates,
154
+ onPrepared: options?.onPrepared }).execute(name, ownedInput);
155
+ if (name.startsWith('flow.')) return createImageFlowService(runtime, { ...ports, resolveImageCandidates }).execute(name, ownedInput);
156
+ return createCommandService(runtime, ports).execute(name, ownedInput);
157
+ }, options);
158
+ }
159
+
160
+ async function generateImage(input, options) {
161
+ const normalized = normalizeGenerationInput('image', input);
162
+ if (!resolveImageCandidates) {
163
+ throw new ApplicationRuntimeError('IRIS_APP_IMAGE_RESOLVER_MISSING', '图片生成需要显式提供候选解析函数');
164
+ }
165
+ return schedule('writer', async runtime => {
166
+ const candidates = await resolveImageCandidates({ modelRef: normalized.modelRef, signal: runtime.signal });
167
+ if (runtime.signal.aborted) throw aborted();
168
+ return createProviderTaskRunner(runtime).submit({ capability: 'image', candidates,
169
+ providerInput: normalized.providerInput, sourceArtifactId: normalized.sourceArtifactId });
170
+ }, options);
171
+ }
172
+
173
+ function dispose() {
174
+ if (disposePromise) return disposePromise;
175
+ state = 'closing';
176
+ stopAll();
177
+ disposePromise = Promise.all([...active].map(job => job.done)).then(() => {
178
+ if (cleanupFailure) throw cleanupFailure;
179
+ state = 'closed';
180
+ return snapshot();
181
+ });
182
+ return disposePromise;
183
+ }
184
+
185
+ return Object.freeze({ execute, generateImage, run, dispose, snapshot });
186
+ }