@mokuyoaxis/dsh-iris 0.1.0

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.
package/lib/client.js ADDED
@@ -0,0 +1,1187 @@
1
+ /**
2
+ * dsh-iris —— Client 半(Iris 工作台 + 悬浮泡泡)。
3
+ *
4
+ * DSH web client bundle 标准形态(与 dsh-at-file / dsh-better-sidebar 同构):
5
+ * window.__ModuleLoader__.load({ id, factory }) → CJS 工厂,require 解析宿主依赖,
6
+ * `inject` 声明本 bundle 需要的 ctx 服务 key(这里是 slots),
7
+ * `apply(ctx)` 里直接 `ctx.slots.inject(...)` 注册座位。
8
+ *
9
+ * 三个零替换风险的座位:
10
+ * - settings.section(id: 'iris-workbench'):Iris 工作台整页 ——
11
+ * 供应商状态(Key 只见 hint)+ 历史任务面板 + 运行中任务进度 + 播放链接;
12
+ * - conversation.input.dock(id: 'iris-progress'):composer 上方常驻进度条,
13
+ * 有运行中任务时显式一行进度,无则渲染 null(零占用)。
14
+ *
15
+ * 数据通道:host 侧 /iris/api/state 同源 JSON 路由(复用 /iris/media 同款
16
+ * webServer 前缀模式),client 侧 fetch 轮询。全走标量,
17
+ * apiKey 永不明文,产物给授权播放链接。
18
+ */
19
+ window.__ModuleLoader__.load({ id: 'dsh-iris', factory: (require) => {
20
+ var module = { exports: {} };
21
+ var exports = module.exports;
22
+
23
+ var React = require('react');
24
+
25
+ var STYLE_ID = 'iris-wb-css';
26
+ var cssText = [
27
+ '.iris-wb { font-size: 13px; line-height: 1.5; color: var(--dsw-alias-label-primary, #e6e6e6); }',
28
+ '.iris-wb-head { display: flex; align-items: baseline; gap: 8px; margin: 0 0 10px; }',
29
+ '.iris-wb-head .iris-wb-title { font-size: 14px; font-weight: 600; }',
30
+ '.iris-wb-head .iris-wb-date { color: var(--dsw-alias-label-secondary, #9a9a9a); font-size: 12px; }',
31
+ '.iris-wb-sec { margin: 14px 0 6px; font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-secondary, #9a9a9a); }',
32
+ '.iris-wb-box { display: grid; gap: 8px; }',
33
+ '.iris-wb-card { background: var(--dsw-alias-bg-layer-1, #26262e); border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 8px; padding: 8px 10px; }',
34
+ '.iris-wb-card.iris-running { border-left: 3px solid var(--dsw-alias-state-warn-primary, #d9a941); }',
35
+ '.iris-wb-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }',
36
+ '.iris-wb-kv { color: var(--dsw-alias-label-secondary, #9a9a9a); }',
37
+ '.iris-wb-badge { font-size: 11px; padding: 1px 6px; border-radius: 4px; background: var(--dsw-alias-bg-layer-2, #32323c); }',
38
+ '.iris-wb-badge.ok { color: var(--dsw-alias-state-success-primary, #6fcf6f); }',
39
+ '.iris-wb-badge.err { color: var(--dsw-alias-state-error-primary, #e06c6c); }',
40
+ '.iris-wb-progress { margin-top: 6px; }',
41
+ '.iris-wb-bar { height: 5px; border-radius: 3px; background: var(--dsw-alias-bg-layer-2, #32323c); overflow: hidden; }',
42
+ '.iris-wb-bar i { display: block; height: 100%; background: var(--dsw-alias-brand-primary, #7aa2f7); }',
43
+ '.iris-wb-link { color: var(--dsw-alias-brand-primary, #7aa2f7); text-decoration: none; }',
44
+ '.iris-wb-link:hover { text-decoration: underline; }',
45
+ '.iris-wb-muted { color: var(--dsw-alias-label-secondary, #9a9a9a); font-size: 12px; }',
46
+ '.iris-wb-empty { color: var(--dsw-alias-label-secondary, #9a9a9a); font-size: 12px; padding: 6px 2px; }',
47
+ '.iris-progress-dock { display: grid; gap: 6px; }',
48
+ '.iris-progress-row { font-size: 12px; display: flex; gap: 8px; align-items: center; color: var(--dsw-alias-label-secondary, #9a9a9a); }',
49
+ '.iris-wb-card { cursor: pointer; }',
50
+ '.iris-wb-card.selected { border-color: var(--dsw-alias-brand-primary, #7aa2f7); }',
51
+ '.iris-wb-drawer { margin-top: 8px; padding: 10px; border-top: 1px dashed var(--dsw-alias-border-l1, #3a3a44); display: grid; gap: 6px; font-size: 12px; line-height: 1.5; }',
52
+ '.iris-wb-drawer .iris-wb-k { color: var(--dsw-alias-label-secondary, #9a9a9a); font-size: 11px; text-transform: uppercase; }',
53
+ '.iris-wb-drawer .iris-wb-kv { font-size: 11px; color: var(--dsw-alias-label-secondary, #9a9a9a); }',
54
+ '.iris-wb-drawer .iris-wb-link { font-size: 11px; }',
55
+ '.iris-wb-drawer .iris-wb-row { gap: 6px; }',
56
+ '.iris-wb-drawer .iris-wb-prompt { white-space: pre-wrap; word-break: break-word; font-size: 12px; }',
57
+ '.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
+ '.iris-wb-loading { color: var(--dsw-alias-label-secondary, #9a9a9a); font-size: 12px; }',
59
+ /* ---- 操作卡片组(阶段 5 GUI 直连) ---- */
60
+ '.iris-act-group { display: grid; gap: 8px; }',
61
+ '.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; }',
62
+ '.iris-act-head { display: flex; align-items: center; gap: 8px; padding: 8px 10px; cursor: pointer; user-select: none; }',
63
+ '.iris-act-head:hover { background: var(--dsw-alias-bg-layer-2, #32323c); }',
64
+ '.iris-act-title { font-size: 13px; font-weight: 600; flex: 1; }',
65
+ '.iris-act-arrow { color: var(--dsw-alias-label-secondary, #9a9a9a); font-size: 10px; transition: transform 0.15s; }',
66
+ '.iris-act-card.open .iris-act-arrow { transform: rotate(90deg); }',
67
+ '.iris-act-body { padding: 4px 10px 12px; display: grid; gap: 8px; border-top: 1px dashed var(--dsw-alias-border-l1, #3a3a44); }',
68
+ '.iris-act-field { display: grid; gap: 4px; }',
69
+ '.iris-act-field label { font-size: 11px; color: var(--dsw-alias-label-secondary, #9a9a9a); }',
70
+ '.iris-act-field input, .iris-act-field textarea, .iris-act-field select { 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: 5px 7px; font-size: 12px; font-family: inherit; width: 100%; box-sizing: border-box; }',
71
+ '.iris-act-field textarea { min-height: 48px; resize: vertical; }',
72
+ '.iris-act-field input[type=checkbox] { width: auto; }',
73
+ '.iris-act-run { background: var(--dsw-alias-brand-primary, #7aa2f7); color: #1a1a1e; border: none; border-radius: 5px; padding: 6px 12px; font-size: 12px; font-weight: 600; cursor: pointer; }',
74
+ '.iris-act-run:disabled { opacity: 0.5; cursor: wait; }',
75
+ '.iris-act-result { white-space: pre-wrap; word-break: break-word; font-size: 12px; color: var(--dsw-alias-label-primary, #e6e6e6); }',
76
+ '.iris-act-result.err { color: var(--dsw-alias-state-error-primary, #e06c6c); }',
77
+ '.iris-act-img { max-width: 100%; max-height: 240px; border-radius: 6px; border: 1px solid var(--dsw-alias-border-l1, #3a3a44); }',
78
+ /* ---- 供应商管理(阶段 6) ---- */
79
+ '.iris-pm { display: grid; gap: 8px; }',
80
+ '.iris-pm-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }',
81
+ '.iris-pm-add { background: var(--dsw-alias-bg-layer-2, #32323c); color: var(--dsw-alias-label-primary, #e6e6e6); border: 1px dashed var(--dsw-alias-border-l1, #3a3a44); border-radius: 6px; padding: 5px 10px; font-size: 12px; cursor: pointer; }',
82
+ '.iris-pm-add:hover { border-color: var(--dsw-alias-brand-primary, #7aa2f7); }',
83
+ '.iris-pm-card { background: var(--dsw-alias-bg-layer-1, #26262e); border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 8px; padding: 8px 10px; display: grid; gap: 6px; }',
84
+ '.iris-pm-card.open { border-color: var(--dsw-alias-brand-primary, #7aa2f7); }',
85
+ '.iris-pm-top { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; cursor: pointer; }',
86
+ '.iris-pm-name { font-weight: 600; font-size: 13px; flex: 1; }',
87
+ '.iris-pm-btn { background: transparent; color: var(--dsw-alias-label-secondary, #9a9a9a); border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 4px; font-size: 11px; padding: 2px 7px; cursor: pointer; }',
88
+ '.iris-pm-btn:hover { color: var(--dsw-alias-label-primary, #e6e6e6); border-color: var(--dsw-alias-label-secondary, #9a9a9a); }',
89
+ '.iris-pm-btn.danger:hover { color: var(--dsw-alias-state-error-primary, #e06c6c); border-color: var(--dsw-alias-state-error-primary, #e06c6c); }',
90
+ '.iris-pm-models { display: flex; flex-wrap: wrap; gap: 4px; }',
91
+ '.iris-pm-model { font-size: 11px; background: var(--dsw-alias-bg-layer-2, #32323c); border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 10px; padding: 1px 8px; color: var(--dsw-alias-label-primary, #e6e6e6); }',
92
+ '.iris-pm-model.dim { opacity: 0.55; }',
93
+ '.iris-pm-cap { color: var(--dsw-alias-brand-primary, #7aa2f7); font-size: 10px; }',
94
+ '.iris-pm-note { font-size: 11px; color: var(--dsw-alias-label-secondary, #9a9a9a); }',
95
+ '.iris-pm-field { display: grid; gap: 3px; }',
96
+ '.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
+ '.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); }',
100
+ '.iris-cap-grid { display: grid; gap: 6px; }',
101
+ '.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
+ '.iris-cap-name { font-weight: 600; min-width: 64px; }',
103
+ '.iris-cap-chip { display: inline-flex; align-items: center; gap: 3px; background: var(--dsw-alias-bg-layer-2, #32323c); border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 10px; padding: 1px 6px; font-size: 11px; }',
104
+ '.iris-cap-chip b { color: var(--dsw-alias-brand-primary, #7aa2f7); }',
105
+ '.iris-cap-auto { color: var(--dsw-alias-label-secondary, #9a9a9a); font-size: 11px; font-style: italic; }',
106
+ '.iris-pm-model-sel { background: var(--dsw-alias-bg-layer-2, #32323c); color: var(--dsw-alias-label-primary, #e6e6e6); border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 4px; padding: 4px 6px; font-size: 12px; width: 100%; }',
107
+ '.iris-wb-drawer .iris-wb-loading { color: var(--dsw-alias-label-secondary, #9a9a9a); font-size: 12px; }',
108
+ '.iris-bubble { position: fixed; z-index: 9999; cursor: grab; user-select: none; touch-action: none; }',
109
+ '.iris-bubble.dragging { cursor: grabbing; }',
110
+ '.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); }',
113
+ '.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; box-shadow: 0 8px 30px rgba(0, 0, 0, 0.45); }',
115
+ '.iris-bubble-panel-head { position: absolute; top: 6px; right: 6px; z-index: 2; }',
116
+ '.iris-bubble-close { border: none; background: transparent; color: var(--dsw-alias-label-secondary, #9a9a9a); font-size: 14px; cursor: pointer; padding: 4px 6px; }',
117
+ '.iris-bubble-close:hover { color: var(--dsw-alias-label-primary, #e6e6e6); }',
118
+ /* 泡泡瘦身(阶段 4 续):标签页 + 紧凑任务行 */
119
+ '.iris-bubble-tabs { display: flex; gap: 6px; margin-bottom: 10px; border-bottom: 1px solid var(--dsw-alias-border-l1, #3a3a44); padding-bottom: 8px; }',
120
+ '.iris-bubble-tab { flex: 1; border: 1px solid var(--dsw-alias-border-l1, #3a3a44); background: var(--dsw-alias-bg-layer-2, #32323c); color: var(--dsw-alias-label-secondary, #9a9a9a); border-radius: 6px; padding: 5px 8px; font-size: 12px; cursor: pointer; }',
121
+ '.iris-bubble-tab.active { color: var(--dsw-alias-label-primary, #e6e6e6); border-color: var(--dsw-alias-brand-primary, #7aa2f7); background: var(--dsw-alias-bg-layer-1, #26262e); }',
122
+ '.iris-task-mini { display: flex; align-items: center; gap: 6px; font-size: 12px; padding: 4px 6px; border-radius: 6px; cursor: pointer; }',
123
+ '.iris-task-mini:hover { background: var(--dsw-alias-bg-layer-2, #32323c); }',
124
+ '.iris-task-mini .iris-task-mini-prompt { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-secondary, #9a9a9a); }',
125
+ '.iris-bubble-more { font-size: 11px; color: var(--dsw-alias-brand-primary, #7aa2f7); background: transparent; border: none; cursor: pointer; padding: 4px 2px; text-align: left; }',
126
+ /* 历史浏览器(设置页按日期分组折叠) */
127
+ '.iris-hist-group { margin-bottom: 4px; }',
128
+ '.iris-hist-head { display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-secondary, #9a9a9a); cursor: pointer; padding: 4px 2px; }',
129
+ '.iris-hist-head:hover { color: var(--dsw-alias-label-primary, #e6e6e6); }',
130
+ /* 常用卡片选择器 + 清理区 */
131
+ '.iris-pick-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 4px 10px; }',
132
+ '.iris-pick-item { display: flex; align-items: center; gap: 6px; font-size: 12px; cursor: pointer; }',
133
+ '.iris-clean { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }',
134
+ '.iris-clean-btn { 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: 6px; padding: 4px 9px; font-size: 12px; cursor: pointer; }',
135
+ '.iris-clean-btn:hover { border-color: var(--dsw-alias-brand-primary, #7aa2f7); }',
136
+ '.iris-clean-btn.danger:hover { color: var(--dsw-alias-state-error-primary, #e06c6c); border-color: var(--dsw-alias-state-error-primary, #e06c6c); }',
137
+ '.iris-clean-note { font-size: 11px; color: var(--dsw-alias-label-secondary, #9a9a9a); }',
138
+ /* 模型池(阶段 9 P4) */
139
+ '.iris-pm-pool { display: grid; gap: 4px; }',
140
+ '.iris-pm-mrows { display: grid; gap: 3px; max-height: 220px; overflow: auto; }',
141
+ '.iris-pm-mrow { display: flex; align-items: center; gap: 6px; font-size: 12px; }',
142
+ '.iris-pm-mname { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }',
143
+ '.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
+ '.iris-pm-mcaps { display: flex; gap: 3px; }',
145
+ '.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); }',
148
+ '.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
+ /* 文件选择器(阶段 10) */
150
+ '.iris-ff { display: grid; gap: 4px; }',
151
+ '.iris-ff-bar { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }',
152
+ '.iris-ff-btn { 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: 5px; padding: 3px 8px; font-size: 11px; cursor: pointer; }',
153
+ '.iris-ff-btn:hover { border-color: var(--dsw-alias-brand-primary, #7aa2f7); }',
154
+ '.iris-ff-btn.primary { background: var(--dsw-alias-brand-primary, #7aa2f7); color: var(--dsw-alias-label-primary-inverted, #fff); border-color: var(--dsw-alias-brand-primary, #7aa2f7); font-weight: 600; }',
155
+ '.iris-ff-btn.primary:hover { filter: brightness(1.1); }',
156
+ '.iris-ff-btn.ghost { background: transparent; color: var(--dsw-alias-label-tertiary, #7a7a85); border-style: dashed; }',
157
+ '.iris-ff-hint { font-size: 10px; line-height: 1.45; color: var(--dsw-alias-label-tertiary, #7a7a85); }',
158
+ '.iris-ff-val { font-size: 11px; color: var(--dsw-alias-label-secondary, #9a9a9a); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }',
159
+ '.iris-ff-val.set { color: var(--dsw-alias-state-success-primary, #6fcf6f); }',
160
+ '.iris-ff-list { display: grid; gap: 2px; max-height: 160px; overflow: auto; border: 1px solid var(--dsw-alias-border-l1, #3a3a44); border-radius: 6px; padding: 4px; background: var(--dsw-alias-bg-layer-2, #32323c); }',
161
+ '.iris-ff-item { display: flex; align-items: center; gap: 6px; font-size: 11px; cursor: pointer; padding: 2px 4px; border-radius: 4px; }',
162
+ '.iris-ff-item:hover { background: var(--dsw-alias-bg-layer-1, #26262e); }',
163
+ '.iris-ff-item .iris-ff-src { color: var(--dsw-alias-brand-primary, #7aa2f7); font-size: 10px; }'
164
+ ].join('\n');
165
+
166
+ function adoptStyles() {
167
+ if (document.getElementById(STYLE_ID) !== null) return;
168
+ var style = document.createElement('style');
169
+ style.id = STYLE_ID;
170
+ style.dataset.plugin = 'dsh-iris';
171
+ style.dataset.pluginCss = STYLE_ID;
172
+ style.textContent = cssText;
173
+ document.head.appendChild(style);
174
+ }
175
+
176
+ var inject = ['slots'];
177
+
178
+ function apply(ctx) {
179
+ try {
180
+ console.log('[iris] client apply start');
181
+ adoptStyles();
182
+
183
+ /* ---- 当前会话 id(文件选择器 L1 用;ctx.sessions 是标准插件能力) ---- */
184
+ var currentSessionId = null;
185
+ try {
186
+ var sessions = ctx && ctx.sessions;
187
+ var listObs = sessions && sessions.list;
188
+ if (listObs && typeof listObs.getSnapshot === 'function') {
189
+ currentSessionId = (listObs.getSnapshot() || {}).current || null;
190
+ if (typeof listObs.subscribe === 'function') {
191
+ listObs.subscribe(function (snap) { currentSessionId = (snap || {}).current || null; });
192
+ }
193
+ }
194
+ } catch (_) { /* 拿不到会话 → L1 退化为只列 iris 产物 */ }
195
+ function sessionId() { return currentSessionId || ''; }
196
+
197
+ /* ---- 三个座位共享一份状态订阅(模块级 pub/sub + SSE 实时推送) ---- */
198
+ var sharedState = null;
199
+ var sharedListeners = [];
200
+ var sharedTimer = null; // 兜底轮询(SSE 断线时状态仍会刷新)
201
+ var sharedSource = null; // EventSource(SSE 主通道)
202
+
203
+ function useIrisState(tickMs) {
204
+ var pair = React.useState(sharedState);
205
+ var setState = pair[1];
206
+ React.useEffect(function () {
207
+ function listener(data) {
208
+ setState(data);
209
+ }
210
+ sharedListeners.push(listener);
211
+ if (sharedState) setState(sharedState);
212
+ if (!sharedTimer && !sharedSource) {
213
+ function apply(data) {
214
+ sharedState = data;
215
+ for (var i = 0; i < sharedListeners.length; i++) {
216
+ sharedListeners[i](data);
217
+ }
218
+ }
219
+ function load() {
220
+ fetch('/iris/api/state')
221
+ .then(function (res) { return res.ok ? res.json() : null; })
222
+ .then(function (data) {
223
+ if (data) apply(data);
224
+ })
225
+ .catch(function () { /* 网络抖动静默 */ });
226
+ }
227
+ load(); // 首屏立即拉一次(SSE 建连前/失败时仍有数据)
228
+ // SSE 主通道:实时接收状态推送,替代 5s 轮询
229
+ try {
230
+ sharedSource = new EventSource('/iris/api/state/events');
231
+ sharedSource.onmessage = function (e) {
232
+ try { apply(JSON.parse(e.data)); } catch (_) { /* 忽略格式错误 */ }
233
+ };
234
+ sharedSource.onerror = function () {
235
+ // 浏览器自动重连(retry: 3000);兜底轮询 30s 在断线期间保持刷新
236
+ };
237
+ } catch (_) { sharedSource = null; }
238
+ // 兜底轮询:SSE 断线/漏推时状态最终一致(30s 一次,远低于原 5s 频率)
239
+ sharedTimer = setInterval(load, 30000);
240
+ }
241
+ return function () {
242
+ var idx = sharedListeners.indexOf(listener);
243
+ if (idx >= 0) sharedListeners.splice(idx, 1);
244
+ if (sharedListeners.length === 0) {
245
+ if (sharedSource) { try { sharedSource.close(); } catch (_) {} sharedSource = null; }
246
+ if (sharedTimer) { clearInterval(sharedTimer); sharedTimer = null; }
247
+ }
248
+ };
249
+ }, [tickMs]);
250
+ return sharedState;
251
+ }
252
+
253
+ /* ---- 任务详情抽屉:按需拉取 /iris/api/task/:id ---- */
254
+ function useTaskDetail(taskId) {
255
+ var pair = React.useState(null);
256
+ var setDetail = pair[1];
257
+ React.useEffect(function () {
258
+ if (!taskId) { setDetail(null); return; }
259
+ var alive = true;
260
+ setDetail(null); // 换任务先清空,显示加载中
261
+ fetch('/iris/api/task/' + encodeURIComponent(taskId))
262
+ .then(function (res) { return res.ok ? res.json() : null; })
263
+ .then(function (data) { if (alive && data) setDetail(data); })
264
+ .catch(function () { /* 静默 */ });
265
+ return function () { alive = false; };
266
+ }, [taskId]);
267
+ return pair[0];
268
+ }
269
+
270
+ function fmtTime(iso) {
271
+ if (!iso) return '';
272
+ var d = new Date(iso);
273
+ return isNaN(d.getTime()) ? '' : d.toLocaleString();
274
+ }
275
+
276
+ function fmtElapsed(ms) {
277
+ if (!ms) return '';
278
+ var s = Math.round(ms / 1000);
279
+ if (s < 60) return s + 's';
280
+ return Math.floor(s / 60) + 'm' + (s % 60) + 's';
281
+ }
282
+
283
+ /* cap → 单字图标(紧凑行/历史分组用) */
284
+ var CAP_EMOJI = { image: '🎨', video: '🎬', tts: '🔊', transcribe: '🎙️', summarize: '📝' };
285
+ function capIcon(cap) { return CAP_EMOJI[cap] || '•'; }
286
+
287
+ /* 相对时间(今天/昨天/更早分组) */
288
+ function dayBucket(iso) {
289
+ if (!iso) return '更早';
290
+ var d = new Date(iso);
291
+ if (isNaN(d.getTime())) return '更早';
292
+ var now = new Date();
293
+ var startToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
294
+ var t = d.getTime();
295
+ if (t >= startToday) return '今天';
296
+ if (t >= startToday - 86400000) return '昨天';
297
+ return '更早';
298
+ }
299
+
300
+ /* 紧凑任务行:泡泡「任务」标签用,单行摘要 */
301
+ function taskRowMini(task, selected, onSelect) {
302
+ var st = task.status === 'succeeded' ? 'ok' : task.status === 'failed' ? 'err' : '';
303
+ var label = task.status === 'succeeded' ? '成功' : task.status === 'failed' ? '失败' : task.status === 'canceled' ? '取消' : (task.progress || '运行中');
304
+ return React.createElement('div', {
305
+ key: task.id, className: 'iris-task-mini' + (selected ? ' selected' : ''),
306
+ onClick: function () { onSelect(selected ? null : task.id); }, title: '点击展开详情'
307
+ },
308
+ React.createElement('span', {}, capIcon(task.cap)),
309
+ React.createElement('span', { className: 'iris-wb-badge ' + st }, label),
310
+ React.createElement('span', { className: 'iris-task-mini-prompt' }, String(task.prompt || task.model || task.cap)),
311
+ React.createElement('span', { className: 'iris-wb-muted' }, task.createdAt ? new Date(task.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : ''));
312
+ }
313
+
314
+ /* ---- 任务详情抽屉:点击任务卡片行展开 ---- */
315
+ function TaskDetailDrawer({ taskId }) {
316
+ var detail = useTaskDetail(taskId);
317
+ if (!detail) {
318
+ return React.createElement('div', { className: 'iris-wb-drawer iris-wb-loading' }, '加载任务详情…');
319
+ }
320
+ var el = [];
321
+ el.push(React.createElement('div', { key: 'k', className: 'iris-wb-k' }, '提示词'));
322
+ el.push(React.createElement('div', { key: 'p', className: 'iris-wb-prompt' }, detail.prompt || '(空)'));
323
+ if (detail.error) {
324
+ el.push(React.createElement('div', { key: 'ek', className: 'iris-wb-k' }, '错误'));
325
+ el.push(React.createElement('div', { key: 'e', className: 'iris-wb-err' }, detail.error));
326
+ }
327
+ var kv = [
328
+ ['ID', detail.id],
329
+ ['能力', detail.cap],
330
+ ['状态', detail.status],
331
+ ['模型', detail.model],
332
+ ['供应商', detail.providerName],
333
+ ['模式', detail.mode || '—'],
334
+ ['远端任务', detail.remoteTaskId || '—'],
335
+ ['发起', detail.createdAt ? fmtTime(detail.createdAt) : '—'],
336
+ ['完成', detail.finishedAt ? fmtTime(detail.finishedAt) : '—'],
337
+ ['耗时', detail.elapsedMs ? fmtElapsed(detail.elapsedMs) : '—']
338
+ ];
339
+ el.push(React.createElement('div', { key: 'kvk', className: 'iris-wb-k' }, '元数据'));
340
+ el.push(React.createElement('div', { key: 'kv', className: 'iris-wb-row' },
341
+ kv.map(function (pair, i) {
342
+ return React.createElement('span', { key: i, className: 'iris-wb-kv' }, pair[0] + ': ' + pair[1]);
343
+ })));
344
+ var files = detail.files || [];
345
+ if (files.length) {
346
+ el.push(React.createElement('div', { key: 'fk', className: 'iris-wb-k' }, '产物文件'));
347
+ el.push(React.createElement('div', { key: 'f', className: 'iris-wb-row' },
348
+ files.map(function (f) {
349
+ return React.createElement('span', { key: f, className: 'iris-wb-kv' }, f);
350
+ })));
351
+ }
352
+ var media = detail.media || [];
353
+ if (media.length) {
354
+ el.push(React.createElement('div', { key: 'mk', className: 'iris-wb-k' }, '播放'));
355
+ el.push(React.createElement('div', { key: 'm', className: 'iris-wb-row' },
356
+ media.map(function (m) {
357
+ return React.createElement('a', {
358
+ key: m.file, className: 'iris-wb-link', href: m.url, target: '_blank', rel: 'noreferrer',
359
+ onClick: function (e) { e.stopPropagation(); }
360
+ }, '▶ ' + m.file);
361
+ })));
362
+ }
363
+ var atts = detail.attachments || [];
364
+ if (atts.length) {
365
+ el.push(React.createElement('div', { key: 'ak', className: 'iris-wb-k' }, '附件'));
366
+ el.push(React.createElement('div', { key: 'a', className: 'iris-wb-row' },
367
+ atts.map(function (a) {
368
+ return React.createElement('span', { key: a.attachmentId, className: 'iris-wb-kv' }, a.attachmentId + (a.file ? ' (' + a.file + ')' : ''));
369
+ })));
370
+ }
371
+ return React.createElement('div', { className: 'iris-wb-drawer' }, ...el);
372
+ }
373
+
374
+ function taskRow(task, isRunning, selected, onSelect) {
375
+ var badge;
376
+ if (task.status === 'succeeded') {
377
+ badge = React.createElement('span', { className: 'iris-wb-badge ok' }, '成功');
378
+ } else if (task.status === 'failed') {
379
+ badge = React.createElement('span', { className: 'iris-wb-badge err' }, '失败');
380
+ } else {
381
+ badge = React.createElement('span', { className: 'iris-wb-badge' }, task.status);
382
+ }
383
+ var cells = [
384
+ React.createElement('span', { className: 'iris-wb-kv' }, task.cap),
385
+ badge,
386
+ React.createElement('span', { className: 'iris-wb-kv' }, task.model || ''),
387
+ React.createElement('span', { className: 'iris-wb-muted' }, String(task.prompt || '').slice(0, 60))
388
+ ];
389
+ if (task.error) cells.push(React.createElement('span', { className: 'iris-wb-badge err' }, String(task.error).slice(0, 40)));
390
+ var links = (task.media || []).map(function (m) {
391
+ return React.createElement('a', {
392
+ key: m.file, className: 'iris-wb-link', href: m.url, target: '_blank', rel: 'noreferrer',
393
+ onClick: function (e) { e.stopPropagation(); }
394
+ }, '▶ ' + m.file);
395
+ });
396
+ var body = [
397
+ React.createElement('div', { className: 'iris-wb-row' }, ...cells),
398
+ links.length ? React.createElement('div', { className: 'iris-wb-row' }, ...links) : null
399
+ ];
400
+ if (isRunning) {
401
+ var pct = String(task.progress || '');
402
+ var isNumericPct = /^\d+(\.\d+)?%$/.test(pct);
403
+ if (isNumericPct) {
404
+ body.push(React.createElement('div', { className: 'iris-wb-progress' },
405
+ React.createElement('div', { className: 'iris-wb-bar' },
406
+ React.createElement('i', { style: { width: pct } })),
407
+ React.createElement('div', { className: 'iris-wb-muted' }, pct + (task.elapsedMs ? ' · ' + fmtElapsed(task.elapsedMs) : ''))));
408
+ } else {
409
+ body.push(React.createElement('div', { className: 'iris-wb-muted' }, (pct || '运行中') + (task.elapsedMs ? ' · ' + fmtElapsed(task.elapsedMs) : '')));
410
+ }
411
+ } else {
412
+ body.push(React.createElement('div', { className: 'iris-wb-muted' },
413
+ task.createdAt ? '发起 ' + fmtTime(task.createdAt) : '',
414
+ task.finishedAt ? ' · 完成 ' + fmtTime(task.finishedAt) : ''));
415
+ }
416
+ if (selected) body.push(React.createElement(TaskDetailDrawer, { key: 'drawer', taskId: task.id }));
417
+ return React.createElement('div', {
418
+ key: task.id,
419
+ className: 'iris-wb-card' + (isRunning ? ' iris-running' : '') + (selected ? ' selected' : ''),
420
+ onClick: function () { onSelect(selected ? null : task.id); },
421
+ title: '点击展开任务详情'
422
+ }, ...body);
423
+ }
424
+
425
+ /* ---- 供应商管理(阶段 6:增删 key + 模型列表 + 测试) ---- */
426
+ function postAction(action, body) {
427
+ return fetch('/iris/api/actions/' + action, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}) })
428
+ .then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); });
429
+ }
430
+
431
+ /* ---- 模型池(阶段 9 P4):发现/手动模型 + verified 标记 + 逐模型测试/移除 + 手动添加 ---- */
432
+ var CAP_SHORT = { 'image-gen': '图', 'video-gen': '视', 'tts': '音', 'transcribe': '转', 'vision': '视👁' };
433
+ function ModelPool(props) {
434
+ var p = props.provider;
435
+ var addPair = React.useState('');
436
+ var newId = addPair[0];
437
+ var setNewId = addPair[1];
438
+ var models = p.models || [];
439
+ function verBadge(m, c) {
440
+ var v = m.verified && m.verified[c];
441
+ if (!v) return React.createElement('span', { className: 'iris-pm-ver', title: '未实测' }, c + '?');
442
+ return React.createElement('span', { className: 'iris-pm-ver ' + (v.ok ? 'ok' : 'bad'), title: (v.ok ? '实测通过 ' : '实测未通过 ') + (v.note || '') }, c + (v.ok ? '✓' : '✗'));
443
+ }
444
+ var rows = models.map(function (m) {
445
+ var caps = m.capabilities || [];
446
+ return React.createElement('div', { key: m.id, className: 'iris-pm-mrow' },
447
+ React.createElement('span', { className: 'iris-pm-mname' }, m.id, m.source === 'manual' ? React.createElement('span', { className: 'iris-pm-mtag' }, '手动') : null),
448
+ React.createElement('span', { className: 'iris-pm-mcaps' }, caps.map(function (c) { return verBadge(m, c); })),
449
+ React.createElement('button', { className: 'iris-pm-btn', title: '逐能力实测(image/tts 产生真实调用)', onClick: function () { props.testModel(p, m); } }, '测'),
450
+ React.createElement('button', { className: 'iris-pm-btn danger', title: '从池移除', onClick: function () { props.removeModel(p, m); } }, '✕'));
451
+ });
452
+ return React.createElement('div', { className: 'iris-pm-pool' },
453
+ React.createElement('div', { className: 'iris-pm-note' }, '模型池(' + models.length + '):'),
454
+ rows.length ? React.createElement('div', { className: 'iris-pm-mrows' }, rows) : React.createElement('div', { className: 'iris-pm-note' }, '暂无模型 — 点「发现模型」拉取,或下方手动添加'),
455
+ React.createElement('div', { className: 'iris-wb-row' },
456
+ React.createElement('input', { className: 'iris-pm-madd', placeholder: '手动添加模型名,如 wan2.7-image', value: newId, onChange: function (e) { setNewId(e.target.value); } }),
457
+ React.createElement('button', { className: 'iris-pm-btn', onClick: function () { props.addModel(p, newId.trim()); setNewId(''); } }, '+ 添加')));
458
+ }
459
+
460
+ function ProviderManager() {
461
+ var listPair = React.useState(null);
462
+ var list = listPair[0];
463
+ var setList = listPair[1];
464
+ var openPair = React.useState(null); // 展开的 provider id
465
+ var setOpen = openPair[1];
466
+ var addingPair = React.useState(false);
467
+ var adding = addingPair[0];
468
+ var setAdding = addingPair[1];
469
+ var newForm = React.useState({ name: '', baseUrl: '', apiKey: '', type: 'openai' });
470
+ var notePair = React.useState('');
471
+ var note = notePair[0];
472
+ var setNote = notePair[1];
473
+ function refresh() {
474
+ postAction('providers_list', {}).then(function (r) { if (r.ok && r.d.providers) setList(r.d.providers); });
475
+ }
476
+ React.useEffect(function () { refresh(); }, []);
477
+ function addProvider() {
478
+ var f = newForm[0];
479
+ if (!f.baseUrl || !f.apiKey) { setNote('baseUrl 和 apiKey 必填'); return; }
480
+ postAction('providers_upsert', f).then(function (r) {
481
+ if (r.ok) { setAdding(false); setNote('已添加 ' + f.name); refresh(); }
482
+ else setNote(r.d.error || '添加失败');
483
+ });
484
+ }
485
+ function toggle(p) {
486
+ postAction('providers_upsert', { id: p.id, enabled: !p.enabled }).then(function (r) {
487
+ if (r.ok) refresh();
488
+ else setNote(r.d.error || '切换失败');
489
+ });
490
+ }
491
+ function remove(p) {
492
+ if (!window.confirm('删除供应商「' + (p.name || p.id) + '」?此操作不可撤销。')) return;
493
+ postAction('providers_remove', { id: p.id }).then(function (r) { if (r.ok) refresh(); else setNote(r.d.error || '删除失败'); });
494
+ }
495
+ function testVision(p) {
496
+ setNote('测试「' + (p.name || p.id) + '」的视觉能力…');
497
+ postAction('providers_test_vision', { id: p.id }).then(function (r) { setNote(r.d.text || r.d.error || '测试完成'); refresh(); });
498
+ }
499
+ function discover(p) {
500
+ setNote('正在发现「' + (p.name || p.id) + '」的模型…');
501
+ postAction('providers_discover', { id: p.id }).then(function (r) { setNote(r.d.text || r.d.error || '发现完成'); refresh(); });
502
+ }
503
+ function testModel(p, m) {
504
+ var caps = (m.capabilities || []).length ? m.capabilities : ['vision'];
505
+ setNote('实测 ' + m.id + '(' + caps.join('/') + ',image/tts 会产生真实调用)…');
506
+ // 逐个能力测,串行;每个存 verified
507
+ caps.reduce(function (chain, c) {
508
+ return chain.then(function () {
509
+ return postAction('providers_test_model', { id: p.id, model_id: m.id, capability: c })
510
+ .then(function (r) { setNote(r.d.text || r.d.error || ''); });
511
+ });
512
+ }, Promise.resolve()).then(function () { refresh(); });
513
+ }
514
+ function removeModel(p, m) {
515
+ if (!window.confirm('从池中移除模型「' + m.id + '」?')) return;
516
+ postAction('providers_remove_model', { id: p.id, model_id: m.id }).then(function (r) { setNote(r.d.text || r.d.error || ''); refresh(); });
517
+ }
518
+ function addModel(p, modelId) {
519
+ if (!modelId) { setNote('模型名不能为空'); return; }
520
+ postAction('providers_add_model', { id: p.id, model_id: modelId }).then(function (r) { setNote(r.d.text || r.d.error || ''); refresh(); });
521
+ }
522
+ var cards = (list || []).map(function (p) {
523
+ var open = openPair[0] === p.id;
524
+ var body = null;
525
+ if (open) {
526
+ body = React.createElement('div', { className: 'iris-pm-body' },
527
+ React.createElement('div', { className: 'iris-pm-note' }, 'Key ' + (p.apiKeyHint || '未配置') + ' · ' + (p.type || '') + ' · ' + (p.mediaProtocol || '')),
528
+ React.createElement('div', { className: 'iris-pm-note' }, '能力: ' + ((p.capabilities || []).join(' / ') || '无')),
529
+ React.createElement(ModelPool, { provider: p, onNote: setNote, onDone: refresh, testModel: testModel, removeModel: removeModel, addModel: addModel }),
530
+ React.createElement('div', { className: 'iris-wb-row' },
531
+ React.createElement('button', { className: 'iris-pm-btn', onClick: function () { discover(p); } }, '发现模型'),
532
+ React.createElement('button', { className: 'iris-pm-btn', onClick: function () { toggle(p); } }, p.enabled ? '停用' : '启用'),
533
+ React.createElement('button', { className: 'iris-pm-btn danger', onClick: function () { remove(p); } }, '删除')));
534
+ }
535
+ return React.createElement('div', { key: p.id, className: 'iris-pm-card' + (open ? ' open' : '') },
536
+ React.createElement('div', { className: 'iris-pm-top', onClick: function () { setOpen(open ? null : p.id); } },
537
+ React.createElement('span', { className: 'iris-pm-name' }, (p.name || p.id) + (p.enabled ? '' : '(停用)')),
538
+ React.createElement('span', { className: 'iris-pm-btn' }, open ? '收起' : '管理')),
539
+ body);
540
+ });
541
+ var addBlock = null;
542
+ if (adding) {
543
+ addBlock = React.createElement('div', { className: 'iris-pm-card' },
544
+ React.createElement('div', { className: 'iris-pm-field' }, React.createElement('label', {}, '名称'), React.createElement('input', { value: newForm[0].name, placeholder: '如 阿里云百炼', onChange: function (e) { newForm[1]({ ...newForm[0], name: e.target.value }); } })),
545
+ React.createElement('div', { className: 'iris-pm-field' }, React.createElement('label', {}, 'Base URL *'), React.createElement('input', { value: newForm[0].baseUrl, placeholder: 'https://dashscope.aliyuncs.com/compatible-mode/v1', onChange: function (e) { newForm[1]({ ...newForm[0], baseUrl: e.target.value }); } })),
546
+ React.createElement('div', { className: 'iris-pm-field' }, React.createElement('label', {}, 'API Key *'), React.createElement('input', { type: 'password', value: newForm[0].apiKey, placeholder: 'sk-...', onChange: function (e) { newForm[1]({ ...newForm[0], apiKey: e.target.value }); } })),
547
+ React.createElement('div', { className: 'iris-wb-row' },
548
+ React.createElement('button', { className: 'iris-act-run', onClick: addProvider }, '保存'),
549
+ React.createElement('button', { className: 'iris-pm-btn', onClick: function () { setAdding(false); } }, '取消')));
550
+ }
551
+ return React.createElement('div', { className: 'iris-pm' },
552
+ React.createElement('div', { className: 'iris-pm-head' },
553
+ React.createElement('span', { className: 'iris-wb-muted' }, (list ? list.length : 0) + ' 个供应商'),
554
+ React.createElement('button', { className: 'iris-pm-add', onClick: function () { setAdding(!adding); } }, adding ? '收起' : '+ 添加供应商')),
555
+ addBlock,
556
+ cards.length ? cards : React.createElement('div', { className: 'iris-wb-empty' }, '暂无供应商 — 点右上角「+ 添加供应商」'),
557
+ note ? React.createElement('div', { className: 'iris-pm-act' }, note) : null);
558
+ }
559
+
560
+ /* ---- 文件字段(阶段 10):让用户"看见并选文件",统一产出一个宿主路径 ---- */
561
+ function FileField(props) {
562
+ var value = props.value || '';
563
+ var onChange = props.onChange;
564
+ var accept = props.accept || '';
565
+ var menuPair = React.useState(null); // null | 'att'
566
+ var menu = menuPair[0];
567
+ var setMenu = menuPair[1];
568
+ var attPair = React.useState(null); // 附件列表
569
+ var atts = attPair[0];
570
+ var setAtts = attPair[1];
571
+ var busyPair = React.useState('');
572
+ var busy = busyPair[0];
573
+ var setBusy = busyPair[1];
574
+ /* 上传优先:宿主路径是逃生口,默认收起,点「⌨️ 高级 · 宿主路径」才展开
575
+ (旧写法以 !value 作初值,会让空字段默认就摊出高级输入框,与上传优先相反) */
576
+ var manualPair = React.useState(false);
577
+ var manual = manualPair[0];
578
+ var setManual = manualPair[1];
579
+ var fileInput = React.useRef(null);
580
+
581
+ function onPickFile(e) {
582
+ var f = e.target.files && e.target.files[0];
583
+ if (!f) return;
584
+ setBusy('上传中…');
585
+ fetch('/iris/api/upload?name=' + encodeURIComponent(f.name), { method: 'POST', body: f })
586
+ .then(function (r) { return r.json(); })
587
+ .then(function (d) { if (d.ok) { onChange(d.path); setBusy(''); } else { setBusy('上传失败:' + (d.error || '')); } })
588
+ .catch(function () { setBusy('上传失败:网络'); });
589
+ e.target.value = '';
590
+ }
591
+ function openAttachments() {
592
+ if (menu === 'att') { setMenu(null); return; }
593
+ setMenu('att');
594
+ if (!atts) {
595
+ setBusy('加载附件…');
596
+ postAction('attachments_list', { session_id: sessionId() }).then(function (r) {
597
+ setAtts((r.ok && r.d.attachments) || []); setBusy('');
598
+ });
599
+ }
600
+ }
601
+ function pickAttachment(a) {
602
+ setBusy('导出中…'); setMenu(null);
603
+ postAction('attachment_export', { session_id: sessionId(), attachment_id: a.attachmentId }).then(function (r) {
604
+ if (r.ok && r.d.path) { onChange(r.d.path); setBusy(''); }
605
+ else setBusy('导出失败:' + ((r.d && r.d.error) || ''));
606
+ });
607
+ }
608
+ var label = value ? (value.split('/').pop() || value) : '未选择文件';
609
+ return React.createElement('div', { className: 'iris-ff' },
610
+ React.createElement('div', { className: 'iris-ff-bar' },
611
+ React.createElement('button', { className: 'iris-ff-btn primary', title: '从本机选文件,上传一份副本到宿主(跨设备 / WSL / 安卓都可用)', onClick: function () { fileInput.current && fileInput.current.click(); } }, '💻 上传文件'),
612
+ React.createElement('button', { className: 'iris-ff-btn', title: '选本会话已有的附件:聊天里粘贴/上传的 🖼,或 🫧 iris 生成过的', onClick: openAttachments }, '📎 会话附件'),
613
+ React.createElement('button', { className: 'iris-ff-btn ghost', title: '高级:直接引用 DSH 宿主机器上的文件路径', onClick: function () { setManual(!manual); } }, '⌨️ 高级 · 宿主路径'),
614
+ React.createElement('input', { ref: fileInput, type: 'file', accept: accept, style: { display: 'none' }, onChange: onPickFile })),
615
+ React.createElement('div', { className: 'iris-ff-val' + (value ? ' set' : ''), title: value || '' }, (value ? '✓ ' : '○ ') + label + (busy ? ' · ' + busy : '')),
616
+ value ? null : React.createElement('div', { className: 'iris-ff-hint' }, '浏览器与 DSH 不在同一台机器(远程 / WSL / 安卓)时,宿主上没有你本机的文件——请用「💻 上传文件」或「📎 会话附件」。'),
617
+ manual ? React.createElement('input', { className: 'iris-pm-madd', placeholder: '高级:粘贴 DSH 宿主上的绝对路径,如 /home/user/pic.png', value: value, onChange: function (e) { onChange(e.target.value); } }) : null,
618
+ menu === 'att' ? React.createElement('div', { className: 'iris-ff-list' },
619
+ (atts && atts.length) ? atts.map(function (a) {
620
+ return React.createElement('div', { key: a.attachmentId, className: 'iris-ff-item', onClick: function () { pickAttachment(a); } },
621
+ React.createElement('span', { className: 'iris-ff-src' }, a.source === 'iris' ? '🫧' : '🖼'),
622
+ React.createElement('span', {}, a.name || a.attachmentId.slice(0, 18)));
623
+ }) : React.createElement('div', { className: 'iris-wb-empty' }, '无可选附件(会话无图或 iris 未生成过)')) : null);
624
+ }
625
+
626
+ /* ---- 操作卡片组(阶段 5 GUI 直连:POST /iris/api/actions/:name) ---- */
627
+ function ActionCard(props) {
628
+ var title = props.title;
629
+ var action = props.action;
630
+ var capability = props.capability; // 可选:该卡片对应能力(如 'image-gen'),用于模型选择/亮暗
631
+ var fields = props.fields || [];
632
+ var openPair = React.useState(false);
633
+ var open = openPair[0];
634
+ var setOpen = openPair[1];
635
+ var valsPair = React.useState({});
636
+ var vals = valsPair[0];
637
+ var setVals = valsPair[1];
638
+ var runPair = React.useState(null);
639
+ var run = runPair[0];
640
+ var setRun = runPair[1];
641
+ var modelPair = React.useState(null); // { assigned, options:[{id,providerId}], tested }
642
+ var modelInfo = modelPair[0];
643
+ var setModelInfo = modelPair[1];
644
+ function setVal(key, value) { var nv = {}; Object.keys(vals).forEach(function (k) { nv[k] = vals[k]; }); nv[key] = value; setVals(nv); }
645
+ function loadModels() {
646
+ if (!capability) return;
647
+ postAction('assignments_get', {}).then(function (r) {
648
+ if (!r.ok || !r.d) return;
649
+ var opts = (r.d.poolByCapability && r.d.poolByCapability[capability]) || [];
650
+ var ord = (r.d.order && r.d.order[capability]) || [];
651
+ var assignedRef = ord[0] || (opts[0] && opts[0].ref) || '';
652
+ var assignedOpt = opts.find(function (o) { return o.ref === assignedRef; });
653
+ // 不再自动填 vals.model:留空 = 用能力分配的解析结果(避免绕过分配)
654
+ setModelInfo({ assigned: assignedOpt ? assignedOpt.label : '', assignedRef: assignedRef, options: opts, loaded: true });
655
+ });
656
+ }
657
+ React.useEffect(function () { if (open && capability) loadModels(); }, [open]);
658
+ function submit() {
659
+ if (run === 'running') return;
660
+ setRun('running');
661
+ fetch('/iris/api/actions/' + action, {
662
+ method: 'POST',
663
+ headers: { 'Content-Type': 'application/json' },
664
+ body: JSON.stringify(vals)
665
+ })
666
+ .then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); })
667
+ .then(function (res) { setRun(res.ok ? { ok: true, text: res.d.text || '', img: res.d.imageDataUrl || null } : { ok: false, text: res.d.error || '执行失败' }); })
668
+ .catch(function () { setRun({ ok: false, text: '网络错误' }); });
669
+ }
670
+ var body = null;
671
+ if (open) {
672
+ var inputs = fields.map(function (f) {
673
+ var node;
674
+ if (f.type === 'file') {
675
+ node = React.createElement(FileField, { value: vals[f.key] || '', accept: f.accept || '', onChange: function (v) { setVal(f.key, v); } });
676
+ return React.createElement('div', { key: f.key, className: 'iris-act-field' },
677
+ React.createElement('label', {}, f.label), node);
678
+ }
679
+ if (f.type === 'textarea') {
680
+ node = React.createElement('textarea', {
681
+ value: vals[f.key] || '', placeholder: f.placeholder || '',
682
+ onChange: function (e) { setVal(f.key, e.target.value); }
683
+ });
684
+ } else if (f.type === 'checkbox') {
685
+ node = React.createElement('input', { type: 'checkbox', checked: !!vals[f.key], onChange: function (e) { setVal(f.key, e.target.checked); } });
686
+ } else {
687
+ node = React.createElement('input', {
688
+ value: vals[f.key] || '', placeholder: f.placeholder || '', type: f.type || 'text',
689
+ onChange: function (e) { setVal(f.key, e.target.value); }
690
+ });
691
+ }
692
+ return React.createElement('div', { key: f.key, className: 'iris-act-field' },
693
+ React.createElement('label', {}, f.label), node);
694
+ });
695
+ var result = null;
696
+ if (run) {
697
+ if (run.ok && run.img) {
698
+ result = React.createElement('div', { className: 'iris-act-result' },
699
+ React.createElement('div', {}, run.text),
700
+ React.createElement('img', { className: 'iris-act-img', src: run.img, alt: '结果' }));
701
+ } else {
702
+ result = React.createElement('div', { className: 'iris-act-result' + (run.ok ? '' : ' err') }, run.text);
703
+ }
704
+ }
705
+ // 只读模型提示:当前能力解析到的模型;改分配去「能力分配」区(唯一写入口)
706
+ var modelBar = null;
707
+ if (capability) {
708
+ var lit = modelInfo && modelInfo.loaded && modelInfo.options.length > 0;
709
+ var shown = (vals.model && vals.model.trim()) || (modelInfo && modelInfo.assigned) || '';
710
+ modelBar = React.createElement('div', { className: 'iris-act-field' },
711
+ React.createElement('label', {},
712
+ React.createElement('span', { className: lit ? 'iris-pm-dot lit' : 'iris-pm-dot' }, '●'),
713
+ ' 模型:' + (shown || '自动') + (lit ? '' : '(无可用模型)')),
714
+ React.createElement('div', { className: 'iris-wb-muted' }, '改分配 → 上方「能力分配(failover 顺序)」'));
715
+ }
716
+ body = React.createElement('div', { className: 'iris-act-body' },
717
+ modelBar,
718
+ inputs,
719
+ React.createElement('button', { className: 'iris-act-run', disabled: run === 'running', onClick: submit },
720
+ run === 'running' ? '执行中…' : '执行'),
721
+ result);
722
+ }
723
+ var headLit = !capability || (modelInfo && modelInfo.loaded && modelInfo.options.length > 0);
724
+ return React.createElement('div', { className: 'iris-act-card' + (open ? ' open' : '') + (headLit ? ' lit' : ' dim') },
725
+ React.createElement('div', { className: 'iris-act-head', onClick: function () { setOpen(!open); } },
726
+ React.createElement('span', { className: 'iris-act-title' }, title),
727
+ capability ? React.createElement('span', { className: headLit ? 'iris-pm-dot lit' : 'iris-pm-dot', title: headLit ? '模型可用' : '无可用模型' }, '●') : null,
728
+ React.createElement('span', { className: 'iris-act-arrow' }, '▶')),
729
+ body);
730
+ }
731
+
732
+ /* ---- 操作卡片注册表(单一来源:设置页全量 / 泡泡常用子集 / 选择器三处共用) ---- */
733
+ var CARD_DEFS = [
734
+ { action: 'image', title: '🎨 画图', capability: 'image-gen', fields: [
735
+ { key: 'prompt', label: '提示词', type: 'textarea', placeholder: '详细描述要生成的图片' },
736
+ { key: 'size', label: '尺寸 (可选)', placeholder: '如 1024*1024' },
737
+ { key: 'model', label: '模型 (可选)', placeholder: '默认 provider 模型' }] },
738
+ { action: 'video', title: '🎬 视频', capability: 'video-gen', fields: [
739
+ { key: 'prompt', label: '提示词', type: 'textarea', placeholder: '描述画面/运动' },
740
+ { key: 'first_frame_path', label: '首帧图片', type: 'file', accept: 'image/*', placeholder: '/path/frame.png' },
741
+ { key: 'size', label: '尺寸 (可选)', placeholder: '如 1280*720' },
742
+ { key: 'model', label: '模型 (可选)', placeholder: '如 wan2.2-t2v-flash' }] },
743
+ { action: 'tts', title: '🔊 语音合成', capability: 'tts', fields: [
744
+ { key: 'text', label: '文本', type: 'textarea', placeholder: '要合成的文字' },
745
+ { key: 'voice', label: '音色 (可选)', placeholder: '如 Cherry' }] },
746
+ { action: 'transcribe', title: '🎙️ 音频转写', capability: 'transcribe', fields: [
747
+ { key: 'audio_path', label: '音频文件', type: 'file', accept: 'audio/*', placeholder: '/path/audio.wav' }] },
748
+ { action: 'video_frames', title: '🎞️ 视频抽帧', fields: [
749
+ { key: 'video_path', label: '视频文件', type: 'file', accept: 'video/*', placeholder: '/path/video.mp4' },
750
+ { key: 'max_frames', label: '最多帧数 (可选)', placeholder: '8(1-20)' },
751
+ { key: 'target_width', label: '目标宽度 (可选)', placeholder: '640' },
752
+ { key: 'format', label: '格式 (可选)', placeholder: 'jpeg 或 png' }] },
753
+ { action: 'media_summarize', title: '📝 视频摘要', capability: 'vision', fields: [
754
+ { key: 'video_path', label: '视频文件', type: 'file', accept: 'video/*', placeholder: '/path/video.mp4' },
755
+ { key: 'question', label: '问题 (可选)', type: 'textarea', placeholder: '默认:总结画面内容/场景/主题' },
756
+ { key: 'max_frames', label: '采样帧数 (可选)', placeholder: '8(1-12)' },
757
+ { key: 'transcribe_text', label: '已有转写文本 (可选)', type: 'textarea', placeholder: '若已转写音轨可粘贴,摘要将结合语音内容' }] },
758
+ { action: 'look', title: '👁 看图', capability: 'vision', fields: [
759
+ { key: 'image_path', label: '图片', type: 'file', accept: 'image/*', placeholder: '/path/image.png' },
760
+ { key: 'question', label: '问题 (可选)', placeholder: '默认:详细描述' }] },
761
+ { action: 'crop', title: '✂️ 裁剪', fields: [
762
+ { key: 'image_path', label: '图片', type: 'file', accept: 'image/*', placeholder: '/path/image.png' },
763
+ { key: 'left', label: 'left', placeholder: '0' },
764
+ { key: 'top', label: 'top', placeholder: '0' },
765
+ { key: 'width', label: 'width', placeholder: '100' },
766
+ { key: 'height', label: 'height', placeholder: '100' }] },
767
+ { action: 'diff', title: '📷 像素差异', fields: [
768
+ { key: 'image_a_path', label: '图 A', type: 'file', accept: 'image/*', placeholder: '/path/a.png' },
769
+ { key: 'image_b_path', label: '图 B', type: 'file', accept: 'image/*', placeholder: '/path/b.png' }] },
770
+ { action: 'locate', title: '📍 定位', capability: 'vision', fields: [
771
+ { key: 'image_path', label: '图片', type: 'file', accept: 'image/*', placeholder: '/path/image.png' },
772
+ { key: 'target', label: '目标', placeholder: '如 红色按钮' },
773
+ { key: 'model', label: '模型 (可选)', placeholder: '如 qwen3-vl-235b-a22b-thinking' }] },
774
+ { action: 'html', title: '🖼️ HTML 截图', fields: [
775
+ { key: 'html', label: 'HTML', type: 'textarea', placeholder: '<h1>Hello</h1>' },
776
+ { key: 'fullPage', label: '整页截图', type: 'checkbox' }] },
777
+ { action: 'ocr', title: '📄 长截图 OCR', capability: 'vision', fields: [
778
+ { key: 'image_path', label: '长截图', type: 'file', accept: 'image/*', placeholder: '/path/long.png' },
779
+ { key: 'chunk_height', label: '分块高度 (可选)', placeholder: '1200' },
780
+ { key: 'overlap', label: '重叠 (可选)', placeholder: '120' }] },
781
+ { action: 'relook', title: '🔄 重看 (附件)', capability: 'vision', fields: [
782
+ { key: 'attachment_id', label: 'attachment id (iris 画图产物)', placeholder: 'sha256:...' },
783
+ { key: 'question', label: '问题', placeholder: '再问一次这张图' }] },
784
+ { action: 'status', title: '📋 任务查询', fields: [
785
+ { key: 'task_id', label: 'task id (留空查最近)', placeholder: 't_xxx 或留空' }] }
786
+ ];
787
+ function renderCard(def) {
788
+ return React.createElement(ActionCard, { key: def.action, title: def.title, action: def.action, capability: def.capability, fields: def.fields });
789
+ }
790
+ function ActionGroups() {
791
+ return React.createElement('div', { className: 'iris-act-group' }, CARD_DEFS.map(renderCard));
792
+ }
793
+
794
+ /* ---- 能力分配(阶段 6 条目 4):每能力一个有序 failover 列表 ---- */
795
+ var CAP_ROWS = [
796
+ { cap: 'image-gen', label: '🎨 画图' },
797
+ { cap: 'video-gen', label: '🎬 视频' },
798
+ { cap: 'tts', label: '🔊 语音' },
799
+ { cap: 'transcribe', label: '🎙️ 转写' },
800
+ { cap: 'vision', label: '👁 视觉' }
801
+ ];
802
+ function CapabilityAssigner() {
803
+ var dataPair = React.useState(null);
804
+ var data = dataPair[0];
805
+ var setData = dataPair[1];
806
+ function load() {
807
+ postAction('assignments_get', {}).then(function (r) {
808
+ if (r.ok && r.d) setData(r.d);
809
+ });
810
+ }
811
+ React.useEffect(function () { load(); }, []);
812
+ function save(cap, refs) {
813
+ postAction('assignments_set', { capability: cap, model_refs: refs }).then(function () {
814
+ load(); // 成败都重载:成功显示新序,失败显示实际落盘序(不骗 UI)
815
+ });
816
+ }
817
+ if (!data) return React.createElement('div', { className: 'iris-wb-muted' }, '加载能力分配…');
818
+ var order = data.order || {};
819
+ var poolBy = data.poolByCapability || {};
820
+ return React.createElement('div', { className: 'iris-cap-grid' }, CAP_ROWS.map(function (row) {
821
+ var list = order[row.cap] || [];
822
+ var opts = poolBy[row.cap] || [];
823
+ var available = opts.filter(function (o) { return list.indexOf(o.ref) < 0; });
824
+ var chips = list.map(function (ref, i) {
825
+ var opt = opts.find(function (o) { return o.ref === ref; });
826
+ return React.createElement('span', { key: ref, className: 'iris-cap-chip' },
827
+ React.createElement('b', {}, (i + 1) + '.'),
828
+ opt ? opt.label : ref,
829
+ i > 0 ? React.createElement('button', {
830
+ className: 'iris-pm-btn', title: '上移',
831
+ onClick: function () { var n = list.slice(); var t = n.splice(i, 1)[0]; n.splice(i - 1, 0, t); save(row.cap, n); }
832
+ }, '↑') : null,
833
+ i < list.length - 1 ? React.createElement('button', {
834
+ className: 'iris-pm-btn', title: '下移',
835
+ onClick: function () { var n = list.slice(); var t = n.splice(i, 1)[0]; n.splice(i + 1, 0, t); save(row.cap, n); }
836
+ }, '↓') : null,
837
+ React.createElement('button', {
838
+ className: 'iris-pm-btn danger', title: '移出列表',
839
+ onClick: function () { save(row.cap, list.filter(function (x) { return x !== ref; })); }
840
+ }, '✕'));
841
+ });
842
+ return React.createElement('div', { key: row.cap, className: 'iris-cap-row' },
843
+ React.createElement('span', { className: 'iris-cap-name' }, row.label),
844
+ chips.length ? chips : React.createElement('span', { className: 'iris-cap-auto' }, '自动(按池顺序)'),
845
+ available.length ? React.createElement('select', {
846
+ className: 'iris-pm-model-sel', value: '',
847
+ onChange: function (e) { if (e.target.value) save(row.cap, list.concat([e.target.value])); }
848
+ },
849
+ React.createElement('option', { value: '' }, '+ 加入 failover'),
850
+ available.map(function (o) { return React.createElement('option', { key: o.ref, value: o.ref }, o.label); })) : null,
851
+ list.length ? React.createElement('button', {
852
+ className: 'iris-pm-btn', title: '清除手动分配,回退自动',
853
+ onClick: function () { save(row.cap, []); }
854
+ }, '恢复自动') : null);
855
+ }));
856
+ }
857
+
858
+ /* ---- 泡泡常用卡片选择(localStorage + 跨座位响应) ---- */
859
+ var BUBBLE_CARDS_KEY = 'iris-bubble-cards';
860
+ var bubbleCardListeners = new Set();
861
+ function readBubbleCards() {
862
+ try {
863
+ var raw = localStorage.getItem(BUBBLE_CARDS_KEY);
864
+ var arr = raw ? JSON.parse(raw) : [];
865
+ return Array.isArray(arr) ? arr.filter(function (a) { return CARD_DEFS.some(function (d) { return d.action === a; }); }) : [];
866
+ } catch (_) { return []; }
867
+ }
868
+ function writeBubbleCards(list) {
869
+ try { localStorage.setItem(BUBBLE_CARDS_KEY, JSON.stringify(list)); } catch (_) {}
870
+ bubbleCardListeners.forEach(function (fn) { fn(list); });
871
+ }
872
+ function useBubbleCards() {
873
+ var pair = React.useState(readBubbleCards);
874
+ var list = pair[0];
875
+ var setList = pair[1];
876
+ React.useEffect(function () {
877
+ function onExt(e) { if (e && e.key === BUBBLE_CARDS_KEY) setList(readBubbleCards()); }
878
+ bubbleCardListeners.add(setList);
879
+ window.addEventListener('storage', onExt); // 跨标签页同步
880
+ return function () { bubbleCardListeners.delete(setList); window.removeEventListener('storage', onExt); };
881
+ }, []);
882
+ return [list, writeBubbleCards];
883
+ }
884
+
885
+ /* ---- 历史浏览器(设置页:按今天/昨天/更早分组折叠) ---- */
886
+ function HistoryBrowser(props) {
887
+ var recent = props.recent || [];
888
+ var selected = props.selected;
889
+ var onSelect = props.onSelect;
890
+ var collapsedPair = React.useState({});
891
+ var collapsed = collapsedPair[0];
892
+ var setCollapsed = collapsedPair[1];
893
+ if (!recent.length) return React.createElement('div', { className: 'iris-wb-empty' }, '尚无任务记录');
894
+ var buckets = { 今天: [], 昨天: [], 更早: [] };
895
+ recent.forEach(function (t) { buckets[dayBucket(t.createdAt)].push(t); });
896
+ var order = ['今天', '昨天', '更早'];
897
+ return React.createElement('div', {}, order.map(function (name) {
898
+ var rows = buckets[name];
899
+ if (!rows.length) return null;
900
+ var isCollapsed = !!collapsed[name];
901
+ return React.createElement('div', { key: name, className: 'iris-hist-group' },
902
+ React.createElement('div', {
903
+ className: 'iris-hist-head',
904
+ onClick: function () { var n = {}; Object.keys(collapsed).forEach(function (k) { n[k] = collapsed[k]; }); n[name] = !isCollapsed; setCollapsed(n); }
905
+ }, React.createElement('span', {}, isCollapsed ? '▶' : '▼'), name + ' · ' + rows.length),
906
+ isCollapsed ? null : React.createElement('div', { className: 'iris-wb-box' },
907
+ rows.map(function (t) { return taskRow(t, false, selected === t.id, onSelect); })));
908
+ }));
909
+ }
910
+
911
+ /* ---- 泡泡常用卡片选择器(设置页:多项勾选 → 出现在悬浮窗「常用」标签) ---- */
912
+ function BubbleCardPicker() {
913
+ var pair = useBubbleCards();
914
+ var selected = pair[0];
915
+ var save = pair[1];
916
+ function toggle(action) {
917
+ var next = selected.indexOf(action) >= 0
918
+ ? selected.filter(function (a) { return a !== action; })
919
+ : selected.concat([action]);
920
+ save(next);
921
+ }
922
+ return React.createElement('div', { className: 'iris-pick-grid' }, CARD_DEFS.map(function (d) {
923
+ return React.createElement('label', { key: d.action, className: 'iris-pick-item' },
924
+ React.createElement('input', { type: 'checkbox', checked: selected.indexOf(d.action) >= 0, onChange: function () { toggle(d.action); } }),
925
+ d.title);
926
+ }));
927
+ }
928
+
929
+ /* ---- 清理区(设置页:删记录/清孤儿产物,破坏性操作二次确认) ---- */
930
+ function CleanupBar() {
931
+ var state = useIrisState(5000);
932
+ var total = (state && state.tasks && state.tasks.recentTotal) || 0;
933
+ var notePair = React.useState('');
934
+ var note = notePair[0];
935
+ var setNote = notePair[1];
936
+ function run(action, body, confirmMsg) {
937
+ if (confirmMsg && !window.confirm(confirmMsg)) return;
938
+ postAction(action, body || {}).then(function (r) {
939
+ setNote(r.ok ? (r.d.text || '完成') : ('失败:' + ((r.d && r.d.error) || '')));
940
+ });
941
+ }
942
+ return React.createElement('div', { className: 'iris-clean' },
943
+ React.createElement('span', { className: 'iris-clean-note' }, '共 ' + total + ' 条终态记录'),
944
+ React.createElement('button', { className: 'iris-clean-btn', onClick: function () { run('tasks_clear', { scope: 'completed' }, '删除全部终态任务记录?产物文件保留在 outputs/。'); } }, '清空已完成'),
945
+ React.createElement('button', { className: 'iris-clean-btn', onClick: function () { run('tasks_clear', { scope: 'older_than', days: 7 }, '删除 7 天前的任务记录?'); } }, '清理 7 天前'),
946
+ React.createElement('button', { className: 'iris-clean-btn', onClick: function () { run('tasks_orphans'); } }, '扫描孤儿产物'),
947
+ React.createElement('button', { className: 'iris-clean-btn danger', onClick: function () { run('tasks_purge_orphans', {}, '删除所有无任务引用的产物文件?此操作不可逆。'); } }, '删除孤儿产物'),
948
+ note ? React.createElement('div', { className: 'iris-clean-note', style: { width: '100%' } }, note) : null);
949
+ }
950
+
951
+ function WorkbenchPanel() {
952
+ var state = useIrisState(5000);
953
+ var running = (state && state.tasks && state.tasks.running) || [];
954
+ var recent = (state && state.tasks && state.tasks.recent) || [];
955
+ var selPair = React.useState(null);
956
+ var selectedTask = selPair[0];
957
+ var setSelectedTask = selPair[1];
958
+ function onSelect(id) { setSelectedTask(id); }
959
+ return React.createElement('div', { className: 'iris-wb' },
960
+ React.createElement('div', { className: 'iris-wb-head' },
961
+ React.createElement('span', { className: 'iris-wb-title' }, 'Iris 工作台'),
962
+ React.createElement('span', { className: 'iris-wb-date' }, state ? '刷新于 ' + new Date().toLocaleTimeString() : '加载中…')),
963
+ React.createElement('div', { className: 'iris-wb-sec' }, '供应商'),
964
+ React.createElement(ProviderManager, {}),
965
+ React.createElement('div', { className: 'iris-wb-sec' }, '能力分配(failover 顺序)'),
966
+ React.createElement(CapabilityAssigner, {}),
967
+ React.createElement('div', { className: 'iris-wb-sec' }, 'Iris 泡泡快捷卡片(勾选后显示在「⚡ 常用」标签)'),
968
+ React.createElement(BubbleCardPicker, {}),
969
+ React.createElement('div', { className: 'iris-wb-sec' }, '运行中任务'),
970
+ React.createElement('div', { className: 'iris-wb-box' },
971
+ running.length ? running.map(function (t) { return taskRow(t, true, selectedTask === t.id, onSelect); })
972
+ : React.createElement('div', { className: 'iris-wb-empty' }, '暂无进行中任务')),
973
+ React.createElement('div', { className: 'iris-wb-sec' }, '历史任务'),
974
+ React.createElement(CleanupBar, {}),
975
+ React.createElement(HistoryBrowser, { recent: recent, selected: selectedTask, onSelect: onSelect }),
976
+ React.createElement('div', { className: 'iris-wb-sec' }, '操作'),
977
+ React.createElement(ActionGroups, {}));
978
+ }
979
+
980
+ /* ---- 泡泡浮层(阶段 4 续:两标签,尽量干净)---- */
981
+ function BubblePanel() {
982
+ var state = useIrisState(5000);
983
+ var running = (state && state.tasks && state.tasks.running) || [];
984
+ var recent = (state && state.tasks && state.tasks.recent) || [];
985
+ var recentTotal = (state && state.tasks && state.tasks.recentTotal) || recent.length;
986
+ var cards = useBubbleCards()[0];
987
+ var tabPair = React.useState('tasks');
988
+ var tab = tabPair[0];
989
+ var setTab = tabPair[1];
990
+ var selPair = React.useState(null);
991
+ var selectedTask = selPair[0];
992
+ var setSelectedTask = selPair[1];
993
+ function onSelect(id) { setSelectedTask(id); }
994
+ var body;
995
+ if (tab === 'tasks') {
996
+ var shown = recent.slice(0, 6);
997
+ body = React.createElement('div', { className: 'iris-wb-box' },
998
+ running.length ? React.createElement('div', {},
999
+ React.createElement('div', { className: 'iris-wb-sec' }, '运行中'),
1000
+ running.map(function (t) { return taskRow(t, true, selectedTask === t.id, onSelect); })) : null,
1001
+ React.createElement('div', { className: 'iris-wb-sec' }, '最近'),
1002
+ shown.length ? shown.map(function (t) { return taskRowMini(t, selectedTask === t.id, onSelect); })
1003
+ : React.createElement('div', { className: 'iris-wb-empty' }, '尚无任务记录'),
1004
+ recent.length > 6 || recentTotal > recent.length
1005
+ ? React.createElement('div', { className: 'iris-bubble-more', style: { cursor: 'default' } },
1006
+ '共 ' + recentTotal + ' 条 · 完整历史与清理见设置页') : null);
1007
+ } else {
1008
+ var defs = CARD_DEFS.filter(function (d) { return cards.indexOf(d.action) >= 0; });
1009
+ body = defs.length
1010
+ ? React.createElement('div', { className: 'iris-act-group' }, defs.map(renderCard))
1011
+ : React.createElement('div', { className: 'iris-wb-empty' }, '还没有常用卡片——去设置页勾选');
1012
+ }
1013
+ return React.createElement('div', {},
1014
+ React.createElement('div', { className: 'iris-bubble-tabs' },
1015
+ React.createElement('button', { className: 'iris-bubble-tab' + (tab === 'tasks' ? ' active' : ''), onClick: function () { setTab('tasks'); } }, '📋 任务'),
1016
+ React.createElement('button', { className: 'iris-bubble-tab' + (tab === 'cards' ? ' active' : ''), onClick: function () { setTab('cards'); } }, '⚡ 常用' + (cards.length ? ' (' + cards.length + ')' : ''))),
1017
+ body,
1018
+ selectedTask ? React.createElement(TaskDetailDrawer, { taskId: selectedTask }) : null);
1019
+ }
1020
+
1021
+ function ProgressDock() {
1022
+ var state = useIrisState(5000);
1023
+ var running = (state && state.tasks && state.tasks.running) || [];
1024
+ if (!running.length) return null;
1025
+ return React.createElement('div', { className: 'iris-progress-dock' },
1026
+ running.map(function (t) {
1027
+ return React.createElement('div', { key: t.id, className: 'iris-progress-row' },
1028
+ React.createElement('span', {}, '🫧 ' + t.cap),
1029
+ React.createElement('span', {}, t.model || ''),
1030
+ React.createElement('span', {}, String(t.progress || '')));
1031
+ }));
1032
+ }
1033
+
1034
+ function FloatingBubble() {
1035
+ var state = useIrisState(5000);
1036
+ var providers = (state && state.providers) || [];
1037
+ var configured = providers.some(function (p) { return p.enabled && p.apiKeyHint; });
1038
+ var running = (state && state.tasks && state.tasks.running) || [];
1039
+
1040
+ var posPair = React.useState(function () {
1041
+ var saved = null;
1042
+ try { saved = JSON.parse(localStorage.getItem('iris-bubble-pos') || 'null'); } catch (_) {}
1043
+ return saved && typeof saved.x === 'number' && typeof saved.y === 'number'
1044
+ ? saved : { x: null, y: null };
1045
+ });
1046
+ var pos = posPair[0];
1047
+ var setPos = posPair[1];
1048
+ var openPair = React.useState(false);
1049
+ var open = openPair[0];
1050
+ var setOpen = openPair[1];
1051
+ var dragRef = React.useRef(null);
1052
+
1053
+ var DEFAULT_X = 70, DEFAULT_Y = 170; // 距右/下(CSS 默认角)
1054
+
1055
+ function bubbleXY() {
1056
+ return {
1057
+ x: pos.x != null ? pos.x : (window.innerWidth - DEFAULT_X),
1058
+ y: pos.y != null ? pos.y : (window.innerHeight - DEFAULT_Y)
1059
+ };
1060
+ }
1061
+
1062
+ function onPointerDown(e) {
1063
+ if (e.button !== 0 && e.pointerType === 'mouse') return;
1064
+ // 面板内部交互不触发外层拖动/开关:只有点在泡泡按钮上才起拖
1065
+ if (!e.target || !e.target.closest || !e.target.closest('.iris-bubble-btn')) return;
1066
+ var target = e.currentTarget;
1067
+ try { target.setPointerCapture(e.pointerId); } catch (_) {}
1068
+ var xy = bubbleXY();
1069
+ dragRef.current = { px: e.clientX, py: e.clientY, moved: false, ox: xy.x, oy: xy.y };
1070
+ }
1071
+
1072
+ // 窗口变化后把泡泡约束回可视区(同步持久化)
1073
+ React.useEffect(function () {
1074
+ function onResize() {
1075
+ setPos(function (old) {
1076
+ if (!old || old.x == null || old.y == null) return old;
1077
+ var nx = Math.min(window.innerWidth - 52, Math.max(6, old.x));
1078
+ var ny = Math.min(window.innerHeight - 52, Math.max(6, old.y));
1079
+ if (nx !== old.x || ny !== old.y) {
1080
+ var np = { x: nx, y: ny };
1081
+ try { localStorage.setItem('iris-bubble-pos', JSON.stringify(np)); } catch (_) {}
1082
+ return np;
1083
+ }
1084
+ return old;
1085
+ });
1086
+ }
1087
+ window.addEventListener('resize', onResize);
1088
+ return function () { window.removeEventListener('resize', onResize); };
1089
+ }, []);
1090
+
1091
+ function onPointerMove(e) {
1092
+ var drag = dragRef.current;
1093
+ if (!drag) return;
1094
+ var dx = e.clientX - drag.px;
1095
+ var dy = e.clientY - drag.py;
1096
+ if (!drag.moved && Math.abs(dx) + Math.abs(dy) > 5) drag.moved = true;
1097
+ if (drag.moved) {
1098
+ var nx = Math.min(window.innerWidth - 52, Math.max(6, drag.ox + dx));
1099
+ var ny = Math.min(window.innerHeight - 52, Math.max(6, drag.oy + dy));
1100
+ setPos({ x: nx, y: ny });
1101
+ }
1102
+ }
1103
+
1104
+ function onPointerUp(e) {
1105
+ var drag = dragRef.current;
1106
+ if (!drag) return;
1107
+ try { e.currentTarget.releasePointerCapture(e.pointerId); } catch (_) {}
1108
+ var moved = drag.moved;
1109
+ dragRef.current = null;
1110
+ if (moved) {
1111
+ try { localStorage.setItem('iris-bubble-pos', JSON.stringify(pos)); } catch (_) {}
1112
+ } else {
1113
+ setOpen(!open);
1114
+ }
1115
+ }
1116
+
1117
+ var style;
1118
+ if (pos.x != null && pos.y != null) {
1119
+ style = { left: pos.x + 'px', top: pos.y + 'px' };
1120
+ } else {
1121
+ style = { right: DEFAULT_X + 'px', bottom: DEFAULT_Y + 'px' };
1122
+ }
1123
+
1124
+ var badge = running.length
1125
+ ? React.createElement('span', { className: 'iris-bubble-badge' }, running.length)
1126
+ : null;
1127
+
1128
+ var panel = null;
1129
+ if (open) {
1130
+ var xy = bubbleXY();
1131
+ var panelLeft = Math.max(8, xy.x - 330);
1132
+ var panelTop = Math.max(8, Math.min(xy.y - 260, window.innerHeight - 520));
1133
+ panel = React.createElement('div', { className: 'iris-bubble-panel', style: { left: panelLeft + 'px', top: panelTop + 'px' } },
1134
+ React.createElement('div', { className: 'iris-bubble-panel-head' },
1135
+ React.createElement('button', { className: 'iris-bubble-close', onClick: function () { setOpen(false); } }, '✕')),
1136
+ React.createElement(BubblePanel, {}));
1137
+ }
1138
+
1139
+ return React.createElement('div', {
1140
+ className: 'iris-bubble' + (configured ? ' lit' : ' dim') + (dragRef.current ? ' dragging' : ''),
1141
+ style: style,
1142
+ onPointerDown: onPointerDown,
1143
+ onPointerMove: onPointerMove,
1144
+ onPointerUp: onPointerUp,
1145
+ title: configured ? 'Iris 已就绪:点击打开快捷面板' : 'Iris 尚未配置:点击打开快捷面板'
1146
+ },
1147
+ React.createElement('div', { className: 'iris-bubble-btn' }, '🫧'),
1148
+ badge,
1149
+ panel);
1150
+ }
1151
+
1152
+ ctx.slots.inject('settings.section', function () {
1153
+ return ctx.slots.register({
1154
+ name: 'settings.section',
1155
+ id: 'iris-workbench',
1156
+ order: 140,
1157
+ label: function () { return 'Iris 工作台'; }
1158
+ }, WorkbenchPanel);
1159
+ });
1160
+
1161
+ ctx.slots.inject('conversation.input.dock', function () {
1162
+ return ctx.slots.register({
1163
+ name: 'conversation.input.dock',
1164
+ id: 'iris-progress',
1165
+ order: 0,
1166
+ label: function () { return 'Iris 任务进度'; }
1167
+ }, ProgressDock);
1168
+ });
1169
+
1170
+ ctx.slots.inject('shell.overlay', function () {
1171
+ return ctx.slots.register({
1172
+ name: 'shell.overlay',
1173
+ id: 'iris-bubble',
1174
+ order: 200,
1175
+ label: function () { return 'Iris 泡泡'; }
1176
+ }, FloatingBubble);
1177
+ });
1178
+ console.log('[iris] client slots registered');
1179
+ } catch (err) {
1180
+ console.error('[iris] client apply failed:', err && err.message);
1181
+ throw err;
1182
+ }
1183
+ }
1184
+
1185
+ module.exports = { inject: inject, apply: apply };
1186
+ return module.exports;
1187
+ }});