@mokuyoaxis/dsh-iris 0.1.2 → 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 (54) 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 +57 -2
  8. package/README.en.md +43 -7
  9. package/README.md +43 -7
  10. package/bin/dsh-iris.js +23 -0
  11. package/docs/ARCHITECTURE.md +95 -0
  12. package/docs/ARTIFACT_LIBRARY.md +24 -0
  13. package/docs/FAULT_INJECTION.md +26 -0
  14. package/docs/HOST_ADAPTER_CONTRACT.md +86 -0
  15. package/docs/HOST_DOCTOR.md +47 -0
  16. package/docs/PROVIDER_ADAPTER_CONTRACT.md +97 -0
  17. package/docs/PROVIDER_HEALTH.md +39 -0
  18. package/docs/PROVIDER_SUBMISSION_CONTRACT.md +60 -0
  19. package/docs/ROADMAP.md +21 -11
  20. package/docs/SECURITY.md +57 -0
  21. package/docs/TASK_SEMANTICS.md +148 -0
  22. package/docs/assets/diagrams/iris-architecture.png +0 -0
  23. package/docs/assets/diagrams/iris-architecture.svg +5 -5
  24. package/docs/releases/0.1.4.md +59 -0
  25. package/lib/actions.js +860 -290
  26. package/lib/adapters.js +7 -7
  27. package/lib/api.js +110 -18
  28. package/lib/artifacts.js +239 -0
  29. package/lib/bundled-skills.js +5 -5
  30. package/lib/client.js +478 -68
  31. package/lib/config.js +192 -9
  32. package/lib/doctor.js +414 -0
  33. package/lib/dsh-host-adapter.js +189 -0
  34. package/lib/host-contract.js +131 -0
  35. package/lib/host-runtime.js +156 -0
  36. package/lib/index.js +142 -157
  37. package/lib/locate.js +1 -1
  38. package/lib/media.js +16 -6
  39. package/lib/prompt-optimizer.js +10 -20
  40. package/lib/provider-adapter.js +371 -0
  41. package/lib/provider-adapters.js +279 -0
  42. package/lib/provider-contract.js +234 -0
  43. package/lib/provider-health.js +59 -0
  44. package/lib/render.js +43 -12
  45. package/lib/task-semantics.js +209 -0
  46. package/lib/tasks.js +500 -32
  47. package/lib/vision.js +23 -49
  48. package/package.json +19 -8
  49. package/user_guide.md +62 -2
  50. package/docs/assets/diagrams/iris-architecture.drawio +0 -1
  51. package/docs/assets/diagrams/iris-task-lifecycle.drawio +0 -1
  52. package/docs/assets/diagrams/iris-workflow-compose.drawio +0 -1
  53. package/docs/releases/0.1.1.md +0 -51
  54. package/docs/releases/0.1.2.md +0 -26
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 = [
@@ -37,6 +73,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
37
73
  '.iris-wb-badge { font-size: 11px; padding: 1px 6px; border-radius: 4px; background: var(--dsw-alias-bg-layer-2, #32323c); }',
38
74
  '.iris-wb-badge.ok { color: var(--dsw-alias-state-success-primary, #6fcf6f); }',
39
75
  '.iris-wb-badge.err { color: var(--dsw-alias-state-error-primary, #e06c6c); }',
76
+ '.iris-wb-badge.warn { color: var(--dsw-alias-state-warn-primary, #d9a941); }',
40
77
  '.iris-wb-progress { margin-top: 6px; }',
41
78
  '.iris-wb-bar { height: 5px; border-radius: 3px; background: var(--dsw-alias-bg-layer-2, #32323c); overflow: hidden; }',
42
79
  '.iris-wb-bar i { display: block; height: 100%; background: var(--dsw-alias-brand-primary, #7aa2f7); }',
@@ -56,6 +93,15 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
56
93
  '.iris-wb-drawer .iris-wb-prompt { white-space: pre-wrap; word-break: break-word; font-size: 12px; }',
57
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; }',
58
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; }',
59
105
  /* ---- 操作卡片组(阶段 5 GUI 直连) ---- */
60
106
  '.iris-act-group { display: grid; gap: 8px; }',
