@mokuyoaxis/dsh-iris 0.2.7 → 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 (160) hide show
  1. package/CHANGELOG.md +81 -1
  2. package/README.en.md +103 -312
  3. package/README.md +100 -310
  4. package/bin/dsh-iris.js +333 -65
  5. package/docs/AGENT_CORE_QUICKSTART.md +64 -0
  6. package/docs/AGENT_QUICKSTART.md +2 -0
  7. package/docs/ARCHITECTURE.md +13 -3
  8. package/docs/AUDIO_FLOWS.md +71 -0
  9. package/docs/CONTACT_SHEET.md +62 -0
  10. package/docs/CORE_CONNECTIONS.md +99 -0
  11. package/docs/CORE_QUICKSTART.md +108 -0
  12. package/docs/CORE_RUNTIME_CONTRACT.md +7 -2
  13. package/docs/CORE_SERVICE.md +97 -0
  14. package/docs/CORE_SKILLS.md +35 -3
  15. package/docs/DSH_COMPATIBILITY.md +10 -0
  16. package/docs/DSH_RC2_ADAPTATION.md +1 -1
  17. package/docs/DSH_RUNTIME.md +6 -2
  18. package/docs/HEADLESS_CLI.md +2 -2
  19. package/docs/IMAGE_FLOW.md +4 -0
  20. package/docs/IMAGE_GENERATION.md +127 -0
  21. package/docs/LOCAL_API.md +4 -2
  22. package/docs/LOCAL_API_CONTRACT.md +39 -14
  23. package/docs/LOCAL_CREATION.md +7 -1
  24. package/docs/LOCAL_OPERATIONS.md +46 -0
  25. package/docs/LOCAL_SELECTION.md +57 -0
  26. package/docs/LOCAL_WEB.md +201 -0
  27. package/docs/MCP.md +191 -0
  28. package/docs/MUSIC.md +2 -0
  29. package/docs/PANEL_EXTENSIONS.md +64 -0
  30. package/docs/PLUGINS.md +189 -0
  31. package/docs/PLUGIN_STARTUP.md +100 -0
  32. package/docs/PROVIDER_ADAPTER_CONTRACT.md +1 -1
  33. package/docs/PROVIDER_EXTENSIONS.md +78 -0
  34. package/docs/PROVIDER_HEALTH.md +3 -1
  35. package/docs/PROVIDER_SETTINGS.md +85 -0
  36. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +6 -0
  37. package/docs/ROADMAP.md +17 -7
  38. package/docs/VISION_CLI.md +1 -1
  39. package/docs/WEB_PROTOTYPE.md +21 -6
  40. package/docs/WEB_RECOVERY.md +46 -0
  41. package/docs/WORKBENCH_LOGIN.md +63 -0
  42. package/docs/assets/screenshots/core-connections-preview.webp +0 -0
  43. package/docs/assets/screenshots/core-model-pools-preview.webp +0 -0
  44. package/docs/assets/screenshots/core-overview-preview.webp +0 -0
  45. package/docs/assets/screenshots/core-spectrum-preview.webp +0 -0
  46. package/docs/assets/screenshots/core-studio-dark-preview.webp +0 -0
  47. package/docs/assets/screenshots/core-studio-mobile-preview.webp +0 -0
  48. package/docs/assets/screenshots/core-studio-preview.webp +0 -0
  49. package/docs/releases/0.3.0-alpha.1.md +51 -0
  50. package/docs/screenshots.md +41 -1
  51. package/lib/actions.js +53 -2
  52. package/lib/adapters.js +20 -11
  53. package/lib/application-runtime.js +34 -7
  54. package/lib/audio-flow-input.js +37 -0
  55. package/lib/client.js +346 -53
  56. package/lib/comfyui-adapter.js +94 -0
  57. package/lib/comfyui-client.js +51 -0
  58. package/lib/comfyui-inspection.js +73 -0
  59. package/lib/comfyui-workflow.js +94 -0
  60. package/lib/config.js +36 -1
  61. package/lib/configured-image-runtime.js +50 -0
  62. package/lib/connection-skill.js +89 -0
  63. package/lib/connection-template.js +23 -0
  64. package/lib/core-artifact-store.js +16 -2
  65. package/lib/core-bridge-selection.js +31 -0
  66. package/lib/core-bridge-tools.js +83 -0
  67. package/lib/core-capability-catalog.js +182 -0
  68. package/lib/core-connection-diagnostics.js +49 -0
  69. package/lib/core-connection-store.js +277 -0
  70. package/lib/core-connections.js +225 -0
  71. package/lib/core-maintenance.js +9 -3
  72. package/lib/core-runtime.js +2 -1
  73. package/lib/core-service-client.js +170 -0
  74. package/lib/core-tasks.js +29 -12
  75. package/lib/core-writer-lease.js +16 -0
  76. package/lib/creation-request-store.js +38 -64
  77. package/lib/dsh-host-adapter.js +4 -1
  78. package/lib/dsh-service-bridge.js +66 -0
  79. package/lib/execution-ownership.js +43 -0
  80. package/lib/flow-store.js +105 -10
  81. package/lib/generation-input.js +10 -1
  82. package/lib/headless-vision.js +7 -7
  83. package/lib/image-edit-input.js +1 -1
  84. package/lib/image-flow.js +73 -29
  85. package/lib/index.js +37 -17
  86. package/lib/local-api-projection.js +10 -5
  87. package/lib/local-api-token-directory.js +31 -0
  88. package/lib/local-api.js +769 -67
  89. package/lib/local-creation.js +89 -24
  90. package/lib/local-operations.js +36 -0
  91. package/lib/local-request-root.js +86 -0
  92. package/lib/local-web-assets.js +37 -0
  93. package/lib/mcp-bridge.js +58 -0
  94. package/lib/mcp-check.js +26 -0
  95. package/lib/mcp-config.js +42 -0
  96. package/lib/mcp-delivery.js +46 -0
  97. package/lib/mcp-http.js +101 -0
  98. package/lib/mcp-server.js +47 -0
  99. package/lib/model-directory-service.js +16 -6
  100. package/lib/model-directory.js +3 -3
  101. package/lib/models.js +2 -2
  102. package/lib/music-transport.js +2 -1
  103. package/lib/operation-runner.js +236 -0
  104. package/lib/operation-store.js +216 -0
  105. package/lib/panel-extension.js +36 -0
  106. package/lib/plugin-contract.js +194 -0
  107. package/lib/plugin-execution.js +51 -0
  108. package/lib/plugin-launch.js +86 -0
  109. package/lib/plugin-operation-store.js +101 -0
  110. package/lib/plugin-operations.js +28 -0
  111. package/lib/plugin-registry.js +109 -0
  112. package/lib/plugin-selection.js +15 -0
  113. package/lib/plugin-source-image.js +5 -0
  114. package/lib/plugin-web-assets.js +54 -0
  115. package/lib/plugins.d.ts +288 -0
  116. package/lib/plugins.js +58 -0
  117. package/lib/provider-adapter.js +5 -1
  118. package/lib/provider-adapters.js +56 -18
  119. package/lib/provider-catalog.js +40 -29
  120. package/lib/provider-config-service.js +105 -23
  121. package/lib/provider-diagnostics.js +56 -0
  122. package/lib/provider-extensions.js +29 -0
  123. package/lib/provider-health.js +8 -1
  124. package/lib/provider-monitor.js +79 -0
  125. package/lib/provider-protocol.js +70 -20
  126. package/lib/provider-registry.js +118 -0
  127. package/lib/provider-response.js +28 -0
  128. package/lib/provider-settings.js +134 -0
  129. package/lib/provider-task-runner.js +22 -9
  130. package/lib/skill-installation.js +23 -11
  131. package/lib/vision-model-routing.js +3 -0
  132. package/lib/web/app.js +642 -0
  133. package/lib/web/appearance.js +111 -0
  134. package/lib/web/comfyui.js +146 -0
  135. package/lib/web/connections.js +377 -0
  136. package/lib/web/creation.js +355 -0
  137. package/lib/web/flow-view.js +33 -0
  138. package/lib/web/gallery.js +132 -0
  139. package/lib/web/index.html +291 -0
  140. package/lib/web/media-client.js +204 -0
  141. package/lib/web/plugin-panel.js +221 -0
  142. package/lib/web/plugin-surface.css +17 -0
  143. package/lib/web/providers.js +492 -0
  144. package/lib/web/recovery.js +105 -0
  145. package/lib/web/startup.js +18 -0
  146. package/lib/web/styles.css +518 -0
  147. package/lib/web/transport.js +96 -0
  148. package/lib/web/workbench-login.js +165 -0
  149. package/lib/workbench-auth-cli.js +47 -0
  150. package/lib/workbench-auth.js +200 -0
  151. package/package.json +20 -5
  152. package/plugins/contact-sheet/index.d.mts +4 -0
  153. package/plugins/contact-sheet/index.mjs +33 -0
  154. package/plugins/contact-sheet/web/board.css +14 -0
  155. package/plugins/contact-sheet/web/board.mjs +93 -0
  156. package/skills/iris-core/SKILL.md +8 -4
  157. package/skills/iris-core/references/cli.md +2 -2
  158. package/skills/iris-core/references/comfyui.md +69 -0
  159. package/skills/iris-core/references/tasks.md +2 -0
  160. package/user_guide.md +5 -1
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.2.7';
24
+ var IRIS_CLIENT_VERSION = '0.3.0-alpha.1';
25
25
  var HOST_CLIENT_PROTOCOL_VERSION = 0;
