@mokuyoaxis/dsh-iris 0.2.0 → 0.2.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 (88) hide show
  1. package/.dsh/skills/iris-compose-media/SKILL.md +10 -5
  2. package/.dsh/skills/iris-compose-media/references/workflows.md +7 -1
  3. package/.dsh/skills/iris-verify-ui/SKILL.md +2 -1
  4. package/CHANGELOG.md +47 -2
  5. package/README.en.md +65 -29
  6. package/README.md +65 -29
  7. package/bin/dsh-iris.js +222 -21
  8. package/docs/AGENT_QUICKSTART.md +10 -6
  9. package/docs/ARCHITECTURE.md +25 -18
  10. package/docs/ARTIFACT_LIBRARY.md +5 -5
  11. package/docs/ARTIFACT_MANIFEST.md +1 -1
  12. package/docs/CLI_MANAGEMENT.md +181 -0
  13. package/docs/COMPOSITE_VISION.md +35 -0
  14. package/docs/CORE_RUNTIME_CONTRACT.md +8 -7
  15. package/docs/DSH_CORE_MIGRATION.md +48 -19
  16. package/docs/DSH_RC2_ADAPTATION.md +4 -2
  17. package/docs/FAULT_INJECTION.md +12 -2
  18. package/docs/HEADLESS_CLI.md +13 -7
  19. package/docs/HOST_ADAPTER_CONTRACT.md +10 -10
  20. package/docs/HOST_DOCTOR.md +3 -3
  21. package/docs/MODEL_PORT_CONTRACT.md +19 -14
  22. package/docs/OCR_MODEL.md +39 -0
  23. package/docs/PROMPT_OPTIMIZER.md +47 -0
  24. package/docs/PROVIDER_ADAPTER_CONTRACT.md +6 -4
  25. package/docs/PROVIDER_HEALTH.md +22 -5
  26. package/docs/PROVIDER_RUNTIME_HARNESS.md +1 -1
  27. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +2 -2
  28. package/docs/ROADMAP.md +29 -19
  29. package/docs/SECURITY.md +5 -5
  30. package/docs/TASK_SEMANTICS.md +2 -2
  31. package/docs/VISION_CLI.md +132 -0
  32. package/docs/VISION_MODEL.md +51 -0
  33. package/docs/WORKBENCH_ARTIFACTS.md +67 -0
  34. package/docs/file-access-across-environments.md +8 -5
  35. package/docs/releases/0.2.1.md +56 -0
  36. package/lib/actions.js +187 -270
  37. package/lib/adapters.js +133 -22
  38. package/lib/api.js +49 -7
  39. package/lib/chromium-browser.js +117 -0
  40. package/lib/client.js +671 -101
  41. package/lib/command-service.js +121 -7
  42. package/lib/composite-vision-routing.js +42 -0
  43. package/lib/config.js +89 -10
  44. package/lib/core-artifact-store.js +45 -13
  45. package/lib/core-maintenance.js +213 -0
  46. package/lib/core-tasks.js +12 -5
  47. package/lib/core-user-projection.js +1 -0
  48. package/lib/dsh-core-adapter.js +63 -9
  49. package/lib/dsh-text-model-adapter.js +222 -0
  50. package/lib/dsh-vision-model-adapter.js +75 -0
  51. package/lib/generation-input.js +36 -5
  52. package/lib/headless-vision.js +181 -0
  53. package/lib/host-runtime.js +1 -0
  54. package/lib/http-vision-model-adapter.js +187 -0
  55. package/lib/image-edit-input.js +34 -0
  56. package/lib/index.js +205 -175
  57. package/lib/locate.js +86 -28
  58. package/lib/media-probe.js +2 -1
  59. package/lib/model-call-runtime.js +45 -0
  60. package/lib/model-invoker.js +4 -2
  61. package/lib/model-port-contract.js +13 -5
  62. package/lib/models.js +10 -5
  63. package/lib/ocr-model-routing.js +26 -0
  64. package/lib/ocr.js +110 -64
  65. package/lib/prompt-optimizer-config.js +2 -1
  66. package/lib/prompt-optimizer-core.js +121 -0
  67. package/lib/prompt-optimizer.js +37 -193
  68. package/lib/provider-adapters.js +85 -9
  69. package/lib/provider-catalog.js +19 -7
  70. package/lib/provider-config-service.js +236 -0
  71. package/lib/provider-contract.js +3 -2
  72. package/lib/provider-health.js +80 -1
  73. package/lib/provider-protocol.js +23 -0
  74. package/lib/provider-task-runner.js +58 -16
  75. package/lib/summarize.js +31 -20
  76. package/lib/summary-input.js +52 -0
  77. package/lib/task-wait.js +40 -0
  78. package/lib/video-input.js +30 -0
  79. package/lib/vision-core.js +42 -0
  80. package/lib/vision-image-input.js +83 -0
  81. package/lib/vision-input.js +31 -0
  82. package/lib/vision-model-routing.js +180 -0
  83. package/lib/vision.js +3 -2
  84. package/lib/workbench-artifacts.js +52 -0
  85. package/lib/workbench-management.js +151 -0
  86. package/lib/workbench-zip.js +39 -0
  87. package/package.json +1 -1
  88. package/user_guide.md +52 -22
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.0';
24
+ var IRIS_CLIENT_VERSION = '0.2.1';
25
25
  var HOST_CLIENT_PROTOCOL_VERSION = 0;
26
26
  var reportedSeats = [];
27
27
 
@@ -232,7 +232,31 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
232
232
  '.iris-gallery-file { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; font-weight: 600; }',
233
233
  '.iris-gallery-meta { color: var(--dsw-alias-label-tertiary, #7a7a85); font-size: 10px; }',
234
234
  '.iris-gallery-info { display: grid; gap: 4px; padding: 8px 9px 9px; }',
235
- '.iris-gallery-actions { display: flex; align-items: center; gap: 8px; }',
235
+ '.iris-gallery-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }',
236
+ '.iris-gallery-card.selected { border-color: var(--dsw-alias-brand-primary, #7aa2f7); }',
237
+ '.iris-gallery-select { display: flex; align-items: center; gap: 6px; font-size: 11px; }',
238
+ '.iris-gallery-select input { width: 18px; height: 18px; margin: 0; accent-color: var(--dsw-alias-brand-primary, #7aa2f7); }',
239
+ '.iris-gallery-bulk { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }',
240
+ '.iris-gallery-management { padding: 12px; margin-bottom: 12px; border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 13px; display: grid; gap: 10px; }',
241
+ '.iris-gallery-management dl { margin: 0; display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 6px 12px; font-size: 12px; }',
242
+ '.iris-gallery-management dt { color: var(--dsw-alias-label-tertiary, #7a7a85); }',
243
+ '.iris-gallery-management dd { margin: 0; overflow-wrap: anywhere; white-space: pre-wrap; }',
244
+ '.iris-gallery-management-list { display: grid; gap: 8px; max-height: 45vh; overflow-y: auto; overflow-wrap: anywhere; font-size: 12px; }',
245
+ '.iris-gallery-transaction { padding: 8px; border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 8px; display: grid; gap: 6px; }',
246
+ '.iris-gallery-filters { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; }',
247
+ '.iris-gallery-filters label { display: grid; gap: 3px; font-size: 11px; flex: 1; min-width: 110px; }',
248
+ '.iris-gallery-filters select { min-height: 34px; padding: 4px 7px; background: var(--dsw-alias-bg-layer-2, #32323c); color: inherit; border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 7px; font: inherit; }',
249
+ '.iris-gallery-pagination { display: flex; justify-content: center; align-items: center; gap: 12px; margin-top: 12px; }',
250
+ '.iris-gallery-analysis { display: grid; grid-template-columns: minmax(120px, .6fr) minmax(0, 1fr); gap: 12px; padding: 12px; margin-bottom: 12px; border: 1px solid var(--dsw-alias-brand-primary, #7aa2f7); border-radius: 13px; background: var(--dsw-alias-bg-layer-1, #26262e); }',
251
+ '.iris-gallery-analysis .iris-gallery-preview { max-height: 260px; border-radius: 8px; }',
252
+ '.iris-gallery-analysis-body { display: grid; gap: 8px; min-width: 0; }',
253
+ '.iris-gallery-analysis-head { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }',
254
+ '.iris-gallery-analysis-head strong { flex: 1; }',
255
+ '.iris-gallery-analysis textarea { width: 100%; box-sizing: border-box; min-height: 140px; max-height: 40vh; resize: vertical; font: inherit; line-height: 1.6; padding: 8px; color: inherit; background: var(--dsw-alias-bg-layer-2, #32323c); border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 7px; }',
256
+ '.iris-gallery-analysis-options { display: grid; gap: 8px; }',
257
+ '.iris-gallery-analysis-options label { display: grid; gap: 3px; font-size: 11px; }',
258
+ '.iris-gallery-analysis-options input { box-sizing: border-box; width: 100%; padding: 6px; color: inherit; background: var(--dsw-alias-bg-layer-2, #32323c); border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 6px; }',
259
+ '@media (max-width: 640px) { .iris-gallery-analysis { grid-template-columns: 1fr; } .iris-gallery-actions button, .iris-gallery-pagination button, .iris-gallery-analysis button, .iris-gallery-management button, .iris-gallery-bulk button { min-height: 40px; } }',
236
260
  '.iris-artifact-mini { display: flex; align-items: center; gap: 7px; min-width: 0; padding: 4px 6px; border-radius: 7px; color: var(--dsw-alias-label-secondary, #9a9a9a); text-decoration: none; }',
237
261
  '.iris-artifact-mini:hover { background: var(--dsw-alias-bg-layer-2, #32323c); color: var(--dsw-alias-label-primary, #e6e6e6); }',
238
262
  '.iris-artifact-mini span:nth-child(2) { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }',
@@ -248,6 +272,10 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
248
272
  '.iris-pm-ver.configured { color: #7da8fa; }',
249
273
  '.iris-pm-ver.verified { color: var(--dsw-alias-state-success-primary, #6fcf6f); }',
250
274
  '.iris-pm-ver.failed { color: #c87a81; border-color: rgba(185,101,109,.5); }',
275
+ '.iris-pm-ver.rate-limited { color: #f87171; border-color: #ef4444; box-shadow: 0 0 7px rgba(239,68,68,.35); }',
276
+ '.iris-pm-ver.cooling, .iris-pm-dot.cooling, .iris-health-row.cooling .iris-health-state { color: #fbbf24; border-color: #f59e0b; box-shadow: none; }',
277
+ '.iris-pm-dot.cooling { background: #fbbf24; opacity: 1; }',
278
+ '.iris-bubble.health-cooling .iris-bubble-btn { border-color: #f59e0b; box-shadow: 0 0 11px rgba(245,158,11,.3); }',
251
279
  '.iris-pm-ver.unconfigured { opacity: .55; }',
252
280
  '.iris-pm-madd { flex: 1; background: var(--dsw-alias-bg-layer-2, #32323c); color: var(--dsw-alias-label-primary, #e6e6e6); border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 4px; padding: 3px 6px; font-size: 12px; }',
253
281
  /* 文件选择器(阶段 10) */
@@ -281,8 +309,14 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
281
309
  '.iris-po-route { padding: 7px 10px; border-radius: 10px; background: rgba(255,255,255,.045); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }',
282
310
  '.iris-po-note { padding: 8px 10px; border-radius: 10px; color: var(--dsw-alias-state-warn-primary, #dfbd69); background: rgba(217,169,65,.08); }',
283
311
  '.iris-po-field { display: grid; gap: 6px; font-size: 11px; color: var(--dsw-alias-label-secondary, #a6a6b2); }',