61
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; }',
@@ -95,8 +141,17 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
95
141
  '.iris-pm-field { display: grid; gap: 3px; }',
96
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; }',
97
143
  '.iris-pm-act { font-size: 11px; color: var(--dsw-alias-label-secondary, #9a9a9a); }',
98
- '.iris-pm-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--dsw-alias-label-secondary, #9a9a9a); margin-right: 4px; }',
99
- '.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); }',
100
155
  '.iris-cap-grid { display: grid; gap: 6px; }',
101
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; }',
102
157
  '.iris-cap-name { font-weight: 600; min-width: 64px; }',
@@ -108,10 +163,12 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
108
163
  '.iris-bubble { position: fixed; z-index: 9999; cursor: grab; user-select: none; touch-action: none; }',
109
164
  '.iris-bubble.dragging { cursor: grabbing; }',
110
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; }',
111
- '.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); }',
112
- '.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); }',
113
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; }',
114
- '.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); }',
115
172
  '.iris-bubble-panel-head { position: absolute; top: 6px; right: 6px; z-index: 2; }',
116
173
  '.iris-bubble-close { border: none; background: transparent; color: var(--dsw-alias-label-secondary, #9a9a9a); font-size: 14px; cursor: pointer; padding: 4px 6px; }',
117
174
  '.iris-bubble-close:hover { color: var(--dsw-alias-label-primary, #e6e6e6); }',
@@ -135,6 +192,22 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
135
192
  '.iris-clean-btn:hover { border-color: var(--dsw-alias-brand-primary, #7aa2f7); }',
136
193
  '.iris-clean-btn.danger:hover { color: var(--dsw-alias-state-error-primary, #e06c6c); border-color: var(--dsw-alias-state-error-primary, #e06c6c); }',
137
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; } }',
138
211
  /* 模型池(阶段 9 P4) */
139
212
  '.iris-pm-pool { display: grid; gap: 4px; }',
140
213
  '.iris-pm-mrows { display: grid; gap: 3px; max-height: 220px; overflow: auto; }',
@@ -143,8 +216,10 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
143
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; }',
144
217
  '.iris-pm-mcaps { display: flex; gap: 3px; }',
145
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); }',
146
- '.iris-pm-ver.ok { color: var(--dsw-alias-state-success-primary, #6fcf6f); }',
147
- '.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; }',
148
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; }',
149
224
  /* 文件选择器(阶段 10) */
150
225
  '.iris-ff { display: grid; gap: 4px; }',
@@ -252,6 +327,8 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
252
327
  if (sharedState) setState(sharedState);
253
328
  if (!sharedTimer && !sharedSource) {
254
329
  function apply(data) {
330
+ if (sharedState && data && sharedState.stateEpoch && data.stateEpoch === sharedState.stateEpoch
331
+ && Number(data.stateRevision || 0) <= Number(sharedState.stateRevision || 0)) return;
255
332
  sharedState = data;
256
333
  for (var i = 0; i < sharedListeners.length; i++) {
257
334
  sharedListeners[i](data);
@@ -291,13 +368,60 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
291
368
  return sharedState;
292
369
  }
293
370
 
294
- /** 功能卡就绪态直接来自共享 Iris 状态,不再等用户点开卡片后才加载。 */
295
- function capabilityReady(state, capability) {
296
- 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;
297
382
  var providers = (state && state.providers) || [];
298
- return providers.some(function (p) {
299
- 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;
300
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
+ }));
301
425
  }
302
426
 
303
427
  /* ---- 任务详情抽屉:按需拉取 /iris/api/task/:id ---- */
@@ -347,10 +471,38 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
347
471
  return '更早';
348
472
  }
349
473
 
