@mokuyoaxis/dsh-iris 0.2.5 → 0.2.6

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 (49) hide show
  1. package/.dsh/skills/iris-use/SKILL.md +2 -2
  2. package/.dsh/skills/iris-use/references/media-contract.md +2 -2
  3. package/CHANGELOG.md +27 -1
  4. package/README.en.md +27 -19
  5. package/README.md +28 -18
  6. package/bin/dsh-iris.js +29 -12
  7. package/docs/AGENT_QUICKSTART.md +7 -7
  8. package/docs/CLI_MANAGEMENT.md +5 -1
  9. package/docs/DSH_COMPATIBILITY.md +8 -4
  10. package/docs/DSH_PROVIDER_CONNECTIONS.md +34 -0
  11. package/docs/FUNCTION_MODEL_ASSIGNMENTS.md +2 -0
  12. package/docs/FUNCTION_MODEL_POOLS.md +84 -0
  13. package/docs/HEADLESS_CLI.md +7 -7
  14. package/docs/HOST_ADAPTER_CONTRACT.md +7 -2
  15. package/docs/HOST_DOCTOR.md +2 -2
  16. package/docs/MUSIC.md +2 -2
  17. package/docs/PROVIDER_HEALTH.md +26 -2
  18. package/docs/ROADMAP.md +5 -3
  19. package/docs/SECURITY.md +12 -2
  20. package/docs/VISION_CLI.md +2 -2
  21. package/docs/releases/0.2.6.md +50 -0
  22. package/lib/actions.js +137 -30
  23. package/lib/adapters.js +36 -9
  24. package/lib/api.js +2 -1
  25. package/lib/client.js +717 -180
  26. package/lib/composite-vision-routing.js +6 -6
  27. package/lib/config.js +140 -29
  28. package/lib/doctor.js +8 -1
  29. package/lib/dsh-core-adapter.js +22 -11
  30. package/lib/dsh-host-adapter.js +10 -0
  31. package/lib/dsh-provider-config.js +35 -0
  32. package/lib/dsh-provider-source.js +102 -0
  33. package/lib/headless-vision.js +5 -5
  34. package/lib/host-contract.js +1 -0
  35. package/lib/http-vision-model-adapter.js +5 -2
  36. package/lib/index.js +54 -35
  37. package/lib/model-directory-service.js +53 -0
  38. package/lib/model-directory.js +103 -0
  39. package/lib/models.js +71 -6
  40. package/lib/ocr-model-routing.js +2 -2
  41. package/lib/provider-adapters.js +42 -9
  42. package/lib/provider-catalog.js +14 -9
  43. package/lib/provider-config-scope.js +16 -0
  44. package/lib/provider-config-service.js +82 -13
  45. package/lib/provider-health.js +33 -0
  46. package/lib/provider-protocol.js +30 -0
  47. package/lib/vision-model-routing.js +25 -12
  48. package/package.json +2 -2
  49. package/user_guide.md +25 -19
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.5';
24
+ var IRIS_CLIENT_VERSION = '0.2.6';
25
25
  var HOST_CLIENT_PROTOCOL_VERSION = 0;
26
26
  var reportedSeats = [];
27
27
 
@@ -81,6 +81,49 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
81
81
  '.iris-wb-link:hover { text-decoration: underline; }',
82
82
  '.iris-wb-muted { color: var(--dsw-alias-label-secondary, #9a9a9a); font-size: 12px; }',
83
83
  '.iris-wb-empty { color: var(--dsw-alias-label-secondary, #9a9a9a); font-size: 12px; padding: 6px 2px; }',
84
+ /* 工作台页面共用视觉规则;不改变对话泡泡和 composer 的布局。 */
85
+ '.iris-workbench-page { --iris-workbench-accent: var(--dsw-alias-brand-primary, #8c9eff); display: grid; gap: 10px; }',
86
+ '.iris-workbench-page *, .iris-workbench-page *::before { box-sizing: border-box; }',
87
+ '.iris-workbench-page [hidden], .iris-health [hidden] { display: none !important; }',
88
+ '.iris-workbench-page > .iris-wb-head { justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px; margin: 2px 0 4px; }',
89
+ '.iris-workbench-page > .iris-wb-head .iris-wb-title { font-size: 20px; font-weight: 650; letter-spacing: -.03em; }',
90
+ '.iris-wb-section { min-width: 0; border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 12px; background: var(--dsw-alias-bg-layer-1, #26262e); }',
91
+ '.iris-wb-section-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 10px 14px; list-style: none; }',
92
+ '.iris-wb-section-head::-webkit-details-marker { display: none; }',
93
+ '.iris-wb-section-head h2 { margin: 0; font-size: 15px; font-weight: 600; letter-spacing: -.01em; }',
94
+ '.iris-wb-section-head p { margin: 4px 0 0; font-size: 12px; color: var(--dsw-alias-label-secondary, #9a9a9a); }',
95
+ 'details.iris-wb-section > summary { cursor: pointer; border-radius: inherit; }',
96
+ '.iris-wb-section-arrow { flex-shrink: 0; display: inline-flex; gap: 7px; align-items: center; font-size: 11px; color: var(--dsw-alias-label-secondary, #9a9a9a); }',
97
+ '.iris-wb-section-arrow svg { transition: transform .15s; }',
98
+ 'details.iris-wb-section[open] > summary .iris-wb-section-arrow svg { transform: rotate(180deg); }',
99
+ '.iris-wb-section-body { min-width: 0; padding: 0 14px 14px; display: grid; gap: 8px; }',
100
+ '.iris-workbench-page .iris-wb-sec { margin: 4px 0 0; font-size: 13px; color: var(--dsw-alias-label-primary, #e6e6e6); }',
101
+ '.iris-workbench-page .iris-pm, .iris-workbench-page .iris-cap-grid, .iris-workbench-page .iris-act-group { gap: 8px; }',
102
+ '.iris-workbench-page .iris-pm-card, .iris-workbench-page .iris-wb-card, .iris-workbench-page .iris-act-card, .iris-workbench-page .iris-cap-row { border-radius: 10px; }',
103
+ '.iris-workbench-page .iris-pm-card, .iris-workbench-page .iris-cap-row { padding: 10px; }',
104
+ '.iris-workbench-page .iris-pm-card { gap: 8px; }',
105
+ '.iris-workbench-page .iris-pm-body { display: grid; gap: 8px; padding-top: 10px; border-top: 1px solid var(--dsw-alias-border-l1, #3a3a44); }',
106
+ '.iris-workbench-page .iris-pm-field { gap: 5px; font-size: 12px; }',
107
+ '.iris-workbench-page :is(.iris-pm-btn, .iris-pm-add, .iris-ff-btn, .iris-act-run, .iris-clean-btn) { min-height: 32px; padding: 5px 9px; border-radius: 8px; font-family: inherit; font-size: 12px; }',
108
+ '.iris-workbench-page :is(button, select, input):focus-visible, .iris-wb-section-head:focus-visible { outline: 2px solid var(--iris-workbench-accent, #8c9eff); outline-offset: 3px; }',
109
+ '.iris-workbench-page :is(input, textarea, select) { font-family: inherit; accent-color: var(--iris-workbench-accent); }',
110
+ '.iris-workbench-page input:not([type=checkbox]):not([type=radio]):not([type=file]), .iris-workbench-page textarea, .iris-workbench-page select { min-width: 0; max-width: 100%; min-height: 34px; padding: 5px 8px; border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 8px; background: var(--dsw-alias-bg-layer-2, #32323c); color: var(--dsw-alias-label-primary, #e6e6e6); font-size: 12px; }',
111
+ '.iris-workbench-page .iris-pm-note { line-height: 1.65; }',
112
+ '.iris-workbench-page .iris-pm-mrows { max-height: 400px; gap: 8px; }',
113
+ '.iris-workbench-page .iris-pm-mrow { min-width: 0; padding: 10px; gap: 6px; flex-wrap: wrap; border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 10px; }',
114
+ '.iris-workbench-page .iris-pm-mname { flex-basis: 100%; white-space: normal; overflow-wrap: anywhere; font-size: 13px; }',
115
+ '.iris-workbench-page .iris-pm-mrow > label { display: flex; align-items: center; flex: 1 1 230px; flex-wrap: wrap; gap: 6px; }',
116
+ '.iris-workbench-page .iris-pm-mrow > label select { flex: 1; }',
117
+ '.iris-workbench-page .iris-cap-row { gap: 7px; }',
118
+ '.iris-workbench-page .iris-cap-row > select:first-of-type { margin-left: auto; }',
119
+ '.iris-workbench-page .iris-cap-row > .iris-pm-note, .iris-workbench-page .iris-cap-row > .iris-cap-feedback { flex-basis: 100%; }',
120
+ '.iris-workbench-page .iris-cap-row > .iris-pm-model-sel { margin-left: 0; }',
121
+ '.iris-workbench-page .iris-cap-chip { padding: 3px 7px; border-radius: 8px; overflow-wrap: anywhere; }',
122
+ '.iris-workbench-page .iris-cap-confirm { padding: 12px; border-radius: 10px; }',
123
+ '.iris-workbench-page .iris-act-head { min-height: 40px; padding: 9px 12px; }',
124
+ '.iris-workbench-page .iris-act-body { padding: 10px 12px 12px; gap: 8px; }',
125
+ '.iris-workbench-page .iris-doctor-row { font-size: 12px; line-height: 1.6; }',
126
+ '@media (max-width: 640px) { .iris-wb-section-head { padding: 12px; } .iris-wb-section-body { padding: 0 12px 12px; } .iris-workbench-page :is(.iris-pm-btn, .iris-pm-add, .iris-ff-btn, .iris-act-run, .iris-clean-btn) { min-height: 40px; } .iris-workbench-page input:not([type=checkbox]):not([type=radio]):not([type=file]), .iris-workbench-page textarea, .iris-workbench-page select { min-height: 36px; } }',
84
127
  '.iris-progress-dock { display: grid; gap: 6px; }',
85
128
  '.iris-progress-row { font-size: 12px; display: flex; gap: 8px; align-items: center; color: var(--dsw-alias-label-secondary, #9a9a9a); }',
86
129
  '.iris-wb-card { cursor: pointer; }',
@@ -157,7 +200,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
157
200
  '.iris-pm-add:hover { border-color: var(--dsw-alias-brand-primary, #7aa2f7); }',
158
201
  '.iris-pm-card { background: var(--dsw-alias-bg-layer-1, #26262e); border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 8px; padding: 8px 10px; display: grid; gap: 6px; }',
159
202
  '.iris-pm-card.open { border-color: var(--dsw-alias-brand-primary, #7aa2f7); }',
160
- '.iris-pm-top { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; cursor: pointer; }',
203
+ '.iris-pm-top { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; width: 100%; padding: 0; border: 0; background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; }',
161
204
  '.iris-pm-name { font-weight: 600; font-size: 13px; flex: 1; }',
162
205
  '.iris-pm-btn { background: transparent; color: var(--dsw-alias-label-secondary, #9a9a9a); border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 4px; font-size: 11px; padding: 2px 7px; cursor: pointer; }',
163
206
  '.iris-pm-btn:hover { color: var(--dsw-alias-label-primary, #e6e6e6); border-color: var(--dsw-alias-label-secondary, #9a9a9a); }',
@@ -168,25 +211,82 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
168
211
  '.iris-pm-cap { color: var(--dsw-alias-brand-primary, #7aa2f7); font-size: 10px; }',
169
212
  '.iris-pm-note { font-size: 11px; color: var(--dsw-alias-label-secondary, #9a9a9a); }',
170
213
  '.iris-pm-field { display: grid; gap: 3px; }',
214
+ 'details[data-provider-directory] { min-width: 0; padding: 10px 12px; border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 8px; }',
215
+ 'details[data-provider-directory] > summary { cursor: pointer; overflow-wrap: anywhere; }',
216
+ 'details[data-provider-directory] p { margin: 6px 0; overflow-wrap: anywhere; }',
217
+ 'details[data-provider-directory] > summary:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary, #6b78e8); outline-offset: 3px; }',
171
218
  '.iris-pm-field input, .iris-pm-field textarea { 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: 4px 6px; font-size: 12px; width: 100%; box-sizing: border-box; }',
172
219
  '.iris-pm-act { font-size: 11px; color: var(--dsw-alias-label-secondary, #9a9a9a); }',
173
220
  '.iris-pm-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--dsw-alias-label-tertiary, #73737d); margin-right: 4px; }',
174
221
  '.iris-pm-dot.configured { background: #6f9df6; box-shadow: 0 0 7px rgba(111,157,246,.55); }',
175
222
  '.iris-pm-dot.verified { background: var(--dsw-alias-state-success-primary, #6fcf6f); box-shadow: 0 0 7px var(--dsw-alias-state-success-primary, #6fcf6f); }',
176
223
  '.iris-pm-dot.failed { background: #b9656d; box-shadow: 0 0 6px rgba(185,101,109,.38); opacity: .82; }',