26
26
  var reportedSeats = [];
27
27
 
@@ -109,6 +109,27 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
109
109
  '.iris-workbench-page :is(input, textarea, select) { font-family: inherit; accent-color: var(--iris-workbench-accent); }',
110
110
  '.iris-workbench-page input:not([type=checkbox]):not([type=radio]):not([type=file]), .iris-workbench-page textarea, .iris-workbench-page select { min-width: 0; max-width: 100%; min-height: 34px; padding: 5px 8px; border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 8px; background: var(--dsw-alias-bg-layer-2, #32323c); color: var(--dsw-alias-label-primary, #e6e6e6); font-size: 12px; }',
111
111
  '.iris-workbench-page .iris-pm-note { line-height: 1.65; }',
112
+ '.iris-comfy-panel { position: relative; isolation: isolate; overflow: hidden; min-width: 0; display: grid; gap: 10px; padding: 12px; border: 1px solid color-mix(in srgb, #6F69B8 30%, var(--dsw-alias-border-l1, #3a3a44)); border-radius: 12px; background: linear-gradient(105deg, rgba(111,105,184,.07), rgba(229,155,118,.04)), var(--dsw-alias-bg-layer-1, #26262e); }',
113
+ '.iris-comfy-panel::before { content: ""; position: absolute; z-index: -1; pointer-events: none; inset: 0; background: linear-gradient(105deg, transparent 20%, #45317D18, #6F69B830, #BB78AD30, #E59B7630, #ED9C9120, #F0D09C30, transparent 80%); transform: translateX(110%); animation: iris-comfy-arrive .65s ease-out 1; }',
114
+ '@keyframes iris-comfy-arrive { from { transform: translateX(-110%); opacity: 0; } 25% { opacity: 1; } to { transform: translateX(110%); opacity: 0; } }',
115
+ '.iris-comfy-head { display: flex; gap: 8px; align-items: center; justify-content: space-between; flex-wrap: wrap; }',
116
+ '.iris-comfy-head strong { font-size: 13px; }',
117
+ '.iris-comfy-tag { border: 1px solid color-mix(in srgb, #BB78AD 30%, var(--dsw-alias-border-l1, #3a3a44)); border-radius: 20px; padding: 2px 8px; font-size: 11px; color: var(--dsw-alias-label-secondary, #9a9a9a); }',
118
+ '.iris-comfy-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }',
119
+ '.iris-comfy-panel label, .iris-comfy-grid label { display: grid; min-width: 0; gap: 5px; font-size: 12px; }',
120
+ '.iris-comfy-panel :is(input,select,textarea) { min-width: 0; max-width: 100%; width: 100%; box-sizing: border-box; border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 8px; padding: 6px 8px; background: var(--dsw-alias-bg-layer-2, #32323c); color: var(--dsw-alias-label-primary, #e6e6e6); font: inherit; }',
121
+ '.iris-comfy-panel input[type=checkbox] { width: auto; min-height: 0; }',
122
+ '.iris-comfy-panel summary { cursor: pointer; font-size: 12px; padding: 4px 0; }',
123
+ '.iris-comfy-panel details > div { margin-top: 8px; }',
124
+ '.iris-comfy-slots { display: grid; gap: 6px; max-height: 280px; overflow: auto; }',
125
+ '.iris-comfy-slot { display: flex; gap: 8px; align-items: center; min-width: 0; padding: 7px 0; border-bottom: 1px solid var(--dsw-alias-border-l1, #3a3a44); }',
126
+ '.iris-comfy-slot label { flex: 1; display: flex; align-items: center; gap: 7px; overflow-wrap: anywhere; }',
127
+ '.iris-comfy-slot select { width: auto; max-width: 100px; }',
128
+ '.iris-comfy-result { font-size: 12px; line-height: 1.65; overflow-wrap: anywhere; }',
129
+ '.iris-comfy-result ul { padding-left: 20px; margin: 5px 0; }',
130
+ '.iris-comfy-panel .iris-act-run, .iris-comfy-card .iris-act-run { background: #6F69B8; color: #fff; border-color: transparent; }',
131
+ '@media (max-width: 640px) { .iris-comfy-grid { grid-template-columns: minmax(0, 1fr); } .iris-comfy-panel { padding: 10px; } }',
132
+ '@media (prefers-reduced-motion: reduce) { .iris-comfy-panel::before { animation: none; opacity: 0; } }',
112
133
  '.iris-workbench-page .iris-pm-mrows { max-height: 400px; gap: 8px; }',
113
134
  '.iris-workbench-page .iris-pm-mrow { min-width: 0; padding: 10px; gap: 6px; flex-wrap: wrap; border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 10px; }',
114
135
  '.iris-workbench-page .iris-pm-mname { flex-basis: 100%; white-space: normal; overflow-wrap: anywhere; font-size: 13px; }',
@@ -183,7 +204,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
183
204
  /* ---- 操作卡片组(阶段 5 GUI 直连) ---- */
184
205
  '.iris-act-group { display: grid; gap: 8px; }',
185
206
  '.iris-act-card { background: var(--dsw-alias-bg-layer-1, #26262e); border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 8px; overflow: hidden; }',
186
- '.iris-act-head { display: flex; align-items: center; gap: 8px; padding: 8px 10px; cursor: pointer; user-select: none; }',
207
+ '.iris-act-head { display: flex; align-items: center; width:100%; gap: 8px; padding: 8px 10px; border:0; background:transparent; color:inherit; font:inherit; text-align:left; cursor: pointer; user-select: none; }',
187
208
  '.iris-act-head:hover { background: var(--dsw-alias-bg-layer-2, #32323c); }',
188
209
  '.iris-act-title { font-size: 13px; font-weight: 600; flex: 1; }',
189
210
  '.iris-act-arrow { color: var(--dsw-alias-label-secondary, #9a9a9a); font-size: 10px; transition: transform 0.15s; }',
@@ -317,7 +338,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
317
338
  '.iris-bubble-more { font-size: 11px; color: var(--dsw-alias-brand-primary, #7aa2f7); background: transparent; border: none; cursor: pointer; padding: 4px 2px; text-align: left; }',
318
339
  /* 历史浏览器(设置页按日期分组折叠) */
319
340
  '.iris-hist-group { margin-bottom: 4px; }',
320
- '.iris-hist-head { display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-secondary, #9a9a9a); cursor: pointer; padding: 4px 2px; }',
341
+ '.iris-hist-head { display: flex; align-items: center; width:100%; gap: 6px; border:0; background:transparent; text-align:left; font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-secondary, #9a9a9a); cursor: pointer; padding: 4px 2px; }',
321
342
  '.iris-hist-head:hover { color: var(--dsw-alias-label-primary, #e6e6e6); }',
322
343
  /* 常用卡片选择器 + 清理区 */
323
344
  '.iris-pick-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 4px 10px; }',
@@ -396,7 +417,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
396
417
  '.iris-ff-val { font-size: 11px; color: var(--dsw-alias-label-secondary, #9a9a9a); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }',
397
418
  '.iris-ff-val.set { color: var(--dsw-alias-state-success-primary, #6fcf6f); }',
398
419
  '.iris-ff-list { display: grid; gap: 2px; max-height: 160px; overflow: auto; border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 6px; padding: 4px; background: var(--dsw-alias-bg-layer-2, #32323c); }',
399
- '.iris-ff-item { display: flex; align-items: center; gap: 6px; font-size: 11px; cursor: pointer; padding: 2px 4px; border-radius: 4px; }',
420
+ '.iris-ff-item { display: flex; align-items: center; width:100%; gap: 6px; border:0; background:transparent; color:inherit; text-align:left; font-size: 11px; cursor: pointer; padding: 2px 4px; border-radius: 4px; }',
400
421
  '.iris-ff-item:hover { background: var(--dsw-alias-bg-layer-1, #26262e); }',
401
422
  '.iris-ff-item .iris-ff-src { color: var(--dsw-alias-brand-primary, #7aa2f7); font-size: 10px; }',
402
423
  /* 对话框级提示词优化器:桌面浮层 + 窄屏底部 Sheet */
@@ -676,7 +697,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
676
697
  : option.protocolSupported === false ? option.reason || '当前接口协议不支持此功能。'
677
698
  : option.unavailable && !health.rateLimited && !health.modelUnavailable && !option.modelUnavailable ? option.reason || '连接尚未就绪,请先检查供应商设置。' : '';
678
699
  return { ...option, providerName: provider ? provider.name : 'DSH 宿主', health: health,
679
- taskOnly: row.taskOnly || host, reason: reason, canTest: !reason };
700
+ taskOnly: row.taskOnly || host || option.imageRouting && option.imageRouting.mediaProtocol === 'comfyui', reason: reason, canTest: !reason };
680
701
  });
681
702
  }
682
703
  function CapabilityHealthOverview() {
@@ -745,7 +766,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
745
766
  if (field) { revealWorkbenchItem(field); field.scrollIntoView({ block: 'center', behavior: 'smooth' }); field.focus({ preventScroll: true }); }
746
767
  }
