@mokuyoaxis/dsh-iris 0.1.3 → 0.1.4

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 (51) hide show
  1. package/.dsh/skills/iris-compose-media/SKILL.md +23 -49
  2. package/.dsh/skills/iris-compose-media/references/task-v2-recovery.md +30 -0
  3. package/.dsh/skills/iris-compose-media/references/workflows.md +38 -0
  4. package/.dsh/skills/iris-verify-ui/SKILL.md +3 -25
  5. package/.dsh/skills/iris-verify-ui/agents/openai.yaml +4 -0
  6. package/.dsh/skills/iris-verify-ui/references/verdict-and-degradation.md +31 -0
  7. package/CHANGELOG.md +24 -2
  8. package/README.en.md +29 -7
  9. package/README.md +29 -7
  10. package/docs/ARCHITECTURE.md +27 -13
  11. package/docs/ARTIFACT_LIBRARY.md +24 -0
  12. package/docs/HOST_ADAPTER_CONTRACT.md +86 -0
  13. package/docs/HOST_DOCTOR.md +47 -0
  14. package/docs/PROVIDER_ADAPTER_CONTRACT.md +97 -0
  15. package/docs/PROVIDER_HEALTH.md +39 -0
  16. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +2 -2
  17. package/docs/ROADMAP.md +18 -8
  18. package/docs/SECURITY.md +6 -2
  19. package/docs/TASK_SEMANTICS.md +3 -2
  20. package/docs/assets/diagrams/iris-architecture.png +0 -0
  21. package/docs/assets/diagrams/iris-architecture.svg +5 -5
  22. package/docs/releases/0.1.4.md +59 -0
  23. package/lib/actions.js +357 -221
  24. package/lib/adapters.js +7 -7
  25. package/lib/api.js +30 -11
  26. package/lib/artifacts.js +239 -0
  27. package/lib/bundled-skills.js +5 -5
  28. package/lib/client.js +370 -55
  29. package/lib/config.js +192 -9
  30. package/lib/doctor.js +175 -4
  31. package/lib/dsh-host-adapter.js +189 -0
  32. package/lib/host-contract.js +131 -0
  33. package/lib/host-runtime.js +156 -0
  34. package/lib/index.js +97 -113
  35. package/lib/locate.js +1 -1
  36. package/lib/media.js +16 -6
  37. package/lib/prompt-optimizer.js +10 -20
  38. package/lib/provider-adapter.js +371 -0
  39. package/lib/provider-adapters.js +279 -0
  40. package/lib/provider-health.js +59 -0
  41. package/lib/render.js +43 -12
  42. package/lib/tasks.js +29 -5
  43. package/lib/vision.js +23 -49
  44. package/package.json +14 -8
  45. package/user_guide.md +42 -2
  46. package/docs/assets/diagrams/iris-architecture.drawio +0 -1
  47. package/docs/assets/diagrams/iris-task-lifecycle.drawio +0 -1
  48. package/docs/assets/diagrams/iris-workflow-compose.drawio +0 -1
  49. package/docs/releases/0.1.1.md +0 -51
  50. package/docs/releases/0.1.2.md +0 -26
  51. package/docs/releases/0.1.3.md +0 -30
package/lib/client.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * DSH web client bundle 标准形态(与 dsh-at-file / dsh-better-sidebar 同构):
5
5
  * window.__ModuleLoader__.load({ id, factory }) → CJS 工厂,require 解析宿主依赖,
6
6
  * `inject` 声明本 bundle 需要的 ctx 服务 key(这里是 slots),
7
- * `apply(ctx)` 里直接 `ctx.slots.inject(...)` 注册座位。
7
+ * `apply(ctx)` 通过本地 clientSlots 适配端口注册座位。
8
8
  *
9
9
  * 三个零替换风险的座位:
10
10
  * - settings.section(id: 'iris-workbench'):Iris 工作台整页 ——
@@ -21,6 +21,42 @@ 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.1.4';
25
+ var HOST_CLIENT_PROTOCOL_VERSION = 0;
26
+ var reportedSeats = [];
27
+
28
+ function reportClientSeat(seat) {
29
+ if (reportedSeats.indexOf(seat) < 0) reportedSeats.push(seat);
30
+ if (!window || typeof window.fetch !== 'function') return;
31
+ try {
32
+ Promise.resolve(window.fetch('/iris/api/host-client', {
33
+ method: 'POST',
34
+ headers: { 'content-type': 'application/json' },
35
+ credentials: 'same-origin',
36
+ body: JSON.stringify({
37
+ pluginId: '@mokuyoaxis/dsh-iris',
38
+ version: IRIS_CLIENT_VERSION,
39
+ protocolVersion: HOST_CLIENT_PROTOCOL_VERSION,
40
+ seats: reportedSeats.slice()
41
+ })
42
+ })).catch(function () {});
43
+ } catch (_) { /* Doctor handshake 不影响 UI */ }
44
+ }
45
+
46
+ // Client bundle 不能导入 server-side Host contract;只在此边界读取 DSH slots 服务。
47
+ function clientSlotsPort(ctx) {
48
+ var slots;
49
+ try { slots = ctx && ctx.slots; } catch (_) { slots = null; }
50
+ if (!slots || typeof slots.inject !== 'function' || typeof slots.register !== 'function') {
51
+ var error = new Error('Iris clientSlots 不可用:DSH slots 需要 inject/register');
52
+ error.code = slots ? 'IRIS_HOST_CAPABILITY_INCOMPATIBLE' : 'IRIS_HOST_CAPABILITY_UNAVAILABLE';
53
+ throw error;
54
+ }
55
+ return Object.freeze({
56
+ inject: function (seat, callback) { return slots.inject(seat, callback); },
57
+ register: function (definition, component) { return slots.register(definition, component); }
58
+ });
59
+ }
24
60
 
25
61
  var STYLE_ID = 'iris-wb-css';
