@mokuyoaxis/dsh-iris 0.1.4 → 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 (110) 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 +99 -1
  5. package/README.en.md +124 -31
  6. package/README.md +124 -31
  7. package/bin/dsh-iris.js +593 -13
  8. package/docs/AGENT_QUICKSTART.md +39 -0
  9. package/docs/ARCHITECTURE.md +87 -64
  10. package/docs/ARTIFACT_LIBRARY.md +5 -5
  11. package/docs/ARTIFACT_MANIFEST.md +60 -0
  12. package/docs/CLI_MANAGEMENT.md +181 -0
  13. package/docs/COMPOSITE_VISION.md +35 -0
  14. package/docs/CORE_RUNTIME_CONTRACT.md +84 -0
  15. package/docs/DSH_CORE_MIGRATION.md +135 -0
  16. package/docs/DSH_RC2_ADAPTATION.md +38 -0
  17. package/docs/FAULT_INJECTION.md +12 -2
  18. package/docs/HEADLESS_CLI.md +223 -0
  19. package/docs/HOST_ADAPTER_CONTRACT.md +36 -17
  20. package/docs/HOST_DOCTOR.md +4 -2
  21. package/docs/MODEL_PORT_CONTRACT.md +305 -0
  22. package/docs/OCR_MODEL.md +39 -0
  23. package/docs/PROMPT_OPTIMIZER.md +47 -0
  24. package/docs/PROVIDER_ADAPTER_CONTRACT.md +53 -10
  25. package/docs/PROVIDER_HEALTH.md +22 -5
  26. package/docs/PROVIDER_RUNTIME_HARNESS.md +33 -0
  27. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +2 -2
  28. package/docs/ROADMAP.md +49 -83
  29. package/docs/SECURITY.md +8 -7
  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/assets/logo/README.md +55 -0
  35. package/docs/assets/logo/iris-bubble-concept.svg +67 -0
  36. package/docs/assets/logo/iris-bubble.svg +14 -0
  37. package/docs/assets/logo/iris-interim-flower.png +0 -0
  38. package/docs/assets/logo/iris-logo-mono-inverse.svg +78 -0
  39. package/docs/assets/logo/iris-logo-mono.svg +78 -0
  40. package/docs/assets/logo/iris-logo.svg +78 -0
  41. package/docs/assets/logo/iris-mark-mono-inverse.svg +42 -0
  42. package/docs/assets/logo/iris-mark-mono.svg +42 -0
  43. package/docs/assets/logo/iris-mark.svg +42 -0
  44. package/docs/file-access-across-environments.md +8 -5
  45. package/docs/releases/0.2.0.md +48 -0
  46. package/docs/releases/0.2.1.md +56 -0
  47. package/lib/actions.js +520 -591
  48. package/lib/adapters.js +184 -36
  49. package/lib/api.js +200 -11
  50. package/lib/chromium-browser.js +117 -0
  51. package/lib/client.js +1390 -105
  52. package/lib/command-service.js +534 -0
  53. package/lib/composite-vision-routing.js +42 -0
  54. package/lib/config.js +143 -51
  55. package/lib/core-artifact-store.js +630 -0
  56. package/lib/core-artifacts.js +4 -0
  57. package/lib/core-attention.js +103 -0
  58. package/lib/core-contract.js +105 -0
  59. package/lib/core-doctor.js +41 -0
  60. package/lib/core-lease-recovery.js +170 -0
  61. package/lib/core-maintenance.js +213 -0
  62. package/lib/core-runtime.js +232 -0
  63. package/lib/core-tasks.js +567 -0
  64. package/lib/core-user-projection.js +193 -0
  65. package/lib/doctor.js +92 -33
  66. package/lib/dsh-core-adapter.js +653 -0
  67. package/lib/dsh-host-adapter.js +58 -7
  68. package/lib/dsh-text-model-adapter.js +222 -0
  69. package/lib/dsh-vision-model-adapter.js +75 -0
  70. package/lib/generation-input.js +126 -0
  71. package/lib/guard.js +3 -3
  72. package/lib/headless-vision.js +181 -0
  73. package/lib/host-runtime.js +1 -0
  74. package/lib/http-vision-model-adapter.js +187 -0
  75. package/lib/image-edit-input.js +34 -0
  76. package/lib/index.js +323 -188
  77. package/lib/locate.js +86 -28
  78. package/lib/media-probe.js +2 -1
  79. package/lib/model-call-runtime.js +45 -0
  80. package/lib/model-invoker.js +220 -0
  81. package/lib/model-port-contract.js +341 -0
  82. package/lib/models.js +105 -14
  83. package/lib/ocr-model-routing.js +26 -0
  84. package/lib/ocr.js +110 -64
  85. package/lib/prompt-optimizer-config.js +2 -1
  86. package/lib/prompt-optimizer-core.js +121 -0
  87. package/lib/prompt-optimizer.js +37 -193
  88. package/lib/provider-adapter.js +49 -23
  89. package/lib/provider-adapters.js +165 -26
  90. package/lib/provider-catalog.js +183 -0
  91. package/lib/provider-config-service.js +236 -0
  92. package/lib/provider-contract.js +74 -4
  93. package/lib/provider-health.js +80 -1
  94. package/lib/provider-protocol.js +74 -2
  95. package/lib/provider-task-runner.js +370 -0
  96. package/lib/summarize.js +31 -20
  97. package/lib/summary-input.js +52 -0
  98. package/lib/task-wait.js +40 -0
  99. package/lib/tasks.js +16 -4
  100. package/lib/video-input.js +30 -0
  101. package/lib/vision-core.js +42 -0
  102. package/lib/vision-image-input.js +83 -0
  103. package/lib/vision-input.js +31 -0
  104. package/lib/vision-model-routing.js +180 -0
  105. package/lib/vision.js +31 -9
  106. package/lib/workbench-artifacts.js +52 -0
  107. package/lib/workbench-management.js +151 -0
  108. package/lib/workbench-zip.js +39 -0
  109. package/package.json +8 -8
  110. package/user_guide.md +71 -26
package/lib/client.js CHANGED
@@ -14,14 +14,14 @@
14
14
  *
15
15
  * 数据通道:host 侧 /iris/api/state 同源 JSON 路由(复用 /iris/media 同款
16
16
  * webServer 前缀模式),client 侧 fetch 轮询。全走标量,
17
- * apiKey 永不明文,产物给授权播放链接。
17
+ * apiKey 永不明文;legacy 产物使用 token 链接,Core 产物使用受 Host/浏览器来源守卫保护的 Artifact ID 链接。
18
18
  */
19
19
  window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) => {
20
20
  var module = { exports: {} };
21
21
  var exports = module.exports;
22
22
 
23
23
  var React = require('react');
24
- var IRIS_CLIENT_VERSION = '0.1.4';
24
+ var IRIS_CLIENT_VERSION = '0.2.1';
25
25
  var HOST_CLIENT_PROTOCOL_VERSION = 0;
26
26
  var reportedSeats = [];
27
27
 
@@ -102,6 +102,35 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
102
102
  '.iris-doctor-row.error { color: var(--dsw-alias-state-error-primary, #e06c6c); }',
103
103
  '.iris-doctor-details { margin-top: 7px; color: var(--dsw-alias-label-secondary, #9a9a9a); }',
104
104
  '.iris-doctor-details summary { cursor: pointer; font-size: 11px; }',
105
+ '.iris-core-runtime { cursor: default; padding: 0; overflow: hidden; }',
106
+ '.iris-core-runtime-head { display: flex; align-items: center; gap: 8px; padding: 9px 10px; }',
107
+ '.iris-core-runtime-title { min-width: 0; flex: 1; display: grid; gap: 1px; }',
108
+ '.iris-core-runtime-title strong { font-size: 12px; }',
109
+ '.iris-core-runtime-body { display: grid; gap: 8px; padding: 0 10px 10px; border-top: 1px dashed var(--dsw-alias-border-l1, #3a3a44); }',
110
+ '.iris-core-runtime-tools { display: flex; align-items: center; gap: 7px; padding-top: 8px; flex-wrap: wrap; }',
111
+ '.iris-core-task-list { display: grid; gap: 6px; }',
112
+ '.iris-core-task-row { width: 100%; box-sizing: border-box; display: grid; grid-template-columns: 9px minmax(0, 1fr) auto; align-items: center; gap: 8px; padding: 8px 9px; text-align: left; color: inherit; background: var(--dsw-alias-bg-layer-2, #32323c); border: 1px solid transparent; border-radius: 8px; cursor: pointer; font-family: inherit; }',
113
+ '.iris-core-task-row:hover, .iris-core-task-row.selected { border-color: var(--dsw-alias-brand-primary, #7aa2f7); }',
114
+ '.iris-core-state-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--dsw-alias-label-tertiary, #73737d); }',
115
+ '.iris-core-state-dot.ok { background: var(--dsw-alias-state-success-primary, #6fcf6f); box-shadow: 0 0 6px rgba(111,207,111,.45); }',
116
+ '.iris-core-state-dot.warn { background: var(--dsw-alias-state-warn-primary, #d9a941); }',
117
+ '.iris-core-state-dot.error { background: #b9656d; }',
118
+ '.iris-core-task-main { min-width: 0; display: grid; gap: 1px; }',
119
+ '.iris-core-task-main strong { font-size: 12px; font-weight: 600; }',
120
+ '.iris-core-task-arrow { color: var(--dsw-alias-label-secondary, #9a9a9a); font-size: 10px; }',
121
+ '.iris-core-detail { display: grid; gap: 9px; padding: 10px; border-radius: 9px; background: var(--dsw-alias-bg-layer-2, #32323c); border: 1px solid var(--dsw-alias-border-l1, #3a3a44); }',
122
+ '.iris-core-detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; }',
123
+ '.iris-core-detail-cell { min-width: 0; display: grid; gap: 1px; }',
124
+ '.iris-core-detail-cell span:last-child { overflow-wrap: anywhere; }',
125
+ '.iris-core-id { user-select: all; overflow-wrap: anywhere; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; }',
126
+ '.iris-core-attempt { display: grid; gap: 3px; padding: 7px 8px; border-radius: 7px; background: var(--dsw-alias-bg-layer-1, #26262e); }',
127
+ '.iris-core-artifact { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; padding: 7px 8px; border-radius: 7px; background: var(--dsw-alias-bg-layer-1, #26262e); }',
128
+ '@media (max-width: 420px) { .iris-core-detail-grid { grid-template-columns: 1fr; } .iris-core-task-row { grid-template-columns: 8px minmax(0, 1fr) auto; padding: 8px; } }',
129
+ /* ---- Core 用户侧只读投影(任务区安全 DTO 行) ---- */
130
+ '.iris-core-user-row { cursor: default; }',
131
+ '.iris-core-user-head { min-width: 0; display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }',
132
+ '.iris-core-user-model { min-width: 0; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }',
133
+ '@media (max-width: 420px) { .iris-core-user-head { display: grid; grid-template-columns: auto minmax(0, 1fr); row-gap: 2px; justify-items: start; } .iris-core-user-head .iris-wb-muted, .iris-core-user-row .iris-core-id { grid-column: 1 / -1; } }',
105
134
  /* ---- 操作卡片组(阶段 5 GUI 直连) ---- */
106
135
  '.iris-act-group { display: grid; gap: 8px; }',
107
136
  '.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; }',
@@ -203,7 +232,31 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
203
232
  '.iris-gallery-file { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; font-weight: 600; }',
204
233
  '.iris-gallery-meta { color: var(--dsw-alias-label-tertiary, #7a7a85); font-size: 10px; }',
205
234
  '.iris-gallery-info { display: grid; gap: 4px; padding: 8px 9px 9px; }',
206
- '.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; } }',
207
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; }',
208
261
  '.iris-artifact-mini:hover { background: var(--dsw-alias-bg-layer-2, #32323c); color: var(--dsw-alias-label-primary, #e6e6e6); }',
209
262
  '.iris-artifact-mini span:nth-child(2) { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }',
@@ -219,6 +272,10 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
219
272
  '.iris-pm-ver.configured { color: #7da8fa; }',
220
273
  '.iris-pm-ver.verified { color: var(--dsw-alias-state-success-primary, #6fcf6f); }',
221
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); }',
222
279
  '.iris-pm-ver.unconfigured { opacity: .55; }',
223
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; }',
224
281
  /* 文件选择器(阶段 10) */