747
768
  function openTask(row) {
748
- var card = document.querySelector('[data-iris-action="' + row.action + '"]');
769
+ var card = document.querySelector('[data-iris-action="' + (row.cap === 'image-gen' ? 'image' : row.action) + '"]');
749
770
  if (!card) return;
750
771
  revealWorkbenchItem(card);
751
772
  if (!card.classList.contains('open')) card.querySelector('.iris-act-head').click();
@@ -841,7 +862,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
841
862
  React.createElement('button', { className: 'iris-health-btn' + (!option.taskOnly ? ' primary' : ''),
842
863
  disabled: !fresh || !option.canTest || !!busy || !!(result && result.phase === 'unknown'),
843
864
  onClick: function () { if (running.current) return; if (option.taskOnly) openTask(row); else confirmPair[1]({ cap: row.cap, ref: option.ref }); } },
844
- option.taskOnly ? '前往功能验证 ↗' : result && result.phase === 'running' ? '正在实测…' : '实测此模型')),
865
+ option.taskOnly ? option.comfyWorkflow ? '前往画图选择工作流 ↗' : '前往功能验证 ↗' : result && result.phase === 'running' ? '正在实测…' : '实测此模型')),
845
866
  option.reason ? React.createElement('p', { className: 'iris-health-caption' }, option.reason) : null,
846
867
  option.ref === 'host:current-vision' ? React.createElement('p', { className: 'iris-health-caption' },
847
868
  '宿主视觉通过真实图片任务验证,请在打开的表单中选择 DSH 当前默认模型。') : null,
@@ -1176,6 +1197,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1176
1197
  React.createElement('option', { value: 'dashscope' }, 'DashScope'),
1177
1198
  React.createElement('option', { value: 'openai-images' }, 'OpenAI Images'),
1178
1199
  React.createElement('option', { value: 'openai-chat-images' }, '聊天生图(网关兼容)'),
1200
+ p.mediaProtocol === 'comfyui' || m.imageProtocol === 'comfyui' ? React.createElement('option', { value: 'comfyui' }, 'ComfyUI · API 工作流') : null,
1179
1201
  React.createElement('option', { value: 'openai-responses-images' }, 'Responses 生图')),
1180
1202
  m.imageRouting && m.imageRouting.protocolInferred ? ' · 协议待确认' : null) : null,
1181
1203
  !m.directoryOnly && caps.includes('music-gen') ? React.createElement('label', { className: 'iris-pm-note' }, '音乐协议 ',
@@ -1190,9 +1212,10 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1190
1212
  return React.createElement('div', { className: 'iris-pm-pool' },
1191
1213
  React.createElement('div', { className: 'iris-pm-note' }, '模型池(' + models.filter(function (m) { return !m.directoryOnly; }).length + ')'
1192
1214
  + (models.some(function (m) { return m.directoryOnly; }) ? ' · 目录待确认 ' + models.filter(function (m) { return m.directoryOnly; }).length + ' 项' : '') + ':'),
1193
- rows.length ? React.createElement('div', { className: 'iris-pm-mrows' }, rows) : React.createElement('div', { className: 'iris-pm-note' }, '暂无模型 — 展开「模型目录」同步,或在下方手动添加'),
1194
- React.createElement(ModelUsageForm, { provider: p, setupSuggested: props.setupSuggested,
1195
- onNote: props.onNote, onDone: props.onDone }));
1215
+ rows.length ? React.createElement('div', { className: 'iris-pm-mrows' }, rows) : React.createElement('div', { className: 'iris-pm-note' }, p.mediaProtocol === 'comfyui'
1216
+ ? '尚无工作流,请在上方导入 API 图并保存。' : '暂无模型 — 展开「模型目录」同步,或在下方手动添加'),
1217
+ p.mediaProtocol !== 'comfyui' ? React.createElement(ModelUsageForm, { provider: p, setupSuggested: props.setupSuggested,
1218
+ onNote: props.onNote, onDone: props.onDone }) : null);
1196
1219
  }
1197
1220
 