26
62
  var cssText = [
@@ -57,6 +93,15 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
57
93
  '.iris-wb-drawer .iris-wb-prompt { white-space: pre-wrap; word-break: break-word; font-size: 12px; }',
58
94
  '.iris-wb-drawer .iris-wb-err { color: var(--dsw-alias-state-error-primary, #e06c6c); white-space: pre-wrap; word-break: break-word; font-size: 12px; }',
59
95
  '.iris-wb-loading { color: var(--dsw-alias-label-secondary, #9a9a9a); font-size: 12px; }',
96
+ '.iris-doctor-top { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }',
97
+ '.iris-doctor-score { font-weight: 600; flex: 1; }',
98
+ '.iris-doctor-list { display: grid; gap: 4px; margin-top: 7px; }',
99
+ '.iris-doctor-row { display: grid; grid-template-columns: 14px 1fr; gap: 6px; font-size: 11px; }',
100
+ '.iris-doctor-row.ok { color: var(--dsw-alias-label-secondary, #9a9a9a); }',
101
+ '.iris-doctor-row.warn { color: var(--dsw-alias-state-warn-primary, #d9a941); }',
102
+ '.iris-doctor-row.error { color: var(--dsw-alias-state-error-primary, #e06c6c); }',
103
+ '.iris-doctor-details { margin-top: 7px; color: var(--dsw-alias-label-secondary, #9a9a9a); }',
104
+ '.iris-doctor-details summary { cursor: pointer; font-size: 11px; }',
60
105
  /* ---- 操作卡片组(阶段 5 GUI 直连) ---- */
61
106
  '.iris-act-group { display: grid; gap: 8px; }',
62
107
  '.iris-act-card { background: var(--dsw-alias-bg-layer-1, #26262e); border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 8px; overflow: hidden; }',
@@ -96,8 +141,17 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
96
141
  '.iris-pm-field { display: grid; gap: 3px; }',
97
142
  '.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; }',
98
143
  '.iris-pm-act { font-size: 11px; color: var(--dsw-alias-label-secondary, #9a9a9a); }',
99
- '.iris-pm-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--dsw-alias-label-secondary, #9a9a9a); margin-right: 4px; }',
100
- '.iris-pm-dot.lit { background: var(--dsw-alias-state-success-primary, #6fcf6f); box-shadow: 0 0 6px var(--dsw-alias-state-success-primary, #6fcf6f); }',
144
+ '.iris-pm-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--dsw-alias-label-tertiary, #73737d); margin-right: 4px; }',
145
+ '.iris-pm-dot.configured { background: #6f9df6; box-shadow: 0 0 7px rgba(111,157,246,.55); }',
146
+ '.iris-pm-dot.verified { background: var(--dsw-alias-state-success-primary, #6fcf6f); box-shadow: 0 0 7px var(--dsw-alias-state-success-primary, #6fcf6f); }',
147
+ '.iris-pm-dot.failed { background: #b9656d; box-shadow: 0 0 6px rgba(185,101,109,.38); opacity: .82; }',
148
+ '.iris-health-grid { display: grid; gap: 5px; }',
149
+ '.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; }',
150
+ '.iris-health-name { font-weight: 600; }',
151
+ '.iris-health-state { color: var(--dsw-alias-label-secondary, #9a9a9a); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }',
152
+ '.iris-health-row.failed .iris-health-state { color: #c87a81; }',
153
+ '.iris-health-row.configured .iris-health-state { color: #7da8fa; }',
154
+ '.iris-health-row.verified .iris-health-state { color: var(--dsw-alias-state-success-primary, #6fcf6f); }',
101
155
  '.iris-cap-grid { display: grid; gap: 6px; }',
102
156
  '.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; }',
103
157
  '.iris-cap-name { font-weight: 600; min-width: 64px; }',
@@ -109,10 +163,12 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
109
163
  '.iris-bubble { position: fixed; z-index: 9999; cursor: grab; user-select: none; touch-action: none; }',
110
164
  '.iris-bubble.dragging { cursor: grabbing; }',
111
165
  '.iris-bubble-btn { width: 44px; height: 44px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 20px; background: var(--dsw-alias-bg-layer-2, #32323c); border: 1px solid var(--dsw-alias-border-l1, #3a3a44); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.35); transition: box-shadow 0.2s, border-color 0.2s, opacity 0.2s; }',
112
- '.iris-bubble.lit .iris-bubble-btn { border-color: var(--dsw-alias-state-success-primary, #6fcf6f); box-shadow: 0 0 14px var(--dsw-alias-state-success-primary, #6fcf6f); }',
113
- '.iris-bubble.dim .iris-bubble-btn { opacity: 0.55; border-color: var(--dsw-alias-border-l1, #3a3a44); }',
166
+ '.iris-bubble.health-configured .iris-bubble-btn { border-color: #6f9df6; box-shadow: 0 0 14px rgba(111,157,246,.55); }',
167
+ '.iris-bubble.health-verified .iris-bubble-btn { border-color: var(--dsw-alias-state-success-primary, #6fcf6f); box-shadow: 0 0 14px var(--dsw-alias-state-success-primary, #6fcf6f); }',
168
+ '.iris-bubble.health-failed .iris-bubble-btn { opacity: .72; border-color: #b9656d; box-shadow: 0 0 11px rgba(185,101,109,.34); }',
169
+ '.iris-bubble.health-unconfigured .iris-bubble-btn { opacity: 0.55; border-color: var(--dsw-alias-border-l1, #3a3a44); }',
114
170
  '.iris-bubble-badge { position: absolute; top: -2px; right: -2px; min-width: 16px; height: 16px; border-radius: 8px; background: var(--dsw-alias-state-warn-primary, #d9a941); color: #1a1a1e; font-size: 10px; font-weight: 700; display: flex; align-items: center; justify-content: center; padding: 0 3px; }',
115
- '.iris-bubble-panel { position: fixed; z-index: 9998; width: 360px; max-height: min(520px, 70vh); overflow: auto; background: var(--dsw-alias-bg-layer-1, #26262e); border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 12px; padding: 12px 14px; font-size: 12px; line-height: 1.45; box-shadow: 0 8px 30px rgba(0, 0, 0, 0.45); }',
171
+ '.iris-bubble-panel { position: fixed; z-index: 9998; box-sizing: border-box; width: min(360px, calc(100vw - 16px)); max-height: min(520px, calc(100dvh - 16px)); overflow: auto; overscroll-behavior: contain; background: var(--dsw-alias-bg-layer-1, #26262e); border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 12px; padding: 12px 14px; font-size: 12px; line-height: 1.45; box-shadow: 0 8px 30px rgba(0, 0, 0, 0.45); }',
116
172
  '.iris-bubble-panel-head { position: absolute; top: 6px; right: 6px; z-index: 2; }',
117
173
  '.iris-bubble-close { border: none; background: transparent; color: var(--dsw-alias-label-secondary, #9a9a9a); font-size: 14px; cursor: pointer; padding: 4px 6px; }',
118
174
  '.iris-bubble-close:hover { color: var(--dsw-alias-label-primary, #e6e6e6); }',
@@ -136,6 +192,22 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
136
192
  '.iris-clean-btn:hover { border-color: var(--dsw-alias-brand-primary, #7aa2f7); }',
137
193
  '.iris-clean-btn.danger:hover { color: var(--dsw-alias-state-error-primary, #e06c6c); border-color: var(--dsw-alias-state-error-primary, #e06c6c); }',
138
194
  '.iris-clean-note { font-size: 11px; color: var(--dsw-alias-label-secondary, #9a9a9a); }',
195
+ /* 独立作品库 v0:任务清理后仍可浏览,窄屏自动收缩。 */
196
+ '.iris-gallery-tools { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; }',
197
+ '.iris-gallery-tools .iris-clean-note { flex: 1; min-width: 100px; }',
198
+ '.iris-gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(148px, 1fr)); gap: 10px; }',
199
+ '.iris-gallery-card { min-width: 0; overflow: hidden; border: 1px solid rgba(150,164,215,.16); border-radius: 13px; background: color-mix(in srgb, var(--dsw-alias-bg-layer-1, #26262e) 92%, transparent); box-shadow: 0 7px 22px rgba(0,0,0,.12); }',
200
+ '.iris-gallery-preview { display: flex; align-items: center; justify-content: center; width: 100%; aspect-ratio: 1 / 1; overflow: hidden; background: rgba(8,10,16,.28); }',
201
+ '.iris-gallery-preview img, .iris-gallery-preview video { width: 100%; height: 100%; object-fit: contain; }',
202
+ '.iris-gallery-preview audio { width: calc(100% - 16px); }',
203
+ '.iris-gallery-file { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; font-weight: 600; }',
204
+ '.iris-gallery-meta { color: var(--dsw-alias-label-tertiary, #7a7a85); font-size: 10px; }',
205
+ '.iris-gallery-info { display: grid; gap: 4px; padding: 8px 9px 9px; }',
206
+ '.iris-gallery-actions { display: flex; align-items: center; gap: 8px; }',
207
+ '.iris-artifact-mini { display: flex; align-items: center; gap: 7px; min-width: 0; padding: 4px 6px; border-radius: 7px; color: var(--dsw-alias-label-secondary, #9a9a9a); text-decoration: none; }',
208
+ '.iris-artifact-mini:hover { background: var(--dsw-alias-bg-layer-2, #32323c); color: var(--dsw-alias-label-primary, #e6e6e6); }',
209
+ '.iris-artifact-mini span:nth-child(2) { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }',
210
+ '@media (max-width: 360px) { .iris-gallery-grid { grid-template-columns: 1fr; } }',
139
211
  /* 模型池(阶段 9 P4) */
140
212
  '.iris-pm-pool { display: grid; gap: 4px; }',
141
213
  '.iris-pm-mrows { display: grid; gap: 3px; max-height: 220px; overflow: auto; }',
@@ -144,8 +216,10 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
144
216
  '.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; }',
145
217
  '.iris-pm-mcaps { display: flex; gap: 3px; }',
146
218
  '.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); }',
147
- '.iris-pm-ver.ok { color: var(--dsw-alias-state-success-primary, #6fcf6f); }',
148
- '.iris-pm-ver.bad { color: var(--dsw-alias-state-error-primary, #e06c6c); }',
219
+ '.iris-pm-ver.configured { color: #7da8fa; }',
220
+ '.iris-pm-ver.verified { color: var(--dsw-alias-state-success-primary, #6fcf6f); }',
221
+ '.iris-pm-ver.failed { color: #c87a81; border-color: rgba(185,101,109,.5); }',
222
+ '.iris-pm-ver.unconfigured { opacity: .55; }',
149
223
  '.iris-pm-madd { flex: 1; background: var(--dsw-alias-bg-layer-2, #32323c); color: var(--dsw-alias-label-primary, #e6e6e6); border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 4px; padding: 3px 6px; font-size: 12px; }',
150
224
  /* 文件选择器(阶段 10) */
151
225
  '.iris-ff { display: grid; gap: 4px; }',
@@ -294,13 +368,60 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
294
368
  return sharedState;
295
369
  }