@@ -252,8 +309,14 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
252
309
  '.iris-po-route { padding: 7px 10px; border-radius: 10px; background: rgba(255,255,255,.045); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }',
253
310
  '.iris-po-note { padding: 8px 10px; border-radius: 10px; color: var(--dsw-alias-state-warn-primary, #dfbd69); background: rgba(217,169,65,.08); }',
254
311
  '.iris-po-field { display: grid; gap: 6px; font-size: 11px; color: var(--dsw-alias-label-secondary, #a6a6b2); }',
255
- '.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; }',
256
- '.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; }',
257
320
  '.iris-po-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }',
258
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; }',
259
322
  '.iris-po-btn { background: rgba(255,255,255,.045); color: var(--dsw-alias-label-secondary, #b0b0bb); }',
@@ -296,15 +359,30 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
296
359
  console.log('[iris] client apply start');
297
360
  adoptStyles();
298
361
 
299
- /* ---- 当前会话 id(文件选择器 L1 用;ctx.sessions 是标准插件能力) ---- */
362
+ /* ---- 当前主视图会话(旧 current / 0.2 mainView 持有者;不猜测列表首项) ---- */
300
363
  var currentSessionId = null;
301
364
  try {
302
- var sessions = ctx && ctx.sessions;
365
+ var sessions = ctx && (typeof ctx.get === 'function' ? ctx.get('sessions') : ctx.sessions);
303
366
  var listObs = sessions && sessions.list;
304
367
  if (listObs && typeof listObs.getSnapshot === 'function') {
305
- currentSessionId = (listObs.getSnapshot() || {}).current || null;
368
+ function readCurrentSession() {
369
+ var snap = listObs.getSnapshot() || {};
370
+ if (Object.prototype.hasOwnProperty.call(snap, 'current')) {
371
+ currentSessionId = typeof snap.current === 'string' && snap.current ? snap.current : null;
372
+ return;
373
+ }
374
+ var rows = Object.values(snap.byId || {}).filter(function (row) {
375
+ return row && typeof row.id === 'string' && row.id && row.retainedBy && row.retainedBy.mainView > 0;
376
+ });
377
+ currentSessionId = rows.length === 1 ? rows[0].id : null;
378
+ }
379
+ readCurrentSession();
306
380
  if (typeof listObs.subscribe === 'function') {
307
- listObs.subscribe(function (snap) { currentSessionId = (snap || {}).current || null; });
381
+ // ObservableSnapshot 的变更通知不承诺携带快照,始终回读权威值。
382
+ var unsubscribeSession = listObs.subscribe(readCurrentSession);
383
+ if (typeof unsubscribeSession === 'function' && typeof ctx.effect === 'function') {
384
+ ctx.effect(function () { return unsubscribeSession; });
385
+ }
308
386
  }
309
387
  }
310
388
  } catch (_) { /* 拿不到会话 → L1 退化为只列 iris 产物 */ }
@@ -338,7 +416,10 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
338
416
  fetch('/iris/api/state')
339
417
  .then(function (res) { return res.ok ? res.json() : null; })
340
418
  .then(function (data) {
341
- if (data) apply(data);
419
+ if (data) {
420
+ apply(data);
421
+ window.dispatchEvent(new CustomEvent('iris-core-refresh-tick'));
422
+ }
342
423
  })
343
424
  .catch(function () { /* 网络抖动静默 */ });
344
425
  }
@@ -372,7 +453,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
372
453
  unconfigured: { label: '未配置', symbol: '○' },
373
454
  configured: { label: '已配置,待验证', symbol: '◌' },
374
455
  verified: { label: '近期验证成功', symbol: '●' },
375
- failed: { label: '认证失败', symbol: '×' }
456
+ failed: { label: '调用受阻', symbol: '×' }
376
457
  };
377
458
 
378
459
  function capabilityHealth(state, capability) {
@@ -396,9 +477,22 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
396
477
  return Math.floor(ms / 86400000) + ' 天前';
397
478
  }
398
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
+
399
488
  function healthText(health) {
400
489
  var status = health && HEALTH_META[health.status] ? health.status : 'unconfigured';
401
- 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);
402
496
  var age = relativeHealthTime(health && health.observedAt);
403
497
  return text + (age ? ' · ' + age : '');
404
498
  }
@@ -415,9 +509,9 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
415
509
  return React.createElement('div', { className: 'iris-health-grid' }, rows.map(function (row) {
416
510
  var health = capabilityHealth(state, row[0]);
417
511
  var meta = HEALTH_META[health.status] || HEALTH_META.unconfigured;
418
- 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) },
419
513
  React.createElement('span', { className: 'iris-health-name' },
420
- 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),
421
515
  row[1]),
422
516
  React.createElement('span', { className: 'iris-health-state', title: healthText(health) }, healthText(health)),
423
517
  React.createElement('span', { className: 'iris-wb-muted' }, String(health.candidateCount || 0) + ' 路'));
@@ -425,7 +519,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
425
519
  }
426
520
 
427
521
  /* ---- 任务详情抽屉:按需拉取 /iris/api/task/:id ---- */
428
- function useTaskDetail(taskId) {
522
+ function useTaskDetail(taskId, revision) {
429
523
  var pair = React.useState(null);
430
524
  var setDetail = pair[1];
431
525
  React.useEffect(function () {
@@ -437,7 +531,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
437
531
  .then(function (data) { if (alive && data) setDetail(data); })
438
532
  .catch(function () { /* 静默 */ });
439
533
  return function () { alive = false; };
440
- }, [taskId]);
534
+ }, [taskId, revision]);
441
535
  return pair[0];
442
536
  }
443
537
 
@@ -513,7 +607,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
513
607
  React.createElement('span', { className: 'iris-wb-muted' }, task.createdAt ? new Date(task.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : ''));
514
608
  }
515
609
 
516
- function TaskRecoveryActions({ detail }) {
610
+ function TaskRecoveryActions({ detail, onDeleted }) {
517
611
  var pair = React.useState('');
518
612
  var note = pair[0];
519
613
  var setNote = pair[1];
@@ -530,13 +624,15 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
530
624
  var canRedeliver = detail.schemaVersion === 2 && !acknowledged && state === 'artifact_unavailable' && detail.remoteTaskId;
531
625
  var canRetry = detail.schemaVersion === 2 && !acknowledged && ['needs_attention', 'failed', 'canceled'].indexOf(state) >= 0
532
626
  && (detail.cap === 'image' || (detail.cap === 'video' && detail.mode === 't2v'));
533
- if (!canAcknowledge && !canRestore && !canReobserve && !canRedeliver && !canRetry) return null;
627
+ var canDelete = detail.status !== 'running' || (detail.schemaVersion === 2 && attentionState && acknowledged);
628
+ if (!canAcknowledge && !canRestore && !canReobserve && !canRedeliver && !canRetry && !canDelete) return null;
534
629
  function run(action, body, confirmMessage) {
535
630
  if (confirmMessage && !window.confirm(confirmMessage)) return;
536
631
  setBusy(true);
537
632
  setNote('处理中…');
538
633
  postAction(action, body).then(function (r) {
539
634
  setNote(r.ok ? ((r.d && r.d.text) || '操作已接受') : ('失败:' + ((r.d && r.d.error) || '未知错误')));
635
+ if (r.ok && action === 'tasks_delete' && typeof onDeleted === 'function') onDeleted();
540
636
  }).catch(function (error) { setNote('失败:' + String((error && error.message) || error)); })
541
637
  .finally(function () { setBusy(false); });
542
638
  }
@@ -551,12 +647,15 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
551
647
  onClick: function () { run('task_ack_attention', { task_id: detail.id }); } }, '标为已读') : null,
552
648
  canRestore ? React.createElement('button', { className: 'iris-pm-btn', disabled: busy,
553
649
  onClick: function () { run('task_restore_attention', { task_id: detail.id }); } }, '恢复提醒') : null,
650
+ canDelete ? React.createElement('button', { className: 'iris-pm-btn danger', disabled: busy,
651
+ onClick: function () { run('tasks_delete', { task_id: detail.id },
652
+ attentionState ? '该任务仍含未知远程事实;删除后无法再观察。确认只删除任务记录?' : '确认删除该任务记录?作品文件保留。'); } }, '删除记录') : null,
554
653
  note ? React.createElement('span', { className: 'iris-clean-note' }, note) : null);
555
654
  }
556
655
 
557
656
  /* ---- 任务详情抽屉:点击任务卡片行展开 ---- */
558
- function TaskDetailDrawer({ taskId }) {
559
- var detail = useTaskDetail(taskId);
657
+ function TaskDetailDrawer({ taskId, revision, onDeleted }) {
658
+ var detail = useTaskDetail(taskId, revision);
560
659
  if (!detail) {
561
660
  return React.createElement('div', { className: 'iris-wb-drawer iris-wb-loading' }, '加载任务详情…');
562
661
  }
@@ -565,7 +664,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
565
664
  el.push(React.createElement('div', { key: 'p', className: 'iris-wb-prompt' }, detail.prompt || '(空)'));
566
665
  if (Number(detail.schemaVersion) === 1) {
567
666
  el.push(React.createElement('div', { key: 'legacy', className: 'iris-clean-note' },
568
- '旧版任务记录 · 仅支持查看。由于缺少可靠的远端受理与交付事实,不提供重新观察、重新交付或知情重试;0.1.3 新任务将使用可信任务语义。'));
667
+ '旧版任务记录 · 仅支持查看恢复事实。由于缺少可靠的远端受理与交付事实,不提供重新观察、重新交付或知情重试;已终止记录仍可删除。'));
569
668
  }
570
669
  var dispositionLabel = attentionDispositionLabel(detail);
571
670
  if (dispositionLabel) {
@@ -622,7 +721,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
622
721
  return React.createElement('span', { key: a.attachmentId, className: 'iris-wb-kv' }, a.attachmentId + (a.file ? ' (' + a.file + ')' : ''));
623
722
  })));
624
723
  }
625
- el.push(React.createElement(TaskRecoveryActions, { key: 'recovery', detail: detail }));
724
+ el.push(React.createElement(TaskRecoveryActions, { key: 'recovery', detail: detail, onDeleted: onDeleted }));
626
725
  return React.createElement('div', { className: 'iris-wb-drawer' }, ...el);
627
726
  }
628
727
 
@@ -668,7 +767,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
668
767
  task.createdAt ? '发起 ' + fmtTime(task.createdAt) : '',
669
768
  task.finishedAt ? ' · 完成 ' + fmtTime(task.finishedAt) : ''));
670
769
  }
671
- if (selected) body.push(React.createElement(TaskDetailDrawer, { key: 'drawer', taskId: task.id }));
770
+ if (selected) body.push(React.createElement(TaskDetailDrawer, { key: 'drawer', taskId: task.id, revision: task.revision, onDeleted: function () { onSelect(null); } }));
672
771
  return React.createElement('div', {
673
772
  key: task.id,
674
773
  className: 'iris-wb-card' + (isRunning ? ' iris-running' : '') + (selected ? ' selected' : ''),
@@ -691,12 +790,16 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
691
790
  var newId = addPair[0];
692
791
  var setNewId = addPair[1];
693
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
+ }
694
797
  function verBadge(m, c) {
695
798
  var health = m.health && m.health[c];
696
799
  var status = health && HEALTH_META[health.status] ? health.status : 'configured';
697
- var cls = 'iris-pm-ver ' + status;
800
+ var cls = 'iris-pm-ver ' + status + (health && health.rateLimited ? ' ' + healthClass(health) : '');
698
801
  var title = healthText(health || { status: status });
699
- var symbol = status === 'verified' ? '✓' : (status === 'failed' ? '×' : (status === 'unconfigured' ? '○' : '?'));
802
+ var symbol = health && health.retryAt ? '◷' : status === 'verified' ? '✓' : (status === 'failed' ? '×' : (status === 'unconfigured' ? '○' : '?'));
700
803
  return React.createElement('button', {
701
804
  key: c,
702
805
  className: cls,
@@ -706,9 +809,21 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
706
809
  }
707
810
  var rows = models.map(function (m) {
708
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; });
709
813
  return React.createElement('div', { key: m.id, className: 'iris-pm-mrow' },
710
814
  React.createElement('span', { className: 'iris-pm-mname' }, m.id, m.source === 'manual' ? React.createElement('span', { className: 'iris-pm-mtag' }, '手动') : null),
711
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,
712
827
  React.createElement('button', { className: 'iris-pm-btn danger', title: '从池移除', onClick: function () { props.removeModel(p, m); } }, '✕'));
713
828
  });
714
829
  return React.createElement('div', { className: 'iris-pm-pool' },
@@ -719,6 +834,59 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
719
834
  React.createElement('button', { className: 'iris-pm-btn', onClick: function () { props.addModel(p, newId.trim()); setNewId(''); } }, '+ 添加')));
