@mokuyoaxis/dsh-iris 0.2.0 → 0.2.2

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 (93) 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 +69 -2
  5. package/README.en.md +67 -31
  6. package/README.md +67 -31
  7. package/bin/dsh-iris.js +222 -21
  8. package/docs/AGENT_QUICKSTART.md +10 -6
  9. package/docs/ARCHITECTURE.md +34 -19
  10. package/docs/ARTIFACT_LIBRARY.md +5 -5
  11. package/docs/ARTIFACT_MANIFEST.md +1 -1
  12. package/docs/CLI_MANAGEMENT.md +183 -0
  13. package/docs/COMPOSITE_VISION.md +35 -0
  14. package/docs/CORE_RUNTIME_CONTRACT.md +16 -7
  15. package/docs/DSH_CORE_MIGRATION.md +48 -19
  16. package/docs/DSH_RC2_ADAPTATION.md +4 -2
  17. package/docs/FAULT_INJECTION.md +21 -2
  18. package/docs/HEADLESS_CLI.md +15 -7
  19. package/docs/HOST_ADAPTER_CONTRACT.md +10 -10
  20. package/docs/HOST_DOCTOR.md +3 -3
  21. package/docs/MODEL_PORT_CONTRACT.md +21 -14
  22. package/docs/OCR_MODEL.md +39 -0
  23. package/docs/PROMPT_OPTIMIZER.md +77 -0
  24. package/docs/PROVIDER_ADAPTER_CONTRACT.md +15 -5
  25. package/docs/PROVIDER_HEALTH.md +22 -5
  26. package/docs/PROVIDER_RUNTIME_HARNESS.md +1 -1
  27. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +2 -2
  28. package/docs/ROADMAP.md +37 -19
  29. package/docs/SECURITY.md +5 -5
  30. package/docs/TASK_SEMANTICS.md +2 -2
  31. package/docs/VISION_CLI.md +132 -0
  32. package/docs/VISION_MODEL.md +51 -0
  33. package/docs/WORKBENCH_ARTIFACTS.md +67 -0
  34. package/docs/file-access-across-environments.md +8 -5
  35. package/docs/releases/0.2.1.md +56 -0
  36. package/docs/releases/0.2.2.md +31 -0
  37. package/lib/actions.js +199 -286
  38. package/lib/adapters.js +216 -41
  39. package/lib/api.js +49 -7
  40. package/lib/capability.js +23 -16
  41. package/lib/chromium-browser.js +117 -0
  42. package/lib/client.js +933 -117
  43. package/lib/command-service.js +121 -7
  44. package/lib/composite-vision-routing.js +42 -0
  45. package/lib/config.js +102 -14
  46. package/lib/core-artifact-store.js +45 -13
  47. package/lib/core-lease-recovery.js +64 -92
  48. package/lib/core-maintenance.js +213 -0
  49. package/lib/core-runtime.js +45 -47
  50. package/lib/core-tasks.js +12 -5
  51. package/lib/core-user-projection.js +1 -0
  52. package/lib/core-writer-lease.js +134 -0
  53. package/lib/dsh-core-adapter.js +63 -9
  54. package/lib/dsh-text-model-adapter.js +222 -0
  55. package/lib/dsh-vision-model-adapter.js +75 -0
  56. package/lib/generation-input.js +36 -5
  57. package/lib/headless-vision.js +181 -0
  58. package/lib/host-runtime.js +1 -0
  59. package/lib/http-vision-model-adapter.js +187 -0
  60. package/lib/image-edit-input.js +34 -0
  61. package/lib/index.js +205 -175
  62. package/lib/locate.js +86 -28
  63. package/lib/media-probe.js +2 -1
  64. package/lib/model-call-runtime.js +45 -0
  65. package/lib/model-invoker.js +4 -2
  66. package/lib/model-port-contract.js +13 -5
  67. package/lib/models.js +29 -42
  68. package/lib/ocr-model-routing.js +26 -0
  69. package/lib/ocr.js +110 -64
  70. package/lib/prompt-optimizer-config.js +21 -10
  71. package/lib/prompt-optimizer-core.js +247 -0
  72. package/lib/prompt-optimizer.js +37 -193
  73. package/lib/provider-adapters.js +87 -10
  74. package/lib/provider-catalog.js +19 -7
  75. package/lib/provider-config-service.js +235 -0
  76. package/lib/provider-contract.js +3 -2
  77. package/lib/provider-health.js +80 -1
  78. package/lib/provider-protocol.js +84 -0
  79. package/lib/provider-task-runner.js +63 -19
  80. package/lib/summarize.js +31 -20
  81. package/lib/summary-input.js +52 -0
  82. package/lib/task-wait.js +40 -0
  83. package/lib/video-input.js +30 -0
  84. package/lib/vision-core.js +42 -0
  85. package/lib/vision-image-input.js +83 -0
  86. package/lib/vision-input.js +31 -0
  87. package/lib/vision-model-routing.js +180 -0
  88. package/lib/vision.js +3 -2
  89. package/lib/workbench-artifacts.js +52 -0
  90. package/lib/workbench-management.js +151 -0
  91. package/lib/workbench-zip.js +39 -0
  92. package/package.json +1 -1
  93. package/user_guide.md +74 -30
package/lib/client.js CHANGED
@@ -21,7 +21,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
21
21
  var exports = module.exports;
22
22
 
23
23
  var React = require('react');
24
- var IRIS_CLIENT_VERSION = '0.2.0';
24
+ var IRIS_CLIENT_VERSION = '0.2.2';
25
25
  var HOST_CLIENT_PROTOCOL_VERSION = 0;
26
26
  var reportedSeats = [];
27
27
 
@@ -232,7 +232,31 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
232
232
  '.iris-gallery-file { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; font-weight: 600; }',
233
233
  '.iris-gallery-meta { color: var(--dsw-alias-label-tertiary, #7a7a85); font-size: 10px; }',
234
234
  '.iris-gallery-info { display: grid; gap: 4px; padding: 8px 9px 9px; }',
235
- '.iris-gallery-actions { display: flex; align-items: center; gap: 8px; }',
235
+ '.iris-gallery-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }',
236
+ '.iris-gallery-card.selected { border-color: var(--dsw-alias-brand-primary, #7aa2f7); }',
237
+ '.iris-gallery-select { display: flex; align-items: center; gap: 6px; font-size: 11px; }',
238
+ '.iris-gallery-select input { width: 18px; height: 18px; margin: 0; accent-color: var(--dsw-alias-brand-primary, #7aa2f7); }',
239
+ '.iris-gallery-bulk { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }',
240
+ '.iris-gallery-management { padding: 12px; margin-bottom: 12px; border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 13px; display: grid; gap: 10px; }',
241
+ '.iris-gallery-management dl { margin: 0; display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 6px 12px; font-size: 12px; }',
242
+ '.iris-gallery-management dt { color: var(--dsw-alias-label-tertiary, #7a7a85); }',
243
+ '.iris-gallery-management dd { margin: 0; overflow-wrap: anywhere; white-space: pre-wrap; }',
244
+ '.iris-gallery-management-list { display: grid; gap: 8px; max-height: 45vh; overflow-y: auto; overflow-wrap: anywhere; font-size: 12px; }',
245
+ '.iris-gallery-transaction { padding: 8px; border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 8px; display: grid; gap: 6px; }',
246
+ '.iris-gallery-filters { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; }',
247
+ '.iris-gallery-filters label { display: grid; gap: 3px; font-size: 11px; flex: 1; min-width: 110px; }',
248
+ '.iris-gallery-filters select { min-height: 34px; padding: 4px 7px; background: var(--dsw-alias-bg-layer-2, #32323c); color: inherit; border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 7px; font: inherit; }',
249
+ '.iris-gallery-pagination { display: flex; justify-content: center; align-items: center; gap: 12px; margin-top: 12px; }',
250
+ '.iris-gallery-analysis { display: grid; grid-template-columns: minmax(120px, .6fr) minmax(0, 1fr); gap: 12px; padding: 12px; margin-bottom: 12px; border: 1px solid var(--dsw-alias-brand-primary, #7aa2f7); border-radius: 13px; background: var(--dsw-alias-bg-layer-1, #26262e); }',
251
+ '.iris-gallery-analysis .iris-gallery-preview { max-height: 260px; border-radius: 8px; }',
252
+ '.iris-gallery-analysis-body { display: grid; gap: 8px; min-width: 0; }',
253
+ '.iris-gallery-analysis-head { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }',
254
+ '.iris-gallery-analysis-head strong { flex: 1; }',
255
+ '.iris-gallery-analysis textarea { width: 100%; box-sizing: border-box; min-height: 140px; max-height: 40vh; resize: vertical; font: inherit; line-height: 1.6; padding: 8px; color: inherit; background: var(--dsw-alias-bg-layer-2, #32323c); border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 7px; }',
256
+ '.iris-gallery-analysis-options { display: grid; gap: 8px; }',
257
+ '.iris-gallery-analysis-options label { display: grid; gap: 3px; font-size: 11px; }',
258
+ '.iris-gallery-analysis-options input { box-sizing: border-box; width: 100%; padding: 6px; color: inherit; background: var(--dsw-alias-bg-layer-2, #32323c); border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 6px; }',
259
+ '@media (max-width: 640px) { .iris-gallery-analysis { grid-template-columns: 1fr; } .iris-gallery-actions button, .iris-gallery-pagination button, .iris-gallery-analysis button, .iris-gallery-management button, .iris-gallery-bulk button { min-height: 40px; } }',
236
260
  '.iris-artifact-mini { display: flex; align-items: center; gap: 7px; min-width: 0; padding: 4px 6px; border-radius: 7px; color: var(--dsw-alias-label-secondary, #9a9a9a); text-decoration: none; }',
237
261
  '.iris-artifact-mini:hover { background: var(--dsw-alias-bg-layer-2, #32323c); color: var(--dsw-alias-label-primary, #e6e6e6); }',
238
262
  '.iris-artifact-mini span:nth-child(2) { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }',
@@ -248,6 +272,10 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
248
272
  '.iris-pm-ver.configured { color: #7da8fa; }',
249
273
  '.iris-pm-ver.verified { color: var(--dsw-alias-state-success-primary, #6fcf6f); }',
250
274
  '.iris-pm-ver.failed { color: #c87a81; border-color: rgba(185,101,109,.5); }',
275
+ '.iris-pm-ver.rate-limited { color: #f87171; border-color: #ef4444; box-shadow: 0 0 7px rgba(239,68,68,.35); }',
276
+ '.iris-pm-ver.cooling, .iris-pm-dot.cooling, .iris-health-row.cooling .iris-health-state { color: #fbbf24; border-color: #f59e0b; box-shadow: none; }',
277
+ '.iris-pm-dot.cooling { background: #fbbf24; opacity: 1; }',
278
+ '.iris-bubble.health-cooling .iris-bubble-btn { border-color: #f59e0b; box-shadow: 0 0 11px rgba(245,158,11,.3); }',
251
279
  '.iris-pm-ver.unconfigured { opacity: .55; }',
252
280
  '.iris-pm-madd { flex: 1; background: var(--dsw-alias-bg-layer-2, #32323c); color: var(--dsw-alias-label-primary, #e6e6e6); border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 4px; padding: 3px 6px; font-size: 12px; }',
253
281
  /* 文件选择器(阶段 10) */
@@ -271,9 +299,31 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
271
299
  '.iris-po-trigger:hover { transform: translateY(-2px) scale(1.10); filter: drop-shadow(0 5px 10px rgba(112,137,255,.42)); }',
272
300
  '.iris-po-trigger:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary, #7aa2f7); outline-offset: 2px; }',
273
301
  '.iris-po-backdrop { position: fixed; inset: 0; z-index: 10019; background: rgba(8,10,18,.30); backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px); touch-action: none; animation: iris-po-fade .16s ease-out; }',
274
- '.iris-po-panel { position: fixed; z-index: 10020; right: max(14px, env(safe-area-inset-right)); bottom: max(78px, calc(env(safe-area-inset-bottom) + 68px)); width: min(440px, calc(100vw - 28px)); max-height: min(680px, calc(100dvh - 104px)); overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; touch-action: pan-y; box-sizing: border-box; padding: 18px; display: grid; gap: 13px; background-color: rgba(35,36,47,.84); background: radial-gradient(circle at 10% -5%, rgba(132,160,255,.24), transparent 42%), color-mix(in srgb, var(--dsw-alias-bg-layer-1, #26262e) 82%, transparent); color: var(--dsw-alias-label-primary, #eeeeF2); border: 1px solid rgba(150,164,215,.25); border-radius: 20px; box-shadow: 0 24px 70px rgba(0,0,0,.48), inset 0 1px 0 rgba(255,255,255,.07); backdrop-filter: blur(28px) saturate(1.28); -webkit-backdrop-filter: blur(28px) saturate(1.28); animation: iris-po-rise .20s cubic-bezier(.2,.8,.2,1); scrollbar-width: thin; }',
302
+ '.iris-po-panel:where(dialog) { left: auto; top: auto; margin: 0; max-width: none; }',
303
+ 'dialog.iris-po-panel::backdrop { background: rgba(8,10,18,.30); backdrop-filter: blur(2px); -webkit-backdrop-filter: blur(2px); }',
304
+ '.iris-po-panel { position: fixed; z-index: 10020; right: max(14px, env(safe-area-inset-right)); bottom: max(78px, calc(env(safe-area-inset-bottom) + 68px)); width: min(560px, calc(100vw - 28px)); max-height: min(780px, calc(100dvh - 104px)); overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; touch-action: pan-y; box-sizing: border-box; padding: 18px; display: grid; gap: 14px; background-color: rgba(35,36,47,.84); background: radial-gradient(circle at 10% -5%, rgba(132,160,255,.20), transparent 42%), color-mix(in srgb, var(--dsw-alias-bg-layer-1, #26262e) 92%, transparent); color: var(--dsw-alias-label-primary, #eeeeF2); border: 1px solid rgba(150,164,215,.25); border-radius: 22px; box-shadow: 0 24px 70px rgba(0,0,0,.36), inset 0 1px 0 rgba(255,255,255,.07); backdrop-filter: blur(28px) saturate(1.28); -webkit-backdrop-filter: blur(28px) saturate(1.28); animation: iris-po-rise .20s cubic-bezier(.2,.8,.2,1); scrollbar-width: thin; outline: none; }',
275
305
  '.iris-po-head { position: sticky; top: -18px; z-index: 1; margin: -18px -18px 0; padding: 16px 18px 10px; display: flex; align-items: center; justify-content: space-between; gap: 10px; background: linear-gradient(to bottom, color-mix(in srgb, var(--dsw-alias-bg-layer-1, #26262e) 88%, transparent) 72%, transparent); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); }',