296
370
 
297
- /** 功能卡就绪态直接来自共享 Iris 状态,不再等用户点开卡片后才加载。 */
298
- function capabilityReady(state, capability) {
299
- if (!capability) return true;
371
+ var HEALTH_META = {
372
+ unconfigured: { label: '未配置', symbol: '○' },
373
+ configured: { label: '已配置,待验证', symbol: '◌' },
374
+ verified: { label: '近期验证成功', symbol: '●' },
375
+ failed: { label: '认证失败', symbol: '×' }
376
+ };
377
+
378
+ function capabilityHealth(state, capability) {
379
+ var health = state && state.health && state.health.capabilities
380
+ && state.health.capabilities[capability];
381
+ if (health && HEALTH_META[health.status]) return health;
300
382
  var providers = (state && state.providers) || [];
301
- return providers.some(function (p) {
302
- return p.enabled && p.apiKeyHint && Array.isArray(p.capabilities) && p.capabilities.indexOf(capability) >= 0;
383
+ var configured = providers.some(function (p) {
384
+ return p.enabled && p.apiKeyHint && Array.isArray(p.capabilities)
385
+ && p.capabilities.indexOf(capability) >= 0;
303
386
  });
387
+ return { status: configured ? 'configured' : 'unconfigured', candidateCount: configured ? 1 : 0 };
388
+ }
389
+
390
+ function relativeHealthTime(value) {
391
+ var ms = Date.now() - Date.parse(String(value || ''));
392
+ if (!Number.isFinite(ms) || ms < 0) return '';
393
+ if (ms < 60000) return '刚刚';
394
+ if (ms < 3600000) return Math.floor(ms / 60000) + ' 分钟前';
395
+ if (ms < 86400000) return Math.floor(ms / 3600000) + ' 小时前';
396
+ return Math.floor(ms / 86400000) + ' 天前';
397
+ }
398
+
399
+ function healthText(health) {
400
+ var status = health && HEALTH_META[health.status] ? health.status : 'unconfigured';
401
+ var text = HEALTH_META[status].label;
402
+ var age = relativeHealthTime(health && health.observedAt);
403
+ return text + (age ? ' · ' + age : '');
404
+ }
405
+
406
+ function CapabilityHealthOverview() {
407
+ var state = useIrisState(5000);
408
+ var rows = [
409
+ ['image-gen', '图片'],
410
+ ['video-gen', '视频'],
411
+ ['tts', '语音'],
412
+ ['transcribe', '转写'],
413
+ ['vision', '视觉']
414
+ ];
415
+ return React.createElement('div', { className: 'iris-health-grid' }, rows.map(function (row) {
416
+ var health = capabilityHealth(state, row[0]);
417
+ var meta = HEALTH_META[health.status] || HEALTH_META.unconfigured;
418
+ return React.createElement('div', { key: row[0], className: 'iris-health-row ' + health.status },
419
+ React.createElement('span', { className: 'iris-health-name' },
420
+ React.createElement('span', { className: 'iris-pm-dot ' + health.status }, meta.symbol),
421
+ row[1]),
422
+ React.createElement('span', { className: 'iris-health-state', title: healthText(health) }, healthText(health)),
423
+ React.createElement('span', { className: 'iris-wb-muted' }, String(health.candidateCount || 0) + ' 路'));
424
+ }));
304
425
  }
305
426
 
306
427
  /* ---- 任务详情抽屉:按需拉取 /iris/api/task/:id ---- */
@@ -571,10 +692,17 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
571
692
  var setNewId = addPair[1];
572
693
  var models = p.models || [];
573
694
  function verBadge(m, c) {
574
- var v = m.verified && m.verified[c];
575
- var cls = 'iris-pm-ver' + (v ? (' ' + (v.ok ? 'ok' : 'bad')) : '');
576
- var title = v ? ((v.ok ? '实测通过:' : '实测未通过:') + (v.note || '')) : '点击单独验证此能力';
577
- return React.createElement('button', { key: c, className: cls, title: title, onClick: function () { props.testModel(p, m, c); } }, c + (v ? (v.ok ? '✓' : '✗') : '?'));
695
+ var health = m.health && m.health[c];
696
+ var status = health && HEALTH_META[health.status] ? health.status : 'configured';
697
+ var cls = 'iris-pm-ver ' + status;
698
+ var title = healthText(health || { status: status });
699
+ var symbol = status === 'verified' ? '✓' : (status === 'failed' ? '×' : (status === 'unconfigured' ? '○' : '?'));
700
+ return React.createElement('button', {
701
+ key: c,
702
+ className: cls,
703
+ title: title + ';点击单独验证此能力',
704
+ onClick: function () { props.testModel(p, m, c); }
705
+ }, c + symbol);
578
706
  }
579
707
  var rows = models.map(function (m) {
580
708
  var caps = m.capabilities || [];
@@ -811,6 +939,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
811
939
  .then(function (res) { setRun(res.ok ? { ok: true, text: res.d.text || '', img: res.d.imageDataUrl || null } : { ok: false, text: res.d.error || '执行失败' }); })
812
940
  .catch(function () { setRun({ ok: false, text: '网络错误' }); });
813
941
  }
942
+ var cardHealth = capability ? capabilityHealth(irisState, capability) : { status: 'verified' };
814
943
  var body = null;
815
944
  if (open) {
816
945
  var inputs = fields.map(function (f) {
@@ -846,16 +975,17 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
846
975
  result = React.createElement('div', { className: 'iris-act-result' + (run.ok ? '' : ' err') }, run.text);
847
976
  }
848
977
  }
849
- // 只读模型提示:当前能力解析到的模型;改分配去「能力分配」区(唯一写入口)
978
+ // 只读模型提示:当前能力解析到的模型与持久健康事实。
850
979
  var modelBar = null;
851
980
  if (capability) {
852
- var lit = modelInfo && modelInfo.loaded && modelInfo.options.length > 0;
981
+ var available = modelInfo && modelInfo.loaded && modelInfo.options.length > 0;
853
982
  var shown = (vals.model && vals.model.trim()) || (modelInfo && modelInfo.assigned) || '';
854
983
  modelBar = React.createElement('div', { className: 'iris-act-field' },
855
984
  React.createElement('label', {},
856
- React.createElement('span', { className: lit ? 'iris-pm-dot lit' : 'iris-pm-dot' }, '●'),
857
- ' 模型:' + (shown || '自动') + (lit ? '' : '(无可用模型)')),
858
- React.createElement('div', { className: 'iris-wb-muted' }, '改分配 → 上方「能力分配(failover 顺序)」'));
985
+ React.createElement('span', { className: 'iris-pm-dot ' + cardHealth.status }, HEALTH_META[cardHealth.status].symbol),
986
+ ' 模型:' + (shown || '自动') + (available ? '' : '(无可用模型)')),
987
+ React.createElement('div', { className: 'iris-wb-muted' },
988
+ healthText(cardHealth) + ' · 改分配 → 上方「能力分配(failover 顺序)」'));
859
989
  }
860
990
  body = React.createElement('div', { className: 'iris-act-body' },
861
991
  modelBar,
@@ -864,13 +994,17 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
864
994
  run === 'running' ? '执行中…' : '执行'),
865
995
  result);
866
996
  }
867
- var headError = !!(run && run !== 'running' && run.ok === false);
868
- var headLit = !capability || (capabilityReady(irisState, capability) && !headError);
869
- var headTitle = headError ? '最近一次执行出错' : (headLit ? 'API 与能力已配置' : '缺少可用 API 或能力模型');
870
- return React.createElement('div', { className: 'iris-act-card' + (open ? ' open' : '') + (headLit ? ' lit' : ' dim') },
997
+ var headStatus = capability ? cardHealth.status : 'verified';
998
+ var headTitle = capability ? healthText(cardHealth) : '本地功能';
999
+ return React.createElement('div', {
1000
+ className: 'iris-act-card' + (open ? ' open' : '') + (capability ? ' health-' + headStatus : '')
1001
+ },
871
1002
  React.createElement('div', { className: 'iris-act-head', onClick: function () { setOpen(!open); } },
872
1003
  React.createElement('span', { className: 'iris-act-title' }, title),
873
- capability ? React.createElement('span', { className: headLit ? 'iris-pm-dot lit' : 'iris-pm-dot', title: headTitle }, '●') : null,
1004
+ capability ? React.createElement('span', {
1005
+ className: 'iris-pm-dot ' + headStatus,
1006
+ title: headTitle
1007
+ }, HEALTH_META[headStatus].symbol) : null,
874
1008
  React.createElement('span', { className: 'iris-act-arrow' }, '▶')),
875
1009
  body);
876
1010
  }
@@ -1054,6 +1188,105 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1054
1188
  }));