177
- '.iris-health-grid { display: grid; gap: 5px; }',
178
- '.iris-health-row { display: grid; grid-template-columns: minmax(72px, auto) 1fr auto; align-items: center; gap: 7px; padding: 6px 9px; border-radius: 9px; background: var(--dsw-alias-bg-layer-1, #26262e); font-size: 11px; }',
179
- '.iris-health-name { font-weight: 600; }',
180
- '.iris-health-state { color: var(--dsw-alias-label-secondary, #9a9a9a); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }',
181
- '.iris-health-row.failed .iris-health-state { color: #c87a81; }',
182
- '.iris-health-row.configured .iris-health-state { color: #7da8fa; }',
183
- '.iris-health-row.verified .iris-health-state { color: var(--dsw-alias-state-success-primary, #6fcf6f); }',
224
+ '.iris-health { --iris-health-accent: var(--dsw-alias-brand-primary, #8c9eff); container-type: inline-size; padding: 0; border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 12px; background: var(--dsw-alias-bg-layer-1, #26262e); }',
225
+ '.iris-health *, .iris-health *::before { box-sizing: border-box; }',
226
+ '.iris-health-head, .iris-health-title-line, .iris-health-card-top, .iris-health-card-foot, .iris-health-detail-head, .iris-health-model-head, .iris-health-confirm-actions { display: flex; align-items: center; gap: 10px; }',
227
+ '.iris-health-head { justify-content: space-between; flex-wrap: wrap; padding: 10px 14px; }',
228
+ '.iris-health-content { padding: 0 14px 14px; }',
229
+ '.iris-health-toolbar { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; }',
230
+ '.iris-health-eyebrow { color: var(--iris-health-accent); font-size: 10px; font-weight: 600; letter-spacing: .14em; margin-bottom: 6px; }',
231
+ '.iris-health h2, .iris-health h3, .iris-health p { margin: 0; }',
232
+ '.iris-health h2 { font-size: 15px; font-weight: 600; letter-spacing: -.01em; }',
233
+ '.iris-health-subtitle, .iris-health-caption { font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-secondary, #9a9a9a); }',
234
+ '.iris-health-subtitle { margin-top: 5px !important; }',
235
+ '.iris-health-btn { display: inline-flex; align-items: center; justify-content: center; gap: 7px; min-height: 32px; padding: 5px 10px; border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 8px; background: var(--dsw-alias-bg-layer-2, #32323c); color: var(--dsw-alias-label-primary, #e6e6e6); font: inherit; font-size: 12px; white-space: nowrap; cursor: pointer; }',
236
+ '.iris-health-btn:hover { border-color: var(--iris-health-accent); }',
237
+ '.iris-health-btn.primary { border-color: transparent; background: var(--iris-health-accent); color: var(--dsw-alias-label-primary-inverted, #1b2034); font-weight: 600; }',
238
+ '.iris-health-btn:disabled { opacity: .5; cursor: default; }',
239
+ '.iris-health button:focus-visible { outline: 2px solid var(--iris-health-accent); outline-offset: 3px; }',
240
+ '.iris-health-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }',
241
+ '.iris-health-row { --iris-health-tone: var(--dsw-alias-label-secondary, #9a9a9a); display: grid; gap: 10px; min-width: 0; padding: 12px; border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 10px; background: var(--dsw-alias-bg-layer-1, #26262e); color: var(--dsw-alias-label-primary, #e6e6e6); font: inherit; text-align: left; cursor: pointer; transition: border-color .15s, transform .15s; }',
242
+ '.iris-health-row:hover:not(:disabled) { transform: translateY(-2px); border-color: var(--iris-health-accent); }',
243
+ '.iris-health-row:disabled { cursor: wait; opacity: .7; }',
244
+ '.iris-health-row.active { border-color: var(--iris-health-accent); box-shadow: inset 0 0 0 1px var(--iris-health-accent); }',
245
+ '.iris-health-row.configured { --iris-health-tone: var(--iris-health-accent); }',
246
+ '.iris-health-row.verified { --iris-health-tone: var(--dsw-alias-state-success-primary, #6fcf6f); }',
247
+ '.iris-health-row.failed, .iris-health-row.rate-limited { --iris-health-tone: var(--dsw-alias-state-error-primary, #e06c6c); }',
248
+ '.iris-health-row.cooling { --iris-health-tone: #c68b23; }',
249
+ '.iris-health-icon { display: inline-flex; align-items: center; justify-content: center; flex: 0 0 32px; height: 32px; border-radius: 9px; background: rgba(132,115,238,.1); color: var(--iris-health-accent); }',
250
+ '.iris-health-card-top { justify-content: space-between; }',
251
+ '.iris-health-name { font-size: 14px; font-weight: 600; }',
252
+ '.iris-health-state { display: inline-flex; align-items: center; gap: 5px; margin-top: 7px; font-size: 11px; color: var(--iris-health-tone); }',
253
+ '.iris-health-state::before { content: ""; width: 5px; height: 5px; border-radius: 50%; background: currentColor; }',
254
+ '.iris-health-card-foot { justify-content: space-between; min-width: 0; padding-top: 8px; border-top: 1px solid var(--dsw-alias-border-l1, #3a3a44); font-size: 11px; color: var(--dsw-alias-label-secondary, #9a9a9a); }',
255
+ '.iris-health-link { color: var(--iris-health-accent); white-space: nowrap; }',
256
+ '.iris-health-footer { margin-top: 13px !important; font-size: 11px; line-height: 1.6; color: var(--dsw-alias-label-secondary, #9a9a9a); }',
257
+ '.iris-health-notice { margin-top: 12px; padding: 11px 13px; border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 10px; font-size: 12px; line-height: 1.6; overflow-wrap: anywhere; }',
258
+ '.iris-health-notice.error { color: var(--dsw-alias-state-error-primary, #e06c6c); }',
259
+ '.iris-health-detail { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--dsw-alias-border-l1, #3a3a44); }',
260
+ '.iris-health-detail-head { justify-content: space-between; align-items: flex-start; margin-bottom: 10px; }',
261
+ '.iris-health-detail-head > button { flex-shrink: 0; }',
262
+ '.iris-health h3 { font-size: 15px; font-weight: 600; margin-bottom: 4px; }',
263
+ '.iris-health-models { display: grid; gap: 8px; }',
264
+ '.iris-health-model { padding: 10px 12px; border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 10px; background: var(--dsw-alias-bg-layer-1, #26262e); min-width: 0; }',
265
+ '.iris-health-model-head { justify-content: space-between; align-items: flex-start; }',
266
+ '.iris-health-model-title { min-width: 0; flex: 1; }',
267
+ '.iris-health-model-title strong { display: block; font-size: 13px; font-weight: 550; overflow-wrap: anywhere; }',
268
+ '.iris-health-result { display: flex; gap: 7px; align-items: flex-start; margin-top: 10px; font-size: 12px; line-height: 1.6; overflow-wrap: anywhere; }',
269
+ '.iris-health-result.passed { color: var(--dsw-alias-state-success-primary, #6fcf6f); }',
270
+ '.iris-health-result.failed, .iris-health-result.unknown { color: var(--dsw-alias-state-error-primary, #e06c6c); }',
271
+ '.iris-health-result.running { color: var(--iris-health-accent); }',
272
+ '.iris-health-spinner { display: inline-block; flex: 0 0 13px; width: 13px; height: 13px; margin-top: 3px; border: 1.5px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: iris-health-spin .8s linear infinite; }',
273
+ '.iris-health-confirm { margin-top: 12px; padding: 14px; border-radius: 11px; border: 1px solid var(--iris-health-accent); background: rgba(132,115,238,.06); font-size: 12px; line-height: 1.7; }',
274
+ '.iris-health-confirm-actions { margin-top: 12px; flex-wrap: wrap; }',
275
+ '@keyframes iris-health-spin { to { transform: rotate(360deg); } }',
276
+ '@container (max-width: 650px) { .iris-health-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }',
277
+ '@container (max-width: 420px) { .iris-health-grid { grid-template-columns: 1fr; } }',
278
+ '@media (max-width: 640px) { .iris-health-head { flex-wrap: nowrap; padding: 12px; } .iris-health-head > div { flex: 1; min-width: 0; } .iris-health-head > button { flex-shrink: 0; } .iris-health-content { padding: 0 12px 12px; } .iris-health-grid { grid-template-columns: 1fr; gap: 8px; } .iris-health-row { position: relative; grid-template-columns: 32px minmax(0, 1fr); gap: 5px 9px; padding: 10px; } .iris-health-card-top { display: contents; } .iris-health-card-top .iris-health-icon { grid-row: 1 / 3; grid-column: 1; } .iris-health-card-top .iris-health-caption { position: absolute; right: 10px; top: 10px; font-size: 10px; } .iris-health-card-main { grid-column: 2; min-width: 0; } .iris-health-name { padding-right: 58px; } .iris-health-state { display: flex; line-height: 1.6; margin-top: 5px; } .iris-health-state::before { flex-shrink: 0; } .iris-health-card-foot { grid-column: 2; border: 0; padding-top: 0; font-size: 10px; flex-wrap: wrap; gap: 3px; } .iris-health-link { margin-left: auto; } .iris-health-btn { min-height: 40px; } .iris-health-model-head { flex-wrap: wrap; } .iris-health-model-head > .iris-health-btn { margin-left: auto; } }',
279
+ '@media (prefers-reduced-motion: reduce) { .iris-health-row { transition: none; } .iris-health-row:hover { transform: none; } .iris-health-spinner { animation: none; } }',
184
280
  '.iris-cap-grid { display: grid; gap: 6px; }',
185
281
  '.iris-cap-row { display: flex; align-items: center; flex-wrap: wrap; gap: 5px; background: var(--dsw-alias-bg-layer-1, #26262e); border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 8px; padding: 6px 9px; font-size: 12px; }',
186
282
  '.iris-cap-name { font-weight: 600; min-width: 64px; }',
187
283
  '.iris-cap-chip { display: inline-flex; align-items: center; gap: 3px; background: var(--dsw-alias-bg-layer-2, #32323c); border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 10px; padding: 1px 6px; font-size: 11px; }',
188
284
  '.iris-cap-chip b { color: var(--dsw-alias-brand-primary, #7aa2f7); }',
189
285
  '.iris-cap-auto { color: var(--dsw-alias-label-secondary, #9a9a9a); font-size: 11px; font-style: italic; }',
286
+ '.iris-cap-feedback { flex-basis: 100%; color: var(--dsw-alias-brand-primary, #7aa2f7); }',
287
+ '.iris-cap-feedback.saved { color: var(--dsw-alias-state-success-primary, #6fcf6f); }',
288
+ '.iris-cap-feedback.unknown, .iris-cap-compatibility { color: var(--dsw-alias-state-error-primary, #e06c6c); }',
289
+ '.iris-cap-confirm { flex-basis: 100%; padding: 8px; border: 1px solid var(--dsw-alias-brand-primary, #7aa2f7); border-radius: 6px; }',
190
290
  '.iris-pm-model-sel { 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: 4px 6px; font-size: 12px; width: 100%; }',
191
291
  '.iris-wb-drawer .iris-wb-loading { color: var(--dsw-alias-label-secondary, #9a9a9a); font-size: 12px; }',
192
292
  '.iris-bubble { position: fixed; z-index: 9999; cursor: grab; user-select: none; touch-action: none; }',
@@ -268,7 +368,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
268
368
  '.iris-pm-mname { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }',
269
369
  '.iris-pm-mtag { font-size: 9px; color: var(--dsw-alias-brand-primary, #7aa2f7); border: 1px solid var(--dsw-alias-brand-primary, #7aa2f7); border-radius: 3px; padding: 0 3px; margin-left: 4px; }',
270
370
  '.iris-pm-mcaps { display: flex; gap: 3px; }',
271
- '.iris-pm-ver { font-size: 10px; padding: 0 4px; border-radius: 3px; background: var(--dsw-alias-bg-layer-2, #32323c); color: var(--dsw-alias-label-secondary, #9a9a9a); }',
371
+ '.iris-pm-ver { font: inherit; font-size: 11px; padding: 3px 7px; border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 7px; background: var(--dsw-alias-bg-layer-2, #32323c); color: var(--dsw-alias-label-secondary, #9a9a9a); cursor: pointer; }',
272
372
  '.iris-pm-ver.configured { color: #7da8fa; }',
273
373
  '.iris-pm-ver.verified { color: var(--dsw-alias-state-success-primary, #6fcf6f); }',
274
374
  '.iris-pm-ver.failed { color: #c87a81; border-color: rgba(185,101,109,.5); }',
@@ -370,13 +470,15 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
370
470
  ].join('\n');
371
471
 
372
472
  function adoptStyles() {
373
- if (document.getElementById(STYLE_ID) !== null) return;
374
- var style = document.createElement('style');
375
- style.id = STYLE_ID;
376
- style.dataset.plugin = 'dsh-iris';
377
- style.dataset.pluginCss = STYLE_ID;
378
- style.textContent = cssText;
379
- document.head.appendChild(style);
473
+ var style = document.getElementById(STYLE_ID);
474
+ if (style === null) {
475
+ style = document.createElement('style');
476
+ style.id = STYLE_ID;
477
+ style.dataset.plugin = 'dsh-iris';
478
+ style.dataset.pluginCss = STYLE_ID;
479
+ document.head.appendChild(style);
480
+ }
481
+ if (style.textContent !== cssText) style.textContent = cssText;
380
482
  }
381
483
 
382
484
  var inject = ['slots'];
@@ -514,7 +616,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
514
616
 
515
617
  function healthText(health) {
516
618
  var status = health && HEALTH_META[health.status] ? health.status : 'unconfigured';
517
- var text = health && health.rateLimited
619
+ var text = health && health.modelUnavailable ? '模型不存在,已暂停自动选用' : health && health.rateLimited
518
620
  ? rateLimitLabel(health) + (health.retryAt
519
621
  ? ',' + new Date(health.retryAt).toLocaleTimeString() + ' 恢复候选资格;可提前实测'
520
622
  : ',实测成功后恢复')
@@ -524,25 +626,238 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
524
626
  return text + (age ? ' · ' + age : '');
525
627
  }
526
628
 
