dsh-plugin-tool-management 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +226 -136
- package/README_EN.md +231 -136
- package/cordis.patch.yml +18 -0
- package/docs/images/agents-md.png +0 -0
- package/docs/images/history.png +0 -0
- package/docs/images/mcp.png +0 -0
- package/docs/images/skills.png +0 -0
- package/docs/images//345/234/272/346/231/257/350/256/260/345/277/206.png +0 -0
- package/lib/agents-md/service.js +240 -0
- package/lib/client.js +2173 -850
- package/lib/history/projcache.js +335 -0
- package/lib/history/tombstone.js +32 -0
- package/lib/history/workspace.js +892 -0
- package/lib/imports/parsers.js +181 -0
- package/lib/index.js +768 -2
- package/lib/rules/provider.js +121 -0
- package/lib/rules/service.js +1657 -0
- package/lib/skills/core.js +10 -1
- package/lib/skills/service.js +5 -5
- package/package.json +18 -8
- package/screenshots.json +7 -0
package/lib/index.js
CHANGED
|
@@ -13,8 +13,44 @@
|
|
|
13
13
|
import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
14
14
|
import { createRequire } from 'node:module';
|
|
15
15
|
import { createSkillsService } from './skills/service.js';
|
|
16
|
+
import { createAgentsMdService } from './agents-md/service.js';
|
|
17
|
+
import { createRulesService } from './rules/service.js';
|
|
18
|
+
import { detectFormat, extractText, parseGenericText, parseJsonlTranscript, parseMarkdownTranscript } from './imports/parsers.js';
|
|
19
|
+
import { homedir } from 'node:os';
|
|
16
20
|
import { spawn } from 'node:child_process';
|
|
17
|
-
import { readdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
21
|
+
import { mkdir, readdir, readFile, rename, stat, unlink, writeFile } from 'node:fs/promises';
|
|
22
|
+
import { dirname, join } from 'node:path';
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Transcript export helpers — mirror the import parsers (src/imports/parsers.js)
|
|
25
|
+
// so an exported file can be imported back losslessly. Pure Node, no ctx.
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
/** 从会话事件数组提取用户/助手纯文本轮次(与导入解析器对称)。 */
|
|
28
|
+
function extractTurnsFromEvents(events) {
|
|
29
|
+
const turns = [];
|
|
30
|
+
for (const ev of events || []) {
|
|
31
|
+
if (!ev || typeof ev !== 'object')
|
|
32
|
+
continue;
|
|
33
|
+
const e = ev;
|
|
34
|
+
let content;
|
|
35
|
+
if (e.type === 'user/message')
|
|
36
|
+
content = e.data && e.data.content;
|
|
37
|
+
else if (e.type === 'assistant/message')
|
|
38
|
+
content = e.data && e.data.message && e.data.message.content;
|
|
39
|
+
else
|
|
40
|
+
continue;
|
|
41
|
+
const text = extractText(content).trim();
|
|
42
|
+
if (text)
|
|
43
|
+
turns.push({ role: e.type === 'user/message' ? 'user' : 'assistant', text });
|
|
44
|
+
}
|
|
45
|
+
return turns;
|
|
46
|
+
}
|
|
47
|
+
/** 序列化为可再导入的转录文本:Codex 风格 Markdown 或 Claude Code 风格 JSONL。 */
|
|
48
|
+
function serializeTurns(turns, format) {
|
|
49
|
+
if (format === 'jsonl') {
|
|
50
|
+
return turns.map((t) => JSON.stringify({ type: t.role, message: { role: t.role, content: t.text } })).join('\n');
|
|
51
|
+
}
|
|
52
|
+
return turns.map((t) => (t.role === 'user' ? '## User\n' : '### Assistant\n') + t.text).join('\n\n');
|
|
53
|
+
}
|
|
18
54
|
export default {
|
|
19
55
|
name: 'dsh-plugin-tool-management-host',
|
|
20
56
|
inject: ['timer', 'fs', 'settings', 'sandboxPolicy', 'webServer', 'tools', 'skills', 'sessions'],
|
|
@@ -55,6 +91,17 @@ export default {
|
|
|
55
91
|
'skill-create', 'skill-import', 'skill-upload', 'skill-delete',
|
|
56
92
|
'skill-trash-restore', 'skill-trash-delete', 'skill-open',
|
|
57
93
|
'skill-custom-add', 'skill-custom-remove',
|
|
94
|
+
// agents-md 写操作(create/update/remove 改预设库;apply 写全局 AGENTS.md;import 从外部内容建预设)
|
|
95
|
+
'agentsmd-create', 'agentsmd-update', 'agentsmd-apply', 'agentsmd-remove', 'agentsmd-import',
|
|
96
|
+
// history 写操作(archive/unarchive 改归档集合;delete 永久删除;retention-set 写保留期)
|
|
97
|
+
'history-archive', 'history-unarchive', 'history-delete', 'history-retention-set',
|
|
98
|
+
'history-unarchive-batch', 'history-delete-batch', 'history-import', 'history-export',
|
|
99
|
+
'history-archive-batch',
|
|
100
|
+
// rules 写操作(v0.3:create/update/remove/restore 改记忆文件;toggle/set-index/
|
|
101
|
+
// set-active 改侧车索引;create-scene/remove-scene 建删场景目录。
|
|
102
|
+
// set-active = 插件内"启用场景"开关,全局持久化)
|
|
103
|
+
'rules-create', 'rules-update', 'rules-remove', 'rules-restore', 'rules-toggle', 'rules-set-index', 'rules-set-active',
|
|
104
|
+
'rules-create-scene', 'rules-remove-scene',
|
|
58
105
|
]);
|
|
59
106
|
const wait = (ms) => ctx.timeout(ms);
|
|
60
107
|
const message = (e) => String((e && e.message) || e);
|
|
@@ -76,6 +123,150 @@ export default {
|
|
|
76
123
|
catch (e) {
|
|
77
124
|
console.error('[dsh-plugin-tool-management] skills provider setup failed:', message(e));
|
|
78
125
|
}
|
|
126
|
+
// ---------- agents-md 预设库 + 切换 ----------
|
|
127
|
+
// DSH 全局指令基线只有 ~/.dsh/AGENTS.md 一个文件,无内置多预设切换;
|
|
128
|
+
// 本服务在插件目录内 data/agents-md-presets/ 维护预设库,「应用」= 写入
|
|
129
|
+
// ~/.dsh/AGENTS.md,新会话生效(当前会话不变,DSH 本身如此)。
|
|
130
|
+
// presetsDir 可由 config 注入(测试用),否则落到插件根 data/。
|
|
131
|
+
const PLUGIN_ROOT = (() => {
|
|
132
|
+
try {
|
|
133
|
+
return dirname(createRequire(import.meta.url).resolve('../package.json'));
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return process.cwd();
|
|
137
|
+
}
|
|
138
|
+
})();
|
|
139
|
+
const agentsMdPresetsDir = String(config?.presetsDir || join(PLUGIN_ROOT, 'data', 'agents-md-presets'));
|
|
140
|
+
const agentsMdService = createAgentsMdService(ctx, {
|
|
141
|
+
presetsDir: agentsMdPresetsDir,
|
|
142
|
+
getGlobalAgentsMdPath: async () => {
|
|
143
|
+
const p = await ensurePaths();
|
|
144
|
+
const sep = p.home.indexOf('\\') >= 0 ? '\\' : '/';
|
|
145
|
+
return p.home + sep + 'AGENTS.md';
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
// ---------- rules(规则/记忆,v0.3)----------
|
|
149
|
+
// 规则真源 $DSH_HOME/rules/<场景>/<name>.md(仅用户级,D1)。**场景 = 一级目录**;
|
|
150
|
+
// 单投影 = 活动场景记忆 → per-agent systemPrompt 段(自动在场,模型无需调用任何工具)。
|
|
151
|
+
// 原"始终层写 ~/.dsh/AGENTS.md"已下线(变更单 01 §4/§10):公共基线由 _shared/ 承担。
|
|
152
|
+
// rulesRoot / rulesStateDir 仅测试注入,生产留空由服务按 DSH_HOME 解析。
|
|
153
|
+
const rulesService = createRulesService(ctx, {
|
|
154
|
+
rulesRoot: String(config?.rulesRoot || ''),
|
|
155
|
+
stateDir: String(config?.rulesStateDir || ''),
|
|
156
|
+
// 场景记忆段预算(字节),默认 65536;仅用于测试与特殊部署调优。
|
|
157
|
+
...(Number.isFinite(Number(config?.rulesMaxBytes))
|
|
158
|
+
? { maxBytes: Number(config.rulesMaxBytes) }
|
|
159
|
+
: {}),
|
|
160
|
+
});
|
|
161
|
+
try {
|
|
162
|
+
ctx.effect(() => rulesService.registerProviders(), 'dsh-plugin-tool-management: rules providers');
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
console.error('[dsh-plugin-tool-management] rules provider setup failed:', message(e));
|
|
166
|
+
}
|
|
167
|
+
// ---------- history(归档会话管理,折叠自 dsh-archive-manager)----------
|
|
168
|
+
// cordis.patch.yml 禁用官方 workspace 与 session-projection-cache,插入本插件
|
|
169
|
+
// 的归档感知子类(lib/history/workspace.js + lib/history/projcache.js)。子类
|
|
170
|
+
// 经 Service.constructor 继承官方服务名(workspaceRegistry / sessionProjectionCache),
|
|
171
|
+
// 因此 ctx.get('workspaceRegistry') 拿到的就是归档子类实例。
|
|
172
|
+
//
|
|
173
|
+
// 保留期(retentionDays):0 = 永久不删除,7/30 = 归档满 N 天后自动永久删除。
|
|
174
|
+
// archivedAt 账本由子类自身维护(data/history-archived-at.json);保留期配置
|
|
175
|
+
// 由本插件 data/history-retention.json 存储。sweeper 在 apply 时跑一次并周期
|
|
176
|
+
// 复跑(默认 6h,可经 config.sweepIntervalMs 覆盖),把到期归档会话批量永久删除。
|
|
177
|
+
const historyRetentionPath = String(config?.historyRetentionPath || join(PLUGIN_ROOT, 'data', 'history-retention.json'));
|
|
178
|
+
const sweepIntervalMs = Number(config?.sweepIntervalMs || 0) || 6 * 60 * 60 * 1000;
|
|
179
|
+
function getHistoryRegistry() {
|
|
180
|
+
const r = ctx.get('workspaceRegistry');
|
|
181
|
+
return r && typeof r.archiveSession === 'function' ? r : undefined;
|
|
182
|
+
}
|
|
183
|
+
async function readHistoryRetention() {
|
|
184
|
+
try {
|
|
185
|
+
const raw = await readFile(historyRetentionPath, 'utf8');
|
|
186
|
+
const obj = JSON.parse(raw);
|
|
187
|
+
const days = Number((obj && obj.retentionDays) ?? 0);
|
|
188
|
+
const updatedAt = Number((obj && obj.updatedAt) ?? 0);
|
|
189
|
+
return {
|
|
190
|
+
retentionDays: Number.isFinite(days) && days >= 0 ? days : 0,
|
|
191
|
+
updatedAt: Number.isFinite(updatedAt) && updatedAt > 0 ? updatedAt : 0,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return { retentionDays: 0, updatedAt: 0 };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async function writeHistoryRetention(retentionDays) {
|
|
199
|
+
try {
|
|
200
|
+
const dir = dirname(historyRetentionPath);
|
|
201
|
+
await mkdir(dir, { recursive: true });
|
|
202
|
+
// updatedAt = 修改时刻:每次改保留期,已归档会话的到期基线重置为此时刻。
|
|
203
|
+
await writeFile(historyRetentionPath, JSON.stringify({ retentionDays, updatedAt: Date.now() }), 'utf8');
|
|
204
|
+
}
|
|
205
|
+
catch (e) {
|
|
206
|
+
console.error('[dsh-plugin-tool-management] write history-retention failed:', message(e));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* 计算到期应删的归档会话。基线 = max(archivedAt(账本)?? createdAt(元数据),
|
|
211
|
+
* updatedAt(最近一次修改保留期的时刻))。改保留期即重置倒计时:到期时刻从
|
|
212
|
+
* 修改时刻起按新天数重新计算;updatedAt 缺失(旧配置)时退回归档时刻语义。
|
|
213
|
+
* retentionDays <= 0 表示永久不删除,返回空集。纯函数:便于测试。
|
|
214
|
+
*/
|
|
215
|
+
function expiredArchivedIds(items, retentionDays, now, updatedAt = 0) {
|
|
216
|
+
if (!(retentionDays > 0))
|
|
217
|
+
return [];
|
|
218
|
+
const cutoff = now - retentionDays * 86400000;
|
|
219
|
+
const out = [];
|
|
220
|
+
for (const it of items) {
|
|
221
|
+
const archived = (typeof it.archivedAt === 'number' && Number.isFinite(it.archivedAt))
|
|
222
|
+
? it.archivedAt
|
|
223
|
+
: (typeof it.createdAt === 'number' && Number.isFinite(it.createdAt) ? it.createdAt : undefined);
|
|
224
|
+
if (archived === undefined)
|
|
225
|
+
continue;
|
|
226
|
+
const baseline = updatedAt > 0 ? Math.max(archived, updatedAt) : archived;
|
|
227
|
+
if (baseline <= cutoff)
|
|
228
|
+
out.push(it.sessionId);
|
|
229
|
+
}
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
async function sweepHistory() {
|
|
233
|
+
const registry = getHistoryRegistry();
|
|
234
|
+
if (!registry)
|
|
235
|
+
return { swept: [] };
|
|
236
|
+
const { retentionDays, updatedAt } = await readHistoryRetention();
|
|
237
|
+
if (!(retentionDays > 0))
|
|
238
|
+
return { swept: [] };
|
|
239
|
+
try {
|
|
240
|
+
const details = (typeof registry.archivedSessionDetails === 'function')
|
|
241
|
+
? (await registry.archivedSessionDetails()).items
|
|
242
|
+
: (await registry.archivedSessionMetadata()).items.map((i) => ({ sessionId: i.sessionId, createdAt: i.createdAt, archivedAt: registry.archivedAt?.(i.sessionId) }));
|
|
243
|
+
const expired = expiredArchivedIds(details, retentionDays, Date.now(), updatedAt);
|
|
244
|
+
if (expired.length === 0)
|
|
245
|
+
return { swept: [] };
|
|
246
|
+
const res = await registry.deleteArchivedSessions({ scope: 'sessions', sessionIds: expired });
|
|
247
|
+
return { swept: res.deletedSessionIds || [] };
|
|
248
|
+
}
|
|
249
|
+
catch (e) {
|
|
250
|
+
console.error('[dsh-plugin-tool-management] history sweep failed:', message(e));
|
|
251
|
+
return { swept: [] };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// 启动时扫一次,再周期复跑。fake-ctx 测试里 ctx.effect 立即调用并 dispose,
|
|
255
|
+
// interval 未提供时退化为不挂钟(不阻塞测试)。
|
|
256
|
+
try {
|
|
257
|
+
void sweepHistory();
|
|
258
|
+
}
|
|
259
|
+
catch { /* 非致命 */ }
|
|
260
|
+
try {
|
|
261
|
+
ctx.effect(() => {
|
|
262
|
+
const timer = ctx;
|
|
263
|
+
const fn = typeof timer.interval === 'function' ? timer.interval : (typeof timer.setInterval === 'function' ? timer.setInterval : undefined);
|
|
264
|
+
if (!fn)
|
|
265
|
+
return () => { };
|
|
266
|
+
return fn(() => { void sweepHistory(); }, sweepIntervalMs);
|
|
267
|
+
}, 'dsh-plugin-tool-management: history sweep');
|
|
268
|
+
}
|
|
269
|
+
catch { /* timer 缺失时静默 */ }
|
|
79
270
|
// ---------- path discovery ----------
|
|
80
271
|
// Known limitation: profile detection probes 'web' then 'headless' by
|
|
81
272
|
// presence of profiles/<name>/cordis.patch.yml, then falls back to any
|
|
@@ -733,7 +924,7 @@ export default {
|
|
|
733
924
|
return { ok: true, notes: map };
|
|
734
925
|
});
|
|
735
926
|
}
|
|
736
|
-
const SETTINGS_DEFAULTS = { pollIntervalMs: 5000, toolDescriptionMaxLength: 0 };
|
|
927
|
+
const SETTINGS_DEFAULTS = { pollIntervalMs: 5000, toolDescriptionMaxLength: 0, requireConfirmForModelRuleWrite: true };
|
|
737
928
|
let pluginSettingsCache = null;
|
|
738
929
|
function clampInt(value, min, max, fallback) {
|
|
739
930
|
// Number(null) is 0 — treat missing/empty input as "use the default".
|
|
@@ -755,6 +946,10 @@ export default {
|
|
|
755
946
|
pollIntervalMs: clampInt(raw && raw.pollIntervalMs, 2000, 60000, SETTINGS_DEFAULTS.pollIntervalMs),
|
|
756
947
|
// 0 = keep descriptions in full (the UI default); > 0 truncates.
|
|
757
948
|
toolDescriptionMaxLength: clampInt(raw && raw.toolDescriptionMaxLength, 0, 2000, SETTINGS_DEFAULTS.toolDescriptionMaxLength),
|
|
949
|
+
// 模型写规则需确认(D2):程序化强制,只读设置供 tools/pre-execute 判定。
|
|
950
|
+
requireConfirmForModelRuleWrite: (raw && typeof raw.requireConfirmForModelRuleWrite === 'boolean')
|
|
951
|
+
? raw.requireConfirmForModelRuleWrite
|
|
952
|
+
: SETTINGS_DEFAULTS.requireConfirmForModelRuleWrite,
|
|
758
953
|
},
|
|
759
954
|
};
|
|
760
955
|
return pluginSettingsCache.value;
|
|
@@ -766,6 +961,9 @@ export default {
|
|
|
766
961
|
const next = {
|
|
767
962
|
pollIntervalMs: clampInt(args.pollIntervalMs, 2000, 60000, current.pollIntervalMs),
|
|
768
963
|
toolDescriptionMaxLength: clampInt(args.toolDescriptionMaxLength, 0, 2000, current.toolDescriptionMaxLength),
|
|
964
|
+
requireConfirmForModelRuleWrite: (args && typeof args.requireConfirmForModelRuleWrite === 'boolean')
|
|
965
|
+
? args.requireConfirmForModelRuleWrite
|
|
966
|
+
: current.requireConfirmForModelRuleWrite,
|
|
769
967
|
};
|
|
770
968
|
const p = await ensurePaths();
|
|
771
969
|
return withWriteLock(async () => {
|
|
@@ -1565,6 +1763,34 @@ export default {
|
|
|
1565
1763
|
return { ok: false, error: '无法调用系统默认打开方式' };
|
|
1566
1764
|
return { ok: true, path: abs };
|
|
1567
1765
|
}
|
|
1766
|
+
// 批量操作目标校验:scope 白名单;sessions 需非空 sessionIds;workspace 需
|
|
1767
|
+
// workspaceId。与 ArchiveWorkspaceRegistry.archivedBatchTargetSchema 语义一致。
|
|
1768
|
+
function parseHistoryBatchTarget(raw) {
|
|
1769
|
+
const t = raw && typeof raw === 'object' ? raw : null;
|
|
1770
|
+
if (!t)
|
|
1771
|
+
return { ok: false, error: '缺少 target' };
|
|
1772
|
+
const scope = t.scope;
|
|
1773
|
+
if (scope !== 'all' && scope !== 'ungrouped' && scope !== 'sessions' && scope !== 'workspace') {
|
|
1774
|
+
return { ok: false, error: 'target.scope 不合法' };
|
|
1775
|
+
}
|
|
1776
|
+
if (scope === 'sessions') {
|
|
1777
|
+
if (!Array.isArray(t.sessionIds) || t.sessionIds.length === 0 ||
|
|
1778
|
+
t.sessionIds.some((id) => typeof id !== 'string' || !String(id).trim())) {
|
|
1779
|
+
return { ok: false, error: 'scope=sessions 需要非空的 sessionIds 数组' };
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
if (scope === 'workspace' && !(typeof t.workspaceId === 'string' && String(t.workspaceId).trim())) {
|
|
1783
|
+
return { ok: false, error: 'scope=workspace 需要 workspaceId' };
|
|
1784
|
+
}
|
|
1785
|
+
return {
|
|
1786
|
+
ok: true,
|
|
1787
|
+
target: {
|
|
1788
|
+
scope,
|
|
1789
|
+
...(scope === 'sessions' ? { sessionIds: t.sessionIds.map((s) => String(s).trim()) } : {}),
|
|
1790
|
+
...(scope === 'workspace' ? { workspaceId: String(t.workspaceId).trim() } : {}),
|
|
1791
|
+
},
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1568
1794
|
const handlers = {
|
|
1569
1795
|
'plugin-version': pluginVersion,
|
|
1570
1796
|
'mcpm-list': mcpmListView,
|
|
@@ -1588,6 +1814,426 @@ export default {
|
|
|
1588
1814
|
// skill-delete / skill-trash-restore / skill-trash-delete
|
|
1589
1815
|
// 成功返回 {ok:true, data};core 业务失败原样透传 {ok:false, error, code?, params?}。
|
|
1590
1816
|
...skillsService.ops,
|
|
1817
|
+
// rules ops(由 ./rules/service.js 提供):rules-list / rules-read / rules-budget /
|
|
1818
|
+
// rules-diagnose / rules-create / rules-update / rules-remove / rules-restore /
|
|
1819
|
+
// rules-trash-list / rules-trash-remove / rules-toggle / rules-set-index /
|
|
1820
|
+
// rules-set-active / rules-create-scene / rules-remove-scene。
|
|
1821
|
+
// 成功返回扁平 {ok:true, ...}(不套 data),失败 {ok:false, error, code?}。
|
|
1822
|
+
...rulesService.ops,
|
|
1823
|
+
// AGENTS.md 预设库 ops(由 ./agents-md/service.js 提供):agentsmd-list /
|
|
1824
|
+
// agentsmd-read / agentsmd-create / agentsmd-update / agentsmd-remove /
|
|
1825
|
+
// agentsmd-apply / agentsmd-get-current
|
|
1826
|
+
'agentsmd-list': () => agentsMdService.list(),
|
|
1827
|
+
'agentsmd-read': (args) => agentsMdService.read(String((args && args.id) || '')),
|
|
1828
|
+
'agentsmd-create': (args) => agentsMdService.create(String((args && args.id) || ''), args && args.from ? String(args.from) : undefined),
|
|
1829
|
+
'agentsmd-update': (args) => agentsMdService.update(String((args && args.id) || ''), String((args && args.content) ?? '')),
|
|
1830
|
+
'agentsmd-apply': (args) => agentsMdService.apply(String((args && args.id) || '')),
|
|
1831
|
+
'agentsmd-get-current': () => agentsMdService.getCurrent(),
|
|
1832
|
+
'agentsmd-remove': (args) => agentsMdService.remove(String((args && args.id) || '')),
|
|
1833
|
+
'agentsmd-import': (args) => agentsMdService.importPreset(String((args && args.id) || ''), String((args && args.content) ?? '')),
|
|
1834
|
+
// History(归档会话管理)ops。只读:history-list / history-retention-get;
|
|
1835
|
+
// 写:history-archive / history-unarchive / history-delete / history-retention-set。
|
|
1836
|
+
// workspaceRegistry 缺失(补丁未生效)时返回明确错误,不崩页面。
|
|
1837
|
+
'history-list': async () => {
|
|
1838
|
+
const registry = getHistoryRegistry();
|
|
1839
|
+
if (!registry)
|
|
1840
|
+
return { ok: false, error: '归档服务未挂载:workspace 已被替换?请确认本插件补丁已生效。' };
|
|
1841
|
+
try {
|
|
1842
|
+
const items = (typeof registry.archivedSessionDetails === 'function')
|
|
1843
|
+
? (await registry.archivedSessionDetails()).items
|
|
1844
|
+
: (await registry.archivedSessionMetadata()).items.map((i) => ({ sessionId: i.sessionId, createdAt: i.createdAt, archivedAt: registry.archivedAt?.(i.sessionId) }));
|
|
1845
|
+
const { retentionDays } = await readHistoryRetention();
|
|
1846
|
+
// 分组增强(best-effort):用 workspace 记账表反查每个归档会话的归属,
|
|
1847
|
+
// 供客户端按项目分组。requireTable 缺失或任何异常都静默降级为扁平列表。
|
|
1848
|
+
if (typeof registry.requireTable === 'function') {
|
|
1849
|
+
try {
|
|
1850
|
+
const table = registry.requireTable();
|
|
1851
|
+
const state = typeof registry.requireState === 'function' ? registry.requireState() : undefined;
|
|
1852
|
+
const wsIds = state && Array.isArray(state.workspaceIds) && state.workspaceIds.length
|
|
1853
|
+
? state.workspaceIds
|
|
1854
|
+
: [...table.entries()].map(([id]) => id);
|
|
1855
|
+
const owned = new Map();
|
|
1856
|
+
const workspaces = {};
|
|
1857
|
+
for (const wid of wsIds) {
|
|
1858
|
+
const rec = table.get(wid);
|
|
1859
|
+
if (!rec || !Array.isArray(rec.sessionIds))
|
|
1860
|
+
continue;
|
|
1861
|
+
for (const sid of rec.sessionIds)
|
|
1862
|
+
if (!owned.has(sid))
|
|
1863
|
+
owned.set(sid, wid);
|
|
1864
|
+
workspaces[wid] = {
|
|
1865
|
+
title: (rec.title && String(rec.title)) || String(wid),
|
|
1866
|
+
...(typeof rec.path === 'string' && rec.path ? { path: rec.path } : {}),
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
for (const it of items) {
|
|
1870
|
+
const wid = owned.get(it.sessionId);
|
|
1871
|
+
if (wid !== undefined)
|
|
1872
|
+
it.workspaceId = wid;
|
|
1873
|
+
}
|
|
1874
|
+
return { ok: true, items, workspaces, retentionDays };
|
|
1875
|
+
}
|
|
1876
|
+
catch (e) { /* 降级为下方扁平返回 */ }
|
|
1877
|
+
}
|
|
1878
|
+
return { ok: true, items, retentionDays };
|
|
1879
|
+
}
|
|
1880
|
+
catch (e) {
|
|
1881
|
+
return { ok: false, error: message(e) };
|
|
1882
|
+
}
|
|
1883
|
+
},
|
|
1884
|
+
// 枚举全部持久化会话(含未归档/冷会话)供导出弹窗选择;只读,不门控。
|
|
1885
|
+
// 标题经投影缓存 best-effort;任何一步失败只降级为缺字段。
|
|
1886
|
+
'history-sessions': async () => {
|
|
1887
|
+
const registry = getHistoryRegistry();
|
|
1888
|
+
if (!registry || typeof registry.listStoredHeaders !== 'function') {
|
|
1889
|
+
return { ok: false, error: '当前环境不支持枚举全部会话(registry 未实现 listStoredHeaders)' };
|
|
1890
|
+
}
|
|
1891
|
+
try {
|
|
1892
|
+
const headers = await registry.listStoredHeaders();
|
|
1893
|
+
const archivedItems = typeof registry.archivedSessionDetails === 'function'
|
|
1894
|
+
? (await registry.archivedSessionDetails()).items
|
|
1895
|
+
: (await registry.archivedSessionMetadata()).items;
|
|
1896
|
+
const archived = new Set(archivedItems.map((i) => i.sessionId));
|
|
1897
|
+
// 工作区归属反查(与 history-list 一致;best-effort)。
|
|
1898
|
+
const table = typeof registry.requireTable === 'function' ? registry.requireTable() : undefined;
|
|
1899
|
+
const state = typeof registry.requireState === 'function' ? registry.requireState() : undefined;
|
|
1900
|
+
const wsIds = state && Array.isArray(state.workspaceIds) && state.workspaceIds.length
|
|
1901
|
+
? state.workspaceIds
|
|
1902
|
+
: (table ? [...table.entries()].map(([id]) => id) : []);
|
|
1903
|
+
const owned = new Map();
|
|
1904
|
+
if (table) {
|
|
1905
|
+
for (const wid of wsIds) {
|
|
1906
|
+
const rec = table.get(wid);
|
|
1907
|
+
if (!rec || !Array.isArray(rec.sessionIds))
|
|
1908
|
+
continue;
|
|
1909
|
+
for (const sid of rec.sessionIds)
|
|
1910
|
+
if (!owned.has(sid))
|
|
1911
|
+
owned.set(sid, wid);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
const cache = ctx.get('sessionProjectionCache');
|
|
1915
|
+
const items = [];
|
|
1916
|
+
for (const h of headers) {
|
|
1917
|
+
const sessionId = h && typeof h.id === 'string' ? h.id : undefined;
|
|
1918
|
+
if (!sessionId)
|
|
1919
|
+
continue;
|
|
1920
|
+
// 过滤子代理派生的会话:不占用用户会话列表,也不应出现在导出选择里。
|
|
1921
|
+
if (h.origin === 'subagent')
|
|
1922
|
+
continue;
|
|
1923
|
+
const item = {
|
|
1924
|
+
sessionId,
|
|
1925
|
+
archived: archived.has(sessionId),
|
|
1926
|
+
};
|
|
1927
|
+
if (typeof h.cwd === 'string' && h.cwd)
|
|
1928
|
+
item.cwd = h.cwd;
|
|
1929
|
+
if (typeof h.createdAt === 'number' && Number.isFinite(h.createdAt))
|
|
1930
|
+
item.createdAt = h.createdAt;
|
|
1931
|
+
const wid = owned.get(sessionId);
|
|
1932
|
+
if (wid !== undefined)
|
|
1933
|
+
item.workspaceId = wid;
|
|
1934
|
+
if (cache && typeof cache.cachedSnapshot === 'function') {
|
|
1935
|
+
try {
|
|
1936
|
+
const snap = cache.cachedSnapshot(h, 0, ['title']);
|
|
1937
|
+
if (snap && snap.values && typeof snap.values.title === 'string')
|
|
1938
|
+
item.title = snap.values.title;
|
|
1939
|
+
}
|
|
1940
|
+
catch { /* title best-effort */ }
|
|
1941
|
+
}
|
|
1942
|
+
// 工作区目录可能已被删除/移动(孤儿会话):best-effort 标记,供导出选择时识别。
|
|
1943
|
+
if (item.cwd) {
|
|
1944
|
+
try {
|
|
1945
|
+
const st = await stat(item.cwd);
|
|
1946
|
+
item.cwdMissing = !st.isDirectory();
|
|
1947
|
+
}
|
|
1948
|
+
catch {
|
|
1949
|
+
item.cwdMissing = true;
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
items.push(item);
|
|
1953
|
+
}
|
|
1954
|
+
return { ok: true, items };
|
|
1955
|
+
}
|
|
1956
|
+
catch (e) {
|
|
1957
|
+
return { ok: false, error: message(e) };
|
|
1958
|
+
}
|
|
1959
|
+
},
|
|
1960
|
+
// 导出默认目录:桌面(存在时)否则用户主目录。只读,不门控。
|
|
1961
|
+
'history-export-defaults': async () => {
|
|
1962
|
+
let dir = homedir();
|
|
1963
|
+
try {
|
|
1964
|
+
const desktop = join(homedir(), 'Desktop');
|
|
1965
|
+
const st = await stat(desktop);
|
|
1966
|
+
if (st.isDirectory())
|
|
1967
|
+
dir = desktop;
|
|
1968
|
+
}
|
|
1969
|
+
catch { /* 桌面路径不可用 → 回退主目录 */ }
|
|
1970
|
+
return { ok: true, defaultDir: dir };
|
|
1971
|
+
},
|
|
1972
|
+
// 列出目录的子目录,供客户端「选择文件夹」弹窗逐级浏览。dir 为空时返回根
|
|
1973
|
+
// 视图(Windows 枚举盘符,其他平台返回 '/')。只读,不门控。
|
|
1974
|
+
'dir-list': async (args) => {
|
|
1975
|
+
const raw = String((args && args.dir) || '').trim();
|
|
1976
|
+
try {
|
|
1977
|
+
if (!raw) {
|
|
1978
|
+
if (process.platform !== 'win32')
|
|
1979
|
+
return { ok: true, current: '/', parent: null, entries: [] };
|
|
1980
|
+
const drives = [];
|
|
1981
|
+
for (let c = 65; c <= 90; c++) {
|
|
1982
|
+
const root = String.fromCharCode(c) + ':\\';
|
|
1983
|
+
try {
|
|
1984
|
+
await stat(root);
|
|
1985
|
+
drives.push({ name: root, path: root });
|
|
1986
|
+
}
|
|
1987
|
+
catch { /* 跳过不存在的盘符 */ }
|
|
1988
|
+
}
|
|
1989
|
+
return { ok: true, current: '', parent: null, entries: drives };
|
|
1990
|
+
}
|
|
1991
|
+
const st = await stat(raw);
|
|
1992
|
+
if (!st.isDirectory())
|
|
1993
|
+
return { ok: false, error: '该路径不是目录' };
|
|
1994
|
+
const parent = dirname(raw);
|
|
1995
|
+
const names = await readdir(raw, { withFileTypes: true });
|
|
1996
|
+
const entries = names
|
|
1997
|
+
.filter((d) => d.isDirectory())
|
|
1998
|
+
.map((d) => ({ name: d.name, path: join(raw, d.name) }))
|
|
1999
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
2000
|
+
return { ok: true, current: raw, parent: parent === raw ? null : parent, entries };
|
|
2001
|
+
}
|
|
2002
|
+
catch (e) {
|
|
2003
|
+
return { ok: false, error: message(e) };
|
|
2004
|
+
}
|
|
2005
|
+
},
|
|
2006
|
+
'history-archive': async (args) => {
|
|
2007
|
+
const registry = getHistoryRegistry();
|
|
2008
|
+
if (!registry)
|
|
2009
|
+
return { ok: false, error: '归档服务未挂载' };
|
|
2010
|
+
const sessionId = String((args && args.sessionId) || '').trim();
|
|
2011
|
+
if (!sessionId)
|
|
2012
|
+
return { ok: false, error: '缺少 sessionId' };
|
|
2013
|
+
try {
|
|
2014
|
+
await registry.archiveSession(sessionId);
|
|
2015
|
+
return { ok: true, sessionId };
|
|
2016
|
+
}
|
|
2017
|
+
catch (e) {
|
|
2018
|
+
return { ok: false, error: message(e) };
|
|
2019
|
+
}
|
|
2020
|
+
},
|
|
2021
|
+
// 批量归档(导出弹窗「归档所选」):把选中的会话收进 History,纳入保留期管理。
|
|
2022
|
+
'history-archive-batch': async (args) => {
|
|
2023
|
+
const registry = getHistoryRegistry();
|
|
2024
|
+
if (!registry)
|
|
2025
|
+
return { ok: false, error: '归档服务未挂载' };
|
|
2026
|
+
const rawIds = (args && args.sessionIds) || [];
|
|
2027
|
+
const ids = Array.isArray(rawIds) ? rawIds.map((s) => String(s).trim()).filter(Boolean) : [];
|
|
2028
|
+
if (!ids.length)
|
|
2029
|
+
return { ok: false, error: '请至少选择一个会话' };
|
|
2030
|
+
const archived = [];
|
|
2031
|
+
const failed = [];
|
|
2032
|
+
for (const sessionId of ids) {
|
|
2033
|
+
try {
|
|
2034
|
+
await registry.archiveSession(sessionId);
|
|
2035
|
+
archived.push(sessionId);
|
|
2036
|
+
}
|
|
2037
|
+
catch (e) {
|
|
2038
|
+
failed.push({ sessionId, error: message(e) });
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
return { ok: true, archived, failed };
|
|
2042
|
+
},
|
|
2043
|
+
'history-unarchive': async (args) => {
|
|
2044
|
+
const registry = getHistoryRegistry();
|
|
2045
|
+
if (!registry)
|
|
2046
|
+
return { ok: false, error: '归档服务未挂载' };
|
|
2047
|
+
const sessionId = String((args && args.sessionId) || '').trim();
|
|
2048
|
+
if (!sessionId)
|
|
2049
|
+
return { ok: false, error: '缺少 sessionId' };
|
|
2050
|
+
try {
|
|
2051
|
+
const r = await registry.unarchiveSession(sessionId);
|
|
2052
|
+
return { ok: true, archivedSessionIds: r.archivedSessionIds };
|
|
2053
|
+
}
|
|
2054
|
+
catch (e) {
|
|
2055
|
+
return { ok: false, error: message(e) };
|
|
2056
|
+
}
|
|
2057
|
+
},
|
|
2058
|
+
'history-delete': async (args) => {
|
|
2059
|
+
const registry = getHistoryRegistry();
|
|
2060
|
+
if (!registry)
|
|
2061
|
+
return { ok: false, error: '归档服务未挂载' };
|
|
2062
|
+
const sessionId = String((args && args.sessionId) || '').trim();
|
|
2063
|
+
if (!sessionId)
|
|
2064
|
+
return { ok: false, error: '缺少 sessionId' };
|
|
2065
|
+
try {
|
|
2066
|
+
await registry.deleteSession(sessionId);
|
|
2067
|
+
return { ok: true, sessionId, deleted: true };
|
|
2068
|
+
}
|
|
2069
|
+
catch (e) {
|
|
2070
|
+
return { ok: false, error: message(e) };
|
|
2071
|
+
}
|
|
2072
|
+
},
|
|
2073
|
+
'history-unarchive-batch': async (args) => {
|
|
2074
|
+
const registry = getHistoryRegistry();
|
|
2075
|
+
if (!registry)
|
|
2076
|
+
return { ok: false, error: '归档服务未挂载' };
|
|
2077
|
+
if (typeof registry.unarchiveSessions !== 'function')
|
|
2078
|
+
return { ok: false, error: '当前环境不支持批量恢复(registry 未实现 unarchiveSessions)' };
|
|
2079
|
+
const parsed = parseHistoryBatchTarget(args && args.target);
|
|
2080
|
+
if (!parsed.ok)
|
|
2081
|
+
return parsed;
|
|
2082
|
+
try {
|
|
2083
|
+
const r = await registry.unarchiveSessions(parsed.target);
|
|
2084
|
+
return { ok: true, unarchivedSessionIds: r.unarchivedSessionIds, archivedSessionIds: r.archivedSessionIds };
|
|
2085
|
+
}
|
|
2086
|
+
catch (e) {
|
|
2087
|
+
return { ok: false, error: message(e) };
|
|
2088
|
+
}
|
|
2089
|
+
},
|
|
2090
|
+
'history-delete-batch': async (args) => {
|
|
2091
|
+
const registry = getHistoryRegistry();
|
|
2092
|
+
if (!registry)
|
|
2093
|
+
return { ok: false, error: '归档服务未挂载' };
|
|
2094
|
+
const parsed = parseHistoryBatchTarget(args && args.target);
|
|
2095
|
+
if (!parsed.ok)
|
|
2096
|
+
return parsed;
|
|
2097
|
+
try {
|
|
2098
|
+
const r = await registry.deleteArchivedSessions(parsed.target);
|
|
2099
|
+
return { ok: true, requestedSessionIds: r.requestedSessionIds, deletedSessionIds: r.deletedSessionIds, skippedSessionIds: r.skippedSessionIds, failures: r.failures };
|
|
2100
|
+
}
|
|
2101
|
+
catch (e) {
|
|
2102
|
+
return { ok: false, error: message(e) };
|
|
2103
|
+
}
|
|
2104
|
+
},
|
|
2105
|
+
// 从其他 Agent(Claude Code / Cursor JSONL、Codex Markdown、通用文本)导入对话,
|
|
2106
|
+
// 通过 sessions.create 的 seed 机制生成一个可继续对话的全新会话。
|
|
2107
|
+
'history-import': async (args) => {
|
|
2108
|
+
const fileName = String((args && args.fileName) || '').trim();
|
|
2109
|
+
const content = String((args && args.content) ?? '');
|
|
2110
|
+
if (!fileName || !content.trim())
|
|
2111
|
+
return { ok: false, error: '缺少文件内容' };
|
|
2112
|
+
let turns;
|
|
2113
|
+
try {
|
|
2114
|
+
const format = detectFormat(fileName, content);
|
|
2115
|
+
turns = format === 'jsonl' ? parseJsonlTranscript(content)
|
|
2116
|
+
: format === 'markdown' ? parseMarkdownTranscript(content)
|
|
2117
|
+
: parseGenericText(content);
|
|
2118
|
+
}
|
|
2119
|
+
catch (e) {
|
|
2120
|
+
return { ok: false, error: '对话解析失败: ' + message(e) };
|
|
2121
|
+
}
|
|
2122
|
+
if (!turns.length)
|
|
2123
|
+
return { ok: false, error: '未能从该文件中识别出对话内容' };
|
|
2124
|
+
const sessions = ctx.get('sessions');
|
|
2125
|
+
if (!sessions || typeof sessions.create !== 'function')
|
|
2126
|
+
return { ok: false, error: '当前环境不支持创建会话(sessions.create 不可用)' };
|
|
2127
|
+
// 仅接受绝对路径的 cwd;非法或缺失时会话不带目录(归入未分组)。
|
|
2128
|
+
const cwdArg = String((args && args.cwd) || '').trim();
|
|
2129
|
+
const cwd = /^([A-Za-z]:[\\/]|\\\\|\/)/.test(cwdArg) ? cwdArg : undefined;
|
|
2130
|
+
// seed 事件信封:seq 从 0 连续、time 为安全整数、surface 事件必须带 surfaceOp:'append'。
|
|
2131
|
+
const base = Date.now();
|
|
2132
|
+
const seed = turns.map((turn, i) => ({
|
|
2133
|
+
type: turn.role === 'user' ? 'user/message' : 'assistant/message',
|
|
2134
|
+
seq: i,
|
|
2135
|
+
time: base + i,
|
|
2136
|
+
data: turn.role === 'user'
|
|
2137
|
+
? { id: 'msg-' + i, role: 'user', content: [{ type: 'text', text: turn.text }], source: { kind: 'typed' } }
|
|
2138
|
+
: { message: { id: 'msg-' + i, role: 'assistant', content: [{ type: 'text', text: turn.text }], source: { kind: 'model', provider: 'imported', model: 'imported' } }, turn: i, step: 0, stream: [] },
|
|
2139
|
+
surfaceOp: 'append',
|
|
2140
|
+
}));
|
|
2141
|
+
try {
|
|
2142
|
+
const created = sessions.create(undefined, { seed, meta: { ...(cwd ? { cwd } : {}), createdAt: base } });
|
|
2143
|
+
return { ok: true, sessionId: created.id, count: turns.length };
|
|
2144
|
+
}
|
|
2145
|
+
catch (e) {
|
|
2146
|
+
return { ok: false, error: '创建会话失败: ' + message(e) };
|
|
2147
|
+
}
|
|
2148
|
+
},
|
|
2149
|
+
// 把选中的归档会话导出为可再导入的转录文件(Markdown / JSONL),写入指定目录。
|
|
2150
|
+
// 每个会话一个文件;读不到正文的冷会话列入 skipped,不中断其余导出。
|
|
2151
|
+
'history-export': async (args) => {
|
|
2152
|
+
const rawIds = (args && args.sessionIds) || [];
|
|
2153
|
+
const ids = Array.isArray(rawIds) ? rawIds.map((s) => String(s).trim()).filter(Boolean) : [];
|
|
2154
|
+
if (!ids.length)
|
|
2155
|
+
return { ok: false, error: '请至少选择一个会话' };
|
|
2156
|
+
const format = String((args && args.format) || 'markdown').toLowerCase();
|
|
2157
|
+
if (format !== 'markdown' && format !== 'jsonl')
|
|
2158
|
+
return { ok: false, error: 'format 需为 markdown 或 jsonl' };
|
|
2159
|
+
const outDir = String((args && args.outDir) || '').trim();
|
|
2160
|
+
if (!/^([A-Za-z]:[\\/]|\\\\|\/)/.test(outDir))
|
|
2161
|
+
return { ok: false, error: '导出目录需为绝对路径' };
|
|
2162
|
+
const sessions = ctx.get('sessions');
|
|
2163
|
+
if (!sessions || typeof sessions.get !== 'function')
|
|
2164
|
+
return { ok: false, error: '当前环境不支持读取会话(sessions.get 不可用)' };
|
|
2165
|
+
// 读会话事件:先取活动 store;冷会话(进程重启后)从持久化后端恢复只读句柄。
|
|
2166
|
+
const readEvents = async (sessionId) => {
|
|
2167
|
+
const live = sessions.get(sessionId);
|
|
2168
|
+
if (live && typeof live.snapshotEvents === 'function') {
|
|
2169
|
+
const ev = live.snapshotEvents();
|
|
2170
|
+
return Array.isArray(ev) ? ev : undefined;
|
|
2171
|
+
}
|
|
2172
|
+
const persistence = ctx.get('sessionPersistence');
|
|
2173
|
+
if (persistence && typeof persistence.prepare === 'function') {
|
|
2174
|
+
try {
|
|
2175
|
+
const prep = await persistence.prepare(sessionId);
|
|
2176
|
+
const ps = prep && prep.session;
|
|
2177
|
+
try {
|
|
2178
|
+
if (ps && typeof ps.snapshotEvents === 'function') {
|
|
2179
|
+
const ev = ps.snapshotEvents();
|
|
2180
|
+
return Array.isArray(ev) ? ev : undefined;
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
finally {
|
|
2184
|
+
const dispose = prep[Symbol.dispose];
|
|
2185
|
+
if (typeof dispose === 'function')
|
|
2186
|
+
dispose();
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
catch {
|
|
2190
|
+
return undefined;
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
return undefined;
|
|
2194
|
+
};
|
|
2195
|
+
try {
|
|
2196
|
+
await mkdir(outDir, { recursive: true });
|
|
2197
|
+
}
|
|
2198
|
+
catch (e) {
|
|
2199
|
+
return { ok: false, error: '创建导出目录失败: ' + message(e) };
|
|
2200
|
+
}
|
|
2201
|
+
const ext = format === 'jsonl' ? '.jsonl' : '.md';
|
|
2202
|
+
const exported = [];
|
|
2203
|
+
const skipped = [];
|
|
2204
|
+
for (const sessionId of ids) {
|
|
2205
|
+
const safe = String(sessionId).replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
2206
|
+
const fileName = safe + ext;
|
|
2207
|
+
const path = join(outDir, fileName);
|
|
2208
|
+
try {
|
|
2209
|
+
const events = await readEvents(sessionId);
|
|
2210
|
+
const turns = extractTurnsFromEvents(events || []);
|
|
2211
|
+
if (!turns.length) {
|
|
2212
|
+
skipped.push({ sessionId, error: events ? '会话中没有可导出的消息' : '无法读取会话内容(不在活动存储且无持久化句柄)' });
|
|
2213
|
+
continue;
|
|
2214
|
+
}
|
|
2215
|
+
await writeFile(path, serializeTurns(turns, format), 'utf8');
|
|
2216
|
+
exported.push({ sessionId, fileName, path, count: turns.length });
|
|
2217
|
+
}
|
|
2218
|
+
catch (e) {
|
|
2219
|
+
skipped.push({ sessionId, error: '导出失败: ' + message(e) });
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
return { ok: true, exported, skipped };
|
|
2223
|
+
},
|
|
2224
|
+
'history-retention-get': async () => {
|
|
2225
|
+
const { retentionDays } = await readHistoryRetention();
|
|
2226
|
+
return { ok: true, retentionDays };
|
|
2227
|
+
},
|
|
2228
|
+
'history-retention-set': async (args) => {
|
|
2229
|
+
const days = Number((args && args.retentionDays) ?? -1);
|
|
2230
|
+
if (!Number.isFinite(days) || days < 0)
|
|
2231
|
+
return { ok: false, error: 'retentionDays 需为非负整数(0=永久不删除)' };
|
|
2232
|
+
await writeHistoryRetention(Math.floor(days));
|
|
2233
|
+
// 设置变更后立即扫一次,UI 反映新策略。
|
|
2234
|
+
await sweepHistory().catch(() => { });
|
|
2235
|
+
return { ok: true, retentionDays: Math.floor(days) };
|
|
2236
|
+
},
|
|
1591
2237
|
};
|
|
1592
2238
|
// ---------- agent-facing tools (standard ctx.tools.register + defineTool) ----------
|
|
1593
2239
|
const text = (value) => [{ type: 'text', text: value }];
|
|
@@ -1713,12 +2359,101 @@ export default {
|
|
|
1713
2359
|
return 'Created DSH skill ' + (data.name || args.name) + ' at ' + (data.path || '(unknown)');
|
|
1714
2360
|
},
|
|
1715
2361
|
}));
|
|
2362
|
+
// AGENTS.md 预设库:模型可查/切,不能造/删(避免模型乱删用户预设)。
|
|
2363
|
+
tools.register(defineTool({
|
|
2364
|
+
name: 'agentsmd_list',
|
|
2365
|
+
description: 'List AGENTS.md presets in the plugin preset library (id, active state).',
|
|
2366
|
+
parameters: {},
|
|
2367
|
+
output: { schema: { type: 'string' }, render: (_a, v) => text(v) },
|
|
2368
|
+
async execute() {
|
|
2369
|
+
const r = await agentsMdService.list();
|
|
2370
|
+
if (!r.ok)
|
|
2371
|
+
throw new Error(r.error);
|
|
2372
|
+
const summary = r.presets.map((p) => p.id + (p.active ? ' [active]' : ''));
|
|
2373
|
+
return 'AGENTS.md presets:\n' + (summary.join('\n') || '(none)') + '\n(Applying takes effect on the next session created; the current session is unchanged.)';
|
|
2374
|
+
},
|
|
2375
|
+
}));
|
|
2376
|
+
tools.register(defineTool({
|
|
2377
|
+
name: 'agentsmd_apply',
|
|
2378
|
+
description: 'Apply one AGENTS.md preset by writing it to ~/.dsh/AGENTS.md. Takes effect on the next session created; the current session is unchanged.',
|
|
2379
|
+
parameters: {
|
|
2380
|
+
id: { type: 'string', required: true, description: 'Preset id (lowercase letters, digits, hyphens).' },
|
|
2381
|
+
},
|
|
2382
|
+
output: { schema: { type: 'string' }, render: (_a, v) => text(v) },
|
|
2383
|
+
async execute(args) {
|
|
2384
|
+
const r = await agentsMdService.apply(args.id);
|
|
2385
|
+
if (!r.ok)
|
|
2386
|
+
throw new Error(r.error);
|
|
2387
|
+
return 'OK: preset ' + args.id + ' applied to ~/.dsh/AGENTS.md (next session; current session unchanged' + (r.backedUp ? '; previous backed up to __last-applied__' : '') + ')';
|
|
2388
|
+
},
|
|
2389
|
+
}));
|
|
2390
|
+
// ---------- rules model tools(v0.3)----------
|
|
2391
|
+
// 活动场景的记忆正文会自动进入系统提示词(无需调用工具读取);这里的工具用于
|
|
2392
|
+
// 查询/编辑规则本身。rule_manager_write 受 tools/pre-execute 审批门禁(D2)。
|
|
2393
|
+
tools.register(defineTool({
|
|
2394
|
+
name: 'rule_manager_list',
|
|
2395
|
+
description: 'List rules/memories under ~/.dsh/scene-memory (id, scene, enabled, description).',
|
|
2396
|
+
parameters: {
|
|
2397
|
+
group: { type: 'string', description: 'Optional group/scene filter.' },
|
|
2398
|
+
},
|
|
2399
|
+
output: { schema: { type: 'string' }, render: (_a, v) => text(v) },
|
|
2400
|
+
async execute(args) {
|
|
2401
|
+
const r = await rulesService.ops['rules-list'](args);
|
|
2402
|
+
if (!r || r.ok === false)
|
|
2403
|
+
throw new Error((r && r.error) || '读取规则失败');
|
|
2404
|
+
const lines = (r.rules || []).map((x) => ('- ' + x.id + ' [' + (x.group || '全局') + '] ' + (x.enabled ? '已启用' : '已停用') +
|
|
2405
|
+
(x.description ? ' — ' + x.description : '')));
|
|
2406
|
+
const scenes = (r.scenes || []).map((s) => s.name + (s.active ? '(启用)' : '(未启用)')).join('、');
|
|
2407
|
+
return '规则(' + (r.rules || []).length + '):\n' + (lines.join('\n') || '(无规则)') +
|
|
2408
|
+
'\n场景:' + (scenes || '(无)') + (r.activeMode === 'all' ? '(默认全部启用)' : '(已收窄)');
|
|
2409
|
+
},
|
|
2410
|
+
}));
|
|
2411
|
+
tools.register(defineTool({
|
|
2412
|
+
name: 'rule_manager_read',
|
|
2413
|
+
description: 'Read the full body of one rule/memory under ~/.dsh/scene-memory.',
|
|
2414
|
+
parameters: {
|
|
2415
|
+
id: { type: 'string', required: true, description: 'Rule id like <scene>/<name>.' },
|
|
2416
|
+
},
|
|
2417
|
+
output: { schema: { type: 'string' }, render: (_a, v) => text(v) },
|
|
2418
|
+
async execute(args) {
|
|
2419
|
+
const r = await rulesService.ops['rules-read'](args);
|
|
2420
|
+
if (!r || r.ok === false)
|
|
2421
|
+
throw new Error((r && r.error) || '读取规则失败');
|
|
2422
|
+
return '# ' + r.rule.id + '\n\n' + (r.rule.body || '');
|
|
2423
|
+
},
|
|
2424
|
+
}));
|
|
2425
|
+
tools.register(defineTool({
|
|
2426
|
+
name: 'rule_manager_write',
|
|
2427
|
+
description: 'Create a new rule/memory as ~/.dsh/scene-memory/<scene>/<name>.md. It becomes active automatically once its scene is enabled. Requires user confirmation (configurable).',
|
|
2428
|
+
parameters: {
|
|
2429
|
+
group: { type: 'string', required: true, description: 'Scene/folder name under ~/.dsh/scene-memory (any Unicode except path separators and < > : " | ? *).' },
|
|
2430
|
+
name: { type: 'string', required: true, description: 'Memory name = the .md file name without the extension; any Unicode is fine (Chinese included), <=64 chars, no path separators or < > : " | ? *, must not start with a dot.' },
|
|
2431
|
+
description: { type: 'string', required: true, description: 'One-sentence description (<=500 chars).' },
|
|
2432
|
+
body: { type: 'string', required: true, description: 'Markdown body (<=256 KiB).' },
|
|
2433
|
+
},
|
|
2434
|
+
output: { schema: { type: 'string' }, render: (_a, v) => text(v) },
|
|
2435
|
+
async execute(args) {
|
|
2436
|
+
const r = await rulesService.ops['rules-create'](args);
|
|
2437
|
+
if (!r || r.ok === false)
|
|
2438
|
+
throw new Error((r && r.error) || '创建规则失败');
|
|
2439
|
+
return 'OK: rule ' + r.rule.id + '(场景「' + (r.rule.group || '全局') + '」启用后自动生效)';
|
|
2440
|
+
},
|
|
2441
|
+
}));
|
|
1716
2442
|
if (typeof ctx.on === 'function') {
|
|
1717
2443
|
;
|
|
1718
2444
|
ctx.on('tools/pre-execute', (exec, next) => {
|
|
1719
2445
|
if (exec && exec.name === 'skill_manager_create') {
|
|
1720
2446
|
return Promise.resolve({ kind: 'ask', reason: 'Create a new skill under DSH_HOME/skills' });
|
|
1721
2447
|
}
|
|
2448
|
+
if (exec && exec.name === 'rule_manager_write') {
|
|
2449
|
+
// D2:模型写规则默认需确认;设置关闭后直接放行。ask 无应答者时降级为拒绝(fail-closed),
|
|
2450
|
+
// 不在此处做任何兜底放行。
|
|
2451
|
+
return readPluginSettings()
|
|
2452
|
+
.then((s) => (s.requireConfirmForModelRuleWrite
|
|
2453
|
+
? { kind: 'ask', reason: 'Write a rule under ~/.dsh/scene-memory' }
|
|
2454
|
+
: next()))
|
|
2455
|
+
.catch(() => ({ kind: 'ask', reason: 'Write a rule under ~/.dsh/scene-memory' }));
|
|
2456
|
+
}
|
|
1722
2457
|
return next();
|
|
1723
2458
|
});
|
|
1724
2459
|
}
|
|
@@ -1923,6 +2658,37 @@ export default {
|
|
|
1923
2658
|
return { kind: 'success', text: lines.length ? '技能(' + lines.length + '):\n' + lines.join('\n') : '未发现任何技能。' };
|
|
1924
2659
|
},
|
|
1925
2660
|
}), 'dsh-plugin-tool-management: /skills command');
|
|
2661
|
+
ctx.effect(() => commands.register({
|
|
2662
|
+
name: 'agents-md',
|
|
2663
|
+
description: '列出 AGENTS.md 预设及当前生效(应用后新会话生效,当前会话不变)',
|
|
2664
|
+
handler: async () => {
|
|
2665
|
+
const r = await agentsMdService.list();
|
|
2666
|
+
if (!r || r.ok === false)
|
|
2667
|
+
return { kind: 'error', text: (r && r.error) || '读取 AGENTS.md 预设失败' };
|
|
2668
|
+
const presets = r.presets || [];
|
|
2669
|
+
if (!presets.length)
|
|
2670
|
+
return { kind: 'success', text: '未发现任何 AGENTS.md 预设。' };
|
|
2671
|
+
const text = 'AGENTS.md 预设(' + presets.length + '):\n' + presets.map((p) => ('- ' + p.id + (p.active ? ' [生效中]' : ''))).join('\n') + '\n(应用后新会话生效,当前会话不变)';
|
|
2672
|
+
return { kind: 'success', text };
|
|
2673
|
+
},
|
|
2674
|
+
}), 'dsh-plugin-tool-management: /agents-md command');
|
|
2675
|
+
ctx.effect(() => commands.register({
|
|
2676
|
+
name: 'scene-memory',
|
|
2677
|
+
description: '列出场景记忆及其场景启用状态',
|
|
2678
|
+
handler: async () => {
|
|
2679
|
+
const r = await rulesService.ops['rules-list']({});
|
|
2680
|
+
if (!r || r.ok === false)
|
|
2681
|
+
return { kind: 'error', text: (r && r.error) || '读取记忆失败' };
|
|
2682
|
+
const rules = r.rules || [];
|
|
2683
|
+
if (!rules.length)
|
|
2684
|
+
return { kind: 'success', text: '未发现任何记忆(~/.dsh/scene-memory 为空)。' };
|
|
2685
|
+
const scenes = r.scenes || [];
|
|
2686
|
+
const text = '场景记忆(' + rules.length + '):\n' + rules.map((rule) => ('- ' + rule.name + ' [' + (rule.group || '全局') + '] · ' + (rule.enabled ? '已启用' : '已停用'))).join('\n') + '\n场景:' + (scenes.map((s) => s.name + (s.active ? '(启用)' : '(未启用)')).join('、') || '(无)') +
|
|
2687
|
+
(r.activeMode === 'all' ? '(默认全部启用)' : '(已收窄)') +
|
|
2688
|
+
'\n(启用场景的记忆正文自动进入系统提示词,无需任何工具调用)';
|
|
2689
|
+
return { kind: 'success', text };
|
|
2690
|
+
},
|
|
2691
|
+
}), 'dsh-plugin-tool-management: /scene-memory command');
|
|
1926
2692
|
}
|
|
1927
2693
|
}
|
|
1928
2694
|
catch (e) {
|