720
835
  }
721
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
+
870
+ function ProviderEndpointEditor(props) {
871
+ var p = props.provider;
872
+ var valuePair = React.useState(p.mediaBaseUrl || '');
873
+ var value = valuePair[0];
874
+ var setValue = valuePair[1];
875
+ React.useEffect(function () { setValue(p.mediaBaseUrl || ''); }, [p.mediaBaseUrl]);
876
+ function save() {
877
+ postAction('providers_upsert', { id: p.id, mediaBaseUrl: value.trim() }).then(function (r) {
878
+ props.onNote(r.ok ? '媒体 Base URL 已更新' : (r.d.error || '更新失败'));
879
+ if (r.ok) props.onDone();
880
+ });
881
+ }
882
+ return React.createElement('div', { className: 'iris-pm-field' },
883
+ React.createElement('label', {}, '媒体 Base URL(可选)'),
884
+ React.createElement('div', { className: 'iris-wb-row' },
885
+ React.createElement('input', { value: value, placeholder: '留空则沿用 Base URL', onChange: function (e) { setValue(e.target.value); } }),
886
+ React.createElement('button', { className: 'iris-pm-btn', onClick: save }, '保存')),
887
+ React.createElement('span', { className: 'iris-pm-note' }, '百炼 Workspace 可填 …maas.aliyuncs.com/compatible-mode/v1;视觉仍使用普通 Base URL。'));
888
+ }
889
+
722
890
  function ProviderManager() {
723
891
  var listPair = React.useState(null);
724
892
  var list = listPair[0];
@@ -728,14 +896,18 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
728
896
  var addingPair = React.useState(false);
729
897
  var adding = addingPair[0];
730
898
  var setAdding = addingPair[1];
731
- var newForm = React.useState({ name: '', baseUrl: '', apiKey: '', type: 'openai', mediaProtocol: 'auto' });
899
+ var newForm = React.useState({ name: '', baseUrl: '', mediaBaseUrl: '', apiKey: '', type: 'openai', mediaProtocol: 'auto' });
732
900
  var notePair = React.useState('');
733
901
  var note = notePair[0];
734
902
  var setNote = notePair[1];
735
903
  function refresh() {
736
904
  postAction('providers_list', {}).then(function (r) { if (r.ok && r.d.providers) setList(r.d.providers); });
737
905
  }
738
- React.useEffect(function () { refresh(); }, []);
906
+ React.useEffect(function () {
907
+ refresh();
908
+ var timer = setInterval(refresh, 5000);
909
+ return function () { clearInterval(timer); };
910
+ }, []);
739
911
  function addProvider() {
740
912
  var f = newForm[0];
741
913
  if (!f.baseUrl || !f.apiKey) { setNote('baseUrl 和 apiKey 必填'); return; }
@@ -789,9 +961,16 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
789
961
  if (open) {
790
962
  body = React.createElement('div', { className: 'iris-pm-body' },
791
963
  React.createElement('div', { className: 'iris-pm-note' }, 'Key ' + (p.apiKeyHint || '未配置') + ' · ' + (p.type || '') + ' · ' + (p.mediaProtocol || '')),
792
- React.createElement('div', { className: 'iris-pm-field' }, React.createElement('label', {}, '媒体协议'), React.createElement('select', { value: p.mediaProtocol || 'openai-images', onChange: function (e) { setProtocol(p, e.target.value); } },
964
+ p.protocolInferred ? React.createElement('div', { className: 'iris-pm-note' }, '协议为推断值,请确认') : null,
965
+ React.createElement(ProviderEndpointEditor, { provider: p, onNote: setNote, onDone: refresh }),
966
+ React.createElement(VisionInputEditor, { provider: p, onNote: setNote, onDone: refresh }),
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); } },
968
+ React.createElement('option', { value: 'auto' }, '自动'),
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,
793
970
  React.createElement('option', { value: 'dashscope' }, 'DashScope(仅阿里云官方 HTTPS)'),
794
- 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 生图'))),
795
974
  React.createElement('div', { className: 'iris-pm-note' }, '能力: ' + ((p.capabilities || []).join(' / ') || '无')),
796
975
  React.createElement(ModelPool, { provider: p, onNote: setNote, onDone: refresh, testModel: testModel, removeModel: removeModel, addModel: addModel }),
797
976
  React.createElement('div', { className: 'iris-wb-row' },
@@ -810,11 +989,14 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
810
989
  addBlock = React.createElement('div', { className: 'iris-pm-card' },
811
990
  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 }); } })),
812
991
  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 }); } })),
992
+ 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 }); } })),
813
993
  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 }); } })),
814
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 }); } },
815
- React.createElement('option', { value: 'auto' }, '自动(按 Base URL 安全判断)'),
995
+ React.createElement('option', { value: 'auto' }, '自动(按媒体 Base URL 安全判断)'),
816
996
  React.createElement('option', { value: 'dashscope' }, 'DashScope(阿里云官方)'),
817
- 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 生图'))),
818
1000
  React.createElement('div', { className: 'iris-wb-row' },
819
1001
  React.createElement('button', { className: 'iris-act-run', onClick: addProvider }, '保存'),
820
1002
  React.createElement('button', { className: 'iris-pm-btn', onClick: function () { setAdding(false); } }, '取消')));
@@ -936,7 +1118,10 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
936
1118
  body: JSON.stringify(vals)
937
1119
  })
938
1120
  .then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
939
- .then(function (res) { setRun(res.ok ? { ok: true, text: res.d.text || '', img: res.d.imageDataUrl || null } : { ok: false, text: res.d.error || '执行失败' }); })
1121
+ .then(function (res) {
1122
+ setRun(res.ok ? { ok: true, text: res.d.text || '', img: res.d.imageUrl || res.d.imageDataUrl || null } : { ok: false, text: res.d.error || '执行失败' });
1123
+ if (res.ok && res.d && res.d.storage === 'core') window.dispatchEvent(new CustomEvent('iris-core-artifacts-changed'));
1124
+ })
940
1125
  .catch(function () { setRun({ ok: false, text: '网络错误' }); });
941
1126
  }
942
1127
  var cardHealth = capability ? capabilityHealth(irisState, capability) : { status: 'verified' };
@@ -982,7 +1167,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
982
1167
  var shown = (vals.model && vals.model.trim()) || (modelInfo && modelInfo.assigned) || '';
983
1168
  modelBar = React.createElement('div', { className: 'iris-act-field' },
984
1169
  React.createElement('label', {},
985
- 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),
986
1171
  ' 模型:' + (shown || '自动') + (available ? '' : '(无可用模型)')),
987
1172
  React.createElement('div', { className: 'iris-wb-muted' },
988
1173
  healthText(cardHealth) + ' · 改分配 → 上方「能力分配(failover 顺序)」'));
@@ -1002,9 +1187,9 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1002
1187
  React.createElement('div', { className: 'iris-act-head', onClick: function () { setOpen(!open); } },
1003
1188
  React.createElement('span', { className: 'iris-act-title' }, title),
1004
1189
  capability ? React.createElement('span', {
1005
- className: 'iris-pm-dot ' + headStatus,
1190
+ className: 'iris-pm-dot ' + healthClass(cardHealth),
1006
1191
  title: headTitle
1007
- }, HEALTH_META[headStatus].symbol) : null,
1192
+ }, cardHealth.retryAt ? '◷' : HEALTH_META[headStatus].symbol) : null,
1008
1193
  React.createElement('span', { className: 'iris-act-arrow' }, '▶')),
1009
1194
  body);
1010
1195
  }
@@ -1165,14 +1350,16 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1165
1350
  /* ---- 历史浏览器(设置页:按今天/昨天/更早分组折叠) ---- */
1166
1351
  function HistoryBrowser(props) {
1167
1352
  var recent = props.recent || [];
1353
+ var coreHistory = props.coreHistory || [];
1168
1354
  var selected = props.selected;
1169
1355
  var onSelect = props.onSelect;
1170
1356
  var collapsedPair = React.useState({});
1171
1357
  var collapsed = collapsedPair[0];
1172
1358
  var setCollapsed = collapsedPair[1];
1173
- if (!recent.length) return React.createElement('div', { className: 'iris-wb-empty' }, '尚无任务记录');
1359
+ if (!recent.length && !coreHistory.length) return React.createElement('div', { className: 'iris-wb-empty' }, '尚无任务记录');
1174
1360
  var buckets = { 今天: [], 昨天: [], 更早: [] };
1175
- recent.forEach(function (t) { buckets[dayBucket(t.createdAt)].push(t); });
1361
+ recent.forEach(function (t) { buckets[dayBucket(t.createdAt)].push({ legacy: t }); });
1362
+ coreHistory.forEach(function (row) { buckets[dayBucket(row.updatedAt)].push({ core: row }); });
1176
1363
  var order = ['今天', '昨天', '更早'];
1177
1364
  return React.createElement('div', {}, order.map(function (name) {
1178
1365
  var rows = buckets[name];
@@ -1184,7 +1371,11 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1184
1371
  onClick: function () { var n = {}; Object.keys(collapsed).forEach(function (k) { n[k] = collapsed[k]; }); n[name] = !isCollapsed; setCollapsed(n); }
1185
1372
  }, React.createElement('span', {}, isCollapsed ? '▶' : '▼'), name + ' · ' + rows.length),
1186
1373
  isCollapsed ? null : React.createElement('div', { className: 'iris-wb-box' },
1187
- rows.map(function (t) { return taskRow(t, false, selected === t.id, onSelect); })));
1374
+ rows.map(function (entry) {
1375
+ return entry.core
1376
+ ? coreTaskCard(entry.core)
1377
+ : taskRow(entry.legacy, false, selected === entry.legacy.id, onSelect);
1378
+ })));
1188
1379
  }));
1189
1380
  }
1190
1381
 
@@ -1214,7 +1405,20 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1214
1405
  var mime = String(item.mime || '');
1215
1406
  if (mime.indexOf('image/') === 0) {
1216
1407
  return React.createElement('a', { className: 'iris-gallery-preview', href: item.url, target: '_blank', rel: 'noreferrer', title: '打开原图' },
1217
- React.createElement('img', { src: item.url, alt: 'Iris 作品 ' + item.file, loading: 'lazy' }));
1408
+ React.createElement('img', {
1409
+ src: item.url, alt: 'Iris 作品 ' + item.file, loading: 'lazy',
1410
+ // 媒体文件丢失(404/损坏)时局部降级为占位提示,作品区其余项不受影响
1411
+ onError: function (event) {
1412
+ var img = event && event.target;
1413
+ var box = img && img.parentNode;
1414
+ if (!box || box.querySelector('span')) return;
1415
+ img.style.display = 'none';
1416
+ var note = document.createElement('span');
1417
+ note.className = 'iris-gallery-meta';
1418
+ note.textContent = '作品文件暂时不可用';
1419
+ box.appendChild(note);
1420
+ }
1421
+ }));
1218
1422
  }