1055
1189
  }
1056
1190
 
1191
+ function fmtBytes(bytes) {
1192
+ var value = Number(bytes || 0);
1193
+ if (value < 1024) return value + 'B';
1194
+ if (value < 1048576) return (value / 1024).toFixed(0) + 'KB';
1195
+ return (value / 1048576).toFixed(1) + 'MB';
1196
+ }
1197
+
1198
+ function artifactIcon(item) {
1199
+ var mime = String(item && item.mime || '');
1200
+ if (mime.indexOf('image/') === 0) return '🖼';
1201
+ if (mime.indexOf('video/') === 0) return '🎬';
1202
+ if (mime.indexOf('audio/') === 0) return '🔊';
1203
+ return '📄';
1204
+ }
1205
+
1206
+ function artifactRowMini(item) {
1207
+ return React.createElement('a', { key: item.id, className: 'iris-artifact-mini', href: item.url, target: '_blank', rel: 'noreferrer', title: '打开作品' },
1208
+ React.createElement('span', {}, artifactIcon(item)),
1209
+ React.createElement('span', {}, item.file),
1210
+ React.createElement('span', { className: 'iris-gallery-meta' }, fmtBytes(item.size)));
1211
+ }
1212
+
1213
+ function artifactPreview(item) {
1214
+ var mime = String(item.mime || '');
1215
+ if (mime.indexOf('image/') === 0) {
1216
+ return React.createElement('a', { className: 'iris-gallery-preview', href: item.url, target: '_blank', rel: 'noreferrer', title: '打开原图' },
1217
+ React.createElement('img', { src: item.url, alt: 'Iris 作品 ' + item.file, loading: 'lazy' }));
1218
+ }
1219
+ if (mime.indexOf('video/') === 0) {
1220
+ return React.createElement('div', { className: 'iris-gallery-preview' },
1221
+ React.createElement('video', { src: item.url, controls: true, playsInline: true, preload: 'metadata' }));
1222
+ }
1223
+ if (mime.indexOf('audio/') === 0) {
1224
+ return React.createElement('div', { className: 'iris-gallery-preview' },
1225
+ React.createElement('audio', { src: item.url, controls: true, preload: 'metadata' }));
1226
+ }
1227
+ return React.createElement('a', { className: 'iris-gallery-preview iris-wb-link', href: item.url, target: '_blank', rel: 'noreferrer' }, '打开文件');
1228
+ }
1229
+
1230
+ function ArtifactGallery(props) {
1231
+ var itemsPair = React.useState([]);
1232
+ var items = itemsPair[0];
1233
+ var setItems = itemsPair[1];
1234
+ var totalPair = React.useState(Number(props.total || 0));
1235
+ var total = totalPair[0];
1236
+ var setTotal = totalPair[1];
1237
+ var busyPair = React.useState(false);
1238
+ var busy = busyPair[0];
1239
+ var setBusy = busyPair[1];
1240
+ var notePair = React.useState('');
1241
+ var note = notePair[0];
1242
+ var setNote = notePair[1];
1243
+
1244
+ function load(append) {
1245
+ if (busy) return;
1246
+ setBusy(true);
1247
+ var offset = append ? items.length : 0;
1248
+ fetch('/iris/api/artifacts?offset=' + offset + '&limit=24')
1249
+ .then(function (res) { return res.ok ? res.json() : Promise.reject(new Error('读取失败')); })
1250
+ .then(function (data) {
1251
+ setItems(append ? items.concat(data.items || []) : (data.items || []));
1252
+ setTotal(Number(data.total || 0));
1253
+ })
1254
+ .catch(function (error) { setNote('作品库读取失败:' + error.message); })
1255
+ .finally(function () { setBusy(false); });
1256
+ }
1257
+
1258
+ function run(action, body, confirmText) {
1259
+ if (confirmText && !window.confirm(confirmText)) return;
1260
+ setBusy(true);
1261
+ postAction(action, body || {}).then(function (result) {
1262
+ var succeeded = result.ok && result.d && result.d.ok !== false;
1263
+ setNote(succeeded ? (result.d.text || '完成') : ('失败:' + ((result.d && (result.d.error || result.d.text)) || '未知错误')));
1264
+ if (succeeded) load(false);
1265
+ else setBusy(false);
1266
+ }).catch(function (error) { setBusy(false); setNote('失败:' + error.message); });
1267
+ }
1268
+
1269
+ React.useEffect(function () { load(false); }, [props.total]);
1270
+
1271
+ return React.createElement('div', {},
1272
+ React.createElement('div', { className: 'iris-gallery-tools' },
1273
+ React.createElement('span', { className: 'iris-clean-note' }, '共 ' + total + ' 个作品 · 清任务历史不会删除'),
1274
+ React.createElement('button', { className: 'iris-clean-btn', disabled: busy, onClick: function () { run('artifacts_reindex'); } }, '找回本地作品'),
1275
+ total ? React.createElement('button', { className: 'iris-clean-btn danger', disabled: busy, onClick: function () { run('artifacts_clear', {}, '永久删除作品库中的全部媒体文件?任务历史会保留,但相关媒体将无法打开。此操作不可逆。'); } }, '清空作品库') : null),
1276
+ items.length ? React.createElement('div', { className: 'iris-gallery-grid' }, items.map(function (item) {
1277
+ return React.createElement('div', { key: item.id, className: 'iris-gallery-card' },
1278
+ artifactPreview(item),
1279
+ React.createElement('div', { className: 'iris-gallery-info' },
1280
+ React.createElement('div', { className: 'iris-gallery-file', title: item.file }, item.file),
1281
+ React.createElement('div', { className: 'iris-gallery-meta' }, fmtTime(item.createdAt) + ' · ' + fmtBytes(item.size)),
1282
+ React.createElement('div', { className: 'iris-gallery-actions' },
1283
+ React.createElement('a', { className: 'iris-wb-link', href: item.url, target: '_blank', rel: 'noreferrer' }, '打开'),
1284
+ React.createElement('button', { className: 'iris-pm-btn danger', disabled: busy, onClick: function () { run('artifacts_delete', { artifact_id: item.id }, '永久删除作品“' + item.file + '”?任务历史会保留,但媒体将无法打开。'); } }, '删除'))));
1285
+ })) : React.createElement('div', { className: 'iris-wb-empty' }, busy ? '正在读取作品…' : '尚无作品;如 outputs/ 中已有文件,可点“找回本地作品”。'),
1286
+ items.length < total ? React.createElement('button', { className: 'iris-clean-btn', disabled: busy, onClick: function () { load(true); } }, busy ? '加载中…' : '加载更多') : null,
1287
+ note ? React.createElement('div', { className: 'iris-clean-note', style: { marginTop: '7px' } }, note) : null);
1288
+ }
1289
+
1057
1290
  /* ---- 泡泡常用卡片选择器(设置页:多项勾选 → 出现在悬浮窗「常用」标签) ---- */