1198
1221
  function ModelUsageForm(props) {
@@ -1343,7 +1366,172 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1343
1366
  React.createElement('div', { className: 'iris-wb-row' },
1344
1367
  React.createElement('input', { value: value, placeholder: '留空则沿用 Base URL', onChange: function (e) { setValue(e.target.value); } }),
1345
1368
  React.createElement('button', { className: 'iris-pm-btn', onClick: save }, '保存')),
1346
- React.createElement('span', { className: 'iris-pm-note' }, '百炼 Workspace 可填 …maas.aliyuncs.com/compatible-mode/v1;视觉仍使用普通 Base URL。'));
1369
+ React.createElement('span', { className: 'iris-pm-note' }, p.mediaProtocol === 'comfyui' ? '留空沿用已保存地址;反向代理有独立媒体地址时才填写。'
1370
+ : '百炼 Workspace 可填 …maas.aliyuncs.com/compatible-mode/v1;视觉仍使用普通 Base URL。'));
1371
+ }
1372
+
1373
+ function ProviderAuthenticationEditor(props) {
1374
+ var p = props.provider, authPair = React.useState(p.auth), keyPair = React.useState(''), notePair = React.useState('');
1375
+ var busyPair = React.useState(false), active = React.useRef(null), unknownPair = React.useState(false);
1376
+ React.useEffect(function () { authPair[1](p.auth); }, [p.auth]);
1377
+ React.useEffect(function () { return function () { if (active.current) active.current.abort(); }; }, []);
1378
+ async function save() {
1379
+ if (active.current || unknownPair[0]) return;
1380
+ var controller = new AbortController(), timer = setTimeout(function () { controller.abort(); }, 15000);
1381
+ active.current = controller; busyPair[1](true);
1382
+ try {
1383
+ var result = await postAction('providers_upsert', { id: p.id, auth: authPair[0], ...(authPair[0] === 'bearer' && keyPair[0].trim() ? { apiKey: keyPair[0].trim() } : {}) }, { signal: controller.signal });
1384
+ if (controller.signal.aborted) throw new Error('请求已结束');
1385
+ if (!result.ok) { notePair[1](result.d.error || '保存失败'); return; }
1386
+ keyPair[1](''); notePair[1]('认证设置已保存'); props.onDone();
1387
+ } catch (_) { unknownPair[1](true); notePair[1]('保存结果未知,请重新打开此供应商核对;未自动重试。'); }
1388
+ finally { clearTimeout(timer); active.current = null; busyPair[1](false); }
1389
+ }
1390
+ return React.createElement('details', {}, React.createElement('summary', {}, '认证设置'),
1391
+ React.createElement('fieldset', { disabled: busyPair[0] || unknownPair[0], style: { border: 0, padding: 0, margin: '8px 0', minWidth: 0 } },
1392
+ React.createElement('label', { className: 'iris-pm-field' }, '认证方式', React.createElement('select', { value: authPair[0], onChange: function (event) { authPair[1](event.target.value); } },
1393
+ React.createElement('option', { value: 'none' }, '无需认证'), React.createElement('option', { value: 'bearer' }, 'Bearer · API Key'))),
1394
+ authPair[0] === 'bearer' ? React.createElement('label', { className: 'iris-pm-field' }, '替换 API Key(留空保留)', React.createElement('input', { type: 'password', value: keyPair[0], onChange: function (event) { keyPair[1](event.target.value); } })) : null,
1395
+ React.createElement('p', { className: 'iris-pm-note' }, '无需认证时不会发送保留的 Key;更改认证会使旧的健康证据失效。'),
1396
+ React.createElement('button', { className: 'iris-pm-btn', onClick: save }, busyPair[0] ? '保存中…' : '保存认证')),
1397
+ notePair[0] ? React.createElement('div', { role: 'status', className: 'iris-pm-note' }, notePair[0]) : null);
1398
+ }
1399
+
1400
+ function ComfyCheckResult(props) {
1401
+ var report = props.report;
1402
+ if (!report) return null;
1403
+ return React.createElement('div', { className: 'iris-comfy-result', role: 'status' },
1404
+ React.createElement('strong', {}, report.ok ? '静态检查通过 · 未生成图片' : '需要修正 ' + report.issues.length + ' 项 · 未生成图片'),
1405
+ report.issues.length ? React.createElement('ul', {}, report.issues.map(function (issue, i) {
1406
+ return React.createElement('li', { key: i }, issue.message);
1407
+ })) : null,
1408
+ React.createElement('div', { className: 'iris-pm-note' }, '只读取节点定义;不证明 GPU、显存、模型家族或自定义节点能够实际运行。'));
1409
+ }
1410
+
1411
+ function comfySlots(draft) {
1412
+ var result = [], labels = { lora_name: 'LoRA', strength_model: '模型权重', strength_clip: 'CLIP 权重',
1413
+ ckpt_name: 'Checkpoint', seed: '种子', steps: '步数', cfg: 'CFG', sampler_name: '采样器', scheduler: '调度器', text: '文本' };
1414
+ Object.entries(draft.graph).forEach(function (pair) {
1415
+ var id = pair[0], node = pair[1];
1416
+ Object.entries(node.inputs).forEach(function (field) {
1417
+ var input = field[0], value = field[1];
1418
+ if (!['string', 'boolean', 'number'].includes(typeof value) || id === draft.promptNode && input === 'text'
1419
+ || id === draft.sizeNode && ['width', 'height'].includes(input)) return;
1420
+ var parameter = draft.parameters.find(function (item) { return item.node === id && item.input === input; });
1421
+ result.push({ key: id + '/' + input, value: value, selected: !!parameter, parameter: parameter || {
1422
+ id: ('n' + id.replace(/[^A-Za-z0-9_]/g, '_') + '_' + input.replace(/[^A-Za-z0-9_]/g, '_')).slice(0, 54) + '_' + result.length,
1423
+ label: (labels[input] || input) + ' · ' + node.class_type + ' #' + id, node: id, input: input,
1424
+ type: typeof value === 'number' ? Number.isSafeInteger(value) && !/^(strength_|cfg$|denoise$)/.test(input) ? 'integer' : 'number' : typeof value
1425
+ } });
1426
+ });
1427
+ });
1428
+ return result;
1429
+ }
1430
+ function comfyDraftWorkflow(draft) {
1431
+ if (!draft.promptNode || !draft.outputNode) throw new Error('请明确选择正向提示词与图片输出节点');
1432
+ var parameters = draft.parameters;
1433
+ return { version: parameters.length ? 1 : 0, graph: draft.graph, promptNode: draft.promptNode, outputNode: draft.outputNode,
1434
+ ...(draft.sizeNode ? { sizeNode: draft.sizeNode } : {}), ...(parameters.length ? { parameters: parameters } : {}) };
1435
+ }
1436
+
1437
+ function ComfyWorkflowEditor(props) {
1438
+ var p = props.provider, draftPair = React.useState(null), draft = draftPair[0], namePair = React.useState('');
1439
+ var autoPair = React.useState(props.setupSuggested === true), reportPair = React.useState(null), notePair = React.useState('');
1440
+ var busyPair = React.useState(''), unknownPair = React.useState(false), replacePair = React.useState(false);
1441
+ var active = React.useRef(null), importVersion = React.useRef(0), original = React.useRef({});
1442
+ React.useEffect(function () { return function () { importVersion.current++; if (active.current) active.current.abort(); }; }, []);
1443
+ function change(field, value) {
1444
+ var next = { ...draft, [field]: value };
1445
+ if (field === 'promptNode' || field === 'sizeNode') next.parameters = next.parameters.filter(function (item) {
1446
+ return !(item.node === next.promptNode && item.input === 'text' || item.node === next.sizeNode && ['width', 'height'].includes(item.input));
1447
+ });
1448
+ draftPair[1](next); reportPair[1](null); replacePair[1](false); notePair[1]('');
1449
+ }
1450
+ async function read(event) {
1451
+ var file = event.target.files[0], ticket = ++importVersion.current;
1452
+ if (!file) return;
1453
+ try {
1454
+ if (file.size > 40 * 1024) throw new Error('工作流最多 40 KiB');
1455
+ var raw = JSON.parse(await file.text()), template = raw && raw.graph && [0, 1].includes(raw.version) ? raw : null;
1456
+ if (ticket !== importVersion.current) return;
1457
+ var graph = template ? template.graph : raw, entries = graph && typeof graph === 'object' && !Array.isArray(graph) ? Object.entries(graph) : [];
1458
+ if (!entries.length || entries.length > 256 || entries.some(function (entry) { var node = entry[1]; return !node || typeof node.class_type !== 'string'
1459
+ || !node.inputs || typeof node.inputs !== 'object' || Array.isArray(node.inputs); })) throw new Error('请使用 Export (API) 节点图或 Iris 参数模板,不支持画布 JSON');
1460
+ var outputs = entries.filter(function (entry) { return entry[1].class_type === 'SaveImage'; });
1461
+ if (template && (template.version === 0 && template.parameters !== undefined || template.version === 1 && (!Array.isArray(template.parameters) || !template.parameters.length))
1462
+ || template && template.parameters !== undefined && (!Array.isArray(template.parameters) || template.parameters.some(function (item) {
1463
+ return !item || typeof item.id !== 'string' || typeof item.node !== 'string' || typeof item.input !== 'string' || !['string', 'integer', 'number', 'boolean'].includes(item.type);
1464
+ }))) throw new Error('参数声明格式不正确');
1465
+ draftPair[1]({ graph: graph, promptNode: template ? template.promptNode : '', outputNode: template ? template.outputNode : outputs.length === 1 ? outputs[0][0] : '',
1466
+ sizeNode: template && template.sizeNode || '', parameters: template && template.parameters || [] });
1467
+ original.current = Object.fromEntries((p.models || []).map(function (model) { return [model.id, model.comfyWorkflow && model.comfyWorkflow.sha256 || null]; }));
1468
+ namePair[1](file.name.replace(/\.json$/i, '').slice(0, 120)); reportPair[1](null); replacePair[1](false);
1469
+ notePair[1]('已读取 ' + entries.length + ' 个节点。请核对绑定,再勾选希望在画图时调整的参数。');
1470
+ } catch (error) { if (ticket === importVersion.current) notePair[1]('导入失败:' + error.message + ';原草稿保留。'); }
1471
+ }
1472
+ async function operate(kind, confirmed) {
1473
+ if (active.current || unknownPair[0]) return;
1474
+ var workflow, name = namePair[0].trim();
1475
+ try { workflow = comfyDraftWorkflow(draft); if (kind === 'save' && !name) throw new Error('请填写工作流名称'); }
1476
+ catch (error) { notePair[1](error.message); return; }
1477
+ var exists = Object.hasOwn(original.current, name);
1478
+ if (kind === 'save' && exists && !confirmed) { replacePair[1](true); return; }
1479
+ var controller = new AbortController(); active.current = controller; busyPair[1](kind); notePair[1]('');
1480
+ var timer = setTimeout(function () { controller.abort(); }, 20000);
1481
+ try {
1482
+ var result = await postAction(kind === 'check' ? 'providers_check_comfy_workflow' : 'providers_save_comfy_workflow', kind === 'check'
1483
+ ? { id: p.id, workflow: workflow } : { id: p.id, model_id: name, workflow: workflow,
1484
+ expectedSha: exists ? original.current[name] : null, enableAutomatic: autoPair[0] }, { signal: controller.signal });
1485
+ if (controller.signal.aborted) throw new Error('请求已结束');
1486
+ if (!result.ok || result.d.ok !== true) { notePair[1](result.d.error || '操作未完成'); return; }
1487
+ if (kind === 'check') reportPair[1](result.d.report);
1488
+ else {
1489
+ original.current[name] = result.d.comfyWorkflow.sha256; replacePair[1](false); notePair[1](result.d.text);
1490
+ props.onDone(); window.dispatchEvent(new CustomEvent('iris-function-configuration-changed'));
1491
+ }
1492
+ } catch (_) {
1493
+ if (kind === 'save') { unknownPair[1](true); notePair[1]('保存结果未知,未自动重试。请关闭并重新打开此供应商,核对已保存工作流;草稿仍在此处。'); }
1494
+ else { reportPair[1](null); notePair[1]('检查未完成,请核对连接后重试;未发起生成。'); }
1495
+ } finally { clearTimeout(timer); active.current = null; busyPair[1](''); }
1496
+ }
1497
+ var entries = draft ? Object.entries(draft.graph) : [], slots = draft ? comfySlots(draft) : [];
1498
+ function select(field, label, options) {
1499
+ return React.createElement('label', {}, label, React.createElement('select', { value: draft[field], onChange: function (event) { change(field, event.target.value); } },
1500
+ React.createElement('option', { value: '' }, field === 'sizeNode' ? '沿用工作流尺寸' : '请选择'),
1501
+ options.map(function (entry) { return React.createElement('option', { key: entry[0], value: entry[0] }, '#' + entry[0] + ' · ' + entry[1].class_type); })));
1502
+ }
1503
+ return React.createElement('section', { className: 'iris-comfy-panel', 'aria-label': 'ComfyUI 工作流' },
1504
+ React.createElement('div', { className: 'iris-comfy-head' }, React.createElement('strong', {}, 'ComfyUI · 工作流'), React.createElement('span', { className: 'iris-comfy-tag' }, '已有服务 · 单张文生图')),
1505
+ React.createElement('p', { className: 'iris-pm-note' }, '导入 API 工作流,保留已有节点与 LoRA。检查和保存不会生成图片。'),
1506
+ React.createElement('fieldset', { disabled: !!busyPair[0] || unknownPair[0], style: { border: 0, padding: 0, margin: 0, minWidth: 0, display: 'grid', gap: 10 } },
1507
+ React.createElement('label', {}, '导入工作流 JSON', React.createElement('input', { type: 'file', accept: '.json,application/json', onChange: read, 'data-comfy-file': p.id })),
1508
+ draft ? React.createElement(React.Fragment, {},
1509
+ React.createElement('label', {}, '工作流名称', React.createElement('input', { value: namePair[0], maxLength: 120, onChange: function (event) { namePair[1](event.target.value); replacePair[1](false); } })),
1510
+ React.createElement('div', { className: 'iris-comfy-grid' },
1511
+ select('promptNode', '正向提示词节点', entries.filter(function (entry) { return typeof entry[1].inputs.text === 'string'; })),
1512
+ select('outputNode', '图片输出节点', entries),
1513
+ select('sizeNode', '尺寸节点', entries.filter(function (entry) { return ['width', 'height'].every(function (key) { return Number.isSafeInteger(entry[1].inputs[key]) && entry[1].inputs[key] > 0; }); }))),
1514
+ React.createElement('details', {}, React.createElement('summary', {}, '公开参数 · 已选 ' + slots.filter(function (slot) { return slot.selected; }).length + ' / 32'),
1515
+ React.createElement('div', { className: 'iris-comfy-slots' }, slots.map(function (slot) {
1516
+ return React.createElement('div', { key: slot.key, className: 'iris-comfy-slot' },
1517
+ React.createElement('label', {}, React.createElement('input', { type: 'checkbox', checked: slot.selected, onChange: function (event) {
1518
+ if (event.target.checked && draft.parameters.length >= 32) { notePair[1]('最多公开 32 个参数'); return; }
1519
+ change('parameters', draft.parameters.filter(function (item) { return item.node !== slot.parameter.node || item.input !== slot.parameter.input; }).concat(event.target.checked ? [slot.parameter] : []));
1520
+ } }), slot.parameter.label || slot.parameter.id),
1521
+ slot.selected ? React.createElement('select', { 'aria-label': (slot.parameter.label || slot.parameter.id) + '类型', value: slot.parameter.type, onChange: function (event) {
1522
+ change('parameters', draft.parameters.map(function (item) { return item.id === slot.parameter.id ? { id: item.id, node: item.node, input: item.input, label: item.label, type: event.target.value } : item; }));
1523
+ } }, (typeof slot.value === 'number' ? [['integer', '整数'], ['number', '数值']] : [[typeof slot.value, typeof slot.value === 'boolean' ? '开关' : '文本']]).map(function (pair) { return React.createElement('option', { key: pair[0], value: pair[0] }, pair[1]); })) : null);
1524
+ }))),
1525
+ React.createElement('label', { style: { display: 'flex', alignItems: 'center' } }, React.createElement('input', { type: 'checkbox', checked: autoPair[0], onChange: function (event) { autoPair[1](event.target.checked); } }), '保存时将图片功能设为自动池'),
1526
+ React.createElement('div', { className: 'iris-wb-row' },
1527
+ React.createElement('button', { className: 'iris-pm-btn', onClick: function () { operate('check'); } }, busyPair[0] === 'check' ? '检查中…' : '只读检查'),
1528
+ React.createElement('button', { className: 'iris-act-run', onClick: function () { operate('save'); } }, busyPair[0] === 'save' ? '保存中…' : '保存工作流')),
1529
+ replacePair[0] ? React.createElement('div', { className: 'iris-comfy-result', role: 'group', 'aria-label': '确认替换工作流' },
1530
+ React.createElement('p', {}, '替换「' + namePair[0] + '」?旧任务不会改绑,更换模板可能影响旧任务的继续观察。'),
1531
+ React.createElement('button', { className: 'iris-pm-btn', onClick: function () { operate('save', true); } }, '确认替换'),
1532
+ React.createElement('button', { className: 'iris-pm-btn', onClick: function () { replacePair[1](false); } }, '取消')) : null) : null),
1533
+ notePair[0] ? React.createElement('div', { className: 'iris-comfy-result', role: 'status' }, notePair[0]) : null,
1534
+ React.createElement(ComfyCheckResult, { report: reportPair[0] }));
1347
1535
  }