276
- '.iris-po-title { display: inline-flex; align-items: center; gap: 9px; font-size: 14px; letter-spacing: .01em; }',
306
+ '.iris-po-title { display: inline-flex; align-items: center; gap: 9px; font-size: 16px; letter-spacing: .01em; }',
307
+ '.iris-po-intro { display: grid; gap: 9px; }',
308
+ '.iris-po-principles { display: flex; flex-wrap: wrap; gap: 6px; }',
309
+ '.iris-po-principles span { padding: 3px 8px; border-radius: 999px; background: rgba(113,139,219,.09); color: var(--dsw-alias-label-secondary, #b4bdd5); font-size: 10px; }',
310
+ '.iris-po-section { min-width: 0; border: 1px solid rgba(137,155,218,.22); border-radius: 16px; background: rgba(128,150,221,.045); overflow: hidden; }',
311
+ '.iris-po-section > summary { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 14px; cursor: pointer; list-style: none; font-size: 13px; font-weight: 650; }',
312
+ '.iris-po-section > summary::-webkit-details-marker { display: none; }',
313
+ '.iris-po-section > summary::after { content: "⌄"; color: var(--dsw-alias-label-secondary, #a6a6b2); transition: transform .15s; }',
314
+ '.iris-po-section[open] > summary::after { transform: rotate(180deg); }',
315
+ '.iris-po-section-caption { margin-left: auto; font-size: 11px; font-weight: 400; color: var(--dsw-alias-label-secondary, #a6a6b2); }',
316
+ '.iris-po-section-body { min-width: 0; display: grid; gap: 13px; padding: 0 14px 14px; }',
317
+ '.iris-po-section:not([open]) > .iris-po-section-body { display: none; }',
318
+ '.iris-po-targets { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 5px; }',
319
+ '.iris-po-target { min-width: 0; min-height: 42px; padding: 7px 3px; border: 1px solid transparent; border-radius: 10px; font: inherit; font-size: 12px; cursor: pointer; color: var(--dsw-alias-label-secondary, #a6a6b2); background: rgba(128,150,221,.05); }',
320
+ '.iris-po-target[aria-pressed="true"] { color: var(--dsw-alias-label-primary, #eeeef2); border-color: rgba(124,147,225,.48); background: rgba(124,147,225,.18); box-shadow: inset 0 1px 0 rgba(255,255,255,.1); font-weight: 650; }',
321
+ '.iris-po-target:disabled { opacity: .45; cursor: not-allowed; }',
322
+ '.iris-po-target:focus-visible, .iris-po-section > summary:focus-visible, .iris-po-config > summary:focus-visible { outline: 2px solid #8d9fe6; outline-offset: 2px; }',
323
+ '.iris-po-source { min-width: 0; padding: 11px 12px; border-radius: 12px; border: 1px solid rgba(150,164,215,.14); background: rgba(128,150,221,.04); }',
324
+ '.iris-po-source-head { display: flex; justify-content: space-between; gap: 10px; font-size: 11px; color: var(--dsw-alias-label-secondary, #a6a6b2); }',
325
+ '.iris-po-source-text { margin: 8px 0 0; max-height: 112px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; font-size: 12px; line-height: 1.65; }',
326
+ '.iris-po-template { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 200px; overflow: auto; font-size: 11px; line-height: 1.65; padding: 10px; border-radius: 10px; background: rgba(128,150,221,.06); }',
277
327
  '.iris-po-mark, .iris-po-settings-mark { display: inline-flex; align-items: center; justify-content: center; width: 29px; height: 29px; border-radius: 999px; background: rgba(255,255,255,.055); box-shadow: inset 0 1px 0 rgba(255,255,255,.10), 0 5px 16px rgba(67,83,168,.16); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); filter: drop-shadow(0 2px 5px rgba(105,128,235,.16)); }',
278
328
  '.iris-po-close { width: 30px; height: 30px; display: inline-flex; align-items: center; justify-content: center; border: 0; border-radius: 999px; background: rgba(255,255,255,.05); color: var(--dsw-alias-label-secondary, #a6a6b2); font-size: 14px; cursor: pointer; transition: background .15s, color .15s; }',
279
329
  '.iris-po-close:hover { background: rgba(255,255,255,.10); color: var(--dsw-alias-label-primary, #eeeef2); }',
@@ -281,8 +331,19 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
281
331
  '.iris-po-route { padding: 7px 10px; border-radius: 10px; background: rgba(255,255,255,.045); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }',
282
332
  '.iris-po-note { padding: 8px 10px; border-radius: 10px; color: var(--dsw-alias-state-warn-primary, #dfbd69); background: rgba(217,169,65,.08); }',
283
333
  '.iris-po-field { display: grid; gap: 6px; font-size: 11px; color: var(--dsw-alias-label-secondary, #a6a6b2); }',
284
- '.iris-po-field select, .iris-po-result textarea { width: 100%; box-sizing: border-box; background: rgba(255,255,255,.055); color: var(--dsw-alias-label-primary, #eeeef2); border: 1px solid rgba(150,164,215,.20); border-radius: 11px; padding: 9px 11px; font: inherit; font-size: 12px; outline: none; transition: border-color .15s, background .15s, box-shadow .15s; }',
285
- '.iris-po-field select:focus, .iris-po-result textarea:focus { border-color: rgba(122,162,247,.72); background: rgba(255,255,255,.075); box-shadow: 0 0 0 3px rgba(122,162,247,.11); }',
334
+ '.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; }',
335
+ '.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); }',
336
+ '.iris-po-field textarea { min-height: 60px; resize: vertical; line-height: 1.55; }',
337
+ '.iris-po-rules { display: grid; gap: 10px; }',
338
+ '.iris-po-editing { display: grid; gap: 8px; padding: 10px 12px; border: 1px solid rgba(150,164,215,.16); border-radius: 12px; }',
339
+ '.iris-po-editing label { display: flex; gap: 8px; align-items: center; font-size: 12px; cursor: pointer; }',
340
+ '.iris-po-suggestion { display: grid; gap: 7px; padding: 10px; margin-top: 8px; border-radius: 10px; background: rgba(128,150,221,.065); overflow-wrap: anywhere; }',
341
+ '.iris-po-suggestion label { display: flex; align-items: start; gap: 8px; font-size: 12px; }',
342
+ '.iris-po-suggestion p { margin: 0; white-space: pre-wrap; font-size: 12px; line-height: 1.6; }',
343
+ '.iris-po-diff { white-space: pre-wrap; overflow-wrap: anywhere; max-height: 28dvh; overflow-y: auto; font-size: 12px; line-height: 1.6; }',
344
+ '.iris-po-diff del, .iris-po-diff ins { display: block; padding: 6px 8px; border-radius: 7px; margin: 5px 0; }',
345
+ '.iris-po-diff del { background: rgba(226,113,126,.12); }',
346
+ '.iris-po-diff ins { background: rgba(99,198,156,.12); text-decoration: none; }',
286
347
  '.iris-po-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }',
287
348
  '.iris-po-btn, .iris-po-primary { min-height: 34px; border: 1px solid rgba(150,164,215,.20); border-radius: 10px; padding: 6px 11px; font: inherit; font-size: 11px; cursor: pointer; transition: transform .15s, background .15s, border-color .15s; }',
288
349
  '.iris-po-btn { background: rgba(255,255,255,.045); color: var(--dsw-alias-label-secondary, #b0b0bb); }',
@@ -290,7 +351,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
290
351
  '.iris-po-primary { background: linear-gradient(135deg, #779cf3, #8c79de); color: #fff; border-color: rgba(174,187,255,.55); font-weight: 650; box-shadow: 0 6px 18px rgba(76,92,188,.22); }',
291
352
  '.iris-po-primary:hover { transform: translateY(-1px); box-shadow: 0 8px 22px rgba(76,92,188,.32); }',
292
353
  '.iris-po-primary:disabled, .iris-po-btn:disabled { opacity: .45; cursor: not-allowed; transform: none; box-shadow: none; }',
293
- '.iris-po-result { display: grid; gap: 8px; padding: 12px; border: 1px solid rgba(137,155,218,.18); border-radius: 15px; background: rgba(10,12,20,.16); }',
354
+ '.iris-po-result { display: grid; gap: 8px; padding: 12px; border: 1px solid rgba(137,155,218,.18); border-radius: 15px; background: rgba(128,150,221,.045); }',
294
355
  '.iris-po-result-label { font-size: 11px; font-weight: 650; letter-spacing: .02em; }',
295
356
  '.iris-po-result textarea { min-height: 156px; max-height: 34dvh; resize: vertical; line-height: 1.58; }',
296
357
  '.iris-po-config { padding: 11px 12px; border: 1px solid rgba(150,164,215,.14); border-radius: 13px; background: rgba(255,255,255,.025); font-size: 11px; }',
@@ -419,7 +480,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
419
480
  unconfigured: { label: '未配置', symbol: '○' },
420
481
  configured: { label: '已配置,待验证', symbol: '◌' },
421
482
  verified: { label: '近期验证成功', symbol: '●' },
422
- failed: { label: '认证失败', symbol: '×' }
483
+ failed: { label: '调用受阻', symbol: '×' }
423
484
  };
424
485
 
425
486
  function capabilityHealth(state, capability) {
@@ -443,9 +504,22 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
443
504
  return Math.floor(ms / 86400000) + ' 天前';
444
505
  }
445
506
 
507
+ function rateLimitLabel(health) {
508
+ return health.retryAt ? '429 冷却中' : health.reason === 'budget' ? '预算耗尽已停用' : '额度耗尽已停用';
509
+ }
510
+
511
+ function healthClass(health) {
512
+ return health.retryAt ? 'cooling' : health.rateLimited ? 'rate-limited' : health.status;
513
+ }
514
+
446
515
  function healthText(health) {
447
516
  var status = health && HEALTH_META[health.status] ? health.status : 'unconfigured';
448
- var text = HEALTH_META[status].label;
517
+ var text = health && health.rateLimited
518
+ ? rateLimitLabel(health) + (health.retryAt
519
+ ? ',' + new Date(health.retryAt).toLocaleTimeString() + ' 恢复候选资格;可提前实测'
520
+ : ',实测成功后恢复')
521
+ : (status === 'failed' && health && ['authentication', 'auth', 'permission'].indexOf(health.category) >= 0
522
+ ? '认证失败' : HEALTH_META[status].label);
449
523
  var age = relativeHealthTime(health && health.observedAt);
450
524
  return text + (age ? ' · ' + age : '');
451
525
  }