629
+ var HEALTH_ROWS = [
630
+ { cap: 'image-gen', label: '图片', action: 'draw', sample: '生成一张红色圆形测试图,可能需要约一分钟。', icon: 'M4 4h16v16H4z M4 16l5-5 4 4 3-3 4 4 M8 8h.01' },
631
+ { cap: 'video-gen', label: '视频', action: 'video', taskOnly: true, sample: '请到视频功能填写创作要求或素材,选择模型后提交验证。', icon: 'M3 6h12v12H3z M15 10l6-3v10l-6-3' },
632
+ { cap: 'tts', label: '语音', action: 'tts', sample: '合成一句“你好”,以接口返回音频作为通过依据。', icon: 'M4 10h4l5-4v12l-5-4H4z M16 8c3 2 3 6 0 8 M19 5c5 4 5 10 0 14' },
633
+ { cap: 'music-gen', label: '音乐', action: 'music', taskOnly: true, sample: '请到音乐功能填写实际创作要求,选择模型后提交验证。', icon: 'M9 17V5l11-2v12 M9 5v4l11-2 M9 17a3 3 0 1 1-3-3h3 M20 15a3 3 0 1 1-3-3h3' },
634
+ { cap: 'transcribe', label: '转写', action: 'transcribe', taskOnly: true, sample: '请到转写功能提供真实音频,选择模型后提交验证。', icon: 'M9 5a3 3 0 0 1 6 0v7a3 3 0 0 1-6 0z M5 10v2a7 7 0 0 0 14 0v-2 M12 19v3 M8 22h8' },
635
+ { cap: 'vision', label: '视觉', action: 'ocr', sample: '发送一张内置红色测试图,检查模型能否识别颜色。', icon: 'M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12z M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0' }
636
+ ];
637
+ function capabilityIcon(row) {
638
+ return React.createElement('svg', { width: 20, height: 20, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor',
639
+ strokeWidth: 1.6, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true }, React.createElement('path', { d: row.icon }));
640
+ }
641
+ function inspectionSupported(snapshot) {
642
+ return !!(snapshot && snapshot.policies && snapshot.readiness && HEALTH_ROWS.every(function (row) {
643
+ return snapshot.policies[row.cap] && ['auto', 'manual', 'disabled'].includes(snapshot.policies[row.cap].mode) && snapshot.readiness[row.cap];
644
+ }));
645
+ }
646
+ function inspectionModels(snapshot, row, requested) {
647
+ if (!snapshot) return [];
648
+ var order = snapshot.order[row.cap] || [];
649
+ var policy = snapshot.policies && snapshot.policies[row.cap];
650
+ var options = (snapshot.poolByCapability[row.cap] || []).slice();
651
+ if (requested && requested.ref && !options.some(function (option) { return option.ref === requested.ref; })) {
652
+ options.push({ ...requested, removed: true });
653
+ }
654
+ return options.filter(function (option) {
655
+ return order.includes(option.ref) || (requested && option.providerId === requested.providerId && option.id === requested.modelId);
656
+ }).sort(function (a, b) { return order.indexOf(a.ref) - order.indexOf(b.ref); }).map(function (option) {
657
+ var provider = snapshot.providers.find(function (item) { return item.id === option.providerId; });
658
+ var model = provider && provider.models.find(function (item) { return item.id === option.id; });
659
+ var capability = snapshot.health && snapshot.health.capabilities && snapshot.health.capabilities[row.cap];
660
+ var evidence = capability && (capability.candidates || []).find(function (item) { return item.providerId === option.providerId && item.modelId === option.id; });
661
+ var health = evidence || model && model.health && model.health[row.cap] || { status: 'configured' };
662
+ var host = option.ref === 'host:current-vision';
663
+ var reason = !inspectionSupported(snapshot) ? '请先让 DSH 加载新版功能池接口,再刷新配置。'
664
+ : policy.mode === 'disabled' ? '此功能已停用,请先调整调用方式。'
665
+ : option.removed ? '此模型已从当前配置中移除。'
666
+ : !order.includes(option.ref) ? '此模型未纳入当前功能池,请先调整调用方式或所选模型。'
667
+ : option.pendingConfirmation ? '请先在供应商中确认模型用途。'
668
+ : !host && (!provider || provider.enabled === false) ? '供应商当前不可用,请先检查设置。'
669
+ : provider && provider.dshProvider && (!provider.connection || !provider.connection.available) ? 'DSH 连接尚未就绪,请先检查宿主供应商设置。'
670
+ : option.protocolSupported === false ? option.reason || '当前接口协议不支持此功能。'
671
+ : option.unavailable && !health.rateLimited && !health.modelUnavailable && !option.modelUnavailable ? option.reason || '连接尚未就绪,请先检查供应商设置。' : '';
672
+ return { ...option, providerName: provider ? provider.name : 'DSH 宿主', health: health,
673
+ taskOnly: row.taskOnly || host, reason: reason, canTest: !reason };
674
+ });
675
+ }
527
676
  function CapabilityHealthOverview() {
528
677
  var state = useIrisState(5000);
529
- var rows = [
530
- ['image-gen', '图片'],
531
- ['video-gen', '视频'],
532
- ['tts', '语音'],
533
- ['transcribe', '转写'],
534
- ['vision', '视觉']
535
- ];
536
- return React.createElement('div', { className: 'iris-health-grid' }, rows.map(function (row) {
537
- var health = capabilityHealth(state, row[0]);
538
- var meta = HEALTH_META[health.status] || HEALTH_META.unconfigured;
539
- return React.createElement('div', { key: row[0], className: 'iris-health-row ' + healthClass(health) },
540
- React.createElement('span', { className: 'iris-health-name' },
541
- React.createElement('span', { className: 'iris-pm-dot ' + healthClass(health) }, health.retryAt ? '◷' : meta.symbol),
542
- row[1]),
543
- React.createElement('span', { className: 'iris-health-state', title: healthText(health) }, healthText(health)),
544
- React.createElement('span', { className: 'iris-wb-muted' }, String(health.candidateCount || 0) + ' 路'));
545
- }));
678
+ var expandedPair = React.useState(false), expanded = expandedPair[0];
679
+ var snapshotPair = React.useState(null), snapshot = snapshotPair[0];
680
+ var loadingPair = React.useState(false), loading = loadingPair[0];
681
+ var freshPair = React.useState(false), fresh = freshPair[0];
682
+ var errorPair = React.useState('');
683
+ var checkedPair = React.useState('');
684
+ var selectedPair = React.useState(null), selected = selectedPair[0];
685
+ var requestedPair = React.useState(null), requested = requestedPair[0];
686
+ var confirmPair = React.useState(null), confirmation = confirmPair[0];
687
+ var resultsPair = React.useState({}), results = resultsPair[0];
688
+ var busyPair = React.useState(null), busy = busyPair[0];
689
+ var running = React.useRef(false), readVersion = React.useRef(0);
690
+ function noteResult(key, value) { resultsPair[1](function (previous) { return { ...previous, [key]: value }; }); }
691
+ function load() {
692
+ var version = ++readVersion.current;
693
+ var controller = new AbortController();
694
+ var timer = setTimeout(function () { controller.abort(); }, 10000);
695
+ loadingPair[1](true); freshPair[1](false); errorPair[1]('');
696
+ return Promise.all([
697
+ postAction('assignments_get', {}, { signal: controller.signal }),
698
+ postAction('providers_list', {}, { signal: controller.signal }),
699
+ fetch('/iris/api/state', { signal: controller.signal }).then(function (r) {
700
+ if (!r.ok) throw new Error('健康状态读取失败(HTTP ' + r.status + ')');
701
+ return r.json();
702
+ })
703
+ ]).then(function (values) {
704
+ if (version !== readVersion.current) return null;
705
+ if (!values[0].ok || values[0].d.ok !== true || !values[1].ok || values[1].d.ok !== true) throw new Error('配置读取失败,请重新检查');
706
+ var data = { ...values[0].d, providers: values[1].d.providers, health: values[2].health };
707
+ snapshotPair[1](data); freshPair[1](true); checkedPair[1](new Date().toLocaleTimeString()); return data;
708
+ }).catch(function (error) {
709
+ if (version === readVersion.current) errorPair[1](error.name === 'AbortError' ? '配置检查超时,请重试;未发起模型实测。' : error.message);
710
+ return null;
711
+ }).finally(function () { clearTimeout(timer); controller.abort(); if (version === readVersion.current) loadingPair[1](false); });
712
+ }
713
+ function inspect(row, target) {
714
+ if (running.current) return;
715
+ expandedPair[1](true);
716
+ selectedPair[1](row.cap); requestedPair[1](target || null); confirmPair[1](null);
717
+ }
718
+ React.useEffect(function () {
719
+ load();
720
+ function open(event) {
721
+ var detail = event.detail || {};
722
+ var row = HEALTH_ROWS.find(function (item) { return item.cap === detail.capability; });
723
+ if (row && !running.current) {
724
+ inspect(row, detail); load();
725
+ var section = document.getElementById('iris-capability-health');
726
+ if (section) section.scrollIntoView({ block: 'start', behavior: 'smooth' });
727
+ }
728
+ }
729
+ window.addEventListener('iris-capability-inspect', open);
730
+ window.addEventListener('iris-function-configuration-changed', load);
731
+ return function () {
732
+ ++readVersion.current;
733
+ window.removeEventListener('iris-capability-inspect', open);
734
+ window.removeEventListener('iris-function-configuration-changed', load);
735
+ };
736
+ }, []);
737
+ function configure(row) {
738
+ var field = document.querySelector('[data-function="' + row.cap + '"] > select');
739
+ if (field) { revealWorkbenchItem(field); field.scrollIntoView({ block: 'center', behavior: 'smooth' }); field.focus({ preventScroll: true }); }
740
+ }
741
+ function openTask(row) {
742
+ var card = document.querySelector('[data-iris-action="' + row.action + '"]');
743
+ if (!card) return;
744
+ revealWorkbenchItem(card);
745
+ if (!card.classList.contains('open')) card.querySelector('.iris-act-head').click();
746
+ card.scrollIntoView({ block: 'start', behavior: 'smooth' });
747
+ }
748
+ function probe(row, option) {
749
+ if (running.current) return;
750
+ running.current = true; confirmPair[1](null);
751
+ requestedPair[1]({ ...option, modelId: option.id });
752
+ var key = row.cap + '|' + option.ref;
753
+ busyPair[1](key); noteResult(key, { phase: 'running', note: '正在核对当前模型范围…' });
754
+ load().then(function (current) {
755
+ var target = inspectionModels(current, row, option).find(function (item) { return item.ref === option.ref; });
756
+ if (!current || !target || !target.canTest || target.taskOnly) {
757
+ noteResult(key, { phase: 'skipped', note: target && target.reason || '配置未确认或调用范围已变化,未发起实测。' }); return;
758
+ }
759
+ noteResult(key, { phase: 'running', note: '正在实测…' + (row.cap === 'image-gen' ? '图片可能需要约一分钟,请勿重复提交。' : '请等待接口返回。') });
760
+ var controller = new AbortController();
761
+ var timer = setTimeout(function () { controller.abort(); }, 90000);
762
+ return postAction('providers_test_model', { id: target.providerId, model_id: target.id, capability: row.cap, confirm_paid: true },
763
+ { signal: controller.signal }).then(function (r) {
764
+ if (!r.ok || !r.d || r.d.ok !== true || (r.d.skipped !== true && typeof r.d.passed !== 'boolean')) {
765
+ noteResult(key, { phase: 'unknown', note: r.d && r.d.error || '未收到有效检测结果;请先核对供应商记录,避免重复计费。' }); return;
766
+ }
767
+ noteResult(key, { phase: r.d.skipped ? 'skipped' : r.d.passed ? 'passed' : 'failed',
768
+ note: String(r.d.text || '').replace(/^\[iris\][^\n]*?:/, '') || (r.d.passed ? '接口实测通过' : '接口未通过实测') });
769
+ }).catch(function () {
770
+ noteResult(key, { phase: 'unknown', note: '结果未确认,请先核对供应商记录,避免重复计费。本页不会自动重试。' });
771
+ }).finally(function () { clearTimeout(timer); });
772
+ }).then(function () { return load(); }).finally(function () { running.current = false; busyPair[1](null); });
773
+ }
774
+ var currentHealth = snapshot && snapshot.health;
775
+ if (!currentHealth || (state && state.health && state.health.generatedAt > currentHealth.generatedAt)) currentHealth = state && state.health;
776
+ var viewSnapshot = snapshot && { ...snapshot, health: currentHealth };
777
+ var supported = inspectionSupported(snapshot);
778
+ var row = HEALTH_ROWS.find(function (item) { return item.cap === selected; });
779
+ var modes = { auto: '自动选择', manual: '所选模型', disabled: '已停用' };
780
+ var verifiedCount = HEALTH_ROWS.filter(function (item) { return capabilityHealth({ health: currentHealth }, item.cap).status === 'verified'; }).length;
781
+ var blockedCount = HEALTH_ROWS.filter(function (item) { return capabilityHealth({ health: currentHealth }, item.cap).status === 'failed'; }).length;
782
+ var summary = busy ? '正在检测,请等待本次结果。' : errorPair[0] ? '配置读取失败,展开查看详情。'
783
+ : snapshot && !supported ? '服务端需重新加载,展开查看说明。'
784
+ : currentHealth ? '6 项能力 · ' + verifiedCount + ' 项近期成功' + (blockedCount ? ' · ' + blockedCount + ' 项调用受阻' : '')
785
+ : '正在读取能力状态…';
786
+ return React.createElement('section', { className: 'iris-health', id: 'iris-capability-health', 'aria-label': '能力状态与检测' },
787
+ React.createElement('div', { className: 'iris-health-head' },
788
+ React.createElement('div', {}, React.createElement('h2', {}, '能力状态与检测'),
789
+ React.createElement('p', { className: 'iris-health-subtitle', role: 'status' }, summary)),
790
+ React.createElement('button', { className: 'iris-health-btn', 'aria-expanded': expanded, 'aria-controls': 'iris-health-content',
791
+ onClick: function () { expandedPair[1](!expanded); confirmPair[1](null); } }, expanded ? '收起检测' : '展开检测')),
792
+ React.createElement('div', { className: 'iris-health-content', id: 'iris-health-content', hidden: !expanded },
793
+ React.createElement('div', { className: 'iris-health-toolbar' },
794
+ React.createElement('span', { className: 'iris-health-caption' }, '配置检查只读;实测前单独确认。'),
795
+ React.createElement('button', { className: 'iris-health-btn', 'data-health-refresh': true, disabled: loading || !!busy, onClick: function () { load(); } },
796
+ loading ? '正在检查配置…' : '检查配置')),
797
+ errorPair[0] ? React.createElement('div', { className: 'iris-health-notice error', role: 'alert' }, errorPair[0]) : null,
798
+ snapshot && !supported ? React.createElement('div', { className: 'iris-health-notice', role: 'status' },
799
+ '服务端尚未加载新版功能池接口。请重启 DSH 后刷新页面;当前只展示历史状态,暂停模型实测。') : null,
800
+ React.createElement('div', { className: 'iris-health-grid' }, HEALTH_ROWS.map(function (item) {
801
+ var health = capabilityHealth({ health: currentHealth }, item.cap);
802
+ var candidates = inspectionModels(viewSnapshot, item);
803
+ var verified = candidates.filter(function (model) { return model.health.status === 'verified'; }).length;
804
+ var ready = snapshot && snapshot.readiness && snapshot.readiness[item.cap];
805
+ var policy = snapshot && snapshot.policies && snapshot.policies[item.cap];
806
+ return React.createElement('button', { key: item.cap, className: 'iris-health-row ' + healthClass(health) + (selected === item.cap ? ' active' : ''),
807
+ 'data-health-capability': item.cap, 'aria-expanded': selected === item.cap, 'aria-controls': 'iris-health-detail', disabled: !!busy,
808
+ onClick: function () { inspect(item); } },
809
+ React.createElement('div', { className: 'iris-health-card-top' },
810
+ React.createElement('span', { className: 'iris-health-icon' }, capabilityIcon(item)),
811
+ React.createElement('span', { className: 'iris-health-caption' }, policy ? modes[policy.mode] : '读取配置')),
812
+ React.createElement('div', { className: 'iris-health-card-main' }, React.createElement('div', { className: 'iris-health-name' }, item.label),
813
+ React.createElement('div', { className: 'iris-health-state', title: healthText(health) }, snapshot || state ? healthText(health) : '正在读取状态…')),
814
+ React.createElement('div', { className: 'iris-health-card-foot' },
815
+ React.createElement('span', {}, ready ? candidates.length ? ready.candidateCount + ' 个可用 · ' + verified + '/' + candidates.length + ' 近期成功' : '暂无候选模型' : '等待配置检查'),
816
+ React.createElement('span', { className: 'iris-health-link' }, '查看检测 ›')));
817
+ })),
818
+ React.createElement('p', { className: 'iris-health-footer', role: 'status' },
819
+ (checkedPair[0] ? '配置检查于 ' + checkedPair[0] + ' · ' : '') + '配置检查只读,实测需单独确认。绿色有效 7 天;不会后台探测。'),
820
+ row ? React.createElement('div', { className: 'iris-health-detail', id: 'iris-health-detail' },
821
+ React.createElement('div', { className: 'iris-health-detail-head' },
822
+ React.createElement('div', {}, React.createElement('h3', {}, row.label + '能力检测'),
823
+ React.createElement('p', { className: 'iris-health-caption' }, row.sample)),
824
+ React.createElement('button', { className: 'iris-health-btn', disabled: !!busy, 'aria-label': '收起能力检测',
825
+ onClick: function () { selectedPair[1](null); confirmPair[1](null); } }, '收起')),
826
+ React.createElement('div', { className: 'iris-health-models' },
827
+ inspectionModels(viewSnapshot, row, requested).map(function (option) {
828
+ var key = row.cap + '|' + option.ref;
829
+ var result = results[key];
830
+ var pending = confirmation && confirmation.ref === option.ref && confirmation.cap === row.cap;
831
+ return React.createElement('div', { key: option.ref, className: 'iris-health-model', 'data-health-model': option.ref, 'aria-busy': busy === key },
832
+ React.createElement('div', { className: 'iris-health-model-head' },
833
+ React.createElement('div', { className: 'iris-health-model-title' }, React.createElement('strong', {}, option.id),
834
+ React.createElement('span', { className: 'iris-health-caption' }, option.providerName + ' · ' + healthText(option.health))),
835
+ React.createElement('button', { className: 'iris-health-btn' + (!option.taskOnly ? ' primary' : ''),
836
+ disabled: !fresh || !option.canTest || !!busy || !!(result && result.phase === 'unknown'),
837
+ onClick: function () { if (running.current) return; if (option.taskOnly) openTask(row); else confirmPair[1]({ cap: row.cap, ref: option.ref }); } },
838
+ option.taskOnly ? '前往功能验证 ↗' : result && result.phase === 'running' ? '正在实测…' : '实测此模型')),
839
+ option.reason ? React.createElement('p', { className: 'iris-health-caption' }, option.reason) : null,
840
+ option.ref === 'host:current-vision' ? React.createElement('p', { className: 'iris-health-caption' },
841
+ '宿主视觉通过真实图片任务验证,请在打开的表单中选择 DSH 当前默认模型。') : null,
842
+ option.health.rateLimited ? React.createElement('p', { className: 'iris-health-caption' }, '显式实测成功后,会解除此模型的冷却或额度停用。') : null,
843
+ option.modelUnavailable || option.health.modelUnavailable ? React.createElement('p', { className: 'iris-health-caption' },
844
+ '当前连接曾返回模型不存在,已暂停自动选用;同步目录不会代替实测。单独实测成功后恢复,可能产生费用。') : null,
845
+ result ? React.createElement('div', { className: 'iris-health-result ' + result.phase, role: result.phase === 'unknown' ? 'alert' : 'status', 'aria-live': 'polite' },
846
+ result.phase === 'running' ? React.createElement('span', { className: 'iris-health-spinner', 'aria-hidden': true }) : null,
847
+ React.createElement('span', {}, (result.phase === 'passed' ? '实测通过 · ' : result.phase === 'failed' ? '实测未通过 · '
848
+ : result.phase === 'skipped' ? '未发起实测 · ' : '') + result.note)) : null,
849
+ pending ? React.createElement('div', { className: 'iris-health-confirm', role: 'group', 'aria-label': '确认模型实测' },
850
+ React.createElement('strong', {}, '确认向此模型发起一次真实请求?'),
851
+ React.createElement('p', {}, row.sample + ' 可能产生费用。本次仅验证 ' + option.id + ',失败不会改用其他模型。'),
852
+ React.createElement('div', { className: 'iris-health-confirm-actions' },
853
+ React.createElement('button', { className: 'iris-health-btn primary', disabled: !!busy, onClick: function () { probe(row, option); } }, '确认并实测'),
854
+ React.createElement('button', { className: 'iris-health-btn', disabled: !!busy, onClick: function () { confirmPair[1](null); } }, '取消'))) : null);
855
+ }),
856
+ !inspectionModels(viewSnapshot, row, requested).length ? React.createElement('div', { className: 'iris-health-notice' },
857
+ snapshot && snapshot.readiness && snapshot.readiness[row.cap] && snapshot.readiness[row.cap].note || '当前没有纳入调用范围的模型,请先配置此功能。') : null),
858
+ React.createElement('div', { className: 'iris-health-confirm-actions' },
859
+ React.createElement('button', { className: 'iris-health-btn', disabled: !!busy, onClick: function () { configure(row); } }, '查看功能设置'),
860
+ React.createElement('span', { className: 'iris-health-caption' }, '只检测当前调用范围内的模型;历史成功不代表所有候选均可用。'))) : null));
546
861
  }