1219
1423
  if (mime.indexOf('video/') === 0) {
1220
1424
  return React.createElement('div', { className: 'iris-gallery-preview' },
@@ -1227,64 +1431,991 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1227
1431
  return React.createElement('a', { className: 'iris-gallery-preview iris-wb-link', href: item.url, target: '_blank', rel: 'noreferrer' }, '打开文件');
1228
1432
  }
1229
1433
 
1230
- function ArtifactGallery(props) {
1231
- var itemsPair = React.useState([]);
1232
- var items = itemsPair[0];
1233
- var setItems = itemsPair[1];
1234
- var totalPair = React.useState(Number(props.total || 0));
1235
- var total = totalPair[0];
1236
- var setTotal = totalPair[1];
1237
- var busyPair = React.useState(false);
1238
- var busy = busyPair[0];
1239
- var setBusy = busyPair[1];
1434
+ function coreGalleryItems(data, limit) {
1435
+ return (((data && data.artifacts && data.artifacts.recent) || []).filter(function (artifact) {
1436
+ return artifact.kind !== 'host-input';
1437
+ }).map(function (artifact) {
1438
+ return {
1439
+ id: artifact.id, file: artifact.id, mime: artifact.mediaType,
1440
+ size: artifact.size, createdAt: artifact.createdAt || '', source: 'core',
1441
+ kind: artifact.kind,
1442
+ digest: artifact.digest && artifact.digest.value ? artifact.digest.value.slice(0, 12) : '',
1443
+ url: '/iris/api/core/artifact/' + encodeURIComponent(artifact.id) + '/media'
1444
+ };
1445
+ })).slice(0, limit || 200);
1446
+ }
1447
+
1448
+ function useCoreWorks(limit) {
1449
+ var pair = React.useState([]);
1450
+ var setItems = pair[1];
1451
+ React.useEffect(function () {
1452
+ var alive = true;
1453
+ function load() {
1454
+ fetch('/iris/api/core/snapshot?limit=' + Math.max(1, Math.min(200, Number(limit) || 6)))
1455
+ .then(function (res) { return res.ok ? res.json() : null; })
1456
+ .then(function (data) { if (alive && data) setItems(coreGalleryItems(data, limit)); })
1457
+ .catch(function () { /* Core 不可用不影响旧作品 */ });
1458
+ }
1459
+ function changed() { load(); }
1460
+ load();
1461
+ window.addEventListener('iris-core-artifacts-changed', changed);
1462
+ window.addEventListener('iris-core-refresh-tick', changed);
1463
+ return function () {
1464
+ alive = false;
1465
+ window.removeEventListener('iris-core-artifacts-changed', changed);
1466
+ window.removeEventListener('iris-core-refresh-tick', changed);
1467
+ };
1468
+ }, [limit]);
1469
+ return pair[0];
1470
+ }
1471
+
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];
1240
1759
  var notePair = React.useState('');
1241
1760
  var note = notePair[0];
1242
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);
1243
1768
 
1244
- function load(append) {
1245
- if (busy) return;
1769
+ function load() {
1770
+ if (request.current) request.current.abort();
1771
+ var controller = new AbortController(); request.current = controller;
1246
1772
  setBusy(true);
1247
- var offset = append ? items.length : 0;
1248
- fetch('/iris/api/artifacts?offset=' + offset + '&limit=24')
1249
- .then(function (res) { return res.ok ? res.json() : Promise.reject(new Error('读取失败')); })
1250
- .then(function (data) {
1251
- setItems(append ? items.concat(data.items || []) : (data.items || []));
1252
- setTotal(Number(data.total || 0));
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 作品。' : '');
1253
1783
  })
1254
- .catch(function (error) { setNote('作品库读取失败:' + error.message); })
1255
- .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); } });
1256
1786
  }
1257
1787
 
1258
1788
  function run(action, body, confirmText) {
1259
1789
  if (confirmText && !window.confirm(confirmText)) return;
1260
- setBusy(true);
1790
+ setMutating(true);
1261
1791
  postAction(action, body || {}).then(function (result) {
1262
1792
  var succeeded = result.ok && result.d && result.d.ok !== false;
1263
1793
  setNote(succeeded ? (result.d.text || '完成') : ('失败:' + ((result.d && (result.d.error || result.d.text)) || '未知错误')));
1264
- if (succeeded) load(false);
1265
- else setBusy(false);
1266
- }).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); });
1267
1801
  }
1268
1802
 