1058
1291
  function BubbleCardPicker() {
1059
1292
  var pair = useBubbleCards();
@@ -1141,11 +1374,60 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1141
1374
  note ? React.createElement('div', { className: 'iris-clean-note' }, note) : null);
1142
1375
  }
1143
1376
 
1377
+ function HostDoctorPanel() {
1378
+ var pair = React.useState(null);
1379
+ var report = pair[0];
1380
+ var setReport = pair[1];
1381
+ var busyPair = React.useState(false);
1382
+ var busy = busyPair[0];
1383
+ var setBusy = busyPair[1];
1384
+ var errorPair = React.useState('');
1385
+ var error = errorPair[0];
1386
+ var setError = errorPair[1];
1387
+
1388
+ function load() {
1389
+ setBusy(true);
1390
+ setError('');
1391
+ fetch('/iris/api/doctor')
1392
+ .then(function (res) { return res.json().then(function (data) { if (!res.ok) throw new Error(data.error || '诊断失败'); return data; }); })
1393
+ .then(function (data) { setReport(data); })
1394
+ .catch(function (err) { setError(String(err && err.message || err)); })
1395
+ .finally(function () { setBusy(false); });
1396
+ }
1397
+ React.useEffect(function () { load(); }, []);
1398
+
1399
+ var checks = report && Array.isArray(report.checks) ? report.checks : [];
1400
+ var issues = checks.filter(function (item) { return item.status !== 'ok'; });
1401
+ var mark = { ok: '✓', warn: '!', error: '×' };
1402
+ function rows(list) {
1403
+ return React.createElement('div', { className: 'iris-doctor-list' }, list.map(function (item) {
1404
+ return React.createElement('div', { key: item.id, className: 'iris-doctor-row ' + item.status },
1405
+ React.createElement('span', {}, mark[item.status] || '·'),
1406
+ React.createElement('span', {}, item.summary));
1407
+ }));
1408
+ }
1409
+ var score = report
1410
+ ? report.summary.errors + ' 错误 · ' + report.summary.warnings + ' 警告'
1411
+ : (busy ? '正在检查宿主…' : '尚未运行');
1412
+ return React.createElement('div', { className: 'iris-wb-card' },
1413
+ React.createElement('div', { className: 'iris-doctor-top' },
1414
+ React.createElement('span', { className: 'iris-doctor-score' }, score),
1415
+ report && report.host ? React.createElement('span', { className: 'iris-wb-muted' }, 'DSH ' + report.host.version) : null,
1416
+ React.createElement('button', { className: 'iris-pm-btn', disabled: busy, onClick: load }, busy ? '检查中…' : '刷新')),
1417
+ error ? React.createElement('div', { className: 'iris-doctor-row error' }, React.createElement('span', {}, '×'), React.createElement('span', {}, error)) : null,
1418
+ issues.length ? rows(issues) : (report ? React.createElement('div', { className: 'iris-wb-muted' }, '当前可观察项全部健康') : null),
1419
+ checks.length ? React.createElement('details', { className: 'iris-doctor-details' },
1420
+ React.createElement('summary', {}, '查看全部 ' + checks.length + ' 项'),
1421
+ rows(checks)) : null,
1422
+ React.createElement('div', { className: 'iris-wb-muted' }, '只读本机注册证据;不调用 Browser、模型或供应商。'));
1423
+ }
1424
+
1144
1425
  function WorkbenchPanel() {
1145
1426
  var state = useIrisState(5000);
1146
1427
  var running = (state && state.tasks && state.tasks.running) || [];
1147
1428
  var recent = (state && state.tasks && state.tasks.recent) || [];
1148
1429
  var attention = (state && state.tasks && state.tasks.attention) || [];
1430
+ var artifactTotal = (state && state.artifacts && state.artifacts.total) || 0;
1149
1431
  var selPair = React.useState(null);
1150
1432
  var selectedTask = selPair[0];
1151
1433
  var setSelectedTask = selPair[1];
@@ -1156,12 +1438,17 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1156
1438
  React.createElement('span', { className: 'iris-wb-date' }, state ? '刷新于 ' + new Date().toLocaleTimeString() : '加载中…')),