@@ -462,9 +536,9 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
462
536
  return React.createElement('div', { className: 'iris-health-grid' }, rows.map(function (row) {
463
537
  var health = capabilityHealth(state, row[0]);
464
538
  var meta = HEALTH_META[health.status] || HEALTH_META.unconfigured;
465
- return React.createElement('div', { key: row[0], className: 'iris-health-row ' + health.status },
539
+ return React.createElement('div', { key: row[0], className: 'iris-health-row ' + healthClass(health) },
466
540
  React.createElement('span', { className: 'iris-health-name' },
467
- React.createElement('span', { className: 'iris-pm-dot ' + health.status }, meta.symbol),
541
+ React.createElement('span', { className: 'iris-pm-dot ' + healthClass(health) }, health.retryAt ? '◷' : meta.symbol),
468
542
  row[1]),
469
543
  React.createElement('span', { className: 'iris-health-state', title: healthText(health) }, healthText(health)),
470
544
  React.createElement('span', { className: 'iris-wb-muted' }, String(health.candidateCount || 0) + ' 路'));
@@ -743,12 +817,16 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
743
817
  var newId = addPair[0];
744
818
  var setNewId = addPair[1];
745
819
  var models = p.models || [];
820
+ function setImageProtocol(m, value) {
821
+ postAction('providers_set_model_image_protocol', { id: p.id, model_id: m.id, imageProtocol: value })
822
+ .then(function (r) { props.onNote(r.ok ? '模型图片协议已更新' : (r.d.error || '更新失败')); if (r.ok) props.onDone(); });
823
+ }
746
824
  function verBadge(m, c) {
747
825
  var health = m.health && m.health[c];
748
826
  var status = health && HEALTH_META[health.status] ? health.status : 'configured';
749
- var cls = 'iris-pm-ver ' + status;
827
+ var cls = 'iris-pm-ver ' + status + (health && health.rateLimited ? ' ' + healthClass(health) : '');
750
828
  var title = healthText(health || { status: status });
751
- var symbol = status === 'verified' ? '✓' : (status === 'failed' ? '×' : (status === 'unconfigured' ? '○' : '?'));
829
+ var symbol = health && health.retryAt ? '◷' : status === 'verified' ? '✓' : (status === 'failed' ? '×' : (status === 'unconfigured' ? '○' : '?'));
752
830
  return React.createElement('button', {
753
831
  key: c,
754
832
  className: cls,
@@ -758,9 +836,22 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
758
836
  }
759
837
  var rows = models.map(function (m) {
760
838
  var caps = m.capabilities || [];
839
+ var blocked = caps.map(function (c) { return m.health && m.health[c]; }).find(function (health) { return health && health.rateLimited; });
761
840
  return React.createElement('div', { key: m.id, className: 'iris-pm-mrow' },
762
841
  React.createElement('span', { className: 'iris-pm-mname' }, m.id, m.source === 'manual' ? React.createElement('span', { className: 'iris-pm-mtag' }, '手动') : null),
763
- React.createElement('span', { className: 'iris-pm-mcaps' }, caps.map(function (c) { return verBadge(m, c); })),
842
+ React.createElement('span', { className: 'iris-pm-mcaps' }, caps.length ? caps.map(function (c) { return verBadge(m, c); }) : '未标注功能'),
843
+ React.createElement(ModelCapabilityEditor, { provider: p, model: m, onNote: props.onNote, onDone: props.onDone }),
844
+ caps.includes('image-gen') ? React.createElement('label', { className: 'iris-pm-note' }, '图片协议 ',
845
+ React.createElement('select', { value: m.imageProtocol || 'auto', onChange: function (e) { setImageProtocol(m, e.target.value); } },
846
+ React.createElement('option', { value: 'auto' }, '继承账号默认(' + (p.mediaProtocol || '自动') + ')'),
847
+ React.createElement('option', { value: 'dashscope' }, 'DashScope'),
848
+ React.createElement('option', { value: 'openai-images' }, 'OpenAI Images'),
849
+ React.createElement('option', { value: 'openai-chat-images' }, '聊天生图(网关兼容)'),
850
+ React.createElement('option', { value: 'openai-responses-images' }, 'Responses 生图')),
851
+ m.imageRouting && m.imageRouting.protocolInferred ? ' · 协议待确认' : null) : null,
852
+ 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,
853
+ onNote: props.onNote, onDone: props.onDone }) : null,
854
+ blocked ? React.createElement('span', { className: 'iris-pm-ver failed ' + healthClass(blocked), title: healthText(blocked) }, rateLimitLabel(blocked)) : null,
764
855
  React.createElement('button', { className: 'iris-pm-btn danger', title: '从池移除', onClick: function () { props.removeModel(p, m); } }, '✕'));
765
856
  });
766
857
  return React.createElement('div', { className: 'iris-pm-pool' },
@@ -771,6 +862,75 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
771
862
  React.createElement('button', { className: 'iris-pm-btn', onClick: function () { props.addModel(p, newId.trim()); setNewId(''); } }, '+ 添加')));
772
863
  }
773
864
 
865
+ function ModelCapabilityEditor(props) {
866
+ var openPair = React.useState(false);
867
+ var capsPair = React.useState([]);
868
+ var savingPair = React.useState(false);
869
+ var selected = capsPair[0];
870
+ var saving = savingPair[0];
871
+ function save() {
872
+ if (saving) return;
873
+ savingPair[1](true);
874
+ postAction('providers_set_model_caps', { id: props.provider.id, model_id: props.model.id, capabilities: selected })
875
+ .then(function (r) {
876
+ if (r.ok) {
877
+ openPair[1](false);
878
+ props.onNote('模型功能标签已保存');
879
+ props.onDone();
880
+ } else props.onNote(r.d.error || '保存失败');
881
+ }).finally(function () { savingPair[1](false); });
882
+ }
883
+ if (!openPair[0]) return React.createElement('button', { className: 'iris-pm-btn', title: '设置此模型可用于哪些功能',
884
+ onClick: function () { capsPair[1]((props.model.capabilities || []).slice()); openPair[1](true); } }, '设置功能');
885
+ return React.createElement('div', { className: 'iris-wb-row' },
886
+ CAP_ROWS.map(function (row) {
887
+ return React.createElement('label', { key: row.cap, className: 'iris-pm-note' },
888
+ React.createElement('input', { type: 'checkbox', value: row.cap, checked: selected.includes(row.cap), disabled: saving,
889
+ onChange: function (e) {
890
+ var checked = e.target.checked;
891
+ capsPair[1](function (previous) {
892
+ return checked ? previous.filter(function (cap) { return cap !== row.cap; }).concat(row.cap)
893
+ : previous.filter(function (cap) { return cap !== row.cap; });
894
+ });
895
+ } }), row.label);
896
+ }),
897
+ React.createElement('button', { className: 'iris-pm-btn', disabled: saving, onClick: save }, saving ? '保存中…' : '保存功能'),
898
+ React.createElement('button', { className: 'iris-pm-btn', disabled: saving, onClick: function () { openPair[1](false); } }, '取消'));
899
+ }
900
+
901
+ function VisionInputEditor(props) {
902
+ var p = props.provider;
903
+ var m = props.model;
904
+ var limits = (m ? m.visionInput : p.visionInput) || {};
905
+ var bytesPair = React.useState(limits.maxBytes === undefined ? '' : String(limits.maxBytes / 1048576));
906
+ var dimensionPair = React.useState(limits.maxDimension === undefined ? '' : String(limits.maxDimension));
907
+ React.useEffect(function () {
908
+ bytesPair[1](limits.maxBytes === undefined ? '' : String(limits.maxBytes / 1048576));
909
+ dimensionPair[1](limits.maxDimension === undefined ? '' : String(limits.maxDimension));
910
+ }, [limits.maxBytes, limits.maxDimension]);
911
+ function save(reset) {
912
+ var value = {};
913
+ if (bytesPair[0] !== '') value.maxBytes = Number(bytesPair[0]) * 1048576;
914
+ if (dimensionPair[0] !== '') value.maxDimension = Number(dimensionPair[0]);
915
+ if (!reset && Object.values(value).some(function (number) { return !Number.isSafeInteger(number) || number <= 0; })) {
916
+ props.onNote('请输入正数 MiB 和正整数像素'); return;
917
+ }
918
+ var action = m ? 'providers_set_model_vision_input' : 'providers_upsert';
919
+ postAction(action, { id: p.id, ...(m ? { model_id: m.id } : {}), visionInput: reset ? null : value })
920
+ .then(function (r) { props.onNote(r.ok ? '视觉输入预算已更新' : (r.d.error || '更新失败')); if (r.ok) props.onDone(); });
921
+ }
922
+ return React.createElement('div', { className: 'iris-pm-field' },
923
+ React.createElement('label', {}, m ? '模型视觉预算(留空继承账号)' : '账号视觉预算(留空使用默认 8 MiB)'),
924
+ React.createElement('div', { className: 'iris-wb-row' },
925
+ React.createElement('input', { type: 'number', min: '0', step: 'any', 'aria-label': '视觉大小上限 MiB',
926
+ placeholder: 'MiB', value: bytesPair[0], onChange: function (e) { bytesPair[1](e.target.value); } }),
927
+ React.createElement('input', { type: 'number', min: '1', step: '1', 'aria-label': '视觉最长边像素',
928
+ placeholder: '最长边 px(可选)', value: dimensionPair[0], onChange: function (e) { dimensionPair[1](e.target.value); } }),
929
+ React.createElement('button', { className: 'iris-pm-btn', onClick: function () { save(false); } }, '保存预算'),
930
+ React.createElement('button', { className: 'iris-pm-btn', onClick: function () { save(true); } }, '恢复继承')),
931
+ React.createElement('span', { className: 'iris-pm-note' }, '用于看图、OCR 每块输入和定位。作品原图保留,定位结果换算回原图。'));
932
+ }
933
+
774
934
  function ProviderEndpointEditor(props) {
775
935
  var p = props.provider;
776
936
  var valuePair = React.useState(p.mediaBaseUrl || '');
@@ -807,7 +967,11 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
807
967
  function refresh() {
808
968
  postAction('providers_list', {}).then(function (r) { if (r.ok && r.d.providers) setList(r.d.providers); });
809
969
  }
810
- React.useEffect(function () { refresh(); }, []);
970
+ React.useEffect(function () {
971
+ refresh();
972
+ var timer = setInterval(refresh, 5000);
973
+ return function () { clearInterval(timer); };
974
+ }, []);
811
975
  function addProvider() {
812
976
  var f = newForm[0];
813
977
  if (!f.baseUrl || !f.apiKey) { setNote('baseUrl 和 apiKey 必填'); return; }
@@ -863,11 +1027,14 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
863
1027
  React.createElement('div', { className: 'iris-pm-note' }, 'Key ' + (p.apiKeyHint || '未配置') + ' · ' + (p.type || '') + ' · ' + (p.mediaProtocol || '')),
864
1028
  p.protocolInferred ? React.createElement('div', { className: 'iris-pm-note' }, '协议为推断值,请确认') : null,
865
1029
  React.createElement(ProviderEndpointEditor, { provider: p, onNote: setNote, onDone: refresh }),
1030
+ React.createElement(VisionInputEditor, { provider: p, onNote: setNote, onDone: refresh }),
866
1031
  React.createElement('div', { className: 'iris-pm-field' }, React.createElement('label', {}, '媒体协议'), React.createElement('select', { value: p.protocolInferred ? 'auto' : (p.mediaProtocol || 'auto'), onChange: function (e) { setProtocol(p, e.target.value); } },
867
1032
  React.createElement('option', { value: 'auto' }, '自动'),
868
- p.mediaProtocol && !['dashscope', 'openai-images', 'auto'].includes(p.mediaProtocol) ? React.createElement('option', { value: p.mediaProtocol }, p.mediaProtocol + '(未支持)') : null,
1033
+ p.mediaProtocol && !['dashscope', 'openai-images', 'openai-chat-images', 'openai-responses-images', 'auto'].includes(p.mediaProtocol) ? React.createElement('option', { value: p.mediaProtocol }, p.mediaProtocol + '(未支持)') : null,
869
1034
  React.createElement('option', { value: 'dashscope' }, 'DashScope(仅阿里云官方 HTTPS)'),
870
- React.createElement('option', { value: 'openai-images' }, 'OpenAI Images 兼容'))),
1035
+ React.createElement('option', { value: 'openai-images' }, 'OpenAI Images 兼容'),
1036
+ React.createElement('option', { value: 'openai-chat-images' }, '聊天生图(网关兼容)'),
1037
+ React.createElement('option', { value: 'openai-responses-images' }, 'Responses 生图'))),
871
1038
  React.createElement('div', { className: 'iris-pm-note' }, '能力: ' + ((p.capabilities || []).join(' / ') || '无')),
872
1039
  React.createElement(ModelPool, { provider: p, onNote: setNote, onDone: refresh, testModel: testModel, removeModel: removeModel, addModel: addModel }),
873
1040
  React.createElement('div', { className: 'iris-wb-row' },
@@ -891,7 +1058,9 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
891
1058
  React.createElement('div', { className: 'iris-pm-field' }, React.createElement('label', {}, '媒体协议'), React.createElement('select', { value: newForm[0].mediaProtocol, onChange: function (e) { newForm[1]({ ...newForm[0], mediaProtocol: e.target.value }); } },
892
1059
  React.createElement('option', { value: 'auto' }, '自动(按媒体 Base URL 安全判断)'),
893
1060
  React.createElement('option', { value: 'dashscope' }, 'DashScope(阿里云官方)'),
894
- React.createElement('option', { value: 'openai-images' }, 'OpenAI Images 兼容'))),
1061
+ React.createElement('option', { value: 'openai-images' }, 'OpenAI Images 兼容'),
1062
+ React.createElement('option', { value: 'openai-chat-images' }, '聊天生图(网关兼容)'),
1063
+ React.createElement('option', { value: 'openai-responses-images' }, 'Responses 生图'))),
895
1064
  React.createElement('div', { className: 'iris-wb-row' },
896
1065
  React.createElement('button', { className: 'iris-act-run', onClick: addProvider }, '保存'),
897
1066
  React.createElement('button', { className: 'iris-pm-btn', onClick: function () { setAdding(false); } }, '取消')));
@@ -1062,7 +1231,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1062
1231
  var shown = (vals.model && vals.model.trim()) || (modelInfo && modelInfo.assigned) || '';
1063
1232
  modelBar = React.createElement('div', { className: 'iris-act-field' },
1064
1233
  React.createElement('label', {},
1065
- React.createElement('span', { className: 'iris-pm-dot ' + cardHealth.status }, HEALTH_META[cardHealth.status].symbol),
1234
+ React.createElement('span', { className: 'iris-pm-dot ' + healthClass(cardHealth) }, cardHealth.retryAt ? '◷' : HEALTH_META[cardHealth.status].symbol),
1066
1235
  ' 模型:' + (shown || '自动') + (available ? '' : '(无可用模型)')),
1067
1236
  React.createElement('div', { className: 'iris-wb-muted' },
1068
1237
  healthText(cardHealth) + ' · 改分配 → 上方「能力分配(failover 顺序)」'));
@@ -1082,9 +1251,9 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1082
1251
  React.createElement('div', { className: 'iris-act-head', onClick: function () { setOpen(!open); } },
1083
1252
  React.createElement('span', { className: 'iris-act-title' }, title),
1084
1253
  capability ? React.createElement('span', {
1085
- className: 'iris-pm-dot ' + headStatus,
1254
+ className: 'iris-pm-dot ' + healthClass(cardHealth),
1086
1255
  title: headTitle
1087
- }, HEALTH_META[headStatus].symbol) : null,
1256
+ }, cardHealth.retryAt ? '◷' : HEALTH_META[headStatus].symbol) : null,
1088
1257
  React.createElement('span', { className: 'iris-act-arrow' }, '▶')),
1089
1258
  body);
1090
1259
  }
@@ -1328,7 +1497,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1328
1497
 