474
+ var TASK_STATE_LABELS = {
475
+ queued: '排队中', running: '运行中', watching_paused: '观察暂停', needs_attention: '需要确认',
476
+ artifact_unavailable: '结果待取回', succeeded: '成功', failed: '失败', canceled: '已取消'
477
+ };
478
+ function taskState(task) {
479
+ if (task && task.userState) return task.userState;
480
+ if (task && task.status === 'succeeded') return 'succeeded';
481
+ if (task && task.status === 'failed') return 'failed';
482
+ if (task && task.status === 'canceled') return 'canceled';
483
+ return 'running';
484
+ }
485
+ function taskStateLabel(task) {
486
+ var state = taskState(task);
487
+ return TASK_STATE_LABELS[state] || state;
488
+ }
489
+ function taskStateClass(task) {
490
+ var state = taskState(task);
491
+ if (state === 'succeeded') return 'ok';
492
+ if (state === 'failed') return 'err';
493
+ if (state === 'watching_paused' || state === 'needs_attention' || state === 'artifact_unavailable') return 'warn';
494
+ return '';
495
+ }
496
+ function attentionDispositionLabel(task) {
497
+ var disposition = task && task.attentionDisposition;
498
+ if (!disposition || disposition.status !== 'acknowledged') return '';
499
+ return disposition.reason === 'retried' ? '已通过重试处理' : '提醒已读';
500
+ }
501
+
350
502
  /* 紧凑任务行:泡泡「任务」标签用,单行摘要 */