547
862
 
548
863
  /* ---- 任务详情抽屉:按需拉取 /iris/api/task/:id ---- */
@@ -804,8 +1119,9 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
804
1119
  }
805
1120
 
806
1121
  /* ---- 供应商管理(阶段 6:增删 key + 模型列表 + 测试) ---- */
807
- function postAction(action, body) {
808
- return fetch('/iris/api/actions/' + action, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}) })
1122
+ function postAction(action, body, options) {
1123
+ return fetch('/iris/api/actions/' + action, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}),
1124
+ ...(options && options.signal ? { signal: options.signal } : {}) })
809
1125
  .then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); });
810
1126
  }
811
1127
 
@@ -813,9 +1129,6 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
813
1129
  var CAP_SHORT = { 'image-gen': '图', 'video-gen': '视', 'tts': '音', 'music-gen': '乐', 'transcribe': '转', 'vision': '视👁' };
814
1130
  function ModelPool(props) {
815
1131
  var p = props.provider;
816
- var addPair = React.useState('');
817
- var newId = addPair[0];
818
- var setNewId = addPair[1];
819
1132
  var models = p.models || [];
820
1133
  function setImageProtocol(m, value) {
821
1134
  postAction('providers_set_model_image_protocol', { id: p.id, model_id: m.id, imageProtocol: value })
@@ -835,18 +1148,23 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
835
1148
  return React.createElement('button', {
836
1149
  key: c,
837
1150
  className: cls,
838
- title: title + ';点击单独验证此能力',
1151
+ title: title + ';打开能力检测',
1152
+ 'aria-label': (HEALTH_ROWS.find(function (row) { return row.cap === c; }) || { label: c }).label + '检测 · ' + m.id,
839
1153
  onClick: function () { props.testModel(p, m, c); }
840
- }, c + symbol);
1154
+ }, (HEALTH_ROWS.find(function (row) { return row.cap === c; }) || { label: c }).label + ' ' + symbol);
841
1155
  }
842
1156
  var rows = models.map(function (m) {
843
1157
  var caps = m.capabilities || [];
844
1158
  var blocked = caps.map(function (c) { return m.health && m.health[c]; }).find(function (health) { return health && health.rateLimited; });
845
1159
  return React.createElement('div', { key: m.id, className: 'iris-pm-mrow' },
846
- React.createElement('span', { className: 'iris-pm-mname' }, m.id, m.source === 'manual' ? React.createElement('span', { className: 'iris-pm-mtag' }, '手动') : null),
847
- React.createElement('span', { className: 'iris-pm-mcaps' }, caps.length ? caps.map(function (c) { return verBadge(m, c); }) : '未标注功能'),
1160
+ React.createElement('span', { className: 'iris-pm-mname' }, m.id, m.source === 'discovered'
1161
+ ? React.createElement('span', { className: 'iris-pm-mtag' }, '待确认用途') : null,
1162
+ m.catalogStatus === 'unlisted' ? React.createElement('span', { className: 'iris-pm-mtag' }, '目录未列出') : null),
1163
+ React.createElement('span', { className: 'iris-pm-mcaps' }, caps.length ? caps.map(function (c) {
1164
+ return m.directoryOnly ? React.createElement('span', { key: c }, (HEALTH_ROWS.find(function (row) { return row.cap === c; }) || { label: c }).label) : verBadge(m, c);
1165
+ }) : '未标注功能'),
848
1166
  React.createElement(ModelCapabilityEditor, { provider: p, model: m, onNote: props.onNote, onDone: props.onDone }),
849
- caps.includes('image-gen') ? React.createElement('label', { className: 'iris-pm-note' }, '图片协议 ',
1167
+ !m.directoryOnly && caps.includes('image-gen') ? React.createElement('label', { className: 'iris-pm-note' }, '图片协议 ',
850
1168
  React.createElement('select', { value: m.imageProtocol || 'auto', onChange: function (e) { setImageProtocol(m, e.target.value); } },
851
1169
  React.createElement('option', { value: 'auto' }, '继承账号默认(' + (p.mediaProtocol || '自动') + ')'),
852
1170
  React.createElement('option', { value: 'dashscope' }, 'DashScope'),
@@ -854,44 +1172,54 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
854
1172
  React.createElement('option', { value: 'openai-chat-images' }, '聊天生图(网关兼容)'),
855
1173
  React.createElement('option', { value: 'openai-responses-images' }, 'Responses 生图')),
856
1174
  m.imageRouting && m.imageRouting.protocolInferred ? ' · 协议待确认' : null) : null,
857
- caps.includes('music-gen') ? React.createElement('label', { className: 'iris-pm-note' }, '音乐协议 ',
1175
+ !m.directoryOnly && caps.includes('music-gen') ? React.createElement('label', { className: 'iris-pm-note' }, '音乐协议 ',
858
1176
  React.createElement('select', { value: m.musicProtocol || '', onChange: function (e) { setMusicProtocol(m, e.target.value); } },
859
1177
  React.createElement('option', { value: '' }, '请选择音乐协议'),
860
1178
  React.createElement('option', { value: 'gemini-web-music' }, 'Chat Completions(内嵌音频)'))) : null,
861
- caps.includes('vision') || (caps.includes('image-gen') && m.imageRouting && m.imageRouting.editSupported) ? React.createElement(VisionInputEditor, { key: 'vision-input', provider: p, model: m,
1179
+ !m.directoryOnly && (caps.includes('vision') || (caps.includes('image-gen') && m.imageRouting && m.imageRouting.editSupported)) ? React.createElement(VisionInputEditor, { key: 'vision-input', provider: p, model: m,
862
1180
  onNote: props.onNote, onDone: props.onDone }) : null,
863
1181
  blocked ? React.createElement('span', { className: 'iris-pm-ver failed ' + healthClass(blocked), title: healthText(blocked) }, rateLimitLabel(blocked)) : null,
864
- React.createElement('button', { className: 'iris-pm-btn danger', title: '从池移除', onClick: function () { props.removeModel(p, m); } }, '✕'));
1182
+ !m.directoryOnly ? React.createElement('button', { className: 'iris-pm-btn danger', title: '从池移除', onClick: function () { props.removeModel(p, m); } }, '✕') : null);
865
1183
  });
866
1184
  return React.createElement('div', { className: 'iris-pm-pool' },
867
- React.createElement('div', { className: 'iris-pm-note' }, '模型池(' + models.length + '):'),
868
- rows.length ? React.createElement('div', { className: 'iris-pm-mrows' }, rows) : React.createElement('div', { className: 'iris-pm-note' }, '暂无模型 — 点「发现模型」拉取,或下方手动添加'),
869
- React.createElement('div', { className: 'iris-wb-row' },
870
- React.createElement('input', { className: 'iris-pm-madd', placeholder: '手动添加模型名,如 wan2.7-image', value: newId, onChange: function (e) { setNewId(e.target.value); } }),
871
- React.createElement('button', { className: 'iris-pm-btn', onClick: function () { props.addModel(p, newId.trim()); setNewId(''); } }, '+ 添加')));
1185
+ React.createElement('div', { className: 'iris-pm-note' }, '模型池(' + models.filter(function (m) { return !m.directoryOnly; }).length + ')'
1186
+ + (models.some(function (m) { return m.directoryOnly; }) ? ' · 目录待确认 ' + models.filter(function (m) { return m.directoryOnly; }).length + ' 项' : '') + ':'),
1187
+ rows.length ? React.createElement('div', { className: 'iris-pm-mrows' }, rows) : React.createElement('div', { className: 'iris-pm-note' }, '暂无模型 — 展开「模型目录」同步,或在下方手动添加'),
1188
+ React.createElement(ModelUsageForm, { provider: p, setupSuggested: props.setupSuggested,
1189
+ onNote: props.onNote, onDone: props.onDone }));
872
1190
  }
873
1191
 
874
- function ModelCapabilityEditor(props) {
875
- var openPair = React.useState(false);
876
- var capsPair = React.useState([]);
1192
+ function ModelUsageForm(props) {
1193
+ var namePair = React.useState(props.model ? props.model.id : '');
1194
+ var capsPair = React.useState(props.model ? (props.model.capabilities || []).slice() : []);
1195
+ var autoPair = React.useState(!props.model && props.setupSuggested === true);
1196
+ var musicPair = React.useState(props.model && props.model.musicProtocol || '');
877
1197
  var savingPair = React.useState(false);
878
1198
  var selected = capsPair[0];
879
1199
  var saving = savingPair[0];
880
1200
  function save() {
881
1201
  if (saving) return;
1202
+ if (!namePair[0].trim()) { props.onNote('请填写模型名称'); return; }
1203
+ if (selected.includes('music-gen') && !musicPair[0]) { props.onNote('请先选择该模型实际支持的音乐协议'); return; }
882
1204
  savingPair[1](true);
883
- postAction('providers_set_model_caps', { id: props.provider.id, model_id: props.model.id, capabilities: selected })
1205
+ postAction('providers_confirm_model', { id: props.provider.id, model_id: namePair[0].trim(), capabilities: selected,
1206
+ enableAutomatic: autoPair[0], ...(selected.includes('music-gen') ? { musicProtocol: musicPair[0] } : {}) })
884
1207
  .then(function (r) {
885
1208
  if (r.ok) {
886
- openPair[1](false);
887
- props.onNote('模型功能标签已保存');
1209
+ if (!props.model) namePair[1]('');
1210
+ props.onNote(r.d.text || '模型用途已确认');
888
1211
  props.onDone();
1212
+ window.dispatchEvent(new CustomEvent('iris-function-configuration-changed'));
889
1213
  } else props.onNote(r.d.error || '保存失败');
1214
+ }).catch(function () { props.onNote('无法保存,请检查与宿主的连接');
890
1215
  }).finally(function () { savingPair[1](false); });
891
1216
  }
892
- if (!openPair[0]) return React.createElement('button', { className: 'iris-pm-btn', title: '设置此模型可用于哪些功能',
893
- onClick: function () { capsPair[1]((props.model.capabilities || []).slice()); openPair[1](true); } }, '设置功能');
894
- return React.createElement('div', { className: 'iris-wb-row' },
1217
+ return React.createElement('div', { className: 'iris-pm-card' },
1218
+ !props.model ? React.createElement('label', { className: 'iris-pm-field' }, '模型名称',
1219
+ React.createElement('input', { value: namePair[0], placeholder: '发现后选模型,或填写服务提供的模型名',
1220
+ onChange: function (e) { namePair[1](e.target.value); } })) : null,
1221
+ React.createElement('div', { className: 'iris-pm-note' }, '确认此模型用于哪些功能;名称和文字接口本身不证明媒体能力。'),
1222
+ React.createElement('div', { className: 'iris-wb-row' },
895
1223
  CAP_ROWS.map(function (row) {
896
1224
  return React.createElement('label', { key: row.cap, className: 'iris-pm-note' },
897
1225
  React.createElement('input', { type: 'checkbox', value: row.cap, checked: selected.includes(row.cap), disabled: saving,
@@ -902,9 +1230,25 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
902
1230
  : previous.filter(function (cap) { return cap !== row.cap; });
903
1231
  });
904
1232
  } }), row.label);
905
- }),
906
- React.createElement('button', { className: 'iris-pm-btn', disabled: saving, onClick: save }, saving ? '保存中…' : '保存功能'),
907
- React.createElement('button', { className: 'iris-pm-btn', disabled: saving, onClick: function () { openPair[1](false); } }, '取消'));
1233
+ })),
1234
+ selected.includes('music-gen') ? React.createElement('label', { className: 'iris-pm-note' }, '音乐协议 ',
1235
+ React.createElement('select', { value: musicPair[0], onChange: function (e) { musicPair[1](e.target.value); } },
1236
+ React.createElement('option', { value: '' }, '请选择音乐协议'),
1237
+ React.createElement('option', { value: 'gemini-web-music' }, 'Chat Completions(内嵌音频)'))) : null,
1238
+ React.createElement('label', { className: 'iris-pm-note' }, React.createElement('input', {
1239
+ type: 'checkbox', checked: autoPair[0], onChange: function (e) { autoPair[1](e.target.checked); }
1240
+ }), '同时启用所选功能的自动池(替换这些功能当前的调用方式,保留原手工名单)'),
1241
+ React.createElement('button', { className: 'iris-pm-btn', disabled: saving, onClick: save }, saving ? '保存中…' : '保存模型用途'));
1242
+ }
1243
+
1244
+ function ModelCapabilityEditor(props) {
1245
+ var openPair = React.useState(false);
1246
+ if (!openPair[0]) return React.createElement('button', { className: 'iris-pm-btn', title: '确认模型用途,加入功能池',
1247
+ onClick: function () { openPair[1](true); } }, '设置功能');
1248
+ return React.createElement('div', {},
1249
+ React.createElement(ModelUsageForm, { provider: props.provider, model: props.model, onNote: props.onNote,
1250
+ onDone: function () { openPair[1](false); props.onDone(); } }),
1251
+ React.createElement('button', { className: 'iris-pm-btn', onClick: function () { openPair[1](false); } }, '取消'));
908
1252
  }
