dsh-recall-plugin 1.5.1 → 1.6.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 +25 -0
- package/README.en.md +15 -4
- package/README.md +10 -5
- package/cordis.patch.yml +1 -0
- package/lib/client.js +340 -52
- package/lib/config.js +24 -9
- package/lib/index.js +126 -12
- package/lib/maintenance.js +33 -8
- package/lib/snapshots.js +216 -214
- package/lib/store.js +37 -4
- package/package.json +9 -7
package/lib/index.js
CHANGED
|
@@ -12,10 +12,11 @@
|
|
|
12
12
|
* 文件拆分见 lib/ 下各模块头注释;本文件只做接线,不承载业务逻辑。
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { createConfig } from './config.js'
|
|
15
|
+
import { createConfig, Config } from './config.js'
|
|
16
16
|
import { createRuntime } from './store.js'
|
|
17
17
|
import { createSnapshots } from './snapshots.js'
|
|
18
18
|
import { createMaintenance } from './maintenance.js'
|
|
19
|
+
import { installSettingsSection } from '@deepseek-ai/dsh-settings'
|
|
19
20
|
|
|
20
21
|
export const name = 'dsh-recall-plugin'
|
|
21
22
|
|
|
@@ -23,8 +24,13 @@ export const name = 'dsh-recall-plugin'
|
|
|
23
24
|
// webServer(Client 半的 HTTP API 通道)。其余服务按需 ctx.get。
|
|
24
25
|
export const inject = ['shell', 'sessions', 'webServer']
|
|
25
26
|
|
|
26
|
-
//
|
|
27
|
-
//
|
|
27
|
+
// 入口配置 schema:cordis 加载器据此校验 insert 行 config 并填充默认值,
|
|
28
|
+
// 非法配置在插件加载时响亮失败(官方「插件配置」文档要求)。
|
|
29
|
+
export { Config }
|
|
30
|
+
|
|
31
|
+
// config 由 cordis.patch.yml 的 insert 行 config 键下发(schema 默认值兜底),
|
|
32
|
+
// 设置页「插件配置」卡片的用户覆盖经 settings namespace 热更新进 cfg
|
|
33
|
+
// (见下方 installSettingsSection 接线)
|
|
28
34
|
export function apply(ctx, config) {
|
|
29
35
|
const webServer = ctx.webServer
|
|
30
36
|
|
|
@@ -34,6 +40,27 @@ export function apply(ctx, config) {
|
|
|
34
40
|
const maint = createMaintenance(ctx, rt, snaps, cfg)
|
|
35
41
|
const state = rt.state
|
|
36
42
|
|
|
43
|
+
// ---- settings namespace「dsh-recall」:设置页「插件配置」分区正规接入 ----
|
|
44
|
+
// installSettingsSection(dsh-settings 官方辅助):settings 服务挂载后以
|
|
45
|
+
// 真 Config schema 注册 namespace、组合 base 取入口 config;服务卸载时
|
|
46
|
+
// 源回退入口 config。解析层 = schema 默认 → 组合 base → 用户文档(设置
|
|
47
|
+
// 卡片写入、dsh-settings 持久化),变更经 watch 热更新进运行中的 cfg
|
|
48
|
+
// (Object.assign 原地改,各域按调用时读取立即生效)。环境变量仍最高
|
|
49
|
+
// 优先(createConfig 内处理)。settings 服务未组装(非 web 部署)时
|
|
50
|
+
// 整段静默不运行,插件照常以入口配置工作。
|
|
51
|
+
let readSettings = () => config
|
|
52
|
+
function applyResolvedConfig(resolved) {
|
|
53
|
+
Object.assign(cfg, createConfig(resolved && typeof resolved === 'object' ? resolved : {}))
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
installSettingsSection(ctx, 'dsh-recall', Config, config, {
|
|
57
|
+
setSource: (fn) => { readSettings = fn },
|
|
58
|
+
onChange: () => applyResolvedConfig(readSettings()),
|
|
59
|
+
})
|
|
60
|
+
} catch (error) {
|
|
61
|
+
rt.recordError('recall settings namespace skipped: ' + String(error))
|
|
62
|
+
}
|
|
63
|
+
|
|
37
64
|
// 平台门控:win32 走 PowerShell 模板,linux/darwin 走 bash 模板
|
|
38
65
|
// (ctx.shell 由 DSH 平台层单选挂载 pwsh/bash 执行器,见 dsh-shell README)。
|
|
39
66
|
// 其余平台干净短路:init 返回 unsupported,Client 弹一次性提示;
|
|
@@ -412,7 +439,9 @@ export function apply(ctx, config) {
|
|
|
412
439
|
homeFallback: store ? !store.home : false
|
|
413
440
|
}
|
|
414
441
|
}
|
|
415
|
-
|
|
442
|
+
// 顺带下发客户端行为开关(fillDraft 等):Client 无须为读配置单开请求,
|
|
443
|
+
// init 是每会话必经的预热通道
|
|
444
|
+
return { ok: Boolean(root), root: root || null, notice, config: { refillDraft: cfg.refillDraft } }
|
|
416
445
|
},
|
|
417
446
|
|
|
418
447
|
'snapshot-info': async (args) => {
|
|
@@ -493,6 +522,68 @@ export function apply(ctx, config) {
|
|
|
493
522
|
return { ok: true }
|
|
494
523
|
},
|
|
495
524
|
|
|
525
|
+
// 设置页「插件配置」卡片读配置:resolved 全量值 + 用户已覆盖字段(来自
|
|
526
|
+
// settings.describe 的 user 层,字段在层里出现即用户覆盖)+ env 锁定
|
|
527
|
+
// 字段(环境变量优先级最高,设置改不动)+ 可写性(只读 provider 禁存)。
|
|
528
|
+
'config-get': async () => {
|
|
529
|
+
const envLocks = {
|
|
530
|
+
gcSnaps: Boolean(process.env && process.env.DSH_RECALL_GC_SNAPS),
|
|
531
|
+
gcHours: Boolean(process.env && process.env.DSH_RECALL_GC_HOURS),
|
|
532
|
+
}
|
|
533
|
+
let overridden = {}
|
|
534
|
+
let writable = false
|
|
535
|
+
try {
|
|
536
|
+
const settings = ctx.get('settings')
|
|
537
|
+
if (settings && typeof settings.describe === 'function') {
|
|
538
|
+
const list = settings.describe()
|
|
539
|
+
const ours = (Array.isArray(list) ? list : []).find((d) => d && d.ns === 'dsh-recall')
|
|
540
|
+
if (ours && ours.user && typeof ours.user === 'object') overridden = ours.user
|
|
541
|
+
writable = settings.writable !== false
|
|
542
|
+
}
|
|
543
|
+
} catch (error) { /* describe 不可用按「无覆盖」处理 */ }
|
|
544
|
+
return {
|
|
545
|
+
ok: true,
|
|
546
|
+
values: {
|
|
547
|
+
gcSnaps: cfg.gcSnaps,
|
|
548
|
+
gcHours: cfg.gcHours,
|
|
549
|
+
maxFileBytes: cfg.maxFileBytes,
|
|
550
|
+
baseExcludes: cfg.baseExcludes.slice(),
|
|
551
|
+
refillDraft: cfg.refillDraft,
|
|
552
|
+
},
|
|
553
|
+
overridden,
|
|
554
|
+
envLocks,
|
|
555
|
+
writable,
|
|
556
|
+
}
|
|
557
|
+
},
|
|
558
|
+
|
|
559
|
+
// 设置页「插件配置」卡片存配置:白名单字段 + 类型清洗后经 settings.update
|
|
560
|
+
// 写进用户层(schema 校验失败会在持久化前 reject,错误信息回显卡片),
|
|
561
|
+
// watch 链路把新值热更新进 cfg,无需重启。
|
|
562
|
+
'config-set': async (args) => {
|
|
563
|
+
const patch = args && args.patch && typeof args.patch === 'object' ? args.patch : {}
|
|
564
|
+
const clean = {}
|
|
565
|
+
if (patch.gcSnaps !== undefined) clean.gcSnaps = Number(patch.gcSnaps)
|
|
566
|
+
if (patch.gcHours !== undefined) clean.gcHours = Number(patch.gcHours)
|
|
567
|
+
if (patch.maxFileBytes !== undefined) clean.maxFileBytes = Number(patch.maxFileBytes)
|
|
568
|
+
if (patch.refillDraft !== undefined) clean.refillDraft = Boolean(patch.refillDraft)
|
|
569
|
+
if (patch.baseExcludes !== undefined) {
|
|
570
|
+
if (!Array.isArray(patch.baseExcludes)) return { ok: false, code: 'BAD_TYPE', message: 'baseExcludes 必须是字符串数组' }
|
|
571
|
+
clean.baseExcludes = patch.baseExcludes.filter((p) => typeof p === 'string' && p.trim())
|
|
572
|
+
}
|
|
573
|
+
if (!Object.keys(clean).length) return { ok: false, code: 'EMPTY_PATCH', message: '没有可写入的配置字段' }
|
|
574
|
+
let settings = null
|
|
575
|
+
try { settings = ctx.get('settings') } catch (error) { settings = null }
|
|
576
|
+
if (!settings || typeof settings.update !== 'function') {
|
|
577
|
+
return { ok: false, code: 'SETTINGS_UNAVAILABLE', message: '设置服务不可用:请在 profile 的 cordis.patch.yml 按 id: recall 覆盖配置' }
|
|
578
|
+
}
|
|
579
|
+
try {
|
|
580
|
+
await settings.update('dsh-recall', clean)
|
|
581
|
+
} catch (error) {
|
|
582
|
+
return { ok: false, code: 'SETTINGS_WRITE_FAILED', message: '配置写入失败:' + String(error && error.message ? error.message : error) }
|
|
583
|
+
}
|
|
584
|
+
return { ok: true }
|
|
585
|
+
},
|
|
586
|
+
|
|
496
587
|
// 设置页「快照管理」卡片:列表 / 磁盘占用 / 单条删除 / 手动 gc。
|
|
497
588
|
// 全部走串行队列——删除 tag 与 gc 与快照争的是同一个 git 仓库。
|
|
498
589
|
'manage': async (args) => {
|
|
@@ -639,12 +730,27 @@ export function apply(ctx, config) {
|
|
|
639
730
|
return { ok: true, messageTexts: texts }
|
|
640
731
|
}
|
|
641
732
|
if (op === 'usage') {
|
|
642
|
-
|
|
643
|
-
if (
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
733
|
+
let bytes = 0
|
|
734
|
+
if (sessionId) {
|
|
735
|
+
// 旧调用方(带会话上下文):单工作区占用
|
|
736
|
+
const root = await rt.resolveRoot(sessionId)
|
|
737
|
+
if (!root) return { ok: false, code: 'NO_ROOT', message: '无法解析当前工作区' }
|
|
738
|
+
const store = state.stores.get(root)
|
|
739
|
+
if (!store) return { ok: false, code: 'NO_STORE', message: '当前工作区尚未创建快照存储' }
|
|
740
|
+
const out = await rt.runShell(rt.scripts.diskUsageScript(store.dir), { stdoutMaxBytes: 4096 })
|
|
741
|
+
bytes = parseInt(rt.scripts.stripBom(out).trim(), 10) || 0
|
|
742
|
+
} else {
|
|
743
|
+
// 新调用方(settings.plugin.item 卡片无会话上下文):全部已知
|
|
744
|
+
// store 汇总。store 全集取内存缓存(启动预热填齐);单个失败
|
|
745
|
+
// 不影响汇总,best-effort。
|
|
746
|
+
for (const store of state.stores.values()) {
|
|
747
|
+
if (!store || !store.dir) continue
|
|
748
|
+
try {
|
|
749
|
+
const out = await rt.runShell(rt.scripts.diskUsageScript(store.dir), { stdoutMaxBytes: 4096 })
|
|
750
|
+
bytes += parseInt(rt.scripts.stripBom(out).trim(), 10) || 0
|
|
751
|
+
} catch (error) { /* 单 store 失败跳过 */ }
|
|
752
|
+
}
|
|
753
|
+
}
|
|
648
754
|
return { ok: true, bytes }
|
|
649
755
|
}
|
|
650
756
|
if (op === 'delete') {
|
|
@@ -707,7 +813,11 @@ export function apply(ctx, config) {
|
|
|
707
813
|
return { ok: true }
|
|
708
814
|
}
|
|
709
815
|
if (op === 'gc') {
|
|
710
|
-
|
|
816
|
+
// 带会话上下文:只 gc 该会话的工作区;无上下文(设置卡片):
|
|
817
|
+
// 全部已知 store 逐个 gc。两者都排进串行队列,与快照互斥。
|
|
818
|
+
const done = sessionId
|
|
819
|
+
? await enqueue(() => maint.runGc(sessionId, true))
|
|
820
|
+
: await enqueue(() => maint.runGcAll())
|
|
711
821
|
return { ok: true, gc: Boolean(done) }
|
|
712
822
|
}
|
|
713
823
|
return { ok: false, code: 'UNKNOWN_OP', message: '未知的管理操作: ' + op }
|
|
@@ -775,8 +885,12 @@ export function apply(ctx, config) {
|
|
|
775
885
|
try {
|
|
776
886
|
const records = await querySvc.listSessions()
|
|
777
887
|
for (const record of records || []) {
|
|
888
|
+
// listSessions 记录形如 {header, live, persisted},会话 id 在
|
|
889
|
+
// header.id——此前误用顶层 record.id(恒 undefined),预热重建
|
|
890
|
+
// 的孤儿快照 sessionId 记为空,树形管理里会落进「已删除会话」。
|
|
891
|
+
const id = record && record.header && record.header.id ? record.header.id : null
|
|
778
892
|
const cwd = record && record.header && record.header.cwd
|
|
779
|
-
if (cwd && !warmupRoots.has(cwd)) warmupRoots.set(cwd,
|
|
893
|
+
if (cwd && !warmupRoots.has(cwd)) warmupRoots.set(cwd, id)
|
|
780
894
|
}
|
|
781
895
|
} catch (error) { /* 冷元数据不可用则退回 live 注册表 */ }
|
|
782
896
|
}
|
package/lib/maintenance.js
CHANGED
|
@@ -11,13 +11,12 @@
|
|
|
11
11
|
* 接线),因此 gc/清理与快照天然互斥,不存在 git 锁竞态。
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
// gc 节流阈值来自 config
|
|
15
|
-
//
|
|
14
|
+
// gc 节流阈值来自 config 域(设置页「插件配置」卡片可实时改写 cfg,
|
|
15
|
+
// 因此这里按调用时取值而不是工厂创建时快照;环境变量
|
|
16
|
+
// DSH_RECALL_GC_SNAPS/GC_HOURS 仍最高优先,见 config.js):
|
|
16
17
|
// 每 gcSnaps 条快照或距上次 gc gcHours 小时,先到先触发。默认「50 条或
|
|
17
18
|
// 24 小时」——重活(gc)一天至多一次的量级,轻会话用户也不会等太久。
|
|
18
19
|
export function createMaintenance(ctx, rt, snaps, config) {
|
|
19
|
-
const GC_SNAPS = config.gcSnaps
|
|
20
|
-
const GC_MS = config.gcHours * 3600000
|
|
21
20
|
const sessions = ctx.sessions
|
|
22
21
|
const state = rt.state
|
|
23
22
|
// 平台选择的脚本模板(gc/purge 两套模板同名导出)
|
|
@@ -97,7 +96,7 @@ export function createMaintenance(ctx, rt, snaps, config) {
|
|
|
97
96
|
const last = state.gcLastAt.get(store.git) || 0
|
|
98
97
|
const count = (state.gcCount.get(store.git) || 0) + 1
|
|
99
98
|
state.gcCount.set(store.git, count)
|
|
100
|
-
if (!force && count <
|
|
99
|
+
if (!force && count < config.gcSnaps && now - last < config.gcHours * 3600000) return false
|
|
101
100
|
state.gcCount.set(store.git, 0)
|
|
102
101
|
try {
|
|
103
102
|
await sweepDeletedSessions()
|
|
@@ -109,12 +108,38 @@ export function createMaintenance(ctx, rt, snaps, config) {
|
|
|
109
108
|
return true
|
|
110
109
|
}
|
|
111
110
|
|
|
111
|
+
// 全局 gc(设置卡片没有会话上下文):清理扫描一次 + 逐 store gc。
|
|
112
|
+
// store 全集取内存缓存(启动预热与历次操作会填齐已知工作区);逐个
|
|
113
|
+
// best-effort,单个失败记错误继续。调用方(manage 端点)把它排进同一条
|
|
114
|
+
// 串行队列,与快照天然互斥,无 git 锁竞态。
|
|
115
|
+
async function runGcAll() {
|
|
116
|
+
const stores = Array.from(new Set(Array.from(state.stores.values()).filter(Boolean)))
|
|
117
|
+
if (!stores.length || !state.gitExe) return false
|
|
118
|
+
try {
|
|
119
|
+
await sweepDeletedSessions()
|
|
120
|
+
} catch (error) {
|
|
121
|
+
rt.recordError('recall sweep failed: ' + String(error))
|
|
122
|
+
}
|
|
123
|
+
let done = 0
|
|
124
|
+
for (const store of stores) {
|
|
125
|
+
try {
|
|
126
|
+
await rt.runShell(S.gcScript(store, state.gitExe), { timeoutMs: 600000, stdoutMaxBytes: 4096 })
|
|
127
|
+
done++
|
|
128
|
+
} catch (error) {
|
|
129
|
+
rt.recordError('recall gc failed for ' + (store && store.git) + ': ' + String(error))
|
|
130
|
+
}
|
|
131
|
+
state.gcLastAt.set(store.git, Date.now())
|
|
132
|
+
state.gcCount.set(store.git, 0)
|
|
133
|
+
}
|
|
134
|
+
return true
|
|
135
|
+
}
|
|
136
|
+
|
|
112
137
|
// 每条消息快照后串行调用(见 index.js 事件接线)
|
|
113
138
|
async function maybeMaintain(sessionId) {
|
|
114
139
|
await runGc(sessionId, false)
|
|
115
140
|
}
|
|
116
141
|
|
|
117
|
-
//
|
|
118
|
-
// purgeSession 是
|
|
119
|
-
return { maybeMaintain, runGc }
|
|
142
|
+
// 模块收敛:只导出被装配层消费的入口;sweepDeletedSessions /
|
|
143
|
+
// purgeSession 是 gc 的内部步骤,不对外暴露面
|
|
144
|
+
return { maybeMaintain, runGc, runGcAll }
|
|
120
145
|
}
|