dsh-plugin-yanyan-toolkit 0.0.0-stage → 1.2.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/LICENSE +29 -0
  3. package/README.i18n.yaml +6 -0
  4. package/README.md +65 -2
  5. package/README.zh.md +66 -0
  6. package/cordis.patch.yml +35 -0
  7. package/icon.svg +8 -0
  8. package/lib/client.js +26 -0
  9. package/lib/index.js +44 -0
  10. package/locale/en.json +6 -0
  11. package/locale/zh.json +6 -0
  12. package/package.json +79 -4
  13. package/packages/dsh-plugin-yanyan-subagent/README.md +72 -0
  14. package/packages/dsh-plugin-yanyan-subagent/README.zh.md +72 -0
  15. package/packages/dsh-plugin-yanyan-subagent/cordis.patch.yml +8 -0
  16. package/packages/dsh-plugin-yanyan-subagent/icon.svg +8 -0
  17. package/packages/dsh-plugin-yanyan-subagent/lib/client.js +1250 -0
  18. package/packages/dsh-plugin-yanyan-subagent/lib/index.js +951 -0
  19. package/packages/dsh-plugin-yanyan-subagent/lib/legacy-hidden-ids.json +9 -0
  20. package/packages/dsh-plugin-yanyan-subagent/lib/log-rewrite.js +382 -0
  21. package/packages/dsh-plugin-yanyan-subagent/lib/zstd-frames.js +81 -0
  22. package/packages/dsh-plugin-yanyan-subagent/locale/en.json +1 -0
  23. package/packages/dsh-plugin-yanyan-subagent/locale/zh.json +1 -0
  24. package/packages/dsh-plugin-yanyan-subagent/package.json +32 -0
  25. package/packages/dsh-plugin-yanyan-update/README.md +24 -0
  26. package/packages/dsh-plugin-yanyan-update/README.zh.md +24 -0
  27. package/packages/dsh-plugin-yanyan-update/cordis.patch.yml +7 -0
  28. package/packages/dsh-plugin-yanyan-update/icon.svg +5 -0
  29. package/packages/dsh-plugin-yanyan-update/lib/client.js +400 -0
  30. package/packages/dsh-plugin-yanyan-update/lib/index.js +840 -0
  31. package/packages/dsh-plugin-yanyan-update/locale/en.json +1 -0
  32. package/packages/dsh-plugin-yanyan-update/locale/zh.json +1 -0
  33. package/packages/dsh-plugin-yanyan-update/package.json +32 -0
  34. package/scripts/cleanup-ghost-subagents.mjs +241 -0