351
503
  function taskRowMini(task, selected, onSelect) {
352
- var st = task.status === 'succeeded' ? 'ok' : task.status === 'failed' ? 'err' : '';
353
- var label = task.status === 'succeeded' ? '成功' : task.status === 'failed' ? '失败' : task.status === 'canceled' ? '取消' : (task.progress || '运行中');
504
+ var st = taskStateClass(task);
505
+ var label = taskStateLabel(task);
354
506
  return React.createElement('div', {
355
507
  key: task.id, className: 'iris-task-mini' + (selected ? ' selected' : ''),
356
508
  onClick: function () { onSelect(selected ? null : task.id); }, title: '点击展开详情'
@@ -361,6 +513,47 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
361
513
  React.createElement('span', { className: 'iris-wb-muted' }, task.createdAt ? new Date(task.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : ''));
362
514
  }
363
515
 
516
+ function TaskRecoveryActions({ detail }) {
517
+ var pair = React.useState('');
518
+ var note = pair[0];
519
+ var setNote = pair[1];
520
+ var busyPair = React.useState(false);
521
+ var busy = busyPair[0];
522
+ var setBusy = busyPair[1];
523
+ var state = taskState(detail);
524
+ var attentionState = ['watching_paused', 'needs_attention', 'artifact_unavailable'].indexOf(state) >= 0;
525
+ var acknowledged = detail.attentionDisposition && detail.attentionDisposition.status === 'acknowledged';
526
+ var canAcknowledge = detail.schemaVersion === 2 && attentionState && !acknowledged;
527
+ var canRestore = detail.schemaVersion === 2 && attentionState && acknowledged;
528
+ var canReobserve = detail.schemaVersion === 2 && !acknowledged && detail.acceptance === 'accepted' && detail.remoteTaskId
529
+ && detail.outcome !== 'succeeded' && detail.outcome !== 'failed' && detail.outcome !== 'canceled';
530
+ var canRedeliver = detail.schemaVersion === 2 && !acknowledged && state === 'artifact_unavailable' && detail.remoteTaskId;
531
+ var canRetry = detail.schemaVersion === 2 && !acknowledged && ['needs_attention', 'failed', 'canceled'].indexOf(state) >= 0
532
+ && (detail.cap === 'image' || (detail.cap === 'video' && detail.mode === 't2v'));
533
+ if (!canAcknowledge && !canRestore && !canReobserve && !canRedeliver && !canRetry) return null;
534
+ function run(action, body, confirmMessage) {
535
+ if (confirmMessage && !window.confirm(confirmMessage)) return;
536
+ setBusy(true);
537
+ setNote('处理中…');
538
+ postAction(action, body).then(function (r) {
539
+ setNote(r.ok ? ((r.d && r.d.text) || '操作已接受') : ('失败:' + ((r.d && r.d.error) || '未知错误')));
540
+ }).catch(function (error) { setNote('失败:' + String((error && error.message) || error)); })
541
+ .finally(function () { setBusy(false); });
542
+ }
543
+ return React.createElement('div', { className: 'iris-wb-row', onClick: function (e) { e.stopPropagation(); } },
544
+ canReobserve ? React.createElement('button', { className: 'iris-pm-btn', disabled: busy, onClick: function () { run('task_reobserve', { task_id: detail.id }); } }, '重新观察') : null,
545
+ canRedeliver ? React.createElement('button', { className: 'iris-pm-btn', disabled: busy, onClick: function () { run('task_redeliver', { task_id: detail.id }); } }, '重新交付') : null,
546
+ canRetry ? React.createElement('button', { className: 'iris-pm-btn danger', disabled: busy, onClick: function () {
547
+ run('task_manual_retry', { task_id: detail.id, confirm_duplicate_charge: true },
548
+ '远端任务可能已经产生,本次会创建新的生成任务,可能重复计费。确认知情重试?');
549
+ } }, '知情重试') : null,
550
+ canAcknowledge ? React.createElement('button', { className: 'iris-pm-btn', disabled: busy,
551
+ onClick: function () { run('task_ack_attention', { task_id: detail.id }); } }, '标为已读') : null,
552
+ canRestore ? React.createElement('button', { className: 'iris-pm-btn', disabled: busy,
553
+ onClick: function () { run('task_restore_attention', { task_id: detail.id }); } }, '恢复提醒') : null,
554
+ note ? React.createElement('span', { className: 'iris-clean-note' }, note) : null);
555
+ }
556
+
364
557
  /* ---- 任务详情抽屉:点击任务卡片行展开 ---- */
365
558
  function TaskDetailDrawer({ taskId }) {
366
559
  var detail = useTaskDetail(taskId);
@@ -370,6 +563,15 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
370
563
  var el = [];
371
564
  el.push(React.createElement('div', { key: 'k', className: 'iris-wb-k' }, '提示词'));
372
565
  el.push(React.createElement('div', { key: 'p', className: 'iris-wb-prompt' }, detail.prompt || '(空)'));
566
+ if (Number(detail.schemaVersion) === 1) {
567
+ el.push(React.createElement('div', { key: 'legacy', className: 'iris-clean-note' },
568
+ '旧版任务记录 · 仅支持查看。由于缺少可靠的远端受理与交付事实,不提供重新观察、重新交付或知情重试;0.1.3 新任务将使用可信任务语义。'));
569
+ }
570
+ var dispositionLabel = attentionDispositionLabel(detail);
571
+ if (dispositionLabel) {
572
+ var related = detail.attentionDisposition.relatedTaskId ? ' · 关联任务 ' + detail.attentionDisposition.relatedTaskId : '';
573
+ el.push(React.createElement('div', { key: 'attention-disposition', className: 'iris-clean-note' }, dispositionLabel + related));
574
+ }
373
575
  if (detail.error) {
374
576
  el.push(React.createElement('div', { key: 'ek', className: 'iris-wb-k' }, '错误'));
375
577
  el.push(React.createElement('div', { key: 'e', className: 'iris-wb-err' }, detail.error));
@@ -377,11 +579,13 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
377
579
  var kv = [
378
580
  ['ID', detail.id],
379
581
  ['能力', detail.cap],
380
- ['状态', detail.status],
582
+ ['状态', taskStateLabel(detail)],
381
583
  ['模型', detail.model],
382
584
  ['供应商', detail.providerName],
383
585
  ['模式', detail.mode || '—'],
384
586
  ['远端任务', detail.remoteTaskId || '—'],
587
+ ['来源任务', detail.retryOf || '—'],
588
+ ['后续重试', (detail.manualRetries || []).map(function (item) { return item.taskId; }).join('、') || '—'],
385
589
  ['发起', detail.createdAt ? fmtTime(detail.createdAt) : '—'],
386
590
  ['完成', detail.finishedAt ? fmtTime(detail.finishedAt) : '—'],
387
591
  ['耗时', detail.elapsedMs ? fmtElapsed(detail.elapsedMs) : '—']
@@ -418,24 +622,25 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
418
622
  return React.createElement('span', { key: a.attachmentId, className: 'iris-wb-kv' }, a.attachmentId + (a.file ? ' (' + a.file + ')' : ''));
419
623
  })));
420
624
  }
625
+ el.push(React.createElement(TaskRecoveryActions, { key: 'recovery', detail: detail }));
421
626
  return React.createElement('div', { className: 'iris-wb-drawer' }, ...el);
422
627
  }
423
628
 