284
- '.iris-po-field select, .iris-po-result textarea { width: 100%; box-sizing: border-box; background: rgba(255,255,255,.055); color: var(--dsw-alias-label-primary, #eeeef2); border: 1px solid rgba(150,164,215,.20); border-radius: 11px; padding: 9px 11px; font: inherit; font-size: 12px; outline: none; transition: border-color .15s, background .15s, box-shadow .15s; }',
285
- '.iris-po-field select:focus, .iris-po-result textarea:focus { border-color: rgba(122,162,247,.72); background: rgba(255,255,255,.075); box-shadow: 0 0 0 3px rgba(122,162,247,.11); }',
312
+ '.iris-po-field select, .iris-po-field textarea, .iris-po-result textarea { width: 100%; box-sizing: border-box; background: rgba(255,255,255,.055); color: var(--dsw-alias-label-primary, #eeeef2); border: 1px solid rgba(150,164,215,.20); border-radius: 11px; padding: 9px 11px; font: inherit; font-size: 12px; outline: none; transition: border-color .15s, background .15s, box-shadow .15s; }',
313
+ '.iris-po-field select:focus, .iris-po-field textarea:focus, .iris-po-result textarea:focus { border-color: rgba(122,162,247,.72); background: rgba(255,255,255,.075); box-shadow: 0 0 0 3px rgba(122,162,247,.11); }',
314
+ '.iris-po-field textarea { min-height: 60px; resize: vertical; line-height: 1.55; }',
315
+ '.iris-po-rules { display: grid; gap: 10px; }',
316
+ '.iris-po-diff { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 28dvh; overflow-y: auto; font-size: 12px; line-height: 1.6; }',
317
+ '.iris-po-diff del, .iris-po-diff ins { display: block; padding: 6px 8px; border-radius: 7px; margin: 5px 0; }',
318
+ '.iris-po-diff del { background: rgba(226,113,126,.12); }',
319
+ '.iris-po-diff ins { background: rgba(99,198,156,.12); text-decoration: none; }',
286
320
  '.iris-po-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }',
287
321
  '.iris-po-btn, .iris-po-primary { min-height: 34px; border: 1px solid rgba(150,164,215,.20); border-radius: 10px; padding: 6px 11px; font: inherit; font-size: 11px; cursor: pointer; transition: transform .15s, background .15s, border-color .15s; }',
288
322
  '.iris-po-btn { background: rgba(255,255,255,.045); color: var(--dsw-alias-label-secondary, #b0b0bb); }',
@@ -419,7 +453,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
419
453
  unconfigured: { label: '未配置', symbol: '○' },
420
454
  configured: { label: '已配置,待验证', symbol: '◌' },
421
455
  verified: { label: '近期验证成功', symbol: '●' },
422
- failed: { label: '认证失败', symbol: '×' }
456
+ failed: { label: '调用受阻', symbol: '×' }
423
457
  };
424
458
 
425
459
  function capabilityHealth(state, capability) {
@@ -443,9 +477,22 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
443
477
  return Math.floor(ms / 86400000) + ' 天前';
444
478
  }
445
479
 
480
+ function rateLimitLabel(health) {
481
+ return health.retryAt ? '429 冷却中' : health.reason === 'budget' ? '预算耗尽已停用' : '额度耗尽已停用';
482
+ }
483
+
484
+ function healthClass(health) {
485
+ return health.retryAt ? 'cooling' : health.rateLimited ? 'rate-limited' : health.status;
486
+ }
487
+
446
488
  function healthText(health) {
447
489
  var status = health && HEALTH_META[health.status] ? health.status : 'unconfigured';
448
- var text = HEALTH_META[status].label;
490
+ var text = health && health.rateLimited
491
+ ? rateLimitLabel(health) + (health.retryAt
492
+ ? ',' + new Date(health.retryAt).toLocaleTimeString() + ' 恢复候选资格;可提前实测'
493
+ : ',实测成功后恢复')
494
+ : (status === 'failed' && health && ['authentication', 'auth', 'permission'].indexOf(health.category) >= 0
495
+ ? '认证失败' : HEALTH_META[status].label);
449
496
  var age = relativeHealthTime(health && health.observedAt);
450
497
  return text + (age ? ' · ' + age : '');
451
498
  }
@@ -462,9 +509,9 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
462
509
  return React.createElement('div', { className: 'iris-health-grid' }, rows.map(function (row) {
463
510
  var health = capabilityHealth(state, row[0]);
464
511
  var meta = HEALTH_META[health.status] || HEALTH_META.unconfigured;
465
- return React.createElement('div', { key: row[0], className: 'iris-health-row ' + health.status },
512
+ return React.createElement('div', { key: row[0], className: 'iris-health-row ' + healthClass(health) },
466
513
  React.createElement('span', { className: 'iris-health-name' },
467
- React.createElement('span', { className: 'iris-pm-dot ' + health.status }, meta.symbol),
514
+ React.createElement('span', { className: 'iris-pm-dot ' + healthClass(health) }, health.retryAt ? '◷' : meta.symbol),
468
515
  row[1]),
469
516
  React.createElement('span', { className: 'iris-health-state', title: healthText(health) }, healthText(health)),
470
517
  React.createElement('span', { className: 'iris-wb-muted' }, String(health.candidateCount || 0) + ' 路'));
@@ -743,12 +790,16 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
743
790
  var newId = addPair[0];
744
791
  var setNewId = addPair[1];
745
792
  var models = p.models || [];
793
+ function setImageProtocol(m, value) {
794
+ postAction('providers_set_model_image_protocol', { id: p.id, model_id: m.id, imageProtocol: value })
795
+ .then(function (r) { props.onNote(r.ok ? '模型图片协议已更新' : (r.d.error || '更新失败')); if (r.ok) props.onDone(); });
796
+ }
746
797
  function verBadge(m, c) {
747
798
  var health = m.health && m.health[c];
748
799
  var status = health && HEALTH_META[health.status] ? health.status : 'configured';
749
- var cls = 'iris-pm-ver ' + status;
800
+ var cls = 'iris-pm-ver ' + status + (health && health.rateLimited ? ' ' + healthClass(health) : '');
750
801
  var title = healthText(health || { status: status });
751
- var symbol = status === 'verified' ? '✓' : (status === 'failed' ? '×' : (status === 'unconfigured' ? '○' : '?'));
802
+ var symbol = health && health.retryAt ? '◷' : status === 'verified' ? '✓' : (status === 'failed' ? '×' : (status === 'unconfigured' ? '○' : '?'));
752
803
  return React.createElement('button', {
753
804
  key: c,
754
805
  className: cls,
@@ -758,9 +809,21 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
758
809
  }
759
810
  var rows = models.map(function (m) {
760
811
  var caps = m.capabilities || [];
812
+ var blocked = caps.map(function (c) { return m.health && m.health[c]; }).find(function (health) { return health && health.rateLimited; });
761
813
  return React.createElement('div', { key: m.id, className: 'iris-pm-mrow' },
762
814
  React.createElement('span', { className: 'iris-pm-mname' }, m.id, m.source === 'manual' ? React.createElement('span', { className: 'iris-pm-mtag' }, '手动') : null),
763
815
  React.createElement('span', { className: 'iris-pm-mcaps' }, caps.map(function (c) { return verBadge(m, c); })),
816
+ caps.includes('image-gen') ? React.createElement('label', { className: 'iris-pm-note' }, '图片协议 ',
817
+ React.createElement('select', { value: m.imageProtocol || 'auto', onChange: function (e) { setImageProtocol(m, e.target.value); } },
818
+ React.createElement('option', { value: 'auto' }, '继承账号默认(' + (p.mediaProtocol || '自动') + ')'),
819
+ React.createElement('option', { value: 'dashscope' }, 'DashScope'),
820
+ React.createElement('option', { value: 'openai-images' }, 'OpenAI Images'),
821
+ React.createElement('option', { value: 'openai-chat-images' }, '聊天生图(网关兼容)'),
822
+ React.createElement('option', { value: 'openai-responses-images' }, 'Responses 生图')),
823
+ m.imageRouting && m.imageRouting.protocolInferred ? ' · 协议待确认' : null) : null,
824
+ caps.includes('vision') || (caps.includes('image-gen') && m.imageRouting && m.imageRouting.mediaProtocol === 'openai-chat-images') ? React.createElement(VisionInputEditor, { key: 'vision-input', provider: p, model: m,
825
+ onNote: props.onNote, onDone: props.onDone }) : null,
826
+ blocked ? React.createElement('span', { className: 'iris-pm-ver failed ' + healthClass(blocked), title: healthText(blocked) }, rateLimitLabel(blocked)) : null,
764
827
  React.createElement('button', { className: 'iris-pm-btn danger', title: '从池移除', onClick: function () { props.removeModel(p, m); } }, '✕'));
765
828
  });
766
829
  return React.createElement('div', { className: 'iris-pm-pool' },
@@ -771,6 +834,39 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
771
834
  React.createElement('button', { className: 'iris-pm-btn', onClick: function () { props.addModel(p, newId.trim()); setNewId(''); } }, '+ 添加')));
772
835
  }
773
836
 
837
+ function VisionInputEditor(props) {
838
+ var p = props.provider;
839
+ var m = props.model;
840
+ var limits = (m ? m.visionInput : p.visionInput) || {};
841
+ var bytesPair = React.useState(limits.maxBytes === undefined ? '' : String(limits.maxBytes / 1048576));
842
+ var dimensionPair = React.useState(limits.maxDimension === undefined ? '' : String(limits.maxDimension));
843
+ React.useEffect(function () {
844
+ bytesPair[1](limits.maxBytes === undefined ? '' : String(limits.maxBytes / 1048576));
845
+ dimensionPair[1](limits.maxDimension === undefined ? '' : String(limits.maxDimension));
846
+ }, [limits.maxBytes, limits.maxDimension]);
847
+ function save(reset) {
848
+ var value = {};
849
+ if (bytesPair[0] !== '') value.maxBytes = Number(bytesPair[0]) * 1048576;
850
+ if (dimensionPair[0] !== '') value.maxDimension = Number(dimensionPair[0]);
851
+ if (!reset && Object.values(value).some(function (number) { return !Number.isSafeInteger(number) || number <= 0; })) {
852
+ props.onNote('请输入正数 MiB 和正整数像素'); return;
853
+ }
854
+ var action = m ? 'providers_set_model_vision_input' : 'providers_upsert';
855
+ postAction(action, { id: p.id, ...(m ? { model_id: m.id } : {}), visionInput: reset ? null : value })
856
+ .then(function (r) { props.onNote(r.ok ? '视觉输入预算已更新' : (r.d.error || '更新失败')); if (r.ok) props.onDone(); });
857
+ }
858
+ return React.createElement('div', { className: 'iris-pm-field' },
859
+ React.createElement('label', {}, m ? '模型视觉预算(留空继承账号)' : '账号视觉预算(留空使用默认 8 MiB)'),
860
+ React.createElement('div', { className: 'iris-wb-row' },
861
+ React.createElement('input', { type: 'number', min: '0', step: 'any', 'aria-label': '视觉大小上限 MiB',
862
+ placeholder: 'MiB', value: bytesPair[0], onChange: function (e) { bytesPair[1](e.target.value); } }),
863
+ React.createElement('input', { type: 'number', min: '1', step: '1', 'aria-label': '视觉最长边像素',
864
+ placeholder: '最长边 px(可选)', value: dimensionPair[0], onChange: function (e) { dimensionPair[1](e.target.value); } }),
865
+ React.createElement('button', { className: 'iris-pm-btn', onClick: function () { save(false); } }, '保存预算'),
866
+ React.createElement('button', { className: 'iris-pm-btn', onClick: function () { save(true); } }, '恢复继承')),
867
+ React.createElement('span', { className: 'iris-pm-note' }, '用于看图、OCR 每块输入和定位。作品原图保留,定位结果换算回原图。'));
868
+ }
869
+
774
870
  function ProviderEndpointEditor(props) {
775
871
  var p = props.provider;
776
872
  var valuePair = React.useState(p.mediaBaseUrl || '');
@@ -807,7 +903,11 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
807
903
  function refresh() {
808
904
  postAction('providers_list', {}).then(function (r) { if (r.ok && r.d.providers) setList(r.d.providers); });
809
905
  }
810
- React.useEffect(function () { refresh(); }, []);
906
+ React.useEffect(function () {
907
+ refresh();
908
+ var timer = setInterval(refresh, 5000);
909
+ return function () { clearInterval(timer); };
910
+ }, []);
811
911
  function addProvider() {
812
912
  var f = newForm[0];
813
913
  if (!f.baseUrl || !f.apiKey) { setNote('baseUrl 和 apiKey 必填'); return; }
@@ -863,11 +963,14 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
863
963
  React.createElement('div', { className: 'iris-pm-note' }, 'Key ' + (p.apiKeyHint || '未配置') + ' · ' + (p.type || '') + ' · ' + (p.mediaProtocol || '')),
864
964
  p.protocolInferred ? React.createElement('div', { className: 'iris-pm-note' }, '协议为推断值,请确认') : null,
865
965
  React.createElement(ProviderEndpointEditor, { provider: p, onNote: setNote, onDone: refresh }),
966
+ React.createElement(VisionInputEditor, { provider: p, onNote: setNote, onDone: refresh }),
866
967
  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); } },
867
968
  React.createElement('option', { value: 'auto' }, '自动'),
868
- p.mediaProtocol && !['dashscope', 'openai-images', 'auto'].includes(p.mediaProtocol) ? React.createElement('option', { value: p.mediaProtocol }, p.mediaProtocol + '(未支持)') : null,
969
+ p.mediaProtocol && !['dashscope', 'openai-images', 'openai-chat-images', 'openai-responses-images', 'auto'].includes(p.mediaProtocol) ? React.createElement('option', { value: p.mediaProtocol }, p.mediaProtocol + '(未支持)') : null,
869
970
  React.createElement('option', { value: 'dashscope' }, 'DashScope(仅阿里云官方 HTTPS)'),
870
- React.createElement('option', { value: 'openai-images' }, 'OpenAI Images 兼容'))),
971
+ React.createElement('option', { value: 'openai-images' }, 'OpenAI Images 兼容'),
972
+ React.createElement('option', { value: 'openai-chat-images' }, '聊天生图(网关兼容)'),
973
+ React.createElement('option', { value: 'openai-responses-images' }, 'Responses 生图'))),
871
974
  React.createElement('div', { className: 'iris-pm-note' }, '能力: ' + ((p.capabilities || []).join(' / ') || '无')),