1157
1439
  React.createElement('div', { className: 'iris-wb-sec' }, '供应商'),
1158
1440
  React.createElement(ProviderManager, {}),
1441
+ React.createElement('div', { className: 'iris-wb-sec' }, '能力健康'),
1442
+ React.createElement(CapabilityHealthOverview, {}),
1443
+ React.createElement('div', { className: 'iris-wb-muted' }, '绿色有效 7 天;真实成功或显式实测会刷新。不会后台探测,也不会因 429、网络或供应商 5xx 变红。'),
1159
1444
  React.createElement('div', { className: 'iris-wb-sec' }, '能力分配(failover 顺序)'),
1160
1445
  React.createElement(CapabilityAssigner, {}),
1161
1446
  React.createElement('div', { className: 'iris-wb-sec' }, 'Iris 泡泡快捷卡片(勾选后显示在「⚡ 常用」标签)'),
1162
1447
  React.createElement(BubbleCardPicker, {}),
1163
1448
  React.createElement('div', { className: 'iris-wb-sec' }, '对话框提示词优化'),
1164
1449
  React.createElement(PromptOptimizerSettings, {}),
1450
+ React.createElement('div', { className: 'iris-wb-sec' }, '宿主诊断'),
1451
+ React.createElement(HostDoctorPanel, {}),
1165
1452
  React.createElement('div', { className: 'iris-wb-sec' }, '运行中任务'),