424
629
  function taskRow(task, isRunning, selected, onSelect) {
425
- var badge;
426
- if (task.status === 'succeeded') {
427
- badge = React.createElement('span', { className: 'iris-wb-badge ok' }, '成功');
428
- } else if (task.status === 'failed') {
429
- badge = React.createElement('span', { className: 'iris-wb-badge err' }, '失败');
430
- } else {
431
- badge = React.createElement('span', { className: 'iris-wb-badge' }, task.status);
432
- }
630
+ var stateClass = taskStateClass(task);
631
+ var badge = React.createElement('span', { className: 'iris-wb-badge' + (stateClass ? ' ' + stateClass : '') }, taskStateLabel(task));
433
632
  var cells = [
434
633
  React.createElement('span', { className: 'iris-wb-kv' }, task.cap),
435
634
  badge,
436
635
  React.createElement('span', { className: 'iris-wb-kv' }, task.model || ''),
437
636
  React.createElement('span', { className: 'iris-wb-muted' }, String(task.prompt || '').slice(0, 60))
438
637
  ];
638
+ if (Number(task.schemaVersion) === 1) {
639
+ cells.push(React.createElement('span', { className: 'iris-wb-badge', title: '旧版记录缺少可靠的远端受理与交付事实,仅支持查看' }, '旧任务 · 只读'));
640
+ }
641
+ var dispositionLabel = attentionDispositionLabel(task);
642
+ if (dispositionLabel) cells.push(React.createElement('span', { className: 'iris-wb-badge ok' }, dispositionLabel));
643
+ if (task.retryOf) cells.push(React.createElement('span', { className: 'iris-wb-badge' }, '重试任务'));
439
644
  if (task.error) cells.push(React.createElement('span', { className: 'iris-wb-badge err' }, String(task.error).slice(0, 40)));
440
645
  var links = (task.media || []).map(function (m) {
441
646
  return React.createElement('a', {
@@ -487,10 +692,17 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
487
692
  var setNewId = addPair[1];
488
693
  var models = p.models || [];
489
694
  function verBadge(m, c) {
490
- var v = m.verified && m.verified[c];
491
- var cls = 'iris-pm-ver' + (v ? (' ' + (v.ok ? 'ok' : 'bad')) : '');
492
- var title = v ? ((v.ok ? '实测通过:' : '实测未通过:') + (v.note || '')) : '点击单独验证此能力';
493
- 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);
494
706
  }
495
707
  var rows = models.map(function (m) {
496
708
  var caps = m.capabilities || [];
@@ -727,6 +939,7 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
727
939
  .then(function (res) { setRun(res.ok ? { ok: true, text: res.d.text || '', img: res.d.imageDataUrl || null } : { ok: false, text: res.d.error || '执行失败' }); })
728
940
  .catch(function () { setRun({ ok: false, text: '网络错误' }); });
729
941
  }
942
+ var cardHealth = capability ? capabilityHealth(irisState, capability) : { status: 'verified' };
730
943
  var body = null;
731
944
  if (open) {
732
945
  var inputs = fields.map(function (f) {
@@ -762,16 +975,17 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
762
975
  result = React.createElement('div', { className: 'iris-act-result' + (run.ok ? '' : ' err') }, run.text);
763
976
  }
764
977
  }
765
- // 只读模型提示:当前能力解析到的模型;改分配去「能力分配」区(唯一写入口)
978
+ // 只读模型提示:当前能力解析到的模型与持久健康事实。
766
979
  var modelBar = null;
767
980
  if (capability) {
768
- var lit = modelInfo && modelInfo.loaded && modelInfo.options.length > 0;
981
+ var available = modelInfo && modelInfo.loaded && modelInfo.options.length > 0;
769
982
  var shown = (vals.model && vals.model.trim()) || (modelInfo && modelInfo.assigned) || '';
770
983
  modelBar = React.createElement('div', { className: 'iris-act-field' },
771
984
  React.createElement('label', {},
772
- React.createElement('span', { className: lit ? 'iris-pm-dot lit' : 'iris-pm-dot' }, '●'),
773
- ' 模型:' + (shown || '自动') + (lit ? '' : '(无可用模型)')),
774
- 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 顺序)」'));
775
989
  }
776
990
  body = React.createElement('div', { className: 'iris-act-body' },
777
991
  modelBar,
@@ -780,13 +994,17 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
780
994
  run === 'running' ? '执行中…' : '执行'),
781
995
  result);
782
996
  }
783
- var headError = !!(run && run !== 'running' && run.ok === false);
784
- var headLit = !capability || (capabilityReady(irisState, capability) && !headError);
785
- var headTitle = headError ? '最近一次执行出错' : (headLit ? 'API 与能力已配置' : '缺少可用 API 或能力模型');
786
- 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
+ },
787
1002
  React.createElement('div', { className: 'iris-act-head', onClick: function () { setOpen(!open); } },
788
1003
  React.createElement('span', { className: 'iris-act-title' }, title),
789
- 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,
790
1008
  React.createElement('span', { className: 'iris-act-arrow' }, '▶')),
791
1009
  body);
792
1010
  }
@@ -970,6 +1188,105 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
970
1188
  }));
