dsh-recall-plugin 1.7.0 → 2.0.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 +23 -0
- package/README.en.md +21 -18
- package/README.md +28 -19
- package/lib/client.js +284 -108
- package/lib/config.js +43 -2
- package/lib/index.js +153 -24
- package/lib/maintenance.js +263 -145
- package/lib/snapshots.js +73 -50
- package/package.json +15 -8
package/lib/client.js
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
* dsh-recall-plugin — Client 半(持久插件形态,浏览器 bundle)
|
|
3
3
|
*
|
|
4
4
|
* 职责:注册 conversation.chat.node 的 user 渲染器,重绘用户消息气泡
|
|
5
|
-
* (文本/图片/JSON
|
|
5
|
+
* (文本/图片/JSON 块;图片经官方 props.renderMessageImages 管线渲染,
|
|
6
|
+
* 见 UserRecallNode 内注释),在复制按钮旁加「撤回」按钮;二次确认面板展示
|
|
6
7
|
* 文件变更清单,确认后经 /api/recall/* 调用 Host 回退文件,并用官方
|
|
7
8
|
* sessions.fork 把对话一并回退(新会话打开、原会话归档)。
|
|
8
9
|
*
|
|
9
10
|
* 这是 dsh.client bundle(exports["./client"]),由 client-modules 打包成
|
|
10
11
|
* /plugins/<pkg>/client.js 注入页面;factory 内 require("react") 由平台
|
|
11
|
-
*
|
|
12
|
+
* 模块表提供。当前为纯 JS + React.createElement 直写。
|
|
12
13
|
*/
|
|
13
14
|
window.__ModuleLoader__.load({
|
|
14
15
|
id: "dsh-recall-plugin",
|
|
@@ -38,9 +39,6 @@ window.__ModuleLoader__.load({
|
|
|
38
39
|
'.dsh-recall-row{flex-direction:column;align-items:flex-end;gap:6px;display:flex}',
|
|
39
40
|
'.dsh-recall-stack{flex-direction:column;align-items:flex-end;gap:8px;min-width:0;max-width:min(525px,82%);display:flex}',
|
|
40
41
|
'.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-img-failed{border:1px dashed var(--dsw-alias-border-l2);border-radius:12px;padding:8px 14px;font-size:13px;line-height:20px;color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0}',
|
|
43
|
-
'.dsh-recall-img-failed:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}',
|
|
44
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)}',
|
|
45
43
|
'.dsh-recall-actions{align-items:center;gap:10px;height:28px;display:flex}',
|
|
46
44
|
'.dsh-recall-time{color:var(--dsw-alias-label-tertiary);white-space:nowrap;padding-right:12px;font-size:14px;line-height:24px}',
|
|
@@ -75,6 +73,7 @@ window.__ModuleLoader__.load({
|
|
|
75
73
|
'.dsh-recall-ex-chip:hover{color:var(--dsw-alias-label-primary)}',
|
|
76
74
|
'.dsh-recall-ex-status{margin-right:auto;font-size:12px;line-height:20px;color:var(--dsw-alias-label-tertiary)}',
|
|
77
75
|
'.dsh-recall-ex-status-error{color:var(--dsw-alias-state-error-primary)}',
|
|
76
|
+
'.dsh-recall-ex-status-success{color:var(--dsw-alias-state-success-primary)}',
|
|
78
77
|
'.dsh-recall-tree{display:flex;flex-direction:column;gap:2px;padding:4px 0}',
|
|
79
78
|
'.dsh-recall-tree-node{display:flex;flex-direction:column;gap:1px}',
|
|
80
79
|
'.dsh-recall-tree-row{display:flex;gap:6px;align-items:center;min-width:0;padding:2px 4px;border-radius:6px;cursor:default}',
|
|
@@ -177,7 +176,7 @@ window.__ModuleLoader__.load({
|
|
|
177
176
|
// has:false 且不再重试,撤回按钮将永不出现。
|
|
178
177
|
// init 顺带下发插件行为开关(refillDraft 等),存进 pluginConfig
|
|
179
178
|
// 供撤回执行链读取——设置页改配置 + 重启后随下一次 init 刷新。
|
|
180
|
-
const pluginConfig = { refillDraft: true }
|
|
179
|
+
const pluginConfig = { refillDraft: true, archiveOriginal: true }
|
|
181
180
|
function ensureInit(sessionId) {
|
|
182
181
|
if (!sessionId) return Promise.resolve()
|
|
183
182
|
const cached = initMap.get(sessionId)
|
|
@@ -185,6 +184,7 @@ window.__ModuleLoader__.load({
|
|
|
185
184
|
const done = api('init', { sessionId }).then((res) => {
|
|
186
185
|
if (res && res.config && typeof res.config === 'object') {
|
|
187
186
|
if (typeof res.config.refillDraft === 'boolean') pluginConfig.refillDraft = res.config.refillDraft
|
|
187
|
+
if (typeof res.config.archiveOriginal === 'boolean') pluginConfig.archiveOriginal = res.config.archiveOriginal
|
|
188
188
|
}
|
|
189
189
|
const notice = res && res.notice
|
|
190
190
|
if (notice && notice.unsupported) {
|
|
@@ -268,52 +268,6 @@ window.__ModuleLoader__.load({
|
|
|
268
268
|
)
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
-
// 图片加载失败的暂态判定(issue #8):会话回放时最早的图片消息先于
|
|
272
|
-
// 会话 binding 就绪渲染,loadImage 以 unknown session 拒绝——这类
|
|
273
|
-
// 失败靠短退避自动重试即可自愈;此前 `.catch(() => {})` 把失败静默
|
|
274
|
-
// 吞掉且 src 恒为 null,图片永久空白且刷新无法恢复。自动重试耗尽
|
|
275
|
-
// 后置 failed,由 ImageBox 提供「点击重试」(对齐官方 MessageImage
|
|
276
|
-
// 的失败态语义),attempt 仅被手动重试递增以复用整条加载链。
|
|
277
|
-
const IMAGE_RETRY_DELAYS = [400, 1500, 4000]
|
|
278
|
-
|
|
279
|
-
function useImageSrc(attachment, loadImage) {
|
|
280
|
-
const [src, setSrc] = React.useState(null)
|
|
281
|
-
const [failed, setFailed] = React.useState(false)
|
|
282
|
-
const [attempt, setAttempt] = React.useState(0)
|
|
283
|
-
React.useEffect(() => {
|
|
284
|
-
let alive = true
|
|
285
|
-
let timer = null
|
|
286
|
-
if (!attachment || typeof loadImage !== 'function') return undefined
|
|
287
|
-
// 复位旧状态:attachment 变化或手动重试时清掉上一轮的 src/failed,
|
|
288
|
-
// 避免加载期间短暂显示上一张图或残留的失败态
|
|
289
|
-
setSrc(null)
|
|
290
|
-
setFailed(false)
|
|
291
|
-
const tryLoad = (retried) => {
|
|
292
|
-
Promise.resolve(loadImage(attachment)).then((url) => {
|
|
293
|
-
if (!alive) return
|
|
294
|
-
if (url) setSrc(String(url))
|
|
295
|
-
else if (retried < IMAGE_RETRY_DELAYS.length) timer = setTimeout(() => tryLoad(retried + 1), IMAGE_RETRY_DELAYS[retried])
|
|
296
|
-
else setFailed(true)
|
|
297
|
-
}).catch(() => {
|
|
298
|
-
if (!alive) return
|
|
299
|
-
if (retried < IMAGE_RETRY_DELAYS.length) timer = setTimeout(() => tryLoad(retried + 1), IMAGE_RETRY_DELAYS[retried])
|
|
300
|
-
else setFailed(true)
|
|
301
|
-
})
|
|
302
|
-
}
|
|
303
|
-
tryLoad(0)
|
|
304
|
-
return () => { alive = false; if (timer) clearTimeout(timer) }
|
|
305
|
-
}, [attachment, loadImage, attempt])
|
|
306
|
-
return { src, failed, retry: () => setAttempt(attempt + 1) }
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function ImageBox(props) {
|
|
310
|
-
const { src, failed, retry } = useImageSrc(props.attachment, props.loadImage)
|
|
311
|
-
if (src) return React.createElement('img', { className: 'dsh-recall-img', src, alt: '' })
|
|
312
|
-
// 加载中保持原零占位语义(不撑气泡);只在确定失败后渲染可重试入口
|
|
313
|
-
if (failed) return React.createElement('button', { type: 'button', className: 'dsh-recall-img-failed', onClick: retry }, '图片加载失败,点击重试')
|
|
314
|
-
return null
|
|
315
|
-
}
|
|
316
|
-
|
|
317
271
|
// kind 语义单表承载(文案/徽章类名/汇总顺序):新增 kind 时只改
|
|
318
272
|
// 这一处,避免出现「映射表改了、CSS 类名忘了」的半改状态
|
|
319
273
|
const KIND_INFO = {
|
|
@@ -404,14 +358,21 @@ window.__ModuleLoader__.load({
|
|
|
404
358
|
|
|
405
359
|
function UserRecallNode(props) {
|
|
406
360
|
const node = props && props.node
|
|
407
|
-
|
|
361
|
+
// 图片渲染入口:官方 ChatNodeSeat 传给本 slot 的 props 契约只有
|
|
362
|
+
// renderMessageImages(内部经 conversation.message.images slot 渲染
|
|
363
|
+
// 官方 MessageImages,自带鉴权、缓存、失败重试与灯箱预览)。
|
|
364
|
+
// 契约里从不存在 loadImage(issue #9):v1.6/v1.7 读该不存在的
|
|
365
|
+
// 字段导致自研加载链从未执行、用户消息图片永久无声空白。
|
|
366
|
+
const renderMessageImages = props && props.renderMessageImages
|
|
408
367
|
const sessionId = props && props.sessionId
|
|
409
368
|
const data = node && node.data ? node.data : {}
|
|
410
369
|
// node.id 是会话事件匹配时写入的真实消息 ID;node.key 是位置键(如 13:input),不能用于快照查询
|
|
411
370
|
const messageId = node ? String(node.id || node.key || '') : ''
|
|
412
371
|
const blocks = Array.isArray(data.content) ? data.content : []
|
|
413
372
|
const text = blocks.filter((b) => b && b.type === 'text' && typeof b.text === 'string').map((b) => b.text).join('')
|
|
414
|
-
|
|
373
|
+
// 官方契约:images 传 image 块数组({attachment}),官方内部取
|
|
374
|
+
// image.attachment.attachmentId —— 不是裸 attachment 对象
|
|
375
|
+
const imageBlocks = blocks.filter((b) => b && b.type === 'image' && b.attachment).map((b) => ({ attachment: b.attachment }))
|
|
415
376
|
const rest = blocks.filter((b) => !b || !(b.type === 'text' && typeof b.text === 'string') && !(b.type === 'image' && b.attachment))
|
|
416
377
|
|
|
417
378
|
const [copied, setCopied] = React.useState(false)
|
|
@@ -513,9 +474,36 @@ window.__ModuleLoader__.load({
|
|
|
513
474
|
if (recall.stage !== 'confirm') return
|
|
514
475
|
const changes = recall.changes || []
|
|
515
476
|
const previewCut = typeof recall.cutSeq === 'number' ? recall.cutSeq : null
|
|
477
|
+
// P0-3:携带预览摘要(total 是完整计数,与 Host 侧 diffFor 的
|
|
478
|
+
// total 对齐;changes 截断到 500 条,不能用来比对)。Host 端
|
|
479
|
+
// 据此在 execute 时校验「预览后文件集是否变化」,变了则返回
|
|
480
|
+
// STALE 拒绝执行,防止把工作区改到用户没看过的新状态。
|
|
481
|
+
const previewTotal = typeof recall.total === 'number' ? recall.total : changes.length
|
|
516
482
|
setRecall({ stage: 'executing', changes })
|
|
517
|
-
api('execute', { messageId, sessionId }).then(async (res) => {
|
|
483
|
+
api('execute', { messageId, sessionId, previewTotal, previewAt: Date.now() }).then(async (res) => {
|
|
518
484
|
if (!res || !res.ok) {
|
|
485
|
+
// STALE:预览后文件变了——自动重新拉一次最新清单回到确认
|
|
486
|
+
// 阶段让用户看新内容,而不是停在错误面板
|
|
487
|
+
if (res && res.code === 'STALE') {
|
|
488
|
+
setRecall({ stage: 'loading' })
|
|
489
|
+
api('preview', { messageId, sessionId }).then((res2) => {
|
|
490
|
+
if (!res2 || !res2.ok) {
|
|
491
|
+
setRecall({ stage: 'error', message: (res2 && (res2.message || res2.error)) || '无法获取快照' })
|
|
492
|
+
return
|
|
493
|
+
}
|
|
494
|
+
setRecall({
|
|
495
|
+
stage: 'confirm',
|
|
496
|
+
changes: res2.changes || [],
|
|
497
|
+
total: typeof res2.total === 'number' ? res2.total : (res2.changes || []).length,
|
|
498
|
+
truncated: Boolean(res2.truncated),
|
|
499
|
+
time: res2.time || null,
|
|
500
|
+
cutSeq: typeof res2.cutSeq === 'number' ? res2.cutSeq : null
|
|
501
|
+
})
|
|
502
|
+
}).catch((error) => {
|
|
503
|
+
setRecall({ stage: 'error', message: String(error) })
|
|
504
|
+
})
|
|
505
|
+
return
|
|
506
|
+
}
|
|
519
507
|
setRecall({ stage: 'error', message: (res && (res.message || res.error)) || '回退失败' })
|
|
520
508
|
return
|
|
521
509
|
}
|
|
@@ -537,8 +525,10 @@ window.__ModuleLoader__.load({
|
|
|
537
525
|
if (typeof sessionsSvc.open === 'function') sessionsSvc.open(childId)
|
|
538
526
|
chatReverted = true
|
|
539
527
|
fillTarget = childId
|
|
540
|
-
//
|
|
541
|
-
|
|
528
|
+
// 回退前的原会话归档(可关,见设置页 archiveOriginal):只是从列表
|
|
529
|
+
// 隐藏、可恢复,避免侧栏出现两个近似会话;关闭时原会话
|
|
530
|
+
// 保留供对照
|
|
531
|
+
if (pluginConfig.archiveOriginal && workspacesSvc && typeof workspacesSvc.archiveSession === 'function') {
|
|
542
532
|
workspacesSvc.archiveSession(sessionId).catch(() => {})
|
|
543
533
|
}
|
|
544
534
|
} else {
|
|
@@ -597,10 +587,14 @@ window.__ModuleLoader__.load({
|
|
|
597
587
|
}
|
|
598
588
|
|
|
599
589
|
const bubbleChildren = []
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
590
|
+
// 图片在上、气泡在下:布局顺序对齐官方 UserStyleBubble(历史会话
|
|
591
|
+
// 里用户消息的观感与官方一致)。Fragment 包一层仅为挂 key。
|
|
592
|
+
if (imageBlocks.length && typeof renderMessageImages === 'function') {
|
|
593
|
+
bubbleChildren.push(React.createElement(React.Fragment, { key: 'images' },
|
|
594
|
+
renderMessageImages({ images: imageBlocks, align: 'end' })
|
|
595
|
+
))
|
|
603
596
|
}
|
|
597
|
+
if (text !== '') bubbleChildren.push(React.createElement('div', { className: 'dsh-recall-bubble', key: 'text' }, text))
|
|
604
598
|
for (let i = 0; i < rest.length; i++) {
|
|
605
599
|
bubbleChildren.push(React.createElement('pre', { className: 'dsh-recall-json', key: 'rest-' + i }, JSON.stringify(rest[i], null, 2)))
|
|
606
600
|
}
|
|
@@ -719,7 +713,7 @@ window.__ModuleLoader__.load({
|
|
|
719
713
|
}, s))
|
|
720
714
|
),
|
|
721
715
|
React.createElement('div', { className: 'dsh-recall-panel-actions' },
|
|
722
|
-
state.message ? React.createElement('span', { className: 'dsh-recall-ex-status' + (state.error ? ' dsh-recall-ex-status-error' : '') }, state.message) : null,
|
|
716
|
+
state.message ? React.createElement('span', { className: 'dsh-recall-ex-status' + (state.error ? ' dsh-recall-ex-status-error' : ' dsh-recall-ex-status-success') }, state.message) : null,
|
|
723
717
|
React.createElement('button', { type: 'button', className: 'dsh-recall-btn', disabled: !dirty || state.busy, onClick: discard }, '放弃修改'),
|
|
724
718
|
React.createElement('button', { type: 'button', className: 'dsh-recall-btn dsh-recall-btn-danger', disabled: !dirty || state.busy, onClick: save }, '保存')
|
|
725
719
|
)
|
|
@@ -735,6 +729,21 @@ window.__ModuleLoader__.load({
|
|
|
735
729
|
const [usage, setUsage] = React.useState(null)
|
|
736
730
|
const [errors, setErrors] = React.useState(null)
|
|
737
731
|
const [state, setState] = React.useState({ busy: false, message: '', error: false })
|
|
732
|
+
// 快照全量计数与当前拉取上限(S1-2):Host 按 limit 切片返回,
|
|
733
|
+
// total 是全量——树默认折叠,全量加载到 2000 上限对 DOM 无压力,
|
|
734
|
+
// 分页与树形组装互斥,故用「加载更多」而非翻页。
|
|
735
|
+
const [limit, setLimit] = React.useState(200)
|
|
736
|
+
const [total, setTotal] = React.useState(0)
|
|
737
|
+
// 存储健康(S2-4):git 可用性 + home/降级分区计数,来自 usage
|
|
738
|
+
// 端点的扩展响应;git 不可用/降级是消息流一次性 toast 之外常驻
|
|
739
|
+
// 可见的排障信息。
|
|
740
|
+
const [health, setHealth] = React.useState(null)
|
|
741
|
+
// 树搜索关键字(S3-4):纯客户端过滤已加载条目,按工作区名/会话
|
|
742
|
+
// 标题/消息文本/快照 ID 匹配;只过滤当前已加载数据,与 S1-2 的
|
|
743
|
+
// limit 正交。
|
|
744
|
+
const [query, setQuery] = React.useState('')
|
|
745
|
+
// 最近错误展开态(S3-5):默认 5 条,可展开全部 20 条/收起
|
|
746
|
+
const [showAllErrors, setShowAllErrors] = React.useState(false)
|
|
738
747
|
// 冷会话标题在服务端要整日志解压(10 秒级),首屏不等它:列表
|
|
739
748
|
// 先出(live/缓存标题),拿到后对缺标题的会话异步补拉再合并。
|
|
740
749
|
const [titlesPending, setTitlesPending] = React.useState(false)
|
|
@@ -783,10 +792,12 @@ window.__ModuleLoader__.load({
|
|
|
783
792
|
}).catch(() => {})
|
|
784
793
|
}
|
|
785
794
|
|
|
786
|
-
function refresh() {
|
|
787
|
-
|
|
795
|
+
function refresh(overLimit) {
|
|
796
|
+
const useLimit = overLimit || limit
|
|
797
|
+
api('manage', { op: 'list', limit: useLimit }).then((res) => {
|
|
788
798
|
if (res && res.ok) {
|
|
789
799
|
setItems(res.items || [])
|
|
800
|
+
setTotal(typeof res.total === 'number' ? res.total : (res.items || []).length)
|
|
790
801
|
fetchTitles(res.items || [])
|
|
791
802
|
fetchMessages(res.items || [])
|
|
792
803
|
}
|
|
@@ -795,7 +806,10 @@ window.__ModuleLoader__.load({
|
|
|
795
806
|
// 渲染后,用户先看到树形内容,占用再异步补上(usage 不带
|
|
796
807
|
// sessionId,Host 汇总全部工作区)。
|
|
797
808
|
api('manage', { op: 'usage' }).then((res) => {
|
|
798
|
-
if (res && res.ok)
|
|
809
|
+
if (res && res.ok) {
|
|
810
|
+
setUsage(res.bytes || 0)
|
|
811
|
+
setHealth({ gitAvailable: res.gitAvailable !== false, homeStores: res.homeStores || 0, fallbackStores: res.fallbackStores || 0 })
|
|
812
|
+
}
|
|
799
813
|
}).catch(() => {})
|
|
800
814
|
api('status', {}).then((res) => {
|
|
801
815
|
if (res && res.ok) setErrors(res.errors || [])
|
|
@@ -803,7 +817,10 @@ window.__ModuleLoader__.load({
|
|
|
803
817
|
}).catch(() => {
|
|
804
818
|
// list 失败时仍尝试补 usage/status,避免整卡全空
|
|
805
819
|
api('manage', { op: 'usage' }).then((res) => {
|
|
806
|
-
if (res && res.ok)
|
|
820
|
+
if (res && res.ok) {
|
|
821
|
+
setUsage(res.bytes || 0)
|
|
822
|
+
setHealth({ gitAvailable: res.gitAvailable !== false, homeStores: res.homeStores || 0, fallbackStores: res.fallbackStores || 0 })
|
|
823
|
+
}
|
|
807
824
|
}).catch(() => {})
|
|
808
825
|
api('status', {}).then((res) => {
|
|
809
826
|
if (res && res.ok) setErrors(res.errors || [])
|
|
@@ -813,6 +830,13 @@ window.__ModuleLoader__.load({
|
|
|
813
830
|
|
|
814
831
|
React.useEffect(() => { refresh() }, [])
|
|
815
832
|
|
|
833
|
+
// S3-5:清空 Host 侧错误缓冲并同步本地状态;Host 已清后下次
|
|
834
|
+
// refresh 拉回为空,不重复请求。
|
|
835
|
+
function clearErrors() {
|
|
836
|
+
setErrors([])
|
|
837
|
+
api('status', { op: 'clear' }).catch(() => {})
|
|
838
|
+
}
|
|
839
|
+
|
|
816
840
|
function run(op, extra, doneText) {
|
|
817
841
|
if (state.busy) return
|
|
818
842
|
setState({ busy: true, message: '执行中…', error: false })
|
|
@@ -832,17 +856,17 @@ window.__ModuleLoader__.load({
|
|
|
832
856
|
function renderDeleteAllConfirm() {
|
|
833
857
|
if (!confirming || confirming.kind !== 'all') return null
|
|
834
858
|
return React.createElement('div', { className: 'dsh-recall-tree-confirm' },
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
859
|
+
'确认删除所有工作区的全部快照?此操作不可恢复。',
|
|
860
|
+
React.createElement('button', {
|
|
861
|
+
type: 'button',
|
|
862
|
+
className: 'dsh-recall-btn dsh-recall-btn-danger',
|
|
863
|
+
onClick: () => {
|
|
864
|
+
setConfirming(null)
|
|
865
|
+
run('deleteAll', {}, '已清空全部快照')
|
|
866
|
+
}
|
|
867
|
+
}, '确认全部删除'),
|
|
868
|
+
React.createElement('button', { type: 'button', className: 'dsh-recall-ex-chip', onClick: () => setConfirming(null) }, '取消')
|
|
869
|
+
)
|
|
846
870
|
}
|
|
847
871
|
|
|
848
872
|
// 树形管理:工作区 → 会话 → 快照三级。折叠状态用 Set<key> 记录,
|
|
@@ -880,7 +904,16 @@ window.__ModuleLoader__.load({
|
|
|
880
904
|
}
|
|
881
905
|
return wsList
|
|
882
906
|
}
|
|
883
|
-
const
|
|
907
|
+
const q = query.trim().toLowerCase()
|
|
908
|
+
const filteredItems = q
|
|
909
|
+
? (items || []).filter((it) =>
|
|
910
|
+
(it.workspace || '').toLowerCase().indexOf(q) >= 0 ||
|
|
911
|
+
(it.sessionTitle || '').toLowerCase().indexOf(q) >= 0 ||
|
|
912
|
+
(it.messageText || '').toLowerCase().indexOf(q) >= 0 ||
|
|
913
|
+
String(it.id || '').toLowerCase().indexOf(q) >= 0
|
|
914
|
+
)
|
|
915
|
+
: items
|
|
916
|
+
const tree = buildTree(filteredItems)
|
|
884
917
|
// 每个节点的删除按钮统一走 run();workspace/session 用新端点 scope
|
|
885
918
|
// 批量删,叶子沿用原单条删除。二次确认文案按节点层级区分。
|
|
886
919
|
function confirmDelete(kind, key, extra, text) {
|
|
@@ -985,21 +1018,63 @@ window.__ModuleLoader__.load({
|
|
|
985
1018
|
}
|
|
986
1019
|
const treeNodes = tree.map(renderWorkspace)
|
|
987
1020
|
|
|
1021
|
+
// 计数用 Host 返回的全量 total 而非已加载条数(S1-2):loaded 为
|
|
1022
|
+
// null 表示首屏加载中(显示 …),已加载但 total 超过当前 limit
|
|
1023
|
+
// 时提示「当前显示最新 X 条」与加载更多入口。
|
|
1024
|
+
const loaded = items ? items.length : null
|
|
1025
|
+
const countText = loaded === null
|
|
1026
|
+
? '共 … 条快照'
|
|
1027
|
+
: '共 ' + total + ' 条快照' + (limit < total ? '(当前显示最新 ' + loaded + ' 条)' : '')
|
|
1028
|
+
|
|
1029
|
+
// 加载更多:上限 2000 与 Host 端钳制一致;total 已含全量计数,
|
|
1030
|
+
// 直接按 total 一次拉满(再往上就是 2000 封顶)
|
|
1031
|
+
function loadMore() {
|
|
1032
|
+
const next = Math.min(Math.max(total, limit), 2000)
|
|
1033
|
+
if (next <= limit) return
|
|
1034
|
+
setLimit(next)
|
|
1035
|
+
refresh(next)
|
|
1036
|
+
}
|
|
1037
|
+
|
|
988
1038
|
return React.createElement('div', { className: 'dsh-recall-ex-card' },
|
|
989
1039
|
React.createElement('div', { className: 'dsh-recall-ex-title' }, '快照管理'),
|
|
990
1040
|
React.createElement('div', { className: 'dsh-recall-ex-note' },
|
|
991
1041
|
usage === null
|
|
992
|
-
?
|
|
993
|
-
:
|
|
1042
|
+
? countText + '。'
|
|
1043
|
+
: countText + ',全部工作区快照存储占用 ' + sizeText(usage) + '。'
|
|
994
1044
|
),
|
|
1045
|
+
health ? React.createElement('div', { className: 'dsh-recall-ex-note', key: 'health' },
|
|
1046
|
+
React.createElement('span', {
|
|
1047
|
+
className: health.gitAvailable ? '' : 'dsh-recall-ex-status-error'
|
|
1048
|
+
}, health.gitAvailable ? 'git 可用' : 'git 不可用(快照引擎依赖 git)'),
|
|
1049
|
+
' · 快照存储:home ' + health.homeStores + ' 个工作区' + (health.fallbackStores ? ',降级 ' + health.fallbackStores + ' 个' : '')
|
|
1050
|
+
) : null,
|
|
1051
|
+
React.createElement('input', {
|
|
1052
|
+
className: 'dsh-recall-ex-input',
|
|
1053
|
+
placeholder: '搜索工作区 / 会话标题 / 消息内容 / ID',
|
|
1054
|
+
value: query,
|
|
1055
|
+
spellCheck: false,
|
|
1056
|
+
onChange: (e) => setQuery(e.target.value),
|
|
1057
|
+
}),
|
|
995
1058
|
treeNodes.length > 0 ? React.createElement('div', { className: 'dsh-recall-tree' }, ...treeNodes) : null,
|
|
1059
|
+
items && items.length === 0 && !q
|
|
1060
|
+
? React.createElement('div', { className: 'dsh-recall-ex-note', key: 'empty' }, '在任意工作区发送一条消息后,这里会出现快照。')
|
|
1061
|
+
: null,
|
|
1062
|
+
q && filteredItems && filteredItems.length === 0
|
|
1063
|
+
? React.createElement('div', { className: 'dsh-recall-ex-note', key: 'no-match' }, '无匹配快照')
|
|
1064
|
+
: null,
|
|
996
1065
|
renderDeleteAllConfirm(),
|
|
997
1066
|
React.createElement('div', { className: 'dsh-recall-panel-actions' },
|
|
998
|
-
state.message ? React.createElement('span', { className: 'dsh-recall-ex-status' + (state.error ? ' dsh-recall-ex-status-error' : '') }, state.message) : null,
|
|
1067
|
+
state.message ? React.createElement('span', { className: 'dsh-recall-ex-status' + (state.error ? ' dsh-recall-ex-status-error' : ' dsh-recall-ex-status-success') }, state.message) : null,
|
|
1068
|
+
limit < total ? React.createElement('button', {
|
|
1069
|
+
type: 'button',
|
|
1070
|
+
className: 'dsh-recall-btn',
|
|
1071
|
+
disabled: state.busy,
|
|
1072
|
+
onClick: loadMore
|
|
1073
|
+
}, '加载更多') : null,
|
|
999
1074
|
React.createElement('button', { type: 'button', className: 'dsh-recall-btn', disabled: state.busy, onClick: refresh }, '刷新'),
|
|
1000
1075
|
React.createElement('button', {
|
|
1001
1076
|
type: 'button',
|
|
1002
|
-
className: 'dsh-recall-btn',
|
|
1077
|
+
className: 'dsh-recall-btn dsh-recall-btn-danger',
|
|
1003
1078
|
disabled: state.busy,
|
|
1004
1079
|
title: '删除全部工作区的所有快照;会直接核对并删除 git tag(即使列表为空也可清理残留)',
|
|
1005
1080
|
onClick: () => setConfirming({ kind: 'all' })
|
|
@@ -1013,9 +1088,15 @@ window.__ModuleLoader__.load({
|
|
|
1013
1088
|
}, '立即 gc')
|
|
1014
1089
|
),
|
|
1015
1090
|
errors && errors.length > 0
|
|
1016
|
-
? React.createElement('div', { className: 'dsh-recall-ex-note' },
|
|
1017
|
-
'
|
|
1018
|
-
|
|
1091
|
+
? React.createElement('div', { className: 'dsh-recall-ex-note', key: 'errors' },
|
|
1092
|
+
React.createElement('div', { className: 'dsh-recall-ex-status' },
|
|
1093
|
+
'最近错误:',
|
|
1094
|
+
(showAllErrors ? errors : errors.slice(0, 5)).map((e, i) => React.createElement('div', { key: i, className: 'dsh-recall-ex-note' }, clockText(e.time) + ' ' + e.message))
|
|
1095
|
+
),
|
|
1096
|
+
React.createElement('div', { className: 'dsh-recall-panel-actions' },
|
|
1097
|
+
errors.length > 5 ? React.createElement('button', { type: 'button', className: 'dsh-recall-ex-chip', onClick: () => setShowAllErrors((v) => !v) }, showAllErrors ? '收起' : '展开全部 (' + errors.length + ')') : null,
|
|
1098
|
+
React.createElement('button', { type: 'button', className: 'dsh-recall-ex-chip', onClick: clearErrors }, '清空')
|
|
1099
|
+
)
|
|
1019
1100
|
)
|
|
1020
1101
|
: null
|
|
1021
1102
|
)
|
|
@@ -1058,6 +1139,16 @@ window.__ModuleLoader__.load({
|
|
|
1058
1139
|
)
|
|
1059
1140
|
}
|
|
1060
1141
|
|
|
1142
|
+
// ConfigForm 的字节↔MB 换算:持久化与 schema 都是字节(config-get
|
|
1143
|
+
// 下发原值),只在 display/input 层换算成人工友好的 MB 小数;round
|
|
1144
|
+
// 2 位小数去尾零,避免默认值 104857600 裸奔成一长串数字
|
|
1145
|
+
// (plan-settings-ux S1-1)。
|
|
1146
|
+
function bytesToMb(bytes) {
|
|
1147
|
+
const n = Number(bytes)
|
|
1148
|
+
if (!Number.isFinite(n) || n <= 0) return ''
|
|
1149
|
+
return String(Math.round((n / 1048576) * 100) / 100)
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1061
1152
|
// 插件配置表单:值经 Host 的 settings namespace「dsh-recall」读写
|
|
1062
1153
|
// (config-get / config-set 端点),保存即持久化进用户 settings 文档
|
|
1063
1154
|
// 并热生效(Host watch 链路原地更新 cfg),无需重启。只提交相对
|
|
@@ -1069,6 +1160,9 @@ window.__ModuleLoader__.load({
|
|
|
1069
1160
|
const [overridden, setOverridden] = React.useState({})
|
|
1070
1161
|
const [writable, setWritable] = React.useState(true)
|
|
1071
1162
|
const [state, setState] = React.useState({ busy: false, message: '', error: false })
|
|
1163
|
+
// 「高级:基础排除表」折叠态(S3-2):与 exclude.txt 同为 gitignore
|
|
1164
|
+
// 编辑器,叠放会造成认知负担——内置规则收进折叠,只留高频项在首屏
|
|
1165
|
+
const [showAdvanced, setShowAdvanced] = React.useState(false)
|
|
1072
1166
|
|
|
1073
1167
|
function load() {
|
|
1074
1168
|
api('config-get', {}).then((res) => {
|
|
@@ -1077,9 +1171,13 @@ window.__ModuleLoader__.load({
|
|
|
1077
1171
|
const next = {
|
|
1078
1172
|
gcSnaps: String(v.gcSnaps == null ? '' : v.gcSnaps),
|
|
1079
1173
|
gcHours: String(v.gcHours == null ? '' : v.gcHours),
|
|
1080
|
-
maxFileBytes:
|
|
1174
|
+
maxFileBytes: bytesToMb(v.maxFileBytes),
|
|
1175
|
+
maxSnapshotsPerWorkspace: String(v.maxSnapshotsPerWorkspace == null ? '' : v.maxSnapshotsPerWorkspace),
|
|
1081
1176
|
baseExcludes: Array.isArray(v.baseExcludes) ? v.baseExcludes.join('\n') : '',
|
|
1082
1177
|
refillDraft: v.refillDraft !== false,
|
|
1178
|
+
snapshotEnabled: v.snapshotEnabled !== false,
|
|
1179
|
+
archiveOriginal: v.archiveOriginal !== false,
|
|
1180
|
+
retentionDays: String(v.retentionDays == null ? '' : v.retentionDays),
|
|
1083
1181
|
}
|
|
1084
1182
|
setDraft(next)
|
|
1085
1183
|
setBaseline(next)
|
|
@@ -1101,7 +1199,7 @@ window.__ModuleLoader__.load({
|
|
|
1101
1199
|
function save() {
|
|
1102
1200
|
if (state.busy || !draft || !baseline) return
|
|
1103
1201
|
const patch = {}
|
|
1104
|
-
for (const key of ['gcSnaps', 'gcHours', 'maxFileBytes', 'baseExcludes', 'refillDraft']) {
|
|
1202
|
+
for (const key of ['gcSnaps', 'gcHours', 'maxFileBytes', 'maxSnapshotsPerWorkspace', 'baseExcludes', 'refillDraft', 'snapshotEnabled', 'archiveOriginal', 'retentionDays']) {
|
|
1105
1203
|
if (draft[key] !== baseline[key]) patch[key] = draft[key]
|
|
1106
1204
|
}
|
|
1107
1205
|
if (!Object.keys(patch).length) {
|
|
@@ -1120,11 +1218,25 @@ window.__ModuleLoader__.load({
|
|
|
1120
1218
|
clean.gcHours = n
|
|
1121
1219
|
}
|
|
1122
1220
|
if (patch.maxFileBytes !== undefined) {
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1221
|
+
// display 层是 MB 小数,持久化仍是字节(Math.round 取整):
|
|
1222
|
+
// model 侧不变,config-get/config-set 往返零改动
|
|
1223
|
+
const mb = Number(patch.maxFileBytes)
|
|
1224
|
+
if (!Number.isFinite(mb) || mb < 0.01) { setState({ busy: false, message: '文件大小上限至少 0.01 MB', error: true }); return }
|
|
1225
|
+
clean.maxFileBytes = Math.round(mb * 1048576)
|
|
1226
|
+
}
|
|
1227
|
+
if (patch.maxSnapshotsPerWorkspace !== undefined) {
|
|
1228
|
+
const n = parseInt(patch.maxSnapshotsPerWorkspace, 10)
|
|
1229
|
+
if (!Number.isFinite(n) || n < 0) { setState({ busy: false, message: '快照总量上限必须是 >= 0 的整数(0 表示不限制)', error: true }); return }
|
|
1230
|
+
clean.maxSnapshotsPerWorkspace = n
|
|
1126
1231
|
}
|
|
1127
1232
|
if (patch.refillDraft !== undefined) clean.refillDraft = Boolean(patch.refillDraft)
|
|
1233
|
+
if (patch.snapshotEnabled !== undefined) clean.snapshotEnabled = Boolean(patch.snapshotEnabled)
|
|
1234
|
+
if (patch.archiveOriginal !== undefined) clean.archiveOriginal = Boolean(patch.archiveOriginal)
|
|
1235
|
+
if (patch.retentionDays !== undefined) {
|
|
1236
|
+
const n = parseInt(patch.retentionDays, 10)
|
|
1237
|
+
if (!Number.isFinite(n) || n < 0) { setState({ busy: false, message: '保留天数必须是 >= 0 的整数(0 表示不启用)', error: true }); return }
|
|
1238
|
+
clean.retentionDays = n
|
|
1239
|
+
}
|
|
1128
1240
|
if (patch.baseExcludes !== undefined) {
|
|
1129
1241
|
clean.baseExcludes = String(patch.baseExcludes).split('\n').map((l) => l.trim()).filter(Boolean)
|
|
1130
1242
|
}
|
|
@@ -1139,7 +1251,7 @@ window.__ModuleLoader__.load({
|
|
|
1139
1251
|
}).catch((e) => setState({ busy: false, message: String(e), error: true }))
|
|
1140
1252
|
}
|
|
1141
1253
|
|
|
1142
|
-
function numRow(key, label, hint) {
|
|
1254
|
+
function numRow(key, label, hint, opts) {
|
|
1143
1255
|
const locked = Boolean(envLocks && envLocks[key])
|
|
1144
1256
|
const changed = Boolean(draft && baseline && draft[key] !== baseline[key])
|
|
1145
1257
|
return React.createElement('div', { className: 'dsh-recall-cfg-row', key: key },
|
|
@@ -1150,8 +1262,11 @@ window.__ModuleLoader__.load({
|
|
|
1150
1262
|
type: 'number',
|
|
1151
1263
|
value: draft ? draft[key] : '',
|
|
1152
1264
|
disabled: locked || !writable,
|
|
1265
|
+
min: opts && opts.min,
|
|
1266
|
+
step: opts && opts.step,
|
|
1153
1267
|
onChange: (e) => edit(key, e.target.value),
|
|
1154
1268
|
}),
|
|
1269
|
+
opts && opts.suffix ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, opts.suffix) : null,
|
|
1155
1270
|
changed && !locked ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, '已修改') : null,
|
|
1156
1271
|
overridden && overridden[key] !== undefined ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, '已覆盖') : null,
|
|
1157
1272
|
locked ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, '环境变量锁定') : null
|
|
@@ -1160,18 +1275,49 @@ window.__ModuleLoader__.load({
|
|
|
1160
1275
|
)
|
|
1161
1276
|
}
|
|
1162
1277
|
|
|
1278
|
+
function resetDefaults() {
|
|
1279
|
+
if (state.busy || !writable) return
|
|
1280
|
+
setState({ busy: true, message: '恢复默认中…', error: false })
|
|
1281
|
+
api('config-reset', {}).then((res) => {
|
|
1282
|
+
if (res && res.ok) {
|
|
1283
|
+
load()
|
|
1284
|
+
setState({ busy: false, message: '已恢复默认值', error: false })
|
|
1285
|
+
} else {
|
|
1286
|
+
setState({ busy: false, message: (res && (res.message || res.error)) || '恢复默认失败', error: true })
|
|
1287
|
+
}
|
|
1288
|
+
}).catch((e) => setState({ busy: false, message: String(e), error: true }))
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1163
1291
|
if (!draft) {
|
|
1164
1292
|
return React.createElement('div', { className: 'dsh-recall-ex-note' }, state.message || '正在读取配置…')
|
|
1165
1293
|
}
|
|
1166
1294
|
|
|
1167
1295
|
return React.createElement('div', { className: 'dsh-recall-ex-card' },
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1296
|
+
React.createElement('div', { className: 'dsh-recall-cfg-row', key: 'snapshotEnabled' },
|
|
1297
|
+
React.createElement('div', { className: 'dsh-recall-cfg-line' },
|
|
1298
|
+
React.createElement('label', { className: 'dsh-recall-cfg-label', htmlFor: 'dsh-recall-cfg-snapshot' }, '启用快照'),
|
|
1299
|
+
React.createElement('input', {
|
|
1300
|
+
id: 'dsh-recall-cfg-snapshot',
|
|
1301
|
+
type: 'checkbox',
|
|
1302
|
+
checked: Boolean(draft.snapshotEnabled),
|
|
1303
|
+
disabled: !writable,
|
|
1304
|
+
onChange: (e) => edit('snapshotEnabled', e.target.checked),
|
|
1305
|
+
}),
|
|
1306
|
+
draft.snapshotEnabled !== baseline.snapshotEnabled ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, '已修改') : null,
|
|
1307
|
+
overridden && overridden.snapshotEnabled !== undefined ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, '已覆盖') : null
|
|
1308
|
+
),
|
|
1309
|
+
React.createElement('div', { className: 'dsh-recall-cfg-hint' }, '关闭后不再新建快照(已有快照仍可撤回),适合临时禁用快照的场合')
|
|
1310
|
+
),
|
|
1311
|
+
numRow('gcSnaps', 'gc 触发条数', '每积累多少条快照触发一次 git gc', { min: 1, step: 1 }),
|
|
1312
|
+
numRow('gcHours', 'gc 触发小时', '距上次 gc 超过多少小时触发(与条数先到先触发)', { min: 1, step: 1 }),
|
|
1313
|
+
numRow('maxFileBytes', '文件大小上限', '超过该大小的文件不进快照、不被回退触碰(单位 MB,支持小数)', { suffix: 'MB', min: 0.01, step: 0.5 }),
|
|
1314
|
+
numRow('maxSnapshotsPerWorkspace', '快照总量上限', '每个工作区保留的最大快照数,超限自动删除最旧的;填 0 表示不限制', { min: 0, step: 1 }),
|
|
1315
|
+
numRow('retentionDays', '快照保留天数', '按天数保留快照,超期自动删除最旧的;填 0 表示不启用(与快照总数上限各自生效)', { min: 0, step: 1 }),
|
|
1171
1316
|
React.createElement('div', { className: 'dsh-recall-cfg-row', key: 'refillDraft' },
|
|
1172
1317
|
React.createElement('div', { className: 'dsh-recall-cfg-line' },
|
|
1173
|
-
React.createElement('label', { className: 'dsh-recall-cfg-label' }, '撤回后回填输入框'),
|
|
1318
|
+
React.createElement('label', { className: 'dsh-recall-cfg-label', htmlFor: 'dsh-recall-cfg-refill' }, '撤回后回填输入框'),
|
|
1174
1319
|
React.createElement('input', {
|
|
1320
|
+
id: 'dsh-recall-cfg-refill',
|
|
1175
1321
|
type: 'checkbox',
|
|
1176
1322
|
checked: Boolean(draft.refillDraft),
|
|
1177
1323
|
disabled: !writable,
|
|
@@ -1182,7 +1328,23 @@ window.__ModuleLoader__.load({
|
|
|
1182
1328
|
),
|
|
1183
1329
|
React.createElement('div', { className: 'dsh-recall-cfg-hint' }, '撤回成功后把被撤回的消息文本回填到输入框,方便修改后重新发送')
|
|
1184
1330
|
),
|
|
1185
|
-
React.createElement('div', { className: 'dsh-recall-cfg-row', key: '
|
|
1331
|
+
React.createElement('div', { className: 'dsh-recall-cfg-row', key: 'archiveOriginal' },
|
|
1332
|
+
React.createElement('div', { className: 'dsh-recall-cfg-line' },
|
|
1333
|
+
React.createElement('label', { className: 'dsh-recall-cfg-label', htmlFor: 'dsh-recall-cfg-archive' }, '撤回后归档原会话'),
|
|
1334
|
+
React.createElement('input', {
|
|
1335
|
+
id: 'dsh-recall-cfg-archive',
|
|
1336
|
+
type: 'checkbox',
|
|
1337
|
+
checked: Boolean(draft.archiveOriginal),
|
|
1338
|
+
disabled: !writable,
|
|
1339
|
+
onChange: (e) => edit('archiveOriginal', e.target.checked),
|
|
1340
|
+
}),
|
|
1341
|
+
draft.archiveOriginal !== baseline.archiveOriginal ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, '已修改') : null,
|
|
1342
|
+
overridden && overridden.archiveOriginal !== undefined ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, '已覆盖') : null
|
|
1343
|
+
),
|
|
1344
|
+
React.createElement('div', { className: 'dsh-recall-cfg-hint' }, '撤回后原会话从列表归档隐藏(可从归档找回);关闭则保留在列表中,方便对照回退前后的上下文')
|
|
1345
|
+
),
|
|
1346
|
+
React.createElement(SectionToggle, { title: '高级:基础排除表', open: showAdvanced, onToggle: () => setShowAdvanced((v) => !v) }),
|
|
1347
|
+
showAdvanced ? React.createElement('div', { className: 'dsh-recall-cfg-row', key: 'baseExcludes' },
|
|
1186
1348
|
React.createElement('div', { className: 'dsh-recall-cfg-line' },
|
|
1187
1349
|
React.createElement('label', { className: 'dsh-recall-cfg-label' }, '基础排除表'),
|
|
1188
1350
|
draft.baseExcludes !== baseline.baseExcludes ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, '已修改') : null,
|
|
@@ -1195,11 +1357,18 @@ window.__ModuleLoader__.load({
|
|
|
1195
1357
|
disabled: !writable,
|
|
1196
1358
|
onChange: (e) => edit('baseExcludes', e.target.value),
|
|
1197
1359
|
}),
|
|
1198
|
-
React.createElement('div', { className: 'dsh-recall-cfg-hint' }, 'gitignore
|
|
1199
|
-
),
|
|
1360
|
+
React.createElement('div', { className: 'dsh-recall-cfg-hint' }, '内置规则,每个工作区共享,建议保持默认;gitignore 语法每行一条,优先级低于「排除配置」里的 exclude.txt(S3-2 折叠)')
|
|
1361
|
+
) : null,
|
|
1200
1362
|
React.createElement('div', { className: 'dsh-recall-panel-actions' },
|
|
1201
|
-
state.message ? React.createElement('span', { className: 'dsh-recall-ex-status' + (state.error ? ' dsh-recall-ex-status-error' : '') }, state.message) : null,
|
|
1363
|
+
state.message ? React.createElement('span', { className: 'dsh-recall-ex-status' + (state.error ? ' dsh-recall-ex-status-error' : ' dsh-recall-ex-status-success') }, state.message) : null,
|
|
1202
1364
|
React.createElement('button', { type: 'button', className: 'dsh-recall-btn', disabled: state.busy || !writable, onClick: () => setDraft(Object.assign({}, baseline)) }, '放弃修改'),
|
|
1365
|
+
React.createElement('button', {
|
|
1366
|
+
type: 'button',
|
|
1367
|
+
className: 'dsh-recall-btn',
|
|
1368
|
+
disabled: state.busy || !writable,
|
|
1369
|
+
title: '把所有字段恢复到插件出厂默认值',
|
|
1370
|
+
onClick: resetDefaults
|
|
1371
|
+
}, '恢复默认'),
|
|
1203
1372
|
React.createElement('button', { type: 'button', className: 'dsh-recall-btn', disabled: state.busy || !writable, onClick: save }, '保存'),
|
|
1204
1373
|
!writable ? React.createElement('span', { className: 'dsh-recall-cfg-tag' }, '只读设置源') : null
|
|
1205
1374
|
)
|
|
@@ -1266,16 +1435,23 @@ window.__ModuleLoader__.load({
|
|
|
1266
1435
|
// 整个插件;lowest renders,负值恰好覆盖默认渲染器实现撤回 UI。
|
|
1267
1436
|
// priority -1 也可能被别的机器上的插件占用(同样抛冲突),所以
|
|
1268
1437
|
// 递减重试三次——最坏情况只是撤回按钮不渲染,绝不让插件加载失败。
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1438
|
+
// chat.node 的 keyed key 与节点 UI 投影 kind 对齐:'user' 是常规
|
|
1439
|
+
// 用户消息;'steering' 是 agent 运行中插入的转向指令——官方仍按
|
|
1440
|
+
// 用户气泡回显,但 keyed 'user' 不命中,会落到默认渲染、撤回按钮
|
|
1441
|
+
// 缺失(实测存储层 role 一致、仅 UI 投影 kind 不同)。两个 key
|
|
1442
|
+
// 各自独立注册,互不抢占。
|
|
1443
|
+
for (const slotKey of ['user', 'steering']) {
|
|
1444
|
+
let mounted = false
|
|
1445
|
+
for (let priority = -1; priority >= -3 && !mounted; priority--) {
|
|
1446
|
+
try {
|
|
1447
|
+
slots.inject('conversation.chat.node', () => slots.register(
|
|
1448
|
+
{ name: 'conversation.chat.node', key: slotKey, priority },
|
|
1449
|
+
UserRecallNode
|
|
1450
|
+
))
|
|
1451
|
+
mounted = true
|
|
1452
|
+
} catch (error) {
|
|
1453
|
+
if (priority === -3) console.error('[dsh-recall-plugin] slot register failed (' + slotKey + '):', error)
|
|
1454
|
+
}
|
|
1279
1455
|
}
|
|
1280
1456
|
}
|
|
1281
1457
|
|