1329
1498
  function coreGalleryItems(data, limit) {
1330
1499
  return (((data && data.artifacts && data.artifacts.recent) || []).filter(function (artifact) {
1331
- return artifact.kind !== 'host-input' && String(artifact.mediaType || '').indexOf('image/') === 0;
1500
+ return artifact.kind !== 'host-input';
1332
1501
  }).map(function (artifact) {
1333
1502
  return {
1334
1503
  id: artifact.id, file: artifact.id, mime: artifact.mediaType,
@@ -1364,99 +1533,458 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1364
1533
  return pair[0];
1365
1534
  }
1366
1535
 
1367
- function ArtifactGallery(props) {
1368
- var itemsPair = React.useState([]);
1369
- var items = itemsPair[0];
1370
- var setItems = itemsPair[1];
1371
- var totalPair = React.useState(Number(props.total || 0));
1372
- var total = totalPair[0];
1373
- var setTotal = totalPair[1];
1374
- var corePair = React.useState([]);
1375
- var coreItems = corePair[0];
1376
- var setCoreItems = corePair[1];
1377
- var busyPair = React.useState(false);
1378
- var busy = busyPair[0];
1379
- var setBusy = busyPair[1];
1536
+ function galleryKindLabel(kind) {
1537
+ return ({ __proto__: null, 'generated-image': '生成图片', 'generated-video': '生成视频', 'generated-audio': '生成音频',
1538
+ 'tts-audio': '合成语音', transcript: '转写文本', 'video-frame': '视频帧', crop: '裁剪图片',
1539
+ 'cropped-image': '裁剪图片', 'pixel-diff': '差异热图', 'diff-heatmap': '差异热图', 'html-screenshot': 'HTML 截图',
1540
+ 'legacy-output': '旧版作品' })[kind] || kind;
1541
+ }
1542
+
1543
+ function fetchWorkbenchJson(url, input, signal) {
1544
+ var options = { signal: signal };
1545
+ if (input) { options.method = 'POST'; options.headers = { 'Content-Type': 'application/json' }; options.body = JSON.stringify(input); }
1546
+ return fetch(url, options).then(function (res) { return res.json().then(function (data) {
1547
+ if (!res.ok || data.ok === false) throw new Error((data.error || '作品操作失败') + (data.transactionId ? '(事务 ' + data.transactionId + ')' : ''));
1548
+ return data;
1549
+ }); });
1550
+ }
1551
+ function copyWorkbenchText(text, setNote) {
1552
+ if (!navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') { setNote('请手动选择并复制 ID:\n' + text); return; }
1553
+ navigator.clipboard.writeText(text).then(function () { setNote('已复制'); }).catch(function () { setNote('请手动选择并复制 ID:\n' + text); });
1554
+ }
1555
+ function saveWorkbenchBlob(blob, name) {
1556
+ var url = URL.createObjectURL(blob), link = document.createElement('a');
1557
+ link.href = url; link.download = name; link.click();
1558
+ setTimeout(function () { URL.revokeObjectURL(url); }, 0);
1559
+ }
1560
+ function deletionBlockerLabel(blocker) {
1561
+ return ({ __proto__: null, task_not_settled: '任务尚未结束或结果未知', task_artifact_reference: '被任务记录引用', task_input_reference: '被改图任务作为原图引用',
1562
+ artifact_task_reference: '该任务仍关联其他作品', retry_reference: '被后续重试任务引用',
1563
+ artifact_relation: '被其他作品引用', uncommitted_relation: '被尚未提交的产物引用' })[blocker.reason] || '仍有引用';
1564
+ }
1565
+
1566
+ function WorkbenchManagement(props) {
1567
+ var type = props.type;
1568
+ var dataPair = React.useState(null), data = dataPair[0], setData = dataPair[1];
1569
+ var busyPair = React.useState(true), busy = busyPair[0], setBusy = busyPair[1];
1570
+ var writingPair = React.useState(false), writing = writingPair[0], setWriting = writingPair[1];
1571
+ var notePair = React.useState(''), note = notePair[0], setNote = notePair[1];
1572
+ var tasksPair = React.useState([]), taskIds = tasksPair[0], setTaskIds = tasksPair[1];
1573
+ var request = React.useRef(null), view = React.useRef(null);
1574
+ var artifactIds = (props.items || []).map(function (item) { return item.id; });
1575
+ function query(tasks) {
1576
+ if (request.current) request.current.abort();
1577
+ var controller = new AbortController(); request.current = controller; setBusy(true); setNote('');
1578
+ var url = type === 'details' ? '/iris/api/works/' + props.item.source + '/' + props.item.id
1579
+ : type === 'transactions' ? '/iris/api/works/transactions' : '/iris/api/works/delete';
1580
+ fetchWorkbenchJson(url, type === 'delete' ? { artifact_ids: artifactIds, task_ids: tasks || [] } : null, controller.signal)
1581
+ .then(function (value) { if (request.current === controller && !controller.signal.aborted) setData(value); })
1582
+ .catch(function (error) { if (request.current === controller && !controller.signal.aborted) { setData(null); setNote(error.message); } })
1583
+ .finally(function () { if (request.current === controller) { request.current = null; setBusy(false); } });
1584
+ }
1585
+ React.useEffect(function () {
1586
+ query([]);
1587
+ if (view.current) view.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
1588
+ return function () { if (request.current) request.current.abort(); request.current = null; };
1589
+ }, []);
1590
+ function chooseTask(id, checked) {
1591
+ var next = checked ? taskIds.concat(id) : taskIds.filter(function (value) { return value !== id; });
1592
+ setTaskIds(next); query(next);
1593
+ }
1594
+ function execute(transaction) {
1595
+ var restoring = type === 'transactions';
1596
+ var message = restoring ? '恢复事务 ' + transaction.transactionId + ' 中的 ' + transaction.files + ' 个文件?已有文件不会被覆盖。'
1597
+ : '将 ' + artifactIds.length + ' 个 Core 作品' + (taskIds.length ? '及 ' + taskIds.length + ' 个已结束任务记录' : '') + '移入回收区?之后可以恢复。';
1598
+ if (!window.confirm(message)) return;
1599
+ var controller = new AbortController(); request.current = controller;
1600
+ setWriting(true); setBusy(true); setNote(''); props.onBusy(true);
1601
+ fetchWorkbenchJson('/iris/api/works/' + (restoring ? 'restore' : 'delete'), restoring
1602
+ ? { transaction_id: transaction.transactionId, confirm_restore: true }
1603
+ : { artifact_ids: artifactIds, task_ids: taskIds, confirm_delete: true }, controller.signal)
1604
+ .then(function (value) {
1605
+ if (request.current !== controller || controller.signal.aborted) return;
1606
+ props.onChanged(value);
1607
+ if (restoring) query([]); else props.onClose();
1608
+ })
1609
+ .catch(function (error) { if (request.current === controller && !controller.signal.aborted) setNote(error.message); })
1610
+ .finally(function () {
1611
+ props.onBusy(false); setWriting(false);
1612
+ if (request.current === controller) { request.current = null; setBusy(false); }
1613
+ });
1614
+ }
1615
+ var item = data && data.item;
1616
+ var titles = { details: '作品详情', delete: '删除预览', transactions: 'Core 回收区' };
1617
+ return React.createElement('section', { ref: view, className: 'iris-gallery-management', 'aria-label': titles[type], 'aria-busy': busy },
1618
+ React.createElement('div', { className: 'iris-gallery-analysis-head' }, React.createElement('strong', {}, titles[type]),
1619
+ React.createElement('button', { type: 'button', className: 'iris-pm-btn', disabled: writing, onClick: props.onClose }, '关闭面板')),
1620
+ busy ? React.createElement('div', { role: 'status', className: 'iris-wb-muted' }, writing ? '正在处理…' : '正在读取…') : null,
1621
+ item ? React.createElement('dl', {}, [
1622
+ ['ID', item.id], ['来源', item.source === 'core' ? 'Core' : '旧版'], ['文件名', item.file], ['媒体类型', item.mime],
1623
+ ['产物类型', galleryKindLabel(item.kind)], ['大小', fmtBytes(item.size)], ['创建时间', fmtTime(item.createdAt)],
1624
+ ['内容核验', item.integrity === 'verified' ? 'SHA-256 已核验' : '文件存在;旧版没有已存哈希'],
1625
+ ['SHA-256', item.digest], ['关联', (item.relations || []).map(function (edge) { return edge.type + ' → ' + edge.artifactId; }).join('\n')],
1626
+ ['元数据', item.metadata && Object.keys(item.metadata).length ? JSON.stringify(item.metadata, null, 2) : '']
1627
+ ].filter(function (row) { return !!row[1]; }).map(function (row) {
1628
+ return [React.createElement('dt', { key: row[0] + '-label' }, row[0]), React.createElement('dd', { key: row[0] }, row[1])];
1629
+ })) : null,
1630
+ item ? React.createElement('div', { className: 'iris-gallery-actions' },
1631
+ React.createElement('a', { className: 'iris-wb-link', href: item.url, target: '_blank', rel: 'noreferrer' }, '打开原文件'),
1632
+ React.createElement('button', { className: 'iris-pm-btn', type: 'button', onClick: function () { copyWorkbenchText(item.id, setNote); } }, '复制 ID')) : null,
1633
+ type === 'delete' && data ? React.createElement('div', { className: 'iris-gallery-management-list' },
1634
+ React.createElement('div', {}, '选中 ' + artifactIds.length + ' 个作品 · ' + data.files + ' 个文件 · ' + fmtBytes(data.bytes)),
1635
+ artifactIds.map(function (id) { return React.createElement('div', { key: id }, id); }),
1636
+ (data.referencingTasks || []).length ? React.createElement('div', { className: 'iris-wb-muted' }, '如需同时移除关联任务记录,请逐项勾选;未结束任务及其他引用会阻止删除。') : null,
1637
+ (data.referencingTasks || []).map(function (task) { return React.createElement('label', { key: task.id, className: 'iris-gallery-select' },
1638
+ React.createElement('input', { type: 'checkbox', 'aria-label': '关联任务 ' + task.id, checked: taskIds.indexOf(task.id) >= 0,
1639
+ disabled: busy || !task.settled, onChange: function (event) { chooseTask(task.id, event.target.checked); } }),
1640
+ '同时移除任务记录 ' + task.id + (task.settled ? '' : '(未结束或结果未知)')); }),
1641
+ (data.blockers || []).map(function (blocker, index) { return React.createElement('div', { key: index, className: 'iris-wb-muted' },
1642
+ deletionBlockerLabel(blocker) + ':' + blocker.id + (blocker.referencedBy ? ' ← ' + blocker.referencedBy : '')); })) : null,
1643
+ type === 'delete' ? React.createElement('div', { className: 'iris-gallery-actions' },
1644
+ React.createElement('button', { className: 'iris-pm-btn danger', type: 'button', disabled: busy || !data || !data.allowed,
1645
+ onClick: function () { execute(); } }, '确认移入回收区'),
1646
+ React.createElement('button', { className: 'iris-pm-btn', type: 'button', disabled: busy, onClick: function () { query(taskIds); } }, '重新预览')) : null,
1647
+ type === 'transactions' && data ? React.createElement('div', { className: 'iris-gallery-management-list' },
1648
+ data.transactions.length ? data.transactions.map(function (transaction) {
1649
+ var states = { __proto__: null, prepared: '准备中', committed: '已隔离', interrupted: '操作中断', restored: '已恢复', 'rolled-back': '已回退' };
1650
+ return React.createElement('div', { key: transaction.transactionId, className: 'iris-gallery-transaction' },
1651
+ React.createElement('div', {}, transaction.transactionId),
1652
+ React.createElement('div', { className: 'iris-wb-muted' }, (transaction.operation === 'cleanup' ? '清理' : '删除') + ' · '
1653
+ + (states[transaction.state] || transaction.state) + ' · ' + transaction.files + ' 文件 · ' + fmtTime(transaction.createdAt)),
1654
+ React.createElement('button', { type: 'button', className: 'iris-pm-btn',
1655
+ disabled: busy || ['prepared', 'committed', 'interrupted'].indexOf(transaction.state) < 0,
1656
+ onClick: function () { execute(transaction); } }, '恢复'));
1657
+ }) : React.createElement('div', { className: 'iris-wb-muted' }, '回收区为空。')) : null,
1658
+ type === 'transactions' ? React.createElement('button', { type: 'button', className: 'iris-pm-btn', disabled: busy, onClick: function () { query([]); } }, '刷新回收区') : null,
1659
+ note ? React.createElement('div', { role: 'status', className: 'iris-clean-note', style: { whiteSpace: 'pre-wrap' } }, note) : null);
1660
+ }
1661
+
1662
+ function ArtifactAnalysis(props) {
1663
+ var item = props.item, action = props.action;
1664
+ var resultPair = React.useState(null), result = resultPair[0], setResult = resultPair[1];
1665
+ var optionsPair = React.useState({ question: '', chunk_height: '1200', overlap: '120' });
1666
+ var options = optionsPair[0], setOptions = optionsPair[1];
1667
+ var notePair = React.useState(''), note = notePair[0], setNote = notePair[1];
1668
+ var request = React.useRef(null);
1669
+ var view = React.useRef(null);
1670
+ var running = result && result.status === 'running';
1671
+ var text = result && result.text || '';
1672
+ function execute() {
1673
+ if (request.current) request.current.abort();
1674
+ var controller = new AbortController();
1675
+ request.current = controller;
1676
+ var input = { artifact_id: item.id };
1677
+ if (action === 'look') input.question = options.question;
1678
+ else {
1679
+ input.chunk_height = Number(options.chunk_height);
1680
+ input.overlap = Number(options.overlap);
1681
+ }
1682
+ setResult({ status: 'running', text: '' }); setNote('');
1683
+ fetch('/iris/api/actions/' + action, { method: 'POST', headers: { 'Content-Type': 'application/json' },
1684
+ body: JSON.stringify(input), signal: controller.signal })
1685
+ .then(function (res) { return res.json().then(function (data) { return { ok: res.ok, data: data }; }); })
1686
+ .then(function (response) {
1687
+ if (request.current !== controller || controller.signal.aborted) return;
1688
+ var data = response.data;
1689
+ setResult({ status: !response.ok || data.ok === false ? 'failed' : (data.status || 'complete'),
1690
+ text: data.text || data.error || '未返回识别结果' });
1691
+ })
1692
+ .catch(function (error) {
1693
+ if (request.current !== controller || controller.signal.aborted) return;
1694
+ setResult({ status: 'failed', text: '识别请求失败:' + error.message });
1695
+ })
1696
+ .finally(function () { if (request.current === controller) request.current = null; });
1697
+ }
1698
+ function cancel() {
1699
+ if (request.current) request.current.abort();
1700
+ request.current = null;
1701
+ setResult({ status: 'canceled', text: '' });
1702
+ }
1703
+ React.useEffect(function () {
1704
+ execute();
1705
+ if (view.current) view.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
1706
+ return function () { if (request.current) request.current.abort(); request.current = null; };
1707
+ }, []);
1708
+ function option(key, value) { setOptions(function (current) { var next = Object.assign({}, current); next[key] = value; return next; }); }
1709
+ function copy() {
1710
+ if (!navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') { setNote('请选中下方文字手动复制'); return; }
1711
+ navigator.clipboard.writeText(text).then(function () { setNote('已复制识别结果'); }).catch(function () { setNote('复制失败,请手动选择文字'); });
1712
+ }
1713
+ function download() {
1714
+ var url = URL.createObjectURL(new Blob([text], { type: 'text/plain;charset=utf-8' }));
1715
+ var link = document.createElement('a'); link.href = url; link.download = item.id + '-' + action + '.txt'; link.click();
1716
+ setTimeout(function () { URL.revokeObjectURL(url); }, 0);
1717
+ }
1718
+ var status = result && result.status || 'running';
1719
+ var labels = { running: '识别中…', complete: '完成', partial: '部分完成', failed: '失败', canceled: '已取消' };
1720
+ return React.createElement('section', { ref: view, className: 'iris-gallery-analysis', 'aria-label': '作品识别', 'aria-busy': !!running },
1721
+ artifactPreview(item),
1722
+ React.createElement('div', { className: 'iris-gallery-analysis-body' },
1723
+ React.createElement('div', { className: 'iris-gallery-analysis-head' },
1724
+ React.createElement('strong', {}, action === 'look' ? '看图结果' : 'OCR 结果'),
1725
+ React.createElement('span', { role: 'status', className: 'iris-wb-badge ' + (status === 'partial' ? 'warn' : status === 'failed' ? 'err' : status === 'complete' ? 'ok' : '') }, labels[status]),
1726
+ running ? React.createElement('button', { className: 'iris-pm-btn', type: 'button', onClick: cancel }, '取消识别') : null,
1727
+ React.createElement('button', { className: 'iris-pm-btn', type: 'button', onClick: props.onClose }, '关闭结果')),
1728
+ React.createElement('details', {}, React.createElement('summary', {}, '更多选项'),
1729
+ React.createElement('div', { className: 'iris-gallery-analysis-options' }, action === 'look'
1730
+ ? React.createElement('label', {}, '看图问题', React.createElement('input', { value: options.question, placeholder: '默认:详细描述这张图片', onChange: function (e) { option('question', e.target.value); } }))
1731
+ : ['chunk_height', 'overlap'].map(function (key) {
1732
+ return React.createElement('label', { key: key }, key === 'chunk_height' ? '分块高度' : '重叠像素',
1733
+ React.createElement('input', { type: 'number', min: key === 'chunk_height' ? 100 : 0, value: options[key], onChange: function (e) { option(key, e.target.value); } }));
1734
+ }),
1735
+ React.createElement('button', { className: 'iris-clean-btn', type: 'button', disabled: !!running, onClick: execute }, '按当前选项重新识别'))),
1736
+ text ? React.createElement('textarea', { 'aria-label': '识别结果', readOnly: true, value: text }) : null,
1737
+ text ? React.createElement('div', { className: 'iris-gallery-actions' },
1738
+ React.createElement('button', { className: 'iris-pm-btn', type: 'button', onClick: copy }, '复制文本'),
1739
+ React.createElement('button', { className: 'iris-pm-btn', type: 'button', onClick: download }, '下载文本')) : null,
1740
+ note ? React.createElement('div', { role: 'status', className: 'iris-clean-note' }, note) : null));
1741
+ }
1742
+
1743
+ function ArtifactImageEdit(props) {
1744
+ var item = props.item;
1745
+ var promptPair = React.useState(''), prompt = promptPair[0], setPrompt = promptPair[1];
1746
+ var modelPair = React.useState(''), model = modelPair[0], setModel = modelPair[1];
1747
+ var modelsPair = React.useState(null), models = modelsPair[0], setModels = modelsPair[1];
1748
+ var resultPair = React.useState(null), result = resultPair[0], setResult = resultPair[1];
1749
+ var request = React.useRef(null);
1750
+ var running = result && result.status === 'running';
1751
+ React.useEffect(function () {
1752
+ var active = true;
1753
+ postAction('providers_list', {}).then(function (response) {
1754
+ if (!active) return;
1755
+ if (!response.ok || !response.d.ok) throw new Error('模型列表读取失败');
1756
+ var choices = [];
1757
+ (response.d.providers || []).forEach(function (provider) {
1758
+ if (!provider.enabled) return;
1759
+ (provider.models || []).forEach(function (entry) {
1760
+ if ((entry.capabilities || []).indexOf('image-gen') < 0
1761
+ || !entry.imageRouting || entry.imageRouting.mediaProtocol !== 'openai-chat-images') return;
1762
+ var health = entry.health && entry.health['image-gen'];
1763
+ choices.push({ id: provider.id + '::' + entry.id, label: provider.name + ' · ' + entry.id,
1764
+ blocked: !!(health && health.rateLimited) });
1765
+ });
1766
+ });
1767
+ setModels(choices);
1768
+ }).catch(function (error) { if (active) { setModels([]); setResult({ status: 'failed', text: error.message }); } });
1769
+ return function () { active = false; if (request.current) request.current.abort(); request.current = null; };
1770
+ }, []);
1771
+ function execute() {
1772
+ if (request.current || !prompt.trim()) return;
1773
+ var controller = new AbortController(); request.current = controller;
1774
+ setResult({ status: 'running', text: '' });
1775
+ fetch('/iris/api/actions/image_edit', { method: 'POST', headers: { 'Content-Type': 'application/json' },
1776
+ body: JSON.stringify({ source_artifact_id: item.id, prompt: prompt.trim(), ...(model ? { model: model } : {}) }), signal: controller.signal })
1777
+ .then(function (res) { return res.json().then(function (data) { return { ok: res.ok && data.ok !== false, data: data }; }); })
1778
+ .then(function (response) {
1779
+ if (request.current !== controller || controller.signal.aborted) return;
1780
+ var data = response.data;
1781
+ setResult({ status: response.ok ? 'complete' : 'failed', text: data.text || data.error || '未返回编辑结果',
1782
+ artifactIds: response.ok ? data.artifactIds || [] : [], taskId: data.taskId });
1783
+ if (response.ok) window.dispatchEvent(new CustomEvent('iris-core-artifacts-changed'));
1784
+ })
1785
+ .catch(function (error) {
1786
+ if (request.current === controller && !controller.signal.aborted) setResult({ status: 'failed', text: '改图请求失败:' + error.message + '。请在任务区检查结果。' });
1787
+ })
1788
+ .finally(function () { if (request.current === controller) request.current = null; });
1789
+ }
1790
+ function cancel() {
1791
+ if (request.current) request.current.abort(); request.current = null;
1792
+ setResult({ status: 'canceled', text: '已停止等待,请在任务区检查是否已生成。不会自动重新提交。' });
1793
+ }
1794
+ return React.createElement('section', { className: 'iris-gallery-analysis', 'aria-label': '作品改图', 'aria-busy': !!running },
1795
+ artifactPreview(item),
1796
+ React.createElement('div', { className: 'iris-gallery-analysis-body' },
1797
+ React.createElement('div', { className: 'iris-gallery-analysis-head' }, React.createElement('strong', {}, '编辑图片'),
1798
+ React.createElement('button', { className: 'iris-pm-btn', type: 'button', onClick: props.onClose }, '关闭改图')),
1799
+ React.createElement('div', { className: 'iris-wb-muted' }, '原图保留,修改版会保存为新作品。此操作会调用生图模型。'),
1800
+ React.createElement('label', {}, '修改指令', React.createElement('textarea', { 'aria-label': '修改指令', value: prompt, disabled: !!running,
1801
+ placeholder: '例如:把蓝色发饰改成白色,保留人物、服装和构图', onChange: function (e) { setPrompt(e.target.value); } })),
1802
+ React.createElement('label', {}, '改图模型', React.createElement('select', { 'aria-label': '改图模型', value: model, disabled: !!running,
1803
+ onChange: function (e) { setModel(e.target.value); } }, React.createElement('option', { value: '' }, '按当前顺序选择可用改图模型'),
1804
+ (models || []).map(function (entry) { return React.createElement('option', { key: entry.id, value: entry.id, disabled: entry.blocked }, entry.label + (entry.blocked ? ' · 冷却或额度耗尽' : '')); }))),
1805
+ models && !models.some(function (entry) { return !entry.blocked; }) ? React.createElement('div', { role: 'status' }, '暂无可用改图模型,请在模型设置中配置聊天生图协议,或手动实测恢复模型。') : null,
1806
+ React.createElement('div', { className: 'iris-gallery-actions' },
1807
+ React.createElement('button', { className: 'iris-clean-btn', type: 'button', disabled: !!running || !prompt.trim() || !models || !models.some(function (entry) { return !entry.blocked; }), onClick: execute }, running ? '编辑中…' : '生成修改版'),
1808
+ running ? React.createElement('button', { className: 'iris-pm-btn', type: 'button', onClick: cancel }, '停止等待') : null),
1809
+ result ? React.createElement('div', { role: 'status', className: 'iris-clean-note', style: { whiteSpace: 'pre-wrap' } }, result.text || '编辑中…',
1810
+ result.taskId ? React.createElement('div', {}, 'task: ' + result.taskId) : null) : null,
1811
+ result && result.artifactIds ? result.artifactIds.map(function (id) { return React.createElement('div', { key: id },
1812
+ React.createElement('a', { href: '/iris/api/core/artifact/' + encodeURIComponent(id) + '/media', target: '_blank', rel: 'noreferrer' }, '打开新作品:' + id)); }) : null));
1813
+ }
1814
+
1815
+ function ArtifactGallery() {
1816
+ var dataPair = React.useState({ total: 0, offset: 0, limit: 24, items: [], kinds: [], counts: { core: 0, legacy: 0 } });
1817
+ var data = dataPair[0], setData = dataPair[1];
1818
+ var queryPair = React.useState({ offset: 0, media_type: 'all', source: 'all', kind: 'all' });
1819
+ var query = queryPair[0], setQuery = queryPair[1];
1820
+ var currentQuery = React.useRef(query); currentQuery.current = query;
1821
+ var busyPair = React.useState(false), busy = busyPair[0], setBusy = busyPair[1];
1822
+ var mutationPair = React.useState(false), mutating = mutationPair[0], setMutating = mutationPair[1];
1380
1823
  var notePair = React.useState('');
1381
1824
  var note = notePair[0];
1382
1825
  var setNote = notePair[1];
1826
+ var analysisPair = React.useState(null), analysis = analysisPair[0], setAnalysis = analysisPair[1];
1827
+ var selectionPair = React.useState([]), selected = selectionPair[0], setSelected = selectionPair[1];
1828
+ var managementPair = React.useState(null), management = managementPair[0], setManagement = managementPair[1];
1829
+ var operationNotePair = React.useState(''), operationNote = operationNotePair[0], setOperationNote = operationNotePair[1];
1830
+ var request = React.useRef(null), sequence = React.useRef(0);
1831
+ var downloadRequest = React.useRef(null);
1383
1832
 
1384
- function load(append) {
1385
- if (busy) return;
1833
+ function load() {
1834
+ if (request.current) request.current.abort();
1835
+ var controller = new AbortController(); request.current = controller;
1386
1836
  setBusy(true);
1387
- var offset = append ? items.length : 0;
1388
- var legacyRequest = fetch('/iris/api/artifacts?offset=' + offset + '&limit=24')
1389
- .then(function (res) { return res.ok ? res.json() : Promise.reject(new Error('旧版作品读取失败')); });
1390
- var coreRequest = append ? Promise.resolve(null) : fetch('/iris/api/core/snapshot?limit=200')
1391
- .then(function (res) {
1392
- if (res.status === 404) return { available: false, backendReloadRequired: true, artifacts: { recent: [] } };
1393
- return res.ok ? res.json() : Promise.reject(new Error('Core 作品读取失败(HTTP ' + res.status + ')'));
1394
- }).catch(function (error) { return { available: false, error: error.message, artifacts: { recent: [] } }; });
1395
- Promise.all([legacyRequest, coreRequest])
1396
- .then(function (values) {
1397
- var data = values[0];
1398
- var core = values[1];
1399
- setItems(append ? items.concat(data.items || []) : (data.items || []));
1400
- setTotal(Number(data.total || 0));
1401
- if (core) {
1402
- var nextCore = coreGalleryItems(core, 200);
1403
- setCoreItems(nextCore);
1404
- if (core.error) setNote(core.error);
1405
- else if (core.backendReloadRequired) setNote('DSH 后端尚未加载 Core 路由;重启后新作品会出现在这里。');
1406
- }
1837
+ var input = currentQuery.current;
1838
+ var url = '/iris/api/works?offset=' + input.offset + '&limit=24&media_type=' + input.media_type
1839
+ + '&source=' + input.source + '&kind=' + encodeURIComponent(input.kind);
1840
+ fetch(url, { signal: controller.signal })
1841
+ .then(function (res) { if (!res.ok) throw new Error(res.status === 404 ? '请重新加载 DSH 后端以启用作品管理' : '作品读取失败(HTTP ' + res.status + ')'); return res.json(); })
1842
+ .then(function (value) {
1843
+ if (request.current !== controller || controller.signal.aborted) return;
1844
+ setData(value);
1845
+ setNote(value.coreError ? 'Core 作品暂时不可用,当前计数仅含可读取作品。'
1846
+ : value.droppedCore ? '已跳过 ' + value.droppedCore + ' 条不可读取的 Core 作品。' : '');
1407
1847
  })
1408
- .catch(function (error) { setNote('作品库读取失败:' + error.message); })
1409
- .finally(function () { setBusy(false); });
1848
+ .catch(function (error) { if (request.current === controller && !controller.signal.aborted) setNote(error.message); })
1849
+ .finally(function () { if (request.current === controller) { request.current = null; setBusy(false); } });
1410
1850
  }
1411
1851
 
1412
1852
  function run(action, body, confirmText) {
1413
1853
  if (confirmText && !window.confirm(confirmText)) return;
1414
- setBusy(true);
1854
+ setMutating(true);
1415
1855
  postAction(action, body || {}).then(function (result) {
1416
1856
  var succeeded = result.ok && result.d && result.d.ok !== false;
1417
1857
  setNote(succeeded ? (result.d.text || '完成') : ('失败:' + ((result.d && (result.d.error || result.d.text)) || '未知错误')));
1418
- if (succeeded) load(false);
1419
- else setBusy(false);
1420
- }).catch(function (error) { setBusy(false); setNote('失败:' + error.message); });
1858
+ if (succeeded) {
1859
+ if (action === 'artifacts_clear' || action === 'artifacts_delete') setSelected(function (current) {
1860
+ return current.filter(function (item) { return item.source !== 'legacy' || (action !== 'artifacts_clear' && item.id !== body.artifact_id); });
1861
+ });
1862
+ load();
1863
+ }
1864
+ }).catch(function (error) { setNote('失败:' + error.message); }).finally(function () { setMutating(false); });
1421
1865
  }
1422
1866
 
1423
1867
  React.useEffect(function () {
1424
- function changed() { load(false); }
1868
+ function changed() { load(); }
1425
1869
  window.addEventListener('iris-core-artifacts-changed', changed);
1426
1870
  window.addEventListener('iris-core-refresh-tick', changed);
1427
- load(false);
1871
+ load();
1428
1872
  return function () {
1429
1873
  window.removeEventListener('iris-core-artifacts-changed', changed);
1430
1874
  window.removeEventListener('iris-core-refresh-tick', changed);
1875
+ if (request.current) request.current.abort(); request.current = null;
1431
1876
  };
1432
- }, [props.total]);
1433
- var galleryItems = coreItems.concat(items).sort(function (a, b) {
1434
- return String(b.createdAt || '').localeCompare(String(a.createdAt || ''));
1435
- });
1436
- var combinedTotal = coreItems.length + total;
1877
+ }, [query.offset, query.media_type, query.source, query.kind]);
1878
+ React.useEffect(function () { return function () { if (downloadRequest.current) downloadRequest.current.abort(); downloadRequest.current = null; }; }, []);
1879
+ function filter(key, value) {
1880
+ setSelected([]); if (management && management.type === 'delete') setManagement(null);
1881
+ setQuery(function (current) { var next = Object.assign({}, current, { offset: 0 }); next[key] = value; if (key !== 'kind') next.kind = 'all'; return next; });
1882
+ }
1883
+ function page(offset) { setQuery(function (current) { return Object.assign({}, current, { offset: offset }); }); }
1884
+ function analyze(item, action) { setManagement(null); sequence.current++; setAnalysis({ item: item, action: action, key: sequence.current }); }
1885
+ function manage(type, items) { setAnalysis(null); sequence.current++; setManagement({ type: type, items: items, item: items && items[0], key: sequence.current }); }
1886
+ function selectedItem(item) { return selected.some(function (value) { return value.source === item.source && value.id === item.id; }); }
1887
+ function choose(item, checked) {
1888
+ if (checked && selected.length >= 200) { setOperationNote('最多选择 200 个作品'); return; }
1889
+ setSelected(function (current) {
1890
+ var present = current.some(function (value) { return value.source === item.source && value.id === item.id; });
1891
+ return checked ? present || current.length >= 200 ? current : current.concat(item)
1892
+ : current.filter(function (value) { return value.source !== item.source || value.id !== item.id; });
1893
+ });
1894
+ }
1895
+ var pageSelected = data.items.length && data.items.every(selectedItem);
1896
+ function choosePage() {
1897
+ 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; }); }); });
1898
+ else {
1899
+ var additions = data.items.filter(function (item) { return !selectedItem(item); });
1900
+ if (selected.length + additions.length > 200) { setOperationNote('最多选择 200 个作品,请减少选择后再选本页'); return; }
1901
+ setSelected(function (current) {
1902
+ var next = current.concat(additions.filter(function (item) {
1903
+ return !current.some(function (value) { return value.source === item.source && value.id === item.id; });
1904
+ }));
1905
+ return next.length <= 200 ? next : current;
1906
+ });
1907
+ }
1908
+ }
1909
+ function changed(value) {
1910
+ if (value.state === 'committed') setSelected([]);
1911
+ setOperationNote((value.state === 'restored' ? '已恢复:' : '已移入回收区:') + value.transactionId);
1912
+ window.dispatchEvent(new CustomEvent('iris-core-artifacts-changed'));
1913
+ }
1914
+ function download(items) {
1915
+ var controller = new AbortController(); downloadRequest.current = controller;
1916
+ setMutating(true); setOperationNote('正在准备下载…');
1917
+ fetch('/iris/api/works/download', { method: 'POST', headers: { 'Content-Type': 'application/json' }, signal: controller.signal,
1918
+ body: JSON.stringify({ items: items.map(function (item) { return { source: item.source, id: item.id }; }) }) })
1919
+ .then(function (res) { if (!res.ok) return res.json().then(function (value) { throw new Error(value.error || '下载失败'); }); return res.blob(); })
1920
+ .then(function (blob) { if (downloadRequest.current === controller && !controller.signal.aborted) { saveWorkbenchBlob(blob, 'iris-works.zip'); setOperationNote('已下载选中作品'); } })
1921
+ .catch(function (error) { if (downloadRequest.current === controller && !controller.signal.aborted) setOperationNote(error.message); })
1922
+ .finally(function () { if (downloadRequest.current === controller) { downloadRequest.current = null; setMutating(false); } });
1923
+ }
1924
+ function cancelDownload() { if (downloadRequest.current) downloadRequest.current.abort(); downloadRequest.current = null; setMutating(false); setOperationNote('已取消下载'); }
1925
+ var pageNumber = Math.floor(data.offset / data.limit) + 1;
1926
+ var pageCount = Math.max(1, Math.ceil(data.total / data.limit));
1927
+ var blocked = busy || mutating;
1437
1928
 
1438
1929
  return React.createElement('div', {},
1439
1930
  React.createElement('div', { className: 'iris-gallery-tools' },
1440
1931
  React.createElement('span', { className: 'iris-clean-note' },
1441
- '共 ' + combinedTotal + ' 个作品 · Core ' + coreItems.length + ' · 旧版 ' + total + ' · 清任务历史不会删除'),
1442
- React.createElement('button', { className: 'iris-clean-btn', disabled: busy, onClick: function () { load(false); } }, '刷新'),
1443
- React.createElement('button', { className: 'iris-clean-btn', disabled: busy, onClick: function () { run('artifacts_reindex'); } }, '扫描旧版 outputs'),
1444
- total ? React.createElement('button', { className: 'iris-clean-btn danger', disabled: busy, onClick: function () { run('artifacts_clear', {}, '永久删除旧版 outputs 作品?Core 作品不会删除;任务历史会保留。此操作不可逆。'); } }, '清空旧版作品') : null),
1445
- galleryItems.length ? React.createElement('div', { className: 'iris-gallery-grid' }, galleryItems.map(function (item) {
1932
+ '匹配 ' + data.total + ' 个作品 · Core ' + data.counts.core + ' · 旧版 ' + data.counts.legacy + ' · 清任务历史不会删除'),
1933
+ React.createElement('button', { className: 'iris-clean-btn', disabled: blocked, onClick: load }, '刷新'),
1934
+ React.createElement('button', { className: 'iris-clean-btn', disabled: mutating, onClick: function () { manage('transactions'); } }, 'Core 回收区'),
1935
+ React.createElement('button', { className: 'iris-clean-btn', disabled: blocked, onClick: function () { run('artifacts_reindex'); } }, '扫描旧版 outputs'),
1936
+ data.counts.legacy ? React.createElement('button', { className: 'iris-clean-btn danger', disabled: blocked, onClick: function () { run('artifacts_clear', {}, '永久删除旧版 outputs 作品?Core 作品不会删除;任务历史会保留。此操作不可逆。'); } }, '清空旧版作品') : null),
1937
+ React.createElement('div', { className: 'iris-gallery-filters' },
1938
+ React.createElement('label', {}, '媒体类型', React.createElement('select', { 'aria-label': '媒体类型', value: query.media_type, disabled: mutating && !!management, onChange: function (e) { filter('media_type', e.target.value); } },
1939
+ [['all', '全部媒体'], ['image', '图片'], ['video', '视频'], ['audio', '音频'], ['text', '文本']].map(function (entry) { return React.createElement('option', { key: entry[0], value: entry[0] }, entry[1]); }))),
1940
+ React.createElement('label', {}, '作品来源', React.createElement('select', { 'aria-label': '作品来源', value: query.source, disabled: mutating && !!management, onChange: function (e) { filter('source', e.target.value); } },
1941
+ [['all', '全部来源'], ['core', 'Core'], ['legacy', '旧版']].map(function (entry) { return React.createElement('option', { key: entry[0], value: entry[0] }, entry[1]); }))),
1942
+ React.createElement('label', {}, '产物类型', React.createElement('select', { 'aria-label': '产物类型', value: query.kind, disabled: mutating && !!management, onChange: function (e) { filter('kind', e.target.value); } },
1943
+ ['all'].concat(data.kinds).map(function (kind) { return React.createElement('option', { key: kind, value: kind }, kind === 'all' ? '全部产物' : galleryKindLabel(kind)); })))),
1944
+ React.createElement('div', { className: 'iris-gallery-bulk' },
1945
+ React.createElement('span', { className: 'iris-wb-muted', role: 'status' }, '已选择 ' + selected.length + ' / 200 · 下载最多 128 MiB'),
1946
+ React.createElement('button', { type: 'button', className: 'iris-pm-btn', disabled: blocked || !data.items.length, onClick: choosePage }, pageSelected ? '取消本页选择' : '选择本页'),
1947
+ React.createElement('button', { type: 'button', className: 'iris-pm-btn', disabled: mutating || !selected.length,
1948
+ onClick: function () { setSelected([]); if (management && management.type === 'delete') setManagement(null); } }, '清空选择'),
1949
+ React.createElement('button', { type: 'button', className: 'iris-pm-btn', disabled: !selected.length,
1950
+ onClick: function () { copyWorkbenchText(selected.map(function (item) { return item.id; }).join('\n'), setOperationNote); } }, '复制选中 ID'),
1951
+ React.createElement('button', { type: 'button', className: 'iris-pm-btn', disabled: mutating || !selected.length, onClick: function () { download(selected); } }, '下载选中作品'),
1952
+ React.createElement('button', { type: 'button', className: 'iris-pm-btn danger', disabled: mutating || !selected.length || selected.some(function (item) { return item.source !== 'core'; }),
1953
+ title: '仅 Core 作品支持可恢复批量删除;请先按来源筛选', onClick: function () { manage('delete', selected); } }, '删除选中 Core 作品'),
1954
+ downloadRequest.current ? React.createElement('button', { type: 'button', className: 'iris-pm-btn', onClick: cancelDownload }, '取消下载') : null),
1955
+ analysis ? React.createElement(analysis.action === 'image_edit' ? ArtifactImageEdit : ArtifactAnalysis, { key: analysis.key, item: analysis.item, action: analysis.action, onClose: function () { setAnalysis(null); } }) : null,
1956
+ management ? React.createElement(WorkbenchManagement, { key: management.key, type: management.type, items: management.items, item: management.item,
1957
+ onBusy: setMutating, onChanged: changed, onClose: function () { setManagement(null); } }) : null,
1958
+ data.items.length ? React.createElement('div', { className: 'iris-gallery-grid', 'aria-busy': busy }, data.items.map(function (item) {
1446
1959
  var isCore = item.source === 'core';
1447
- return React.createElement('div', { key: (isCore ? 'core-' : 'legacy-') + item.id, className: 'iris-gallery-card' },
1960
+ return React.createElement('div', { key: (isCore ? 'core-' : 'legacy-') + item.id, className: 'iris-gallery-card' + (selectedItem(item) ? ' selected' : '') },
1448
1961
  artifactPreview(item),
1449
1962
  React.createElement('div', { className: 'iris-gallery-info' },
1963
+ React.createElement('label', { className: 'iris-gallery-select' }, React.createElement('input', { type: 'checkbox',
1964
+ 'aria-label': '选择作品 ' + item.id, checked: selectedItem(item), disabled: mutating || (!selectedItem(item) && selected.length >= 200),
1965
+ onChange: function (event) { choose(item, event.target.checked); } }), '选择'),
1450
1966
  React.createElement('div', { className: 'iris-gallery-file', title: item.file }, item.file),
1451
1967
  React.createElement('div', { className: 'iris-gallery-meta' },
1452
- (isCore ? 'Core · ' + (item.kind || 'artifact') + (item.digest ? ' · sha256 ' + item.digest : '') : fmtTime(item.createdAt)) + ' · ' + fmtBytes(item.size)),
1968
+ (isCore ? 'Core · ' + galleryKindLabel(item.kind) : '旧版') + ' · ' + fmtTime(item.createdAt) + ' · ' + fmtBytes(item.size)),
1453
1969
  React.createElement('div', { className: 'iris-gallery-actions' },
1454
1970
  React.createElement('a', { className: 'iris-wb-link', href: item.url, target: '_blank', rel: 'noreferrer' }, '打开'),
1455
- isCore ? React.createElement('span', { className: 'iris-wb-muted' }, '内容哈希保护')
1456
- : React.createElement('button', { className: 'iris-pm-btn danger', disabled: busy, onClick: function () { run('artifacts_delete', { artifact_id: item.id }, '永久删除作品“' + item.file + '”?任务历史会保留,但媒体将无法打开。'); } }, '删除'))));
1457
- })) : React.createElement('div', { className: 'iris-wb-empty' }, busy ? '正在读取作品…' : '尚无作品;旧 outputs 文件可点“扫描旧版 outputs”找回。'),
1458
- items.length < total ? React.createElement('button', { className: 'iris-clean-btn', disabled: busy, onClick: function () { load(true); } }, busy ? '加载中…' : '加载更多旧版作品') : null,
1459
- note ? React.createElement('div', { className: 'iris-clean-note', style: { marginTop: '7px' } }, note) : null);
1971
+ React.createElement('button', { className: 'iris-pm-btn', type: 'button', disabled: mutating, onClick: function () { manage('details', [item]); } }, '详情'),
1972
+ isCore ? React.createElement('button', { className: 'iris-pm-btn danger', type: 'button', disabled: mutating, onClick: function () { manage('delete', [item]); } }, '移入回收区') : null,
1973
+ isCore && /^image\/(png|jpeg|webp|gif)$/.test(item.mime) ? ['look', 'ocr'].map(function (action) {
1974
+ return React.createElement('button', { key: action, type: 'button', className: 'iris-pm-btn', disabled: mutating, onClick: function () { analyze(item, action); } }, action === 'look' ? '看图' : 'OCR');
1975
+ }) : null,
1976
+ isCore && /^image\/(png|jpeg|webp)$/.test(item.mime) ? React.createElement('button', { type: 'button', className: 'iris-pm-btn', disabled: mutating,
1977
+ onClick: function () { analyze(item, 'image_edit'); } }, '改图') : null,
1978
+ isCore ? React.createElement('span', { className: 'iris-wb-muted', title: '打开或识别时核验原文件' }, '内容哈希保护')
1979
+ : React.createElement('button', { className: 'iris-pm-btn danger', disabled: blocked, onClick: function () { run('artifacts_delete', { artifact_id: item.id }, '永久删除作品“' + item.file + '”?任务历史会保留,但媒体将无法打开。'); } }, '删除'))));
1980
+ })) : React.createElement('div', { className: 'iris-wb-empty' }, busy ? '正在读取作品…'
1981
+ : query.media_type !== 'all' || query.source !== 'all' || query.kind !== 'all' ? '没有匹配的作品,请调整筛选条件。' : '尚无作品;旧 outputs 文件可点“扫描旧版 outputs”找回。'),
1982
+ React.createElement('div', { className: 'iris-gallery-pagination' },
1983
+ React.createElement('button', { type: 'button', className: 'iris-clean-btn', disabled: blocked || pageNumber <= 1, onClick: function () { page(Math.max(0, data.offset - data.limit)); } }, '上一页'),
1984
+ React.createElement('span', { className: 'iris-wb-muted', role: 'status' }, '第 ' + pageNumber + ' / ' + pageCount + ' 页'),
1985
+ React.createElement('button', { type: 'button', className: 'iris-clean-btn', disabled: blocked || pageNumber >= pageCount, onClick: function () { page(data.offset + data.limit); } }, '下一页')),
1986
+ note ? React.createElement('div', { className: 'iris-clean-note', style: { marginTop: '7px' } }, note) : null,
1987
+ operationNote ? React.createElement('div', { role: 'status', className: 'iris-clean-note', style: { marginTop: '7px', whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' } }, operationNote) : null);
1460
1988
  }
1461
1989
 
1462
1990
  function coreCapabilityLabel(value) {
@@ -1669,9 +2197,14 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1669
2197
  if (draft === null) return null;
1670
2198
  draft = String(draft).trim();
1671
2199
  if (!draft) return null;
1672
- return isTts ? { text: draft, confirmBilling: true }
2200
+ var sourceArtifactId;
2201
+ if (props.row.sourceArtifactId) {
2202
+ sourceArtifactId = window.prompt('请确认新改图任务使用的原图 Artifact ID:', props.row.sourceArtifactId);
2203
+ if (sourceArtifactId === null || !String(sourceArtifactId).trim()) return null;
2204
+ }
2205
+ return isTts ? { text: draft, confirmBilling: true }
1673
2206
  : isTranscribe ? { audio_url: draft, confirmBilling: true }
1674
- : { prompt: draft, confirmBilling: true };
2207
+ : { prompt: draft, confirmBilling: true, ...(sourceArtifactId ? { source_artifact_id: String(sourceArtifactId).trim() } : {}) };
1675
2208
  }
1676
2209
  });