@@ -0,0 +1,1250 @@
1
+ // dsh-plugin-yanyan-subagent — Client half(纯 JavaScript,无 JSX、无构建工具)。
2
+ //
3
+ // 装载方式照抄 dsh-plugin-wechat-session/lib/client.js:47-60:通过 __ModuleLoader__
4
+ // 注册一个 factory,宿主用 require('react') / require('react-dom') 提供运行时。
5
+ // 拿不到 react 就降级成纯 DOM 注入;拿不到 react-dom 就把浮层 div 直接 append
6
+ // 到 document.body(官方 header.actions 槽会裁剪溢出内容,浮层必须脱离该容器)。
7
+ //
8
+ // 所有 UI 文案固定走内部中文(UI_zh);en 字典只用于满足 ctx.locale.register
9
+ // 「zh 为 key 源、en 键集完整」的注册契约,不参与实际显示,保证中文 UI。
10
+ //
11
+ // 行尾删除按钮文字使用 U+2715(乘号,普通 Unicode 数学符号,非 emoji),
12
+ // 并带中文 aria-label / title;语义属性 data-dsh-plugin + data-dsh-part 全局覆盖。
13
+
14
+ (function (root) {
15
+ if (!root) return;
16
+ if (!root.__ModuleLoader__ || typeof root.__ModuleLoader__.load !== 'function') return;
17
+
18
+ root.__ModuleLoader__.load({
19
+ id: 'dsh-plugin-yanyan-subagent',
20
+ factory: function (require) {
21
+ var module = { exports: {} };
22
+ var exports = module.exports;
23
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
24
+
25
+ var React = null;
26
+ try { React = require('react') || null; } catch (e) { React = null; }
27
+ var ReactDOM = null;
28
+ try { ReactDOM = require('react-dom') || null; } catch (e) { ReactDOM = null; }
29
+
30
+ var doc = root.document || null;
31
+ var NS = 'yanyan-subagent';
32
+ var ROUTE_BASE = '/__dsh-plugin-yanyan-subagent';
33
+ var DEFAULT_TAB_LABEL = '智能体管理';
34
+ var X_MARK = '\u2715'; // U+2715 MULTIPLICATION X,非 emoji
35
+
36
+ // localStorage 键(改名 / 自动清理设置持久化)。
37
+ var LS_LABEL = 'yanyan-subagent.tabLabel';
38
+ var LS_AUTO = 'yanyan-subagent.autoCleanEnabled';
39
+ var LS_IDLE = 'yanyan-subagent.idleMinutes';
40
+ var LS_INTERVAL = 'yanyan-subagent.checkIntervalMin';
41
+ // 已彻底删除的 id 清单。subagentCatalog 由父会话日志的 subagent/catalog
42
+ // 事件 append-only 重建(官方 dsh-chunked-list 仅追加),删目录/归档都
43
+ // 无法移除引用:宿主下次 refreshProjections 会把条目带回来。只有把 id
44
+ // 记在浏览器本地、在渲染与清理时持续过滤,才能让删除真正生效。
45
+ var LS_DELETED = 'yanyan-subagent.deletedIds';
46
+
47
+ // ───────────────── 字典(zh 为 key 源,en 键集完整对照) ─────────────────
48
+ var UI_zh = {
49
+ tab: '智能体管理',
50
+ title: '子智能体管理',
51
+ empty: '当前会话暂无子智能体',
52
+ idle: '空闲',
53
+ running: '运行中',
54
+ delete: '删除',
55
+ deleting: '删除中…',
56
+ deleted: '已删除',
57
+ cleanIdle: '清理全部空闲',
58
+ cleaning: '清理中…',
59
+ cleanDone: '已清理 {count} 个空闲子智能体',
60
+ failed: '失败',
61
+ close: '关闭',
62
+ rename: '重命名标签',
63
+ save: '保存',
64
+ autoClean: '自动清理空闲子智能体',
65
+ idleMinutes: '空闲超过(分钟)',
66
+ confirmDelete: '确定彻底删除该子智能体?此操作不可逆,将删除其会话数据并从列表移除。',
67
+ cancel: '取消',
68
+ settings: '设置',
69
+ refresh: '刷新',
70
+ tabRenameHint: '输入新的标签名称',
71
+ unknown: '未知',
72
+ unsupported: '浏览器数据不可用',
73
+ // 扩展键(en 必须逐键对照)。
74
+ confirmDeleteRunning: '该子智能体正在运行,将停止并彻底删除(不可逆)。确定继续?',
75
+ officialNoMatch: '无法定位该子智能体,请在智能体管理标签中删除',
76
+ idleThreshold: '空闲阈值',
77
+ minutes: '分钟',
78
+ saved: '已保存',
79
+ diagnostic: '诊断',
80
+ root: '根会话',
81
+ noRoot: '未找到当前会话',
82
+ stopHint: '运行中,删除将停止该子智能体',
83
+ checkInterval: '巡检间隔(分钟)',
84
+ autoCleanHint: '开启后按巡检间隔自动清理超过阈值的空闲子智能体',
85
+ scrubResidual: '清理残留行',
86
+ scrubbing: '正在根除残留行…',
87
+ scrubDone: '已根除 {count} 条残留目录行,侧边栏已同步刷新',
88
+ scrubNoResidual: '没有发现需要清理的残留行',
89
+ scrubFailed: '清理残留行失败',
90
+ catalogRetired: '已彻底删除,残留目录行已从会话日志根除',
91
+ catalogQueued: '已删除;日志改写遇到并发写入,已排队下次启动补做',
92
+ };
93
+ var UI_en = {
94
+ tab: 'Agent Manager',
95
+ title: 'Subagent Management',
96
+ empty: 'No subagents in this session',
97
+ idle: 'Idle',
98
+ running: 'Running',
99
+ delete: 'Delete',
100
+ deleting: 'Deleting...',
101
+ deleted: 'Deleted',
102
+ cleanIdle: 'Clean All Idle',
103
+ cleaning: 'Cleaning...',
104
+ cleanDone: 'Cleaned {count} idle subagents',
105
+ failed: 'Failed',
106
+ close: 'Close',
107
+ rename: 'Rename Tab',
108
+ save: 'Save',
109
+ autoClean: 'Auto-clean idle subagents',
110
+ idleMinutes: 'Idle over (minutes)',
111
+ confirmDelete: 'Delete this subagent permanently? This is irreversible: its session data is removed and it disappears from the list.',
112
+ cancel: 'Cancel',
113
+ settings: 'Settings',
114
+ refresh: 'Refresh',
115
+ tabRenameHint: 'Enter a new tab name',
116
+ unknown: 'Unknown',
117
+ unsupported: 'Browser data unavailable',
118
+ confirmDeleteRunning: 'This subagent is running and will be stopped and deleted permanently (irreversible). Continue?',
119
+ officialNoMatch: 'Cannot locate this subagent; delete it in the Agent Manager tab',
120
+ idleThreshold: 'Idle threshold',
121
+ minutes: 'min',
122
+ saved: 'Saved',
123
+ diagnostic: 'Diagnostic',
124
+ root: 'Root session',
125
+ noRoot: 'Current session not found',
126
+ stopHint: 'Running; deletion will stop this subagent',
127
+ checkInterval: 'Check interval (minutes)',
128
+ autoCleanHint: 'When enabled, idle subagents over the threshold are cleaned at each check',
129
+ scrubResidual: 'Clean residual rows',
130
+ scrubbing: 'Removing residual rows...',
131
+ scrubDone: 'Removed {count} residual catalog rows; sidebar refreshed',
132
+ scrubNoResidual: 'No residual rows to clean',
133
+ scrubFailed: 'Failed to clean residual rows',
134
+ catalogRetired: 'Deleted permanently; residual catalog row removed from the session log',
135
+ catalogQueued: 'Deleted; log rewrite hit a concurrent write and is queued for next start',
136
+ };
137
+
138
+ // 固定中文翻译;支持 {name} 占位符替换(cleanDone 的 {count})。
139
+ function t(key, params) {
140
+ var s = Object.prototype.hasOwnProperty.call(UI_zh, key) ? UI_zh[key] : key;
141
+ if (params) {
142
+ for (var k in params) {
143
+ if (!Object.prototype.hasOwnProperty.call(params, k)) continue;
144
+ s = s.split('{' + k + '}').join(String(params[k]));
145
+ }
146
+ }
147
+ return s;
148
+ }
149
+
150
+ // ───────────────── 纯 helpers ─────────────────
151
+ function clampInt(v, min, max, fallback) {
152
+ var n = Number(v);
153
+ if (!Number.isFinite(n)) return fallback;
154
+ n = Math.floor(n);
155
+ if (n < min || n > max) return fallback;
156
+ return n;
157
+ }
158
+
159
+ // catalog 兼容解析:数组 / {entries:[]} / {rows:[]} / 空。
160
+ // 磁盘 projcache 里的 chunked-list 条目字段是 `childId`,而客户端
161
+ // session store 的投影通常已转成 `id`;这里统一补齐 id,保证删除按钮、
162
+ // 隐藏清单过滤、根会话判定在任何数据源下都拿到同一个主键。
163
+ function parseCatalogLocal(raw) {
164
+ var isRow = function (x) { return !!x && typeof x === 'object' && !Array.isArray(x); };
165
+ var list;
166
+ if (Array.isArray(raw)) list = raw.filter(isRow);
167
+ else if (raw && typeof raw === 'object') {
168
+ if (Array.isArray(raw.entries)) list = raw.entries.filter(isRow);
169
+ else if (Array.isArray(raw.rows)) list = raw.rows.filter(isRow);
170
+ else list = [];
171
+ } else list = [];
172
+ var out = [];
173
+ for (var i = 0; i < list.length; i++) {
174
+ var r = list[i];
175
+ if (!r.id && r.childId) r.id = r.childId;
176
+ if (!r.childId && r.id) r.childId = r.id;
177
+ out.push(r);
178
+ }
179
+ return out;
180
+ }
181
+
182
+ // 空闲清理筛选(与 host 的 idleFilter 同逻辑,供自动清理 / 批量清理复用)。
183
+ function idleFilterLocal(rows, options) {
184
+ var opts = options || {};
185
+ var rootId = opts.rootId;
186
+ var idleMinutes = typeof opts.idleMinutes === 'number' && Number.isFinite(opts.idleMinutes)
187
+ ? opts.idleMinutes : 30;
188
+ var now = typeof opts.now === 'number' && Number.isFinite(opts.now) ? opts.now : Date.now();
189
+ var since = typeof opts.idleSince === 'function' ? opts.idleSince : function () { return now; };
190
+ var threshold = idleMinutes > 0 ? idleMinutes * 60 * 1000 : 0;
191
+ var out = [];
192
+ var list = Array.isArray(rows) ? rows : [];
193
+ for (var i = 0; i < list.length; i++) {
194
+ var row = list[i];
195
+ if (!row || typeof row !== 'object' || Array.isArray(row)) continue;
196
+ if (row.kind === 'diagnostic') continue;
197
+ if (row.activity !== 'inactive') continue;
198
+ if (rootId != null && row.id === rootId) continue;
199
+ var started = since(row.id);
200
+ var base = typeof started === 'number' && Number.isFinite(started) ? started : now;
201
+ if (threshold > 0 && (now - base) < threshold) continue;
202
+ out.push(row);
203
+ }
204
+ return out;
205
+ }
206
+
207
+ function readLS(key) {
208
+ try { return root.localStorage.getItem(key); } catch (e) { return null; }
209
+ }
210
+ function writeLS(key, val) {
211
+ try { root.localStorage.setItem(key, String(val)); } catch (e) {}
212
+ }
213
+
214
+ // ───────────────── 已删除 id 的持久化隐藏清单 ─────────────────
215
+ // catalog 的 childId 可能带或不带 `session-` 前缀(同一会话在父 catalog
216
+ // 里是裸 uuid,在归档清单里是 session-uuid)。用「去前缀后的核心 id」
217
+ // 做主键,保证两种写法都命中。
218
+ function coreId(id) {
219
+ return String(id || '').replace(/^session-/, '');
220
+ }
221
+ function readDeletedIds() {
222
+ try {
223
+ var raw = readLS(LS_DELETED);
224
+ var arr = raw ? JSON.parse(raw) : [];
225
+ if (!Array.isArray(arr)) return [];
226
+ return arr.filter(function (x) { return typeof x === 'string' && x; }).map(coreId);
227
+ } catch (e) { return []; }
228
+ }
229
+ function writeDeletedIds(list) {
230
+ // 去重 + 限定上限,避免 localStorage 无限增长。
231
+ var seen = {}, out = [];
232
+ for (var i = 0; i < list.length; i++) {
233
+ var c = coreId(list[i]);
234
+ if (!c || seen[c]) continue;
235
+ seen[c] = true; out.push(c);
236
+ }
237
+ if (out.length > 500) out = out.slice(out.length - 500);
238
+ writeLS(LS_DELETED, JSON.stringify(out));
239
+ }
240
+ function markDeleted(id) {
241
+ var list = readDeletedIds();
242
+ var c = coreId(id);
243
+ if (c && list.indexOf(c) === -1) { list.push(c); writeDeletedIds(list); }
244
+ }
245
+ function isDeleted(id) {
246
+ var c = coreId(id);
247
+ if (!c) return false;
248
+ return readDeletedIds().indexOf(c) !== -1;
249
+ }
250
+ // 从 catalog 行集合里剔除已删除条目,供渲染 / 批量清理 / 自动清理复用。
251
+ function filterDeleted(rows) {
252
+ var list = Array.isArray(rows) ? rows : [];
253
+ var out = [];
254
+ for (var i = 0; i < list.length; i++) {
255
+ var r = list[i];
256
+ if (!r || typeof r !== 'object') continue;
257
+ if (r.id && isDeleted(r.id)) continue;
258
+ if (r.childId && isDeleted(r.childId)) continue;
259
+ out.push(r);
260
+ }
261
+ return out;
262
+ }
263
+
264
+ // 服务懒查:一律走 ctx.get / ctx.reflect.get / ctx[name],不读未 inject 的属性。
265
+ var rootCtx = null;
266
+ function lookup(name) {
267
+ if (!rootCtx) return null;
268
+ try { var v = rootCtx.get(name); if (v) return v; } catch (e) {}
269
+ try {
270
+ if (rootCtx.reflect && typeof rootCtx.reflect.get === 'function') {
271
+ var v2 = rootCtx.reflect.get(name); if (v2) return v2;
272
+ }
273
+ } catch (e2) {}
274
+ try { var v3 = rootCtx[name]; if (v3) return v3; } catch (e3) {}
275
+ return null;
276
+ }
277
+
278
+ function postJson(path, body) {
279
+ var f = root.fetch || (typeof fetch !== 'undefined' ? fetch : null);
280
+ if (!f) return Promise.reject(new Error('fetch-unavailable'));
281
+ return f.call(root, path, {
282
+ method: 'POST',
283
+ headers: { 'content-type': 'application/json' },
284
+ body: JSON.stringify(body),
285
+ }).then(function (r) { return r.json(); });
286
+ }
287
+
288
+ // ───────────────── 共享状态 ─────────────────
289
+ var shared = {
290
+ tabLabel: DEFAULT_TAB_LABEL,
291
+ autoCleanEnabled: false,
292
+ idleMinutes: 30,
293
+ checkIntervalMin: 1,
294
+ renameDraft: null,
295
+ autoDraft: { autoCleanEnabled: false, idleMinutes: 30, checkIntervalMin: 1 },
296
+ rows: [],
297
+ rootId: '',
298
+ unsupported: false,
299
+ firstSeen: new Map(), // id -> 首次观察到 inactive 的时刻
300
+ rowStatus: {}, // id -> {state:'deleting'|'deleted'|'failed', message?}
301
+ cleanStatus: '',
302
+ settingsStatus: '',
303
+ };
304
+
305
+ // 已挂载的浮层 view 列表;renderAll 统一刷新。
306
+ var views = [];
307
+ var nativeOverlay = null;
308
+ var nativeView = null;
309
+ var officialObserver = null;
310
+ var officialButtons = [];
311
+ var autoTimer = null;
312
+
313
+ function makeEmitter() {
314
+ var subs = [];
315
+ return {
316
+ subscribe: function (fn) {
317
+ subs.push(fn);
318
+ return function () { var i = subs.indexOf(fn); if (i >= 0) subs.splice(i, 1); };
319
+ },
320
+ emit: function (v) {
321
+ for (var i = 0; i < subs.length; i++) { try { subs[i](v); } catch (e) {} }
322
+ },
323
+ };
324
+ }
325
+ var labelEmitter = makeEmitter(); // 改名后通知顶部按钮刷新文字
326
+ var closeEmitter = makeEmitter(); // React 浮层关闭通知
327
+
328
+ function readTabLabel() { return shared.tabLabel || DEFAULT_TAB_LABEL; }
329
+
330
+ function renderAll() {
331
+ for (var i = 0; i < views.length; i++) { try { views[i].update(); } catch (e) {} }
332
+ }
333
+
334
+ // ───────────────── 数据面 ─────────────────
335
+ // rootId 推导:snap.byId 里 retainedBy.mainView > 0 的会话(照抄
336
+ // dsh-session-archive 的 mainViewSessionId 思路);找不到时遍历
337
+ // projectionsBySession 取有 subagentCatalog 的那个。
338
+ function deriveRootId(ctx) {
339
+ try {
340
+ var snap = ctx.sessions.list.getSnapshot();
341
+ var byId = snap && snap.byId;
342
+ if (byId) {
343
+ for (var k in byId) {
344
+ var row = byId[k];
345
+ if (row && row.retainedBy && (row.retainedBy.mainView || 0) > 0) return row.id || k;
346
+ }
347
+ }
348
+ var pbs = snap && snap.projectionsBySession;
349
+ if (pbs) {
350
+ for (var sid in pbs) {
351
+ var p = pbs[sid];
352
+ if (p && p.values && p.values.subagentCatalog != null) return sid;
353
+ }
354
+ }
355
+ } catch (e) {}
356
+ return '';
357
+ }
358
+
359
+ // 返回 null 表示本地投影里没有 subagentCatalog 键(需回退 host /list);
360
+ // 返回数组(含空数组)表示投影存在、数据可用。
361
+ function readLocalCatalog(ctx, rootId) {
362
+ try {
363
+ var list = ctx.sessions && ctx.sessions.list;
364
+ if (!list || typeof list.getSnapshot !== 'function') return null;
365
+ var snap = list.getSnapshot();
366
+ var pbs = snap && snap.projectionsBySession;
367
+ if (!pbs) return null;
368
+ var proj = rootId ? pbs[rootId] : null;
369
+ if (proj && proj.values && proj.values.subagentCatalog != null) {
370
+ return parseCatalogLocal(proj.values.subagentCatalog);
371
+ }
372
+ for (var sid in pbs) {
373
+ var p = pbs[sid];
374
+ if (p && p.values && p.values.subagentCatalog != null) {
375
+ return parseCatalogLocal(p.values.subagentCatalog);
376
+ }
377
+ }
378
+ return null;
379
+ } catch (e) { return null; }
380
+ }
381
+
382
+ function loadRows(ctx) {
383
+ var rootId = deriveRootId(ctx);
384
+ var local = readLocalCatalog(ctx, rootId);
385
+ if (local !== null) {
386
+ // append-only catalog 会被宿主重建,这里持续过滤已删除 id。
387
+ return Promise.resolve({ rootId: rootId, rows: filterDeleted(local), unsupported: false });
388
+ }
389
+ // 本地投影读不到:回退 host 侧兜底数据源。
390
+ return postJson(ROUTE_BASE + '/list', { rootSessionId: rootId })
391
+ .then(function (data) {
392
+ if (data && data.ok && Array.isArray(data.rows)) {
393
+ return { rootId: rootId, rows: filterDeleted(parseCatalogLocal(data.rows)), unsupported: false };
394
+ }
395
+ return { rootId: rootId, rows: [], unsupported: true };
396
+ })
397
+ .catch(function () {
398
+ return { rootId: rootId, rows: [], unsupported: true };
399
+ });
400
+ }
401
+
402
+ // 空闲起点:官方没有 idleSince / lastActiveAt 字段,用内存 Map 记首次观察到
403
+ // inactive 的时刻;变回 running 就删除该记录。
404
+ function updateIdleTracking(rows) {
405
+ var alive = {};
406
+ for (var i = 0; i < rows.length; i++) {
407
+ var r = rows[i];
408
+ if (!r || !r.id || r.kind === 'diagnostic') continue;
409
+ alive[r.id] = true;
410
+ if (r.activity === 'running') shared.firstSeen.delete(r.id);
411
+ else if (r.activity === 'inactive' && !shared.firstSeen.has(r.id)) {
412
+ shared.firstSeen.set(r.id, Date.now());
413
+ }
414
+ }
415
+ // 清理已消失的 id,避免 Map 无限增长。
416
+ shared.firstSeen.forEach(function (_v, id) {
417
+ if (!alive[id]) shared.firstSeen.delete(id);
418
+ });
419
+ }
420
+ function idleSince(id) {
421
+ return shared.firstSeen.has(id) ? shared.firstSeen.get(id) : Date.now();
422
+ }
423
+ function idleMs(id) { return Date.now() - idleSince(id); }
424
+
425
+ // 侧边栏同步刷新:只调 refreshProjections 不足以让「已归档 / 会话列表」里
426
+ // 的残留行消失,必须补 sessions.refresh()(借鉴 subagent-clean 的
427
+ // refreshParents),再 refreshProjections 让本会话目录按新日志重新 fold。
428
+ function refreshSidebar(ctx, rootId) {
429
+ try {
430
+ if (ctx.sessions && typeof ctx.sessions.refresh === 'function') {
431
+ ctx.sessions.refresh();
432
+ }
433
+ } catch (e) {}
434
+ try {
435
+ if (rootId && ctx.sessions && typeof ctx.sessions.refreshProjections === 'function') {
436
+ ctx.sessions.refreshProjections(rootId);
437
+ }
438
+ } catch (e2) {}
439
+ }
440
+
441
+ // trigger=false 表示「数据变化订阅回调」只重读快照并重渲染,不再主动请求
442
+ // 投影刷新:refreshProjections 本身会让 list 再次 fire,若在订阅回调里
443
+ // 无条件调用会形成自激循环。启动 / 打开浮层 / 手动刷新 / 删除后仍会
444
+ // 调用 refreshProjections(rootId) 触发刷新(存在则调,不存在跳过)。
445
+ function refresh(ctx, trigger) {
446
+ return loadRows(ctx).then(function (res) {
447
+ shared.rootId = res.rootId;
448
+ shared.rows = res.rows;
449
+ shared.unsupported = res.unsupported;
450
+ updateIdleTracking(res.rows);
451
+ if (trigger !== false) {
452
+ try {
453
+ if (ctx.sessions && typeof ctx.sessions.refreshProjections === 'function') {
454
+ ctx.sessions.refreshProjections(res.rootId);
455
+ }
456
+ } catch (e) {}
457
+ }
458
+ renderAll();
459
+ });
460
+ }
461
+
462
+ function loadSettings(ctx) {
463
+ var label = readLS(LS_LABEL);
464
+ shared.tabLabel = label || DEFAULT_TAB_LABEL;
465
+ var auto = readLS(LS_AUTO);
466
+ shared.autoCleanEnabled = auto === null ? false : (auto === 'true');
467
+ var idle = readLS(LS_IDLE);
468
+ shared.idleMinutes = idle === null ? 30 : clampInt(idle, 1, 10080, 30);
469
+ var interval = readLS(LS_INTERVAL);
470
+ shared.checkIntervalMin = interval === null ? 1 : clampInt(interval, 1, 1440, 1);
471
+ shared.autoDraft = {
472
+ autoCleanEnabled: shared.autoCleanEnabled,
473
+ idleMinutes: shared.idleMinutes,
474
+ checkIntervalMin: shared.checkIntervalMin,
475
+ };
476
+ // 读取 host /config 补齐本地从未存过的缺省值(标签文字等配置可读)。
477
+ return postJson(ROUTE_BASE + '/config', {})
478
+ .then(function (data) {
479
+ // 一次性迁移清单(旧版归档残留):并入持久化隐藏清单,
480
+ // 防止 append-only catalog 重建后这些幽灵条目重新出现。
481
+ // 放在 cfg 检查之前,保证即便 /config 只回清单也能生效。
482
+ if (data && Array.isArray(data.legacyHiddenIds)) {
483
+ for (var k = 0; k < data.legacyHiddenIds.length; k++) {
484
+ var lid = data.legacyHiddenIds[k];
485
+ if (typeof lid === 'string' && lid) markDeleted(lid);
486
+ }
487
+ }
488
+ var cfg = data && data.config;
489
+ if (!cfg) return;
490
+ if (label === null && typeof cfg.tabLabel === 'string' && cfg.tabLabel) shared.tabLabel = cfg.tabLabel;
491
+ if (auto === null && typeof cfg.autoCleanEnabled === 'boolean') {
492
+ shared.autoCleanEnabled = cfg.autoCleanEnabled;
493
+ shared.autoDraft.autoCleanEnabled = cfg.autoCleanEnabled;
494
+ }
495
+ if (idle === null && Number.isFinite(cfg.idleMinutes)) {
496
+ shared.idleMinutes = clampInt(cfg.idleMinutes, 1, 10080, 30);
497
+ shared.autoDraft.idleMinutes = shared.idleMinutes;
498
+ }
499
+ if (interval === null && Number.isFinite(cfg.checkIntervalMin)) {
500
+ shared.checkIntervalMin = clampInt(cfg.checkIntervalMin, 1, 1440, 1);
501
+ shared.autoDraft.checkIntervalMin = shared.checkIntervalMin;
502
+ }
503
+ // host 下发的一次性迁移清单(旧版归档残留):并入持久化隐藏清单,
504
+ // 防止 append-only catalog 重建后这些幽灵条目重新出现。
505
+ if (Array.isArray(data.legacyHiddenIds)) {
506
+ for (var k = 0; k < data.legacyHiddenIds.length; k++) {
507
+ var lid = data.legacyHiddenIds[k];
508
+ if (typeof lid === 'string' && lid) markDeleted(lid);
509
+ }
510
+ }
511
+ })
512
+ .catch(function () {});
513
+ }
514
+
515
+ // ───────────────── 删除 ─────────────────
516
+ // 单个删除改为「物理删除」:调 host /purge(复用现成 dsh-session-archive
517
+ // 的 /delete 物理删除管线 + host 侧清理 catalog 投影引用 / workspace 记账 /
518
+ // 归档台账 / mnemon 虚拟目录)。成功后把 id 写入本地隐藏清单并从列表移除,
519
+ // 因为 append-only catalog 会在下次刷新时被宿主从日志重建。
520
+ // 每行独立 try/catch 隔离;root 会话绝不删;running 传 stopActivity:true。
521
+ function deleteOne(ctx, row, stopActivity) {
522
+ var id = row && row.id;
523
+ if (!id || id === shared.rootId || !row || row.kind === 'diagnostic') return Promise.resolve();
524
+ shared.rowStatus[id] = { state: 'deleting' };
525
+ renderAll();
526
+ var body = {
527
+ sessionId: id,
528
+ rootSessionId: shared.rootId || '',
529
+ stopActivity: !!stopActivity,
530
+ };
531
+ return postJson(ROUTE_BASE + '/purge', body)
532
+ .then(function (data) {
533
+ if (!data || !data.ok) {
534
+ throw new Error((data && (data.message || data.error)) || 'purge-failed');
535
+ }
536
+ // 删除已落地:写入持久化隐藏清单,并从当前列表立即移除该行。
537
+ markDeleted(id);
538
+ shared.rows = filterDeleted(shared.rows);
539
+ shared.rowStatus[id] = { state: 'deleted' };
540
+ // host 已把父会话日志里的 catalog 行原位退休(append-only 根治),
541
+ // 用状态提示把这一结果反馈给用户,区别于「仅本地隐藏」。
542
+ if (data.catalogQueued) shared.rowStatus[id].hint = t('catalogQueued');
543
+ else if (data.catalogRetired > 0) shared.rowStatus[id].hint = t('catalogRetired');
544
+ })
545
+ .catch(function (e) {
546
+ shared.rowStatus[id] = { state: 'failed', message: String((e && e.message) || e) };
547
+ })
548
+ .then(function () {
549
+ renderAll();
550
+ // 侧边栏残留行同步消失:refresh() + refreshProjections() 一起调。
551
+ refreshSidebar(ctx, shared.rootId);
552
+ });
553
+ }
554
+
555
+ function onRowDelete(ctx, row) {
556
+ if (!row || !row.id || row.id === shared.rootId || row.kind === 'diagnostic') return;
557
+ var running = row.activity === 'running';
558
+ var msg = running ? t('confirmDeleteRunning') : t('confirmDelete');
559
+ var ok = true;
560
+ try { ok = root.confirm(msg); } catch (e) { ok = true; }
561
+ if (!ok) return;
562
+ deleteOne(ctx, row, running);
563
+ }
564
+
565
+ // 清理残留行:host /scrub 把「子会话目录已不在、catalog 行还在」的引用
566
+ // 从父会话日志原位退休;这是 append-only 根治后的一次性兜底,专治旧版
567
+ // 归档 / 重建遗留的幽灵行(对应图2/图3 里删不掉的条目)。
568
+ function scrubResidual(ctx) {
569
+ if (!shared.rootId) return Promise.resolve();
570
+ shared.cleanStatus = t('scrubbing');
571
+ renderAll();
572
+ return postJson(ROUTE_BASE + '/scrub', {
573
+ rootSessionId: shared.rootId,
574
+ everywhere: false,
575
+ }).then(function (data) {
576
+ if (!data || !data.ok) {
577
+ throw new Error((data && (data.message || data.error)) || 'scrub-failed');
578
+ }
579
+ var n = Number(data.removed) || 0;
580
+ shared.cleanStatus = n > 0 ? t('scrubDone', { count: n }) : t('scrubNoResidual');
581
+ // 残留目录行已从日志根除,刷新侧边栏令其同步消失。
582
+ refreshSidebar(ctx, shared.rootId);
583
+ return refresh(ctx, true);
584
+ }).catch(function (e) {
585
+ shared.cleanStatus = t('scrubFailed') + ': ' + String((e && e.message) || e);
586
+ renderAll();
587
+ });
588
+ }
589
+
590
+ function cleanAllIdle(ctx) {
591
+ var targets = idleFilterLocal(filterDeleted(shared.rows), {
592
+ rootId: shared.rootId,
593
+ idleMinutes: shared.idleMinutes,
594
+ now: Date.now(),
595
+ idleSince: idleSince,
596
+ });
597
+ shared.cleanStatus = targets.length ? t('cleaning') : '';
598
+ renderAll();
599
+ var chain = Promise.resolve();
600
+ var count = 0;
601
+ targets.forEach(function (r) {
602
+ chain = chain.then(function () {
603
+ return deleteOne(ctx, r, false).then(function () {
604
+ var st = shared.rowStatus[r.id];
605
+ if (st && st.state === 'deleted') count += 1;
606
+ });
607
+ });
608
+ });
609
+ return chain.then(function () {
610
+ shared.cleanStatus = t('cleanDone', { count: count });
611
+ renderAll();
612
+ // 批量清理后同样补 sessions.refresh(),侧边栏残留行同步消失。
613
+ refreshSidebar(ctx, shared.rootId);
614
+ });
615
+ }
616
+
617
+ // 自动清理巡检:live check 失败(unsupported)时 fail-closed 不删;
618
+ // running 绝不碰(idleFilterLocal 只选 inactive)。
619
+ function autoCleanTick(ctx) {
620
+ if (!shared.autoCleanEnabled) return;
621
+ return loadRows(ctx).then(function (res) {
622
+ if (res.unsupported || !res.rows.length) return; // fail-closed
623
+ shared.rootId = res.rootId;
624
+ shared.rows = filterDeleted(res.rows);
625
+ updateIdleTracking(shared.rows);
626
+ var targets = idleFilterLocal(shared.rows, {
627
+ rootId: res.rootId,
628
+ idleMinutes: shared.idleMinutes,
629
+ now: Date.now(),
630
+ idleSince: idleSince,
631
+ });
632
+ renderAll();
633
+ var chain = Promise.resolve();
634
+ targets.forEach(function (r) {
635
+ chain = chain.then(function () { return deleteOne(ctx, r, false); });
636
+ });
637
+ return chain.then(function () {
638
+ // 自动清理后同步刷新侧边栏(与手动删除 / 批量清理一致)。
639
+ refreshSidebar(ctx, res.rootId);
640
+ });
641
+ }).catch(function () {});
642
+ }
643
+
644
+ function reconfigureInterval(ctx) {
645
+ if (autoTimer) { try { root.clearInterval(autoTimer); } catch (e) {} autoTimer = null; }
646
+ var ms = Math.max(1, shared.checkIntervalMin) * 60 * 1000;
647
+ autoTimer = root.setInterval(function () { autoCleanTick(ctx); }, ms);
648
+ }
649
+
650
+ function saveSettings(ctx) {
651
+ var d = shared.autoDraft || {};
652
+ shared.autoCleanEnabled = !!d.autoCleanEnabled;
653
+ shared.idleMinutes = clampInt(d.idleMinutes, 1, 10080, shared.idleMinutes);
654
+ shared.checkIntervalMin = clampInt(d.checkIntervalMin, 1, 1440, shared.checkIntervalMin);
655
+ writeLS(LS_AUTO, shared.autoCleanEnabled);
656
+ writeLS(LS_IDLE, shared.idleMinutes);
657
+ writeLS(LS_INTERVAL, shared.checkIntervalMin);
658
+ shared.settingsStatus = t('saved');
659
+ reconfigureInterval(ctx); // 配置变更即时重排 interval
660
+ renderAll();
661
+ }
662
+
663
+ function saveTabLabel() {
664
+ var raw = (shared.renameDraft == null ? shared.tabLabel : shared.renameDraft);
665
+ var v = String(raw || '').trim().slice(0, 24) || DEFAULT_TAB_LABEL;
666
+ writeLS(LS_LABEL, v);
667
+ shared.tabLabel = v;
668
+ labelEmitter.emit(v); // 立即更新顶部按钮文字
669
+ renderAll();
670
+ }
671
+
672
+ // ───────────────── DOM helpers ─────────────────
673
+ function el(cls, tag) {
674
+ var e = doc.createElement(tag || 'div');
675
+ if (cls) e.className = cls;
676
+ return e;
677
+ }
678
+ function button(text, cls, part, onClick) {
679
+ var b = doc.createElement('button');
680
+ b.type = 'button';
681
+ b.className = cls || 'yanyan-btn';
682
+ b.setAttribute('data-dsh-plugin', 'yanyan-subagent');
683
+ if (part) b.setAttribute('data-dsh-part', part);
684
+ if (text != null) b.textContent = text;
685
+ if (onClick) b.addEventListener('click', onClick);
686
+ return b;
687
+ }
688
+ function lineText(text, cls, part) {
689
+ var s = el(cls, 'span');
690
+ if (part) s.setAttribute('data-dsh-part', part);
691
+ s.textContent = text;
692
+ return s;
693
+ }
694
+ function formatIdle(row) {
695
+ if (!row || row.kind === 'diagnostic' || row.activity === 'running') return '';
696
+ var ms = idleMs(row.id);
697
+ var mins = Math.floor(ms / 60000);
698
+ return mins > 0 ? (mins + ' ' + t('minutes')) : ('0 ' + t('minutes'));
699
+ }
700
+ function statusText(st) {
701
+ if (!st) return '';
702
+ if (st.state === 'deleting') return t('deleting');
703
+ if (st.state === 'deleted') return t('deleted') + (st.hint ? (' · ' + st.hint) : '');
704
+ if (st.state === 'failed') return t('failed') + (st.message ? (': ' + st.message) : '');
705
+ return '';
706
+ }
707
+
708
+ // ───────────────── 浮层骨架(只建一次,后续只增量刷新列表/状态) ─────────────────
709
+ function createView(container, ctx, isPortal) {
710
+ var view = { container: container, ctx: ctx, refs: {}, expandedSettings: false };
711
+ container.setAttribute('data-dsh-plugin', 'yanyan-subagent');
712
+ container.setAttribute('data-dsh-part', 'overlay');
713
+
714
+ // 头部:标题 + 关闭。
715
+ var header = el('yanyan-header');
716
+ header.setAttribute('data-dsh-part', 'header');
717
+ header.appendChild(lineText(t('title'), 'yanyan-title', 'title'));
718
+ header.appendChild(button(t('close'), 'yanyan-close', 'close', function () {
719
+ if (isPortal) closeEmitter.emit();
720
+ else closeNativeOverlay();
721
+ }));
722
+
723
+ // 工具栏:刷新 + 设置开关。
724
+ var toolbar = el('yanyan-toolbar');
725
+ toolbar.setAttribute('data-dsh-part', 'toolbar');
726
+ toolbar.appendChild(button(t('refresh'), 'yanyan-btn', 'refresh', function () {
727
+ shared.cleanStatus = ''; shared.settingsStatus = ''; refresh(ctx);
728
+ }));
729
+ toolbar.appendChild(button(t('settings'), 'yanyan-btn', 'settings-toggle', function () {
730
+ view.expandedSettings = !view.expandedSettings;
731
+ view.update();
732
+ }));
733
+
734
+ // 重命名标签:输入框 + 保存。
735
+ var rename = el('yanyan-rename');
736
+ rename.setAttribute('data-dsh-part', 'rename');
737
+ var renameLabel = el('yanyan-label', 'label'); renameLabel.textContent = t('rename');
738
+ var renameInput = doc.createElement('input');
739
+ renameInput.type = 'text'; renameInput.className = 'yanyan-input';
740
+ renameInput.setAttribute('data-dsh-part', 'rename-input');
741
+ renameInput.setAttribute('placeholder', t('tabRenameHint'));
742
+ renameInput.value = shared.tabLabel;
743
+ renameInput.addEventListener('input', function () { shared.renameDraft = renameInput.value; });
744
+ var renameSave = button(t('save'), 'yanyan-btn-primary', 'rename-save', saveTabLabel);
745
+ rename.appendChild(renameLabel); rename.appendChild(renameInput); rename.appendChild(renameSave);
746
+
747
+ // 设置区:自动清理开关 + 空闲分钟 + 巡检间隔 + 保存(默认折叠)。
748
+ var settings = el('yanyan-settings');
749
+ settings.setAttribute('data-dsh-part', 'settings');
750
+ var autoWrap = el('yanyan-check', 'label');
751
+ var autoCheck = doc.createElement('input');
752
+ autoCheck.type = 'checkbox';
753
+ autoCheck.setAttribute('data-dsh-part', 'auto-clean');
754
+ autoCheck.checked = shared.autoCleanEnabled;
755
+ autoCheck.addEventListener('change', function () { shared.autoDraft.autoCleanEnabled = autoCheck.checked; });
756
+ autoWrap.appendChild(autoCheck);
757
+ autoWrap.appendChild(doc.createTextNode(' ' + t('autoClean')));
758
+ var idleLabel = el('yanyan-label', 'label'); idleLabel.textContent = t('idleMinutes');
759
+ var idleInput = doc.createElement('input');
760
+ idleInput.type = 'number'; idleInput.min = '1'; idleInput.max = '10080';
761
+ idleInput.className = 'yanyan-input yanyan-small';
762
+ idleInput.setAttribute('data-dsh-part', 'idle-minutes');
763
+ idleInput.value = String(shared.idleMinutes);
764
+ idleInput.addEventListener('input', function () { shared.autoDraft.idleMinutes = idleInput.value; });
765
+ var intervalLabel = el('yanyan-label', 'label'); intervalLabel.textContent = t('checkInterval');
766
+ var intervalInput = doc.createElement('input');
767
+ intervalInput.type = 'number'; intervalInput.min = '1'; intervalInput.max = '1440';
768
+ intervalInput.className = 'yanyan-input yanyan-small';
769
+ intervalInput.setAttribute('data-dsh-part', 'check-interval');
770
+ intervalInput.value = String(shared.checkIntervalMin);
771
+ intervalInput.addEventListener('input', function () { shared.autoDraft.checkIntervalMin = intervalInput.value; });
772
+ var hint = el('yanyan-hint'); hint.textContent = t('autoCleanHint');
773
+ var settingsSave = button(t('save'), 'yanyan-btn-primary', 'settings-save', function () { saveSettings(ctx); });
774
+ var settingsStatus = el('yanyan-hint'); settingsStatus.setAttribute('data-dsh-part', 'settings-status');
775
+ var grid = el('yanyan-grid');
776
+ grid.appendChild(autoWrap); grid.appendChild(idleLabel); grid.appendChild(idleInput);
777
+ grid.appendChild(intervalLabel); grid.appendChild(intervalInput);
778
+ settings.appendChild(grid); settings.appendChild(hint);
779
+ settings.appendChild(settingsSave); settings.appendChild(settingsStatus);
780
+
781
+ // 列表容器(增量重建,行内无输入框,不会丢焦点)。
782
+ var listEl = el('yanyan-list'); listEl.setAttribute('data-dsh-part', 'list');
783
+
784
+ // 底部:阈值提示 + 清理全部空闲 + 清理残留行 + 结果。
785
+ var footer = el('yanyan-footer'); footer.setAttribute('data-dsh-part', 'footer');
786
+ var threshold = lineText('', 'yanyan-threshold', 'idle-threshold');
787
+ var cleanBtn = button(t('cleanIdle'), 'yanyan-btn-primary', 'clean-idle', function () { cleanAllIdle(ctx); });
788
+ var scrubBtn = button(t('scrubResidual'), 'yanyan-btn', 'scrub-residual', function () { scrubResidual(ctx); });
789
+ var result = lineText('', 'yanyan-result', 'clean-result');
790
+ footer.appendChild(threshold); footer.appendChild(cleanBtn); footer.appendChild(scrubBtn); footer.appendChild(result);
791
+
792
+ container.appendChild(header);
793
+ container.appendChild(toolbar);
794
+ container.appendChild(rename);
795
+ container.appendChild(settings);
796
+ container.appendChild(listEl);
797
+ container.appendChild(footer);
798
+
799
+ view.refs = {
800
+ settings: settings, list: listEl, threshold: threshold, result: result,
801
+ cleanBtn: cleanBtn, scrubBtn: scrubBtn, autoCheck: autoCheck, idleInput: idleInput,
802
+ intervalInput: intervalInput, settingsStatus: settingsStatus, renameInput: renameInput,
803
+ };
804
+
805
+ view.update = function () { updateView(view); };
806
+ return view;
807
+ }
808
+
809
+ function updateView(view) {
810
+ var refs = view.refs;
811
+ refs.settings.style.display = view.expandedSettings ? '' : 'none';
812
+ refs.threshold.textContent = t('idleThreshold') + ' ' + shared.idleMinutes + ' ' + t('minutes');
813
+ refs.result.textContent = shared.cleanStatus || '';
814
+ refs.settingsStatus.textContent = shared.settingsStatus || '';
815
+ // 回填设置输入(骨架不重建,避免覆盖用户正在输入的值时丢焦点)。
816
+ if (document.activeElement !== refs.autoCheck) refs.autoCheck.checked = shared.autoCleanEnabled;
817
+ if (document.activeElement !== refs.idleInput) refs.idleInput.value = String(shared.idleMinutes);
818
+ if (document.activeElement !== refs.intervalInput) refs.intervalInput.value = String(shared.checkIntervalMin);
819
+ if (document.activeElement !== refs.renameInput && shared.renameDraft == null) {
820
+ refs.renameInput.value = shared.tabLabel;
821
+ }
822
+ refs.cleanBtn.disabled = false;
823
+
824
+ var list = refs.list;
825
+ list.innerHTML = '';
826
+ if (shared.unsupported) {
827
+ list.appendChild(lineText(t('unsupported'), 'yanyan-empty', 'unsupported'));
828
+ return;
829
+ }
830
+ if (!shared.rows.length) {
831
+ list.appendChild(lineText(t('empty'), 'yanyan-empty', 'empty'));
832
+ return;
833
+ }
834
+ for (var i = 0; i < shared.rows.length; i++) {
835
+ var r = shared.rows[i];
836
+ if (!r || r.kind === 'diagnostic') continue;
837
+ // 双保险:即便 refresh 之后写入了新数组,渲染前再过滤一次隐藏清单。
838
+ if ((r.id && isDeleted(r.id)) || (r.childId && isDeleted(r.childId))) continue;
839
+ list.appendChild(renderRow(view.ctx, r));
840
+ }
841
+ }
842
+
843
+ function renderRow(ctx, row) {
844
+ var line = el('yanyan-row');
845
+ line.setAttribute('data-dsh-part', 'row');
846
+ var name = (row.label || row.name || row.id || t('unknown'));
847
+ line.appendChild(lineText(name, 'yanyan-row-name', 'row-name'));
848
+
849
+ var running = row.activity === 'running';
850
+ var isDiag = row.kind === 'diagnostic';
851
+ var statusCls = 'yanyan-status ' + (running ? 'yanyan-running' : 'yanyan-idle');
852
+ var statusLabel = isDiag ? t('diagnostic') : (running ? t('running') : t('idle'));
853
+ var statusEl = lineText(statusLabel, statusCls, 'row-status');
854
+ line.appendChild(statusEl);
855
+ if (row.mode) line.appendChild(lineText(String(row.mode), 'yanyan-mode', 'row-mode'));
856
+ if (!isDiag && !running) {
857
+ line.appendChild(lineText(formatIdle(row), 'yanyan-idle-ms', 'row-idle'));
858
+ }
859
+ if (running) line.appendChild(lineText(t('stopHint'), 'yanyan-hint yanyan-stop', 'row-stop-hint'));
860
+
861
+ var st = shared.rowStatus[row.id];
862
+ if (st) {
863
+ var sEl = lineText(statusText(st), 'yanyan-row-state', 'row-state');
864
+ line.appendChild(sEl);
865
+ }
866
+
867
+ // 根会话 / 诊断行不显示删除按钮;其余(含 running)允许删除。
868
+ var isRoot = row.id === shared.rootId;
869
+ if (!isDiag && !isRoot && row.id) {
870
+ var del = button(X_MARK, 'yanyan-x', 'row-delete', function () { onRowDelete(ctx, row); });
871
+ del.setAttribute('aria-label', t('delete'));
872
+ del.title = t('delete');
873
+ if (st && st.state === 'deleted') del.disabled = true;
874
+ line.appendChild(del);
875
+ } else if (isRoot) {
876
+ line.appendChild(lineText(t('root'), 'yanyan-root-tag', 'row-root'));
877
+ }
878
+ return line;
879
+ }
880
+
881
+ // ───────────────── React 路径(slots + createPortal) ─────────────────
882
+ var canReact = !!(React && React.createElement && React.useState && React.useEffect && React.useRef);
883
+ var canPortal = !!(ReactDOM && ReactDOM.createPortal);
884
+
885
+ function PortalHost(props) {
886
+ var ref = React.useRef(null);
887
+ React.useEffect(function () {
888
+ if (!ref.current) return;
889
+ var view = createView(ref.current, props.ctx, true);
890
+ views.push(view);
891
+ view.update();
892
+ refresh(props.ctx);
893
+ return function () {
894
+ var i = views.indexOf(view); if (i >= 0) views.splice(i, 1);
895
+ try { if (ref.current) ref.current.innerHTML = ''; } catch (e) {}
896
+ };
897
+ }, []);
898
+ return React.createElement('div', { ref: ref, className: 'yanyan-portal-host' });
899
+ }
900
+
901
+ // ActionButton 组件只创建一次:槽的 render 工厂每次返回新组件类型会反复
902
+ // 卸载重挂丢 state。label 用 readTabLabel() 支持改名即时生效。
903
+ function makeActionButton(ctx) {
904
+ return function ActionButton() {
905
+ var openState = React.useState(false);
906
+ var open = openState[0], setOpen = openState[1];
907
+ var labelState = React.useState(readTabLabel());
908
+ var lbl = labelState[0], setLbl = labelState[1];
909
+ React.useEffect(function () {
910
+ var u1 = labelEmitter.subscribe(function (v) { setLbl(v); });
911
+ var u2 = closeEmitter.subscribe(function () { setOpen(false); });
912
+ return function () { u1(); u2(); };
913
+ }, []);
914
+ var onClick = function () {
915
+ if (canPortal) setOpen(function (v) { return !v; });
916
+ else toggleOverlayNative(ctx);
917
+ };
918
+ var kids = [React.createElement('span', { key: 't' }, lbl || t('tab'))];
919
+ if (open && canPortal) {
920
+ kids.push(ReactDOM.createPortal(
921
+ React.createElement(PortalHost, { ctx: ctx }), doc.body));
922
+ }
923
+ return React.createElement('button', {
924
+ 'data-dsh-plugin': 'yanyan-subagent',
925
+ 'data-dsh-part': 'session-tab',
926
+ className: 'yanyan-tab-btn',
927
+ title: t('title'),
928
+ onClick: onClick,
929
+ }, kids);
930
+ };
931
+ }
932
+
933
+ // ───────────────── 纯 DOM 浮层(无 react-dom 时) ─────────────────
934
+ function toggleOverlayNative(ctx) {
935
+ if (nativeOverlay) { closeNativeOverlay(); return; }
936
+ try {
937
+ var div = doc.createElement('div');
938
+ div.className = 'yanyan-native-overlay';
939
+ nativeOverlay = div;
940
+ doc.body.appendChild(div);
941
+ var view = createView(div, ctx, false);
942
+ views.push(view);
943
+ nativeView = view;
944
+ view.update();
945
+ refresh(ctx);
946
+ } catch (e) {
947
+ try { if (nativeOverlay) nativeOverlay.remove(); } catch (e2) {}
948
+ nativeOverlay = null; nativeView = null;
949
+ }
950
+ }
951
+ function closeNativeOverlay() {
952
+ if (nativeView) {
953
+ var i = views.indexOf(nativeView); if (i >= 0) views.splice(i, 1);
954
+ nativeView = null;
955
+ }
956
+ if (nativeOverlay) { try { nativeOverlay.remove(); } catch (e) {} nativeOverlay = null; }
957
+ }
958
+ function closeAllViews() {
959
+ closeNativeOverlay();
960
+ try { closeEmitter.emit(); } catch (e) {}
961
+ for (var i = 0; i < views.length; i++) {
962
+ try { views[i].container.innerHTML = ''; } catch (e2) {}
963
+ }
964
+ views.length = 0;
965
+ }
966
+
967
+ // ctx.slots 运行时不可用时,降级为纯 DOM 注入顶部按钮(尽力而为,不抛错)。
968
+ function injectNativeTabButton(ctx) {
969
+ try {
970
+ var host = doc.querySelector('[data-session-header-actions], .session-header-actions, header')
971
+ || doc.body;
972
+ var btn = doc.createElement('button');
973
+ btn.type = 'button';
974
+ btn.className = 'yanyan-tab-btn';
975
+ btn.setAttribute('data-dsh-plugin', 'yanyan-subagent');
976
+ btn.setAttribute('data-dsh-part', 'session-tab');
977
+ btn.textContent = readTabLabel();
978
+ btn.addEventListener('click', function () { toggleOverlayNative(ctx); });
979
+ host.appendChild(btn);
980
+ var unsub = labelEmitter.subscribe(function (l) { btn.textContent = l || t('tab'); });
981
+ return function () {
982
+ try { unsub(); } catch (e) {}
983
+ try { btn.remove(); } catch (e2) {}
984
+ };
985
+ } catch (e) { return function () {}; }
986
+ }
987
+
988
+ // ───────────────── 图3 官方列表行尾加 U+2715(可选增强,尽力而为) ─────────────────
989
+ // 官方行没有 data-id,无法可靠拿 sessionId;点击时按行文本去本地 catalog
990
+ // 做模糊匹配(label/name 包含匹配 + 顺序对应),匹配到唯一 id 才删除,
991
+ // 否则提示到「智能体管理」标签中删除。做不到可靠删除就诚实说明限制。
992
+ function onOfficialXClick(ctx, item, items) {
993
+ var text = (item.textContent || '').trim();
994
+ var rows = filterDeleted(shared.rows).filter(function (r) {
995
+ return r && r.kind !== 'diagnostic' && r.id;
996
+ });
997
+ var matches = [];
998
+ for (var i = 0; i < rows.length; i++) {
999
+ var r = rows[i];
1000
+ var l = String(r.label || '').trim();
1001
+ var n = String(r.name || '').trim();
1002
+ if ((l && text.indexOf(l) !== -1) || (n && text.indexOf(n) !== -1)) matches.push(r);
1003
+ }
1004
+ if (matches.length !== 1) {
1005
+ // 顺序对应:用行序号与非诊断 catalog 序号对齐收敛,仍要求文本命中。
1006
+ matches = [];
1007
+ try {
1008
+ var idx = Array.prototype.indexOf.call(items, item);
1009
+ var byOrder = rows[idx];
1010
+ if (byOrder) {
1011
+ var l2 = String(byOrder.label || '').trim();
1012
+ var n2 = String(byOrder.name || '').trim();
1013
+ if ((l2 && text.indexOf(l2) !== -1) || (n2 && text.indexOf(n2) !== -1)) matches = [byOrder];
1014
+ }
1015
+ } catch (e) { matches = []; }
1016
+ }
1017
+ if (matches.length === 1 && matches[0].id) {
1018
+ var row = matches[0];
1019
+ var ok = true;
1020
+ try { ok = root.confirm(row.activity === 'running' ? t('confirmDeleteRunning') : t('confirmDelete')); }
1021
+ catch (e3) { ok = true; }
1022
+ if (ok) deleteOne(ctx, row, row.activity === 'running');
1023
+ } else {
1024
+ try { root.alert(t('officialNoMatch')); } catch (e4) {}
1025
+ }
1026
+ }
1027
+
1028
+ function injectOfficialButtons(ctx, panel) {
1029
+ var items = panel.querySelectorAll('[role="treeitem"]');
1030
+ for (var i = 0; i < items.length; i++) {
1031
+ (function (item) {
1032
+ if (item.querySelector('[data-dsh-part="official-row-delete"]')) return;
1033
+ var b = doc.createElement('button');
1034
+ b.type = 'button';
1035
+ b.className = 'yanyan-official-x';
1036
+ b.setAttribute('data-dsh-plugin', 'yanyan-subagent');
1037
+ b.setAttribute('data-dsh-part', 'official-row-delete');
1038
+ b.setAttribute('aria-label', t('delete'));
1039
+ b.title = t('delete');
1040
+ b.textContent = X_MARK;
1041
+ b.addEventListener('click', function (ev) {
1042
+ if (ev && ev.stopPropagation) ev.stopPropagation();
1043
+ if (ev && ev.preventDefault) ev.preventDefault();
1044
+ onOfficialXClick(ctx, item, items);
1045
+ });
1046
+ item.appendChild(b);
1047
+ officialButtons.push(b);
1048
+ })(items[i]);
1049
+ }
1050
+ }
1051
+ function clearOfficialButtons() {
1052
+ for (var i = 0; i < officialButtons.length; i++) {
1053
+ try { officialButtons[i].remove(); } catch (e) {}
1054
+ }
1055
+ officialButtons.length = 0;
1056
+ }
1057
+
1058
+ // ───────────────── 样式 ─────────────────
1059
+ var STYLE_CSS = [
1060
+ '.yanyan-tab-btn{display:inline-flex;align-items:center;gap:6px;',
1061
+ 'background:transparent;border:1px solid var(--yanyan-border,#d0d7de);',
1062
+ 'color:var(--yanyan-fg,#1f2328);border-radius:6px;padding:4px 10px;',
1063
+ 'font-size:12px;cursor:pointer;white-space:nowrap}',
1064
+ '.yanyan-tab-btn:hover{border-color:var(--dsw-alias-button-primary-fill,#2f6feb)}',
1065
+ '.yanyan-portal-host,.yanyan-native-overlay{position:fixed;top:64px;right:24px;',
1066
+ 'width:380px;max-width:calc(100vw - 48px);max-height:min(72vh,600px);',
1067
+ 'z-index:2147483000;background:var(--yanyan-bg,#fff);color:var(--yanyan-fg,#1f2328);',
1068
+ 'border:1px solid var(--yanyan-border,#d0d7de);border-radius:12px;',
1069
+ 'box-shadow:0 14px 40px rgba(0,0,0,.22);display:flex;flex-direction:column;',
1070
+ 'overflow:hidden;font-size:13px;font-family:inherit}',
1071
+ '.yanyan-header{display:flex;align-items:center;justify-content:space-between;',
1072
+ 'padding:12px 14px;border-bottom:1px solid var(--yanyan-border,#d0d7de)}',
1073
+ '.yanyan-title{font-weight:600;font-size:14px}',
1074
+ '.yanyan-toolbar{display:flex;gap:8px;padding:8px 14px}',
1075
+ '.yanyan-btn,.yanyan-close{background:transparent;border:1px solid var(--yanyan-border,#d0d7de);',
1076
+ 'color:var(--yanyan-fg,#1f2328);border-radius:6px;padding:4px 10px;font-size:12px;cursor:pointer}',
1077
+ '.yanyan-btn:hover,.yanyan-close:hover{border-color:var(--dsw-alias-button-primary-fill,#2f6feb)}',
1078
+ '.yanyan-btn-primary{background:var(--dsw-alias-button-primary-fill,#2f6feb);',
1079
+ 'color:var(--dsw-alias-label-primary-foreground,#fff);border:none;border-radius:6px;',
1080
+ 'padding:6px 12px;font-size:12px;cursor:pointer}',
1081
+ '.yanyan-btn-primary:hover{background:var(--dsw-alias-button-primary-hover,#2559c4)}',
1082
+ '.yanyan-btn-primary:disabled{opacity:.55;cursor:default}',
1083
+ '.yanyan-rename{display:flex;align-items:center;gap:8px;padding:8px 14px;',
1084
+ 'border-bottom:1px solid var(--yanyan-border,#d0d7de)}',
1085
+ '.yanyan-label{font-size:12px;color:var(--yanyan-muted,#656d76);white-space:nowrap}',
1086
+ '.yanyan-input{flex:1;min-width:0;background:var(--yanyan-input-bg,#f6f8fa);',
1087
+ 'border:1px solid var(--yanyan-border,#d0d7de);color:var(--yanyan-fg,#1f2328);',
1088
+ 'border-radius:6px;padding:5px 8px;font-size:12px;font-family:inherit}',
1089
+ '.yanyan-input.yanyan-small{width:76px;flex:none}',
1090
+ '.yanyan-settings{padding:10px 14px;border-bottom:1px solid var(--yanyan-border,#d0d7de)}',
1091
+ '.yanyan-grid{display:grid;grid-template-columns:auto 1fr auto;gap:6px 8px;align-items:center}',
1092
+ '.yanyan-check{display:flex;align-items:center;gap:6px;font-size:12px}',
1093
+ '.yanyan-hint{font-size:11px;color:var(--yanyan-muted,#656d76);margin:8px 0 0}',
1094
+ '.yanyan-settings .yanyan-btn-primary{margin-top:8px}',
1095
+ '.yanyan-list{flex:1;overflow:auto;min-height:60px}',
1096
+ '.yanyan-row{display:flex;align-items:center;gap:8px;padding:8px 14px;',
1097
+ 'border-bottom:1px solid var(--yanyan-border,#d0d7de)}',
1098
+ '.yanyan-row-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
1099
+ '.yanyan-status{font-size:11px;padding:1px 6px;border-radius:10px;white-space:nowrap}',
1100
+ '.yanyan-idle{background:var(--yanyan-idle-bg,#eaeef2);color:var(--yanyan-muted,#656d76)}',
1101
+ '.yanyan-running{background:var(--yanyan-run-bg,#dafbe1);color:#1a7f37}',
1102
+ '.yanyan-mode,.yanyan-idle-ms,.yanyan-root-tag{font-size:11px;',
1103
+ 'color:var(--yanyan-muted,#656d76);white-space:nowrap}',
1104
+ '.yanyan-stop{color:#9a6700;white-space:normal}',
1105
+ '.yanyan-row-state{font-size:11px;color:#cf222e;white-space:nowrap}',
1106
+ '.yanyan-x,.yanyan-official-x{border:none;background:transparent;cursor:pointer;',
1107
+ 'color:var(--yanyan-muted,#656d76);font-size:14px;line-height:1;padding:2px 6px;',
1108
+ 'border-radius:4px;flex:none}',
1109
+ '.yanyan-x:hover,.yanyan-official-x:hover{background:var(--yanyan-danger-bg,#ffebe9);color:#cf222e}',
1110
+ '.yanyan-x:disabled{opacity:.4;cursor:default}',
1111
+ '.yanyan-official-x{margin-left:auto;font-size:12px}',
1112
+ '.yanyan-footer{display:flex;align-items:center;gap:10px;padding:10px 14px;',
1113
+ 'border-top:1px solid var(--yanyan-border,#d0d7de);flex-wrap:wrap}',
1114
+ '.yanyan-threshold{font-size:11px;color:var(--yanyan-muted,#656d76)}',
1115
+ '.yanyan-result{font-size:11px;color:#1a7f37;flex-basis:100%}',
1116
+ '.yanyan-empty{padding:20px 14px;text-align:center;font-size:12px;',
1117
+ 'color:var(--yanyan-muted,#656d76)}',
1118
+ '@media (prefers-color-scheme:dark){',
1119
+ '.yanyan-portal-host,.yanyan-native-overlay{--yanyan-bg:#1c2128;--yanyan-fg:#e6edf3;',
1120
+ '--yanyan-border:#30363d;--yanyan-muted:#8b949e;--yanyan-input-bg:#0d1117;',
1121
+ '--yanyan-idle-bg:#21262d;--yanyan-run-bg:#12261e;--yanyan-danger-bg:#2d1315}',
1122
+ '.yanyan-tab-btn,.yanyan-btn,.yanyan-close{--yanyan-fg:#e6edf3;--yanyan-border:#30363d}}',
1123
+ ].join('');
1124
+
1125
+ function injectStyles(ctx) {
1126
+ var styles = lookup('styles');
1127
+ if (styles && typeof styles.insert === 'function') {
1128
+ try { styles.insert(STYLE_CSS); return; } catch (e) {}
1129
+ }
1130
+ if (!doc || doc.getElementById('yanyan-subagent-style')) return;
1131
+ var style = doc.createElement('style');
1132
+ style.id = 'yanyan-subagent-style';
1133
+ style.setAttribute('data-dsh-plugin', 'yanyan-subagent');
1134
+ style.textContent = STYLE_CSS;
1135
+ (doc.head || doc.body).appendChild(style);
1136
+ }
1137
+
1138
+ // ───────────────── apply ─────────────────
1139
+ var inject = ['locale', 'sessions', 'slots'];
1140
+
1141
+ function apply(ctx) {
1142
+ rootCtx = ctx;
1143
+
1144
+ // 字典注册(register 返回值直接作 effect 清理函数)。
1145
+ try {
1146
+ if (ctx.locale && typeof ctx.locale.register === 'function') {
1147
+ ctx.effect(function () {
1148
+ return ctx.locale.register(NS, { zh: UI_zh, en: UI_en });
1149
+ }, 'yanyan-subagent: dictionaries');
1150
+ }
1151
+ } catch (e) {}
1152
+
1153
+ try { injectStyles(ctx); } catch (e) {}
1154
+
1155
+ // 设置(异步读取 host /config 补齐缺省值)。
1156
+ loadSettings(ctx).then(function () { renderAll(); });
1157
+
1158
+ // 顶部标签(决定性 Slot)。绝不复用官方 subagent-catalog id;官方 order=10,
1159
+ // 用 12 排在旁边。slots 不可用或 React 不可用时降级纯 DOM 按钮。
1160
+ var teardownTab = function () {};
1161
+ try {
1162
+ if (canReact && ctx.slots && typeof ctx.slots.inject === 'function' &&
1163
+ typeof ctx.slots.register === 'function' && doc) {
1164
+ // ActionButton 组件只创建一次(槽的 render 工厂每次返回新组件类型会
1165
+ // 反复卸载重挂丢 state)。它是一个不含 hooks 的薄组件:内部用
1166
+ // React.createElement 渲染真正带 hooks 的组件,因此无论宿主把注册项
1167
+ // 当组件类型 createElement、还是当 render 回调直接调用都不会触发
1168
+ // "Invalid hook call"(照抄 dsh-plugin-wechat-session 的双保险形态)。
1169
+ var ActionButtonInner = makeActionButton(ctx);
1170
+ var ActionButton = function ActionButton() {
1171
+ return React.createElement(ActionButtonInner, null);
1172
+ };
1173
+ ctx.slots.inject('conversation.session.header.actions', function () {
1174
+ return ctx.slots.register({
1175
+ name: 'conversation.session.header.actions',
1176
+ id: 'yanyan-subagent-mgr',
1177
+ order: 12,
1178
+ locale: NS,
1179
+ // label 也走 readTabLabel(),改名后若宿主用该字段做无障碍/溢出菜单文字,
1180
+ // 同样能同步;未改名时返回值即默认「智能体管理」(= t('tab'))。
1181
+ label: function () { return readTabLabel(); },
1182
+ }, ActionButton);
1183
+ });
1184
+ } else {
1185
+ teardownTab = injectNativeTabButton(ctx);
1186
+ }
1187
+ } catch (e) {
1188
+ teardownTab = injectNativeTabButton(ctx);
1189
+ }
1190
+
1191
+ // 所有 observer / interval / 订阅 / 事件监听统一放进单个 ctx.effect 撤销。
1192
+ ctx.effect(function () {
1193
+ var disposed = false;
1194
+ var unsubs = [];
1195
+
1196
+ // 订阅 sessions.list 数据变化,触发重渲染(只重读快照,不主动
1197
+ // refreshProjections,避免订阅回调自激循环)。
1198
+ try {
1199
+ var list = ctx.sessions && ctx.sessions.list;
1200
+ if (list && typeof list.subscribe === 'function') {
1201
+ var sub = list.subscribe(function () { if (!disposed) refresh(ctx, false); });
1202
+ unsubs.push(function () {
1203
+ try {
1204
+ if (typeof sub === 'function') sub();
1205
+ else if (sub && typeof sub.unsubscribe === 'function') sub.unsubscribe();
1206
+ } catch (e) {}
1207
+ });
1208
+ }
1209
+ } catch (e) {}
1210
+
1211
+ // 首屏刷新。
1212
+ refresh(ctx);
1213
+
1214
+ // 自动清理巡检(默认关闭;配置变更即时重排 interval)。
1215
+ reconfigureInterval(ctx);
1216
+ unsubs.push(function () {
1217
+ if (autoTimer) { try { root.clearInterval(autoTimer); } catch (e) {} autoTimer = null; }
1218
+ });
1219
+
1220
+ // 图3 官方列表浮层出现时,给每行追加 U+2715(可选增强)。
1221
+ try {
1222
+ if (root.MutationObserver && doc && doc.body) {
1223
+ officialObserver = new root.MutationObserver(function () {
1224
+ if (disposed) return;
1225
+ try {
1226
+ var panels = doc.querySelectorAll('[data-subagent-panel]');
1227
+ for (var i = 0; i < panels.length; i++) injectOfficialButtons(ctx, panels[i]);
1228
+ } catch (e) {}
1229
+ });
1230
+ officialObserver.observe(doc.body, { childList: true, subtree: true });
1231
+ unsubs.push(function () { try { officialObserver.disconnect(); } catch (e) {} });
1232
+ }
1233
+ } catch (e) {}
1234
+ unsubs.push(function () { clearOfficialButtons(); });
1235
+
1236
+ return function () {
1237
+ disposed = true;
1238
+ for (var i = 0; i < unsubs.length; i++) { try { unsubs[i](); } catch (e) {} }
1239
+ closeAllViews();
1240
+ try { teardownTab(); } catch (e2) {}
1241
+ };
1242
+ }, 'yanyan-subagent: lifecycle');
1243
+ }
1244
+
1245
+ exports.inject = inject;
1246
+ exports.apply = apply;
1247
+ return module.exports;
1248
+ },
1249
+ });
1250
+ })(typeof globalThis !== 'undefined' ? globalThis : this);