dsh-recall-plugin 1.5.0 → 1.5.1
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 +13 -0
- package/README.md +0 -3
- package/lib/client.js +21 -8
- package/lib/index.js +73 -25
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
本文件格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循语义化版本。
|
|
4
4
|
|
|
5
|
+
## [1.5.1] - 2026-08-18
|
|
6
|
+
|
|
7
|
+
### 修复
|
|
8
|
+
|
|
9
|
+
- 设置页冷启动优化:`exclude-get` 首次遍历工作区、逐文件 shell 读改为并行 + 30s 结果缓存(保存后失效),二次打开设置页不再重复付出首访代价。
|
|
10
|
+
- 冷会话标题/消息文本补齐引入通用并发限制器(`runLimited`,同时最多 4 个):首次大量冷数据时 `readSession` 整日志解压不再全量并发压垮磁盘/CPU。
|
|
11
|
+
- 启动预热叠加 `sessionQuery.listSessions()` 冷元数据:冷启动时 `ctx.sessions.list()` 常为空(惰性载入),此前设置页首次打开仍要现场建 store,现开机即预热全部历史工作区。
|
|
12
|
+
- Client 侧 `usage`/`status` 补数据延后到 `list` 返回后异步执行:首屏先渲染树形内容,磁盘占用与错误日志随后补齐;`list` 失败时仍尝试补这两个数据,避免整卡全空。
|
|
13
|
+
|
|
14
|
+
### 变更
|
|
15
|
+
|
|
16
|
+
- Client 侧 `titles` 请求的 sessionIds 去重(`Set` 去重 + 过滤空值)。
|
|
17
|
+
|
|
5
18
|
## [1.5.0] - 2026-08-18
|
|
6
19
|
|
|
7
20
|
### 新增
|
package/README.md
CHANGED
package/lib/client.js
CHANGED
|
@@ -639,15 +639,28 @@ window.__ModuleLoader__.load({
|
|
|
639
639
|
fetchTitles(res.items || [])
|
|
640
640
|
fetchMessages(res.items || [])
|
|
641
641
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
642
|
+
// 列表返回后再补 usage/status:首次冷启动时磁盘占用和错误日志
|
|
643
|
+
// 都各要一条 shell,和 list 并发会抢资源拖慢首屏;延后到列表
|
|
644
|
+
// 渲染后,用户先看到树形内容,占用再异步补上。
|
|
645
|
+
if (sessionId) {
|
|
646
|
+
api('manage', { op: 'usage', sessionId }).then((res) => {
|
|
647
|
+
if (res && res.ok) setUsage(res.bytes || 0)
|
|
648
|
+
}).catch(() => {})
|
|
649
|
+
}
|
|
650
|
+
api('status', {}).then((res) => {
|
|
651
|
+
if (res && res.ok) setErrors(res.errors || [])
|
|
646
652
|
}).catch(() => {})
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
if (
|
|
650
|
-
|
|
653
|
+
}).catch(() => {
|
|
654
|
+
// list 失败时仍尝试补 usage/status,避免整卡全空
|
|
655
|
+
if (sessionId) {
|
|
656
|
+
api('manage', { op: 'usage', sessionId }).then((res) => {
|
|
657
|
+
if (res && res.ok) setUsage(res.bytes || 0)
|
|
658
|
+
}).catch(() => {})
|
|
659
|
+
}
|
|
660
|
+
api('status', {}).then((res) => {
|
|
661
|
+
if (res && res.ok) setErrors(res.errors || [])
|
|
662
|
+
}).catch(() => {})
|
|
663
|
+
})
|
|
651
664
|
}
|
|
652
665
|
|
|
653
666
|
React.useEffect(() => { refresh() }, [sessionId])
|
package/lib/index.js
CHANGED
|
@@ -51,6 +51,9 @@ export function apply(ctx, config) {
|
|
|
51
51
|
// 也不是零成本(1 条 shell + 若干日志解压)。设置页打开、删除后刷新
|
|
52
52
|
// 都会重拉,30s 缓存让二次打开即时;delete 与新快照落地时失效。
|
|
53
53
|
let listCache = { at: 0, payload: null }
|
|
54
|
+
// 排除配置枚举缓存(30s):exclude-get 首次要遍历工作区、逐文件 shell 读,
|
|
55
|
+
// 设置页反复打开时不该每次重算;exclude-set 成功写入后立即失效。
|
|
56
|
+
let excludeCache = { at: 0, payload: null }
|
|
54
57
|
|
|
55
58
|
// 会话标题缓存(apply 级跨请求共享):冷会话标题要 readSession 整日志
|
|
56
59
|
// 解压 + 重放校验(大日志 10 秒级),绝不能挡列表首屏——list 只查
|
|
@@ -138,7 +141,9 @@ export function apply(ctx, config) {
|
|
|
138
141
|
if (cwd) roots.add(cwd)
|
|
139
142
|
}
|
|
140
143
|
const byFile = new Map()
|
|
141
|
-
|
|
144
|
+
// 并行解析全部 root:冷启动时每个 root 首次 resolveStore 可能触发 shell
|
|
145
|
+
// 建目录/写 root.txt,串行会随工作区数量线性变慢;Promise.all 让它们并发。
|
|
146
|
+
await Promise.all(Array.from(roots).map(async (root) => {
|
|
142
147
|
try {
|
|
143
148
|
const store = await rt.resolveStore(root)
|
|
144
149
|
if (store && !byFile.has(store.excludeFile)) byFile.set(store.excludeFile, { store, roots: [] })
|
|
@@ -146,7 +151,7 @@ export function apply(ctx, config) {
|
|
|
146
151
|
} catch (error) {
|
|
147
152
|
/* 单个根解析失败只影响它自己,不拖垮整个列表 */
|
|
148
153
|
}
|
|
149
|
-
}
|
|
154
|
+
}))
|
|
150
155
|
// 磁盘兜底:冷启动时会话注册表为空(惰性载入),但 home 容器目录可能
|
|
151
156
|
// 早已存在(历史快照)。容器在 ⇒ 共享 exclude.txt 可编辑(哪怕从未
|
|
152
157
|
// 写过、内容为空);容器不在 ⇒ 全新安装,让设置页显示引导文案。
|
|
@@ -278,6 +283,21 @@ export function apply(ctx, config) {
|
|
|
278
283
|
return run
|
|
279
284
|
}
|
|
280
285
|
|
|
286
|
+
// 通用并发限制器:冷会话标题/消息文本补齐会 readSession 整日志解压,
|
|
287
|
+
// 首次大量冷数据时全量 Promise.all 会同时压垮磁盘/CPU。限制同时最多
|
|
288
|
+
// CONCURRENCY 个任务,剩余排队执行;这是纯内存调度,不依赖额外依赖。
|
|
289
|
+
async function runLimited(tasks, concurrency) {
|
|
290
|
+
const limit = concurrency > 0 ? concurrency : 4
|
|
291
|
+
let index = 0
|
|
292
|
+
const workers = Array.from({ length: Math.min(limit, tasks.length) }, async () => {
|
|
293
|
+
while (index < tasks.length) {
|
|
294
|
+
const task = tasks[index++]
|
|
295
|
+
await task()
|
|
296
|
+
}
|
|
297
|
+
})
|
|
298
|
+
await Promise.all(workers)
|
|
299
|
+
}
|
|
300
|
+
|
|
281
301
|
// 收集全量快照记录(内存 + 磁盘 dump 并集),供树形管理的按工作区/
|
|
282
302
|
// 会话批量删除使用。返回 Map<消息ID, {id, root, sessionId, time}>;
|
|
283
303
|
// 磁盘条目缺 root 时按 store 的 root.txt/内存映射补全(与 manage list
|
|
@@ -443,14 +463,19 @@ export function apply(ctx, config) {
|
|
|
443
463
|
// 设置页「撤回设置」标签的配置读取。不支持平台照常短路:Client
|
|
444
464
|
// 显示不可用提示而不是空白表单,与 init 的 notice 语义对齐。
|
|
445
465
|
if (!supported) return { ok: false, unsupported: true }
|
|
466
|
+
// 30s 结果缓存:首次进入要并行 resolveStore + 逐文件 shell 读,
|
|
467
|
+
// 二次打开/切标签不应重复付出这份代价;exclude-set 写入后失效。
|
|
468
|
+
if (excludeCache.payload && Date.now() - excludeCache.at < 30000) return excludeCache.payload
|
|
446
469
|
const byFile = await listExcludeFiles()
|
|
447
|
-
|
|
448
|
-
|
|
470
|
+
// 并行读取各 exclude 文件内容:每个文件一条 shell,串行会放大延迟
|
|
471
|
+
const files = await Promise.all(Array.from(byFile.entries()).map(async ([path, info]) => {
|
|
449
472
|
let content = ''
|
|
450
473
|
try { content = await snaps.readExclude(info.store) } catch (error) { content = '' }
|
|
451
|
-
|
|
452
|
-
}
|
|
453
|
-
|
|
474
|
+
return { path, home: Boolean(info.store.home), roots: info.roots, content }
|
|
475
|
+
}))
|
|
476
|
+
const payload = { ok: true, files }
|
|
477
|
+
excludeCache = { at: Date.now(), payload }
|
|
478
|
+
return payload
|
|
454
479
|
},
|
|
455
480
|
|
|
456
481
|
'exclude-set': async (args) => {
|
|
@@ -463,6 +488,8 @@ export function apply(ctx, config) {
|
|
|
463
488
|
const info = byFile.get(path)
|
|
464
489
|
if (!info) return { ok: false, code: 'UNKNOWN_PATH', message: '未知的排除文件路径' }
|
|
465
490
|
await snaps.writeExclude(info.store, content)
|
|
491
|
+
// 写入后立即失效:设置页保存后刷新必须看到最新内容
|
|
492
|
+
excludeCache.payload = null
|
|
466
493
|
return { ok: true }
|
|
467
494
|
},
|
|
468
495
|
|
|
@@ -542,10 +569,14 @@ export function apply(ctx, config) {
|
|
|
542
569
|
// 重放校验,大日志 10 秒级——独立于列表让首屏即时。并发交给
|
|
543
570
|
// sessionQuery 自带的 inspect 并发闸,这里全量并行发车。
|
|
544
571
|
if (!supported) return { ok: false, unsupported: true }
|
|
545
|
-
const ids = Array.
|
|
572
|
+
const ids = Array.from(new Set(
|
|
573
|
+
(Array.isArray(args && args.sessionIds) ? args.sessionIds.map(String) : []).filter(Boolean)
|
|
574
|
+
)).slice(0, 100)
|
|
546
575
|
const out = {}
|
|
547
|
-
|
|
548
|
-
|
|
576
|
+
// 并发限 4:冷标题 readSession 是重 IO,全量并发会把首次设置页
|
|
577
|
+
// 的 shell/日志解压都挤在一起;限制后列表本身不受影响,标题渐进补齐。
|
|
578
|
+
await runLimited(ids.map((sid) => async () => {
|
|
579
|
+
if (out[sid] !== undefined) return
|
|
549
580
|
let title = liveTitleFast(sid)
|
|
550
581
|
if (title === null) {
|
|
551
582
|
const query = ctx.get('sessionQuery')
|
|
@@ -558,7 +589,7 @@ export function apply(ctx, config) {
|
|
|
558
589
|
}
|
|
559
590
|
sessionTitles.set(sid, title)
|
|
560
591
|
out[sid] = title
|
|
561
|
-
}))
|
|
592
|
+
}), 4)
|
|
562
593
|
return { ok: true, titles: out }
|
|
563
594
|
}
|
|
564
595
|
if (op === 'messages') {
|
|
@@ -576,7 +607,7 @@ export function apply(ctx, config) {
|
|
|
576
607
|
bySession.get(sid).push(mid)
|
|
577
608
|
}
|
|
578
609
|
const texts = {}
|
|
579
|
-
await
|
|
610
|
+
await runLimited(Array.from(bySession.entries()).map(([sid, mids]) => async () => {
|
|
580
611
|
// 该会话所有消息都已缓存(含 null)时,不必 readSession 冷读
|
|
581
612
|
const allCached = mids.every((mid) => messageTexts.has(String(sid) + '\u0000' + String(mid)))
|
|
582
613
|
let log = null
|
|
@@ -604,7 +635,7 @@ export function apply(ctx, config) {
|
|
|
604
635
|
messageTexts.set(key, text)
|
|
605
636
|
texts[mid] = text
|
|
606
637
|
}
|
|
607
|
-
}))
|
|
638
|
+
}), 4)
|
|
608
639
|
return { ok: true, messageTexts: texts }
|
|
609
640
|
}
|
|
610
641
|
if (op === 'usage') {
|
|
@@ -730,16 +761,33 @@ export function apply(ctx, config) {
|
|
|
730
761
|
// 启动预热:所有已存在工作区解析存储、重建索引与孤儿快照,
|
|
731
762
|
// 并清理旧版项目内 blobs 目录(home 可用时)。
|
|
732
763
|
// 不触发维护(gc/清理):开机预热应尽量轻,重活等第一条消息再按节流来。
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
.
|
|
740
|
-
.
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
764
|
+
// 冷启动时 ctx.sessions.list() 常为空(惰性载入),必须叠加
|
|
765
|
+
// sessionQuery.listSessions() 冷元数据,否则设置页首次打开仍要现场建 store。
|
|
766
|
+
// apply 不是 async,这里用 IIFE 把冷元数据扫描包成异步任务。
|
|
767
|
+
;(async () => {
|
|
768
|
+
const warmupRoots = new Map()
|
|
769
|
+
for (const session of ctx.sessions.list()) {
|
|
770
|
+
const cwd = session && session.header && session.header.cwd
|
|
771
|
+
if (cwd && !warmupRoots.has(cwd)) warmupRoots.set(cwd, session.id)
|
|
772
|
+
}
|
|
773
|
+
const querySvc = ctx.get('sessionQuery')
|
|
774
|
+
if (querySvc && typeof querySvc.listSessions === 'function') {
|
|
775
|
+
try {
|
|
776
|
+
const records = await querySvc.listSessions()
|
|
777
|
+
for (const record of records || []) {
|
|
778
|
+
const cwd = record && record.header && record.header.cwd
|
|
779
|
+
if (cwd && !warmupRoots.has(cwd)) warmupRoots.set(cwd, record.id || null)
|
|
780
|
+
}
|
|
781
|
+
} catch (error) { /* 冷元数据不可用则退回 live 注册表 */ }
|
|
782
|
+
}
|
|
783
|
+
for (const [cwd, sessionId] of warmupRoots) {
|
|
784
|
+
Promise.resolve(rt.resolveStore(cwd))
|
|
785
|
+
.then(() => rt.tryUpgradeToHome(cwd))
|
|
786
|
+
.then((store) => rt.ensureGit(cwd, store))
|
|
787
|
+
.then(() => snaps.loadIndex(cwd, sessionId))
|
|
788
|
+
.then(() => snaps.rebuildOrphans(cwd, sessionId))
|
|
789
|
+
.then(() => rt.cleanupLegacy(cwd))
|
|
790
|
+
.catch(() => {})
|
|
791
|
+
}
|
|
792
|
+
})()
|
|
745
793
|
}
|