dsh-recall-plugin 1.2.2 → 1.4.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/CHANGELOG.md +109 -0
- package/README.en.md +124 -118
- package/README.md +125 -119
- package/cordis.patch.yml +19 -11
- package/lib/client.js +780 -456
- package/lib/config.js +36 -0
- package/lib/index.js +541 -164
- package/lib/maintenance.js +120 -107
- package/lib/scripts.posix.js +366 -290
- package/lib/scripts.pwsh.js +432 -334
- package/lib/snapshots.js +214 -192
- package/lib/store.js +342 -249
- package/package.json +52 -57
package/lib/client.js
CHANGED
|
@@ -1,456 +1,780 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-recall-plugin — Client 半(持久插件形态,浏览器 bundle)
|
|
3
|
-
*
|
|
4
|
-
* 职责:注册 conversation.chat.node 的 user 渲染器,重绘用户消息气泡
|
|
5
|
-
* (文本/图片/JSON 块),在复制按钮旁加「撤回」按钮;二次确认面板展示
|
|
6
|
-
* 文件变更清单,确认后经 /api/recall/* 调用 Host 回退文件,并用官方
|
|
7
|
-
* sessions.fork 把对话一并回退(新会话打开、原会话归档)。
|
|
8
|
-
*
|
|
9
|
-
* 这是 dsh.client bundle(exports["./client"]),由 client-modules 打包成
|
|
10
|
-
* /plugins/<pkg>/client.js 注入页面;factory 内 require("react") 由平台
|
|
11
|
-
* 模块表提供。零构建依赖:纯 JS + React.createElement。
|
|
12
|
-
*/
|
|
13
|
-
window.__ModuleLoader__.load({
|
|
14
|
-
id: "dsh-recall-plugin",
|
|
15
|
-
factory: (require) => {
|
|
16
|
-
const React = require("react")
|
|
17
|
-
|
|
18
|
-
return {
|
|
19
|
-
name: "dsh-recall-plugin",
|
|
20
|
-
apply(ctx) {
|
|
21
|
-
const slots = ctx.get('slots')
|
|
22
|
-
if (!slots) return
|
|
23
|
-
// 官方会话服务:fork 到已完成 turn 前缀 + open 切到新会话;
|
|
24
|
-
// workspaces 的归档只是从列表隐藏、可恢复,用来收走回退前的原会话。
|
|
25
|
-
const sessionsSvc = ctx.get('sessions')
|
|
26
|
-
const workspacesSvc = ctx.get('workspaces')
|
|
27
|
-
|
|
28
|
-
// Host HTTP API(动态插件的 harness RPC 在此换成 fetch 调用)
|
|
29
|
-
function api(name, args) {
|
|
30
|
-
return fetch('/api/recall/' + name, {
|
|
31
|
-
method: 'POST',
|
|
32
|
-
headers: { 'content-type': 'application/json' },
|
|
33
|
-
body: JSON.stringify(args || {})
|
|
34
|
-
}).then((r) => r.json())
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const css = [
|
|
38
|
-
'.dsh-recall-row{flex-direction:column;align-items:flex-end;gap:6px;display:flex}',
|
|
39
|
-
'.dsh-recall-stack{flex-direction:column;align-items:flex-end;gap:8px;min-width:0;max-width:min(525px,82%);display:flex}',
|
|
40
|
-
'.dsh-recall-bubble{background:var(--dsw-specific-bubble);max-width:100%;color:var(--dsw-alias-label-primary);border-radius:22px;padding:10px 16px;font-size:16px;line-height:24px;white-space:pre-wrap;word-break:break-word}',
|
|
41
|
-
'.dsh-recall-img{max-width:100%;max-height:320px;border-radius:12px;object-fit:contain}',
|
|
42
|
-
'.dsh-recall-json{margin:0;max-width:100%;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary);white-space:pre-wrap;word-break:break-word;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:8px 10px;background:var(--dsw-alias-markdown-code-block)}',
|
|
43
|
-
'.dsh-recall-actions{align-items:center;gap:10px;height:28px;display:flex}',
|
|
44
|
-
'.dsh-recall-time{color:var(--dsw-alias-label-tertiary);white-space:nowrap;padding-right:12px;font-size:14px;line-height:24px}',
|
|
45
|
-
'.dsh-recall-action{width:28px;height:28px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:28px;justify-content:center;align-items:center;padding:6px;display:inline-flex}',
|
|
46
|
-
'.dsh-recall-action:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}',
|
|
47
|
-
'@media (hover:hover){[data-time-hover-root] .dsh-recall-time{opacity:0;transition:opacity 80ms}[data-time-hover-root]:hover .dsh-recall-time,[data-time-hover-root]:focus-within .dsh-recall-time{opacity:1}}',
|
|
48
|
-
'.dsh-recall-panel{width:min(480px,100%);box-sizing:border-box;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l1);border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:8px;text-align:left;box-shadow:0 8px 28px rgba(0,0,0,.22)}',
|
|
49
|
-
'.dsh-recall-panel-title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600;line-height:22px}',
|
|
50
|
-
'.dsh-recall-panel-note{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px;word-break:break-word}',
|
|
51
|
-
'.dsh-recall-list{max-height:220px;overflow:auto;display:flex;flex-direction:column;gap:2px;padding:4px 0}',
|
|
52
|
-
'.dsh-recall-file{display:flex;gap:8px;align-items:baseline;font-size:12px;line-height:20px}',
|
|
53
|
-
'.dsh-recall-badge{flex:none;font-size:11px;line-height:18px;padding:0 6px;border-radius:6px}',
|
|
54
|
-
'.dsh-recall-badge-modified{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover)}',
|
|
55
|
-
'.dsh-recall-badge-restored{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}',
|
|
56
|
-
'.dsh-recall-badge-added{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-interactive-bg-hover)}',
|
|
57
|
-
'.dsh-recall-rel{min-width:0;color:var(--dsw-alias-label-primary);word-break:break-all;font-family:var(--dsw-font-code, ui-monospace, SFMono-Regular, Consolas, monospace)}',
|
|
58
|
-
'.dsh-recall-panel-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:2px}',
|
|
59
|
-
'.dsh-recall-btn{border:none;border-radius:8px;padding:5px 14px;font-size:13px;line-height:20px;cursor:pointer;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}',
|
|
60
|
-
'.dsh-recall-btn:hover{color:var(--dsw-alias-label-primary)}',
|
|
61
|
-
'.dsh-recall-btn-danger{background:var(--dsw-alias-state-error-primary);color:#fff}',
|
|
62
|
-
'.dsh-recall-btn-danger:hover{color:#fff;filter:brightness(1.08)}',
|
|
63
|
-
'.dsh-recall-toast{position:fixed;top:18px;left:50%;transform:translateX(-50%);z-index:10000;max-width:min(560px,86vw);box-sizing:border-box;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:10px 16px;font-size:13px;line-height:20px;box-shadow:0 8px 28px rgba(0,0,0,.22);display:flex;align-items:baseline;gap:8px;opacity:0;transition:opacity .25s ease;pointer-events:auto}',
|
|
64
|
-
'.dsh-recall-toast.dsh-recall-toast-in{opacity:1}',
|
|
65
|
-
'.dsh-recall-toast-tag{flex:none;font-weight:600;color:var(--dsw-alias-state-error-primary)}'
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
try {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
return
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
return React.createElement('
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
),
|
|
267
|
-
|
|
268
|
-
React.createElement('div', { className: 'dsh-recall-panel-actions' },
|
|
269
|
-
React.createElement('button', { type: 'button', className: 'dsh-recall-btn', onClick: closePanel }, '
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
)
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
React.createElement('div', { className: 'dsh-recall-
|
|
283
|
-
recall.
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
1
|
+
/**
|
|
2
|
+
* dsh-recall-plugin — Client 半(持久插件形态,浏览器 bundle)
|
|
3
|
+
*
|
|
4
|
+
* 职责:注册 conversation.chat.node 的 user 渲染器,重绘用户消息气泡
|
|
5
|
+
* (文本/图片/JSON 块),在复制按钮旁加「撤回」按钮;二次确认面板展示
|
|
6
|
+
* 文件变更清单,确认后经 /api/recall/* 调用 Host 回退文件,并用官方
|
|
7
|
+
* sessions.fork 把对话一并回退(新会话打开、原会话归档)。
|
|
8
|
+
*
|
|
9
|
+
* 这是 dsh.client bundle(exports["./client"]),由 client-modules 打包成
|
|
10
|
+
* /plugins/<pkg>/client.js 注入页面;factory 内 require("react") 由平台
|
|
11
|
+
* 模块表提供。零构建依赖:纯 JS + React.createElement。
|
|
12
|
+
*/
|
|
13
|
+
window.__ModuleLoader__.load({
|
|
14
|
+
id: "dsh-recall-plugin",
|
|
15
|
+
factory: (require) => {
|
|
16
|
+
const React = require("react")
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
name: "dsh-recall-plugin",
|
|
20
|
+
apply(ctx) {
|
|
21
|
+
const slots = ctx.get('slots')
|
|
22
|
+
if (!slots) return
|
|
23
|
+
// 官方会话服务:fork 到已完成 turn 前缀 + open 切到新会话;
|
|
24
|
+
// workspaces 的归档只是从列表隐藏、可恢复,用来收走回退前的原会话。
|
|
25
|
+
const sessionsSvc = ctx.get('sessions')
|
|
26
|
+
const workspacesSvc = ctx.get('workspaces')
|
|
27
|
+
|
|
28
|
+
// Host HTTP API(动态插件的 harness RPC 在此换成 fetch 调用)
|
|
29
|
+
function api(name, args) {
|
|
30
|
+
return fetch('/api/recall/' + name, {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: { 'content-type': 'application/json' },
|
|
33
|
+
body: JSON.stringify(args || {})
|
|
34
|
+
}).then((r) => r.json())
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const css = [
|
|
38
|
+
'.dsh-recall-row{flex-direction:column;align-items:flex-end;gap:6px;display:flex}',
|
|
39
|
+
'.dsh-recall-stack{flex-direction:column;align-items:flex-end;gap:8px;min-width:0;max-width:min(525px,82%);display:flex}',
|
|
40
|
+
'.dsh-recall-bubble{background:var(--dsw-specific-bubble);max-width:100%;color:var(--dsw-alias-label-primary);border-radius:22px;padding:10px 16px;font-size:16px;line-height:24px;white-space:pre-wrap;word-break:break-word}',
|
|
41
|
+
'.dsh-recall-img{max-width:100%;max-height:320px;border-radius:12px;object-fit:contain}',
|
|
42
|
+
'.dsh-recall-json{margin:0;max-width:100%;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary);white-space:pre-wrap;word-break:break-word;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:8px 10px;background:var(--dsw-alias-markdown-code-block)}',
|
|
43
|
+
'.dsh-recall-actions{align-items:center;gap:10px;height:28px;display:flex}',
|
|
44
|
+
'.dsh-recall-time{color:var(--dsw-alias-label-tertiary);white-space:nowrap;padding-right:12px;font-size:14px;line-height:24px}',
|
|
45
|
+
'.dsh-recall-action{width:28px;height:28px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;border-radius:28px;justify-content:center;align-items:center;padding:6px;display:inline-flex}',
|
|
46
|
+
'.dsh-recall-action:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)}',
|
|
47
|
+
'@media (hover:hover){[data-time-hover-root] .dsh-recall-time{opacity:0;transition:opacity 80ms}[data-time-hover-root]:hover .dsh-recall-time,[data-time-hover-root]:focus-within .dsh-recall-time{opacity:1}}',
|
|
48
|
+
'.dsh-recall-panel{width:min(480px,100%);box-sizing:border-box;background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l1);border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:8px;text-align:left;box-shadow:0 8px 28px rgba(0,0,0,.22)}',
|
|
49
|
+
'.dsh-recall-panel-title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600;line-height:22px}',
|
|
50
|
+
'.dsh-recall-panel-note{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px;word-break:break-word}',
|
|
51
|
+
'.dsh-recall-list{max-height:220px;overflow:auto;display:flex;flex-direction:column;gap:2px;padding:4px 0}',
|
|
52
|
+
'.dsh-recall-file{display:flex;gap:8px;align-items:baseline;font-size:12px;line-height:20px}',
|
|
53
|
+
'.dsh-recall-badge{flex:none;font-size:11px;line-height:18px;padding:0 6px;border-radius:6px}',
|
|
54
|
+
'.dsh-recall-badge-modified{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover)}',
|
|
55
|
+
'.dsh-recall-badge-restored{color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}',
|
|
56
|
+
'.dsh-recall-badge-added{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-interactive-bg-hover)}',
|
|
57
|
+
'.dsh-recall-rel{min-width:0;color:var(--dsw-alias-label-primary);word-break:break-all;font-family:var(--dsw-font-code, ui-monospace, SFMono-Regular, Consolas, monospace)}',
|
|
58
|
+
'.dsh-recall-panel-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:2px}',
|
|
59
|
+
'.dsh-recall-btn{border:none;border-radius:8px;padding:5px 14px;font-size:13px;line-height:20px;cursor:pointer;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}',
|
|
60
|
+
'.dsh-recall-btn:hover{color:var(--dsw-alias-label-primary)}',
|
|
61
|
+
'.dsh-recall-btn-danger{background:var(--dsw-alias-state-error-primary);color:#fff}',
|
|
62
|
+
'.dsh-recall-btn-danger:hover{color:#fff;filter:brightness(1.08)}',
|
|
63
|
+
'.dsh-recall-toast{position:fixed;top:18px;left:50%;transform:translateX(-50%);z-index:10000;max-width:min(560px,86vw);box-sizing:border-box;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:10px 16px;font-size:13px;line-height:20px;box-shadow:0 8px 28px rgba(0,0,0,.22);display:flex;align-items:baseline;gap:8px;opacity:0;transition:opacity .25s ease;pointer-events:auto}',
|
|
64
|
+
'.dsh-recall-toast.dsh-recall-toast-in{opacity:1}',
|
|
65
|
+
'.dsh-recall-toast-tag{flex:none;font-weight:600;color:var(--dsw-alias-state-error-primary)}',
|
|
66
|
+
'.dsh-recall-ex-tab{display:flex;flex-direction:column;gap:16px;max-width:720px;padding:4px 0;text-align:left}',
|
|
67
|
+
'.dsh-recall-ex-card{display:flex;flex-direction:column;gap:8px}',
|
|
68
|
+
'.dsh-recall-ex-title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:600;line-height:22px}',
|
|
69
|
+
'.dsh-recall-ex-note{color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px;word-break:break-word}',
|
|
70
|
+
'.dsh-recall-ex-area{width:100%;box-sizing:border-box;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l1);border-radius:10px;padding:8px 10px;font-size:12px;line-height:20px;font-family:var(--dsw-font-code, ui-monospace, SFMono-Regular, Consolas, monospace);resize:vertical;min-height:120px}',
|
|
71
|
+
'.dsh-recall-ex-quick{display:flex;flex-wrap:wrap;gap:8px;align-items:center}',
|
|
72
|
+
'.dsh-recall-ex-input{flex:1;min-width:180px;box-sizing:border-box;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-border-l1);border-radius:8px;padding:5px 10px;font-size:13px;line-height:20px}',
|
|
73
|
+
'.dsh-recall-ex-chip{border:none;border-radius:6px;padding:2px 8px;font-size:12px;line-height:18px;cursor:pointer;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover)}',
|
|
74
|
+
'.dsh-recall-ex-chip:hover{color:var(--dsw-alias-label-primary)}',
|
|
75
|
+
'.dsh-recall-ex-status{margin-right:auto;font-size:12px;line-height:20px;color:var(--dsw-alias-label-tertiary)}',
|
|
76
|
+
'.dsh-recall-ex-status-error{color:var(--dsw-alias-state-error-primary)}',
|
|
77
|
+
'.dsh-recall-snap{display:flex;flex-direction:column;gap:2px;padding:4px 0;border-bottom:1px solid var(--dsw-alias-border-l1)}',
|
|
78
|
+
'.dsh-recall-snap:last-child{border-bottom:none}',
|
|
79
|
+
'.dsh-recall-snap-main{display:flex;gap:8px;align-items:baseline;font-size:12px;line-height:20px}',
|
|
80
|
+
'.dsh-recall-snap-meta{display:flex;gap:8px;align-items:baseline;font-size:11px;line-height:18px;color:var(--dsw-alias-label-tertiary)}',
|
|
81
|
+
'.dsh-recall-snap-ws{flex:none;font-weight:600;color:var(--dsw-alias-label-secondary)}',
|
|
82
|
+
'.dsh-recall-snap-confirm{display:flex;gap:8px;align-items:center;font-size:12px;line-height:20px;color:var(--dsw-alias-label-secondary)}'
|
|
83
|
+
].join('')
|
|
84
|
+
// 静态 bundle 的 ctx 可能不提供 styles 服务,降级为直接注入 <style>
|
|
85
|
+
const stylesSvc = ctx.get('styles')
|
|
86
|
+
if (stylesSvc && typeof stylesSvc.insert === 'function') {
|
|
87
|
+
stylesSvc.insert(css)
|
|
88
|
+
} else if (typeof document !== 'undefined') {
|
|
89
|
+
const tag = document.createElement('style')
|
|
90
|
+
tag.setAttribute('data-plugin', 'dsh-recall-plugin')
|
|
91
|
+
tag.textContent = css
|
|
92
|
+
document.head.appendChild(tag)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 每会话独立的 init 缓存:单槽缓存(initedSessionId + initDone)
|
|
96
|
+
// 在 A→B→A 切换后会让 A 复用 B 的 init promise——Host 侧虽有
|
|
97
|
+
// indexLoaded 兜底不至于出错,但 A 的 notice/降级提示会随 B 的
|
|
98
|
+
// 结果走,语义不对;Map 形态才与「每会话预热一次」的意图一致。
|
|
99
|
+
const initMap = new Map()
|
|
100
|
+
|
|
101
|
+
// 降级提示:每个种类每次页面加载只弹一次(Set 去重),避免切会话时
|
|
102
|
+
// 反复打扰。纯 DOM 直插(与剪贴板同样的零依赖思路),7 秒后自动淡出。
|
|
103
|
+
const noticeShown = new Set()
|
|
104
|
+
function showNotice(kind, text) {
|
|
105
|
+
if (noticeShown.has(kind) || typeof document === 'undefined') return
|
|
106
|
+
noticeShown.add(kind)
|
|
107
|
+
try {
|
|
108
|
+
const el = document.createElement('div')
|
|
109
|
+
el.className = 'dsh-recall-toast'
|
|
110
|
+
const tag = document.createElement('span')
|
|
111
|
+
tag.className = 'dsh-recall-toast-tag'
|
|
112
|
+
tag.textContent = '撤回插件'
|
|
113
|
+
const body = document.createElement('span')
|
|
114
|
+
body.textContent = text
|
|
115
|
+
el.appendChild(tag)
|
|
116
|
+
el.appendChild(body)
|
|
117
|
+
el.addEventListener('click', () => dismiss(), { once: true })
|
|
118
|
+
document.body.appendChild(el)
|
|
119
|
+
requestAnimationFrame(() => el.classList.add('dsh-recall-toast-in'))
|
|
120
|
+
const timer = setTimeout(dismiss, 7000)
|
|
121
|
+
let dismissed = false
|
|
122
|
+
function dismiss() {
|
|
123
|
+
if (dismissed) return
|
|
124
|
+
dismissed = true
|
|
125
|
+
clearTimeout(timer)
|
|
126
|
+
el.classList.remove('dsh-recall-toast-in')
|
|
127
|
+
setTimeout(() => el.remove(), 300)
|
|
128
|
+
}
|
|
129
|
+
} catch (e) { /* 提示失败不影响主流程 */ }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 每个会话只向 Host 注册一次(预热其根目录解析缓存)。
|
|
133
|
+
// 返回 init 的 promise:Host 端 init 要跑数条 PowerShell(建仓/loadIndex),
|
|
134
|
+
// snapshot-info 必须等它完成后再查,否则冷启动时索引尚未载入会误判
|
|
135
|
+
// has:false 且不再重试,撤回按钮将永不出现。
|
|
136
|
+
function ensureInit(sessionId) {
|
|
137
|
+
if (!sessionId) return Promise.resolve()
|
|
138
|
+
const cached = initMap.get(sessionId)
|
|
139
|
+
if (cached) return cached
|
|
140
|
+
const done = api('init', { sessionId }).then((res) => {
|
|
141
|
+
const notice = res && res.notice
|
|
142
|
+
if (notice && notice.unsupported) {
|
|
143
|
+
showNotice('unsupported', '撤回插件仅支持 Windows / Linux / macOS,当前平台的快照不可用。')
|
|
144
|
+
}
|
|
145
|
+
if (notice && notice.gitMissing) {
|
|
146
|
+
showNotice('git', '未检测到 git CLI,撤回功能不可用(快照引擎依赖 git)。安装 git 并重启 DSH 后即可使用。')
|
|
147
|
+
}
|
|
148
|
+
if (notice && notice.homeFallback) {
|
|
149
|
+
showNotice('home', 'home 目录不可写,快照已降级存储到项目内 .dsh-recall-snapshots 目录。')
|
|
150
|
+
}
|
|
151
|
+
}).catch(() => {
|
|
152
|
+
// init 失败(如页面先于 Host API 就绪加载)时清掉标记:否则本会话
|
|
153
|
+
// 内被判定“已初始化”,撤回按钮永不出现;清掉后下一条消息挂载会重试
|
|
154
|
+
initMap.delete(sessionId)
|
|
155
|
+
})
|
|
156
|
+
initMap.set(sessionId, done)
|
|
157
|
+
return done
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 消息时间:当天只显示时分,跨天显示月/日 时分
|
|
161
|
+
function clockText(ms) {
|
|
162
|
+
try {
|
|
163
|
+
// time 字段缺失或非法时返回空串:Invalid Date 不会 throw,
|
|
164
|
+
// 不拦会渲染出 "NaN/NaN NaN:NaN" 这样的坏时间戳
|
|
165
|
+
if (!ms || isNaN(new Date(ms).getTime())) return ''
|
|
166
|
+
const d = new Date(ms)
|
|
167
|
+
const now = new Date()
|
|
168
|
+
const sameDay = d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate()
|
|
169
|
+
const hh = String(d.getHours()).padStart(2, '0')
|
|
170
|
+
const mm = String(d.getMinutes()).padStart(2, '0')
|
|
171
|
+
return sameDay ? hh + ':' + mm : (d.getMonth() + 1) + '/' + d.getDate() + ' ' + hh + ':' + mm
|
|
172
|
+
} catch (e) {
|
|
173
|
+
return ''
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 复制按钮走浏览器剪贴板;无 primitives 依赖,直接调用并带降级
|
|
178
|
+
function writeClipboard(text) {
|
|
179
|
+
try {
|
|
180
|
+
if (navigator && navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
|
|
181
|
+
return navigator.clipboard.writeText(text).then(() => true, () => false)
|
|
182
|
+
}
|
|
183
|
+
} catch (e) { /* fall through */ }
|
|
184
|
+
try {
|
|
185
|
+
if (typeof document !== 'undefined' && typeof document.execCommand === 'function') {
|
|
186
|
+
const el = document.createElement('textarea')
|
|
187
|
+
el.value = text
|
|
188
|
+
el.setAttribute('readonly', '')
|
|
189
|
+
el.style.position = 'fixed'
|
|
190
|
+
el.style.left = '-9999px'
|
|
191
|
+
document.body.appendChild(el)
|
|
192
|
+
el.select()
|
|
193
|
+
try {
|
|
194
|
+
return Promise.resolve(document.execCommand('copy'))
|
|
195
|
+
} finally {
|
|
196
|
+
el.remove()
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} catch (e) { /* ignore */ }
|
|
200
|
+
return Promise.resolve(false)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function CopyIcon() {
|
|
204
|
+
return React.createElement('svg', { width: 16, height: 16, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', strokeWidth: 1.4, 'aria-hidden': true },
|
|
205
|
+
React.createElement('rect', { x: 5.5, y: 5.5, width: 8, height: 8, rx: 1.5 }),
|
|
206
|
+
React.createElement('path', { d: 'M10.5 5.5V4a1.5 1.5 0 0 0-1.5-1.5H4A1.5 1.5 0 0 0 2.5 4v5A1.5 1.5 0 0 0 4 10.5h1.5' })
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function CheckIcon() {
|
|
211
|
+
return React.createElement('svg', { width: 16, height: 16, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true },
|
|
212
|
+
React.createElement('path', { d: 'm3 8.5 3.2 3.2L13 5' })
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function UndoIcon() {
|
|
217
|
+
return React.createElement('svg', { width: 16, height: 16, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true },
|
|
218
|
+
React.createElement('path', { d: 'M7.5 3.5 3.5 7.5l4 4' }),
|
|
219
|
+
React.createElement('path', { d: 'M4.5 7.5h5a3 3 0 0 1 0 6H8' })
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function useImageSrc(attachment, loadImage) {
|
|
224
|
+
const [src, setSrc] = React.useState(null)
|
|
225
|
+
React.useEffect(() => {
|
|
226
|
+
let alive = true
|
|
227
|
+
if (!attachment || typeof loadImage !== 'function') return undefined
|
|
228
|
+
Promise.resolve(loadImage(attachment)).then((url) => {
|
|
229
|
+
if (alive && url) setSrc(String(url))
|
|
230
|
+
}).catch(() => {})
|
|
231
|
+
return () => { alive = false }
|
|
232
|
+
}, [attachment, loadImage])
|
|
233
|
+
return src
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function ImageBox(props) {
|
|
237
|
+
const src = useImageSrc(props.attachment, props.loadImage)
|
|
238
|
+
if (!src) return null
|
|
239
|
+
return React.createElement('img', { className: 'dsh-recall-img', src, alt: '' })
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// kind 语义单表承载(文案/徽章类名/汇总顺序):新增 kind 时只改
|
|
243
|
+
// 这一处,避免出现「映射表改了、CSS 类名忘了」的半改状态
|
|
244
|
+
const KIND_INFO = {
|
|
245
|
+
modified: { label: '修改', cls: 'modified' },
|
|
246
|
+
restored: { label: '恢复', cls: 'restored' },
|
|
247
|
+
added: { label: '删除', cls: 'added' }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function summaryText(counts) {
|
|
251
|
+
const parts = []
|
|
252
|
+
for (const kind of Object.keys(KIND_INFO)) {
|
|
253
|
+
if (counts[kind] > 0) parts.push(KIND_INFO[kind].label + ' ' + counts[kind])
|
|
254
|
+
}
|
|
255
|
+
return parts.join(' · ')
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function recallPanel(recall, closePanel, executeRecall) {
|
|
259
|
+
if (recall.stage === 'loading') {
|
|
260
|
+
return React.createElement('div', { className: 'dsh-recall-panel' },
|
|
261
|
+
React.createElement('div', { className: 'dsh-recall-panel-title' }, '正在计算变更…')
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
if (recall.stage === 'error') {
|
|
265
|
+
return React.createElement('div', { className: 'dsh-recall-panel' },
|
|
266
|
+
React.createElement('div', { className: 'dsh-recall-panel-title' }, '无法回退'),
|
|
267
|
+
React.createElement('div', { className: 'dsh-recall-panel-note' }, recall.message || ''),
|
|
268
|
+
React.createElement('div', { className: 'dsh-recall-panel-actions' },
|
|
269
|
+
React.createElement('button', { type: 'button', className: 'dsh-recall-btn', onClick: closePanel }, '关闭')
|
|
270
|
+
)
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
if (recall.stage === 'confirm') {
|
|
274
|
+
const changes = recall.changes || []
|
|
275
|
+
const total = typeof recall.total === 'number' ? recall.total : changes.length
|
|
276
|
+
const counts = { modified: 0, restored: 0, added: 0 }
|
|
277
|
+
for (const c of changes) {
|
|
278
|
+
if (c && counts[c.kind] !== undefined) counts[c.kind]++
|
|
279
|
+
}
|
|
280
|
+
const rows = changes.map((c, i) => {
|
|
281
|
+
const info = KIND_INFO[c.kind]
|
|
282
|
+
return React.createElement('div', { className: 'dsh-recall-file', key: i },
|
|
283
|
+
React.createElement('span', { className: 'dsh-recall-badge dsh-recall-badge-' + (info ? info.cls : '') }, info ? info.label : (c.kind || '')),
|
|
284
|
+
React.createElement('span', { className: 'dsh-recall-rel' }, c.rel || '')
|
|
285
|
+
)
|
|
286
|
+
})
|
|
287
|
+
if (recall.truncated) {
|
|
288
|
+
rows.push(React.createElement('div', { className: 'dsh-recall-panel-note', key: 'truncated' }, '…仅显示前 ' + changes.length + ' 条,共 ' + total + ' 个文件将变更'))
|
|
289
|
+
}
|
|
290
|
+
// cutSeq 为 null 表示该消息是会话第一条用户消息:文件可回退但对话无从回退
|
|
291
|
+
const canRevertChat = typeof recall.cutSeq === 'number'
|
|
292
|
+
return React.createElement('div', { className: 'dsh-recall-panel' },
|
|
293
|
+
React.createElement('div', { className: 'dsh-recall-panel-title' }, '整段回退'),
|
|
294
|
+
React.createElement('div', { className: 'dsh-recall-panel-note' },
|
|
295
|
+
'将项目恢复到' + (recall.time ? ' ' + clockText(recall.time) + ' ' : ' ') + '发送该消息时的状态。共 ' + total + ' 个文件将变更' + (summaryText(counts) ? '(' + summaryText(counts) + ')' : '') + '。此操作会覆盖当前文件内容;回退前会自动保存一份当前状态的安全快照(不含在下方清单内)。'
|
|
296
|
+
),
|
|
297
|
+
React.createElement('div', { className: 'dsh-recall-panel-note' },
|
|
298
|
+
canRevertChat
|
|
299
|
+
? '对话将一并回退到该消息之前:该消息及之后的全部对话会从当前视图移除,原会话归档保存(可从归档找回)。'
|
|
300
|
+
: '该消息是本会话中第一条用户消息,无法回退对话;确认后仅回退项目文件。'
|
|
301
|
+
),
|
|
302
|
+
changes.length > 0 ? React.createElement('div', { className: 'dsh-recall-list' }, ...rows) : null,
|
|
303
|
+
React.createElement('div', { className: 'dsh-recall-panel-actions' },
|
|
304
|
+
React.createElement('button', { type: 'button', className: 'dsh-recall-btn', onClick: closePanel }, '取消'),
|
|
305
|
+
React.createElement('button', { type: 'button', className: 'dsh-recall-btn dsh-recall-btn-danger', onClick: executeRecall }, '确认回退')
|
|
306
|
+
)
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
if (recall.stage === 'executing') {
|
|
310
|
+
return React.createElement('div', { className: 'dsh-recall-panel' },
|
|
311
|
+
React.createElement('div', { className: 'dsh-recall-panel-title' }, '正在回退…')
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
if (recall.stage === 'done') {
|
|
315
|
+
return React.createElement('div', { className: 'dsh-recall-panel' },
|
|
316
|
+
React.createElement('div', { className: 'dsh-recall-panel-title' }, '回退完成'),
|
|
317
|
+
React.createElement('div', { className: 'dsh-recall-panel-note' },
|
|
318
|
+
recall.chatReverted
|
|
319
|
+
? '项目文件与对话已回退到该消息之前。新会话已打开,原会话已归档(可从归档找回)。'
|
|
320
|
+
: '项目已恢复到发送该消息时的状态。' + (recall.chatError ? ' 对话回退失败:' + recall.chatError : '')
|
|
321
|
+
),
|
|
322
|
+
React.createElement('div', { className: 'dsh-recall-panel-actions' },
|
|
323
|
+
React.createElement('button', { type: 'button', className: 'dsh-recall-btn', onClick: closePanel }, '关闭')
|
|
324
|
+
)
|
|
325
|
+
)
|
|
326
|
+
}
|
|
327
|
+
return null
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function UserRecallNode(props) {
|
|
331
|
+
const node = props && props.node
|
|
332
|
+
const loadImage = props && props.loadImage
|
|
333
|
+
const sessionId = props && props.sessionId
|
|
334
|
+
const data = node && node.data ? node.data : {}
|
|
335
|
+
// node.id 是会话事件匹配时写入的真实消息 ID;node.key 是位置键(如 13:input),不能用于快照查询
|
|
336
|
+
const messageId = node ? String(node.id || node.key || '') : ''
|
|
337
|
+
const blocks = Array.isArray(data.content) ? data.content : []
|
|
338
|
+
const text = blocks.filter((b) => b && b.type === 'text' && typeof b.text === 'string').map((b) => b.text).join('')
|
|
339
|
+
const images = blocks.filter((b) => b && b.type === 'image' && b.attachment).map((b) => b.attachment)
|
|
340
|
+
const rest = blocks.filter((b) => !b || !(b.type === 'text' && typeof b.text === 'string') && !(b.type === 'image' && b.attachment))
|
|
341
|
+
|
|
342
|
+
const [copied, setCopied] = React.useState(false)
|
|
343
|
+
const [hasSnapshot, setHasSnapshot] = React.useState(false)
|
|
344
|
+
const [recall, setRecall] = React.useState({ stage: 'idle' })
|
|
345
|
+
|
|
346
|
+
React.useEffect(() => {
|
|
347
|
+
let alive = true
|
|
348
|
+
// 先等 init 预热完成再查快照存在性:避免索引未载入时误判 has:false
|
|
349
|
+
ensureInit(sessionId).then(() => {
|
|
350
|
+
if (!messageId || !alive) return
|
|
351
|
+
return api('snapshot-info', { messageId }).then((res) => {
|
|
352
|
+
if (alive && res && res.has) setHasSnapshot(true)
|
|
353
|
+
}).catch(() => {})
|
|
354
|
+
})
|
|
355
|
+
return () => { alive = false }
|
|
356
|
+
}, [messageId, sessionId])
|
|
357
|
+
|
|
358
|
+
const onCopy = () => {
|
|
359
|
+
if (copied) return
|
|
360
|
+
writeClipboard(text).then(() => {
|
|
361
|
+
setCopied(true)
|
|
362
|
+
const timer = ctx.get('timer')
|
|
363
|
+
if (timer && typeof timer.timeout === 'function') {
|
|
364
|
+
timer.timeout(() => setCopied(false), 1200)
|
|
365
|
+
} else {
|
|
366
|
+
setTimeout(() => setCopied(false), 1200)
|
|
367
|
+
}
|
|
368
|
+
})
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const openPreview = () => {
|
|
372
|
+
if (recall.stage === 'loading' || recall.stage === 'executing') return
|
|
373
|
+
setRecall({ stage: 'loading' })
|
|
374
|
+
api('preview', { messageId, sessionId }).then((res) => {
|
|
375
|
+
if (!res || !res.ok) {
|
|
376
|
+
setRecall({ stage: 'error', message: (res && (res.message || res.error)) || '无法获取快照' })
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
setRecall({
|
|
380
|
+
stage: 'confirm',
|
|
381
|
+
changes: res.changes || [],
|
|
382
|
+
total: typeof res.total === 'number' ? res.total : (res.changes || []).length,
|
|
383
|
+
truncated: Boolean(res.truncated),
|
|
384
|
+
time: res.time || null,
|
|
385
|
+
cutSeq: typeof res.cutSeq === 'number' ? res.cutSeq : null
|
|
386
|
+
})
|
|
387
|
+
}).catch((error) => {
|
|
388
|
+
setRecall({ stage: 'error', message: String(error) })
|
|
389
|
+
})
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const executeRecall = () => {
|
|
393
|
+
if (recall.stage !== 'confirm') return
|
|
394
|
+
const changes = recall.changes || []
|
|
395
|
+
const previewCut = typeof recall.cutSeq === 'number' ? recall.cutSeq : null
|
|
396
|
+
setRecall({ stage: 'executing', changes })
|
|
397
|
+
api('execute', { messageId, sessionId }).then(async (res) => {
|
|
398
|
+
if (!res || !res.ok) {
|
|
399
|
+
setRecall({ stage: 'error', message: (res && (res.message || res.error)) || '回退失败' })
|
|
400
|
+
return
|
|
401
|
+
}
|
|
402
|
+
// 文件已回退;对话回退独立进行,失败只降级为“仅文件回退”而不是整体失败
|
|
403
|
+
const cutSeq = typeof res.cutSeq === 'number' ? res.cutSeq : previewCut
|
|
404
|
+
let chatReverted = false
|
|
405
|
+
let chatError = ''
|
|
406
|
+
if (cutSeq !== null && sessionsSvc && typeof sessionsSvc.fork === 'function') {
|
|
407
|
+
try {
|
|
408
|
+
// 撤回语义是「回退」而非「复制」:新会话顶替原会话(原会话已
|
|
409
|
+
// 归档),必须原样继承标题。increaseTitle 是官方侧栏「复制会话」
|
|
410
|
+
// 用来区分新旧会话的,会把标题改成「xxx 2」且多次撤回时数字
|
|
411
|
+
// 不断递增,与「同一会话回退」的用户观感相悖,故不传。
|
|
412
|
+
const childId = await sessionsSvc.fork({ sessionId, atSeq: cutSeq })
|
|
413
|
+
if (childId) {
|
|
414
|
+
if (typeof sessionsSvc.open === 'function') sessionsSvc.open(childId)
|
|
415
|
+
chatReverted = true
|
|
416
|
+
// 回退前的原会话归档:只是从列表隐藏、可恢复,避免侧栏出现两个近似会话
|
|
417
|
+
if (workspacesSvc && typeof workspacesSvc.archiveSession === 'function') {
|
|
418
|
+
workspacesSvc.archiveSession(sessionId).catch(() => {})
|
|
419
|
+
}
|
|
420
|
+
} else {
|
|
421
|
+
chatError = '未返回新会话'
|
|
422
|
+
}
|
|
423
|
+
} catch (error) {
|
|
424
|
+
chatError = String(error)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
setHasSnapshot(false)
|
|
428
|
+
// 注:快照 tag 在 Host 侧有意保留(幂等回退),刷新页面后
|
|
429
|
+
// 该消息的撤回按钮会重新出现——这是「可再次回退到同一点」
|
|
430
|
+
// 的特性而非 bug;此处 setHasSnapshot(false) 只是当前视图
|
|
431
|
+
// 的即时反馈,不代表快照已删。
|
|
432
|
+
setRecall({ stage: 'done', count: typeof res.count === 'number' ? res.count : changes.length, chatReverted, chatError })
|
|
433
|
+
}).catch((error) => {
|
|
434
|
+
setRecall({ stage: 'error', message: String(error) })
|
|
435
|
+
})
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const closePanel = () => setRecall({ stage: 'idle' })
|
|
439
|
+
|
|
440
|
+
const bubbleChildren = []
|
|
441
|
+
if (text !== '') bubbleChildren.push(React.createElement('div', { className: 'dsh-recall-bubble', key: 'text' }, text))
|
|
442
|
+
for (let i = 0; i < images.length; i++) {
|
|
443
|
+
bubbleChildren.push(React.createElement(ImageBox, { key: 'img-' + i, attachment: images[i], loadImage }))
|
|
444
|
+
}
|
|
445
|
+
for (let i = 0; i < rest.length; i++) {
|
|
446
|
+
bubbleChildren.push(React.createElement('pre', { className: 'dsh-recall-json', key: 'rest-' + i }, JSON.stringify(rest[i], null, 2)))
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const actions = []
|
|
450
|
+
actions.push(React.createElement('span', { className: 'dsh-recall-time', key: 'time' }, clockText(data.time)))
|
|
451
|
+
actions.push(React.createElement('button', {
|
|
452
|
+
key: 'copy',
|
|
453
|
+
type: 'button',
|
|
454
|
+
className: 'dsh-recall-action',
|
|
455
|
+
'aria-label': copied ? '已复制' : '复制',
|
|
456
|
+
title: copied ? '已复制' : '复制',
|
|
457
|
+
onClick: onCopy
|
|
458
|
+
}, copied ? React.createElement(CheckIcon, {}) : React.createElement(CopyIcon, {})))
|
|
459
|
+
if (hasSnapshot) {
|
|
460
|
+
actions.push(React.createElement('button', {
|
|
461
|
+
key: 'recall',
|
|
462
|
+
type: 'button',
|
|
463
|
+
className: 'dsh-recall-action',
|
|
464
|
+
'aria-label': '撤回',
|
|
465
|
+
title: '整段回退:文件与对话一并回到该消息之前',
|
|
466
|
+
onClick: openPreview
|
|
467
|
+
}, React.createElement(UndoIcon, {})))
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return React.createElement('div', { className: 'dsh-recall-row', 'data-time-hover-root': true },
|
|
471
|
+
bubbleChildren.length > 0 ? React.createElement('div', { className: 'dsh-recall-stack', key: 'stack' }, ...bubbleChildren) : null,
|
|
472
|
+
React.createElement('div', { className: 'dsh-recall-actions', key: 'actions' }, ...actions),
|
|
473
|
+
recallPanel(recall, closePanel, executeRecall)
|
|
474
|
+
)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ---- 设置页「撤回设置」标签:exclude.txt 可视化快速编辑 ----
|
|
478
|
+
|
|
479
|
+
// 常用排除建议:一键追加的高频项,覆盖构建产物/日志/密钥三类最
|
|
480
|
+
// 常见诉求;已存在的条目自动从候选里滤掉,避免重复点击堆叠。
|
|
481
|
+
const EXCLUDE_SUGGESTIONS = ['dist/', 'build/', 'out/', 'coverage/', '*.log', '.env']
|
|
482
|
+
|
|
483
|
+
// 单个 exclude 文件的编辑卡片。draft/baseline 分离实现「未保存
|
|
484
|
+
// 修改」判定(textarea 所见即将保存的原文,不偷偷规范化);
|
|
485
|
+
// key=file.path 挂载,父级重载列表时整卡重建、草稿随之丢弃。
|
|
486
|
+
function ExcludeCard(props) {
|
|
487
|
+
const file = props.file
|
|
488
|
+
const [draft, setDraft] = React.useState(file.content || '')
|
|
489
|
+
const [baseline, setBaseline] = React.useState(file.content || '')
|
|
490
|
+
const [quick, setQuick] = React.useState('')
|
|
491
|
+
const [state, setState] = React.useState({ busy: false, message: '', error: false })
|
|
492
|
+
const dirty = draft !== baseline
|
|
493
|
+
|
|
494
|
+
// 追加一条模式:先补齐行尾换行再拼接,保证每条模式独占一行
|
|
495
|
+
// (exclude.txt 按行解析,两条挤一行会双双失效)
|
|
496
|
+
function appendPattern(pattern) {
|
|
497
|
+
setDraft((d) => (d && !d.endsWith('\n') ? d + '\n' : d) + pattern + '\n')
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function addQuick() {
|
|
501
|
+
const t = quick.trim()
|
|
502
|
+
if (!t) return
|
|
503
|
+
appendPattern(t)
|
|
504
|
+
setQuick('')
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function save() {
|
|
508
|
+
if (state.busy || !dirty) return
|
|
509
|
+
setState({ busy: true, message: '保存中…', error: false })
|
|
510
|
+
api('exclude-set', { path: file.path, content: draft }).then((res) => {
|
|
511
|
+
if (res && res.ok) {
|
|
512
|
+
setBaseline(draft)
|
|
513
|
+
setState({ busy: false, message: '已保存,下一次快照 / 预览 / 回退时生效', error: false })
|
|
514
|
+
} else {
|
|
515
|
+
setState({ busy: false, message: (res && (res.message || res.error)) || '保存失败', error: true })
|
|
516
|
+
}
|
|
517
|
+
}).catch((error) => {
|
|
518
|
+
setState({ busy: false, message: String(error), error: true })
|
|
519
|
+
})
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function discard() {
|
|
523
|
+
if (state.busy) return
|
|
524
|
+
setDraft(baseline)
|
|
525
|
+
setState({ busy: false, message: '', error: false })
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const draftLines = draft.split('\n').map((l) => l.trim())
|
|
529
|
+
const suggestions = EXCLUDE_SUGGESTIONS.filter((s) => draftLines.indexOf(s) < 0)
|
|
530
|
+
|
|
531
|
+
return React.createElement('div', { className: 'dsh-recall-ex-card' },
|
|
532
|
+
React.createElement('div', { className: 'dsh-recall-ex-title' }, '快照排除项'),
|
|
533
|
+
React.createElement('div', { className: 'dsh-recall-ex-note' },
|
|
534
|
+
file.home
|
|
535
|
+
? '此配置全局共享,对所有工作区的快照生效(存储位置:' + file.path + ')。'
|
|
536
|
+
: 'home 目录不可写时此工作区降级存储,排除配置独立生效(存储位置:' + file.path + ')。'
|
|
537
|
+
),
|
|
538
|
+
React.createElement('div', { className: 'dsh-recall-ex-note' }, 'gitignore 语法,一行一条,支持 # 注释;命中排除的文件与目录不进入快照,也不会被回退触碰。'),
|
|
539
|
+
React.createElement('textarea', {
|
|
540
|
+
className: 'dsh-recall-ex-area',
|
|
541
|
+
value: draft,
|
|
542
|
+
spellCheck: false,
|
|
543
|
+
onChange: (e) => setDraft(e.target.value)
|
|
544
|
+
}),
|
|
545
|
+
React.createElement('div', { className: 'dsh-recall-ex-quick' },
|
|
546
|
+
React.createElement('input', {
|
|
547
|
+
className: 'dsh-recall-ex-input',
|
|
548
|
+
value: quick,
|
|
549
|
+
placeholder: '输入路径或模式,回车快速添加',
|
|
550
|
+
onChange: (e) => setQuick(e.target.value),
|
|
551
|
+
onKeyDown: (e) => { if (e.key === 'Enter') { e.preventDefault(); addQuick() } }
|
|
552
|
+
}),
|
|
553
|
+
React.createElement('button', { type: 'button', className: 'dsh-recall-btn', onClick: addQuick }, '添加'),
|
|
554
|
+
...suggestions.map((s) => React.createElement('button', {
|
|
555
|
+
key: 'chip-' + s,
|
|
556
|
+
type: 'button',
|
|
557
|
+
className: 'dsh-recall-ex-chip',
|
|
558
|
+
title: '点击追加 ' + s,
|
|
559
|
+
onClick: () => appendPattern(s)
|
|
560
|
+
}, s))
|
|
561
|
+
),
|
|
562
|
+
React.createElement('div', { className: 'dsh-recall-panel-actions' },
|
|
563
|
+
state.message ? React.createElement('span', { className: 'dsh-recall-ex-status' + (state.error ? ' dsh-recall-ex-status-error' : '') }, state.message) : null,
|
|
564
|
+
React.createElement('button', { type: 'button', className: 'dsh-recall-btn', disabled: !dirty || state.busy, onClick: discard }, '放弃修改'),
|
|
565
|
+
React.createElement('button', { type: 'button', className: 'dsh-recall-btn dsh-recall-btn-danger', disabled: !dirty || state.busy, onClick: save }, '保存')
|
|
566
|
+
)
|
|
567
|
+
)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// 快照管理卡片:列表(时间倒序)/ 磁盘占用 / 单条删除 / 手动 gc /
|
|
571
|
+
// 最近错误。此前用户唯一的治理手段是等 gc 节流联动,无任何自助
|
|
572
|
+
// 清理与排障入口;全部操作走 Host 的 manage/status 端点(串行队列
|
|
573
|
+
// 在 Host 侧保证,卡片只管发请求)。
|
|
574
|
+
function ManageCard(props) {
|
|
575
|
+
const sessionId = props.sessionId
|
|
576
|
+
const [items, setItems] = React.useState(null)
|
|
577
|
+
const [usage, setUsage] = React.useState(null)
|
|
578
|
+
const [errors, setErrors] = React.useState(null)
|
|
579
|
+
const [state, setState] = React.useState({ busy: false, message: '', error: false })
|
|
580
|
+
// 冷会话标题在服务端要整日志解压(10 秒级),首屏不等它:列表
|
|
581
|
+
// 先出(live/缓存标题),拿到后对缺标题的会话异步补拉再合并。
|
|
582
|
+
const [titlesPending, setTitlesPending] = React.useState(false)
|
|
583
|
+
|
|
584
|
+
function sizeText(bytes) {
|
|
585
|
+
if (!bytes || bytes <= 0) return '0 MB'
|
|
586
|
+
if (bytes < 1048576) return (bytes / 1024).toFixed(0) + ' KB'
|
|
587
|
+
if (bytes < 1073741824) return (bytes / 1048576).toFixed(1) + ' MB'
|
|
588
|
+
return (bytes / 1073741824).toFixed(2) + ' GB'
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function fetchTitles(list) {
|
|
592
|
+
const missing = Array.from(new Set(
|
|
593
|
+
(list || []).filter((it) => it.sessionId && !it.sessionTitle).map((it) => it.sessionId)
|
|
594
|
+
)).slice(0, 100)
|
|
595
|
+
if (!missing.length) { setTitlesPending(false); return }
|
|
596
|
+
setTitlesPending(true)
|
|
597
|
+
api('manage', { op: 'titles', sessionIds: missing }).then((res) => {
|
|
598
|
+
const map = res && res.ok ? res.titles : null
|
|
599
|
+
if (map) {
|
|
600
|
+
setItems((prev) => (prev || []).map((it) => (
|
|
601
|
+
it.sessionId && map[it.sessionId] ? Object.assign({}, it, { sessionTitle: map[it.sessionId] }) : it
|
|
602
|
+
)))
|
|
603
|
+
}
|
|
604
|
+
setTitlesPending(false)
|
|
605
|
+
}).catch(() => setTitlesPending(false))
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function refresh() {
|
|
609
|
+
api('manage', { op: 'list' }).then((res) => {
|
|
610
|
+
if (res && res.ok) {
|
|
611
|
+
setItems(res.items || [])
|
|
612
|
+
fetchTitles(res.items || [])
|
|
613
|
+
}
|
|
614
|
+
}).catch(() => {})
|
|
615
|
+
if (sessionId) {
|
|
616
|
+
api('manage', { op: 'usage', sessionId }).then((res) => {
|
|
617
|
+
if (res && res.ok) setUsage(res.bytes || 0)
|
|
618
|
+
}).catch(() => {})
|
|
619
|
+
}
|
|
620
|
+
api('status', {}).then((res) => {
|
|
621
|
+
if (res && res.ok) setErrors(res.errors || [])
|
|
622
|
+
}).catch(() => {})
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
React.useEffect(() => { refresh() }, [sessionId])
|
|
626
|
+
|
|
627
|
+
function run(op, extra, doneText) {
|
|
628
|
+
if (state.busy) return
|
|
629
|
+
setState({ busy: true, message: '执行中…', error: false })
|
|
630
|
+
api('manage', Object.assign({ op, sessionId }, extra || {})).then((res) => {
|
|
631
|
+
if (res && res.ok) {
|
|
632
|
+
setState({ busy: false, message: doneText, error: false })
|
|
633
|
+
refresh()
|
|
634
|
+
} else {
|
|
635
|
+
setState({ busy: false, message: (res && (res.message || res.error)) || '操作失败', error: true })
|
|
636
|
+
}
|
|
637
|
+
}).catch((e) => setState({ busy: false, message: String(e), error: true }))
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// 列表行:第一行工作区名 + 会话标题(已删除会话标灰)+ 时间 +
|
|
641
|
+
// 截断 ID;第二行删除按钮。完整 ID 仍走行 title 悬停。
|
|
642
|
+
// 删除走行内二次确认(confirmingId 记录正在确认的行):
|
|
643
|
+
// 点「删除」先换成「确认删除?是/否」,避免误点直接清 tag。
|
|
644
|
+
const [confirmingId, setConfirmingId] = React.useState(null)
|
|
645
|
+
const rows = (items || []).slice(0, 50).map((it) => {
|
|
646
|
+
const confirming = confirmingId === it.id
|
|
647
|
+
return React.createElement('div', { className: 'dsh-recall-snap', key: it.id, title: it.id },
|
|
648
|
+
React.createElement('div', { className: 'dsh-recall-snap-main' },
|
|
649
|
+
React.createElement('span', { className: 'dsh-recall-snap-ws' }, it.workspace || '未知工作区'),
|
|
650
|
+
React.createElement('span', { className: 'dsh-recall-rel' }, it.sessionTitle || (titlesPending && it.sessionId ? '…' : '(已删除会话)')),
|
|
651
|
+
React.createElement('span', { className: 'dsh-recall-snap-meta' }, clockText(it.time) + ' ' + it.id.slice(0, 12) + '…')
|
|
652
|
+
),
|
|
653
|
+
confirming
|
|
654
|
+
? React.createElement('div', { className: 'dsh-recall-snap-confirm' },
|
|
655
|
+
'确认删除该快照?此操作不可恢复。',
|
|
656
|
+
React.createElement('button', {
|
|
657
|
+
type: 'button',
|
|
658
|
+
className: 'dsh-recall-ex-chip',
|
|
659
|
+
onClick: () => { setConfirmingId(null); run('delete', { messageId: it.id, root: it.root || null }, '已删除') }
|
|
660
|
+
}, '确认'),
|
|
661
|
+
React.createElement('button', { type: 'button', className: 'dsh-recall-ex-chip', onClick: () => setConfirmingId(null) }, '取消')
|
|
662
|
+
)
|
|
663
|
+
: React.createElement('div', null,
|
|
664
|
+
React.createElement('button', {
|
|
665
|
+
type: 'button',
|
|
666
|
+
className: 'dsh-recall-ex-chip',
|
|
667
|
+
title: '删除该快照(tag 与索引条目)',
|
|
668
|
+
onClick: () => setConfirmingId(it.id)
|
|
669
|
+
}, '删除')
|
|
670
|
+
)
|
|
671
|
+
)
|
|
672
|
+
})
|
|
673
|
+
|
|
674
|
+
return React.createElement('div', { className: 'dsh-recall-ex-card' },
|
|
675
|
+
React.createElement('div', { className: 'dsh-recall-ex-title' }, '快照管理'),
|
|
676
|
+
React.createElement('div', { className: 'dsh-recall-ex-note' },
|
|
677
|
+
usage === null
|
|
678
|
+
? '共 ' + (items ? items.length : '…') + ' 条快照。'
|
|
679
|
+
: '共 ' + (items ? items.length : '…') + ' 条快照,当前工作区快照存储占用 ' + sizeText(usage) + '。'
|
|
680
|
+
),
|
|
681
|
+
rows.length > 0 ? React.createElement('div', { className: 'dsh-recall-list' }, ...rows) : null,
|
|
682
|
+
React.createElement('div', { className: 'dsh-recall-panel-actions' },
|
|
683
|
+
state.message ? React.createElement('span', { className: 'dsh-recall-ex-status' + (state.error ? ' dsh-recall-ex-status-error' : '') }, state.message) : null,
|
|
684
|
+
React.createElement('button', { type: 'button', className: 'dsh-recall-btn', disabled: state.busy, onClick: refresh }, '刷新'),
|
|
685
|
+
React.createElement('button', {
|
|
686
|
+
type: 'button',
|
|
687
|
+
className: 'dsh-recall-btn',
|
|
688
|
+
disabled: state.busy || !sessionId,
|
|
689
|
+
title: '立即执行一次 git gc(压缩对象库释放空间)',
|
|
690
|
+
onClick: () => run('gc', {}, 'gc 完成')
|
|
691
|
+
}, '立即 gc')
|
|
692
|
+
),
|
|
693
|
+
errors && errors.length > 0
|
|
694
|
+
? React.createElement('div', { className: 'dsh-recall-ex-note' },
|
|
695
|
+
'最近错误:',
|
|
696
|
+
errors.slice(0, 5).map((e, i) => React.createElement('div', { key: i, className: 'dsh-recall-ex-note' }, clockText(e.time) + ' ' + e.message))
|
|
697
|
+
)
|
|
698
|
+
: null
|
|
699
|
+
)
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// 「插件」设置分区的标签页主体:拉取 Host 枚举的 exclude 文件列表
|
|
703
|
+
// (home 存储通常合并为一条,降级工作区各一条)。标签页被选中后
|
|
704
|
+
// 由设置外壳保持挂载,本地草稿在标签间切换时不丢失。
|
|
705
|
+
function RecallSettingsTab(props) {
|
|
706
|
+
const [files, setFiles] = React.useState(null)
|
|
707
|
+
const [error, setError] = React.useState('')
|
|
708
|
+
|
|
709
|
+
function load() {
|
|
710
|
+
api('exclude-get', {}).then((res) => {
|
|
711
|
+
if (res && res.ok) { setFiles(res.files || []); setError(''); return }
|
|
712
|
+
if (res && res.unsupported) { setError('当前平台不支持快照功能,排除配置不可用。'); return }
|
|
713
|
+
setError((res && (res.message || res.error)) || '无法读取排除配置')
|
|
714
|
+
}).catch((e) => setError(String(e)))
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
React.useEffect(() => { load() }, [])
|
|
718
|
+
|
|
719
|
+
let body = null
|
|
720
|
+
if (error) {
|
|
721
|
+
body = React.createElement('div', { className: 'dsh-recall-ex-card' },
|
|
722
|
+
React.createElement('div', { className: 'dsh-recall-ex-note' }, error),
|
|
723
|
+
React.createElement('div', { className: 'dsh-recall-panel-actions' },
|
|
724
|
+
React.createElement('button', { type: 'button', className: 'dsh-recall-btn', onClick: load }, '重试')
|
|
725
|
+
)
|
|
726
|
+
)
|
|
727
|
+
} else if (files === null) {
|
|
728
|
+
body = React.createElement('div', { className: 'dsh-recall-ex-note' }, '正在加载排除配置…')
|
|
729
|
+
} else if (!files.length) {
|
|
730
|
+
// 全新安装且从未发过消息:store 还没建,先告诉用户怎么触发
|
|
731
|
+
body = React.createElement('div', { className: 'dsh-recall-ex-note' }, '尚未创建任何快照存储:在任意工作区发送一条消息后,这里会出现可编辑的排除配置。')
|
|
732
|
+
} else {
|
|
733
|
+
body = React.createElement('div', { className: 'dsh-recall-ex-card' },
|
|
734
|
+
...files.map((f) => React.createElement(ExcludeCard, { key: f.path, file: f }))
|
|
735
|
+
)
|
|
736
|
+
}
|
|
737
|
+
// 快照管理卡片挂标签页底部:与排除配置同属撤回治理面,
|
|
738
|
+
// sessionId 从设置外壳 props 透传(usage/gc 按工作区定位 store)
|
|
739
|
+
return React.createElement('div', { className: 'dsh-recall-ex-tab' },
|
|
740
|
+
body,
|
|
741
|
+
React.createElement(ManageCard, { sessionId: props && props.sessionId })
|
|
742
|
+
)
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// keyed slot 显式用低于默认 0 的 priority 注册:默认 0 通常被平台/
|
|
746
|
+
// 其他插件的渲染器占据,不指定 priority 会因 keyed slot 冲突拒载
|
|
747
|
+
// 整个插件;lowest renders,负值恰好覆盖默认渲染器实现撤回 UI。
|
|
748
|
+
// priority -1 也可能被别的机器上的插件占用(同样抛冲突),所以
|
|
749
|
+
// 递减重试三次——最坏情况只是撤回按钮不渲染,绝不让插件加载失败。
|
|
750
|
+
let mounted = false
|
|
751
|
+
for (let priority = -1; priority >= -3 && !mounted; priority--) {
|
|
752
|
+
try {
|
|
753
|
+
slots.inject('conversation.chat.node', () => slots.register(
|
|
754
|
+
{ name: 'conversation.chat.node', key: 'user', priority },
|
|
755
|
+
UserRecallNode
|
|
756
|
+
))
|
|
757
|
+
mounted = true
|
|
758
|
+
} catch (error) {
|
|
759
|
+
if (priority === -3) console.error('[dsh-recall-plugin] slot register failed:', error)
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// 「插件」设置分区挂自己的标签页:settings.plugins.tab 是 root 级
|
|
764
|
+
// list slot,id 唯一即无冲突(与 keyed slot 的 priority 冲突是两套
|
|
765
|
+
// 语义),order 20 排在官方「插件配置」(0) 与「插件清单」(10) 之后。
|
|
766
|
+
// 该 slot 由 ui-settings-plugins 声明;未组装设置页的部署里 inject
|
|
767
|
+
// 只是永不匹配的待定注册,天然无害。try/catch 是防御性兜底——注册
|
|
768
|
+
// 失败绝不该拖垮撤回按钮的主功能。
|
|
769
|
+
try {
|
|
770
|
+
slots.inject('settings.plugins.tab', () => slots.register(
|
|
771
|
+
{ name: 'settings.plugins.tab', id: 'dsh-recall', order: 20, label: '撤回设置' },
|
|
772
|
+
RecallSettingsTab
|
|
773
|
+
))
|
|
774
|
+
} catch (error) {
|
|
775
|
+
console.error('[dsh-recall-plugin] settings tab register failed:', error)
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
})
|