909
1253
 
910
1254
  function VisionInputEditor(props) {
@@ -940,6 +1284,42 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
940
1284
  React.createElement('span', { className: 'iris-pm-note' }, '用于看图、OCR 每块输入、定位和改图的发送副本。作品原图保留,定位结果换算回原图。'));
941
1285
  }
942
1286
 
1287
+ function ProviderDirectory(props) {
1288
+ var p = props.provider, directory = p.directory || { status: 'unknown' };
1289
+ var busyPair = React.useState(false), notePair = React.useState(''), running = React.useRef(false);
1290
+ function synchronize() {
1291
+ if (running.current || !p.directory) return;
1292
+ running.current = true; busyPair[1](true); notePair[1]('');
1293
+ var controller = new AbortController();
1294
+ var timer = setTimeout(function () { controller.abort(); }, 90000);
1295
+ postAction('providers_sync_directory', { id: p.id, force: true }, { signal: controller.signal }).then(function (r) {
1296
+ if (!r.ok || !r.d || r.d.ok !== true || typeof r.d.synced !== 'boolean') {
1297
+ notePair[1]('同步结果未确认,请刷新状态查看。' + (r.d && r.d.error ? ' ' + String(r.d.error) : '')); return;
1298
+ }
1299
+ notePair[1](r.d.text || (r.d.synced ? '模型目录已同步' : '目录同步失败,保留上次完整目录'));
1300
+ props.onDone();
1301
+ }).catch(function () { notePair[1]('同步结果未确认,请刷新状态查看;本次没有发起模型生成。'); })
1302
+ .finally(function () { clearTimeout(timer); running.current = false; busyPair[1](false); });
1303
+ }
1304
+ var summary = busyPair[0] ? '正在同步…' : directory.checkedAt
1305
+ ? (directory.status === 'stale' ? '缓存待更新' : '已同步') + ' · ' + directory.modelCount + ' 个模型 · 目录未列出 ' + (directory.missing || []).length + ' 项'
1306
+ : '尚未同步';
1307
+ return React.createElement('details', { className: 'iris-pm-field', 'data-provider-directory': p.id, 'aria-busy': busyPair[0] },
1308
+ React.createElement('summary', { className: 'iris-pm-note' }, '模型目录 · ' + summary),
1309
+ React.createElement('div', { className: 'iris-wb-row' },
1310
+ React.createElement('span', { className: 'iris-pm-note' }, directory.checkedAt ? '上次同步:' + new Date(directory.checkedAt).toLocaleString() : '目录与模型用途分别保存'),
1311
+ React.createElement('button', { className: 'iris-pm-btn', disabled: busyPair[0] || !p.directory || !p.enabled,
1312
+ onClick: synchronize }, busyPair[0] ? '正在同步…' : '同步目录')),
1313
+ !p.directory ? React.createElement('p', { className: 'iris-pm-note' }, '请先让 DSH 加载新版 Iris,再同步目录。') : null,
1314
+ directory.error && !notePair[0] ? React.createElement('p', { className: 'iris-pm-note', role: 'status' }, '上次同步未完成,保留旧目录:' + directory.error) : null,
1315
+ notePair[0] ? React.createElement('p', { className: 'iris-pm-note', role: 'status' }, notePair[0]) : null,
1316
+ React.createElement('p', { className: 'iris-pm-note' }, '同步只读取供应商目录,新模型仍需确认用途。目录未列出不等于下线;实测与生成需单独发起。'),
1317
+ (directory.added || []).length ? React.createElement('p', { className: 'iris-pm-note' }, '本次新增:' + directory.added.slice(0, 5).join('、')
1318
+ + (directory.added.length > 5 ? ' 等 ' + directory.added.length + ' 项,完整条目见下方列表' : '')) : null,
1319
+ (directory.missing || []).length ? React.createElement('p', { className: 'iris-pm-note' }, '目录未列出:' + directory.missing.slice(0, 5).join('、')
1320
+ + (directory.missing.length > 5 ? ' 等 ' + directory.missing.length + ' 项,原配置继续保留' : '')) : null);
1321
+ }
1322
+
943
1323
  function ProviderEndpointEditor(props) {
944
1324
  var p = props.provider;
945
1325
  var valuePair = React.useState(p.mediaBaseUrl || '');
@@ -970,11 +1350,22 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
970
1350
  var adding = addingPair[0];
971
1351
  var setAdding = addingPair[1];
972
1352
  var newForm = React.useState({ name: '', baseUrl: '', mediaBaseUrl: '', apiKey: '', type: 'openai', mediaProtocol: 'auto' });
1353
+ var hostConnections = React.useState([]);
1354
+ var hostChoice = React.useState('');
1355
+ var setupPair = React.useState(false);
973
1356
  var notePair = React.useState('');
974
1357
  var note = notePair[0];
975
1358
  var setNote = notePair[1];
976
1359
  function refresh() {
977
- postAction('providers_list', {}).then(function (r) { if (r.ok && r.d.providers) setList(r.d.providers); });
1360
+ postAction('providers_list', {}).then(function (r) { if (r.ok && r.d.providers) {
1361
+ setList(r.d.providers); setupPair[1](r.d.setupSuggested === true);
1362
+ } });
1363
+ }
1364
+ function loadHostConnections() {
1365
+ postAction('providers_host_connections', {}).then(function (r) {
1366
+ if (r.ok) { hostConnections[1](r.d.connections || []); if (r.d.note) setNote(r.d.note); }
1367
+ else setNote(r.d.error || '无法读取 DSH 连接');
1368
+ });
978
1369
  }
979
1370
  React.useEffect(function () {
980
1371
  refresh();
@@ -983,9 +1374,20 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
983
1374
  }, []);
984
1375
  function addProvider() {
985
1376
  var f = newForm[0];
1377
+ if (hostChoice[0]) {
1378
+ var connection = hostConnections[0].find(function (item) { return JSON.stringify([item.settingsNs, item.provider]) === hostChoice[0]; });
1379
+ if (!connection || !connection.available) { setNote('请重新选择可用的 DSH 连接'); return; }
1380
+ postAction('providers_upsert', { name: f.name || connection.name, type: 'openai', enabled: true,
1381
+ mediaProtocol: f.mediaProtocol, dshProvider: { provider: connection.provider, settingsNs: connection.settingsNs } })
1382
+ .then(function (r) {
1383
+ if (r.ok) { setAdding(false); openPair[1](r.d.providerId); setNote('已引用 DSH 连接,请在下方确认模型用途,即可启用自动池'); refresh(); }
1384
+ else setNote(r.d.error || '添加失败');
1385
+ });
1386
+ return;
1387
+ }
986
1388
  if (!f.baseUrl || !f.apiKey) { setNote('baseUrl 和 apiKey 必填'); return; }
987
1389
  postAction('providers_upsert', f).then(function (r) {
988
- if (r.ok) { setAdding(false); setNote('已添加 ' + f.name); refresh(); }
1390
+ if (r.ok) { setAdding(false); openPair[1](r.d.providerId); setNote('已连接,请在下方确认模型用途'); refresh(); }
989
1391
  else setNote(r.d.error || '添加失败');
990
1392
  });
991
1393
  }
@@ -999,21 +1401,10 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
999
1401
  if (!window.confirm('删除供应商「' + (p.name || p.id) + '」?此操作不可撤销。')) return;
1000
1402
  postAction('providers_remove', { id: p.id }).then(function (r) { if (r.ok) refresh(); else setNote(r.d.error || '删除失败'); });
1001
1403
  }
1002
- function testVision(p) {
1003
- if (!window.confirm('视觉实测会向供应商发送一张内置测试图,可能产生费用。继续吗?')) return;
1004
- setNote('测试「' + (p.name || p.id) + '」的视觉能力…');
1005
- postAction('providers_test_vision', { id: p.id, confirm_paid: true }).then(function (r) { setNote(r.d.text || r.d.error || '测试完成'); refresh(); });
1006
- }
1007
- function discover(p) {
1008
- setNote('正在发现「' + (p.name || p.id) + '」的模型…');
1009
- postAction('providers_discover', { id: p.id }).then(function (r) { setNote(r.d.text || r.d.error || '发现完成'); refresh(); });
1010
- }
1011
1404
  function testModel(p, m, capability) {
1012
- var noProbe = capability === 'video-gen' || capability === 'transcribe' || capability === 'music-gen';
1013
- if (!noProbe && !window.confirm('将用模型「' + m.id + '」发起一次真实的 ' + capability + ' 请求,可能产生费用。继续吗?')) return;
1014
- setNote('验证 ' + m.id + ' · ' + capability + '…');
1015
- postAction('providers_test_model', { id: p.id, model_id: m.id, capability: capability, confirm_paid: !noProbe })
1016
- .then(function (r) { setNote(r.d.text || r.d.error || ''); refresh(); });
1405
+ window.dispatchEvent(new CustomEvent('iris-capability-inspect', {
1406
+ detail: { capability: capability, providerId: p.id, modelId: m.id }
1407
+ }));
1017
1408
  }
