dsh-plugin-tool-management 0.14.0 → 0.15.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +560 -519
  2. package/docs/images/1-EN.png +0 -0
  3. package/docs/images/1.png +0 -0
  4. package/docs/images/2-EN.png +0 -0
  5. package/docs/images/2.png +0 -0
  6. package/docs/images/3-EN.png +0 -0
  7. package/docs/images/3.png +0 -0
  8. package/docs/images/4-EN.png +0 -0
  9. package/docs/images/4.png +0 -0
  10. package/docs/images/5-EN.png +0 -0
  11. package/docs/images/5.png +0 -0
  12. package/docs/images/6-EN.png +0 -0
  13. package/docs/images/6.png +0 -0
  14. package/docs/images/7-EN.png +0 -0
  15. package/docs/images/7.png +0 -0
  16. package/docs/images/8-EN.png +0 -0
  17. package/docs/images/8.png +0 -0
  18. package/docs/update.md +391 -365
  19. package/lib/audit-log.js +157 -0
  20. package/lib/client.js +3800 -2504
  21. package/lib/compat/patch-dialect.js +3 -1
  22. package/lib/compat/preset-reach.js +37 -6
  23. package/lib/compat/probe.js +149 -7
  24. package/lib/context-inject.js +5 -3
  25. package/lib/index.js +236 -30
  26. package/lib/mcp/loader-token.js +53 -28
  27. package/lib/mcp/manager.js +146 -7
  28. package/lib/memories/archive-engine.js +70 -2
  29. package/lib/memories/archive.js +8 -1
  30. package/lib/memories/index-io.js +6 -0
  31. package/lib/op-registry.js +18 -4
  32. package/lib/ops/compat.js +109 -35
  33. package/lib/ops/scene-records.js +34 -0
  34. package/lib/ops/scene-sync.js +12 -1
  35. package/lib/ops/sessions.js +23 -8
  36. package/lib/ops/snapshot.js +596 -0
  37. package/lib/scenes/candidates.js +10 -5
  38. package/lib/sessions/bridge.js +5 -0
  39. package/lib/sessions/workspace.js +17 -5
  40. package/lib/skills/core.js +12 -3
  41. package/lib/tools/deps.js +1 -1
  42. package/lib/tools/table.js +5 -1
  43. package/package.json +112 -108