1348
1536
 
1349
1537
  function ProviderManager() {
@@ -1355,7 +1543,8 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1355
1543
  var addingPair = React.useState(false);
1356
1544
  var adding = addingPair[0];
1357
1545
  var setAdding = addingPair[1];
1358
- var newForm = React.useState({ name: '', baseUrl: '', mediaBaseUrl: '', apiKey: '', type: 'openai', mediaProtocol: 'auto' });
1546
+ var newForm = React.useState({ name: '', baseUrl: '', mediaBaseUrl: '', apiKey: '', auth: 'bearer', type: 'openai', mediaProtocol: 'auto' });
1547
+ var savingRef = React.useRef(false), savingPair = React.useState(false), unknownPair = React.useState(false);
1359
1548
  var hostConnections = React.useState([]);
1360
1549
  var hostChoice = React.useState('');
1361
1550
  var setupPair = React.useState(false);
@@ -1379,23 +1568,25 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1379
1568
  return function () { clearInterval(timer); };
1380
1569
  }, []);
1381
1570
  function addProvider() {
1571
+ if (savingRef.current || unknownPair[0]) return;
1382
1572
  var f = newForm[0];
1573
+ var payload;
1383
1574
  if (hostChoice[0]) {
1384
1575
  var connection = hostConnections[0].find(function (item) { return JSON.stringify([item.settingsNs, item.provider]) === hostChoice[0]; });
1385
1576
  if (!connection || !connection.available) { setNote('请重新选择可用的 DSH 连接'); return; }
1386
- postAction('providers_upsert', { name: f.name || connection.name, type: 'openai', enabled: true,
1387
- mediaProtocol: f.mediaProtocol, dshProvider: { provider: connection.provider, settingsNs: connection.settingsNs } })
1388
- .then(function (r) {
1389
- if (r.ok) { setAdding(false); openPair[1](r.d.providerId); setNote('已引用 DSH 连接,请在下方确认模型用途,即可启用自动池'); refresh(); }
1390
- else setNote(r.d.error || '添加失败');
1391
- });
1392
- return;
1577
+ payload = { name: f.name || connection.name, type: 'openai', enabled: true,
1578
+ mediaProtocol: f.mediaProtocol, dshProvider: { provider: connection.provider, settingsNs: connection.settingsNs } };
1579
+ } else {
1580
+ if (!f.baseUrl.trim() || (f.auth !== 'none' && !f.apiKey.trim())) { setNote('请填写 Base URL;Bearer 认证还需要 API Key'); return; }
1581
+ payload = { ...f, ...(f.auth === 'none' ? { apiKey: '' } : {}) };
1393
1582
  }
1394
- if (!f.baseUrl || !f.apiKey) { setNote('baseUrl 和 apiKey 必填'); return; }
1395
- postAction('providers_upsert', f).then(function (r) {
1396
- if (r.ok) { setAdding(false); openPair[1](r.d.providerId); setNote('已连接,请在下方确认模型用途'); refresh(); }
1583
+ savingRef.current = true; savingPair[1](true);
1584
+ var controller = new AbortController(), timer = setTimeout(function () { controller.abort(); }, 15000);
1585
+ postAction('providers_upsert', payload, { signal: controller.signal }).then(function (r) {
1586
+ if (r.ok) { setAdding(false); openPair[1](r.d.providerId); setNote(f.mediaProtocol === 'comfyui' ? '连接已保存,请导入 API 工作流;尚未生成图片' : '已连接,请在下方确认模型用途'); refresh(); }
1397
1587
  else setNote(r.d.error || '添加失败');
1398
- });
1588
+ }).catch(function () { unknownPair[1](true); setNote('保存结果未知,请关闭添加表单并核对供应商列表;未自动重试。'); })
1589
+ .finally(function () { clearTimeout(timer); savingRef.current = false; savingPair[1](false); });
1399
1590
  }
1400
1591
  function toggle(p) {
1401
1592
  postAction('providers_upsert', { id: p.id, enabled: !p.enabled }).then(function (r) {
@@ -1426,19 +1617,26 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1426
1617
  var body = null;
1427
1618
  if (open) {
1428
1619
  body = React.createElement('div', { className: 'iris-pm-body', id: 'iris-provider-' + p.id },
1429
- React.createElement('div', { className: 'iris-pm-note' }, 'Key ' + (p.apiKeyHint || '未配置') + ' · ' + (p.type || '') + ' · ' + (p.mediaProtocol || '')),
1620
+ React.createElement('div', { className: 'iris-pm-note' }, (p.auth === 'none' ? '无需认证' : 'Key ' + (p.apiKeyHint || '未配置')) + ' · ' + (p.type || '') + ' · ' + (p.mediaProtocol || '')),
1430
1621
  p.dshProvider ? React.createElement('div', { className: 'iris-pm-note' }, '引用 DSH:' + p.dshProvider.provider
1431
1622
  + ' · 地址和密钥由宿主管理;' + (p.connection && p.connection.available ? '连接可用' : ((p.connection && p.connection.reason) || '连接不可用'))) : null,
1432
1623
  p.protocolInferred ? React.createElement('div', { className: 'iris-pm-note' }, '协议为推断值,请确认') : null,
1433
- React.createElement(ProviderEndpointEditor, { provider: p, onNote: setNote, onDone: refresh }),
1434
- React.createElement(VisionInputEditor, { provider: p, onNote: setNote, onDone: refresh }),
1624
+ p.mediaProtocol === 'comfyui' ? React.createElement('details', {}, React.createElement('summary', {}, '连接设置'),
1625
+ React.createElement('div', { className: 'iris-pm-note' }, '服务地址:' + (p.mediaBaseUrl || p.baseUrl)),
1626
+ React.createElement(ProviderEndpointEditor, { provider: p, onNote: setNote, onDone: refresh }),
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 }),
1629
+ !p.dshProvider ? React.createElement(ProviderAuthenticationEditor, { provider: p, onDone: refresh }) : null,
1630
+ React.createElement(VisionInputEditor, { provider: p, onNote: setNote, onDone: refresh })),
1435
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); } },
1436
1632
  React.createElement('option', { value: 'auto' }, '自动'),
1437
- p.mediaProtocol && !['dashscope', 'openai-images', 'openai-chat-images', 'openai-responses-images', 'auto'].includes(p.mediaProtocol) ? React.createElement('option', { value: p.mediaProtocol }, p.mediaProtocol + '(未支持)') : null,
1633
+ p.mediaProtocol && !['dashscope', 'openai-images', 'openai-chat-images', 'openai-responses-images', 'comfyui', 'auto'].includes(p.mediaProtocol) ? React.createElement('option', { value: p.mediaProtocol }, p.mediaProtocol + '(未支持)') : null,
1438
1634
  React.createElement('option', { value: 'dashscope' }, 'DashScope(仅阿里云官方 HTTPS)'),