1269
- React.useEffect(function () { load(false); }, [props.total]);
1803
+ React.useEffect(function () {
1804
+ function changed() { load(); }
1805
+ window.addEventListener('iris-core-artifacts-changed', changed);
1806
+ window.addEventListener('iris-core-refresh-tick', changed);
1807
+ load();
1808
+ return function () {
1809
+ window.removeEventListener('iris-core-artifacts-changed', changed);
1810
+ window.removeEventListener('iris-core-refresh-tick', changed);
1811
+ if (request.current) request.current.abort(); request.current = null;
1812
+ };
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;
1270
1864
 
1271
1865
  return React.createElement('div', {},
1272
1866
  React.createElement('div', { className: 'iris-gallery-tools' },
1273
- React.createElement('span', { className: 'iris-clean-note' }, '共 ' + total + ' 个作品 · 清任务历史不会删除'),
1274
- React.createElement('button', { className: 'iris-clean-btn', disabled: busy, onClick: function () { run('artifacts_reindex'); } }, '找回本地作品'),
1275
- total ? React.createElement('button', { className: 'iris-clean-btn danger', disabled: busy, onClick: function () { run('artifacts_clear', {}, '永久删除作品库中的全部媒体文件?任务历史会保留,但相关媒体将无法打开。此操作不可逆。'); } }, '清空作品库') : null),
1276
- items.length ? React.createElement('div', { className: 'iris-gallery-grid' }, items.map(function (item) {
1277
- return React.createElement('div', { key: item.id, className: 'iris-gallery-card' },
1867
+ React.createElement('span', { className: 'iris-clean-note' },
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) {
1895
+ var isCore = item.source === 'core';
1896
+ return React.createElement('div', { key: (isCore ? 'core-' : 'legacy-') + item.id, className: 'iris-gallery-card' + (selectedItem(item) ? ' selected' : '') },
1278
1897
  artifactPreview(item),
1279
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); } }), '选择'),
1280
1902
  React.createElement('div', { className: 'iris-gallery-file', title: item.file }, item.file),
1281
- React.createElement('div', { className: 'iris-gallery-meta' }, fmtTime(item.createdAt) + ' · ' + fmtBytes(item.size)),
1903
+ React.createElement('div', { className: 'iris-gallery-meta' },
1904
+ (isCore ? 'Core · ' + galleryKindLabel(item.kind) : '旧版') + ' · ' + fmtTime(item.createdAt) + ' · ' + fmtBytes(item.size)),
1282
1905
  React.createElement('div', { className: 'iris-gallery-actions' },
1283
1906
  React.createElement('a', { className: 'iris-wb-link', href: item.url, target: '_blank', rel: 'noreferrer' }, '打开'),
1284
- React.createElement('button', { className: 'iris-pm-btn danger', disabled: busy, onClick: function () { run('artifacts_delete', { artifact_id: item.id }, '永久删除作品“' + item.file + '”?任务历史会保留,但媒体将无法打开。'); } }, '删除'))));
1285
- })) : React.createElement('div', { className: 'iris-wb-empty' }, busy ? '正在读取作品…' : '尚无作品;如 outputs/ 中已有文件,可点“找回本地作品”。'),
1286
- items.length < total ? React.createElement('button', { className: 'iris-clean-btn', disabled: busy, onClick: function () { load(true); } }, busy ? '加载中…' : '加载更多') : null,
1287
- 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);
1924
+ }
1925
+
1926
+ function coreCapabilityLabel(value) {
1927
+ return ({ image: '图片生成', video: '视频生成', tts: '语音合成', transcribe: '音频转写' })[value] || String(value || '媒体任务');
1928
+ }
1929
+
1930
+ /* 高级诊断与用户任务区共用五类用户状态投影;与服务端 lib/core-user-projection.js 保持同一映射,
1931
+ 文案必须区分“远端可能还在运行”和“已经明确失败”。 */
1932
+ function coreTaskState(task) {
1933
+ if (task.outcome === 'succeeded' && task.deliveryState === 'ready') return { tone: 'ok', state: 'succeeded', label: '已完成,作品可用' };
1934
+ if (task.outcome === 'succeeded' && task.deliveryState === 'failed') return { tone: 'error', state: 'delivery_failed', label: '已生成,作品取回失败' };
1935
+ if (task.outcome === 'succeeded') return { tone: 'warn', state: 'running', label: '已生成,正在保存作品' };
1936
+ if (task.outcome === 'failed') return { tone: 'error', state: 'attention', label: '已失败' };
1937
+ if (task.outcome === 'canceled') return { tone: 'idle', state: 'attention', label: '已取消' };
1938
+ if (task.outcome === 'unknown') return { tone: 'warn', state: 'attention', label: '任务结果未知,需要检查' };
1939
+ if (task.acceptance === 'unknown') return { tone: 'warn', state: 'attention', label: '提交结果未知,请检查后处理' };
1940
+ if (task.deliveryState === 'failed') return { tone: 'error', state: 'attention', label: '结果状态异常,需要检查' };
1941
+ if (task.watchState === 'suspended' || task.watchState === 'exhausted') return { tone: 'warn', state: 'observation_paused', label: '观察已暂停,远端可能仍在运行' };
1942
+ return { tone: 'idle', state: 'running', label: '运行中' };
1943
+ }
1944
+
1945
+ function coreAttemptState(attempt) {
1946
+ if (attempt.resultKind === 'completed') return '已完成';
1947
+ if (attempt.acceptance === 'not_accepted') return '未受理';
1948
+ if (attempt.acceptance === 'unknown') return '是否受理未知';
1949
+ if (attempt.acceptance === 'accepted') return '已受理';
1950
+ return '正在提交';
1951
+ }
1952
+
1953
+ function coreModelLabel(value) {
1954
+ var model = String(value || '');
1955
+ var separator = model.indexOf('::');
1956
+ return separator >= 0 ? model.slice(separator + 2) : (model || '未记录');
1957
+ }
1958
+
1959
+ /* 诊断层允许更多事实:取消三态如实区分,不把"已请求/无法确认"显示成"已取消"。 */
1960
+ function coreCancelLabel(value) {
1961
+ if (value === 'remote_confirmed') return '供应商已明确确认取消';
1962
+ if (value === 'local_confirmed') return '本地已确认取消(远端未确认)';
1963
+ if (value === 'requested') return '取消请求进行中';
1964
+ if (value === 'unknown') return '已请求取消,远端结果未确认;可显式重新观察';
1965
+ return '无取消请求';
1966
+ }
1967
+
1968
+ /* ---- Core 用户侧只读投影:任务区消费 /iris/api/core/snapshot 的 userTasks 安全 DTO ---- */
1969
+ var EMPTY_CORE_USER = Object.freeze({ rows: [], available: false, degraded: false, droppedTasks: 0 });
1970
+
1971
+ function useCoreUserTasks() {
1972
+ var pair = React.useState(EMPTY_CORE_USER);
1973
+ var setData = pair[1];
1974
+ React.useEffect(function () {
1975
+ var alive = true;
1976
+ function load() {
1977
+ fetch('/iris/api/core/snapshot?limit=200')
1978
+ .then(function (res) { return res.ok ? res.json() : null; })
1979
+ .then(function (data) {
1980
+ if (!alive || !data || !Array.isArray(data.userTasks)) return;
1981
+ setData({
1982
+ rows: data.userTasks,
1983
+ available: !!data.available,
1984
+ degraded: !!data.degraded,
1985
+ droppedTasks: Number(data.droppedTasks || 0)
1986
+ });
1987
+ })
1988
+ .catch(function () { /* Core 不可用不阻塞 legacy 任务区 */ });
1989
+ }
1990
+ function changed() { load(); }
1991
+ load();
1992
+ window.addEventListener('iris-core-artifacts-changed', changed);
1993
+ window.addEventListener('iris-core-refresh-tick', changed);
1994
+ return function () {
1995
+ alive = false;
1996
+ window.removeEventListener('iris-core-artifacts-changed', changed);
1997
+ window.removeEventListener('iris-core-refresh-tick', changed);
1998
+ };
1999
+ }, []);
2000
+ return pair[0];
2001
+ }
2002
+
2003
+ /* 五类用户状态 → 既有 badge 色调;不新增第三套卡片样式,复用 iris-wb/iris-task-mini。 */
2004
+ function coreUserStateClass(state) {
2005
+ if (state === 'succeeded') return 'ok';
2006
+ if (state === 'delivery_failed' || state === 'attention') return 'err';
2007
+ if (state === 'observation_paused') return 'warn';
2008
+ return '';
2009
+ }
2010
+
2011
+ function coreTaskBadge(row) {
2012
+ var tone = row.historical === true ? '' : coreUserStateClass(row.userState);
2013
+ return React.createElement('span', { className: 'iris-wb-badge' + (tone ? ' ' + tone : '') }, row.label);
2014
+ }
2015
+
2016
+ /* 完成提示只出现一次:会话内 Map 记录 core:id@revision,刷新页面后从事实重新计算基线。 */
2017
+ var coreCompletionSeen = null; // null = 尚未建立基线(首屏把既有完成项记为已见)
2018
+ function coreRowKey(row) { return 'core:' + row.id + '@' + Number(row.revision || 0); }
2019
+ function noteCoreCompletions(rows) {
2020
+ var current = {};
2021
+ for (var i = 0; i < rows.length; i++) {
2022
+ if (rows[i].userState === 'succeeded') current[coreRowKey(rows[i])] = true;
2023
+ }
2024
+ if (coreCompletionSeen === null) { coreCompletionSeen = current; return []; }
2025
+ var added = [];
2026
+ for (var j = 0; j < rows.length; j++) {
2027
+ var row = rows[j];
2028
+ if (row.userState === 'succeeded' && !coreCompletionSeen[coreRowKey(row)]) added.push(row);
2029
+ }
2030
+ for (var key in current) coreCompletionSeen[key] = true;
2031
+ return added;
2032
+ }
2033
+
2034
+ /* D1/D2 人工动作按钮:投影门满足才出现;每次点击只调用一次显式单步 API,绝不
2035
+ 重新提交、也不间接重新生成;点击期间按钮禁用防重复,完成后复用
2036
+ iris-core-refresh-tick 让任务区/高级诊断重拉同一快照。服务端 Command 门仍
2037
+ 是最终裁决(binding/能力校验失败返回稳定错误,行内提示,不改任务事实)。 */
2038
+ function CoreTaskManualButton(props) {
2039
+ var busyPair = React.useState(false);
2040
+ var busy = busyPair[0];
2041
+ var setBusy = busyPair[1];
2042
+ var notePair = React.useState('');
2043
+ var note = notePair[0];
2044
+ var setNote = notePair[1];
2045
+ function act() {
2046
+ if (busy) return; // 防重复触发:请求未结束前后端各拦一层
2047
+ var body = props.buildBody ? props.buildBody() : {};
2048
+ if (body === null) return; // 用户取消输入(如重试时放弃重新输入 prompt)
2049
+ if (props.confirm && !window.confirm(props.confirm)) return; // 改变远端事实/计费的动作需要二次确认
2050
+ setBusy(true);
2051
+ setNote('');
2052
+ fetch('/iris/api/core/task/' + encodeURIComponent(props.id) + '/' + props.action, {
2053
+ method: 'POST',
2054
+ headers: { 'Content-Type': 'application/json' },
2055
+ body: JSON.stringify(body)
2056
+ }).then(function (res) {
2057
+ return res.json().then(function (data) { return { status: res.status, data: data }; })
2058
+ .catch(function () { return { status: res.status, data: null }; });
2059
+ }).then(function (result) {
2060
+ var detail = result.data && result.data.error;
2061
+ if (result.status !== 200) {
2062
+ setNote(props.failPrefix + '被拒绝:' + (detail && detail.message || '状态 ' + result.status));
2063
+ return;
2064
+ }
2065
+ var created = result.data && result.data.task;
2066
+ if (props.createdPrefix && created && created.id && created.id !== props.id) {
2067
+ setNote(props.createdPrefix + created.id);
2068
+ }
2069
+ window.dispatchEvent(new CustomEvent('iris-core-refresh-tick'));
2070
+ }).catch(function () {
2071
+ setNote(props.failPrefix + '请求失败;任务事实未改动');
2072
+ }).finally(function () { setBusy(false); });
2073
+ }
2074
+ return React.createElement('span', { className: 'iris-wb-row', style: { alignItems: 'center' } },
2075
+ React.createElement('button', {
2076
+ type: 'button', className: 'iris-pm-btn', disabled: busy,
2077
+ title: props.title,
2078
+ onClick: act
2079
+ }, busy ? props.busyLabel : props.idleLabel),
2080
+ note ? React.createElement('span', { className: 'iris-wb-err' }, note) : null);
2081
+ }
2082
+
2083
+ /* D1 reobserve:显式单步观察入口,投影标记 observable 才出现。 */
2084
+ function CoreReobserveButton(props) {
2085
+ if (!props.row || props.row.observable !== true) return null;
2086
+ return React.createElement(CoreTaskManualButton, {
2087
+ id: props.row.id, action: 'reobserve',
2088
+ idleLabel: '重新观察', busyLabel: '观察中…', failPrefix: '重新观察',
2089
+ title: '只向供应商查询一次该任务的最新状态,绝不重新提交'
2090
+ });
2091
+ }
2092
+
2093
+ /* D2 redeliver:delivery_failed 的唯一恢复入口;重新下载已生成的作品,不重新生成。 */
2094
+ function CoreRedeliverButton(props) {
2095
+ if (!props.row || props.row.redeliverable !== true) return null;
2096
+ return React.createElement(CoreTaskManualButton, {
2097
+ id: props.row.id, action: 'redeliver',
2098
+ idleLabel: '重新取回作品', busyLabel: '取回中…', failPrefix: '重新取回作品',
2099
+ title: '重新下载已生成的作品,不会再次生成,也不会增加生成费用'
2100
+ });
2101
+ }
2102
+
2103
+ /* D3 cancel:改变远端事实,需二次确认。只有供应商明确确认才会显示「已取消」;
2104
+ 不支持或无法确认时保持真实状态(观察暂停/结果未知),绝不伪造已取消。 */
2105
+ function CoreCancelButton(props) {
2106
+ if (!props.row || props.row.cancelable !== true) return null;
2107
+ return React.createElement(CoreTaskManualButton, {
2108
+ id: props.row.id, action: 'cancel',
2109
+ idleLabel: '取消任务', busyLabel: '取消中…', failPrefix: '取消任务',
2110
+ title: '请求远端取消该任务;只有供应商明确确认才会记为已取消',
2111
+ confirm: '请求远端取消该任务?\n只有供应商明确确认后才会记为「已取消」;不支持或无法确认时保持真实状态,绝不伪造已取消。'
2112
+ });
2113
+ }
2114
+
2115
+ /* D4 retry as new task:产生新的真实计费。Core 记录不持久化生成指令,所以必须
2116
+ 重新输入 prompt(绝不从旧任务"恢复");二次确认文案明确重复计费。 */
2117
+ function CoreRetryButton(props) {
2118
+ if (!props.row || props.row.retryable !== true) return null;
2119
+ return React.createElement(CoreTaskManualButton, {
2120
+ id: props.row.id, action: 'retry',
2121
+ idleLabel: '重试为新任务', busyLabel: '创建中…', failPrefix: '重试',
2122
+ title: '创建一个新任务重新生成,可能产生重复费用;生成指令需要重新输入',
2123
+ confirm: '将创建一个新任务并可能产生重复生成费用,确定继续?',
2124
+ createdPrefix: '已创建新任务:',
2125
+ buildBody: function () {
2126
+ var isTts = props.row.capability === 'tts';
2127
+ var isTranscribe = props.row.capability === 'transcribe';
2128
+ var draft = window.prompt(isTts
2129
+ ? 'Core 记录不保存合成文本。请重新输入新任务要合成的文本:'
2130
+ : isTranscribe
2131
+ ? 'Core 记录不保存音频地址。请重新输入新任务要转写音频的公网或 oss:// 地址:'
2132
+ : 'Core 记录不保存生成指令。请重新输入新任务使用的生成指令:');
2133
+ if (draft === null) return null;
2134
+ draft = String(draft).trim();
2135
+ if (!draft) return null;
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 }
2142
+ : isTranscribe ? { audio_url: draft, confirmBilling: true }
2143
+ : { prompt: draft, confirmBilling: true, ...(sourceArtifactId ? { source_artifact_id: String(sourceArtifactId).trim() } : {}) };
2144
+ }
2145
+ });
2146
+ }
2147
+
2148
+ /* 注意力处置(Host 偏好,零 Core 写入):不再提醒/恢复提醒/移除/恢复显示。
2149
+ 文案明确"仅在本机隐藏提醒,任务与产物记录保留",不使用"消除记录"类措辞。 */
2150
+ function CoreDispositionButtons(props) {
2151
+ var row = props.row;
2152
+ if (!row || row.suppressed === true || row.historical === true) return null;
2153
+ if (row.disposition === 'hidden') {
2154
+ return React.createElement(CoreTaskManualButton, {
2155
+ id: row.id, action: 'unhide',
2156
+ idleLabel: '恢复显示', busyLabel: '恢复中…', failPrefix: '恢复显示',
2157
+ title: '在本机恢复显示该任务;Core 记录一直在,只是被隐藏'
2158
+ });
2159
+ }
2160
+ var attentionish = row.userState === 'attention' || row.userState === 'delivery_failed';
2161
+ var children = [];
2162
+ if (attentionish && row.disposition !== 'acknowledged') {
2163
+ children.push(React.createElement(CoreTaskManualButton, {
2164
+ key: 'ack', id: row.id, action: 'acknowledge',
2165
+ idleLabel: '不再提醒', busyLabel: '处理中…', failPrefix: '不再提醒',
2166
+ title: '仅在本机不再提醒该任务;任务与产物记录保留在 Core'
2167
+ }));
2168
+ children.push(React.createElement(CoreTaskManualButton, {
2169
+ key: 'hide', id: row.id, action: 'hide',
2170
+ idleLabel: '移除', busyLabel: '移除中…', failPrefix: '移除',
2171
+ title: '仅在本机隐藏提醒,任务与产物记录保留在 Core',
2172
+ confirm: '仅在本机隐藏此任务的提醒;任务与产物记录保留在 Core,可随时在高级诊断恢复显示。确定移除?'
2173
+ }));
2174
+ }
2175
+ if (attentionish && row.disposition === 'acknowledged') {
2176
+ children.push(React.createElement(CoreTaskManualButton, {
2177
+ key: 'restore', id: row.id, action: 'restore',
2178
+ idleLabel: '恢复提醒', busyLabel: '恢复中…', failPrefix: '恢复提醒',
2179
+ title: '取消"不再提醒",该任务重新出现在需要处理分区'
2180
+ }));
2181
+ }
2182
+ return children.length ? React.createElement('span', {}, children) : null;
2183
+ }
2184
+
2185
+ /* 只读卡片:显示稳定 Task ID、模型、更新时间和关联作品;不渲染供应商身份、错误原文或路径,无展开/删除入口。
2186
+ 「重新观察」/「重新取回作品」/「取消任务」/「重试为新任务」是仅有的动作(子组件,卡片本体仍不 fetch),
2187
+ 且仅当投影标记对应门时出现。 */
2188
+ function coreTaskCard(row) {
2189
+ var artifactIds = Array.isArray(row.artifactIds) ? row.artifactIds : [];
2190
+ var links = row.mediaReady && artifactIds.length
2191
+ ? React.createElement('div', { className: 'iris-wb-row' }, artifactIds.map(function (id, index) {
2192
+ return React.createElement('a', {
2193
+ key: id, className: 'iris-wb-link', target: '_blank', rel: 'noreferrer',
2194
+ href: '/iris/api/core/artifact/' + encodeURIComponent(id) + '/media'
2195
+ }, '▶ 作品 ' + (index + 1));
2196
+ }))
2197
+ : null;
2198
+ return React.createElement('div', {
2199
+ key: 'core:' + row.id,
2200
+ className: 'iris-wb-card iris-core-user-row',
2201
+ title: '只读投影:更多事实见下方「高级诊断 · Core 任务事实」'
2202
+ },
2203
+ React.createElement('div', { className: 'iris-core-user-head' },
2204
+ React.createElement('span', {}, capIcon(row.capability)),
2205
+ coreTaskBadge(row),
2206
+ React.createElement('span', { className: 'iris-wb-kv iris-core-user-model' }, row.model || '未记录'),
2207
+ React.createElement('span', { className: 'iris-wb-muted' }, '更新 ' + fmtTime(row.updatedAt))),
2208
+ React.createElement('div', { className: 'iris-wb-row' },
2209
+ React.createElement('span', { className: 'iris-core-id' }, row.id)),
2210
+ React.createElement(CoreReobserveButton, { row: row }),
2211
+ React.createElement(CoreRedeliverButton, { row: row }),
2212
+ React.createElement(CoreCancelButton, { row: row }),
2213
+ React.createElement(CoreRetryButton, { row: row }),
2214
+ React.createElement(CoreDispositionButtons, { row: row }),
2215
+ row.userState === 'succeeded' && !row.mediaReady
2216
+ ? React.createElement('div', { className: 'iris-wb-row' },
2217
+ React.createElement('span', { className: 'iris-wb-badge warn', title: '关联作品记录不在当前快照中,请稍后刷新' }, '作品文件暂时不可用'))
2218
+ : null,
2219
+ links);
2220
+ }
2221
+
2222
+ function coreTaskMini(row) {
2223
+ return React.createElement('div', {
2224
+ key: 'core:' + row.id, className: 'iris-task-mini',
2225
+ title: row.label + ' · ' + row.id
2226
+ },
2227
+ React.createElement('span', {}, capIcon(row.capability)),
2228
+ coreTaskBadge(row),
2229
+ React.createElement('span', { className: 'iris-task-mini-prompt' }, row.model || row.id),
2230
+ React.createElement('span', { className: 'iris-wb-muted' }, fmtTime(row.updatedAt)));
2231
+ }
2232
+
2233
+ /* hidden 与 suppressed(重试成功自动静默)从用户任务区整体消失;canceled
2234
+ 用 historical 进入历史且不计 attention;acknowledged 异常行也进入历史但可恢复。 */
2235
+ function splitCoreUserRows(rows) {
2236
+ var live = [], attention = [], done = [], acknowledged = [];
2237
+ for (var i = 0; i < rows.length; i++) {
2238
+ var row = rows[i];
2239
+ if (!row || row.suppressed === true || row.disposition === 'hidden') continue;
2240
+ if (row.historical === true) done.push(row);
2241
+ else if (row.userState === 'running' || row.userState === 'observation_paused') live.push(row);
2242
+ else if (row.userState === 'attention' || row.userState === 'delivery_failed') {
2243
+ if (row.disposition === 'acknowledged') acknowledged.push(row);
2244
+ else attention.push(row);
2245
+ }
2246
+ else if (row.userState === 'succeeded') done.push(row);
2247
+ }
2248
+ return { live: live, attention: attention, done: done, acknowledged: acknowledged };
2249
+ }
2250
+
2251
+ function CoreRuntimePanel() {
2252
+ var dataPair = React.useState(null);
2253
+ var data = dataPair[0];
2254
+ var setData = dataPair[1];
2255
+ var busyPair = React.useState(false);
2256
+ var busy = busyPair[0];
2257
+ var setBusy = busyPair[1];
2258
+ var errorPair = React.useState('');
2259
+ var error = errorPair[0];
2260
+ var setError = errorPair[1];
2261
+ var openPair = React.useState(false);
2262
+ var open = openPair[0];
2263
+ var setOpen = openPair[1];
2264
+ var selectedPair = React.useState('');
2265
+ var selectedCoreTask = selectedPair[0];
2266
+ var setSelectedCoreTask = selectedPair[1];
2267
+ var copyPair = React.useState('');
2268
+ var copyNote = copyPair[0];
2269
+ var setCopyNote = copyPair[1];
2270
+ function load() {
2271
+ if (busy) return;
2272
+ setBusy(true);
2273
+ setError('');
2274
+ fetch('/iris/api/core/snapshot?limit=12')
2275
+ .then(function (res) {
2276
+ if (res.status === 404) return { available: false, backendReloadRequired: true, tasks: { total: 0, recent: [] }, artifacts: { total: 0, recent: [] } };
2277
+ return res.ok ? res.json() : Promise.reject(new Error('读取失败(HTTP ' + res.status + ')'));
2278
+ }).then(setData)
2279
+ .catch(function (caught) { setError('Core 运行事实读取失败:' + caught.message); })
2280
+ .finally(function () { setBusy(false); });
2281
+ }
2282
+ function copyCoreId(value, label) {
2283
+ setCopyNote('');
2284
+ var clipboard = window.navigator && window.navigator.clipboard;
2285
+ if (!clipboard || typeof clipboard.writeText !== 'function') {
2286
+ setCopyNote('浏览器不允许自动复制;请长按上方 ID 复制。');
2287
+ return;
2288
+ }
2289
+ clipboard.writeText(value)
2290
+ .then(function () { setCopyNote(label + ' 已复制'); })
2291
+ .catch(function () { setCopyNote('复制失败;请长按上方 ID 复制。'); });
2292
+ }
2293
+ React.useEffect(function () {
2294
+ function changed() { load(); }
2295
+ load();
2296
+ window.addEventListener('iris-core-artifacts-changed', changed);
2297
+ window.addEventListener('iris-core-refresh-tick', changed);
2298
+ return function () {
2299
+ window.removeEventListener('iris-core-artifacts-changed', changed);
2300
+ window.removeEventListener('iris-core-refresh-tick', changed);
2301
+ };
2302
+ }, []);
2303
+ var taskData = data && data.tasks || { total: 0, recent: [] };
2304
+ var artifactData = data && data.artifacts || { total: 0, recent: [] };
2305
+ /* 处置偏好投影:诊断层可见 hidden/acknowledged/suppressed,并可恢复显示。 */
2306
+ var dispositionMap = {};
2307
+ ((data && data.userTasks) || []).forEach(function (row) { dispositionMap[row.id] = row; });
2308
+ function dispositionLabelOf(row) {
2309
+ if (!row) return '';
2310
+ if (row.suppressed) return '已由新任务成功交付自动静默';
2311
+ if (row.disposition === 'hidden') return '已在本机隐藏(记录保留,可恢复显示)';
2312
+ if (row.disposition === 'acknowledged') return '已设「不再提醒」(本机偏好,记录保留)';
2313
+ return '';
2314
+ }
2315
+ var selectedTask = (taskData.recent || []).find(function (task) { return task.id === selectedCoreTask; });
2316
+ var taskRows = (taskData.recent || []).slice(0, 6).map(function (task) {
2317
+ var state = coreTaskState(task);
2318
+ var disposed = dispositionMap[task.id];
2319
+ var hiddenMark = disposed && disposed.disposition === 'hidden' ? ' · 已隐藏' : '';
2320
+ var selected = selectedCoreTask === task.id;
2321
+ return React.createElement('button', {
2322
+ key: task.id,
2323
+ type: 'button',
2324
+ className: 'iris-core-task-row' + (selected ? ' selected' : ''),
2325
+ 'aria-expanded': selected,
2326
+ onClick: function () { setSelectedCoreTask(selected ? '' : task.id); }
2327
+ },
2328
+ React.createElement('span', { className: 'iris-core-state-dot ' + state.tone }),
2329
+ React.createElement('span', { className: 'iris-core-task-main' },
2330
+ React.createElement('strong', {}, coreCapabilityLabel(task.capability) + ' · ' + state.label + hiddenMark),
2331
+ React.createElement('span', { className: 'iris-wb-muted' }, coreModelLabel(task.modelRef) + ' · ' + fmtTime(task.updatedAt))),
2332
+ React.createElement('span', { className: 'iris-core-task-arrow' }, selected ? '▼' : '›'));
2333
+ });
2334
+ var selectedArtifacts = selectedTask ? (selectedTask.artifactIds || []).map(function (id) {
2335
+ return (artifactData.recent || []).find(function (artifact) { return artifact.id === id; }) || { id: id };
2336
+ }) : [];
2337
+ var selectedDisposition = selectedTask ? dispositionMap[selectedTask.id] : null;
2338
+ var dispositionNote = dispositionLabelOf(selectedDisposition);
2339
+ var detail = selectedTask ? React.createElement('div', { className: 'iris-core-detail' },
2340
+ React.createElement('div', { className: 'iris-doctor-top' },
2341
+ React.createElement('strong', { className: 'iris-doctor-score' }, '任务事实'),
2342
+ React.createElement('button', { className: 'iris-pm-btn', type: 'button', onClick: function () { copyCoreId(selectedTask.id, 'Task ID'); } }, '复制 Task ID')),
2343
+ React.createElement('div', { className: 'iris-core-id' }, selectedTask.id),
2344
+ React.createElement('div', { className: 'iris-core-detail-grid' },
2345
+ React.createElement('div', { className: 'iris-core-detail-cell' }, React.createElement('span', { className: 'iris-wb-muted' }, '模型'), React.createElement('span', {}, coreModelLabel(selectedTask.modelRef))),
2346
+ React.createElement('div', { className: 'iris-core-detail-cell' }, React.createElement('span', { className: 'iris-wb-muted' }, 'Provider'), React.createElement('span', { className: 'iris-core-id' }, selectedTask.providerId || '未记录')),
2347
+ React.createElement('div', { className: 'iris-core-detail-cell' }, React.createElement('span', { className: 'iris-wb-muted' }, '取消状态'), React.createElement('span', {}, coreCancelLabel(selectedTask.cancelState))),
2348
+ dispositionNote ? React.createElement('div', { className: 'iris-core-detail-cell' }, React.createElement('span', { className: 'iris-wb-muted' }, '处置状态'), React.createElement('span', {}, dispositionNote)) : null,
2349
+ React.createElement('div', { className: 'iris-core-detail-cell' }, React.createElement('span', { className: 'iris-wb-muted' }, '重试来源'), React.createElement('span', { className: 'iris-core-id' }, selectedTask.retriedFrom || '无')),
2350
+ React.createElement('div', { className: 'iris-core-detail-cell' }, React.createElement('span', { className: 'iris-wb-muted' }, '创建时间'), React.createElement('span', {}, fmtTime(selectedTask.createdAt))),
2351
+ React.createElement('div', { className: 'iris-core-detail-cell' }, React.createElement('span', { className: 'iris-wb-muted' }, '最后更新'), React.createElement('span', {}, fmtTime(selectedTask.updatedAt)))),
2352
+ React.createElement('div', {},
2353
+ React.createElement('div', { className: 'iris-wb-muted', style: { marginBottom: '4px' } }, '尝试记录 · ' + (selectedTask.attempts || []).length),
2354
+ React.createElement('div', { className: 'iris-core-task-list' }, (selectedTask.attempts || []).map(function (attempt) {
2355
+ return React.createElement('div', { key: attempt.id, className: 'iris-core-attempt' },
2356
+ React.createElement('span', {}, '第 ' + attempt.ordinal + ' 次 · ' + coreAttemptState(attempt) + ' · ' + coreModelLabel(attempt.model)),
2357
+ React.createElement('span', { className: 'iris-wb-muted' }, fmtTime(attempt.finishedAt || attempt.startedAt)),
2358
+ attempt.error && attempt.error.safeMessage ? React.createElement('span', { className: 'iris-wb-err' }, attempt.error.safeMessage) : null);
2359
+ }))),
2360
+ React.createElement('div', {},
2361
+ React.createElement('div', { className: 'iris-wb-muted', style: { marginBottom: '4px' } }, '关联作品 · ' + selectedArtifacts.length),
2362
+ selectedArtifacts.length ? React.createElement('div', { className: 'iris-core-task-list' }, selectedArtifacts.map(function (artifact) {
2363
+ var canOpen = selectedTask.capability === 'image' && (!artifact.mediaType || String(artifact.mediaType).indexOf('image/') === 0);
2364
+ return React.createElement('div', { key: artifact.id, className: 'iris-core-artifact' },
2365
+ React.createElement('span', { className: 'iris-core-id', style: { flex: 1 } }, artifact.id),
2366
+ artifact.size ? React.createElement('span', { className: 'iris-wb-muted' }, fmtBytes(artifact.size)) : null,
2367
+ canOpen ? React.createElement('a', { className: 'iris-wb-link', href: '/iris/api/core/artifact/' + encodeURIComponent(artifact.id) + '/media', target: '_blank', rel: 'noreferrer' }, '打开作品') : null,
2368
+ React.createElement('button', { className: 'iris-pm-btn', type: 'button', onClick: function () { copyCoreId(artifact.id, 'Artifact ID'); } }, '复制 ID'));
2369
+ })) : React.createElement('div', { className: 'iris-wb-empty' }, '当前任务没有可用作品')),
2370
+ selectedTask.lastError && selectedTask.lastError.safeMessage ? React.createElement('div', { className: 'iris-wb-err' }, selectedTask.lastError.safeMessage) : null,
2371
+ React.createElement(CoreReobserveButton, {
2372
+ row: selectedTask && selectedTask.acceptance === 'accepted' && selectedTask.remoteTaskId
2373
+ && ['none', 'unknown'].indexOf(selectedTask.outcome) >= 0 && selectedTask.phase !== 'terminal'
2374
+ ? { id: selectedTask.id, observable: true } : null
2375
+ }),
2376
+ React.createElement(CoreRedeliverButton, {
2377
+ row: selectedTask && selectedTask.outcome === 'succeeded'
2378
+ && selectedTask.deliveryState === 'failed' && selectedTask.remoteTaskId
2379
+ ? { id: selectedTask.id, redeliverable: true } : null
2380
+ }),
2381
+ React.createElement(CoreCancelButton, {
2382
+ row: selectedTask && selectedTask.acceptance === 'accepted' && selectedTask.remoteTaskId
2383
+ && ['none', 'unknown'].indexOf(selectedTask.outcome) >= 0 && selectedTask.phase !== 'terminal'
2384
+ && (!selectedTask.cancelState || selectedTask.cancelState === 'none')
2385
+ ? { id: selectedTask.id, cancelable: true } : null
2386
+ }),
2387
+ React.createElement(CoreRetryButton, {
2388
+ row: selectedTask && selectedTask.phase === 'terminal'
2389
+ && !(selectedTask.outcome === 'succeeded' && selectedTask.deliveryState === 'ready')
2390
+ ? { id: selectedTask.id, capability: selectedTask.capability, sourceArtifactId: selectedTask.sourceArtifactId, retryable: true } : null
2391
+ }),
2392
+ React.createElement(CoreDispositionButtons, {
2393
+ row: selectedDisposition
2394
+ ? { id: selectedDisposition.id, userState: selectedDisposition.userState,
2395
+ disposition: selectedDisposition.disposition, suppressed: selectedDisposition.suppressed }
2396
+ : null
2397
+ }),
2398
+ React.createElement('div', { className: 'iris-clean-note' }, '事实面板:「重新观察」只查询远端最新状态、「重新取回作品」只重新下载已生成的产物、「取消任务」只有供应商明确确认才记为已取消、「重试为新任务」会创建新任务并可能产生重复费用;这里不会重新提交、删除或修改既有任务。')) : null;
2399
+ var countText = data && data.available
2400
+ ? taskData.total + ' 个任务 · ' + artifactData.total + ' 个作品'
2401
+ : (busy ? '正在读取…' : '查看底层任务与作品关系');
2402
+ return React.createElement('div', { className: 'iris-wb-card iris-core-runtime' },
2403
+ React.createElement('div', { className: 'iris-core-runtime-head' },
2404
+ React.createElement('div', { className: 'iris-core-runtime-title' },
2405
+ React.createElement('strong', {}, 'Core 任务事实'),
2406
+ React.createElement('span', { className: 'iris-wb-muted' }, countText)),
2407
+ React.createElement('button', { className: 'iris-pm-btn', type: 'button', 'aria-expanded': open, onClick: function () { setOpen(!open); } }, open ? '收起' : '展开')),
2408
+ open ? React.createElement('div', { className: 'iris-core-runtime-body' },
2409
+ React.createElement('div', { className: 'iris-core-runtime-tools' },
2410
+ React.createElement('span', { className: 'iris-wb-muted', style: { flex: 1 } }, '只读开发诊断 · 点击任务查看尝试与作品'),
2411
+ React.createElement('button', { className: 'iris-pm-btn', type: 'button', disabled: busy, onClick: load }, busy ? '读取中…' : '刷新')),
2412
+ error ? React.createElement('div', { className: 'iris-doctor-row error' }, React.createElement('span', {}, '×'), React.createElement('span', {}, error)) : null,
2413
+ data && !data.available ? React.createElement('div', { className: 'iris-wb-empty' },
2414
+ data.backendReloadRequired ? 'DSH 后端尚未加载 Core 路由;请重启 DSH 后重试。' : '尚无 Core 运行事实;读取不会创建目录。') : null,
2415
+ data && data.available ? (taskRows.length ? React.createElement('div', { className: 'iris-core-task-list' }, taskRows)
2416
+ : React.createElement('div', { className: 'iris-wb-empty' }, '当前没有 Core Task')) : null,
2417
+ detail,
2418
+ copyNote ? React.createElement('div', { className: 'iris-clean-note' }, copyNote) : null) : null);
1288
2419
  }