971
1189
  }
972
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
+
973
1290
  /* ---- 泡泡常用卡片选择器(设置页:多项勾选 → 出现在悬浮窗「常用」标签) ---- */
974
1291
  function BubbleCardPicker() {
975
1292
  var pair = useBubbleCards();
@@ -1057,10 +1374,60 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1057
1374
  note ? React.createElement('div', { className: 'iris-clean-note' }, note) : null);
1058
1375
  }
1059
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
+
1060
1425
  function WorkbenchPanel() {
1061
1426
  var state = useIrisState(5000);
1062
1427
  var running = (state && state.tasks && state.tasks.running) || [];
1063
1428
  var recent = (state && state.tasks && state.tasks.recent) || [];
1429
+ var attention = (state && state.tasks && state.tasks.attention) || [];
1430
+ var artifactTotal = (state && state.artifacts && state.artifacts.total) || 0;
1064
1431
  var selPair = React.useState(null);
1065
1432
  var selectedTask = selPair[0];
1066
1433
  var setSelectedTask = selPair[1];
@@ -1071,16 +1438,27 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1071
1438
  React.createElement('span', { className: 'iris-wb-date' }, state ? '刷新于 ' + new Date().toLocaleTimeString() : '加载中…')),
1072
1439
  React.createElement('div', { className: 'iris-wb-sec' }, '供应商'),
1073
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 变红。'),
1074
1444
  React.createElement('div', { className: 'iris-wb-sec' }, '能力分配(failover 顺序)'),
1075
1445
  React.createElement(CapabilityAssigner, {}),
1076
1446
  React.createElement('div', { className: 'iris-wb-sec' }, 'Iris 泡泡快捷卡片(勾选后显示在「⚡ 常用」标签)'),
1077
1447
  React.createElement(BubbleCardPicker, {}),
1078
1448
  React.createElement('div', { className: 'iris-wb-sec' }, '对话框提示词优化'),
1079
1449
  React.createElement(PromptOptimizerSettings, {}),
1450
+ React.createElement('div', { className: 'iris-wb-sec' }, '宿主诊断'),
1451
+ React.createElement(HostDoctorPanel, {}),
1080
1452
  React.createElement('div', { className: 'iris-wb-sec' }, '运行中任务'),
1081
1453
  React.createElement('div', { className: 'iris-wb-box' },
1082
1454
  running.length ? running.map(function (t) { return taskRow(t, true, selectedTask === t.id, onSelect); })
1083
1455
  : React.createElement('div', { className: 'iris-wb-empty' }, '暂无进行中任务')),
1456
+ attention.length ? React.createElement('div', {},
1457
+ React.createElement('div', { className: 'iris-wb-sec' }, '需要处理'),
1458
+ React.createElement('div', { className: 'iris-wb-box' },
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 }),
1084
1462
  React.createElement('div', { className: 'iris-wb-sec' }, '历史任务'),
1085
1463
  React.createElement(CleanupBar, {}),
1086
1464
  React.createElement(HistoryBrowser, { recent: recent, selected: selectedTask, onSelect: onSelect }),
@@ -1093,7 +1471,10 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1093
1471
  var state = useIrisState(5000);
1094
1472
  var running = (state && state.tasks && state.tasks.running) || [];
1095
1473
  var recent = (state && state.tasks && state.tasks.recent) || [];