1439
1635
  React.createElement('option', { value: 'openai-images' }, 'OpenAI Images 兼容'),
1440
1636
  React.createElement('option', { value: 'openai-chat-images' }, '聊天生图(网关兼容)'),
1441
- React.createElement('option', { value: 'openai-responses-images' }, 'Responses 生图'))),
1637
+ React.createElement('option', { value: 'openai-responses-images' }, 'Responses 生图'),
1638
+ !p.dshProvider ? React.createElement('option', { value: 'comfyui' }, 'ComfyUI · API 工作流') : null)),
1639
+ p.mediaProtocol === 'comfyui' ? React.createElement(ComfyWorkflowEditor, { key: p.id, provider: p, setupSuggested: setupPair[0], onDone: refresh }) : null,
1442
1640
  React.createElement('div', { className: 'iris-pm-note' }, '能力: ' + ((p.capabilities || []).join(' / ') || '无')),
1443
1641
  React.createElement(ProviderDirectory, { provider: p, onDone: refresh }),
1444
1642
  React.createElement(ModelPool, { provider: p, setupSuggested: setupPair[0], onNote: setNote, onDone: refresh, testModel: testModel, removeModel: removeModel }),
@@ -1455,9 +1653,17 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1455
1653
  });
1456
1654
  var addBlock = null;