1289
2420
 
1290
2421
  /* ---- 泡泡常用卡片选择器(设置页:多项勾选 → 出现在悬浮窗「常用」标签) ---- */
@@ -1428,6 +2559,9 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1428
2559
  var recent = (state && state.tasks && state.tasks.recent) || [];
1429
2560
  var attention = (state && state.tasks && state.tasks.attention) || [];
1430
2561
  var artifactTotal = (state && state.artifacts && state.artifacts.total) || 0;
2562
+ // 同一任务区合并 legacy 与 Core 只读投影;Core 行仅有的动作是「重新观察」「重新取回作品」与需确认的「取消任务」。
2563
+ var coreUser = useCoreUserTasks();
2564
+ var coreGroups = splitCoreUserRows(coreUser.rows || []);
1431
2565
  var selPair = React.useState(null);
1432
2566
  var selectedTask = selPair[0];
1433
2567
  var setSelectedTask = selPair[1];
@@ -1440,7 +2574,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1440
2574
  React.createElement(ProviderManager, {}),
1441
2575
  React.createElement('div', { className: 'iris-wb-sec' }, '能力健康'),
1442
2576
  React.createElement(CapabilityHealthOverview, {}),
1443
- React.createElement('div', { className: 'iris-wb-muted' }, '绿色有效 7 天;真实成功或显式实测会刷新。不会后台探测,也不会因 429、网络或供应商 5xx 变红。'),
2577
+ React.createElement('div', { className: 'iris-wb-muted' }, '绿色有效 7 天;真实成功或显式实测会刷新。429 冷却显示琥珀色,额度或预算耗尽显示红色;点击模型能力可实测恢复。不会后台探测。'),
1444
2578
  React.createElement('div', { className: 'iris-wb-sec' }, '能力分配(failover 顺序)'),