1166
1453
  React.createElement('div', { className: 'iris-wb-box' },
1167
1454
  running.length ? running.map(function (t) { return taskRow(t, true, selectedTask === t.id, onSelect); })
@@ -1170,6 +1457,8 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1170
1457
  React.createElement('div', { className: 'iris-wb-sec' }, '需要处理'),
1171
1458
  React.createElement('div', { className: 'iris-wb-box' },
1172
1459
  attention.map(function (t) { return taskRow(t, false, selectedTask === t.id, onSelect); }))) : null,
1460
+ React.createElement('div', { className: 'iris-wb-sec' }, '作品库'),
1461
+ React.createElement(ArtifactGallery, { total: artifactTotal }),
1173
1462
  React.createElement('div', { className: 'iris-wb-sec' }, '历史任务'),
1174
1463
  React.createElement(CleanupBar, {}),
1175
1464
  React.createElement(HistoryBrowser, { recent: recent, selected: selectedTask, onSelect: onSelect }),
@@ -1184,6 +1473,8 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1184
1473
  var recent = (state && state.tasks && state.tasks.recent) || [];
1185
1474
  var attention = (state && state.tasks && state.tasks.attention) || [];
1186
1475
  var recentTotal = (state && state.tasks && state.tasks.recentTotal) || recent.length;
1476
+ var works = (state && state.artifacts && state.artifacts.recent) || [];
1477
+ var artifactTotal = (state && state.artifacts && state.artifacts.total) || works.length;
1187
1478
  var cards = useBubbleCards()[0];
1188
1479
  var tabPair = React.useState('tasks');
1189
1480
  var tab = tabPair[0];
@@ -1194,11 +1485,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1194
1485
  function onSelect(id) { setSelectedTask(id); }
1195
1486
  var body;
1196
1487
  if (tab === 'tasks') {
1197
- // 快捷面板承载当前任务、需要处理的异常事实与可直接查看的成功产物。
1198
- var succeeded = recent.filter(function (t) {
1199
- return t.status === 'succeeded' && Array.isArray(t.media) && t.media.length > 0;
1200
- });
1201
- var shown = succeeded.slice(0, 6);
1488
+ // 快捷面板承载当前任务、需要处理的异常事实与独立作品库中的最近作品。
1202
1489
  body = React.createElement('div', { className: 'iris-wb-box' },
1203
1490
  running.length ? React.createElement('div', {},
1204
1491
  React.createElement('div', { className: 'iris-wb-sec' }, '运行中'),
@@ -1206,12 +1493,12 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1206
1493
  attention.length ? React.createElement('div', {},
1207
1494
  React.createElement('div', { className: 'iris-wb-sec' }, '需要处理'),
1208
1495
  attention.slice(0, 4).map(function (t) { return taskRowMini(t, selectedTask === t.id, onSelect); })) : null,
1209
- React.createElement('div', { className: 'iris-wb-sec' }, '最近完成'),
1210
- shown.length ? shown.map(function (t) { return taskRowMini(t, selectedTask === t.id, onSelect); })
1211
- : React.createElement('div', { className: 'iris-wb-empty' }, '尚无成功产物'),
1212
- succeeded.length > 6 || recentTotal > succeeded.length
1496
+ React.createElement('div', { className: 'iris-wb-sec' }, '最近作品'),
1497
+ works.length ? works.map(artifactRowMini)
1498
+ : React.createElement('div', { className: 'iris-wb-empty' }, '尚无作品'),
1499
+ artifactTotal > works.length || recentTotal > 0
1213
1500
  ? React.createElement('div', { className: 'iris-bubble-more', style: { cursor: 'default' } },
1214
- '失败、取消与完整历史请到 Iris 工作台查看') : null);
1501
+ '完整作品库、失败与取消历史请到 Iris 工作台查看') : null);
1215
1502
  } else {
1216
1503
  var defs = CARD_DEFS.filter(function (d) { return cards.indexOf(d.action) >= 0; });
1217
1504
  body = defs.length
@@ -1470,7 +1757,9 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1470
1757
  function FloatingBubble() {
1471
1758
  var state = useIrisState(5000);
1472
1759
  var providers = (state && state.providers) || [];
1473
- var configured = providers.some(function (p) { return p.enabled && p.apiKeyHint; });
1760
+ var fallbackConfigured = providers.some(function (p) { return p.enabled && p.apiKeyHint; });
1761
+ var overallHealth = state && state.health && HEALTH_META[state.health.overall]
1762
+ ? state.health.overall : (fallbackConfigured ? 'configured' : 'unconfigured');
1474
1763
  var running = (state && state.tasks && state.tasks.running) || [];
1475
1764
  var attention = (state && state.tasks && state.tasks.attention) || [];
1476
1765
 
@@ -1538,17 +1827,30 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1538
1827
  }
1539
1828
  }