1677
2210
  }
@@ -1918,7 +2451,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1918
2451
  React.createElement(CoreRetryButton, {
1919
2452
  row: selectedTask && selectedTask.phase === 'terminal'
1920
2453
  && !(selectedTask.outcome === 'succeeded' && selectedTask.deliveryState === 'ready')
1921
- ? { id: selectedTask.id, retryable: true } : null
2454
+ ? { id: selectedTask.id, capability: selectedTask.capability, sourceArtifactId: selectedTask.sourceArtifactId, retryable: true } : null
1922
2455
  }),
1923
2456
  React.createElement(CoreDispositionButtons, {
1924
2457
  row: selectedDisposition
@@ -2105,7 +2638,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
2105
2638
  React.createElement(ProviderManager, {}),
2106
2639
  React.createElement('div', { className: 'iris-wb-sec' }, '能力健康'),
2107
2640
  React.createElement(CapabilityHealthOverview, {}),
2108
- React.createElement('div', { className: 'iris-wb-muted' }, '绿色有效 7 天;真实成功或显式实测会刷新。不会后台探测,也不会因 429、网络或供应商 5xx 变红。'),
2641
+ React.createElement('div', { className: 'iris-wb-muted' }, '绿色有效 7 天;真实成功或显式实测会刷新。429 冷却显示琥珀色,额度或预算耗尽显示红色;点击模型能力可实测恢复。不会后台探测。'),
2109
2642
  React.createElement('div', { className: 'iris-wb-sec' }, '能力分配(failover 顺序)'),
2110
2643
  React.createElement(CapabilityAssigner, {}),
2111
2644
  React.createElement('div', { className: 'iris-wb-sec' }, 'Iris 泡泡快捷卡片(勾选后显示在「⚡ 常用」标签)'),
@@ -2205,6 +2738,25 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
2205
2738
  selectedTask ? React.createElement(TaskDetailDrawer, { taskId: selectedTask }) : null);
2206
2739
  }