1445
2579
  React.createElement(CapabilityAssigner, {}),
1446
2580
  React.createElement('div', { className: 'iris-wb-sec' }, 'Iris 泡泡快捷卡片(勾选后显示在「⚡ 常用」标签)'),
@@ -1451,17 +2585,25 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1451
2585
  React.createElement(HostDoctorPanel, {}),
1452
2586
  React.createElement('div', { className: 'iris-wb-sec' }, '运行中任务'),
1453
2587
  React.createElement('div', { className: 'iris-wb-box' },
1454
- running.length ? running.map(function (t) { return taskRow(t, true, selectedTask === t.id, onSelect); })
2588
+ running.length || coreGroups.live.length ? [].concat(
2589
+ running.map(function (t) { return taskRow(t, true, selectedTask === t.id, onSelect); }),
2590
+ coreGroups.live.map(coreTaskCard))
1455
2591
  : React.createElement('div', { className: 'iris-wb-empty' }, '暂无进行中任务')),
1456
- attention.length ? React.createElement('div', {},
2592
+ attention.length || coreGroups.attention.length || coreUser.degraded ? React.createElement('div', {},
1457
2593
  React.createElement('div', { className: 'iris-wb-sec' }, '需要处理'),
1458
2594
  React.createElement('div', { className: 'iris-wb-box' },
1459
- attention.map(function (t) { return taskRow(t, false, selectedTask === t.id, onSelect); }))) : null,
2595
+ coreUser.degraded ? React.createElement('div', { className: 'iris-clean-note' },
2596
+ '部分 Core 记录损坏或媒体缺失,已跳过 ' + coreUser.droppedTasks + ' 条;其余事实照常显示。') : null,
2597
+ [].concat(
2598
+ attention.map(function (t) { return taskRow(t, false, selectedTask === t.id, onSelect); }),
2599
+ coreGroups.attention.map(coreTaskCard)))) : null,
1460
2600
  React.createElement('div', { className: 'iris-wb-sec' }, '作品库'),
1461
2601
  React.createElement(ArtifactGallery, { total: artifactTotal }),
2602
+ React.createElement('div', { className: 'iris-wb-sec' }, '高级诊断'),
2603
+ React.createElement(CoreRuntimePanel, {}),
1462
2604
  React.createElement('div', { className: 'iris-wb-sec' }, '历史任务'),
1463
2605
  React.createElement(CleanupBar, {}),
1464
- React.createElement(HistoryBrowser, { recent: recent, selected: selectedTask, onSelect: onSelect }),
2606
+ React.createElement(HistoryBrowser, { recent: recent, coreHistory: [].concat(coreGroups.done, coreGroups.acknowledged), selected: selectedTask, onSelect: onSelect }),
1465
2607
  React.createElement('div', { className: 'iris-wb-sec' }, '操作'),
1466
2608
  React.createElement(ActionGroups, {}));
1467
2609
  }
@@ -1473,8 +2615,23 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1473
2615
  var recent = (state && state.tasks && state.tasks.recent) || [];
1474
2616
  var attention = (state && state.tasks && state.tasks.attention) || [];
1475
2617
  var recentTotal = (state && state.tasks && state.tasks.recentTotal) || recent.length;
1476
- var works = (state && state.artifacts && state.artifacts.recent) || [];
1477
- var artifactTotal = (state && state.artifacts && state.artifacts.total) || works.length;
2618
+ var legacyWorks = (state && state.artifacts && state.artifacts.recent) || [];
2619
+ var coreWorks = useCoreWorks(6);
2620
+ // 同一任务/作品区合并 Core 只读投影;完成提示会话内只出现一次,刷新后从事实重算。
2621
+ var coreUser = useCoreUserTasks();
2622
+ var coreGroups = splitCoreUserRows(coreUser.rows || []);
2623
+ var arrivedPair = React.useState([]);
2624
+ var coreArrived = arrivedPair[0];
2625
+ var setCoreArrived = arrivedPair[1];
2626
+ var coreRowsKey = (coreUser.rows || []).map(function (row) { return row.id + '@' + row.revision; }).join(',');
2627
+ React.useEffect(function () {
2628
+ var added = noteCoreCompletions(coreUser.rows || []);
2629
+ if (added.length) setCoreArrived(added);
2630
+ }, [coreRowsKey]);
2631
+ var works = coreWorks.concat(legacyWorks).sort(function (a, b) {
2632
+ return String(b.createdAt || '').localeCompare(String(a.createdAt || ''));
2633
+ }).slice(0, 6);
2634
+ var artifactTotal = ((state && state.artifacts && state.artifacts.total) || legacyWorks.length) + coreWorks.length;
1478
2635
  var cards = useBubbleCards()[0];
1479
2636
  var tabPair = React.useState('tasks');
1480
2637
  var tab = tabPair[0];
@@ -1487,12 +2644,16 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1487
2644
  if (tab === 'tasks') {
1488
2645
  // 快捷面板承载当前任务、需要处理的异常事实与独立作品库中的最近作品。
1489
2646
  body = React.createElement('div', { className: 'iris-wb-box' },
1490
- running.length ? React.createElement('div', {},
2647
+ coreArrived.length ? React.createElement('div', { className: 'iris-wb-badge ok', role: 'status' },
2648
+ '新完成:' + coreArrived.map(function (row) { return row.model || row.id; }).join('、')) : null,
2649
+ running.length || coreGroups.live.length ? React.createElement('div', {},
1491
2650
  React.createElement('div', { className: 'iris-wb-sec' }, '运行中'),
1492
- running.map(function (t) { return taskRow(t, true, selectedTask === t.id, onSelect); })) : null,
1493
- attention.length ? React.createElement('div', {},
2651
+ running.map(function (t) { return taskRow(t, true, selectedTask === t.id, onSelect); }),
2652
+ coreGroups.live.map(coreTaskMini)) : null,
2653
+ attention.length || coreGroups.attention.length ? React.createElement('div', {},
1494
2654
  React.createElement('div', { className: 'iris-wb-sec' }, '需要处理'),
1495
- attention.slice(0, 4).map(function (t) { return taskRowMini(t, selectedTask === t.id, onSelect); })) : null,
2655
+ attention.slice(0, 4).map(function (t) { return taskRowMini(t, selectedTask === t.id, onSelect); }),
2656
+ coreGroups.attention.slice(0, 4).map(coreTaskMini)) : null,
1496
2657
  React.createElement('div', { className: 'iris-wb-sec' }, '最近作品'),
1497
2658
  works.length ? works.map(artifactRowMini)
1498
2659
  : React.createElement('div', { className: 'iris-wb-empty' }, '尚无作品'),
@@ -1513,6 +2674,17 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1513
2674
  selectedTask ? React.createElement(TaskDetailDrawer, { taskId: selectedTask }) : null);
1514
2675
  }