1018
1409
  function setProtocol(p, value) {
1019
1410
  postAction('providers_upsert', { id: p.id, mediaProtocol: value }).then(function (r) {
@@ -1024,16 +1415,14 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1024
1415
  if (!window.confirm('从池中移除模型「' + m.id + '」?')) return;
1025
1416
  postAction('providers_remove_model', { id: p.id, model_id: m.id }).then(function (r) { setNote(r.d.text || r.d.error || ''); refresh(); });
1026
1417
  }
1027
- function addModel(p, modelId) {
1028
- if (!modelId) { setNote('模型名不能为空'); return; }
1029
- postAction('providers_add_model', { id: p.id, model_id: modelId }).then(function (r) { setNote(r.d.text || r.d.error || ''); refresh(); });
1030
- }
1031
1418
  var cards = (list || []).map(function (p) {
1032
1419
  var open = openPair[0] === p.id;
1033
1420
  var body = null;
1034
1421
  if (open) {
1035
- body = React.createElement('div', { className: 'iris-pm-body' },
1422
+ body = React.createElement('div', { className: 'iris-pm-body', id: 'iris-provider-' + p.id },
1036
1423
  React.createElement('div', { className: 'iris-pm-note' }, 'Key ' + (p.apiKeyHint || '未配置') + ' · ' + (p.type || '') + ' · ' + (p.mediaProtocol || '')),
1424
+ p.dshProvider ? React.createElement('div', { className: 'iris-pm-note' }, '引用 DSH:' + p.dshProvider.provider
1425
+ + ' · 地址和密钥由宿主管理;' + (p.connection && p.connection.available ? '连接可用' : ((p.connection && p.connection.reason) || '连接不可用'))) : null,
1037
1426
  p.protocolInferred ? React.createElement('div', { className: 'iris-pm-note' }, '协议为推断值,请确认') : null,
1038
1427
  React.createElement(ProviderEndpointEditor, { provider: p, onNote: setNote, onDone: refresh }),
1039
1428
  React.createElement(VisionInputEditor, { provider: p, onNote: setNote, onDone: refresh }),
@@ -1045,14 +1434,15 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1045
1434
  React.createElement('option', { value: 'openai-chat-images' }, '聊天生图(网关兼容)'),
1046
1435
  React.createElement('option', { value: 'openai-responses-images' }, 'Responses 生图'))),
1047
1436
  React.createElement('div', { className: 'iris-pm-note' }, '能力: ' + ((p.capabilities || []).join(' / ') || '无')),
1048
- React.createElement(ModelPool, { provider: p, onNote: setNote, onDone: refresh, testModel: testModel, removeModel: removeModel, addModel: addModel }),
1437
+ React.createElement(ProviderDirectory, { provider: p, onDone: refresh }),
1438
+ React.createElement(ModelPool, { provider: p, setupSuggested: setupPair[0], onNote: setNote, onDone: refresh, testModel: testModel, removeModel: removeModel }),
1049
1439
  React.createElement('div', { className: 'iris-wb-row' },
1050
- React.createElement('button', { className: 'iris-pm-btn', onClick: function () { discover(p); } }, '发现模型'),
1051
1440
  React.createElement('button', { className: 'iris-pm-btn', onClick: function () { toggle(p); } }, p.enabled ? '停用' : '启用'),
1052
1441
  React.createElement('button', { className: 'iris-pm-btn danger', onClick: function () { remove(p); } }, '删除')));
1053
1442
  }
1054
1443
  return React.createElement('div', { key: p.id, className: 'iris-pm-card' + (open ? ' open' : '') },
1055
- React.createElement('div', { className: 'iris-pm-top', onClick: function () { setOpen(open ? null : p.id); } },
1444
+ React.createElement('button', { type: 'button', className: 'iris-pm-top', 'aria-expanded': open, 'aria-controls': 'iris-provider-' + p.id,
1445
+ onClick: function () { setOpen(open ? null : p.id); } },
1056
1446
  React.createElement('span', { className: 'iris-pm-name' }, (p.name || p.id) + (p.enabled ? '' : '(停用)')),
1057
1447
  React.createElement('span', { className: 'iris-pm-btn' }, open ? '收起' : '管理')),
1058
1448
  body);
@@ -1060,12 +1450,19 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1060
1450
  var addBlock = null;
1061
1451
  if (adding) {
1062
1452
  addBlock = React.createElement('div', { className: 'iris-pm-card' },
1453
+ React.createElement('div', { className: 'iris-pm-field' }, React.createElement('label', {}, '连接来源'),
1454
+ React.createElement('select', { value: hostChoice[0], onChange: function (e) { hostChoice[1](e.target.value); } },
1455
+ React.createElement('option', { value: '' }, '手动填写供应商'),
1456
+ hostConnections[0].map(function (connection) { return React.createElement('option', {
1457
+ key: JSON.stringify([connection.settingsNs, connection.provider]), value: JSON.stringify([connection.settingsNs, connection.provider]), disabled: !connection.available
1458
+ }, 'DSH · ' + connection.name + (connection.available ? '' : '(' + connection.reason + ')')); }))),
1459
+ hostChoice[0] ? React.createElement('div', { className: 'iris-pm-note' }, '地址与密钥随 DSH 连接更新,不保存密钥副本。请选择该服务实际支持的媒体协议;模型能力仍需明确配置。') : null,
1063
1460
  React.createElement('div', { className: 'iris-pm-field' }, React.createElement('label', {}, '名称'), React.createElement('input', { value: newForm[0].name, placeholder: '如 阿里云百炼', onChange: function (e) { newForm[1]({ ...newForm[0], name: e.target.value }); } })),
1064
- React.createElement('div', { className: 'iris-pm-field' }, React.createElement('label', {}, 'Base URL *'), React.createElement('input', { value: newForm[0].baseUrl, placeholder: 'https://dashscope.aliyuncs.com/compatible-mode/v1', onChange: function (e) { newForm[1]({ ...newForm[0], baseUrl: e.target.value }); } })),
1065
- React.createElement('div', { className: 'iris-pm-field' }, React.createElement('label', {}, '媒体 Base URL(可选)'), React.createElement('input', { value: newForm[0].mediaBaseUrl, placeholder: 'Workspace 媒体端点;留空沿用 Base URL', onChange: function (e) { newForm[1]({ ...newForm[0], mediaBaseUrl: e.target.value }); } })),
1066
- React.createElement('div', { className: 'iris-pm-field' }, React.createElement('label', {}, 'API Key *'), React.createElement('input', { type: 'password', value: newForm[0].apiKey, placeholder: 'sk-...', onChange: function (e) { newForm[1]({ ...newForm[0], apiKey: e.target.value }); } })),
1461
+ !hostChoice[0] ? React.createElement('div', { className: 'iris-pm-field' }, React.createElement('label', {}, 'Base URL *'), React.createElement('input', { value: newForm[0].baseUrl, placeholder: 'https://dashscope.aliyuncs.com/compatible-mode/v1', onChange: function (e) { newForm[1]({ ...newForm[0], baseUrl: e.target.value }); } })) : null,
1462
+ !hostChoice[0] ? React.createElement('div', { className: 'iris-pm-field' }, React.createElement('label', {}, '媒体 Base URL(可选)'), React.createElement('input', { value: newForm[0].mediaBaseUrl, placeholder: 'Workspace 媒体端点;留空沿用 Base URL', onChange: function (e) { newForm[1]({ ...newForm[0], mediaBaseUrl: e.target.value }); } })) : null,
1463
+ !hostChoice[0] ? React.createElement('div', { className: 'iris-pm-field' }, React.createElement('label', {}, 'API Key *'), React.createElement('input', { type: 'password', value: newForm[0].apiKey, placeholder: 'sk-...', onChange: function (e) { newForm[1]({ ...newForm[0], apiKey: e.target.value }); } })) : null,
1067
1464
  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 }); } },
1068
- React.createElement('option', { value: 'auto' }, '自动(按媒体 Base URL 安全判断)'),
1465
+ React.createElement('option', { value: 'auto' }, hostChoice[0] ? '请选择该服务支持的媒体协议' : '自动(按媒体 Base URL 安全判断)'),
1069
1466
  React.createElement('option', { value: 'dashscope' }, 'DashScope(阿里云官方)'),
1070
1467
  React.createElement('option', { value: 'openai-images' }, 'OpenAI Images 兼容'),
1071
1468
  React.createElement('option', { value: 'openai-chat-images' }, '聊天生图(网关兼容)'),
@@ -1077,7 +1474,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1077
1474
  return React.createElement('div', { className: 'iris-pm' },
1078
1475
  React.createElement('div', { className: 'iris-pm-head' },
1079
1476
  React.createElement('span', { className: 'iris-wb-muted' }, (list ? list.length : 0) + ' 个供应商'),
1080
- React.createElement('button', { className: 'iris-pm-add', onClick: function () { setAdding(!adding); } }, adding ? '收起' : '+ 添加供应商')),
1477
+ React.createElement('button', { className: 'iris-pm-add', onClick: function () { if (!adding) { hostChoice[1](''); loadHostConnections(); } setAdding(!adding); } }, adding ? '收起' : '+ 添加供应商')),
1081
1478
  addBlock,
1082
1479
  cards.length ? cards : React.createElement('div', { className: 'iris-wb-empty' }, '暂无供应商 — 点右上角「+ 添加供应商」'),
1083
1480
  note ? React.createElement('div', { className: 'iris-pm-act' }, note) : null);
@@ -1169,9 +1566,10 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1169
1566
  var setModelInfo = modelPair[1];
1170
1567
  var irisState = useIrisState(5000);
1171
1568
  function setVal(key, value) { var nv = {}; Object.keys(vals).forEach(function (k) { nv[k] = vals[k]; }); nv[key] = value; setVals(nv); }
1172
- function loadModels() {
1569
+ function loadModels(event) {
1173
1570
  if (!capability) return;
1174
- postAction('assignments_get', {}).then(function (r) {
1571
+ var snapshot = event && event.detail && event.detail.functionAssignments;
1572
+ (snapshot ? Promise.resolve({ ok: true, d: snapshot }) : postAction('assignments_get', {})).then(function (r) {
1175
1573
  if (!r.ok || !r.d) return;
1176
1574
  var opts = (r.d.poolByCapability && r.d.poolByCapability[capability]) || [];
1177
1575
  var ord = (r.d.order && r.d.order[capability]) || [];
@@ -1179,10 +1577,17 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1179
1577
  var assignedRef = ord[0] || '';
1180
1578
  var assignedOpt = opts.find(function (o) { return o.ref === assignedRef; });
1181
1579
  // 不再自动填 vals.model:留空 = 用能力分配的解析结果(避免绕过分配)
1182
- setModelInfo({ assigned: assignedOpt ? assignedOpt.label : '', assignedRef: assignedRef, options: opts, loaded: true });
1580
+ setModelInfo({ assigned: assignedOpt ? assignedOpt.label : '', assignedRef: assignedRef, options: opts, loaded: true,
1581
+ mode: r.d.policies && r.d.policies[capability] && r.d.policies[capability].mode,
1582
+ readiness: r.d.readiness && r.d.readiness[capability] });
1183
1583
  });
1184
1584
  }
1185
- React.useEffect(function () { if (open && capability) loadModels(); }, [open]);
1585
+ React.useEffect(function () {
1586
+ if (!open || !capability) return;
1587
+ loadModels();
1588
+ window.addEventListener('iris-function-configuration-changed', loadModels);
1589
+ return function () { window.removeEventListener('iris-function-configuration-changed', loadModels); };
1590
+ }, [open]);
1186
1591
  function submit() {
1187
1592
  if (run === 'running') return;
1188
1593
  setRun('running');
@@ -1199,13 +1604,18 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1199
1604
  .catch(function () { setRun({ ok: false, text: '网络错误' }); });
1200
1605
  }
1201
1606
  var cardHealth = capability ? capabilityHealth(irisState, capability) : { status: 'verified' };
1607
+ // Host references can be ready without raw endpoint/key fields in the state snapshot.
1608
+ if (modelInfo && modelInfo.readiness) {
1609
+ if (modelInfo.mode === 'disabled') cardHealth = { status: 'unconfigured' };
1610
+ else if (modelInfo.readiness.candidateCount > 0 && cardHealth.status === 'unconfigured') cardHealth = { status: 'configured' };
1611
+ }
1202
1612
  var body = null;
1203
1613
  if (open) {
1204
1614
  var inputs = fields.map(function (f) {
1205
1615
  var node;
1206
1616
  if (f.key === 'model' && capability) {
1207
1617
  node = React.createElement('select', { value: vals.model || '', onChange: function (e) { setVal('model', e.target.value); } },
1208
- React.createElement('option', { value: '' }, '按功能分配顺序'),
1618
+ React.createElement('option', { value: '' }, modelInfo && modelInfo.mode === 'auto' ? '按功能池顺序自动选择' : '按所选模型顺序'),
1209
1619
  ((modelInfo && modelInfo.options) || []).map(function (option) {
1210
1620
  return React.createElement('option', { key: option.ref, value: option.ref, disabled: option.unavailable }, option.label + (option.unavailable ? ' · 当前不可用' : ''));
1211
1621
  }));
@@ -1249,14 +1659,15 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1249
1659
  // 只读模型提示:当前能力解析到的模型与持久健康事实。
1250
1660
  var modelBar = null;
1251
1661
  if (capability) {
1252
- var available = modelInfo && modelInfo.loaded && modelInfo.options.length > 0;
1662
+ var available = modelInfo && modelInfo.loaded && (modelInfo.readiness ? modelInfo.readiness.candidateCount > 0 : modelInfo.options.length > 0);
1253
1663
  var shown = (vals.model && vals.model.trim()) || (modelInfo && modelInfo.assigned) || '';
1254
1664
  modelBar = React.createElement('div', { className: 'iris-act-field' },
1255
1665
  React.createElement('label', {},
1256
1666
  React.createElement('span', { className: 'iris-pm-dot ' + healthClass(cardHealth) }, cardHealth.retryAt ? '◷' : HEALTH_META[cardHealth.status].symbol),
1257
1667
  ' 模型:' + (shown || '未分配') + (available ? '' : '(无可用模型)')),
1258
1668
  React.createElement('div', { className: 'iris-wb-muted' },
1259
- healthText(cardHealth) + ' · 改分配 → 上方「功能分配(调用顺序)」'));
1669
+ (modelInfo && modelInfo.readiness ? modelInfo.readiness.candidateCount + ' 个候选 · ' + (modelInfo.readiness.note || '实际权限以调用结果为准') : healthText(cardHealth))
1670
+ + ' · 调整方式 → 上方「功能池与调用方式」'));
1260
1671
  }
1261
1672
  body = React.createElement('div', { className: 'iris-act-body' },
1262
1673
  modelBar,
@@ -1268,7 +1679,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1268
1679
  var headStatus = capability ? cardHealth.status : 'verified';
1269
1680
  var headTitle = capability ? healthText(cardHealth) : '本地功能';
1270
1681
  return React.createElement('div', {
1271
- className: 'iris-act-card' + (open ? ' open' : '') + (capability ? ' health-' + headStatus : '')
1682
+ className: 'iris-act-card' + (open ? ' open' : '') + (capability ? ' health-' + headStatus : ''), 'data-iris-action': action
1272
1683
  },
1273
1684
  React.createElement('div', { className: 'iris-act-head', onClick: function () { setOpen(!open); } },
1274
1685
  React.createElement('span', { className: 'iris-act-title' }, title),
@@ -1360,59 +1771,168 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1360
1771
  { cap: 'vision', label: '👁 视觉' }
1361
1772
  ];
1362
1773
  function CapabilityAssigner() {
1363
- var dataPair = React.useState(null);
1364
- var data = dataPair[0];
1365
- var setData = dataPair[1];
1366
- function load() {
1367
- postAction('assignments_get', {}).then(function (r) {
1368
- if (r.ok && r.d) setData(r.d);
1369
- });
1774
+ var dataPair = React.useState(null), data = dataPair[0], setData = dataPair[1];
1775
+ var notePair = React.useState('');
1776
+ var feedbackPair = React.useState({}), feedback = feedbackPair[0];
1777
+ var busyPair = React.useState(null), busy = busyPair[0];
1778
+ var loadingPair = React.useState(false), loading = loadingPair[0];
1779
+ var confirmPair = React.useState(null), confirmation = confirmPair[0];
1780
+ var saving = React.useRef(false), readVersion = React.useRef(0);
1781
+ var labels = { auto: '按功能自动选择', manual: '仅用所选模型', disabled: '停用' };
1782
+ function compatible(snapshot) {
1783
+ return !!(snapshot && snapshot.policies && snapshot.readiness && CAP_ROWS.every(function (row) {
1784
+ return snapshot.policies[row.cap] && ['auto', 'manual', 'disabled'].includes(snapshot.policies[row.cap].mode) && snapshot.readiness[row.cap];
1785
+ }));
1370
1786
  }
1371
- React.useEffect(function () { load(); }, []);
1372
- function save(cap, refs) {
1373
- postAction('assignments_set', { capability: cap, model_refs: refs }).then(function () {
1374
- load(); // 成败都重载:成功显示新序,失败显示实际落盘序(不骗 UI)
1375
- });
1787
+ function setFeedback(cap, value) {
1788
+ feedbackPair[1](function (previous) { return { ...previous, [cap]: value }; });
1789
+ }
1790
+ function request(action, body) {
1791
+ var controller = new AbortController();
1792
+ var timer = setTimeout(function () { controller.abort(); }, 10000);
1793
+ return postAction(action, body, { signal: controller.signal }).finally(function () { clearTimeout(timer); });
1794
+ }
1795
+ function load(event) {
1796
+ var version = ++readVersion.current;
1797
+ var shared = event && event.detail && event.detail.functionAssignments;
1798
+ if (shared) { setData(shared); loadingPair[1](false); return Promise.resolve(shared); }
1799
+ loadingPair[1](true);
1800
+ return request('assignments_get', {}).then(function (r) {
1801
+ if (version !== readVersion.current) return null;
1802
+ if (!r.ok || !r.d || r.d.ok !== true) throw new Error(r.d && r.d.error || '无法读取功能池');
1803
+ setData(r.d); notePair[1](''); return r.d;
1804
+ }).catch(function (error) {
1805
+ if (version === readVersion.current) notePair[1](error.name === 'AbortError' ? '读取状态超时,请重新检查' : '无法读取功能池,请检查与宿主的连接');
1806
+ return null;
1807
+ }).finally(function () { if (version === readVersion.current) loadingPair[1](false); });
1808
+ }
1809
+ React.useEffect(function () {
1810
+ load();
1811
+ window.addEventListener('iris-function-configuration-changed', load);
1812
+ return function () { window.removeEventListener('iris-function-configuration-changed', load); };
1813
+ }, []);
1814
+ function update(body) {
1815
+ if (saving.current || !compatible(data)) return;
1816
+ saving.current = true; busyPair[1](body.capability); confirmPair[1](null);
1817
+ ++readVersion.current; // A read started before this write cannot overwrite its result.
1818
+ loadingPair[1](false);
1819
+ var desired = body.mode || (body.model_refs.length ? 'manual' : 'disabled');
1820
+ setFeedback(body.capability, { phase: 'saving', message: '正在保存…' });
1821
+ request('assignments_set', body).then(function (r) {
1822
+ if (!r.ok || !r.d || r.d.ok !== true) throw new Error(r.d && r.d.error || '未收到保存确认,请刷新状态核对');
1823
+ if ((body.mode && r.d.mode !== body.mode) || !Array.isArray(r.d.model_refs)) {
1824
+ setData(function (previous) { return { ...previous, policies: null, readiness: null }; });
1825
+ throw new Error('服务端未确认新策略,请重启 DSH 后刷新页面');
1826
+ }
1827
+ setData(function (previous) { return { ...previous,
1828
+ policies: { ...previous.policies, [body.capability]: { ...previous.policies[body.capability], mode: desired,
1829
+ ...(body.hostVision !== undefined ? { hostVision: body.hostVision } : {}) } },
1830
+ order: { ...previous.order, [body.capability]: r.d.model_refs }
1831
+ }; });
1832
+ setFeedback(body.capability, { phase: 'refreshing', message: '已保存为“' + labels[desired] + '”,正在更新候选…' });
1833
+ return load().then(function (snapshot) {
1834
+ if (!snapshot) {
1835
+ setFeedback(body.capability, { phase: 'saved-stale', message: '已保存为“' + labels[desired] + '”,候选未能刷新,请核对状态' });
1836
+ return;
1837
+ }
1838
+ var policy = snapshot.policies && snapshot.policies[body.capability];
1839
+ var confirmed = compatible(snapshot) && policy.mode === desired
1840
+ && (body.hostVision === undefined || policy.hostVision === body.hostVision);
1841
+ setFeedback(body.capability, confirmed
1842
+ ? { phase: 'saved', message: '已保存:' + labels[desired] + ' · ' + snapshot.readiness[body.capability].candidateCount + ' 个可用候选' }
1843
+ : { phase: 'unknown', message: '读取的状态与本次选择不一致,请核对服务端状态' });
1844
+ window.dispatchEvent(new CustomEvent('iris-function-configuration-changed', { detail: { functionAssignments: snapshot } }));
1845
+ });
1846
+ }).catch(function (error) {
1847
+ setFeedback(body.capability, { phase: 'unknown', message: error.name === 'AbortError'
1848
+ ? '未收到保存确认,请刷新状态核对,暂勿重复提交' : error.message || '保存结果未确认,请刷新状态核对' });
1849
+ }).finally(function () { saving.current = false; busyPair[1](null); });
1376
1850
  }
1377
- if (!data) return React.createElement('div', { className: 'iris-wb-muted' }, '加载能力分配…');
1378
- var order = data.order || {};
1379
- var poolBy = data.poolByCapability || {};
1380
- return React.createElement('div', { className: 'iris-cap-grid' }, CAP_ROWS.map(function (row) {
1381
- var list = order[row.cap] || [];
1382
- var opts = poolBy[row.cap] || [];
1383
- var available = opts.filter(function (o) { return list.indexOf(o.ref) < 0; });
1384
- var chips = list.map(function (ref, i) {
1385
- var opt = opts.find(function (o) { return o.ref === ref; });
1386
- return React.createElement('span', { key: ref, className: 'iris-cap-chip' },
1387
- React.createElement('b', {}, (i + 1) + '.'),
1388
- opt ? opt.label + (opt.unavailable ? ' · 当前不可用' : '') : ref + ' · 配置已失效',
1389
- i > 0 ? React.createElement('button', {
1390
- className: 'iris-pm-btn', title: '上移',
1391
- onClick: function () { var n = list.slice(); var t = n.splice(i, 1)[0]; n.splice(i - 1, 0, t); save(row.cap, n); }
1392
- }, '↑') : null,
1393
- i < list.length - 1 ? React.createElement('button', {
1394
- className: 'iris-pm-btn', title: '下移',
1395
- onClick: function () { var n = list.slice(); var t = n.splice(i, 1)[0]; n.splice(i + 1, 0, t); save(row.cap, n); }
1396
- }, '↓') : null,
1397
- React.createElement('button', {
1398
- className: 'iris-pm-btn danger', title: '移出列表',
1399
- onClick: function () { save(row.cap, list.filter(function (x) { return x !== ref; })); }
1400
- }, '✕'));
1851
+ function check(cap) {
1852
+ if (saving.current) return;
1853
+ return load().then(function (snapshot) {
1854
+ if (compatible(snapshot)) setFeedback(cap, { phase: 'saved', message: '当前已保存:' + labels[snapshot.policies[cap].mode]
1855
+ + ' · ' + snapshot.readiness[cap].candidateCount + ' 个可用候选' });
1401
1856
  });
1402
- return React.createElement('div', { key: row.cap, className: 'iris-cap-row' },
1403
- React.createElement('span', { className: 'iris-cap-name' }, row.label),
1404
- chips.length ? chips : React.createElement('span', { className: 'iris-cap-auto' }, '尚未分配模型'),
1405
- available.length ? React.createElement('select', {
1406
- className: 'iris-pm-model-sel', value: '',
1407
- onChange: function (e) { if (e.target.value) save(row.cap, list.concat([e.target.value])); }
1408
- },
1409
- React.createElement('option', { value: '' }, '+ 分配模型'),
1410
- available.map(function (o) { return React.createElement('option', { key: o.ref, value: o.ref }, o.label); })) : null,
1411
- list.length ? React.createElement('button', {
1412
- className: 'iris-pm-btn', title: '清空分配后,该功能不再发起新调用',
1413
- onClick: function () { save(row.cap, []); }
1414
- }, '清空分配') : null);
1415
- }));
1857
+ }
1858
+ function save(cap, refs) { update({ capability: cap, model_refs: refs }); }
1859
+ if (!data) return React.createElement('div', { className: 'iris-wb-muted', role: 'status' },
1860
+ notePair[0] || '加载功能池…', notePair[0] ? React.createElement('button', { className: 'iris-pm-btn', disabled: loading,
1861
+ onClick: function () { load(); } }, loading ? '正在检查…' : '重新检查') : null);
1862
+ var supported = compatible(data);
1863
+ var order = data.order || {}, poolBy = data.poolByCapability || {};
1864
+ return React.createElement('div', { className: 'iris-cap-grid' },
1865
+ !supported ? React.createElement('div', { className: 'iris-cap-compatibility', role: 'alert' },
1866
+ '当前服务端尚未加载功能池策略,已暂停切换。请重启 DSH 后刷新页面;仅刷新页面无法更新服务端。',
1867
+ React.createElement('button', { className: 'iris-pm-btn', disabled: loading,
1868
+ onClick: function () { load(); } }, loading ? '正在检查…' : '重新检查服务端')) : null,
1869
+ CAP_ROWS.map(function (row) {
1870
+ var policy = data.policies && data.policies[row.cap] || { mode: (order[row.cap] || []).length ? 'manual' : 'disabled', models: order[row.cap] || [] };
1871
+ var mode = policy.mode;
1872
+ var list = order[row.cap] || [];
1873
+ var opts = poolBy[row.cap] || [];
1874
+ var status = data.readiness && data.readiness[row.cap];
1875
+ var available = opts.filter(function (o) { return list.indexOf(o.ref) < 0; });
1876
+ var message = feedback[row.cap];
1877
+ var blocked = !supported || !!busy || (message && message.phase === 'unknown');
1878
+ function setMode(value, hostVision) {
1879
+ if (blocked || saving.current) return;
1880
+ var body = { capability: row.cap, mode: value,
1881
+ ...(row.cap === 'vision' ? { hostVision: hostVision === undefined ? policy.hostVision === true : hostVision } : {}) };
1882
+ if (value === 'auto' && mode !== 'auto') {
1883
+ var count = opts.filter(function (o) { return !o.unavailable && !o.pendingConfirmation
1884
+ && (o.ref !== 'host:current-vision' || policy.hostVision === true); }).length;
1885
+ setFeedback(row.cap, null);
1886
+ confirmPair[1]({ cap: row.cap, body: body, count: count }); return;
1887
+ }
1888
+ update(body);
1889
+ }
1890
+ var chips = list.map(function (ref, i) {
1891
+ var opt = opts.find(function (o) { return o.ref === ref; });
1892
+ return React.createElement('span', { key: ref, className: 'iris-cap-chip' },
1893
+ React.createElement('b', {}, (i + 1) + '.'),
1894
+ opt ? opt.label + (opt.unavailable ? ' · ' + (opt.reason || '当前不可用') : '') : ref + ' · 配置已失效',
1895
+ mode === 'manual' && i > 0 ? React.createElement('button', { className: 'iris-pm-btn', title: '上移', disabled: blocked,
1896
+ onClick: function () { var n = list.slice(); var t = n.splice(i, 1)[0]; n.splice(i - 1, 0, t); save(row.cap, n); }
1897
+ }, '↑') : null,
1898
+ mode === 'manual' && i < list.length - 1 ? React.createElement('button', { className: 'iris-pm-btn', title: '下移', disabled: blocked,
1899
+ onClick: function () { var n = list.slice(); var t = n.splice(i, 1)[0]; n.splice(i + 1, 0, t); save(row.cap, n); }
1900
+ }, '↓') : null,
1901
+ mode === 'manual' ? React.createElement('button', { className: 'iris-pm-btn danger', title: '移出列表', disabled: blocked,
1902
+ onClick: function () { save(row.cap, list.filter(function (x) { return x !== ref; })); }
1903
+ }, '✕') : null);
1904
+ });
1905
+ return React.createElement('div', { key: row.cap, className: 'iris-cap-row', 'data-function': row.cap, 'aria-busy': busy === row.cap },
1906
+ React.createElement('span', { className: 'iris-cap-name' }, row.label),
1907
+ React.createElement('select', { 'aria-label': row.cap + ' 调用方式', value: confirmation && confirmation.cap === row.cap ? 'auto' : mode, disabled: blocked,
1908
+ onChange: function (e) { setMode(e.target.value); } },
1909
+ React.createElement('option', { value: 'auto' }, '按功能自动选择'),
1910
+ React.createElement('option', { value: 'manual' }, '仅用所选模型'),
1911
+ React.createElement('option', { value: 'disabled' }, '停用')),
1912
+ status && !(confirmation && confirmation.cap === row.cap) && !(message && ['saving', 'refreshing', 'saved-stale', 'unknown'].includes(message.phase)) ? React.createElement('span', { className: 'iris-pm-note', title: '候选来自配置和连接元数据;实际接口权限以调用结果为准' },
1913
+ status.candidateCount + ' 个可用候选' + (status.note ? ' · ' + status.note : '')) : null,
1914
+ confirmation && confirmation.cap === row.cap ? React.createElement('div', { className: 'iris-cap-confirm', role: 'group', 'aria-label': '确认自动选择' },
1915
+ React.createElement('div', {}, '尚未保存:将启用按功能自动选择,当前有 ' + confirmation.count + ' 个已确认候选。保留原手工名单;未确认的新发现模型不会自动调用。'),
1916
+ React.createElement('button', { className: 'iris-pm-btn', disabled: !!busy, onClick: function () { update(confirmation.body); } }, '确认启用自动池'),
1917
+ React.createElement('button', { className: 'iris-pm-btn', disabled: !!busy, onClick: function () { confirmPair[1](null); } }, '取消')) : null,
1918
+ message ? React.createElement('div', { className: 'iris-cap-feedback ' + message.phase, role: message.phase === 'unknown' ? 'alert' : 'status', 'aria-live': 'polite' },
1919
+ message.message, ['unknown', 'saved-stale'].includes(message.phase) ? React.createElement('button', { className: 'iris-pm-btn', disabled: !!busy || loading,
1920
+ onClick: function () { check(row.cap); } }, loading ? '正在核对…' : '刷新状态') : null) : null,
1921
+ mode === 'auto' ? React.createElement('span', { className: 'iris-pm-note' }, '按池顺序选择;确认模型用途后自动加入,无需再次分配。') : null,
1922
+ mode === 'auto' && row.cap === 'vision' ? React.createElement('label', { className: 'iris-pm-note' },
1923
+ React.createElement('input', { type: 'checkbox', checked: policy.hostVision === true, disabled: blocked,
1924
+ onChange: function (e) { setMode('auto', e.target.checked); } }),
1925
+ '使用DSH当前视觉模型(执行前核对图片能力)') : null,
1926
+ chips,
1927
+ mode === 'manual' && available.length ? React.createElement('select', { className: 'iris-pm-model-sel', value: '', disabled: blocked,
1928
+ onChange: function (e) { if (e.target.value) save(row.cap, list.concat([e.target.value])); } },
1929
+ React.createElement('option', { value: '' }, '+ 分配模型'),
1930
+ available.map(function (o) { return React.createElement('option', { key: o.ref, value: o.ref }, o.label); })) : null,
1931
+ mode === 'manual' && list.length ? React.createElement('button', { className: 'iris-pm-btn', title: '清空分配后,该功能不再发起新调用', disabled: blocked,
1932
+ onClick: function () { save(row.cap, []); }
1933
+ }, '清空分配') : null);
1934
+ }),
1935
+ notePair[0] ? React.createElement('div', { className: 'iris-pm-note' }, notePair[0]) : null);
1416
1936
  }
1417
1937
 
1418
1938
  /* ---- 泡泡常用卡片选择(localStorage + 跨座位响应) ---- */
@@ -1829,7 +2349,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1829
2349
  React.createElement('label', {}, '改图模型', React.createElement('select', { 'aria-label': '改图模型', value: model, disabled: !!running,
1830
2350
  onChange: function (e) { setModel(e.target.value); } }, React.createElement('option', { value: '' }, '按当前顺序选择可用改图模型'),
1831
2351
  (models || []).map(function (entry) { return React.createElement('option', { key: entry.id, value: entry.id, disabled: entry.blocked }, entry.label + (entry.blocked ? ' · 冷却或额度耗尽' : '')); }))),
1832
- models && !models.some(function (entry) { return !entry.blocked; }) ? React.createElement('div', { role: 'status' }, '暂无可用改图模型,请在生图名单分配聊天生图或 DashScope Qwen 改图模型,并检查模型是否可用。') : null,
2352
+ models && !models.some(function (entry) { return !entry.blocked; }) ? React.createElement('div', { role: 'status' }, '暂无可用改图模型,请检查图片功能的调用方式,确认聊天生图或 DashScope Qwen 改图模型用途及连接。') : null,
1833
2353
  React.createElement('div', { className: 'iris-gallery-actions' },
1834
2354
  React.createElement('button', { className: 'iris-clean-btn', type: 'button', disabled: !!running || !prompt.trim() || !models || !models.some(function (entry) { return !entry.blocked; }), onClick: execute }, running ? '编辑中…' : '生成修改版'),
1835
2355
  running ? React.createElement('button', { className: 'iris-pm-btn', type: 'button', onClick: cancel }, '停止等待') : null),
@@ -2664,6 +3184,24 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
2664
3184
  React.createElement('div', { className: 'iris-wb-muted' }, '只读本机注册证据;不调用 Browser、模型或供应商。'));
2665
3185
  }
2666
3186
 
3187
+ function revealWorkbenchItem(node) {
3188
+ for (var parent = node.parentElement; parent; parent = parent.parentElement) {
3189
+ if (parent.tagName === 'DETAILS') parent.open = true;
3190
+ }
3191
+ }
3192
+ function WorkbenchSection(props) {
3193
+ var expandedPair = React.useState(false);
3194
+ return React.createElement(props.collapsible ? 'details' : 'section', { className: 'iris-wb-section',
3195
+ 'data-iris-section': props.name, 'aria-label': props.title,
3196
+ onToggle: props.collapsible ? function (event) { expandedPair[1](event.currentTarget.open); } : undefined },
3197
+ React.createElement(props.collapsible ? 'summary' : 'div', { className: 'iris-wb-section-head' },
3198
+ React.createElement('div', {}, React.createElement('h2', {}, props.title),
3199
+ props.description ? React.createElement('p', {}, props.description) : null),
3200
+ props.collapsible ? React.createElement('span', { className: 'iris-wb-section-arrow', 'aria-hidden': true }, expandedPair[0] ? '收起' : '展开',
3201
+ React.createElement('svg', { width: 15, height: 15, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.8 },
3202
+ React.createElement('path', { d: 'm6 9 6 6 6-6' }))) : null),
3203
+ React.createElement('div', { className: 'iris-wb-section-body' }, props.children));
3204
+ }
2667
3205
  function WorkbenchPanel() {
2668
3206
  var state = useIrisState(5000);
2669
3207
  var running = (state && state.tasks && state.tasks.running) || [];
@@ -2677,46 +3215,45 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
2677
3215
  var selectedTask = selPair[0];
2678
3216
  var setSelectedTask = selPair[1];
2679
3217
  function onSelect(id) { setSelectedTask(id); }
2680
- return React.createElement('div', { className: 'iris-wb' },
3218
+ return React.createElement('div', { className: 'iris-wb iris-workbench-page' },
2681
3219
  React.createElement('div', { className: 'iris-wb-head' },
2682
3220
  React.createElement('span', { className: 'iris-wb-title' }, 'Iris 工作台'),
2683
3221
  React.createElement('span', { className: 'iris-wb-date' }, state ? '刷新于 ' + new Date().toLocaleTimeString() : '加载中…')),
2684
- React.createElement('div', { className: 'iris-wb-sec' }, '供应商'),
2685
- React.createElement(ProviderManager, {}),
2686
- React.createElement('div', { className: 'iris-wb-sec' }, '能力健康'),
2687
3222
  React.createElement(CapabilityHealthOverview, {}),
2688
- React.createElement('div', { className: 'iris-wb-muted' }, '绿色有效 7 天;真实成功或显式实测会刷新。429 冷却显示琥珀色,额度或预算耗尽显示红色;点击模型能力可实测恢复。不会后台探测。'),
2689
- React.createElement('div', { className: 'iris-wb-sec' }, '功能分配(调用顺序)'),
2690
- React.createElement(CapabilityAssigner, {}),
2691
- React.createElement('div', { className: 'iris-wb-sec' }, 'Iris 泡泡快捷卡片(勾选后显示在「⚡ 常用」标签)'),
2692
- React.createElement(BubbleCardPicker, {}),
2693
- React.createElement('div', { className: 'iris-wb-sec' }, '对话框提示词优化'),
2694
- React.createElement(PromptOptimizerSettings, {}),
2695
- React.createElement('div', { className: 'iris-wb-sec' }, '宿主诊断'),
2696
- React.createElement(HostDoctorPanel, {}),
2697
- React.createElement('div', { className: 'iris-wb-sec' }, '运行中任务'),
2698
- React.createElement('div', { className: 'iris-wb-box' },
3223
+ React.createElement(WorkbenchSection, { name: 'providers', title: '供应商与模型', description: '管理连接、模型用途与接口协议。' },
3224
+ React.createElement(ProviderManager, {})),
3225
+ React.createElement(WorkbenchSection, { name: 'pools', title: '功能池与调用方式', description: '选择自动池、所选模型或停用,查看每项功能的候选范围。', collapsible: true },
3226
+ React.createElement(CapabilityAssigner, {})),
3227
+ React.createElement(WorkbenchSection, { name: 'running', title: '运行中任务' },
3228
+ React.createElement('div', { className: 'iris-wb-box' },
2699
3229
  running.length || coreGroups.live.length ? [].concat(
2700
3230
  running.map(function (t) { return taskRow(t, true, selectedTask === t.id, onSelect); }),
2701
3231
  coreGroups.live.map(coreTaskCard))
2702
- : React.createElement('div', { className: 'iris-wb-empty' }, '暂无进行中任务')),
2703
- attention.length || coreGroups.attention.length || coreUser.degraded ? React.createElement('div', {},
2704
- React.createElement('div', { className: 'iris-wb-sec' }, '需要处理'),
3232
+ : React.createElement('div', { className: 'iris-wb-empty' }, '暂无进行中任务'))),
3233
+ attention.length || coreGroups.attention.length || coreUser.degraded ? React.createElement(WorkbenchSection, { name: 'attention', title: '需要处理' },
2705
3234
  React.createElement('div', { className: 'iris-wb-box' },
2706
3235
  coreUser.degraded ? React.createElement('div', { className: 'iris-clean-note' },
2707
3236
  '部分 Core 记录损坏或媒体缺失,已跳过 ' + coreUser.droppedTasks + ' 条;其余事实照常显示。') : null,
2708
3237
  [].concat(
2709
3238
  attention.map(function (t) { return taskRow(t, false, selectedTask === t.id, onSelect); }),
2710
3239
  coreGroups.attention.map(coreTaskCard)))) : null,
2711
- React.createElement('div', { className: 'iris-wb-sec' }, '作品库'),
2712
- React.createElement(ArtifactGallery, { total: artifactTotal }),
2713
- React.createElement('div', { className: 'iris-wb-sec' }, '高级诊断'),
2714
- React.createElement(CoreRuntimePanel, {}),
2715
- React.createElement('div', { className: 'iris-wb-sec' }, '历史任务'),
2716
- React.createElement(CleanupBar, {}),
2717
- React.createElement(HistoryBrowser, { recent: recent, coreHistory: [].concat(coreGroups.done, coreGroups.acknowledged), selected: selectedTask, onSelect: onSelect }),
2718
- React.createElement('div', { className: 'iris-wb-sec' }, '操作'),
2719
- React.createElement(ActionGroups, {}));
3240
+ React.createElement(WorkbenchSection, { name: 'artifacts', title: '作品库', description: '查看、下载和继续处理已有作品。' },
3241
+ React.createElement(ArtifactGallery, { total: artifactTotal })),
3242
+ React.createElement(WorkbenchSection, { name: 'actions', title: '创作工具', description: '生成媒体、导入图片、识别与处理素材。', collapsible: true },
3243
+ React.createElement(ActionGroups, {})),
3244
+ React.createElement(WorkbenchSection, { name: 'preferences', title: '快捷入口与偏好', description: '设置泡泡快捷卡片与对话框提示词优化。', collapsible: true },
3245
+ React.createElement('div', { className: 'iris-wb-sec' }, 'Iris 泡泡快捷卡片(勾选后显示在「⚡ 常用」标签)'),
3246
+ React.createElement(BubbleCardPicker, {}),
3247
+ React.createElement('div', { className: 'iris-wb-sec' }, '对话框提示词优化'),
3248
+ React.createElement(PromptOptimizerSettings, {})),
3249
+ React.createElement(WorkbenchSection, { name: 'diagnostics', title: '诊断与运行信息', description: '查看宿主装载与 Core 任务事实。', collapsible: true },
3250
+ React.createElement('div', { className: 'iris-wb-sec' }, '宿主诊断'),
3251
+ React.createElement(HostDoctorPanel, {}),
3252
+ React.createElement('div', { className: 'iris-wb-sec' }, '高级诊断'),
3253
+ React.createElement(CoreRuntimePanel, {})),
3254
+ React.createElement(WorkbenchSection, { name: 'history', title: '历史任务', description: '查看历史记录与清理选项。', collapsible: true },
3255
+ React.createElement(CleanupBar, {}),
3256
+ React.createElement(HistoryBrowser, { recent: recent, coreHistory: [].concat(coreGroups.done, coreGroups.acknowledged), selected: selectedTask, onSelect: onSelect })));
2720
3257
  }
2721
3258
 
2722
3259
  /* ---- 泡泡浮层(阶段 4 续:两标签,尽量干净)---- */