1474
+ var attention = (state && state.tasks && state.tasks.attention) || [];
1096
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;
1097
1478
  var cards = useBubbleCards()[0];
1098
1479
  var tabPair = React.useState('tasks');
1099
1480
  var tab = tabPair[0];
@@ -1104,21 +1485,20 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1104
1485
  function onSelect(id) { setSelectedTask(id); }
1105
1486
  var body;
1106
1487
  if (tab === 'tasks') {
1107
- // 快捷面板只承载当前任务与可直接查看的成功产物;失败/取消历史留在完整工作台。
1108
- var succeeded = recent.filter(function (t) {
1109
- return t.status === 'succeeded' && Array.isArray(t.media) && t.media.length > 0;
1110
- });
1111
- var shown = succeeded.slice(0, 6);
1488
+ // 快捷面板承载当前任务、需要处理的异常事实与独立作品库中的最近作品。
1112
1489
  body = React.createElement('div', { className: 'iris-wb-box' },
1113
1490
  running.length ? React.createElement('div', {},
1114
1491
  React.createElement('div', { className: 'iris-wb-sec' }, '运行中'),
1115
1492
  running.map(function (t) { return taskRow(t, true, selectedTask === t.id, onSelect); })) : null,
1116
- React.createElement('div', { className: 'iris-wb-sec' }, '最近完成'),
1117
- shown.length ? shown.map(function (t) { return taskRowMini(t, selectedTask === t.id, onSelect); })
1118
- : React.createElement('div', { className: 'iris-wb-empty' }, '尚无成功产物'),
1119
- succeeded.length > 6 || recentTotal > succeeded.length
1493
+ attention.length ? React.createElement('div', {},
1494
+ React.createElement('div', { className: 'iris-wb-sec' }, '需要处理'),
1495
+ attention.slice(0, 4).map(function (t) { return taskRowMini(t, selectedTask === t.id, onSelect); })) : null,
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
1120
1500
  ? React.createElement('div', { className: 'iris-bubble-more', style: { cursor: 'default' } },
1121
- '失败、取消与完整历史请到 Iris 工作台查看') : null);
1501
+ '完整作品库、失败与取消历史请到 Iris 工作台查看') : null);
1122
1502
  } else {
1123
1503
  var defs = CARD_DEFS.filter(function (d) { return cards.indexOf(d.action) >= 0; });
1124
1504
  body = defs.length
@@ -1377,8 +1757,11 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1377
1757
  function FloatingBubble() {
1378
1758
  var state = useIrisState(5000);
1379
1759
  var providers = (state && state.providers) || [];
1380
- 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');
1381
1763
  var running = (state && state.tasks && state.tasks.running) || [];
1764
+ var attention = (state && state.tasks && state.tasks.attention) || [];
1382
1765
 
1383
1766
  var posPair = React.useState(function () {
1384
1767
  var saved = null;
@@ -1444,17 +1827,30 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1444
1827
  }
1445
1828
  }
1446
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
+
1447
1839
  function onPointerUp(e) {
1448
1840
  var drag = dragRef.current;
1449
1841
  if (!drag) return;
1450
1842
  try { e.currentTarget.releasePointerCapture(e.pointerId); } catch (_) {}
1451
1843
  var moved = drag.moved;
1452
1844
  dragRef.current = null;
1453
- if (moved) {
1454
- try { localStorage.setItem('iris-bubble-pos', JSON.stringify(pos)); } catch (_) {}
1455
- } else {
1456
- setOpen(!open);
1457
- }
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();
1458
1854
  }
1459
1855
 
1460
1856
  var style;
@@ -1464,15 +1860,18 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1464
1860
  style = { right: DEFAULT_X + 'px', bottom: DEFAULT_Y + 'px' };
1465
1861
  }
1466
1862
 
1467
- var badge = running.length
1468
- ? React.createElement('span', { className: 'iris-bubble-badge' }, running.length)
1863
+ var badgeCount = running.length + attention.length;
1864
+ var badge = badgeCount
1865
+ ? React.createElement('span', { className: 'iris-bubble-badge', title: attention.length ? attention.length + ' 个任务需要处理' : '' }, badgeCount)
1469
1866
  : null;
1470
1867
 
1471
1868
  var panel = null;
1472
1869
  if (open) {
1473
1870
  var xy = bubbleXY();
1474
- var panelLeft = Math.max(8, xy.x - 330);
1475
- 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));
1476
1875
  panel = React.createElement('div', { className: 'iris-bubble-panel', style: { left: panelLeft + 'px', top: panelTop + 'px' } },
1477
1876
  React.createElement('div', { className: 'iris-bubble-panel-head' },
1478
1877
  React.createElement('button', { className: 'iris-bubble-close', onClick: function () { setOpen(false); } }, '✕')),
@@ -1480,52 +1879,63 @@ window.__ModuleLoader__.load({ id: '@mokuyoaxis/dsh-iris', factory: (require) =>
1480
1879
  }
1481
1880
 
1482
1881
  return React.createElement('div', {
1483
- className: 'iris-bubble' + (configured ? ' lit' : ' dim') + (dragRef.current ? ' dragging' : ''),
1882
+ className: 'iris-bubble health-' + overallHealth + (dragRef.current ? ' dragging' : ''),
1484
1883
  style: style,
1485
1884
  onPointerDown: onPointerDown,
1486
1885
  onPointerMove: onPointerMove,
1487
1886
  onPointerUp: onPointerUp,
1488
- title: configured ? 'Iris 已就绪:点击打开快捷面板' : 'Iris 尚未配置:点击打开快捷面板'
1887
+ onPointerCancel: onPointerCancel,
1888
+ title: 'Iris ' + healthText({ status: overallHealth }) + ':点击打开快捷面板'
1489
1889
  },
1490
1890
  React.createElement('div', { className: 'iris-bubble-btn' }, '🫧'),
1491
1891
  badge,
1492
1892
  panel);
1493
1893
  }
1494
1894
 
1495
- ctx.slots.inject('settings.section', function () {
1496
- return ctx.slots.register({
1895
+ var clientSlots = clientSlotsPort(ctx);
1896
+
1897
+ clientSlots.inject('settings.section', function () {
1898
+ var dispose = clientSlots.register({
1497
1899
  name: 'settings.section',
1498
1900
  id: 'iris-workbench',
1499
1901
  order: 140,
1500
1902
  label: function () { return 'Iris 工作台'; }
1501
1903
  }, WorkbenchPanel);
1904
+ reportClientSeat('settings.section');
1905
+ return dispose;
1502
1906
  });
1503
1907
 
1504
- ctx.slots.inject('conversation.input.right', function () {
1505
- return ctx.slots.register({
1908
+ clientSlots.inject('conversation.input.right', function () {
1909
+ var dispose = clientSlots.register({
1506
1910
  name: 'conversation.input.right',
1507
1911
  id: 'iris-prompt-optimizer',
1508
1912
  order: 40,
1509
1913
  label: function () { return 'Iris 提示词优化'; }
1510
1914
  }, PromptOptimizerControl);
1915
+ reportClientSeat('conversation.input.right');
1916
+ return dispose;
1511
1917
  });
1512
1918
 
1513
- ctx.slots.inject('conversation.input.dock', function () {
1514
- return ctx.slots.register({
1919
+ clientSlots.inject('conversation.input.dock', function () {
1920
+ var dispose = clientSlots.register({
1515
1921
  name: 'conversation.input.dock',
1516
1922
  id: 'iris-progress',
1517
1923
  order: 0,
1518
1924
  label: function () { return 'Iris 任务进度'; }
1519
1925
  }, ProgressDock);
1926
+ reportClientSeat('conversation.input.dock');
1927
+ return dispose;
1520
1928
  });
1521
1929
 
1522
- ctx.slots.inject('shell.overlay', function () {
1523
- return ctx.slots.register({
1930
+ clientSlots.inject('shell.overlay', function () {
1931
+ var dispose = clientSlots.register({
1524
1932
  name: 'shell.overlay',
1525
1933
  id: 'iris-bubble',
1526
1934
  order: 200,
1527
1935
  label: function () { return 'Iris 泡泡'; }
1528
1936
  }, FloatingBubble);
1937
+ reportClientSeat('shell.overlay');
1938
+ return dispose;
1529
1939
  });
1530
1940
  console.log('[iris] client slots registered');
1531
1941
  } catch (err) {