1540
1829
 
1830
+ function persistCurrentPosition() {
1831
+ setPos(function (current) {
1832
+ if (current && current.x != null && current.y != null) {
1833
+ try { localStorage.setItem('iris-bubble-pos', JSON.stringify(current)); } catch (_) {}
1834
+ }
1835
+ return current;
1836
+ });
1837
+ }
1838
+
1541
1839
  function onPointerUp(e) {
1542
1840
  var drag = dragRef.current;
1543
1841
  if (!drag) return;
1544
1842
  try { e.currentTarget.releasePointerCapture(e.pointerId); } catch (_) {}
1545
1843
  var moved = drag.moved;
1546
1844
  dragRef.current = null;
1547
- if (moved) {
1548
- try { localStorage.setItem('iris-bubble-pos', JSON.stringify(pos)); } catch (_) {}
1549
- } else {
1550
- setOpen(!open);
1551
- }
1845
+ if (moved) persistCurrentPosition();
1846
+ else setOpen(!open);
1847
+ }
1848
+
1849
+ function onPointerCancel(e) {
1850
+ if (!dragRef.current) return;
1851
+ try { e.currentTarget.releasePointerCapture(e.pointerId); } catch (_) {}
1852
+ dragRef.current = null;
1853
+ persistCurrentPosition();
1552
1854
  }
1553
1855
 
1554
1856
  var style;
@@ -1566,8 +1868,10 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1566
1868
  var panel = null;
1567
1869
  if (open) {
1568
1870
  var xy = bubbleXY();
1569
- var panelLeft = Math.max(8, xy.x - 330);
1570
- var panelTop = Math.max(8, Math.min(xy.y - 260, window.innerHeight - 520));
1871
+ var panelWidth = Math.min(360, Math.max(0, window.innerWidth - 16));
1872
+ var panelHeight = Math.min(520, Math.max(0, window.innerHeight - 16));
1873
+ var panelLeft = Math.max(8, Math.min(xy.x - panelWidth + 30, window.innerWidth - panelWidth - 8));
1874
+ var panelTop = Math.max(8, Math.min(xy.y - 260, window.innerHeight - panelHeight - 8));
1571
1875
  panel = React.createElement('div', { className: 'iris-bubble-panel', style: { left: panelLeft + 'px', top: panelTop + 'px' } },
1572
1876
  React.createElement('div', { className: 'iris-bubble-panel-head' },
1573
1877
  React.createElement('button', { className: 'iris-bubble-close', onClick: function () { setOpen(false); } }, '✕')),
@@ -1575,52 +1879,63 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1575
1879
  }
1576
1880
 
1577
1881
  return React.createElement('div', {
1578
- className: 'iris-bubble' + (configured ? ' lit' : ' dim') + (dragRef.current ? ' dragging' : ''),
1882
+ className: 'iris-bubble health-' + overallHealth + (dragRef.current ? ' dragging' : ''),
1579
1883
  style: style,
1580
1884
  onPointerDown: onPointerDown,
1581
1885
  onPointerMove: onPointerMove,
1582
1886
  onPointerUp: onPointerUp,
1583
- title: configured ? 'Iris 已就绪:点击打开快捷面板' : 'Iris 尚未配置:点击打开快捷面板'
1887
+ onPointerCancel: onPointerCancel,
1888
+ title: 'Iris ' + healthText({ status: overallHealth }) + ':点击打开快捷面板'
1584
1889
  },
1585
1890
  React.createElement('div', { className: 'iris-bubble-btn' }, '🫧'),
1586
1891
  badge,
1587
1892
  panel);
1588
1893
  }
1589
1894
 
1590
- ctx.slots.inject('settings.section', function () {
1591
- return ctx.slots.register({
1895
+ var clientSlots = clientSlotsPort(ctx);
1896
+
1897
+ clientSlots.inject('settings.section', function () {
1898
+ var dispose = clientSlots.register({
1592
1899
  name: 'settings.section',
1593
1900
  id: 'iris-workbench',
1594
1901
  order: 140,
1595
1902
  label: function () { return 'Iris 工作台'; }
1596
1903
  }, WorkbenchPanel);
1904
+ reportClientSeat('settings.section');
1905
+ return dispose;
1597
1906
  });
1598
1907
 
1599
- ctx.slots.inject('conversation.input.right', function () {
1600
- return ctx.slots.register({
1908
+ clientSlots.inject('conversation.input.right', function () {
1909
+ var dispose = clientSlots.register({
1601
1910
  name: 'conversation.input.right',
1602
1911
  id: 'iris-prompt-optimizer',
1603
1912
  order: 40,
1604
1913
  label: function () { return 'Iris 提示词优化'; }
1605
1914
  }, PromptOptimizerControl);
1915
+ reportClientSeat('conversation.input.right');
1916
+ return dispose;
1606
1917
  });
1607
1918
 
1608
- ctx.slots.inject('conversation.input.dock', function () {
1609
- return ctx.slots.register({
1919
+ clientSlots.inject('conversation.input.dock', function () {
1920
+ var dispose = clientSlots.register({
1610
1921
  name: 'conversation.input.dock',
1611
1922
  id: 'iris-progress',
1612
1923
  order: 0,
1613
1924
  label: function () { return 'Iris 任务进度'; }
1614
1925
  }, ProgressDock);
1926
+ reportClientSeat('conversation.input.dock');
1927
+ return dispose;
1615
1928
  });
1616
1929
 
1617
- ctx.slots.inject('shell.overlay', function () {
1618
- return ctx.slots.register({
1930
+ clientSlots.inject('shell.overlay', function () {
1931
+ var dispose = clientSlots.register({
1619
1932
  name: 'shell.overlay',
1620
1933
  id: 'iris-bubble',
1621
1934
  order: 200,
1622
1935
  label: function () { return 'Iris 泡泡'; }
1623
1936
  }, FloatingBubble);
1937
+ reportClientSeat('shell.overlay');
1938
+ return dispose;
1624
1939
  });
1625
1940
  console.log('[iris] client slots registered');
1626
1941
  } catch (err) {