@@ -0,0 +1,157 @@
1
+ // 「最近改动」流水(0.15.0 B2)—— 写类 op 成功之后追加一条 JSONL,兼容页倒序显示。
2
+ //
3
+ // 为什么要有它:启用态被三方动过 —— 面板点击、模型的 `_switch` 工具、场景引擎 ——
4
+ // 事后回答不了「这个开关是谁、什么时候关的」。所有写动作的收口点都在 op 层,包一层就能
5
+ // 同时拿到「谁」(调用方)与「改了什么」(对象名)。
6
+ //
7
+ // 三条硬规矩(改这个文件前先读):
8
+ // ① **只记对象名,绝不记参数全文** —— env / headers 里可能是凭据,记忆与档案是用户内容,
9
+ // 写进一份常驻磁盘的流水等于把它们再复制一份到没人看守的地方。
10
+ // ② 写失败一律静默 —— 先例见 index.ts 的 confirm-bypass 留痕(`.catch(() => {})`):
11
+ // 流水是附属能力,任何情况下都不能挡着真正的写操作,也不能把界面点成红的。
12
+ // ③ readonly 与失败/抛错的调用不记 —— 记的是「变更成功」,不是「有人试过」。
13
+ import { appendFile, readFile, stat, unlink, writeFile } from 'node:fs/promises';
14
+ import { hubPath } from './hub.js';
15
+ import { OP_REGISTRY } from './op-registry.js';
16
+ const AUDIT_FILE = 'audit.jsonl';
17
+ /** 轮转阈值:超过 512KB 保留最近 500 条。这页的本意是「最近改动」,不是无限期审计。 */
18
+ const ROTATE_OVER_BYTES = 512 * 1024;
19
+ const ROTATE_KEEP_LINES = 500;
20
+ /** 读上限:界面上限 500 条,再多没有意义(列表本身也只渲染最近这些)。 */
21
+ const READ_MAX_LIMIT = 500;
22
+ /**
23
+ * 门禁按写、实质是「往外拷 / 只读枚举」的 op 不记:
24
+ * 它们不改任何状态,记下来只会把真变更淹没(导出/查表点了多少次不是"最近改动")。
25
+ */
26
+ const AUDIT_SKIP = new Set(['mcpm-reveal', 'mcpm-export', 'bundle-export', 'preset-tools', 'skill-open']);
27
+ /** 摘要拿不到具体对象时的占位(界面按 `audit.target.*` 翻成「配置」,词典不跟着数据走)。 */
28
+ export const AUDIT_TARGET_CONFIG = '@config';
29
+ /** 这条 op 属不属于「写类」:判据从登记表来,不再手抄一份名单(登记改了这里自动跟上)。 */
30
+ export function isWriteClassOp(op) {
31
+ const cls = OP_REGISTRY[op];
32
+ if (!cls || cls.readonly === true)
33
+ return false;
34
+ return cls.write === true || cls.serviceWrite === true || cls.frozen === true;
35
+ }
36
+ const str = (v) => String(v == null ? '' : v).trim();
37
+ /**
38
+ * op + args → 对象名摘要。按 op 前缀取那一域「名字字段」的第一个非空值,取不到就归「配置」。
39
+ * 参数全文一律不碰(见文件头第 ① 条)。
40
+ */
41
+ export function auditTargetOf(op, args) {
42
+ const a = (args && typeof args === 'object' ? args : {});
43
+ if (op.startsWith('mcpm-'))
44
+ return str(a.serverName) || str(a.id) || AUDIT_TARGET_CONFIG;
45
+ if (op.startsWith('skill-'))
46
+ return str(a.name) || str(a.key) || str(a.skill) || str(a.root) || AUDIT_TARGET_CONFIG;
47
+ if (op.startsWith('subagent-'))
48
+ return str(a.name) || str(a.agent) || AUDIT_TARGET_CONFIG;
49
+ if (op.startsWith('agentsmd-'))
50
+ return str(a.id) || str(a.nextId) || AUDIT_TARGET_CONFIG;
51
+ if (op.startsWith('scene-'))
52
+ return str(a.scene) || AUDIT_TARGET_CONFIG;
53
+ // rules-* 既管记忆也管场景:先按记忆 id,再按场景名(`rules-set-active` 传的是 scene)。
54
+ if (op.startsWith('rules-'))
55
+ return str(a.id) || str(a.scene) || AUDIT_TARGET_CONFIG;
56
+ if (op.startsWith('history-'))
57
+ return str(a.sessionId) || str(a.id) || AUDIT_TARGET_CONFIG;
58
+ return AUDIT_TARGET_CONFIG;
59
+ }
60
+ /** 一条流水(同步发起、异步落盘,失败静默)。 */
61
+ export function recordAudit(entry) {
62
+ appendAudit(entry).catch(() => { });
63
+ }
64
+ /**
65
+ * 一次 op 结果要不要记:写类 + 本次成功(`ok !== false`,与服务端「成功返回扁平 {ok:true,…}」
66
+ * 的口径一致;不返回 ok 的按成功处理)。
67
+ */
68
+ export function recordOpAudit(op, args, source, result) {
69
+ if (!isWriteClassOp(op) || AUDIT_SKIP.has(op))
70
+ return;
71
+ if (result && typeof result === 'object' && result.ok === false)
72
+ return;
73
+ recordAudit({ ts: Date.now(), op, target: auditTargetOf(op, args), source });
74
+ }
75
+ async function appendAudit(entry) {
76
+ const abs = hubPath(AUDIT_FILE);
77
+ let size = 0;
78
+ try {
79
+ size = (await stat(abs)).size;
80
+ }
81
+ catch { /* 还没有这个文件 */ }
82
+ if (size > ROTATE_OVER_BYTES) {
83
+ const kept = await tailLines(abs, ROTATE_KEEP_LINES);
84
+ await writeFile(abs, kept.length ? kept.join('\n') + '\n' : '', 'utf8');
85
+ }
86
+ await appendFile(abs, JSON.stringify(entry) + '\n', 'utf8');
87
+ }
88
+ /** 读末尾 n 行(文件不存在 = 空)。整份读入:这份流水被轮转压在 512KB 以内,不会失控。 */
89
+ async function tailLines(abs, n) {
90
+ let raw = '';
91
+ try {
92
+ raw = await readFile(abs, 'utf8');
93
+ }
94
+ catch {
95
+ return [];
96
+ }
97
+ const lines = raw.split('\n').filter((l) => l.trim());
98
+ return lines.slice(Math.max(0, lines.length - n));
99
+ }
100
+ /**
101
+ * 清空流水(界面上的「清除记录」)。**只删文件、自己不补记** —— 调用它的 op 是写类,
102
+ * 成功后由通用包装层记一条 `audit-clear`,于是"谁清过流水"本身也留在流水里,
103
+ * 而不是留下一段无声的空白。
104
+ */
105
+ export async function clearAudit() {
106
+ const abs = hubPath(AUDIT_FILE);
107
+ let lines = 0;
108
+ try {
109
+ lines = (await readFile(abs, 'utf8')).split('\n').filter((l) => l.trim()).length;
110
+ }
111
+ catch {
112
+ lines = 0;
113
+ }
114
+ try {
115
+ await unlink(abs);
116
+ return { ok: true, removed: lines };
117
+ }
118
+ catch (e) {
119
+ const err = e;
120
+ if (err && err.code === 'ENOENT')
121
+ return { ok: true, removed: 0 }; // 本来就没有 = 已经是空的
122
+ return { ok: false, error: String((err && err.message) || e) };
123
+ }
124
+ }
125
+ /**
126
+ * 读流水(倒序)。`domain` 是 op 前缀(`mcpm` / `skill` / `rules` …),省略 = 全部。
127
+ * 坏行直接跳过:手改过的流水文件不该让兼容页报错。
128
+ */
129
+ export async function readAudit(args) {
130
+ const wanted = Number(args && args.limit);
131
+ const limit = Math.min(READ_MAX_LIMIT, Math.max(1, Number.isFinite(wanted) && wanted > 0 ? wanted : 100));
132
+ const domain = str(args && args.domain);
133
+ const lines = await tailLines(hubPath(AUDIT_FILE), Math.max(limit * 4, 600));
134
+ const out = [];
135
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
136
+ let e;
137
+ try {
138
+ e = JSON.parse(lines[i]);
139
+ }
140
+ catch {
141
+ continue;
142
+ }
143
+ if (!e || typeof e.op !== 'string' || typeof e.ts !== 'number')
144
+ continue;
145
+ if (domain && e.op.indexOf(domain + '-') !== 0 && e.op !== domain)
146
+ continue;
147
+ out.push({
148
+ ts: e.ts,
149
+ op: e.op,
150
+ target: str(e.target),
151
+ source: e.source === 'model' || e.source === 'engine' ? e.source : 'panel',
152
+ });
153
+ if (out.length >= limit)
154
+ break;
155
+ }
156
+ return out;
157
+ }