1457
1655
  if (adding) {
1458
- addBlock = React.createElement('div', { className: 'iris-pm-card' },
1656
+ addBlock = React.createElement('fieldset', { className: 'iris-pm-card' + (newForm[0].mediaProtocol === 'comfyui' ? ' iris-comfy-panel' : ''), disabled: savingPair[0] || unknownPair[0] },
1657
+ !hostChoice[0] ? React.createElement('div', { className: 'iris-pm-field' }, React.createElement('label', {}, '连接预设'),
1658
+ React.createElement('select', { value: newForm[0].mediaProtocol === 'comfyui' ? 'comfyui' : 'custom', onChange: function (e) {
1659
+ newForm[1](e.target.value === 'comfyui' ? { name: 'ComfyUI', baseUrl: 'http://127.0.0.1:8188', mediaBaseUrl: '', apiKey: '', type: 'openai', auth: 'none', mediaProtocol: 'comfyui' }
1660
+ : { name: '', baseUrl: '', mediaBaseUrl: '', apiKey: '', type: 'openai', auth: 'bearer', mediaProtocol: 'auto' });
1661
+ } }, React.createElement('option', { value: 'custom' }, '自定义供应商'), React.createElement('option', { value: 'comfyui' }, 'ComfyUI · 已有服务'))) : null,
1459
1662
  React.createElement('div', { className: 'iris-pm-field' }, React.createElement('label', {}, '连接来源'),
1460
- React.createElement('select', { value: hostChoice[0], onChange: function (e) { hostChoice[1](e.target.value); } },
1663
+ React.createElement('select', { value: hostChoice[0], onChange: function (e) {
1664
+ hostChoice[1](e.target.value);
1665
+ if (e.target.value && newForm[0].mediaProtocol === 'comfyui') newForm[1]({ name: '', baseUrl: '', mediaBaseUrl: '', apiKey: '', auth: 'bearer', type: 'openai', mediaProtocol: 'auto' });
1666
+ } },
1461
1667
  React.createElement('option', { value: '' }, '手动填写供应商'),
1462
1668
  hostConnections[0].map(function (connection) { return React.createElement('option', {
1463
1669
  key: JSON.stringify([connection.settingsNs, connection.provider]), value: JSON.stringify([connection.settingsNs, connection.provider]), disabled: !connection.available
@@ -1466,26 +1672,82 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1466
1672
  React.createElement('div', { className: 'iris-pm-field' }, React.createElement('label', {}, '名称'), React.createElement('input', { value: newForm[0].name, placeholder: '如 阿里云百炼', onChange: function (e) { newForm[1]({ ...newForm[0], name: e.target.value }); } })),
1467
1673
  !hostChoice[0] ? React.createElement('div', { className: 'iris-pm-field' }, React.createElement('label', {}, 'Base URL *'), React.createElement('input', { value: newForm[0].baseUrl, placeholder: 'https://dashscope.aliyuncs.com/compatible-mode/v1', onChange: function (e) { newForm[1]({ ...newForm[0], baseUrl: e.target.value }); } })) : null,
1468
1674
  !hostChoice[0] ? React.createElement('div', { className: 'iris-pm-field' }, React.createElement('label', {}, '媒体 Base URL(可选)'), React.createElement('input', { value: newForm[0].mediaBaseUrl, placeholder: 'Workspace 媒体端点;留空沿用 Base URL', onChange: function (e) { newForm[1]({ ...newForm[0], mediaBaseUrl: e.target.value }); } })) : null,
1469
- !hostChoice[0] ? React.createElement('div', { className: 'iris-pm-field' }, React.createElement('label', {}, 'API Key *'), React.createElement('input', { type: 'password', value: newForm[0].apiKey, placeholder: 'sk-...', onChange: function (e) { newForm[1]({ ...newForm[0], apiKey: e.target.value }); } })) : null,
1675
+ !hostChoice[0] ? React.createElement('div', { className: 'iris-pm-field' }, React.createElement('label', {}, '认证方式'), React.createElement('select', { value: newForm[0].auth, onChange: function (e) { newForm[1]({ ...newForm[0], auth: e.target.value }); } },
1676
+ React.createElement('option', { value: 'bearer' }, 'Bearer · API Key'), React.createElement('option', { value: 'none' }, '无需认证(本地或内网服务)'))) : null,
1677
+ !hostChoice[0] && newForm[0].auth !== 'none' ? React.createElement('div', { className: 'iris-pm-field' }, React.createElement('label', {}, 'API Key *'), React.createElement('input', { type: 'password', value: newForm[0].apiKey, placeholder: 'sk-...', onChange: function (e) { newForm[1]({ ...newForm[0], apiKey: e.target.value }); } })) : null,
1470
1678
  React.createElement('div', { className: 'iris-pm-field' }, React.createElement('label', {}, '媒体协议'), React.createElement('select', { value: newForm[0].mediaProtocol, onChange: function (e) { newForm[1]({ ...newForm[0], mediaProtocol: e.target.value }); } },
1471
1679
  React.createElement('option', { value: 'auto' }, hostChoice[0] ? '请选择该服务支持的媒体协议' : '自动(按媒体 Base URL 安全判断)'),
1472
1680
  React.createElement('option', { value: 'dashscope' }, 'DashScope(阿里云官方)'),
1473
1681
  React.createElement('option', { value: 'openai-images' }, 'OpenAI Images 兼容'),
1474
1682
  React.createElement('option', { value: 'openai-chat-images' }, '聊天生图(网关兼容)'),
1475
- React.createElement('option', { value: 'openai-responses-images' }, 'Responses 生图'))),
1683
+ React.createElement('option', { value: 'openai-responses-images' }, 'Responses 生图'),
1684
+ !hostChoice[0] ? React.createElement('option', { value: 'comfyui' }, 'ComfyUI · API 工作流') : null)),
1685
+ newForm[0].mediaProtocol === 'comfyui' ? React.createElement('p', { className: 'iris-pm-note' }, '连接已有 ComfyUI 服务。先保存连接,再导入 Export (API) 工作流;Iris 不安装服务或模型。') : null,
1476
1686
  React.createElement('div', { className: 'iris-wb-row' },
1477
- React.createElement('button', { className: 'iris-act-run', onClick: addProvider }, '保存'),
1687
+ React.createElement('button', { className: 'iris-act-run', onClick: addProvider }, savingPair[0] ? '保存中…' : '保存'),
1478
1688
  React.createElement('button', { className: 'iris-pm-btn', onClick: function () { setAdding(false); } }, '取消')));
1479
1689
  }
1480
1690
  return React.createElement('div', { className: 'iris-pm' },
1481
1691
  React.createElement('div', { className: 'iris-pm-head' },
1482
1692
  React.createElement('span', { className: 'iris-wb-muted' }, (list ? list.length : 0) + ' 个供应商'),
1483
- React.createElement('button', { className: 'iris-pm-add', onClick: function () { if (!adding) { hostChoice[1](''); loadHostConnections(); } setAdding(!adding); } }, adding ? '收起' : '+ 添加供应商')),
1693
+ React.createElement('button', { className: 'iris-pm-add', disabled: savingPair[0], onClick: function () { if (!adding) { hostChoice[1](''); loadHostConnections(); } unknownPair[1](false); setAdding(!adding); } }, adding ? '收起' : '+ 添加供应商')),
1484
1694
  addBlock,
1485
1695
  cards.length ? cards : React.createElement('div', { className: 'iris-wb-empty' }, '暂无供应商 — 点右上角「+ 添加供应商」'),
1486
1696
  note ? React.createElement('div', { className: 'iris-pm-act' }, note) : null);
1487
1697
  }
1488
1698
 
1699
+ function comfyParameterLabel(parameter) {
1700
+ return parameter.label || ({ lora_name: 'LoRA', strength_model: '模型权重', strength_clip: 'CLIP 权重',
1701
+ ckpt_name: 'Checkpoint', seed: '种子', steps: '步数', sampler_name: '采样器', scheduler: '调度器', cfg: 'CFG' })[parameter.input]
1702
+ || (parameter.id === 'negative' && parameter.input === 'text' ? '负面提示词' : parameter.id);
1703
+ }
1704
+ function ComfyParameters(props) {
1705
+ var option = props.option, parameters = option.comfyWorkflow.parameters || [];
1706
+ var reportPair = React.useState(null), choicesPair = React.useState([]), notePair = React.useState(''), busyPair = React.useState(false);
1707
+ var active = React.useRef(null), sequence = React.useRef(0);
1708
+ React.useEffect(function () { return function () { sequence.current++; if (active.current) active.current.abort(); }; }, []);
1709
+ function change(id, value) {
1710
+ sequence.current++; if (active.current) active.current.abort(); active.current = null; busyPair[1](false);
1711
+ reportPair[1](null); notePair[1]('参数已更改,提交前将重新预检。'); props.onChange({ ...props.values, [id]: value });
1712
+ }
1713
+ async function check() {
1714
+ if (active.current || props.disabled) return;
1715
+ var controller = new AbortController(), ticket = ++sequence.current; active.current = controller; busyPair[1](true); notePair[1]('');
1716
+ var timer = setTimeout(function () { controller.abort(); }, 20000);
1717
+ try {
1718
+ var result = await postAction('providers_check_comfy_workflow', { id: option.providerId, model_id: option.id,
1719
+ ...(Object.keys(props.values).length ? { workflow_parameters: props.values } : {}) }, { signal: controller.signal });
1720
+ if (ticket !== sequence.current) return;
1721
+ if (!result.ok || result.d.ok !== true) { reportPair[1](null); notePair[1](result.d.error || '检查失败'); return; }
1722
+ reportPair[1](result.d.report); choicesPair[1](result.d.report.parameters || []);
1723
+ } catch (_) { if (ticket === sequence.current) { reportPair[1](null); notePair[1]('检查未完成;未发起生成。可核对服务后重试。'); } }
1724
+ finally { clearTimeout(timer); if (ticket === sequence.current) { active.current = null; busyPair[1](false); } }
1725
+ }
1726
+ return React.createElement('section', { className: 'iris-comfy-panel', 'aria-label': 'ComfyUI 画图参数' },
1727
+ React.createElement('div', { className: 'iris-comfy-head' }, React.createElement('strong', {}, 'ComfyUI · ' + option.id), React.createElement('span', { className: 'iris-comfy-tag' }, '单张文生图')),
1728
+ React.createElement('div', { className: 'iris-pm-note' }, '沿用工作流默认值,只调整已公开的参数。LoRA 候选可通过只读检查获取。'),
1729
+ parameters.length ? React.createElement('details', {}, React.createElement('summary', {}, '工作流参数 · ' + parameters.length),
1730
+ React.createElement('fieldset', { className: 'iris-comfy-grid', disabled: props.disabled, style: { border: 0, padding: 0, margin: '8px 0 0', minWidth: 0 } }, parameters.map(function (parameter) {
1731
+ var value = Object.hasOwn(props.values, parameter.id) ? props.values[parameter.id] : parameter.default;
1732
+ var checked = choicesPair[0].find(function (item) { return item.id === parameter.id; });
1733
+ var choices = checked && checked.availableChoices || parameter.choices, input;
1734
+ if (choices) input = React.createElement('select', { value: JSON.stringify(value), onChange: function (event) { change(parameter.id, JSON.parse(event.target.value)); } },
1735
+ !choices.includes(value) ? React.createElement('option', { value: JSON.stringify(value) }, String(value) + '(当前未列出)') : null,
1736
+ choices.map(function (choice) { return React.createElement('option', { key: JSON.stringify(choice), value: JSON.stringify(choice) }, String(choice)); }));
1737
+ else if (parameter.type === 'boolean') input = React.createElement('input', { type: 'checkbox', checked: value, onChange: function (event) { change(parameter.id, event.target.checked); } });
1738
+ else input = React.createElement(parameter.type === 'string' && parameter.input === 'text' ? 'textarea' : 'input', {
1739
+ type: parameter.type === 'string' ? 'text' : 'number', value: value, min: parameter.min, max: parameter.max, step: parameter.type === 'integer' ? 1 : 'any',
1740
+ onChange: function (event) { var text = event.target.value; change(parameter.id, parameter.type === 'string' || text === '' ? text : Number(text)); }
1741
+ });
1742
+ return React.createElement('label', { key: parameter.id }, comfyParameterLabel(parameter), input,
1743
+ checked && checked.choicesTruncated ? React.createElement('span', { className: 'iris-pm-note' }, '选项仅列前 256 项,可手填名称:',
1744
+ React.createElement('input', { type: 'text', value: value, onChange: function (event) { change(parameter.id, event.target.value); } })) : null);
1745
+ }))) : React.createElement('div', { className: 'iris-pm-note' }, '此模板未公开额外参数。'),
1746
+ React.createElement('div', {}, React.createElement('button', { className: 'iris-pm-btn', disabled: props.disabled || busyPair[0], onClick: check }, busyPair[0] ? '检查中…' : '只读检查并读取选项')),
1747
+ notePair[0] ? React.createElement('div', { className: 'iris-comfy-result', role: 'status' }, notePair[0]) : null,
1748
+ React.createElement(ComfyCheckResult, { report: reportPair[0] }));
1749
+ }
1750
+
1489
1751
  /* ---- 文件字段(阶段 10):让用户"看见并选文件",统一产出一个宿主路径 ---- */
1490
1752
  function FileField(props) {
1491
1753
  var value = props.value || '';
@@ -1546,7 +1808,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1546
1808
  manual ? React.createElement('input', { className: 'iris-pm-madd', placeholder: '高级:粘贴 DSH 宿主上的绝对路径,如 /home/user/pic.png', value: value, onChange: function (e) { onChange(e.target.value); } }) : null,
1547
1809
  menu === 'att' ? React.createElement('div', { className: 'iris-ff-list' },
1548
1810
  (atts && atts.length) ? atts.map(function (a) {
1549
- return React.createElement('div', { key: a.attachmentId, className: 'iris-ff-item', onClick: function () { pickAttachment(a); } },
1811
+ return React.createElement('button', { type: 'button', key: a.attachmentId, className: 'iris-ff-item', onClick: function () { pickAttachment(a); } },
1550
1812
  React.createElement('span', { className: 'iris-ff-src' }, a.source === 'iris' ? '🫧' : '🖼'),
1551
1813
  React.createElement('span', {}, a.name || a.attachmentId.slice(0, 18)));
1552
1814
  }) : React.createElement('div', { className: 'iris-wb-empty' }, '无可选附件(会话无图或 iris 未生成过)')) : null);
@@ -1570,6 +1832,9 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1570
1832
  var modelPair = React.useState(null); // { assigned, options:[{id,providerId}], tested }
1571
1833
  var modelInfo = modelPair[0];
1572
1834
  var setModelInfo = modelPair[1];
1835
+ var submitting = React.useRef(false);
1836
+ var selectedOption = modelInfo && modelInfo.options.find(function (option) { return option.ref === vals.model; });
1837
+ var selectedComfy = action === 'image' && selectedOption && selectedOption.comfyWorkflow;
1573
1838
  var irisState = useIrisState(5000);
1574
1839
  function setVal(key, value) { var nv = {}; Object.keys(vals).forEach(function (k) { nv[k] = vals[k]; }); nv[key] = value; setVals(nv); }
1575
1840
  function loadModels(event) {
@@ -1595,19 +1860,30 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1595
1860
  return function () { window.removeEventListener('iris-function-configuration-changed', loadModels); };
1596
1861
  }, [open]);
1597
1862
  function submit() {
1598
- if (run === 'running') return;
1863
+ if (submitting.current || run === 'running' || run && run.unknown) return;
1864
+ var payload = vals;
1865
+ if (selectedComfy) {
1866
+ var values = vals.workflow_parameters || {}, declared = selectedComfy.parameters || [];
1867
+ if (Object.keys(values).some(function (id) { var field = declared.find(function (item) { return item.id === id; }), value = values[id];
1868
+ return !field || (field.type === 'integer' ? !Number.isSafeInteger(value) : field.type === 'number' ? typeof value !== 'number' || !Number.isFinite(value) : typeof value !== field.type)
1869
+ || field.min !== undefined && value < field.min || field.max !== undefined && value > field.max || field.choices && !field.choices.includes(value);
1870
+ })) { setRun({ ok: false, text: '请检查工作流参数的类型、范围和选项;尚未发起生成。' }); return; }
1871
+ payload = { ...vals, workflow_sha: selectedComfy.sha256 };
1872
+ }
1873
+ submitting.current = true;
1599
1874
  setRun('running');
1600
1875
  fetch('/iris/api/actions/' + action, {
1601
1876
  method: 'POST',
1602
1877
  headers: { 'Content-Type': 'application/json' },
1603
- body: JSON.stringify(vals)
1878
+ body: JSON.stringify(payload)
1604
1879
  })
1605
1880
  .then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
1606
1881
  .then(function (res) {
1607
1882
  setRun(res.ok ? { ok: true, text: res.d.text || '', img: res.d.imageUrl || res.d.imageDataUrl || null, audio: res.d.audioUrl || null } : { ok: false, text: res.d.error || '执行失败' });
1608
1883
  if (res.ok && res.d && res.d.storage === 'core') window.dispatchEvent(new CustomEvent('iris-core-artifacts-changed'));
1609
1884
  })
1610
- .catch(function () { setRun({ ok: false, text: '网络错误' }); });
1885
+ .catch(function () { setRun({ ok: false, unknown: !!selectedComfy, text: selectedComfy ? '提交结果未知,请到任务列表核对;未自动重试。' : '网络错误' }); })
1886
+ .finally(function () { submitting.current = false; });
1611
1887
  }
1612
1888
  var cardHealth = capability ? capabilityHealth(irisState, capability) : { status: 'verified' };
1613
1889
  // Host references can be ready without raw endpoint/key fields in the state snapshot.
@@ -1620,7 +1896,9 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1620
1896
  var inputs = fields.map(function (f) {
1621
1897
  var node;
1622
1898
  if (f.key === 'model' && capability) {
1623
- node = React.createElement('select', { value: vals.model || '', onChange: function (e) { setVal('model', e.target.value); } },
1899
+ node = React.createElement('select', { value: vals.model || '', disabled: run === 'running', onChange: function (e) {
1900
+ var next = { ...vals, model: e.target.value }; delete next.workflow_parameters; delete next.workflow_sha; setVals(next);
1901
+ } },
1624
1902
  React.createElement('option', { value: '' }, modelInfo && modelInfo.mode === 'auto' ? '按功能池顺序自动选择' : '按所选模型顺序'),
1625
1903
  ((modelInfo && modelInfo.options) || []).map(function (option) {
1626
1904
  return React.createElement('option', { key: option.ref, value: option.ref, disabled: option.unavailable }, option.label + (option.unavailable ? ' · 当前不可用' : ''));
@@ -1678,16 +1956,18 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1678
1956
  body = React.createElement('div', { className: 'iris-act-body' },
1679
1957
  modelBar,
1680
1958
  inputs,
1681
- React.createElement('button', { className: 'iris-act-run', disabled: run === 'running', onClick: submit },
1682
- run === 'running' ? '执行中…' : (props.submitLabel || '执行')),
1959
+ selectedComfy ? React.createElement(ComfyParameters, { key: selectedOption.ref + '@' + selectedComfy.sha256, option: selectedOption,
1960
+ values: vals.workflow_parameters || {}, disabled: run === 'running', onChange: function (values) { setVal('workflow_parameters', values); } }) : null,
1961
+ React.createElement('button', { className: 'iris-act-run', disabled: run === 'running' || !!(run && run.unknown), onClick: submit },
1962
+ run === 'running' ? '执行中…' : (props.submitLabel || (selectedComfy ? '生成图片' : '执行'))),
1683
1963
  result);
1684
1964
  }
1685
1965
  var headStatus = capability ? cardHealth.status : 'verified';
1686
1966
  var headTitle = capability ? healthText(cardHealth) : '本地功能';
1687
1967
  return React.createElement('div', {
1688
- className: 'iris-act-card' + (open ? ' open' : '') + (capability ? ' health-' + headStatus : ''), 'data-iris-action': action
1968
+ className: 'iris-act-card' + (open ? ' open' : '') + (selectedComfy ? ' iris-comfy-card' : '') + (capability ? ' health-' + headStatus : ''), 'data-iris-action': action
1689
1969
  },
1690
- React.createElement('div', { className: 'iris-act-head', onClick: function () { setOpen(!open); } },
1970
+ React.createElement('button', { type: 'button', className: 'iris-act-head', 'aria-expanded': open, onClick: function () { setOpen(!open); } },
1691
1971
  React.createElement('span', { className: 'iris-act-title' }, title),
1692
1972
  capability ? React.createElement('span', {
1693
1973
  className: 'iris-pm-dot ' + healthClass(cardHealth),
@@ -1987,8 +2267,8 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1987
2267
  if (!rows.length) return null;
1988
2268
  var isCollapsed = !!collapsed[name];
1989
2269
  return React.createElement('div', { key: name, className: 'iris-hist-group' },
1990
- React.createElement('div', {
1991
- className: 'iris-hist-head',
2270
+ React.createElement('button', {
2271
+ type: 'button', className: 'iris-hist-head', 'aria-expanded': !isCollapsed,
1992
2272
  onClick: function () { var n = {}; Object.keys(collapsed).forEach(function (k) { n[k] = collapsed[k]; }); n[name] = !isCollapsed; setCollapsed(n); }
1993
2273
  }, React.createElement('span', {}, isCollapsed ? '▶' : '▼'), name + ' · ' + rows.length),
1994
2274
  isCollapsed ? null : React.createElement('div', { className: 'iris-wb-box' },
@@ -2070,11 +2350,12 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
2070
2350
  var pair = React.useState([]);
2071
2351
  var setItems = pair[1];
2072
2352
  React.useEffect(function () {
2073
- var alive = true;
2353
+ var alive = true, sequence = 0;
2074
2354
  function load() {
2355
+ var current = ++sequence;
2075
2356
  fetch('/iris/api/core/snapshot?limit=' + Math.max(1, Math.min(200, Number(limit) || 6)))
2076
2357
  .then(function (res) { return res.ok ? res.json() : null; })
2077
- .then(function (data) { if (alive && data) setItems(coreGalleryItems(data, limit)); })
2358
+ .then(function (data) { if (alive && current === sequence && data) setItems(coreGalleryItems(data, limit)); })
2078
2359
  .catch(function () { /* Core 不可用不影响旧作品 */ });
2079
2360
  }
2080
2361
  function changed() { load(); }
@@ -2590,12 +2871,13 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
2590
2871
  var pair = React.useState(EMPTY_CORE_USER);
2591
2872
  var setData = pair[1];
2592
2873
  React.useEffect(function () {
2593
- var alive = true;
2874
+ var alive = true, sequence = 0;
2594
2875
  function load() {
2876
+ var current = ++sequence;
2595
2877
  fetch('/iris/api/core/snapshot?limit=200')
2596
2878
  .then(function (res) { return res.ok ? res.json() : null; })
2597
2879
  .then(function (data) {
2598
- if (!alive || !data || !Array.isArray(data.userTasks)) return;
2880
+ if (!alive || current !== sequence || !data || !Array.isArray(data.userTasks)) return;
2599
2881
  setData({
2600
2882
  rows: data.userTasks,
2601
2883
  available: !!data.available,
@@ -2885,17 +3167,20 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
2885
3167
  var copyPair = React.useState('');
2886
3168
  var copyNote = copyPair[0];
2887
3169
  var setCopyNote = copyPair[1];
3170
+ var requestRef = React.useRef({ active: false, sequence: 0 });
2888
3171
  function load() {
2889
- if (busy) return;
3172
+ var request = requestRef.current;
3173
+ if (busy || !request.active) return;
3174
+ var current = ++request.sequence;
2890
3175
  setBusy(true);
2891
3176
  setError('');
2892
3177
  fetch('/iris/api/core/snapshot?limit=12')
2893
3178
  .then(function (res) {
2894
3179
  if (res.status === 404) return { available: false, backendReloadRequired: true, tasks: { total: 0, recent: [] }, artifacts: { total: 0, recent: [] } };
2895
3180
  return res.ok ? res.json() : Promise.reject(new Error('读取失败(HTTP ' + res.status + ')'));
2896
- }).then(setData)
2897
- .catch(function (caught) { setError('Core 运行事实读取失败:' + caught.message); })
2898
- .finally(function () { setBusy(false); });
3181
+ }).then(function (value) { if (request.active && current === request.sequence) setData(value); })
3182
+ .catch(function (caught) { if (request.active && current === request.sequence) setError('Core 运行事实读取失败:' + caught.message); })
3183
+ .finally(function () { if (request.active && current === request.sequence) setBusy(false); });
2899
3184
  }
2900
3185
  function copyCoreId(value, label) {
2901
3186
  setCopyNote('');
@@ -2909,11 +3194,14 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
2909
3194
  .catch(function () { setCopyNote('复制失败;请长按上方 ID 复制。'); });
2910
3195
  }
2911
3196
  React.useEffect(function () {
3197
+ requestRef.current.active = true;
2912
3198
  function changed() { load(); }
2913
3199
  load();
2914
3200
  window.addEventListener('iris-core-artifacts-changed', changed);
2915
3201
  window.addEventListener('iris-core-refresh-tick', changed);
2916
3202
  return function () {
3203
+ requestRef.current.active = false;
3204
+ requestRef.current.sequence++;
2917
3205
  window.removeEventListener('iris-core-artifacts-changed', changed);
2918
3206
  window.removeEventListener('iris-core-refresh-tick', changed);
2919
3207
  };
@@ -4118,6 +4406,11 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
4118
4406
 
4119
4407
  return React.createElement('div', {
4120
4408
  className: 'iris-bubble health-' + overallHealth + (dragRef.current ? ' dragging' : ''),
4409
+ role: 'button', tabIndex: 0, 'aria-label': 'Iris 快捷面板', 'aria-expanded': open,
4410
+ onKeyDown: function (event) {
4411
+ if (event.target !== event.currentTarget || (event.key !== 'Enter' && event.key !== ' ')) return;
4412
+ event.preventDefault(); setOpen(!open);
4413
+ },
4121
4414
  style: style,
4122
4415
  onPointerDown: onPointerDown,
4123
4416
  onPointerMove: onPointerMove,