872
975
  React.createElement(ModelPool, { provider: p, onNote: setNote, onDone: refresh, testModel: testModel, removeModel: removeModel, addModel: addModel }),
873
976
  React.createElement('div', { className: 'iris-wb-row' },
@@ -891,7 +994,9 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
891
994
  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 }); } },
892
995
  React.createElement('option', { value: 'auto' }, '自动(按媒体 Base URL 安全判断)'),
893
996
  React.createElement('option', { value: 'dashscope' }, 'DashScope(阿里云官方)'),
894
- React.createElement('option', { value: 'openai-images' }, 'OpenAI Images 兼容'))),
997
+ React.createElement('option', { value: 'openai-images' }, 'OpenAI Images 兼容'),
998
+ React.createElement('option', { value: 'openai-chat-images' }, '聊天生图(网关兼容)'),
999
+ React.createElement('option', { value: 'openai-responses-images' }, 'Responses 生图'))),
895
1000
  React.createElement('div', { className: 'iris-wb-row' },
896
1001
  React.createElement('button', { className: 'iris-act-run', onClick: addProvider }, '保存'),
897
1002
  React.createElement('button', { className: 'iris-pm-btn', onClick: function () { setAdding(false); } }, '取消')));
@@ -1062,7 +1167,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1062
1167
  var shown = (vals.model && vals.model.trim()) || (modelInfo && modelInfo.assigned) || '';
1063
1168
  modelBar = React.createElement('div', { className: 'iris-act-field' },
1064
1169
  React.createElement('label', {},
1065
- React.createElement('span', { className: 'iris-pm-dot ' + cardHealth.status }, HEALTH_META[cardHealth.status].symbol),
1170
+ React.createElement('span', { className: 'iris-pm-dot ' + healthClass(cardHealth) }, cardHealth.retryAt ? '◷' : HEALTH_META[cardHealth.status].symbol),
1066
1171
  ' 模型:' + (shown || '自动') + (available ? '' : '(无可用模型)')),
1067
1172
  React.createElement('div', { className: 'iris-wb-muted' },
1068
1173
  healthText(cardHealth) + ' · 改分配 → 上方「能力分配(failover 顺序)」'));
@@ -1082,9 +1187,9 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1082
1187
  React.createElement('div', { className: 'iris-act-head', onClick: function () { setOpen(!open); } },
1083
1188
  React.createElement('span', { className: 'iris-act-title' }, title),
1084
1189
  capability ? React.createElement('span', {
1085
- className: 'iris-pm-dot ' + headStatus,
1190
+ className: 'iris-pm-dot ' + healthClass(cardHealth),
1086
1191
  title: headTitle
1087
- }, HEALTH_META[headStatus].symbol) : null,
1192
+ }, cardHealth.retryAt ? '◷' : HEALTH_META[headStatus].symbol) : null,
1088
1193
  React.createElement('span', { className: 'iris-act-arrow' }, '▶')),
1089
1194
  body);
1090
1195
  }
@@ -1328,7 +1433,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1328
1433
 