2207
2740
 
2741
+ var PROMPT_TARGET_OPTIONS = [
2742
+ { value: 'general', label: '通用', description: '整理目标、背景、约束与输出要求。' },
2743
+ { value: 'image', label: '画图', description: '整理已有主体、场景与构图要求,未指定细节不自动补齐。' },
2744
+ { value: 'image-edit', label: '改图', description: '明确要修改的部分,保留原图其余特征。' },
2745
+ { value: 'video', label: '视频', description: '组织动作、镜头与时间上的连续性。' },
2746
+ { value: 's2v', label: '数字人', description: '围绕已有首帧与音频设计表情、动作和镜头。' }
2747
+ ];
2748
+
2749
+ // 折叠相同首尾,只显示中间替换区;按 Unicode code point 比较,不拆开 emoji。
2750
+ function promptChanges(original, edited) {
2751
+ var before = Array.from(original);
2752
+ var after = Array.from(edited);
2753
+ var start = 0;
2754
+ while (start < before.length && start < after.length && before[start] === after[start]) start++;
2755
+ var end = 0;
2756
+ while (end < before.length - start && end < after.length - start && before[before.length - end - 1] === after[after.length - end - 1]) end++;
2757
+ return { removed: before.slice(start, before.length - end).join(''), added: after.slice(start, after.length - end).join('') };
2758
+ }
2759
+
2208
2760
  function PromptOptimizerControl(props) {
2209
2761
  var draft = props.useInput(function (value) { return value.draft; });
2210
2762
  var phase = props.useInput(function (value) { return value.phase; });
@@ -2215,6 +2767,8 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
2215
2767
  var openPair = React.useState(false);
2216
2768
  var open = openPair[0];
2217
2769
  var setOpen = openPair[1];
2770
+ var generalPair = React.useState(true);
2771
+ var generalOpen = generalPair[0];
2218
2772
  var targetPair = React.useState('general');
2219
2773
  var target = targetPair[0];
2220
2774
  var setTarget = targetPair[1];
@@ -2230,8 +2784,49 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
2230
2784
  var configPair = React.useState(null);
2231
2785
  var configState = configPair[0];
2232
2786
  var setConfigState = configPair[1];
2787
+ var optimizationPair = React.useState('');
2788
+ var optimizationText = optimizationPair[0];
2789
+ var setOptimizationText = optimizationPair[1];
2790
+ var prefixPair = React.useState('');
2791
+ var prefixText = prefixPair[0];
2792
+ var setPrefixText = prefixPair[1];
2793
+ var suffixPair = React.useState('');
2794
+ var suffixText = suffixPair[0];
2795
+ var setSuffixText = suffixPair[1];
2796
+ var editedPair = React.useState('');
2797
+ var edited = editedPair[0];
2798
+ var setEdited = editedPair[1];
2799
+ var editingPair = React.useState('faithful');
2800
+ var editing = editingPair[0];
2801
+ var suggestionsPair = React.useState(null);
2802
+ var suggestions = suggestionsPair[0];
2803
+ var setSuggestions = suggestionsPair[1];
2804
+ var selectedSuggestionsPair = React.useState([]);
2805
+ var selectedSuggestions = selectedSuggestionsPair[0];
2806
+ var setSelectedSuggestions = selectedSuggestionsPair[1];
2807
+ var undoPair = React.useState([]);
2808
+ var suggestionUndo = undoPair[0];
2809
+ var setSuggestionUndo = undoPair[1];
2810
+ var editedRef = React.useRef(edited);
2811
+ var resultRef = React.useRef(result);
2812
+ editedRef.current = edited;
2813
+ resultRef.current = result;
2233
2814
  var abortRef = React.useRef(null);
2234
2815
  var fileRef = React.useRef(null);
2816
+ var panelRef = React.useRef(null);
2817
+ var lastWrittenRef = React.useRef(null);
2818
+ var resultSessionRef = React.useRef(null);
2819
+ var sessionRef = React.useRef(props.sessionId);
2820
+ sessionRef.current = props.sessionId;
2821
+ var supportsRules = !!(configState && configState.capabilities && configState.capabilities.rules === true);
2822
+ var supportsAssembly = !!(configState && configState.capabilities && configState.capabilities.assemble === true);
2823
+ var editingCapabilities = configState && configState.capabilities && configState.capabilities.editing;
2824
+ var supportsFaithful = Array.isArray(editingCapabilities) && editingCapabilities.indexOf('faithful') >= 0 && editingCapabilities.indexOf('rewrite') >= 0;
2825
+ var supportsSuggestions = Array.isArray(editingCapabilities) && editingCapabilities.indexOf('suggestions') >= 0;
2826
+ function supportsTarget(value) {
2827
+ var available = configState && configState.capabilities && configState.capabilities.targets;
2828
+ return Array.isArray(available) ? available.indexOf(value) >= 0 : value !== 'image-edit';
2829
+ }
2235
2830
 
2236
2831
  function promptRequest(operation, body, signal) {
2237
2832
  return fetch('/iris/api/prompt-optimizer/' + operation, {
@@ -2257,6 +2852,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
2257
2852
  React.useEffect(function () {
2258
2853
  return function () {
2259
2854
  if (abortRef.current) abortRef.current.abort();
2855
+ abortRef.current = null;
2260
2856
  };
2261
2857
  }, []);
2262
2858
 
@@ -2267,53 +2863,193 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
2267
2863
  return function () { window.removeEventListener('iris-prompt-config-changed', changed); };
2268
2864
  }, []);
2269
2865
 
2866
+ React.useEffect(function () {
2867
+ cancel();
2868
+ setResult(null);
2869
+ setSuggestions(null); setSelectedSuggestions([]); setSuggestionUndo([]);
2870
+ lastWrittenRef.current = null;
2871
+ resultSessionRef.current = null;
2872
+ }, [props.sessionId]);
2873
+
2874
+ React.useEffect(function () {
2875
+ if (!open) return;
2876
+ var previous = document.activeElement;
2877
+ var dialog = panelRef.current;
2878
+ if (dialog) { dialog.showModal(); dialog.focus(); }
2879
+ function keyboard(event) {
2880
+ if (event.key === 'Escape') { event.preventDefault(); closePanel(); }
2881
+ if (event.key !== 'Tab' || !panelRef.current) return;
2882
+ var items = Array.from(panelRef.current.querySelectorAll('button:not(:disabled), textarea:not(:disabled), summary, input:not(:disabled)'))
2883
+ .filter(function (item) { return item.getClientRects().length > 0; });
2884
+ if (!items.length) return;
2885
+ var first = items[0], last = items[items.length - 1];
2886
+ if (event.shiftKey && (document.activeElement === first || document.activeElement === panelRef.current)) {
2887
+ event.preventDefault(); last.focus();
2888
+ } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
2889
+ }
2890
+ window.addEventListener('keydown', keyboard);
2891
+ return function () {
2892
+ window.removeEventListener('keydown', keyboard);
2893
+ if (dialog && dialog.open) dialog.close();
2894
+ if (previous && previous.isConnected) previous.focus();
2895
+ };
2896
+ }, [open]);
2897
+
2270
2898
  function publishConfig(data) {
2271
2899
  setConfigState(data);
2272
2900
  try { window.dispatchEvent(new CustomEvent('iris-prompt-config-changed', { detail: data })); } catch (_) {}
2273
2901
  }
2274
2902
 
2275
- function optimize() {
2903
+ function selectedRules(mode) {
2904
+ var rules = [];
2905
+ if (!supportsRules) return rules;
2906
+ if (mode === 'optimize' && optimizationText.trim()) rules.push({ id: 'custom-optimization', label: '本次改写规则', kind: 'optimization', text: optimizationText });
2907
+ if (prefixText.trim()) rules.push({ id: 'custom-prefix', label: '输出前缀', kind: 'output', position: 'prefix', text: prefixText });
2908
+ if (suffixText.trim()) rules.push({ id: 'custom-suffix', label: '输出后缀', kind: 'output', position: 'suffix', text: suffixText });
2909
+ return rules;
2910
+ }
2911
+
2912
+ function optimize(mode) {
2913
+ mode = mode === 'assemble' ? 'assemble' : 'optimize';
2914
+ if (!supportsTarget(target)) { setNote('当前后端尚未支持此优化目标,请更新并重新载入'); return; }
2915
+ if (mode === 'assemble' && !supportsAssembly) { setNote('当前后端尚未支持只组装,请更新并重新载入'); return; }
2276
2916
  if (!draft || !draft.trim()) { setNote('请先在输入框中写入提示词'); return; }
2277
2917
  if (refCount) { setNote('当前版本暂不改写含 @ 或 / 结构化引用的草稿,以免引用失效'); return; }
2278
2918
  if (phase !== 'plain') { setNote('输入框正忙,请稍后再试'); return; }
2919
+ if (mode === 'optimize' && supportsFaithful && editing === 'faithful' && optimizationText.trim()) {
2920
+ setNote('忠实整理只调整分段;使用本次改写规则前,请选择“改写表达”。'); return;
2921
+ }
2279
2922
  if (abortRef.current) abortRef.current.abort();
2280
2923
  var controller = new AbortController();
2924
+ var requestSessionId = props.sessionId;
2281
2925
  abortRef.current = controller;
2282
2926
  setBusy(true);
2283
- setNote('正在调用模型优化…这可能产生模型费用');
2927
+ setNote(mode === 'assemble' ? '正在组装,不调用模型…' : '正在调用模型优化…这可能产生模型费用');
2284
2928
  setResult(null);
2929
+ setSuggestions(null); setSelectedSuggestions([]); setSuggestionUndo([]);
2930
+ lastWrittenRef.current = null;
2285
2931
  promptRequest('optimize', {
2286
2932
  text: draft,
2287
2933
  target: target,
2934
+ mode: mode,
2935
+ editing: mode === 'optimize' && supportsFaithful ? editing : undefined,
2936
+ rules: selectedRules(mode),
2288
2937
  sessionId: props.sessionId,
2289
2938
  route: selectedRoute || undefined
2290
2939
  }, controller.signal).then(function (data) {
2940
+ if (abortRef.current !== controller || controller.signal.aborted) return;
2941
+ if (sessionRef.current !== requestSessionId) throw new Error('会话已切换,请在当前会话重新生成预览');
2942
+ if (!data.ok || typeof data.optimized !== 'string' || typeof data.original !== 'string') throw new Error('未收到完整优化结果');
2943
+ if (data.original !== draft) throw new Error('结果原稿与本次请求不一致');
2944
+ if (mode === 'optimize' && supportsFaithful && editing === 'faithful'
2945
+ && (data.editing?.strategy !== 'faithful' || data.editing.coverage !== 'exact' || data.editing.order !== 'preserved')) throw new Error('后端未确认完整的忠实整理结果');
2291
2946
  setResult(data);
2947
+ setEdited(data.optimized);
2948
+ resultSessionRef.current = requestSessionId;
2292
2949
  setNote('已生成预览,尚未写回输入框');
2293
2950
  }).catch(function (err) {
2951
+ if (abortRef.current !== controller) return;
2294
2952
  if (controller.signal.aborted) setNote('已取消');
2295
2953
  else setNote(String(err && err.message || err));
2296
2954
  }).finally(function () {
2297
- if (abortRef.current === controller) abortRef.current = null;
2298
- setBusy(false);
2955
+ if (abortRef.current === controller) { abortRef.current = null; setBusy(false); }
2299
2956
  });
2300
2957
  }
2301
2958
 
2959
+ function requestSuggestions() {
2960
+ if (!supportsSuggestions || !result || busy || !validEdited()) return;
2961
+ if (resultSessionRef.current !== props.sessionId) { setNote('会话已切换,请重新生成预览'); return; }
2962
+ var snapshot = edited;
2963
+ var previousResult = result;
2964
+ var requestSessionId = props.sessionId;
2965
+ var controller = new AbortController();
2966
+ abortRef.current = controller;
2967
+ setBusy(true); setNote('正在获取可选建议,正文保持原样;这会调用文本模型。');
2968
+ promptRequest('optimize', { text: snapshot, target: result.target, mode: 'optimize', editing: 'suggestions', rules: [],
2969
+ sessionId: props.sessionId, route: selectedRoute || undefined }, controller.signal).then(function (data) {
2970
+ if (abortRef.current !== controller || controller.signal.aborted) return;
2971
+ if (sessionRef.current !== requestSessionId || resultRef.current !== previousResult || editedRef.current !== snapshot) {
2972
+ setNote('预览已变化,本次建议未载入;请针对当前文字重新获取。'); return;
2973
+ }
2974
+ var ids = [];
2975
+ if (!data.ok || data.original !== snapshot || data.optimized !== snapshot || data.target !== previousResult.target || data.editing?.strategy !== 'suggestions'
2976
+ || !Array.isArray(data.suggestions) || data.suggestions.length > 6 || !Array.isArray(data.questions) || data.questions.length > 6
2977
+ || !data.suggestions.every(function (item) {
2978
+ if (!item || typeof item.id !== 'string' || !/^suggestion-[1-6]$/.test(item.id) || ids.indexOf(item.id) >= 0
2979
+ || typeof item.label !== 'string' || !item.label.trim() || item.label.length > 80
2980
+ || typeof item.text !== 'string' || !item.text.trim() || item.text.length > 600) return false;
2981
+ ids.push(item.id); return true;
2982
+ }) || !data.questions.every(function (text) { return typeof text === 'string' && text.trim() && text.length <= 600; })) throw new Error('未收到完整补充建议');
2983
+ setSuggestions({ source: snapshot, items: data.suggestions, questions: data.questions, adopted: [] });
2984
+ setSelectedSuggestions([]); setSuggestionUndo([]);
2985
+ setNote('建议尚未加入正文;只有采纳所选建议才会改变预览。');
2986
+ }).catch(function (err) {
2987
+ if (abortRef.current !== controller) return;
2988
+ setNote(controller.signal.aborted ? '已取消,原有预览保留' : String(err && err.message || err));
2989
+ }).finally(function () {
2990
+ if (abortRef.current === controller) { abortRef.current = null; setBusy(false); }
2991
+ });
2992
+ }
2993
+
2994
+ function adoptSuggestions() {
2995
+ if (!suggestions || busy || !selectedSuggestions.length) return;
2996
+ var items = suggestions.items.filter(function (item) { return selectedSuggestions.indexOf(item.id) >= 0 && suggestions.adopted.indexOf(item.id) < 0; });
2997
+ if (!items.length) return;
2998
+ var next = edited + '\n\n' + items.map(function (item) { return item.text; }).join('\n\n');
2999
+ if (next.length > 16000) { setNote('采纳后将超过 16000 字符,请减少建议或手动编辑'); return; }
3000
+ var ids = items.map(function (item) { return item.id; });
3001
+ setSuggestionUndo(suggestionUndo.concat([{ before: edited, after: next, ids: ids }]));
3002
+ setSuggestions(Object.assign({}, suggestions, { adopted: suggestions.adopted.concat(ids) }));
3003
+ setEdited(next); editedRef.current = next; setSelectedSuggestions([]);
3004
+ setNote('已将所选建议加入预览,尚未写回;可以撤回上次采纳。');
3005
+ }
3006
+
3007
+ function undoSuggestions() {
3008
+ var last = suggestionUndo[suggestionUndo.length - 1];
3009
+ if (!last || busy) return;
3010
+ if (edited !== last.after) { setNote('预览已被手动修改;请手动移除建议,撤回不会覆盖你的编辑。'); return; }
3011
+ setEdited(last.before); editedRef.current = last.before;
3012
+ setSuggestionUndo(suggestionUndo.slice(0, -1));
3013
+ if (suggestions) setSuggestions(Object.assign({}, suggestions, { adopted: suggestions.adopted.filter(function (id) { return last.ids.indexOf(id) < 0; }) }));
3014
+ setNote('已撤回上次采纳,尚未写回输入框。');
3015
+ }
3016
+
3017
+ function editPreview(value) {
3018
+ setEdited(value); editedRef.current = value;
3019
+ setSuggestions(null); setSelectedSuggestions([]); setSuggestionUndo([]);
3020
+ }
3021
+
2302
3022
  function cancel() {
2303
- if (abortRef.current) abortRef.current.abort();
3023
+ if (abortRef.current) {
3024
+ abortRef.current.abort();
3025
+ abortRef.current = null;
3026
+ setBusy(false);
3027
+ setNote('已取消');
3028
+ }
3029
+ }
3030
+
3031
+ function closePanel() { cancel(); setOpen(false); }
3032
+
3033
+ function validEdited() {
3034
+ if (!edited.trim() || edited.length > 16000) { setNote('结果必须是非空文本且不超过 16000 字符'); return false; }
3035
+ return true;
2304
3036
  }
2305
3037
 
2306
3038
  function replaceDraft(text) {
2307
3039
  if (!result) return;
2308
- if (draft !== result.original && !window.confirm('输入框内容在优化期间已变化,仍要替换吗?')) return;
3040
+ if (resultSessionRef.current !== props.sessionId) { setNote('会话已切换,请在当前会话重新生成预览'); return; }
3041
+ if (refCount || phase !== 'plain') { setNote('当前输入框正忙或含结构化引用,暂不写回'); return; }
3042
+ if (text !== result.original && !validEdited()) return;
3043
+ if (draft !== result.original && draft !== lastWrittenRef.current && !window.confirm('输入框内容在优化期间已变化,仍要替换吗?')) return;
2309
3044
  props.inputActions.setDraft(text);
3045
+ lastWrittenRef.current = text;
2310
3046
  setNote(text === result.original ? '已恢复优化前原文' : '已写回输入框;不会自动发送');
2311
3047
  }
2312
3048
 
2313
3049
  function copyResult() {
2314
- if (!result) return;
3050
+ if (!result || !validEdited()) return;
2315
3051
  if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
2316
- navigator.clipboard.writeText(result.optimized).then(function () { setNote('已复制优化结果'); }).catch(function () { setNote('复制失败,请手动选择文本'); });
3052
+ navigator.clipboard.writeText(edited).then(function () { setNote('已复制优化结果'); }).catch(function () { setNote('复制失败,请手动选择文本'); });
2317
3053
  } else {
2318
3054
  setNote('浏览器不支持自动复制,请手动选择文本');
2319
3055
  }
@@ -2379,39 +3115,117 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
2379
3115
  var reasoningSetting = configState && configState.config && configState.config.generation && configState.config.generation.reasoningEffort || 'off-if-supported';
2380
3116
  var reasoningLabel = reasoningSetting === 'off-if-supported' ? '支持时关闭' : reasoningSetting === 'provider-default' ? '供应商默认' : reasoningSetting === 'inherit' ? '跟随会话' : reasoningSetting;
2381
3117
  var outputBudget = configState && configState.config && configState.config.generation && configState.config.generation.maxOutputTokens || 1200;
3118
+ var changes = result ? promptChanges(result.original, edited) : null;
3119
+ var targetInfo = PROMPT_TARGET_OPTIONS.find(function (item) { return item.value === target; }) || PROMPT_TARGET_OPTIONS[0];
3120
+ var resultTarget = result && PROMPT_TARGET_OPTIONS.find(function (item) { return item.value === result.target; });
3121
+ var config = configState && configState.config;
3122
+ var targetTemplate = config && config.targets && config.targets[target];
3123
+ function ruleField(label, value, update, placeholder) {
3124
+ return React.createElement('label', { className: 'iris-po-field' }, React.createElement('span', {}, label),
3125
+ React.createElement('textarea', { value: value, disabled: busy || !supportsRules, placeholder: placeholder,
3126
+ onChange: function (event) { update(event.target.value); } }));
3127
+ }
2382
3128
 
2383
3129
  var panel = null;
2384
3130
  if (open) {
2385
3131
  panel = React.createElement(React.Fragment, {},
2386
- React.createElement('div', { className: 'iris-po-backdrop', onClick: function () { setOpen(false); }, 'aria-hidden': 'true' }),
2387
- React.createElement('div', { className: 'iris-po-panel', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Iris 提示词优化', onClick: function (e) { e.stopPropagation(); } },
3132
+ React.createElement('dialog', { ref: panelRef, tabIndex: -1, className: 'iris-po-panel', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Iris 提示词优化',
3133
+ onCancel: function (event) { event.preventDefault(); closePanel(); },
3134
+ onClick: function (event) {
3135
+ event.stopPropagation();
3136
+ if (event.target !== event.currentTarget) return;
3137
+ var bounds = event.currentTarget.getBoundingClientRect();
3138
+ if (event.clientX < bounds.left || event.clientX > bounds.right || event.clientY < bounds.top || event.clientY > bounds.bottom) closePanel();
3139
+ } },
2388
3140
  React.createElement('div', { className: 'iris-po-head' },
2389
3141
  React.createElement('strong', { className: 'iris-po-title' }, React.createElement('span', { className: 'iris-po-mark' }, '🫧'), 'Iris 提示词优化'),
2390
- React.createElement('button', { className: 'iris-po-close', onClick: function () { setOpen(false); }, title: '关闭' }, '✕')),
2391
- React.createElement('div', { className: 'iris-po-muted' }, '只处理当前未发送草稿,不读取会话历史;调用模型可能产生费用。'),
2392
- React.createElement('label', { className: 'iris-po-field' },
2393
- React.createElement('span', {}, '优化目标'),
2394
- React.createElement('select', { value: target, disabled: busy, onChange: function (e) { setTarget(e.target.value); } },
2395
- React.createElement('option', { value: 'general' }, '通用对话'),
2396
- React.createElement('option', { value: 'image' }, '图片生成'),
2397
- React.createElement('option', { value: 'video' }, '视频生成'),
2398
- React.createElement('option', { value: 's2v' }, '首尾帧视频'))),
2399
- React.createElement('div', { className: 'iris-po-route', title: routeLabel }, '模型:' + routeLabel + ' · 思考:' + reasoningLabel + ' · 输出:' + outputBudget + ' tokens'),
3142
+ React.createElement('button', { className: 'iris-po-close', onClick: closePanel, title: '关闭', 'aria-label': '关闭提示词优化' }, '✕')),
3143
+ React.createElement('div', { className: 'iris-po-intro' },
3144
+ React.createElement('div', { className: 'iris-po-muted' }, '按用途整理草稿,先预览和编辑,再决定是否写回。'),
3145
+ React.createElement('div', { className: 'iris-po-principles' },
3146
+ React.createElement('span', {}, '保留原意'), React.createElement('span', {}, '可查看改动'), React.createElement('span', {}, '确认后写回'))),
3147
+ React.createElement('details', { className: 'iris-po-section', open: generalOpen,
3148
+ onToggle: function (event) { generalPair[1](event.currentTarget.open); } },
3149
+ React.createElement('summary', {}, '一般功能',
3150
+ React.createElement('span', { className: 'iris-po-section-caption' }, targetInfo.label + ' · 提示词优化' + (result ? ' · 有预览' : ''))),
3151
+ React.createElement('div', { className: 'iris-po-section-body' },
3152
+ React.createElement('div', { className: 'iris-po-targets', role: 'group', 'aria-label': '优化目标' },
3153
+ PROMPT_TARGET_OPTIONS.map(function (item) {
3154
+ return React.createElement('button', { key: item.value, type: 'button', className: 'iris-po-target',
3155
+ 'aria-pressed': target === item.value, value: item.value,
3156
+ disabled: busy || !supportsTarget(item.value), title: supportsTarget(item.value) ? item.description : '当前后端未提供此目标',
3157
+ onClick: function () { setTarget(item.value); } }, item.label);
3158
+ })),
3159
+ React.createElement('div', { className: 'iris-po-muted' }, targetInfo.description),
3160
+ supportsFaithful ? React.createElement('div', { className: 'iris-po-editing', role: 'group', 'aria-label': '编辑方式' },
3161
+ ['faithful', 'rewrite'].map(function (strategy) {
3162
+ return React.createElement('label', { key: strategy }, React.createElement('input', { type: 'radio', name: 'iris-prompt-editing',
3163
+ value: strategy, checked: editing === strategy, disabled: busy, onChange: function () { editingPair[1](strategy); } }), strategy === 'faithful' ? '忠实整理(默认)' : '改写表达');
3164
+ }),
3165
+ React.createElement('div', { className: 'iris-po-muted' }, editing === 'faithful' ? '保持原文和句序,只调整分组与分段;不补充细节。' : '按本次规则改写,结果需逐项核对;未授权内容保持原意。')) : null,
3166
+ React.createElement('div', { className: 'iris-po-source' },
3167
+ React.createElement('div', { className: 'iris-po-source-head' }, '当前草稿', React.createElement('span', {}, (draft || '').length + ' 字符')),
3168
+ React.createElement('p', { className: 'iris-po-source-text' }, draft || '先在对话输入框写下你的想法。')),
3169
+ React.createElement('details', { className: 'iris-po-config' },
3170
+ React.createElement('summary', {}, '本次规则(可选)'),
3171
+ React.createElement('div', { className: 'iris-po-rules' },
3172
+ ruleField('改写规则 · 仅智能优化使用', optimizationText, setOptimizationText, '例如:保持简短,保留全部否定条件'),
3173
+ ruleField('输出前缀 · 原样加入结果开头', prefixText, setPrefixText, '例如:请以摄影师的视角描述以下画面。'),
3174
+ ruleField('输出后缀 · 原样加入结果末尾', suffixText, setSuffixText, '例如:禁止添加文字和水印。')),
3175
+ React.createElement('div', { className: 'iris-po-muted' }, supportsRules ? '非空内容表示本次选择;只组装保留原稿,只添加前后缀。刷新后重置,不修改 JSON 配置。' : '当前后端尚未支持规则,请更新后重新载入。')),
2400
3176
  React.createElement('div', { className: 'iris-po-actions' },
2401
- React.createElement('button', { className: 'iris-po-primary', disabled: busy || !draft || !draft.trim() || phase !== 'plain' || refCount > 0, onClick: optimize }, busy ? '优化中…' : '生成预览'),
3177
+ React.createElement('button', { className: 'iris-po-primary', disabled: busy || !draft || !draft.trim() || phase !== 'plain' || refCount > 0, onClick: function () { optimize('optimize'); } }, busy ? '处理中…' : supportsFaithful ? editing === 'faithful' ? '忠实整理' : '改写表达' : '智能优化'),
3178
+ React.createElement('button', { className: 'iris-po-btn', disabled: busy || !supportsAssembly || !draft || !draft.trim() || phase !== 'plain' || refCount > 0, onClick: function () { optimize('assemble'); } }, '只组装(不调用模型)'),
2402
3179
  busy ? React.createElement('button', { className: 'iris-po-btn', onClick: cancel }, '取消') : null),
3180
+ React.createElement('div', { className: 'iris-po-muted' }, '智能优化调用文本模型,可能产生费用;只组装原稿与前后缀。'),
2403
3181
  result ? React.createElement('div', { className: 'iris-po-result' },
2404
- React.createElement('div', { className: 'iris-po-result-label' }, '优化结果'),
2405
- React.createElement('textarea', { readOnly: true, value: result.optimized }),
2406
- React.createElement('div', { className: 'iris-po-route' }, '实际使用:' + result.route.provider + ' / ' + result.route.model + ' · 思考:' + (result.route.reasoningEffort === 'provider-default' ? '供应商默认' : result.route.reasoningEffort)),
3182
+ React.createElement('div', { className: 'iris-po-result-label' }, '结果预览 · ' + (resultTarget ? resultTarget.label + ' · ' : '') + '可编辑 · ' + edited.length + ' 字符'),
3183
+ React.createElement('textarea', { 'aria-label': '结果预览', value: edited, onChange: function (event) { editPreview(event.target.value); } }),
3184
+ result.editing?.strategy === 'faithful' ? React.createElement('div', { className: 'iris-po-muted' }, '本次整理保留了全部原文片段与句序;手动编辑或采纳建议后,请重新核对内容。') : null,
3185
+ supportsSuggestions ? React.createElement('details', { className: 'iris-po-config' },
3186
+ React.createElement('summary', {}, '可选补充与待确认'),
3187
+ React.createElement('div', { className: 'iris-po-muted' }, '建议独立生成,默认未采纳;不会自动加入正文或写回。每次获取会调用文本模型。'),
3188
+ React.createElement('button', { className: 'iris-po-btn', disabled: busy || !edited.trim(), onClick: requestSuggestions }, '获取补充建议'),
3189
+ suggestions ? React.createElement('div', {},
3190
+ suggestions.items.map(function (item) {
3191
+ var adopted = suggestions.adopted.indexOf(item.id) >= 0;
3192
+ return React.createElement('div', { className: 'iris-po-suggestion', key: item.id },
3193
+ React.createElement('label', {}, React.createElement('input', { type: 'checkbox', checked: adopted || selectedSuggestions.indexOf(item.id) >= 0, disabled: busy || adopted,
3194
+ onChange: function (event) { setSelectedSuggestions(event.target.checked ? selectedSuggestions.concat([item.id]) : selectedSuggestions.filter(function (id) { return id !== item.id; })); } }), item.label + (adopted ? ' · 已加入预览' : '')),
3195
+ React.createElement('p', {}, item.text));
3196
+ }),
3197
+ !suggestions.items.length ? React.createElement('div', { className: 'iris-po-muted' }, '没有额外补充建议。') : null,
3198
+ suggestions.questions.length ? React.createElement('div', { className: 'iris-po-suggestion' }, React.createElement('strong', {}, '待确认 · 尚未作为要求'),
3199
+ suggestions.questions.map(function (question, index) { return React.createElement('p', { key: index }, question); })) : null,
3200
+ React.createElement('div', { className: 'iris-po-actions' },
3201
+ React.createElement('button', { className: 'iris-po-btn', disabled: busy || !selectedSuggestions.length, onClick: adoptSuggestions }, '采纳所选建议'),
3202
+ React.createElement('button', { className: 'iris-po-btn', disabled: busy || !suggestionUndo.length, onClick: undoSuggestions }, '撤回上次采纳'))) : null) : null,
3203
+ React.createElement('details', { className: 'iris-po-config' }, React.createElement('summary', {}, '原文与改动'),
3204
+ React.createElement('textarea', { 'aria-label': '优化前原文', readOnly: true, value: result.original }),
3205
+ React.createElement('div', { className: 'iris-po-muted' }, '以下折叠相同首尾,显示中间替换区:'),
3206
+ React.createElement('div', { className: 'iris-po-diff' },
3207
+ changes.removed ? React.createElement('del', {}, changes.removed) : null,
3208
+ changes.added ? React.createElement('ins', {}, changes.added) : null,
3209
+ !changes.removed && !changes.added ? '与原文相同' : null)),
3210
+ React.createElement('details', { className: 'iris-po-config' }, React.createElement('summary', {}, '本次参与规则'),
3211
+ (result.rules || []).length ? (result.rules || []).map(function (rule) {
3212
+ return React.createElement('div', { className: 'iris-po-diff', key: rule.id }, rule.label + '(' + (rule.kind === 'optimization' ? '指导改写' : rule.position === 'prefix' ? '输出前缀' : '输出后缀') + ')\n' + rule.text);
3213
+ }) : React.createElement('div', { className: 'iris-po-muted' }, '未选择额外规则'),
3214
+ result.processing ? React.createElement('div', { className: 'iris-po-diff' }, '优化处理规则:\n' + result.processing.systemPrompt + '\n\n目标模板:\n' + result.processing.targetTemplate) : null),
3215
+ React.createElement('div', { className: 'iris-po-route' }, result.route ? '实际使用:' + result.route.provider + ' / ' + result.route.model + ' · 思考:' + (result.route.reasoningEffort === 'provider-default' ? '供应商默认' : result.route.reasoningEffort) : '本次只组装,未调用模型'),
2407
3216
  React.createElement('div', { className: 'iris-po-actions' },
2408
- React.createElement('button', { className: 'iris-po-primary', onClick: function () { replaceDraft(result.optimized); } }, '写回输入框'),
3217
+ React.createElement('button', { className: 'iris-po-primary', onClick: function () { replaceDraft(edited); } }, '写回输入框'),
2409
3218
  React.createElement('button', { className: 'iris-po-btn', onClick: copyResult }, '复制'),
2410
- React.createElement('button', { className: 'iris-po-btn', onClick: optimize }, '再优化'),
3219
+ React.createElement('button', { className: 'iris-po-btn', disabled: busy, onClick: function () { optimize(result.mode); } }, '重新生成预览'),
2411
3220
  React.createElement('button', { className: 'iris-po-btn', onClick: function () { replaceDraft(result.original); } }, '恢复原文'))) : null,
2412
- note ? React.createElement('div', { className: 'iris-po-note' }, note) : null,
3221
+ note ? React.createElement('div', { className: 'iris-po-note', role: 'status' }, note) : null)),
2413
3222
  React.createElement('details', { className: 'iris-po-config' },
2414
- React.createElement('summary', {}, 'JSON 配置'),
3223
+ React.createElement('summary', {}, '指令与模型设置'),
3224
+ React.createElement('div', { className: 'iris-po-route', title: routeLabel }, '模型:' + routeLabel + ' · 思考:' + reasoningLabel + ' · 输出:' + outputBudget + ' tokens'),
3225
+ React.createElement('details', {}, React.createElement('summary', {}, '查看当前功能的优化指令'),
3226
+ React.createElement('div', { className: 'iris-po-template' }, config && config.systemPrompt || '配置尚未载入'),
3227
+ React.createElement('div', { className: 'iris-po-template' }, targetTemplate || '当前后端未提供此目标模板')),
3228
+ React.createElement('div', { className: 'iris-po-result-label' }, 'JSON 配置'),
2415
3229
  React.createElement('div', { className: 'iris-po-muted' }, '可导出后修改 systemPrompt、targets、route 和 generation,再重新导入。'),
2416
3230
  React.createElement('div', { className: 'iris-po-actions' },
2417
3231
  React.createElement('button', { className: 'iris-po-btn', onClick: exportConfig }, '导出'),
@@ -2428,7 +3242,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
2428
3242
  type: 'button',
2429
3243
  title: refCount ? '含结构化引用的草稿暂不支持优化' : '优化当前输入框提示词',
2430
3244
  'aria-expanded': open ? 'true' : 'false',
2431
- onClick: function () { setOpen(!open); }
3245
+ onClick: function () { if (open) closePanel(); else setOpen(true); }
2432
3246
  }, '🫧'),
2433
3247
  panel);
2434
3248
  }
@@ -2473,8 +3287,10 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
2473
3287
  var state = useIrisState(5000);
2474
3288
  var providers = (state && state.providers) || [];
2475
3289
  var fallbackConfigured = providers.some(function (p) { return p.enabled && p.apiKeyHint; });
2476
- var overallHealth = state && state.health && HEALTH_META[state.health.overall]
3290
+ var overallState = state && state.health && HEALTH_META[state.health.overall]
2477
3291
  ? state.health.overall : (fallbackConfigured ? 'configured' : 'unconfigured');
3292
+ var overallCooling = overallState === 'failed' && state.health.retryAt;
3293
+ var overallHealth = overallCooling ? 'cooling' : overallState;
2478
3294
  var running = (state && state.tasks && state.tasks.running) || [];
2479
3295
  var attention = (state && state.tasks && state.tasks.attention) || [];
2480
3296
 
@@ -2600,7 +3416,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
2600
3416
  onPointerMove: onPointerMove,
2601
3417
  onPointerUp: onPointerUp,
2602
3418
  onPointerCancel: onPointerCancel,
2603
- title: 'Iris ' + healthText({ status: overallHealth }) + ':点击打开快捷面板'
3419
+ title: 'Iris ' + healthText({ status: overallState, rateLimited: !!overallCooling, retryAt: overallCooling || undefined }) + ':点击打开快捷面板'
2604
3420
  },
2605
3421
  React.createElement('div', { className: 'iris-bubble-btn' }, React.createElement(IRIS_MARK_SVG)),
2606
3422
  badge,