1515
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
+
1516
2688
  function PromptOptimizerControl(props) {
1517
2689
  var draft = props.useInput(function (value) { return value.draft; });
1518
2690
  var phase = props.useInput(function (value) { return value.phase; });
@@ -1538,8 +2710,26 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1538
2710
  var configPair = React.useState(null);
1539
2711
  var configState = configPair[0];
1540
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];
1541
2725
  var abortRef = React.useRef(null);
1542
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);
1543
2733
 
1544
2734
  function promptRequest(operation, body, signal) {
1545
2735
  return fetch('/iris/api/prompt-optimizer/' + operation, {
@@ -1565,6 +2755,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1565
2755
  React.useEffect(function () {
1566
2756
  return function () {
1567
2757
  if (abortRef.current) abortRef.current.abort();
2758
+ abortRef.current = null;
1568
2759
  };
1569
2760
  }, []);
1570
2761
 
@@ -1575,53 +2766,96 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1575
2766
  return function () { window.removeEventListener('iris-prompt-config-changed', changed); };
1576
2767
  }, []);
1577
2768
 
2769
+ React.useEffect(function () {
2770
+ cancel();
2771
+ setResult(null);
2772
+ lastWrittenRef.current = null;
2773
+ resultSessionRef.current = null;
2774
+ }, [props.sessionId]);
2775
+
1578
2776
  function publishConfig(data) {
1579
2777
  setConfigState(data);
1580
2778
  try { window.dispatchEvent(new CustomEvent('iris-prompt-config-changed', { detail: data })); } catch (_) {}
1581
2779
  }
1582
2780
 
1583
- 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; }
1584
2793
  if (!draft || !draft.trim()) { setNote('请先在输入框中写入提示词'); return; }
1585
2794
  if (refCount) { setNote('当前版本暂不改写含 @ 或 / 结构化引用的草稿,以免引用失效'); return; }
1586
2795
  if (phase !== 'plain') { setNote('输入框正忙,请稍后再试'); return; }
1587
2796
  if (abortRef.current) abortRef.current.abort();
1588
2797
  var controller = new AbortController();
2798
+ var requestSessionId = props.sessionId;
1589
2799
  abortRef.current = controller;
1590
2800
  setBusy(true);
1591
- setNote('正在调用模型优化…这可能产生模型费用');
2801
+ setNote(mode === 'assemble' ? '正在组装,不调用模型…' : '正在调用模型优化…这可能产生模型费用');
1592
2802
  setResult(null);
2803
+ lastWrittenRef.current = null;
1593
2804
  promptRequest('optimize', {
1594
2805
  text: draft,
1595
2806
  target: target,
2807
+ mode: mode,
2808
+ rules: selectedRules(mode),
1596
2809
  sessionId: props.sessionId,
1597
2810
  route: selectedRoute || undefined
1598
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('未收到完整优化结果');
1599
2815
  setResult(data);
2816
+ setEdited(data.optimized);
2817
+ resultSessionRef.current = requestSessionId;
1600
2818
  setNote('已生成预览,尚未写回输入框');
1601
2819
  }).catch(function (err) {
2820
+ if (abortRef.current !== controller) return;
1602
2821
  if (controller.signal.aborted) setNote('已取消');
1603
2822
  else setNote(String(err && err.message || err));
1604
2823
  }).finally(function () {
1605
- if (abortRef.current === controller) abortRef.current = null;
1606
- setBusy(false);
2824
+ if (abortRef.current === controller) { abortRef.current = null; setBusy(false); }
1607
2825
  });
1608
2826
  }
1609
2827
 
1610
2828
  function cancel() {
1611
- 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;
1612
2842
  }
1613
2843
 
1614
2844
  function replaceDraft(text) {
1615
2845
  if (!result) return;
1616
- 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;
1617
2850
  props.inputActions.setDraft(text);
2851
+ lastWrittenRef.current = text;
1618
2852
  setNote(text === result.original ? '已恢复优化前原文' : '已写回输入框;不会自动发送');
1619
2853
  }
1620
2854
 
1621
2855
  function copyResult() {
1622
- if (!result) return;
2856
+ if (!result || !validEdited()) return;
1623
2857
  if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
1624
- navigator.clipboard.writeText(result.optimized).then(function () { setNote('已复制优化结果'); }).catch(function () { setNote('复制失败,请手动选择文本'); });
2858
+ navigator.clipboard.writeText(edited).then(function () { setNote('已复制优化结果'); }).catch(function () { setNote('复制失败,请手动选择文本'); });
1625
2859
  } else {
1626
2860
  setNote('浏览器不支持自动复制,请手动选择文本');
1627
2861
  }
@@ -1687,15 +2921,21 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1687
2921
  var reasoningSetting = configState && configState.config && configState.config.generation && configState.config.generation.reasoningEffort || 'off-if-supported';
1688
2922
  var reasoningLabel = reasoningSetting === 'off-if-supported' ? '支持时关闭' : reasoningSetting === 'provider-default' ? '供应商默认' : reasoningSetting === 'inherit' ? '跟随会话' : reasoningSetting;
1689
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
+ }
1690
2930
 
1691
2931
  var panel = null;
1692
2932
  if (open) {
1693
2933
  panel = React.createElement(React.Fragment, {},
1694
- 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' }),
1695
2935
  React.createElement('div', { className: 'iris-po-panel', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Iris 提示词优化', onClick: function (e) { e.stopPropagation(); } },
1696
2936
  React.createElement('div', { className: 'iris-po-head' },
1697
2937
  React.createElement('strong', { className: 'iris-po-title' }, React.createElement('span', { className: 'iris-po-mark' }, '🫧'), 'Iris 提示词优化'),
1698
- React.createElement('button', { className: 'iris-po-close', onClick: function () { setOpen(false); }, title: '关闭' }, '✕')),
2938
+ React.createElement('button', { className: 'iris-po-close', onClick: closePanel, title: '关闭' }, '✕')),
1699
2939
  React.createElement('div', { className: 'iris-po-muted' }, '只处理当前未发送草稿,不读取会话历史;调用模型可能产生费用。'),
1700
2940
  React.createElement('label', { className: 'iris-po-field' },
1701
2941
  React.createElement('span', {}, '优化目标'),
@@ -1705,17 +2945,37 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1705
2945
  React.createElement('option', { value: 'video' }, '视频生成'),
1706
2946
  React.createElement('option', { value: 's2v' }, '首尾帧视频'))),
1707
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 配置。' : '当前后端尚未支持规则,请更新后重新载入。')),
1708
2955
  React.createElement('div', { className: 'iris-po-actions' },
1709
- 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'); } }, '只组装(不调用模型)'),
1710
2958
  busy ? React.createElement('button', { className: 'iris-po-btn', onClick: cancel }, '取消') : null),
1711
2959
  result ? React.createElement('div', { className: 'iris-po-result' },
1712
- React.createElement('div', { className: 'iris-po-result-label' }, '优化结果'),
1713
- React.createElement('textarea', { readOnly: true, value: result.optimized }),
1714
- 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) : '本次只组装,未调用模型'),
1715
2975
  React.createElement('div', { className: 'iris-po-actions' },
1716
- React.createElement('button', { className: 'iris-po-primary', onClick: function () { replaceDraft(result.optimized); } }, '写回输入框'),
2976
+ React.createElement('button', { className: 'iris-po-primary', onClick: function () { replaceDraft(edited); } }, '写回输入框'),
1717
2977
  React.createElement('button', { className: 'iris-po-btn', onClick: copyResult }, '复制'),
1718
- React.createElement('button', { className: 'iris-po-btn', onClick: optimize }, '再优化'),
2978
+ React.createElement('button', { className: 'iris-po-btn', disabled: busy, onClick: function () { optimize(result.mode); } }, '重新生成预览'),
1719
2979
  React.createElement('button', { className: 'iris-po-btn', onClick: function () { replaceDraft(result.original); } }, '恢复原文'))) : null,
1720
2980
  note ? React.createElement('div', { className: 'iris-po-note' }, note) : null,
1721
2981
  React.createElement('details', { className: 'iris-po-config' },
@@ -1736,7 +2996,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1736
2996
  type: 'button',
1737
2997
  title: refCount ? '含结构化引用的草稿暂不支持优化' : '优化当前输入框提示词',
1738
2998
  'aria-expanded': open ? 'true' : 'false',
1739
- onClick: function () { setOpen(!open); }
2999
+ onClick: function () { if (open) closePanel(); else setOpen(true); }
1740
3000
  }, '🫧'),
1741
3001
  panel);
1742
3002
  }
@@ -1754,12 +3014,37 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1754
3014
  }));
1755
3015
  }
1756
3016
 
3017
+ /* 曲线鸢尾花:矢量源见 docs/assets/logo/iris-bubble.svg。
3018
+ 使用实色与无 ID 的路径,多个座位同时渲染也不会发生 SVG 引用冲突。 */
3019
+ var IRIS_MARK_PARTS = [
3020
+ { fill: '#7db9a4', d: 'M30.5 39h3v18h-3z' },
3021
+ { fill: '#7db9a4', d: 'M32 55C24 55 20 50 19 44C26 45 31 48 32 55Z' },
3022
+ { fill: '#9cd1b7', d: 'M32 54C40 53 44 49 45 43C38 44 33 48 32 54Z' },
3023
+ { fill: '#b5a0f3', d: 'M29 31C18 30 11 22 15 14C24 13 30 20 29 31Z' },
3024
+ { fill: '#b5a0f3', d: 'M35 31C46 30 53 22 49 14C40 13 34 20 35 31Z' },
3025
+ { fill: '#e0d4ff', d: 'M32 7C24 13 23 23 32 34C41 23 40 13 32 7Z' },
3026
+ { fill: '#9b80df', d: 'M31 31C22 24 11 28 8 36C12 45 24 47 31 34Z' },
3027
+ { fill: '#9b80df', d: 'M33 31C42 24 53 28 56 36C52 45 40 47 33 34Z' },
3028
+ { fill: '#c4aff6', d: 'M32 29C24 35 23 44 32 50C41 44 40 35 32 29Z' },
3029
+ { fill: '#f2ce83', d: 'M32 31C29 32 28 34 29 36L32 40L35 36C36 34 35 32 32 31Z' }
3030
+ ];
3031
+ function IRIS_MARK_SVG() {
3032
+ return React.createElement('svg', {
3033
+ width: 26, height: 26, viewBox: '0 0 64 64',
3034
+ role: 'img', 'aria-label': 'Iris', focusable: 'false'
3035
+ }, IRIS_MARK_PARTS.map(function (part, i) {
3036
+ return React.createElement('path', { key: i, d: part.d, fill: part.fill });
3037
+ }));
3038
+ }
3039
+
1757
3040
  function FloatingBubble() {
1758
3041
  var state = useIrisState(5000);
1759
3042
  var providers = (state && state.providers) || [];
1760
3043
  var fallbackConfigured = providers.some(function (p) { return p.enabled && p.apiKeyHint; });
1761
- var overallHealth = state && state.health && HEALTH_META[state.health.overall]
3044
+ var overallState = state && state.health && HEALTH_META[state.health.overall]
1762
3045
  ? state.health.overall : (fallbackConfigured ? 'configured' : 'unconfigured');
3046
+ var overallCooling = overallState === 'failed' && state.health.retryAt;
3047
+ var overallHealth = overallCooling ? 'cooling' : overallState;
1763
3048
  var running = (state && state.tasks && state.tasks.running) || [];
1764
3049
  var attention = (state && state.tasks && state.tasks.attention) || [];
1765
3050
 
@@ -1885,9 +3170,9 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1885
3170
  onPointerMove: onPointerMove,
1886
3171
  onPointerUp: onPointerUp,
1887
3172
  onPointerCancel: onPointerCancel,
1888
- title: 'Iris ' + healthText({ status: overallHealth }) + ':点击打开快捷面板'
3173
+ title: 'Iris ' + healthText({ status: overallState, rateLimited: !!overallCooling, retryAt: overallCooling || undefined }) + ':点击打开快捷面板'
1889
3174
  },
1890
- React.createElement('div', { className: 'iris-bubble-btn' }, '🫧'),
3175
+ React.createElement('div', { className: 'iris-bubble-btn' }, React.createElement(IRIS_MARK_SVG)),
1891
3176
  badge,
1892
3177
  panel);
1893
3178
  }