1329
1434
  function coreGalleryItems(data, limit) {
1330
1435
  return (((data && data.artifacts && data.artifacts.recent) || []).filter(function (artifact) {
1331
- return artifact.kind !== 'host-input' && String(artifact.mediaType || '').indexOf('image/') === 0;
1436
+ return artifact.kind !== 'host-input';
1332
1437
  }).map(function (artifact) {
1333
1438
  return {
1334
1439
  id: artifact.id, file: artifact.id, mime: artifact.mediaType,
@@ -1364,99 +1469,458 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1364
1469
  return pair[0];
1365
1470
  }
1366
1471
 
1367
- function ArtifactGallery(props) {
1368
- var itemsPair = React.useState([]);
1369
- var items = itemsPair[0];
1370
- var setItems = itemsPair[1];
1371
- var totalPair = React.useState(Number(props.total || 0));
1372
- var total = totalPair[0];
1373
- var setTotal = totalPair[1];
1374
- var corePair = React.useState([]);
1375
- var coreItems = corePair[0];
1376
- var setCoreItems = corePair[1];
1377
- var busyPair = React.useState(false);
1378
- var busy = busyPair[0];
1379
- var setBusy = busyPair[1];
1472
+ function galleryKindLabel(kind) {
1473
+ return ({ __proto__: null, 'generated-image': '生成图片', 'generated-video': '生成视频', 'generated-audio': '生成音频',
1474
+ 'tts-audio': '合成语音', transcript: '转写文本', 'video-frame': '视频帧', crop: '裁剪图片',
1475
+ 'cropped-image': '裁剪图片', 'pixel-diff': '差异热图', 'diff-heatmap': '差异热图', 'html-screenshot': 'HTML 截图',
1476
+ 'legacy-output': '旧版作品' })[kind] || kind;
1477
+ }
1478
+
1479
+ function fetchWorkbenchJson(url, input, signal) {
1480
+ var options = { signal: signal };
1481
+ if (input) { options.method = 'POST'; options.headers = { 'Content-Type': 'application/json' }; options.body = JSON.stringify(input); }
1482
+ return fetch(url, options).then(function (res) { return res.json().then(function (data) {
1483
+ if (!res.ok || data.ok === false) throw new Error((data.error || '作品操作失败') + (data.transactionId ? '(事务 ' + data.transactionId + ')' : ''));
1484
+ return data;
1485
+ }); });
1486
+ }
1487
+ function copyWorkbenchText(text, setNote) {
1488
+ if (!navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') { setNote('请手动选择并复制 ID:\n' + text); return; }
1489
+ navigator.clipboard.writeText(text).then(function () { setNote('已复制'); }).catch(function () { setNote('请手动选择并复制 ID:\n' + text); });
1490
+ }
1491
+ function saveWorkbenchBlob(blob, name) {
1492
+ var url = URL.createObjectURL(blob), link = document.createElement('a');
1493
+ link.href = url; link.download = name; link.click();
1494
+ setTimeout(function () { URL.revokeObjectURL(url); }, 0);
1495
+ }
1496
+ function deletionBlockerLabel(blocker) {
1497
+ return ({ __proto__: null, task_not_settled: '任务尚未结束或结果未知', task_artifact_reference: '被任务记录引用', task_input_reference: '被改图任务作为原图引用',
1498
+ artifact_task_reference: '该任务仍关联其他作品', retry_reference: '被后续重试任务引用',
1499
+ artifact_relation: '被其他作品引用', uncommitted_relation: '被尚未提交的产物引用' })[blocker.reason] || '仍有引用';
1500
+ }
1501
+
1502
+ function WorkbenchManagement(props) {
1503
+ var type = props.type;
1504
+ var dataPair = React.useState(null), data = dataPair[0], setData = dataPair[1];
1505
+ var busyPair = React.useState(true), busy = busyPair[0], setBusy = busyPair[1];
1506
+ var writingPair = React.useState(false), writing = writingPair[0], setWriting = writingPair[1];
1507
+ var notePair = React.useState(''), note = notePair[0], setNote = notePair[1];
1508
+ var tasksPair = React.useState([]), taskIds = tasksPair[0], setTaskIds = tasksPair[1];
1509
+ var request = React.useRef(null), view = React.useRef(null);
1510
+ var artifactIds = (props.items || []).map(function (item) { return item.id; });
1511
+ function query(tasks) {
1512
+ if (request.current) request.current.abort();
1513
+ var controller = new AbortController(); request.current = controller; setBusy(true); setNote('');
1514
+ var url = type === 'details' ? '/iris/api/works/' + props.item.source + '/' + props.item.id
1515
+ : type === 'transactions' ? '/iris/api/works/transactions' : '/iris/api/works/delete';
1516
+ fetchWorkbenchJson(url, type === 'delete' ? { artifact_ids: artifactIds, task_ids: tasks || [] } : null, controller.signal)
1517
+ .then(function (value) { if (request.current === controller && !controller.signal.aborted) setData(value); })
1518
+ .catch(function (error) { if (request.current === controller && !controller.signal.aborted) { setData(null); setNote(error.message); } })
1519
+ .finally(function () { if (request.current === controller) { request.current = null; setBusy(false); } });
1520
+ }
1521
+ React.useEffect(function () {
1522
+ query([]);
1523
+ if (view.current) view.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
1524
+ return function () { if (request.current) request.current.abort(); request.current = null; };
1525
+ }, []);
1526
+ function chooseTask(id, checked) {
1527
+ var next = checked ? taskIds.concat(id) : taskIds.filter(function (value) { return value !== id; });
1528
+ setTaskIds(next); query(next);
1529
+ }
1530
+ function execute(transaction) {
1531
+ var restoring = type === 'transactions';
1532
+ var message = restoring ? '恢复事务 ' + transaction.transactionId + ' 中的 ' + transaction.files + ' 个文件?已有文件不会被覆盖。'
1533
+ : '将 ' + artifactIds.length + ' 个 Core 作品' + (taskIds.length ? '及 ' + taskIds.length + ' 个已结束任务记录' : '') + '移入回收区?之后可以恢复。';
1534
+ if (!window.confirm(message)) return;
1535
+ var controller = new AbortController(); request.current = controller;
1536
+ setWriting(true); setBusy(true); setNote(''); props.onBusy(true);
1537
+ fetchWorkbenchJson('/iris/api/works/' + (restoring ? 'restore' : 'delete'), restoring
1538
+ ? { transaction_id: transaction.transactionId, confirm_restore: true }
1539
+ : { artifact_ids: artifactIds, task_ids: taskIds, confirm_delete: true }, controller.signal)
1540
+ .then(function (value) {
1541
+ if (request.current !== controller || controller.signal.aborted) return;
1542
+ props.onChanged(value);
1543
+ if (restoring) query([]); else props.onClose();
1544
+ })
1545
+ .catch(function (error) { if (request.current === controller && !controller.signal.aborted) setNote(error.message); })
1546
+ .finally(function () {
1547
+ props.onBusy(false); setWriting(false);
1548
+ if (request.current === controller) { request.current = null; setBusy(false); }
1549
+ });
1550
+ }
1551
+ var item = data && data.item;
1552
+ var titles = { details: '作品详情', delete: '删除预览', transactions: 'Core 回收区' };
1553
+ return React.createElement('section', { ref: view, className: 'iris-gallery-management', 'aria-label': titles[type], 'aria-busy': busy },
1554
+ React.createElement('div', { className: 'iris-gallery-analysis-head' }, React.createElement('strong', {}, titles[type]),
1555
+ React.createElement('button', { type: 'button', className: 'iris-pm-btn', disabled: writing, onClick: props.onClose }, '关闭面板')),
1556
+ busy ? React.createElement('div', { role: 'status', className: 'iris-wb-muted' }, writing ? '正在处理…' : '正在读取…') : null,
1557
+ item ? React.createElement('dl', {}, [
1558
+ ['ID', item.id], ['来源', item.source === 'core' ? 'Core' : '旧版'], ['文件名', item.file], ['媒体类型', item.mime],
1559
+ ['产物类型', galleryKindLabel(item.kind)], ['大小', fmtBytes(item.size)], ['创建时间', fmtTime(item.createdAt)],
1560
+ ['内容核验', item.integrity === 'verified' ? 'SHA-256 已核验' : '文件存在;旧版没有已存哈希'],
1561
+ ['SHA-256', item.digest], ['关联', (item.relations || []).map(function (edge) { return edge.type + ' → ' + edge.artifactId; }).join('\n')],
1562
+ ['元数据', item.metadata && Object.keys(item.metadata).length ? JSON.stringify(item.metadata, null, 2) : '']
1563
+ ].filter(function (row) { return !!row[1]; }).map(function (row) {
1564
+ return [React.createElement('dt', { key: row[0] + '-label' }, row[0]), React.createElement('dd', { key: row[0] }, row[1])];
1565
+ })) : null,
1566
+ item ? React.createElement('div', { className: 'iris-gallery-actions' },
1567
+ React.createElement('a', { className: 'iris-wb-link', href: item.url, target: '_blank', rel: 'noreferrer' }, '打开原文件'),
1568
+ React.createElement('button', { className: 'iris-pm-btn', type: 'button', onClick: function () { copyWorkbenchText(item.id, setNote); } }, '复制 ID')) : null,
1569
+ type === 'delete' && data ? React.createElement('div', { className: 'iris-gallery-management-list' },
1570
+ React.createElement('div', {}, '选中 ' + artifactIds.length + ' 个作品 · ' + data.files + ' 个文件 · ' + fmtBytes(data.bytes)),
1571
+ artifactIds.map(function (id) { return React.createElement('div', { key: id }, id); }),
1572
+ (data.referencingTasks || []).length ? React.createElement('div', { className: 'iris-wb-muted' }, '如需同时移除关联任务记录,请逐项勾选;未结束任务及其他引用会阻止删除。') : null,
1573
+ (data.referencingTasks || []).map(function (task) { return React.createElement('label', { key: task.id, className: 'iris-gallery-select' },
1574
+ React.createElement('input', { type: 'checkbox', 'aria-label': '关联任务 ' + task.id, checked: taskIds.indexOf(task.id) >= 0,
1575
+ disabled: busy || !task.settled, onChange: function (event) { chooseTask(task.id, event.target.checked); } }),
1576
+ '同时移除任务记录 ' + task.id + (task.settled ? '' : '(未结束或结果未知)')); }),
1577
+ (data.blockers || []).map(function (blocker, index) { return React.createElement('div', { key: index, className: 'iris-wb-muted' },
1578
+ deletionBlockerLabel(blocker) + ':' + blocker.id + (blocker.referencedBy ? ' ← ' + blocker.referencedBy : '')); })) : null,
1579
+ type === 'delete' ? React.createElement('div', { className: 'iris-gallery-actions' },
1580
+ React.createElement('button', { className: 'iris-pm-btn danger', type: 'button', disabled: busy || !data || !data.allowed,
1581
+ onClick: function () { execute(); } }, '确认移入回收区'),
1582
+ React.createElement('button', { className: 'iris-pm-btn', type: 'button', disabled: busy, onClick: function () { query(taskIds); } }, '重新预览')) : null,
1583
+ type === 'transactions' && data ? React.createElement('div', { className: 'iris-gallery-management-list' },
1584
+ data.transactions.length ? data.transactions.map(function (transaction) {
1585
+ var states = { __proto__: null, prepared: '准备中', committed: '已隔离', interrupted: '操作中断', restored: '已恢复', 'rolled-back': '已回退' };
1586
+ return React.createElement('div', { key: transaction.transactionId, className: 'iris-gallery-transaction' },
1587
+ React.createElement('div', {}, transaction.transactionId),
1588
+ React.createElement('div', { className: 'iris-wb-muted' }, (transaction.operation === 'cleanup' ? '清理' : '删除') + ' · '
1589
+ + (states[transaction.state] || transaction.state) + ' · ' + transaction.files + ' 文件 · ' + fmtTime(transaction.createdAt)),
1590
+ React.createElement('button', { type: 'button', className: 'iris-pm-btn',
1591
+ disabled: busy || ['prepared', 'committed', 'interrupted'].indexOf(transaction.state) < 0,
1592
+ onClick: function () { execute(transaction); } }, '恢复'));
1593
+ }) : React.createElement('div', { className: 'iris-wb-muted' }, '回收区为空。')) : null,
1594
+ type === 'transactions' ? React.createElement('button', { type: 'button', className: 'iris-pm-btn', disabled: busy, onClick: function () { query([]); } }, '刷新回收区') : null,
1595
+ note ? React.createElement('div', { role: 'status', className: 'iris-clean-note', style: { whiteSpace: 'pre-wrap' } }, note) : null);
1596
+ }
1597
+
1598
+ function ArtifactAnalysis(props) {
1599
+ var item = props.item, action = props.action;
1600
+ var resultPair = React.useState(null), result = resultPair[0], setResult = resultPair[1];
1601
+ var optionsPair = React.useState({ question: '', chunk_height: '1200', overlap: '120' });
1602
+ var options = optionsPair[0], setOptions = optionsPair[1];
1603
+ var notePair = React.useState(''), note = notePair[0], setNote = notePair[1];
1604
+ var request = React.useRef(null);
1605
+ var view = React.useRef(null);
1606
+ var running = result && result.status === 'running';
1607
+ var text = result && result.text || '';
1608
+ function execute() {
1609
+ if (request.current) request.current.abort();
1610
+ var controller = new AbortController();
1611
+ request.current = controller;
1612
+ var input = { artifact_id: item.id };
1613
+ if (action === 'look') input.question = options.question;
1614
+ else {
1615
+ input.chunk_height = Number(options.chunk_height);
1616
+ input.overlap = Number(options.overlap);
1617
+ }
1618
+ setResult({ status: 'running', text: '' }); setNote('');
1619
+ fetch('/iris/api/actions/' + action, { method: 'POST', headers: { 'Content-Type': 'application/json' },
1620
+ body: JSON.stringify(input), signal: controller.signal })
1621
+ .then(function (res) { return res.json().then(function (data) { return { ok: res.ok, data: data }; }); })
1622
+ .then(function (response) {
1623
+ if (request.current !== controller || controller.signal.aborted) return;
1624
+ var data = response.data;
1625
+ setResult({ status: !response.ok || data.ok === false ? 'failed' : (data.status || 'complete'),
1626
+ text: data.text || data.error || '未返回识别结果' });
1627
+ })
1628
+ .catch(function (error) {
1629
+ if (request.current !== controller || controller.signal.aborted) return;
1630
+ setResult({ status: 'failed', text: '识别请求失败:' + error.message });
1631
+ })
1632
+ .finally(function () { if (request.current === controller) request.current = null; });
1633
+ }
1634
+ function cancel() {
1635
+ if (request.current) request.current.abort();
1636
+ request.current = null;
1637
+ setResult({ status: 'canceled', text: '' });
1638
+ }
1639
+ React.useEffect(function () {
1640
+ execute();
1641
+ if (view.current) view.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
1642
+ return function () { if (request.current) request.current.abort(); request.current = null; };
1643
+ }, []);
1644
+ function option(key, value) { setOptions(function (current) { var next = Object.assign({}, current); next[key] = value; return next; }); }
1645
+ function copy() {
1646
+ if (!navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') { setNote('请选中下方文字手动复制'); return; }
1647
+ navigator.clipboard.writeText(text).then(function () { setNote('已复制识别结果'); }).catch(function () { setNote('复制失败,请手动选择文字'); });
1648
+ }
1649
+ function download() {
1650
+ var url = URL.createObjectURL(new Blob([text], { type: 'text/plain;charset=utf-8' }));
1651
+ var link = document.createElement('a'); link.href = url; link.download = item.id + '-' + action + '.txt'; link.click();
1652
+ setTimeout(function () { URL.revokeObjectURL(url); }, 0);
1653
+ }
1654
+ var status = result && result.status || 'running';
1655
+ var labels = { running: '识别中…', complete: '完成', partial: '部分完成', failed: '失败', canceled: '已取消' };
1656
+ return React.createElement('section', { ref: view, className: 'iris-gallery-analysis', 'aria-label': '作品识别', 'aria-busy': !!running },
1657
+ artifactPreview(item),
1658
+ React.createElement('div', { className: 'iris-gallery-analysis-body' },
1659
+ React.createElement('div', { className: 'iris-gallery-analysis-head' },
1660
+ React.createElement('strong', {}, action === 'look' ? '看图结果' : 'OCR 结果'),
1661
+ React.createElement('span', { role: 'status', className: 'iris-wb-badge ' + (status === 'partial' ? 'warn' : status === 'failed' ? 'err' : status === 'complete' ? 'ok' : '') }, labels[status]),
1662
+ running ? React.createElement('button', { className: 'iris-pm-btn', type: 'button', onClick: cancel }, '取消识别') : null,
1663
+ React.createElement('button', { className: 'iris-pm-btn', type: 'button', onClick: props.onClose }, '关闭结果')),
1664
+ React.createElement('details', {}, React.createElement('summary', {}, '更多选项'),
1665
+ React.createElement('div', { className: 'iris-gallery-analysis-options' }, action === 'look'
1666
+ ? React.createElement('label', {}, '看图问题', React.createElement('input', { value: options.question, placeholder: '默认:详细描述这张图片', onChange: function (e) { option('question', e.target.value); } }))
1667
+ : ['chunk_height', 'overlap'].map(function (key) {
1668
+ return React.createElement('label', { key: key }, key === 'chunk_height' ? '分块高度' : '重叠像素',
1669
+ React.createElement('input', { type: 'number', min: key === 'chunk_height' ? 100 : 0, value: options[key], onChange: function (e) { option(key, e.target.value); } }));
1670
+ }),
1671
+ React.createElement('button', { className: 'iris-clean-btn', type: 'button', disabled: !!running, onClick: execute }, '按当前选项重新识别'))),
1672
+ text ? React.createElement('textarea', { 'aria-label': '识别结果', readOnly: true, value: text }) : null,
1673
+ text ? React.createElement('div', { className: 'iris-gallery-actions' },
1674
+ React.createElement('button', { className: 'iris-pm-btn', type: 'button', onClick: copy }, '复制文本'),
1675
+ React.createElement('button', { className: 'iris-pm-btn', type: 'button', onClick: download }, '下载文本')) : null,
1676
+ note ? React.createElement('div', { role: 'status', className: 'iris-clean-note' }, note) : null));
1677
+ }
1678
+
1679
+ function ArtifactImageEdit(props) {
1680
+ var item = props.item;
1681
+ var promptPair = React.useState(''), prompt = promptPair[0], setPrompt = promptPair[1];
1682
+ var modelPair = React.useState(''), model = modelPair[0], setModel = modelPair[1];
1683
+ var modelsPair = React.useState(null), models = modelsPair[0], setModels = modelsPair[1];
1684
+ var resultPair = React.useState(null), result = resultPair[0], setResult = resultPair[1];
1685
+ var request = React.useRef(null);
1686
+ var running = result && result.status === 'running';
1687
+ React.useEffect(function () {
1688
+ var active = true;
1689
+ postAction('providers_list', {}).then(function (response) {
1690
+ if (!active) return;
1691
+ if (!response.ok || !response.d.ok) throw new Error('模型列表读取失败');
1692
+ var choices = [];
1693
+ (response.d.providers || []).forEach(function (provider) {
1694
+ if (!provider.enabled) return;
1695
+ (provider.models || []).forEach(function (entry) {
1696
+ if ((entry.capabilities || []).indexOf('image-gen') < 0
1697
+ || !entry.imageRouting || entry.imageRouting.mediaProtocol !== 'openai-chat-images') return;
1698
+ var health = entry.health && entry.health['image-gen'];
1699
+ choices.push({ id: provider.id + '::' + entry.id, label: provider.name + ' · ' + entry.id,
1700
+ blocked: !!(health && health.rateLimited) });
1701
+ });
1702
+ });
1703
+ setModels(choices);
1704
+ }).catch(function (error) { if (active) { setModels([]); setResult({ status: 'failed', text: error.message }); } });
1705
+ return function () { active = false; if (request.current) request.current.abort(); request.current = null; };
1706
+ }, []);
1707
+ function execute() {
1708
+ if (request.current || !prompt.trim()) return;
1709
+ var controller = new AbortController(); request.current = controller;
1710
+ setResult({ status: 'running', text: '' });
1711
+ fetch('/iris/api/actions/image_edit', { method: 'POST', headers: { 'Content-Type': 'application/json' },
1712
+ body: JSON.stringify({ source_artifact_id: item.id, prompt: prompt.trim(), ...(model ? { model: model } : {}) }), signal: controller.signal })
1713
+ .then(function (res) { return res.json().then(function (data) { return { ok: res.ok && data.ok !== false, data: data }; }); })
1714
+ .then(function (response) {
1715
+ if (request.current !== controller || controller.signal.aborted) return;
1716
+ var data = response.data;
1717
+ setResult({ status: response.ok ? 'complete' : 'failed', text: data.text || data.error || '未返回编辑结果',
1718
+ artifactIds: response.ok ? data.artifactIds || [] : [], taskId: data.taskId });
1719
+ if (response.ok) window.dispatchEvent(new CustomEvent('iris-core-artifacts-changed'));
1720
+ })
1721
+ .catch(function (error) {
1722
+ if (request.current === controller && !controller.signal.aborted) setResult({ status: 'failed', text: '改图请求失败:' + error.message + '。请在任务区检查结果。' });
1723
+ })
1724
+ .finally(function () { if (request.current === controller) request.current = null; });
1725
+ }
1726
+ function cancel() {
1727
+ if (request.current) request.current.abort(); request.current = null;
1728
+ setResult({ status: 'canceled', text: '已停止等待,请在任务区检查是否已生成。不会自动重新提交。' });
1729
+ }
1730
+ return React.createElement('section', { className: 'iris-gallery-analysis', 'aria-label': '作品改图', 'aria-busy': !!running },
1731
+ artifactPreview(item),
1732
+ React.createElement('div', { className: 'iris-gallery-analysis-body' },
1733
+ React.createElement('div', { className: 'iris-gallery-analysis-head' }, React.createElement('strong', {}, '编辑图片'),
1734
+ React.createElement('button', { className: 'iris-pm-btn', type: 'button', onClick: props.onClose }, '关闭改图')),
1735
+ React.createElement('div', { className: 'iris-wb-muted' }, '原图保留,修改版会保存为新作品。此操作会调用生图模型。'),
1736
+ React.createElement('label', {}, '修改指令', React.createElement('textarea', { 'aria-label': '修改指令', value: prompt, disabled: !!running,
1737
+ placeholder: '例如:把蓝色发饰改成白色,保留人物、服装和构图', onChange: function (e) { setPrompt(e.target.value); } })),
1738
+ React.createElement('label', {}, '改图模型', React.createElement('select', { 'aria-label': '改图模型', value: model, disabled: !!running,
1739
+ onChange: function (e) { setModel(e.target.value); } }, React.createElement('option', { value: '' }, '按当前顺序选择可用改图模型'),
1740
+ (models || []).map(function (entry) { return React.createElement('option', { key: entry.id, value: entry.id, disabled: entry.blocked }, entry.label + (entry.blocked ? ' · 冷却或额度耗尽' : '')); }))),
1741
+ models && !models.some(function (entry) { return !entry.blocked; }) ? React.createElement('div', { role: 'status' }, '暂无可用改图模型,请在模型设置中配置聊天生图协议,或手动实测恢复模型。') : null,
1742
+ React.createElement('div', { className: 'iris-gallery-actions' },
1743
+ React.createElement('button', { className: 'iris-clean-btn', type: 'button', disabled: !!running || !prompt.trim() || !models || !models.some(function (entry) { return !entry.blocked; }), onClick: execute }, running ? '编辑中…' : '生成修改版'),
1744
+ running ? React.createElement('button', { className: 'iris-pm-btn', type: 'button', onClick: cancel }, '停止等待') : null),
1745
+ result ? React.createElement('div', { role: 'status', className: 'iris-clean-note', style: { whiteSpace: 'pre-wrap' } }, result.text || '编辑中…',
1746
+ result.taskId ? React.createElement('div', {}, 'task: ' + result.taskId) : null) : null,
1747
+ result && result.artifactIds ? result.artifactIds.map(function (id) { return React.createElement('div', { key: id },
1748
+ React.createElement('a', { href: '/iris/api/core/artifact/' + encodeURIComponent(id) + '/media', target: '_blank', rel: 'noreferrer' }, '打开新作品:' + id)); }) : null));
1749
+ }
1750
+
1751
+ function ArtifactGallery() {
1752
+ var dataPair = React.useState({ total: 0, offset: 0, limit: 24, items: [], kinds: [], counts: { core: 0, legacy: 0 } });
1753
+ var data = dataPair[0], setData = dataPair[1];
1754
+ var queryPair = React.useState({ offset: 0, media_type: 'all', source: 'all', kind: 'all' });
1755
+ var query = queryPair[0], setQuery = queryPair[1];
1756
+ var currentQuery = React.useRef(query); currentQuery.current = query;
1757
+ var busyPair = React.useState(false), busy = busyPair[0], setBusy = busyPair[1];
1758
+ var mutationPair = React.useState(false), mutating = mutationPair[0], setMutating = mutationPair[1];
1380
1759
  var notePair = React.useState('');
1381
1760
  var note = notePair[0];
1382
1761
  var setNote = notePair[1];
1762
+ var analysisPair = React.useState(null), analysis = analysisPair[0], setAnalysis = analysisPair[1];
1763
+ var selectionPair = React.useState([]), selected = selectionPair[0], setSelected = selectionPair[1];
1764
+ var managementPair = React.useState(null), management = managementPair[0], setManagement = managementPair[1];
1765
+ var operationNotePair = React.useState(''), operationNote = operationNotePair[0], setOperationNote = operationNotePair[1];
1766
+ var request = React.useRef(null), sequence = React.useRef(0);
1767
+ var downloadRequest = React.useRef(null);
1383
1768
 
1384
- function load(append) {
1385
- if (busy) return;
1769
+ function load() {
1770
+ if (request.current) request.current.abort();
1771
+ var controller = new AbortController(); request.current = controller;
1386
1772
  setBusy(true);
1387
- var offset = append ? items.length : 0;
1388
- var legacyRequest = fetch('/iris/api/artifacts?offset=' + offset + '&limit=24')
1389
- .then(function (res) { return res.ok ? res.json() : Promise.reject(new Error('旧版作品读取失败')); });
1390
- var coreRequest = append ? Promise.resolve(null) : fetch('/iris/api/core/snapshot?limit=200')
1391
- .then(function (res) {
1392
- if (res.status === 404) return { available: false, backendReloadRequired: true, artifacts: { recent: [] } };
1393
- return res.ok ? res.json() : Promise.reject(new Error('Core 作品读取失败(HTTP ' + res.status + ')'));
1394
- }).catch(function (error) { return { available: false, error: error.message, artifacts: { recent: [] } }; });
1395
- Promise.all([legacyRequest, coreRequest])
1396
- .then(function (values) {
1397
- var data = values[0];
1398
- var core = values[1];
1399
- setItems(append ? items.concat(data.items || []) : (data.items || []));
1400
- setTotal(Number(data.total || 0));
1401
- if (core) {
1402
- var nextCore = coreGalleryItems(core, 200);
1403
- setCoreItems(nextCore);
1404
- if (core.error) setNote(core.error);
1405
- else if (core.backendReloadRequired) setNote('DSH 后端尚未加载 Core 路由;重启后新作品会出现在这里。');
1406
- }
1773
+ var input = currentQuery.current;
1774
+ var url = '/iris/api/works?offset=' + input.offset + '&limit=24&media_type=' + input.media_type
1775
+ + '&source=' + input.source + '&kind=' + encodeURIComponent(input.kind);
1776
+ fetch(url, { signal: controller.signal })
1777
+ .then(function (res) { if (!res.ok) throw new Error(res.status === 404 ? '请重新加载 DSH 后端以启用作品管理' : '作品读取失败(HTTP ' + res.status + ')'); return res.json(); })
1778
+ .then(function (value) {
1779
+ if (request.current !== controller || controller.signal.aborted) return;
1780
+ setData(value);
1781
+ setNote(value.coreError ? 'Core 作品暂时不可用,当前计数仅含可读取作品。'
1782
+ : value.droppedCore ? '已跳过 ' + value.droppedCore + ' 条不可读取的 Core 作品。' : '');
1407
1783
  })
1408
- .catch(function (error) { setNote('作品库读取失败:' + error.message); })
1409
- .finally(function () { setBusy(false); });
1784
+ .catch(function (error) { if (request.current === controller && !controller.signal.aborted) setNote(error.message); })
1785
+ .finally(function () { if (request.current === controller) { request.current = null; setBusy(false); } });
1410
1786
  }
1411
1787
 
1412
1788
  function run(action, body, confirmText) {
1413
1789
  if (confirmText && !window.confirm(confirmText)) return;
1414
- setBusy(true);
1790
+ setMutating(true);
1415
1791
  postAction(action, body || {}).then(function (result) {
1416
1792
  var succeeded = result.ok && result.d && result.d.ok !== false;
1417
1793
  setNote(succeeded ? (result.d.text || '完成') : ('失败:' + ((result.d && (result.d.error || result.d.text)) || '未知错误')));
1418
- if (succeeded) load(false);
1419
- else setBusy(false);
1420
- }).catch(function (error) { setBusy(false); setNote('失败:' + error.message); });
1794
+ if (succeeded) {
1795
+ if (action === 'artifacts_clear' || action === 'artifacts_delete') setSelected(function (current) {
1796
+ return current.filter(function (item) { return item.source !== 'legacy' || (action !== 'artifacts_clear' && item.id !== body.artifact_id); });
1797
+ });
1798
+ load();
1799
+ }
1800
+ }).catch(function (error) { setNote('失败:' + error.message); }).finally(function () { setMutating(false); });
1421
1801
  }
1422
1802
 
1423
1803
  React.useEffect(function () {
1424
- function changed() { load(false); }
1804
+ function changed() { load(); }
1425
1805
  window.addEventListener('iris-core-artifacts-changed', changed);
1426
1806
  window.addEventListener('iris-core-refresh-tick', changed);
1427
- load(false);
1807
+ load();
1428
1808
  return function () {
1429
1809
  window.removeEventListener('iris-core-artifacts-changed', changed);
1430
1810
  window.removeEventListener('iris-core-refresh-tick', changed);
1811
+ if (request.current) request.current.abort(); request.current = null;
1431
1812
  };
1432
- }, [props.total]);
1433
- var galleryItems = coreItems.concat(items).sort(function (a, b) {
1434
- return String(b.createdAt || '').localeCompare(String(a.createdAt || ''));
1435
- });
1436
- var combinedTotal = coreItems.length + total;
1813
+ }, [query.offset, query.media_type, query.source, query.kind]);
1814
+ React.useEffect(function () { return function () { if (downloadRequest.current) downloadRequest.current.abort(); downloadRequest.current = null; }; }, []);
1815
+ function filter(key, value) {
1816
+ setSelected([]); if (management && management.type === 'delete') setManagement(null);
1817
+ setQuery(function (current) { var next = Object.assign({}, current, { offset: 0 }); next[key] = value; if (key !== 'kind') next.kind = 'all'; return next; });
1818
+ }
1819
+ function page(offset) { setQuery(function (current) { return Object.assign({}, current, { offset: offset }); }); }
1820
+ function analyze(item, action) { setManagement(null); sequence.current++; setAnalysis({ item: item, action: action, key: sequence.current }); }
1821
+ function manage(type, items) { setAnalysis(null); sequence.current++; setManagement({ type: type, items: items, item: items && items[0], key: sequence.current }); }
1822
+ function selectedItem(item) { return selected.some(function (value) { return value.source === item.source && value.id === item.id; }); }
1823
+ function choose(item, checked) {
1824
+ if (checked && selected.length >= 200) { setOperationNote('最多选择 200 个作品'); return; }
1825
+ setSelected(function (current) {
1826
+ var present = current.some(function (value) { return value.source === item.source && value.id === item.id; });
1827
+ return checked ? present || current.length >= 200 ? current : current.concat(item)
1828
+ : current.filter(function (value) { return value.source !== item.source || value.id !== item.id; });
1829
+ });
1830
+ }
1831
+ var pageSelected = data.items.length && data.items.every(selectedItem);
1832
+ function choosePage() {
1833
+ if (pageSelected) setSelected(function (current) { return current.filter(function (item) { return !data.items.some(function (value) { return value.source === item.source && value.id === item.id; }); }); });
1834
+ else {
1835
+ var additions = data.items.filter(function (item) { return !selectedItem(item); });
1836
+ if (selected.length + additions.length > 200) { setOperationNote('最多选择 200 个作品,请减少选择后再选本页'); return; }
1837
+ setSelected(function (current) {
1838
+ var next = current.concat(additions.filter(function (item) {
1839
+ return !current.some(function (value) { return value.source === item.source && value.id === item.id; });
1840
+ }));
1841
+ return next.length <= 200 ? next : current;
1842
+ });
1843
+ }
1844
+ }
1845
+ function changed(value) {
1846
+ if (value.state === 'committed') setSelected([]);
1847
+ setOperationNote((value.state === 'restored' ? '已恢复:' : '已移入回收区:') + value.transactionId);
1848
+ window.dispatchEvent(new CustomEvent('iris-core-artifacts-changed'));
1849
+ }
1850
+ function download(items) {
1851
+ var controller = new AbortController(); downloadRequest.current = controller;
1852
+ setMutating(true); setOperationNote('正在准备下载…');
1853
+ fetch('/iris/api/works/download', { method: 'POST', headers: { 'Content-Type': 'application/json' }, signal: controller.signal,
1854
+ body: JSON.stringify({ items: items.map(function (item) { return { source: item.source, id: item.id }; }) }) })
1855
+ .then(function (res) { if (!res.ok) return res.json().then(function (value) { throw new Error(value.error || '下载失败'); }); return res.blob(); })
1856
+ .then(function (blob) { if (downloadRequest.current === controller && !controller.signal.aborted) { saveWorkbenchBlob(blob, 'iris-works.zip'); setOperationNote('已下载选中作品'); } })
1857
+ .catch(function (error) { if (downloadRequest.current === controller && !controller.signal.aborted) setOperationNote(error.message); })
1858
+ .finally(function () { if (downloadRequest.current === controller) { downloadRequest.current = null; setMutating(false); } });
1859
+ }
1860
+ function cancelDownload() { if (downloadRequest.current) downloadRequest.current.abort(); downloadRequest.current = null; setMutating(false); setOperationNote('已取消下载'); }
1861
+ var pageNumber = Math.floor(data.offset / data.limit) + 1;
1862
+ var pageCount = Math.max(1, Math.ceil(data.total / data.limit));
1863
+ var blocked = busy || mutating;
1437
1864
 
1438
1865
  return React.createElement('div', {},
1439
1866
  React.createElement('div', { className: 'iris-gallery-tools' },
1440
1867
  React.createElement('span', { className: 'iris-clean-note' },
1441
- '共 ' + combinedTotal + ' 个作品 · Core ' + coreItems.length + ' · 旧版 ' + total + ' · 清任务历史不会删除'),
1442
- React.createElement('button', { className: 'iris-clean-btn', disabled: busy, onClick: function () { load(false); } }, '刷新'),
1443
- React.createElement('button', { className: 'iris-clean-btn', disabled: busy, onClick: function () { run('artifacts_reindex'); } }, '扫描旧版 outputs'),
1444
- total ? React.createElement('button', { className: 'iris-clean-btn danger', disabled: busy, onClick: function () { run('artifacts_clear', {}, '永久删除旧版 outputs 作品?Core 作品不会删除;任务历史会保留。此操作不可逆。'); } }, '清空旧版作品') : null),
1445
- galleryItems.length ? React.createElement('div', { className: 'iris-gallery-grid' }, galleryItems.map(function (item) {
1868
+ '匹配 ' + data.total + ' 个作品 · Core ' + data.counts.core + ' · 旧版 ' + data.counts.legacy + ' · 清任务历史不会删除'),
1869
+ React.createElement('button', { className: 'iris-clean-btn', disabled: blocked, onClick: load }, '刷新'),
1870
+ React.createElement('button', { className: 'iris-clean-btn', disabled: mutating, onClick: function () { manage('transactions'); } }, 'Core 回收区'),
1871
+ React.createElement('button', { className: 'iris-clean-btn', disabled: blocked, onClick: function () { run('artifacts_reindex'); } }, '扫描旧版 outputs'),
1872
+ data.counts.legacy ? React.createElement('button', { className: 'iris-clean-btn danger', disabled: blocked, onClick: function () { run('artifacts_clear', {}, '永久删除旧版 outputs 作品?Core 作品不会删除;任务历史会保留。此操作不可逆。'); } }, '清空旧版作品') : null),
1873
+ React.createElement('div', { className: 'iris-gallery-filters' },
1874
+ React.createElement('label', {}, '媒体类型', React.createElement('select', { 'aria-label': '媒体类型', value: query.media_type, disabled: mutating && !!management, onChange: function (e) { filter('media_type', e.target.value); } },
1875
+ [['all', '全部媒体'], ['image', '图片'], ['video', '视频'], ['audio', '音频'], ['text', '文本']].map(function (entry) { return React.createElement('option', { key: entry[0], value: entry[0] }, entry[1]); }))),
1876
+ React.createElement('label', {}, '作品来源', React.createElement('select', { 'aria-label': '作品来源', value: query.source, disabled: mutating && !!management, onChange: function (e) { filter('source', e.target.value); } },
1877
+ [['all', '全部来源'], ['core', 'Core'], ['legacy', '旧版']].map(function (entry) { return React.createElement('option', { key: entry[0], value: entry[0] }, entry[1]); }))),
1878
+ React.createElement('label', {}, '产物类型', React.createElement('select', { 'aria-label': '产物类型', value: query.kind, disabled: mutating && !!management, onChange: function (e) { filter('kind', e.target.value); } },
1879
+ ['all'].concat(data.kinds).map(function (kind) { return React.createElement('option', { key: kind, value: kind }, kind === 'all' ? '全部产物' : galleryKindLabel(kind)); })))),
1880
+ React.createElement('div', { className: 'iris-gallery-bulk' },
1881
+ React.createElement('span', { className: 'iris-wb-muted', role: 'status' }, '已选择 ' + selected.length + ' / 200 · 下载最多 128 MiB'),
1882
+ React.createElement('button', { type: 'button', className: 'iris-pm-btn', disabled: blocked || !data.items.length, onClick: choosePage }, pageSelected ? '取消本页选择' : '选择本页'),
1883
+ React.createElement('button', { type: 'button', className: 'iris-pm-btn', disabled: mutating || !selected.length,
1884
+ onClick: function () { setSelected([]); if (management && management.type === 'delete') setManagement(null); } }, '清空选择'),
1885
+ React.createElement('button', { type: 'button', className: 'iris-pm-btn', disabled: !selected.length,
1886
+ onClick: function () { copyWorkbenchText(selected.map(function (item) { return item.id; }).join('\n'), setOperationNote); } }, '复制选中 ID'),
1887
+ React.createElement('button', { type: 'button', className: 'iris-pm-btn', disabled: mutating || !selected.length, onClick: function () { download(selected); } }, '下载选中作品'),
1888
+ React.createElement('button', { type: 'button', className: 'iris-pm-btn danger', disabled: mutating || !selected.length || selected.some(function (item) { return item.source !== 'core'; }),
1889
+ title: '仅 Core 作品支持可恢复批量删除;请先按来源筛选', onClick: function () { manage('delete', selected); } }, '删除选中 Core 作品'),
1890
+ downloadRequest.current ? React.createElement('button', { type: 'button', className: 'iris-pm-btn', onClick: cancelDownload }, '取消下载') : null),
1891
+ analysis ? React.createElement(analysis.action === 'image_edit' ? ArtifactImageEdit : ArtifactAnalysis, { key: analysis.key, item: analysis.item, action: analysis.action, onClose: function () { setAnalysis(null); } }) : null,
1892
+ management ? React.createElement(WorkbenchManagement, { key: management.key, type: management.type, items: management.items, item: management.item,
1893
+ onBusy: setMutating, onChanged: changed, onClose: function () { setManagement(null); } }) : null,
1894
+ data.items.length ? React.createElement('div', { className: 'iris-gallery-grid', 'aria-busy': busy }, data.items.map(function (item) {
1446
1895
  var isCore = item.source === 'core';
1447
- return React.createElement('div', { key: (isCore ? 'core-' : 'legacy-') + item.id, className: 'iris-gallery-card' },
1896
+ return React.createElement('div', { key: (isCore ? 'core-' : 'legacy-') + item.id, className: 'iris-gallery-card' + (selectedItem(item) ? ' selected' : '') },
1448
1897
  artifactPreview(item),
1449
1898
  React.createElement('div', { className: 'iris-gallery-info' },
1899
+ React.createElement('label', { className: 'iris-gallery-select' }, React.createElement('input', { type: 'checkbox',
1900
+ 'aria-label': '选择作品 ' + item.id, checked: selectedItem(item), disabled: mutating || (!selectedItem(item) && selected.length >= 200),
1901
+ onChange: function (event) { choose(item, event.target.checked); } }), '选择'),
1450
1902
  React.createElement('div', { className: 'iris-gallery-file', title: item.file }, item.file),
1451
1903
  React.createElement('div', { className: 'iris-gallery-meta' },
1452
- (isCore ? 'Core · ' + (item.kind || 'artifact') + (item.digest ? ' · sha256 ' + item.digest : '') : fmtTime(item.createdAt)) + ' · ' + fmtBytes(item.size)),
1904
+ (isCore ? 'Core · ' + galleryKindLabel(item.kind) : '旧版') + ' · ' + fmtTime(item.createdAt) + ' · ' + fmtBytes(item.size)),
1453
1905
  React.createElement('div', { className: 'iris-gallery-actions' },
1454
1906
  React.createElement('a', { className: 'iris-wb-link', href: item.url, target: '_blank', rel: 'noreferrer' }, '打开'),
1455
- isCore ? React.createElement('span', { className: 'iris-wb-muted' }, '内容哈希保护')
1456
- : React.createElement('button', { className: 'iris-pm-btn danger', disabled: busy, onClick: function () { run('artifacts_delete', { artifact_id: item.id }, '永久删除作品“' + item.file + '”?任务历史会保留,但媒体将无法打开。'); } }, '删除'))));
1457
- })) : React.createElement('div', { className: 'iris-wb-empty' }, busy ? '正在读取作品…' : '尚无作品;旧 outputs 文件可点“扫描旧版 outputs”找回。'),
1458
- items.length < total ? React.createElement('button', { className: 'iris-clean-btn', disabled: busy, onClick: function () { load(true); } }, busy ? '加载中…' : '加载更多旧版作品') : null,
1459
- note ? React.createElement('div', { className: 'iris-clean-note', style: { marginTop: '7px' } }, note) : null);
1907
+ React.createElement('button', { className: 'iris-pm-btn', type: 'button', disabled: mutating, onClick: function () { manage('details', [item]); } }, '详情'),
1908
+ isCore ? React.createElement('button', { className: 'iris-pm-btn danger', type: 'button', disabled: mutating, onClick: function () { manage('delete', [item]); } }, '移入回收区') : null,
1909
+ isCore && /^image\/(png|jpeg|webp|gif)$/.test(item.mime) ? ['look', 'ocr'].map(function (action) {
1910
+ return React.createElement('button', { key: action, type: 'button', className: 'iris-pm-btn', disabled: mutating, onClick: function () { analyze(item, action); } }, action === 'look' ? '看图' : 'OCR');
1911
+ }) : null,
1912
+ isCore && /^image\/(png|jpeg|webp)$/.test(item.mime) ? React.createElement('button', { type: 'button', className: 'iris-pm-btn', disabled: mutating,
1913
+ onClick: function () { analyze(item, 'image_edit'); } }, '改图') : null,
1914
+ isCore ? React.createElement('span', { className: 'iris-wb-muted', title: '打开或识别时核验原文件' }, '内容哈希保护')
1915
+ : React.createElement('button', { className: 'iris-pm-btn danger', disabled: blocked, onClick: function () { run('artifacts_delete', { artifact_id: item.id }, '永久删除作品“' + item.file + '”?任务历史会保留,但媒体将无法打开。'); } }, '删除'))));
1916
+ })) : React.createElement('div', { className: 'iris-wb-empty' }, busy ? '正在读取作品…'
1917
+ : query.media_type !== 'all' || query.source !== 'all' || query.kind !== 'all' ? '没有匹配的作品,请调整筛选条件。' : '尚无作品;旧 outputs 文件可点“扫描旧版 outputs”找回。'),
1918
+ React.createElement('div', { className: 'iris-gallery-pagination' },
1919
+ React.createElement('button', { type: 'button', className: 'iris-clean-btn', disabled: blocked || pageNumber <= 1, onClick: function () { page(Math.max(0, data.offset - data.limit)); } }, '上一页'),
1920
+ React.createElement('span', { className: 'iris-wb-muted', role: 'status' }, '第 ' + pageNumber + ' / ' + pageCount + ' 页'),
1921
+ React.createElement('button', { type: 'button', className: 'iris-clean-btn', disabled: blocked || pageNumber >= pageCount, onClick: function () { page(data.offset + data.limit); } }, '下一页')),
1922
+ note ? React.createElement('div', { className: 'iris-clean-note', style: { marginTop: '7px' } }, note) : null,
1923
+ operationNote ? React.createElement('div', { role: 'status', className: 'iris-clean-note', style: { marginTop: '7px', whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' } }, operationNote) : null);
1460
1924
  }
1461
1925
 
1462
1926
  function coreCapabilityLabel(value) {
@@ -1669,9 +2133,14 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1669
2133
  if (draft === null) return null;
1670
2134
  draft = String(draft).trim();
1671
2135
  if (!draft) return null;
1672
- return isTts ? { text: draft, confirmBilling: true }
2136
+ var sourceArtifactId;
2137
+ if (props.row.sourceArtifactId) {
2138
+ sourceArtifactId = window.prompt('请确认新改图任务使用的原图 Artifact ID:', props.row.sourceArtifactId);
2139
+ if (sourceArtifactId === null || !String(sourceArtifactId).trim()) return null;
2140
+ }
2141
+ return isTts ? { text: draft, confirmBilling: true }
1673
2142
  : isTranscribe ? { audio_url: draft, confirmBilling: true }
1674
- : { prompt: draft, confirmBilling: true };
2143
+ : { prompt: draft, confirmBilling: true, ...(sourceArtifactId ? { source_artifact_id: String(sourceArtifactId).trim() } : {}) };
1675
2144
  }
1676
2145
  });
1677
2146
  }
@@ -1918,7 +2387,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1918
2387
  React.createElement(CoreRetryButton, {
1919
2388
  row: selectedTask && selectedTask.phase === 'terminal'
1920
2389
  && !(selectedTask.outcome === 'succeeded' && selectedTask.deliveryState === 'ready')
1921
- ? { id: selectedTask.id, retryable: true } : null
2390
+ ? { id: selectedTask.id, capability: selectedTask.capability, sourceArtifactId: selectedTask.sourceArtifactId, retryable: true } : null
1922
2391
  }),
1923
2392
  React.createElement(CoreDispositionButtons, {
1924
2393
  row: selectedDisposition
@@ -2105,7 +2574,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
2105
2574
  React.createElement(ProviderManager, {}),
2106
2575
  React.createElement('div', { className: 'iris-wb-sec' }, '能力健康'),
2107
2576
  React.createElement(CapabilityHealthOverview, {}),
2108
- React.createElement('div', { className: 'iris-wb-muted' }, '绿色有效 7 天;真实成功或显式实测会刷新。不会后台探测,也不会因 429、网络或供应商 5xx 变红。'),
2577
+ React.createElement('div', { className: 'iris-wb-muted' }, '绿色有效 7 天;真实成功或显式实测会刷新。429 冷却显示琥珀色,额度或预算耗尽显示红色;点击模型能力可实测恢复。不会后台探测。'),
2109
2578
  React.createElement('div', { className: 'iris-wb-sec' }, '能力分配(failover 顺序)'),
2110
2579
  React.createElement(CapabilityAssigner, {}),
2111
2580
  React.createElement('div', { className: 'iris-wb-sec' }, 'Iris 泡泡快捷卡片(勾选后显示在「⚡ 常用」标签)'),
@@ -2205,6 +2674,17 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
2205
2674
  selectedTask ? React.createElement(TaskDetailDrawer, { taskId: selectedTask }) : null);
2206
2675
  }
2207
2676
 
2677
+ // 折叠相同首尾,只显示中间替换区;按 Unicode code point 比较,不拆开 emoji。
2678
+ function promptChanges(original, edited) {
2679
+ var before = Array.from(original);
2680
+ var after = Array.from(edited);
2681
+ var start = 0;
2682
+ while (start < before.length && start < after.length && before[start] === after[start]) start++;
2683
+ var end = 0;
2684
+ while (end < before.length - start && end < after.length - start && before[before.length - end - 1] === after[after.length - end - 1]) end++;
2685
+ return { removed: before.slice(start, before.length - end).join(''), added: after.slice(start, after.length - end).join('') };
2686
+ }
2687
+
2208
2688
  function PromptOptimizerControl(props) {
2209
2689
  var draft = props.useInput(function (value) { return value.draft; });
2210
2690
  var phase = props.useInput(function (value) { return value.phase; });
@@ -2230,8 +2710,26 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
2230
2710
  var configPair = React.useState(null);
2231
2711
  var configState = configPair[0];
2232
2712
  var setConfigState = configPair[1];
2713
+ var optimizationPair = React.useState('');
2714
+ var optimizationText = optimizationPair[0];
2715
+ var setOptimizationText = optimizationPair[1];
2716
+ var prefixPair = React.useState('');
2717
+ var prefixText = prefixPair[0];
2718
+ var setPrefixText = prefixPair[1];
2719
+ var suffixPair = React.useState('');
2720
+ var suffixText = suffixPair[0];
2721
+ var setSuffixText = suffixPair[1];
2722
+ var editedPair = React.useState('');
2723
+ var edited = editedPair[0];
2724
+ var setEdited = editedPair[1];
2233
2725
  var abortRef = React.useRef(null);
2234
2726
  var fileRef = React.useRef(null);
2727
+ var lastWrittenRef = React.useRef(null);
2728
+ var resultSessionRef = React.useRef(null);
2729
+ var sessionRef = React.useRef(props.sessionId);
2730
+ sessionRef.current = props.sessionId;
2731
+ var supportsRules = !!(configState && configState.capabilities && configState.capabilities.rules === true);
2732
+ var supportsAssembly = !!(configState && configState.capabilities && configState.capabilities.assemble === true);
2235
2733
 
2236
2734
  function promptRequest(operation, body, signal) {
2237
2735
  return fetch('/iris/api/prompt-optimizer/' + operation, {
@@ -2257,6 +2755,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
2257
2755
  React.useEffect(function () {
2258
2756
  return function () {
2259
2757
  if (abortRef.current) abortRef.current.abort();
2758
+ abortRef.current = null;
2260
2759
  };
2261
2760
  }, []);
2262
2761
 
@@ -2267,53 +2766,96 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
2267
2766
  return function () { window.removeEventListener('iris-prompt-config-changed', changed); };
2268
2767
  }, []);
2269
2768
 
2769
+ React.useEffect(function () {
2770
+ cancel();
2771
+ setResult(null);
2772
+ lastWrittenRef.current = null;
2773
+ resultSessionRef.current = null;
2774
+ }, [props.sessionId]);
2775
+
2270
2776
  function publishConfig(data) {
2271
2777
  setConfigState(data);
2272
2778
  try { window.dispatchEvent(new CustomEvent('iris-prompt-config-changed', { detail: data })); } catch (_) {}
2273
2779
  }
2274
2780
 
2275
- function optimize() {
2781
+ function selectedRules(mode) {
2782
+ var rules = [];
2783
+ if (!supportsRules) return rules;
2784
+ if (mode === 'optimize' && optimizationText.trim()) rules.push({ id: 'custom-optimization', label: '本次改写规则', kind: 'optimization', text: optimizationText });
2785
+ if (prefixText.trim()) rules.push({ id: 'custom-prefix', label: '输出前缀', kind: 'output', position: 'prefix', text: prefixText });
2786
+ if (suffixText.trim()) rules.push({ id: 'custom-suffix', label: '输出后缀', kind: 'output', position: 'suffix', text: suffixText });
2787
+ return rules;
2788
+ }
2789
+
2790
+ function optimize(mode) {
2791
+ mode = mode === 'assemble' ? 'assemble' : 'optimize';
2792
+ if (mode === 'assemble' && !supportsAssembly) { setNote('当前后端尚未支持只组装,请更新并重新载入'); return; }
2276
2793
  if (!draft || !draft.trim()) { setNote('请先在输入框中写入提示词'); return; }
2277
2794
  if (refCount) { setNote('当前版本暂不改写含 @ 或 / 结构化引用的草稿,以免引用失效'); return; }
2278
2795
  if (phase !== 'plain') { setNote('输入框正忙,请稍后再试'); return; }
2279
2796
  if (abortRef.current) abortRef.current.abort();
2280
2797
  var controller = new AbortController();
2798
+ var requestSessionId = props.sessionId;
2281
2799
  abortRef.current = controller;
2282
2800
  setBusy(true);
2283
- setNote('正在调用模型优化…这可能产生模型费用');
2801
+ setNote(mode === 'assemble' ? '正在组装,不调用模型…' : '正在调用模型优化…这可能产生模型费用');
2284
2802
  setResult(null);
2803
+ lastWrittenRef.current = null;
2285
2804
  promptRequest('optimize', {
2286
2805
  text: draft,
2287
2806
  target: target,
2807
+ mode: mode,
2808
+ rules: selectedRules(mode),
2288
2809
  sessionId: props.sessionId,
2289
2810
  route: selectedRoute || undefined
2290
2811
  }, controller.signal).then(function (data) {
2812
+ if (abortRef.current !== controller || controller.signal.aborted) return;
2813
+ if (sessionRef.current !== requestSessionId) throw new Error('会话已切换,请在当前会话重新生成预览');
2814
+ if (!data.ok || typeof data.optimized !== 'string' || typeof data.original !== 'string') throw new Error('未收到完整优化结果');
2291
2815
  setResult(data);
2816
+ setEdited(data.optimized);
2817
+ resultSessionRef.current = requestSessionId;
2292
2818
  setNote('已生成预览,尚未写回输入框');
2293
2819
  }).catch(function (err) {
2820
+ if (abortRef.current !== controller) return;
2294
2821
  if (controller.signal.aborted) setNote('已取消');
2295
2822
  else setNote(String(err && err.message || err));
2296
2823
  }).finally(function () {
2297
- if (abortRef.current === controller) abortRef.current = null;
2298
- setBusy(false);
2824
+ if (abortRef.current === controller) { abortRef.current = null; setBusy(false); }
2299
2825
  });
2300
2826
  }
2301
2827
 
2302
2828
  function cancel() {
2303
- if (abortRef.current) abortRef.current.abort();
2829
+ if (abortRef.current) {
2830
+ abortRef.current.abort();
2831
+ abortRef.current = null;
2832
+ setBusy(false);
2833
+ setNote('已取消');
2834
+ }
2835
+ }
2836
+
2837
+ function closePanel() { cancel(); setOpen(false); }
2838
+
2839
+ function validEdited() {
2840
+ if (!edited.trim() || edited.length > 16000) { setNote('结果必须是非空文本且不超过 16000 字符'); return false; }
2841
+ return true;
2304
2842
  }
2305
2843
 
2306
2844
  function replaceDraft(text) {
2307
2845
  if (!result) return;
2308
- if (draft !== result.original && !window.confirm('输入框内容在优化期间已变化,仍要替换吗?')) return;
2846
+ if (resultSessionRef.current !== props.sessionId) { setNote('会话已切换,请在当前会话重新生成预览'); return; }
2847
+ if (refCount || phase !== 'plain') { setNote('当前输入框正忙或含结构化引用,暂不写回'); return; }
2848
+ if (text !== result.original && !validEdited()) return;
2849
+ if (draft !== result.original && draft !== lastWrittenRef.current && !window.confirm('输入框内容在优化期间已变化,仍要替换吗?')) return;
2309
2850
  props.inputActions.setDraft(text);
2851
+ lastWrittenRef.current = text;
2310
2852
  setNote(text === result.original ? '已恢复优化前原文' : '已写回输入框;不会自动发送');
2311
2853
  }
2312
2854
 
2313
2855
  function copyResult() {
2314
- if (!result) return;
2856
+ if (!result || !validEdited()) return;
2315
2857
  if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
2316
- navigator.clipboard.writeText(result.optimized).then(function () { setNote('已复制优化结果'); }).catch(function () { setNote('复制失败,请手动选择文本'); });
2858
+ navigator.clipboard.writeText(edited).then(function () { setNote('已复制优化结果'); }).catch(function () { setNote('复制失败,请手动选择文本'); });
2317
2859
  } else {
2318
2860
  setNote('浏览器不支持自动复制,请手动选择文本');
2319
2861
  }
@@ -2379,15 +2921,21 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
2379
2921
  var reasoningSetting = configState && configState.config && configState.config.generation && configState.config.generation.reasoningEffort || 'off-if-supported';
2380
2922
  var reasoningLabel = reasoningSetting === 'off-if-supported' ? '支持时关闭' : reasoningSetting === 'provider-default' ? '供应商默认' : reasoningSetting === 'inherit' ? '跟随会话' : reasoningSetting;
2381
2923
  var outputBudget = configState && configState.config && configState.config.generation && configState.config.generation.maxOutputTokens || 1200;
2924
+ var changes = result ? promptChanges(result.original, edited) : null;
2925
+ function ruleField(label, value, update, placeholder) {
2926
+ return React.createElement('label', { className: 'iris-po-field' }, React.createElement('span', {}, label),
2927
+ React.createElement('textarea', { value: value, disabled: busy || !supportsRules, placeholder: placeholder,
2928
+ onChange: function (event) { update(event.target.value); } }));
2929
+ }
2382
2930
 
2383
2931
  var panel = null;
2384
2932
  if (open) {
2385
2933
  panel = React.createElement(React.Fragment, {},
2386
- React.createElement('div', { className: 'iris-po-backdrop', onClick: function () { setOpen(false); }, 'aria-hidden': 'true' }),
2934
+ React.createElement('div', { className: 'iris-po-backdrop', onClick: closePanel, 'aria-hidden': 'true' }),
2387
2935
  React.createElement('div', { className: 'iris-po-panel', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Iris 提示词优化', onClick: function (e) { e.stopPropagation(); } },
2388
2936
  React.createElement('div', { className: 'iris-po-head' },
2389
2937
  React.createElement('strong', { className: 'iris-po-title' }, React.createElement('span', { className: 'iris-po-mark' }, '🫧'), 'Iris 提示词优化'),
2390
- React.createElement('button', { className: 'iris-po-close', onClick: function () { setOpen(false); }, title: '关闭' }, '✕')),
2938
+ React.createElement('button', { className: 'iris-po-close', onClick: closePanel, title: '关闭' }, '✕')),
2391
2939
  React.createElement('div', { className: 'iris-po-muted' }, '只处理当前未发送草稿,不读取会话历史;调用模型可能产生费用。'),
2392
2940
  React.createElement('label', { className: 'iris-po-field' },
2393
2941
  React.createElement('span', {}, '优化目标'),
@@ -2397,17 +2945,37 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
2397
2945
  React.createElement('option', { value: 'video' }, '视频生成'),
2398
2946
  React.createElement('option', { value: 's2v' }, '首尾帧视频'))),
2399
2947
  React.createElement('div', { className: 'iris-po-route', title: routeLabel }, '模型:' + routeLabel + ' · 思考:' + reasoningLabel + ' · 输出:' + outputBudget + ' tokens'),
2948
+ React.createElement('details', { className: 'iris-po-config' },
2949
+ React.createElement('summary', {}, '本次规则(可选)'),
2950
+ React.createElement('div', { className: 'iris-po-rules' },
2951
+ ruleField('改写规则 · 仅智能优化使用', optimizationText, setOptimizationText, '例如:保持简短,保留全部否定条件'),
2952
+ ruleField('输出前缀 · 原样加入结果开头', prefixText, setPrefixText, '例如:请以摄影师的视角描述以下画面。'),
2953
+ ruleField('输出后缀 · 原样加入结果末尾', suffixText, setSuffixText, '例如:禁止添加文字和水印。')),
2954
+ React.createElement('div', { className: 'iris-po-muted' }, supportsRules ? '非空内容表示本次选择;只组装保留原稿,只添加前后缀。刷新后重置,不修改 JSON 配置。' : '当前后端尚未支持规则,请更新后重新载入。')),
2400
2955
  React.createElement('div', { className: 'iris-po-actions' },
2401
- React.createElement('button', { className: 'iris-po-primary', disabled: busy || !draft || !draft.trim() || phase !== 'plain' || refCount > 0, onClick: optimize }, busy ? '优化中…' : '生成预览'),
2956
+ React.createElement('button', { className: 'iris-po-primary', disabled: busy || !draft || !draft.trim() || phase !== 'plain' || refCount > 0, onClick: function () { optimize('optimize'); } }, busy ? '处理中…' : '智能优化'),
2957
+ React.createElement('button', { className: 'iris-po-btn', disabled: busy || !supportsAssembly || !draft || !draft.trim() || phase !== 'plain' || refCount > 0, onClick: function () { optimize('assemble'); } }, '只组装(不调用模型)'),
2402
2958
  busy ? React.createElement('button', { className: 'iris-po-btn', onClick: cancel }, '取消') : null),
2403
2959
  result ? React.createElement('div', { className: 'iris-po-result' },
2404
- React.createElement('div', { className: 'iris-po-result-label' }, '优化结果'),
2405
- React.createElement('textarea', { readOnly: true, value: result.optimized }),
2406
- React.createElement('div', { className: 'iris-po-route' }, '实际使用:' + result.route.provider + ' / ' + result.route.model + ' · 思考:' + (result.route.reasoningEffort === 'provider-default' ? '供应商默认' : result.route.reasoningEffort)),
2960
+ React.createElement('div', { className: 'iris-po-result-label' }, '结果预览 · 可编辑'),
2961
+ React.createElement('textarea', { 'aria-label': '结果预览', value: edited, onChange: function (event) { setEdited(event.target.value); } }),
2962
+ React.createElement('details', { className: 'iris-po-config' }, React.createElement('summary', {}, '原文与改动'),
2963
+ React.createElement('textarea', { 'aria-label': '优化前原文', readOnly: true, value: result.original }),
2964
+ React.createElement('div', { className: 'iris-po-muted' }, '以下折叠相同首尾,显示中间替换区:'),
2965
+ React.createElement('div', { className: 'iris-po-diff' },
2966
+ changes.removed ? React.createElement('del', {}, changes.removed) : null,
2967
+ changes.added ? React.createElement('ins', {}, changes.added) : null,
2968
+ !changes.removed && !changes.added ? '与原文相同' : null)),
2969
+ React.createElement('details', { className: 'iris-po-config' }, React.createElement('summary', {}, '本次参与规则'),
2970
+ (result.rules || []).length ? (result.rules || []).map(function (rule) {
2971
+ return React.createElement('div', { className: 'iris-po-diff', key: rule.id }, rule.label + '(' + (rule.kind === 'optimization' ? '指导改写' : rule.position === 'prefix' ? '输出前缀' : '输出后缀') + ')\n' + rule.text);
2972
+ }) : React.createElement('div', { className: 'iris-po-muted' }, '未选择额外规则'),
2973
+ result.processing ? React.createElement('div', { className: 'iris-po-diff' }, '优化处理规则:\n' + result.processing.systemPrompt + '\n\n目标模板:\n' + result.processing.targetTemplate) : null),
2974
+ React.createElement('div', { className: 'iris-po-route' }, result.route ? '实际使用:' + result.route.provider + ' / ' + result.route.model + ' · 思考:' + (result.route.reasoningEffort === 'provider-default' ? '供应商默认' : result.route.reasoningEffort) : '本次只组装,未调用模型'),
2407
2975
  React.createElement('div', { className: 'iris-po-actions' },
2408
- React.createElement('button', { className: 'iris-po-primary', onClick: function () { replaceDraft(result.optimized); } }, '写回输入框'),
2976
+ React.createElement('button', { className: 'iris-po-primary', onClick: function () { replaceDraft(edited); } }, '写回输入框'),
2409
2977
  React.createElement('button', { className: 'iris-po-btn', onClick: copyResult }, '复制'),
2410
- React.createElement('button', { className: 'iris-po-btn', onClick: optimize }, '再优化'),
2978
+ React.createElement('button', { className: 'iris-po-btn', disabled: busy, onClick: function () { optimize(result.mode); } }, '重新生成预览'),
2411
2979
  React.createElement('button', { className: 'iris-po-btn', onClick: function () { replaceDraft(result.original); } }, '恢复原文'))) : null,
2412
2980
  note ? React.createElement('div', { className: 'iris-po-note' }, note) : null,
2413
2981
  React.createElement('details', { className: 'iris-po-config' },
@@ -2428,7 +2996,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
2428
2996
  type: 'button',
2429
2997
  title: refCount ? '含结构化引用的草稿暂不支持优化' : '优化当前输入框提示词',
2430
2998
  'aria-expanded': open ? 'true' : 'false',
2431
- onClick: function () { setOpen(!open); }
2999
+ onClick: function () { if (open) closePanel(); else setOpen(true); }
2432
3000
  }, '🫧'),
2433
3001
  panel);
2434
3002
  }
@@ -2473,8 +3041,10 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
2473
3041
  var state = useIrisState(5000);
2474
3042
  var providers = (state && state.providers) || [];
2475
3043
  var fallbackConfigured = providers.some(function (p) { return p.enabled && p.apiKeyHint; });
2476
- var overallHealth = state && state.health && HEALTH_META[state.health.overall]
3044
+ var overallState = state && state.health && HEALTH_META[state.health.overall]
2477
3045
  ? state.health.overall : (fallbackConfigured ? 'configured' : 'unconfigured');
3046
+ var overallCooling = overallState === 'failed' && state.health.retryAt;
3047
+ var overallHealth = overallCooling ? 'cooling' : overallState;
2478
3048
  var running = (state && state.tasks && state.tasks.running) || [];
2479
3049
  var attention = (state && state.tasks && state.tasks.attention) || [];
2480
3050
 
@@ -2600,7 +3170,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
2600
3170
  onPointerMove: onPointerMove,
2601
3171
  onPointerUp: onPointerUp,
2602
3172
  onPointerCancel: onPointerCancel,
2603
- title: 'Iris ' + healthText({ status: overallHealth }) + ':点击打开快捷面板'
3173
+ title: 'Iris ' + healthText({ status: overallState, rateLimited: !!overallCooling, retryAt: overallCooling || undefined }) + ':点击打开快捷面板'
2604
3174
  },
2605
3175
  React.createElement('div', { className: 'iris-bubble-btn' }, React.createElement(IRIS_MARK_SVG)),
2606
3176
  badge,