dsh-plugin-yanyan-toolkit 0.0.0-stage → 1.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/CHANGELOG.md +30 -0
- package/LICENSE +29 -0
- package/README.i18n.yaml +6 -0
- package/README.md +65 -2
- package/README.zh.md +66 -0
- package/cordis.patch.yml +35 -0
- package/icon.svg +8 -0
- package/lib/client.js +26 -0
- package/lib/index.js +44 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +79 -4
- package/packages/dsh-plugin-yanyan-subagent/README.md +72 -0
- package/packages/dsh-plugin-yanyan-subagent/README.zh.md +72 -0
- package/packages/dsh-plugin-yanyan-subagent/cordis.patch.yml +8 -0
- package/packages/dsh-plugin-yanyan-subagent/icon.svg +8 -0
- package/packages/dsh-plugin-yanyan-subagent/lib/client.js +1250 -0
- package/packages/dsh-plugin-yanyan-subagent/lib/index.js +951 -0
- package/packages/dsh-plugin-yanyan-subagent/lib/legacy-hidden-ids.json +9 -0
- package/packages/dsh-plugin-yanyan-subagent/lib/log-rewrite.js +382 -0
- package/packages/dsh-plugin-yanyan-subagent/lib/zstd-frames.js +81 -0
- package/packages/dsh-plugin-yanyan-subagent/locale/en.json +1 -0
- package/packages/dsh-plugin-yanyan-subagent/locale/zh.json +1 -0
- package/packages/dsh-plugin-yanyan-subagent/package.json +32 -0
- package/packages/dsh-plugin-yanyan-update/README.md +24 -0
- package/packages/dsh-plugin-yanyan-update/README.zh.md +24 -0
- package/packages/dsh-plugin-yanyan-update/cordis.patch.yml +7 -0
- package/packages/dsh-plugin-yanyan-update/icon.svg +5 -0
- package/packages/dsh-plugin-yanyan-update/lib/client.js +400 -0
- package/packages/dsh-plugin-yanyan-update/lib/index.js +840 -0
- package/packages/dsh-plugin-yanyan-update/locale/en.json +1 -0
- package/packages/dsh-plugin-yanyan-update/locale/zh.json +1 -0
- package/packages/dsh-plugin-yanyan-update/package.json +32 -0
- package/scripts/cleanup-ghost-subagents.mjs +241 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "一次性迁移:旧版「删除=归档」遗留、已由 scripts/cleanup-ghost-subagents.mjs 从磁盘清除的 mnemon 虚拟委派 id。subagentCatalog 由 append-only 会话日志重建,清缓存后宿主仍可能把它们带回来;host /config 把本清单下发给 client,client 在启动时写入 yanyan-subagent.deletedIds 持久化隐藏清单,保证这些条目不再显示。id 均为去 session- 前缀后的核心 id。前 3 条来自「已归档」残留,最后 1 条是用户报错里的 session/not-found 条目。",
|
|
3
|
+
"ids": [
|
|
4
|
+
"mnemon-run-v1-47f5a5fc-1659-46da-9b66-8dbdbed10a0f",
|
|
5
|
+
"mnemon-run-v1-b59053b1-57bb-4a23-bbd8-972e210c1cb0",
|
|
6
|
+
"mnemon-run-v1-8a02e7a3-b67f-44ec-8277-ef733fc6c252",
|
|
7
|
+
"mnemon-run-v1-debd95cd-2980-4801-9299-c1da04bc5775"
|
|
8
|
+
]
|
|
9
|
+
}
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
// dsh-plugin-yanyan-subagent / lib/log-rewrite.js
|
|
2
|
+
//
|
|
3
|
+
// 根治 append-only `subagentCatalog` 残留:把父会话日志里的
|
|
4
|
+
// `subagent/catalog` 事件「原位退休」成 `subagent-delete/retired`。
|
|
5
|
+
//
|
|
6
|
+
// 移植自 huajiao1998/dsh-plugin-subagent-clean@0.3.0 的 rewriteCatalogRows /
|
|
7
|
+
// retireCatalogRow / pending 队列 / scrubCatalogRows,原始实现来自
|
|
8
|
+
// heiheiha798/dsh-plugin-subagent-delete@0.2.3(MIT)。
|
|
9
|
+
// 版权所有 (c) heiheiha798;派生改动同样以 MIT 许可发布。
|
|
10
|
+
//
|
|
11
|
+
// 为什么必须「原位替换」而不是删行 / 重编号:
|
|
12
|
+
// ① 会话日志的 `seq` 必须从 0 连续,删行会留洞;
|
|
13
|
+
// ② `compaction/summary` 压缩事件会引用更早的 seq 区间,重编号会错位。
|
|
14
|
+
// 把命中行改成「未知类型 + ignorable:true」,seq/time 原样保留,其余事件
|
|
15
|
+
// 逐字节不动;目录投影只在 `type === "subagent/catalog"` 时生成行,于是
|
|
16
|
+
// 该行从父目录 / 侧边栏彻底消失,而日志的连续性与压缩引用都不受影响。
|
|
17
|
+
// `ignorable` 的放行规则来自 dsh-session-format-v3-to-v4(未知类型 +
|
|
18
|
+
// ignorable 才被保留)。
|
|
19
|
+
//
|
|
20
|
+
// 本模块全部为纯文件操作,`dshHome` 显式传参,不读 process.env,方便单测。
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
existsSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync,
|
|
24
|
+
renameSync, mkdirSync,
|
|
25
|
+
} from 'node:fs';
|
|
26
|
+
import { join, dirname } from 'node:path';
|
|
27
|
+
import { decodeLog, encodeLog } from './zstd-frames.js';
|
|
28
|
+
|
|
29
|
+
/** 与 index.js 的 coreId 保持一致:去 `session-` 前缀后的核心 id。 */
|
|
30
|
+
export function logCoreId(value) {
|
|
31
|
+
return String(value || '').replace(/^session-/, '');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 会话 id 的两种拼写(裸 uuid 与 `session-` 前缀)。 */
|
|
35
|
+
export function logVariants(sessionId) {
|
|
36
|
+
const c = logCoreId(sessionId);
|
|
37
|
+
return [c, 'session-' + c];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 扫 `<dshHome>/sessions/<slug>/<variant>` 找会话目录。 */
|
|
41
|
+
export function findSessionDirs(dshHome, sessionId) {
|
|
42
|
+
const root = join(dshHome, 'sessions');
|
|
43
|
+
let slugs = [];
|
|
44
|
+
try {
|
|
45
|
+
slugs = readdirSync(root, { withFileTypes: true })
|
|
46
|
+
.filter((e) => e.isDirectory())
|
|
47
|
+
.map((e) => e.name);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
const variants = logVariants(sessionId);
|
|
52
|
+
const found = [];
|
|
53
|
+
for (const slug of slugs) {
|
|
54
|
+
for (const variant of variants) {
|
|
55
|
+
const candidate = join(root, slug, variant);
|
|
56
|
+
try {
|
|
57
|
+
if (statSync(candidate).isDirectory()) found.push(candidate);
|
|
58
|
+
} catch (error) { /* 继续扫 */ }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return found;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 列出 sessions 根下所有 `session.v4.jsonl.zstd` 的路径。 */
|
|
65
|
+
export function allSessionLogPaths(dshHome) {
|
|
66
|
+
const root = join(dshHome, 'sessions');
|
|
67
|
+
let slugs = [];
|
|
68
|
+
try {
|
|
69
|
+
slugs = readdirSync(root, { withFileTypes: true })
|
|
70
|
+
.filter((e) => e.isDirectory())
|
|
71
|
+
.map((e) => e.name);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
const out = [];
|
|
76
|
+
for (const slug of slugs) {
|
|
77
|
+
let segs = [];
|
|
78
|
+
try { segs = readdirSync(join(root, slug)); } catch (error) { continue; }
|
|
79
|
+
for (const seg of segs) {
|
|
80
|
+
const p = join(root, slug, seg, 'session.v4.jsonl.zstd');
|
|
81
|
+
if (existsSync(p)) out.push({ logPath: p, parentId: seg, dir: join(root, slug, seg) });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 一遍扫全 sessions 根,收集所有已落盘的会话目录名。 */
|
|
88
|
+
export function storedSessionDirNames(dshHome) {
|
|
89
|
+
const ids = new Set();
|
|
90
|
+
for (const entry of allSessionLogPaths(dshHome)) ids.add(entry.parentId);
|
|
91
|
+
// 目录存在但没有日志的会话也算「还在」。
|
|
92
|
+
const root = join(dshHome, 'sessions');
|
|
93
|
+
let slugs = [];
|
|
94
|
+
try {
|
|
95
|
+
slugs = readdirSync(root, { withFileTypes: true })
|
|
96
|
+
.filter((e) => e.isDirectory())
|
|
97
|
+
.map((e) => e.name);
|
|
98
|
+
} catch (error) { return ids; }
|
|
99
|
+
for (const slug of slugs) {
|
|
100
|
+
try {
|
|
101
|
+
for (const e of readdirSync(join(root, slug), { withFileTypes: true })) {
|
|
102
|
+
if (e.isDirectory()) ids.add(e.name);
|
|
103
|
+
}
|
|
104
|
+
} catch (error) { /* 继续扫 */ }
|
|
105
|
+
}
|
|
106
|
+
return ids;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** 删掉某会话的投影缓存文件,让宿主下次重新按(已改写的)日志 fold。 */
|
|
110
|
+
export function dropProjectionCache(dshHome, sessionId) {
|
|
111
|
+
const root = join(dshHome, 'storages', 'session_projcache', 'sessions');
|
|
112
|
+
let n = 0;
|
|
113
|
+
for (const variant of logVariants(sessionId)) {
|
|
114
|
+
try {
|
|
115
|
+
const p = join(root, variant + '.json');
|
|
116
|
+
if (existsSync(p)) { rmSync(p, { force: true }); n += 1; }
|
|
117
|
+
} catch (error) { /* 尽力而为 */ }
|
|
118
|
+
}
|
|
119
|
+
return n;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 从父会话日志读出「仍会生成目录行」的 catalog 事件。
|
|
124
|
+
* 已退休的 `subagent-delete/retired` 不再计入 —— 返回的正是宿主重建投影后
|
|
125
|
+
* 还会显示、也就是用户想清理的那些行。日志是真相源,比会被重建的 projcache
|
|
126
|
+
* 缓存可靠。
|
|
127
|
+
*/
|
|
128
|
+
export function readCatalogEvents(dshHome, parentSessionId) {
|
|
129
|
+
const out = [];
|
|
130
|
+
for (const dir of findSessionDirs(dshHome, parentSessionId)) {
|
|
131
|
+
const logPath = join(dir, 'session.v4.jsonl.zstd');
|
|
132
|
+
if (!existsSync(logPath)) continue;
|
|
133
|
+
let text;
|
|
134
|
+
try { text = decodeLog(readFileSync(logPath)); } catch (error) { continue; }
|
|
135
|
+
const lines = text.split('\n').filter((line) => line.length > 0);
|
|
136
|
+
for (const line of lines.slice(1)) {
|
|
137
|
+
let event = null;
|
|
138
|
+
try { event = JSON.parse(line); } catch (error) { continue; }
|
|
139
|
+
if (event === null || event.type !== 'subagent/catalog') continue;
|
|
140
|
+
const data = event.data;
|
|
141
|
+
if (data === null || typeof data !== 'object' || typeof data.childId !== 'string') continue;
|
|
142
|
+
out.push({
|
|
143
|
+
childId: data.childId,
|
|
144
|
+
label: String(data.label || ''),
|
|
145
|
+
name: String(data.name || ''),
|
|
146
|
+
mode: String(data.mode || ''),
|
|
147
|
+
createdAt: Number(data.childCreatedAt || 0),
|
|
148
|
+
seq: event.seq,
|
|
149
|
+
time: event.time,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
break; // 同一父会话只可能有一份日志,命中即用。
|
|
153
|
+
}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 改写一份明确指定的父会话日志:把 shouldDrop(childId) 命中的
|
|
159
|
+
* `subagent/catalog` 事件原位退休。写临时文件 → 前后各 stat 一次 → 一致才
|
|
160
|
+
* rename,绝不在并发写入时覆盖宿主刚追加的事件(丢事件会破坏 seq 连续性)。
|
|
161
|
+
*
|
|
162
|
+
* @returns {{ok:boolean, removed:number, kept?:number, reason?:string}}
|
|
163
|
+
*/
|
|
164
|
+
export function rewriteCatalogRowsInFile(logPath, shouldDrop) {
|
|
165
|
+
if (!logPath || !existsSync(logPath)) return { ok: false, removed: 0, reason: 'no-log' };
|
|
166
|
+
let before;
|
|
167
|
+
let text;
|
|
168
|
+
try {
|
|
169
|
+
before = statSync(logPath);
|
|
170
|
+
text = decodeLog(readFileSync(logPath));
|
|
171
|
+
} catch (error) {
|
|
172
|
+
return { ok: false, removed: 0, reason: 'decode-failed: ' + String((error && error.message) || error) };
|
|
173
|
+
}
|
|
174
|
+
const lines = text.split('\n').filter((line) => line.length > 0);
|
|
175
|
+
if (lines.length < 2) return { ok: false, removed: 0, reason: 'empty-log' };
|
|
176
|
+
const header = lines[0];
|
|
177
|
+
const kept = [];
|
|
178
|
+
let removed = 0;
|
|
179
|
+
for (const line of lines.slice(1)) {
|
|
180
|
+
let event = null;
|
|
181
|
+
try { event = JSON.parse(line); } catch (error) { kept.push(line); continue; }
|
|
182
|
+
if (event !== null && event.type === 'subagent/catalog') {
|
|
183
|
+
const childId = event.data && event.data.childId;
|
|
184
|
+
if (typeof childId === 'string' && shouldDrop(childId) === true) {
|
|
185
|
+
kept.push(JSON.stringify({
|
|
186
|
+
type: 'subagent-delete/retired',
|
|
187
|
+
seq: event.seq,
|
|
188
|
+
time: event.time,
|
|
189
|
+
ignorable: true,
|
|
190
|
+
data: { childId, retiredAt: Date.now() },
|
|
191
|
+
}));
|
|
192
|
+
removed += 1;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
kept.push(line);
|
|
197
|
+
}
|
|
198
|
+
if (removed === 0) return { ok: true, removed: 0 };
|
|
199
|
+
|
|
200
|
+
const encoded = encodeLog(header, kept);
|
|
201
|
+
// ① 读完后第一次 stat:若日志在解码期间被写过,放弃。
|
|
202
|
+
let after;
|
|
203
|
+
try { after = statSync(logPath); } catch (error) {
|
|
204
|
+
return { ok: false, removed: 0, reason: 'stat-failed' };
|
|
205
|
+
}
|
|
206
|
+
if (after.size !== before.size || after.mtimeMs !== before.mtimeMs) {
|
|
207
|
+
return { ok: false, removed: 0, reason: 'concurrent-write' };
|
|
208
|
+
}
|
|
209
|
+
const tmp = logPath + '.rewrite-' + String(process.pid);
|
|
210
|
+
try {
|
|
211
|
+
writeFileSync(tmp, encoded, { mode: before.mode });
|
|
212
|
+
} catch (error) {
|
|
213
|
+
try { rmSync(tmp, { force: true }); } catch (e2) { /* ignore */ }
|
|
214
|
+
return { ok: false, removed: 0, reason: 'write-failed: ' + String((error && error.message) || error) };
|
|
215
|
+
}
|
|
216
|
+
// ② rename 前再 stat 一次:第一次检查之后追加的单个事件也不能被覆盖。
|
|
217
|
+
let last;
|
|
218
|
+
try { last = statSync(logPath); } catch (error) { last = null; }
|
|
219
|
+
if (last === null || last.size !== before.size || last.mtimeMs !== before.mtimeMs) {
|
|
220
|
+
try { rmSync(tmp, { force: true }); } catch (e2) { /* ignore */ }
|
|
221
|
+
return { ok: false, removed: 0, reason: 'concurrent-write' };
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
renameSync(tmp, logPath);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
try { rmSync(tmp, { force: true }); } catch (e2) { /* ignore */ }
|
|
227
|
+
return { ok: false, removed: 0, reason: 'rename-failed: ' + String((error && error.message) || error) };
|
|
228
|
+
}
|
|
229
|
+
return { ok: true, removed, kept: kept.length };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** 按父会话 id 找到它的日志并改写。 */
|
|
233
|
+
export function rewriteCatalogRows(dshHome, parentSessionId, shouldDrop) {
|
|
234
|
+
for (const dir of findSessionDirs(dshHome, parentSessionId)) {
|
|
235
|
+
const candidate = join(dir, 'session.v4.jsonl.zstd');
|
|
236
|
+
if (existsSync(candidate)) {
|
|
237
|
+
const result = rewriteCatalogRowsInFile(candidate, shouldDrop);
|
|
238
|
+
if (result.ok === true && result.removed > 0) dropProjectionCache(dshHome, parentSessionId);
|
|
239
|
+
return result;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return { ok: false, removed: 0, reason: 'no-log' };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 扫全部会话日志,退休所有 childId 命中 coreIds 的 catalog 行。
|
|
247
|
+
* 这样既能命中 rootSessionId 那条父日志,也能命中「孙辈 / 幽灵条目散落在
|
|
248
|
+
* 其它历史会话」的情况。返回每个被改写文件的明细。
|
|
249
|
+
*/
|
|
250
|
+
export function retireCatalogRowsEverywhere(dshHome, coreIds, parentHint) {
|
|
251
|
+
const set = new Set((Array.isArray(coreIds) ? coreIds : [coreIds]).map(logCoreId));
|
|
252
|
+
const shouldDrop = (id) => set.has(logCoreId(id));
|
|
253
|
+
const out = { files: [], removed: 0, failed: [], scanned: 0 };
|
|
254
|
+
let entries = allSessionLogPaths(dshHome);
|
|
255
|
+
// 优先处理父会话日志(用户当下看的那个),其余按路径顺序。
|
|
256
|
+
if (parentHint) {
|
|
257
|
+
const hintCore = logCoreId(parentHint);
|
|
258
|
+
entries = entries.slice().sort((a, b) => {
|
|
259
|
+
const ah = logCoreId(a.parentId) === hintCore ? 0 : 1;
|
|
260
|
+
const bh = logCoreId(b.parentId) === hintCore ? 0 : 1;
|
|
261
|
+
return ah - bh;
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
for (const entry of entries) {
|
|
265
|
+
out.scanned += 1;
|
|
266
|
+
let result;
|
|
267
|
+
try { result = rewriteCatalogRowsInFile(entry.logPath, shouldDrop); }
|
|
268
|
+
catch (error) { result = { ok: false, reason: String((error && error.message) || error) }; }
|
|
269
|
+
if (result.ok === true && result.removed > 0) {
|
|
270
|
+
out.files.push({ parentId: entry.parentId, removed: result.removed });
|
|
271
|
+
out.removed += result.removed;
|
|
272
|
+
dropProjectionCache(dshHome, entry.parentId);
|
|
273
|
+
} else if (result.ok === false && result.reason && result.reason !== 'no-log') {
|
|
274
|
+
out.failed.push({ parentId: entry.parentId, reason: result.reason });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* 退休单个子智能体的 catalog 行。命中 id 用 coreId 比较,兼容 catalog 里的
|
|
282
|
+
* 裸 childId 与调用方传来的 `session-` 前缀写法。失败时排队到 pending 文件,
|
|
283
|
+
* 宿主下次启动补做。
|
|
284
|
+
*/
|
|
285
|
+
export function retireCatalogRow(dshHome, parentSessionId, childId, pendingPath, everywhere = true) {
|
|
286
|
+
const core = logCoreId(childId);
|
|
287
|
+
const result = everywhere
|
|
288
|
+
? retireCatalogRowsEverywhere(dshHome, [core], parentSessionId)
|
|
289
|
+
: rewriteCatalogRows(dshHome, parentSessionId, (id) => logCoreId(id) === core);
|
|
290
|
+
// 全库模式下某份历史日志若因并发写失败,按它自己的 parentId 精确排队,
|
|
291
|
+
// 宿主下次启动用 gone 谓词补做(不能只靠父会话的 key,否则补不到那份日志)。
|
|
292
|
+
if (pendingPath && Array.isArray(result.failed)) {
|
|
293
|
+
for (const f of result.failed) {
|
|
294
|
+
if (f && typeof f.parentId === 'string') queueCatalogRetire(pendingPath, f.parentId, core);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (result.removed > 0) return { retired: true, queued: false, removed: result.removed, result };
|
|
298
|
+
// 没删到行:可能是日志里本来就没有(幽灵只存在于缓存),不算失败、不排队。
|
|
299
|
+
// 明确失败的判定:单文件模式看 result.ok===false;全库模式看 failed 非空。
|
|
300
|
+
const hardFailed = everywhere
|
|
301
|
+
? (Array.isArray(result.failed) && result.failed.length > 0)
|
|
302
|
+
: (result.ok === false);
|
|
303
|
+
if (!hardFailed) return { retired: false, queued: false, removed: 0, result };
|
|
304
|
+
if (pendingPath) queueCatalogRetire(pendingPath, parentSessionId, core);
|
|
305
|
+
return { retired: false, queued: true, removed: 0, result };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function pendingFilePath(dshHome) {
|
|
309
|
+
return join(dshHome, 'yanyan-subagent-pending.json');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function readPending(pendingPath) {
|
|
313
|
+
try {
|
|
314
|
+
const parsed = JSON.parse(readFileSync(pendingPath, 'utf8'));
|
|
315
|
+
return parsed !== null && typeof parsed === 'object' ? parsed : {};
|
|
316
|
+
} catch (error) {
|
|
317
|
+
return {};
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function queueCatalogRetire(pendingPath, parentSessionId, childCoreId) {
|
|
322
|
+
try {
|
|
323
|
+
const pending = readPending(pendingPath);
|
|
324
|
+
const list = Array.isArray(pending[parentSessionId]) ? pending[parentSessionId] : [];
|
|
325
|
+
if (list.includes(childCoreId) !== true) list.push(childCoreId);
|
|
326
|
+
pending[parentSessionId] = list;
|
|
327
|
+
mkdirSync(dirname(pendingPath), { recursive: true });
|
|
328
|
+
writeFileSync(pendingPath, JSON.stringify(pending, null, 2), 'utf8');
|
|
329
|
+
} catch (error) { /* 排队是尽力而为 */ }
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* 宿主启动时重放排队的退休。只在父会话「冷」(无 live agent)时改写;
|
|
334
|
+
* 用 shouldDrop=「子会话目录已不在」的谓词,保证只退休确实已删除的行。
|
|
335
|
+
* liveAgentIds 为当前存活会话 id 的 Set(由调用方从 ctx.agents 采集)。
|
|
336
|
+
*/
|
|
337
|
+
export function flushPendingRewrites(dshHome, liveAgentIds) {
|
|
338
|
+
const pendingPath = pendingFilePath(dshHome);
|
|
339
|
+
const pending = readPending(pendingPath);
|
|
340
|
+
const parents = Object.keys(pending);
|
|
341
|
+
if (parents.length === 0) return { ok: true, parents: 0, removed: 0 };
|
|
342
|
+
const stored = storedSessionDirNames(dshHome);
|
|
343
|
+
const live = liveAgentIds instanceof Set ? liveAgentIds : new Set();
|
|
344
|
+
const gone = (id) => !stored.has(id) && !stored.has('session-' + id);
|
|
345
|
+
let removed = 0;
|
|
346
|
+
let cleared = 0;
|
|
347
|
+
for (const parentId of parents) {
|
|
348
|
+
if (live.has(parentId) || live.has(logCoreId(parentId)) || live.has('session-' + logCoreId(parentId))) continue;
|
|
349
|
+
try {
|
|
350
|
+
const result = rewriteCatalogRows(dshHome, parentId, gone);
|
|
351
|
+
if (result.ok === true) removed += result.removed;
|
|
352
|
+
else continue; // 保持队列,下次再试
|
|
353
|
+
} catch (error) { continue; }
|
|
354
|
+
delete pending[parentId];
|
|
355
|
+
cleared += 1;
|
|
356
|
+
}
|
|
357
|
+
try { writeFileSync(pendingPath, JSON.stringify(pending, null, 2), 'utf8'); } catch (error) { /* ignore */ }
|
|
358
|
+
return { ok: true, parents: parents.length, cleared, removed };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** 扫父会话里所有「子会话目录已不在、catalog 行还在」的残留并退休。 */
|
|
362
|
+
export function scrubCatalogRows(dshHome, parentSessionId, everywhere = false) {
|
|
363
|
+
const stored = storedSessionDirNames(dshHome);
|
|
364
|
+
const gone = (id) => !stored.has(id) && !stored.has('session-' + id);
|
|
365
|
+
if (everywhere) {
|
|
366
|
+
const out = { files: [], removed: 0, failed: [], scanned: 0 };
|
|
367
|
+
for (const entry of allSessionLogPaths(dshHome)) {
|
|
368
|
+
out.scanned += 1;
|
|
369
|
+
let result;
|
|
370
|
+
try { result = rewriteCatalogRowsInFile(entry.logPath, gone); }
|
|
371
|
+
catch (error) { result = { ok: false, reason: String((error && error.message) || error) }; }
|
|
372
|
+
if (result.ok === true && result.removed > 0) {
|
|
373
|
+
out.files.push({ parentId: entry.parentId, removed: result.removed });
|
|
374
|
+
out.removed += result.removed;
|
|
375
|
+
dropProjectionCache(dshHome, entry.parentId);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return out;
|
|
379
|
+
}
|
|
380
|
+
const result = rewriteCatalogRows(dshHome, parentSessionId, gone);
|
|
381
|
+
return { files: result.removed > 0 ? [{ parentId: parentSessionId, removed: result.removed }] : [], removed: result.removed, ok: result.ok, reason: result.reason };
|
|
382
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// dsh-plugin-yanyan-subagent / lib/zstd-frames.js
|
|
2
|
+
//
|
|
3
|
+
// 逐帧解码 / 重编码 DSH 的 `session.v4.jsonl.zstd` 会话日志。
|
|
4
|
+
//
|
|
5
|
+
// 移植自 huajiao1998/dsh-plugin-subagent-clean@0.3.0 的 src/zstd-frames.js,
|
|
6
|
+
// 原始实现来自 heiheiha798/dsh-plugin-subagent-delete@0.2.3(MIT)。
|
|
7
|
+
// 版权所有 (c) heiheiha798;本文件及其在 dsh-plugin-yanyan-subagent 内的
|
|
8
|
+
// 派生改动同样以 MIT 许可发布。详见本包 README 的「许可与致谢」章节。
|
|
9
|
+
//
|
|
10
|
+
// 为什么需要自己扫帧:DSH 把会话日志按「header 单独一帧、其后每批事件
|
|
11
|
+
// 一帧」写成一串独立可解码的 Zstandard 帧,`zstdDecompressSync` 只解第一帧,
|
|
12
|
+
// 必须先按 zstd 帧格式逐帧切开再分别解压、拼接。
|
|
13
|
+
|
|
14
|
+
import { zstdCompressSync, zstdDecompressSync } from 'node:zlib';
|
|
15
|
+
|
|
16
|
+
const ZSTD_MAGIC = 0xfd2fb528;
|
|
17
|
+
const SKIP_MASK = 0xfffffff0;
|
|
18
|
+
const SKIP_MAGIC = 0x184d2a50;
|
|
19
|
+
const NL = String.fromCharCode(10);
|
|
20
|
+
|
|
21
|
+
/** 枚举一份 artifact 里所有可独立解码的 Zstandard 帧。 */
|
|
22
|
+
export function scanZstdFrames(buf) {
|
|
23
|
+
const frames = [];
|
|
24
|
+
let pos = 0;
|
|
25
|
+
while (pos + 4 <= buf.length) {
|
|
26
|
+
const magic = buf.readUInt32LE(pos);
|
|
27
|
+
// skippable frame(0x184D2A50..0x184D2A5F):8 字节头 + size 字节载荷。
|
|
28
|
+
if ((magic & SKIP_MASK) === SKIP_MAGIC) {
|
|
29
|
+
if (pos + 8 > buf.length) break;
|
|
30
|
+
const size = buf.readUInt32LE(pos + 4);
|
|
31
|
+
frames.push({ start: pos, end: pos + 8 + size });
|
|
32
|
+
pos += 8 + size;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (magic !== ZSTD_MAGIC) break;
|
|
36
|
+
const start = pos;
|
|
37
|
+
let p = pos + 4;
|
|
38
|
+
const fhd = buf[p];
|
|
39
|
+
p += 1;
|
|
40
|
+
const fcsFlag = fhd >> 6;
|
|
41
|
+
const singleSegment = (fhd >> 5) & 1;
|
|
42
|
+
const hasChecksum = (fhd >> 2) & 1;
|
|
43
|
+
const dictIdFlag = fhd & 3;
|
|
44
|
+
if (!singleSegment) p += 1;
|
|
45
|
+
p += [0, 1, 2, 4][dictIdFlag];
|
|
46
|
+
p += [singleSegment ? 1 : 0, 2, 4, 8][fcsFlag];
|
|
47
|
+
for (;;) {
|
|
48
|
+
if (p + 3 > buf.length) return frames;
|
|
49
|
+
const bh = buf[p] | (buf[p + 1] << 8) | (buf[p + 2] << 16);
|
|
50
|
+
const last = bh & 1;
|
|
51
|
+
const type = (bh >> 1) & 3;
|
|
52
|
+
const size = bh >>> 3;
|
|
53
|
+
p += 3 + (type === 1 ? 1 : size);
|
|
54
|
+
if (last) break;
|
|
55
|
+
}
|
|
56
|
+
if (hasChecksum) p += 4;
|
|
57
|
+
frames.push({ start, end: p });
|
|
58
|
+
pos = p;
|
|
59
|
+
}
|
|
60
|
+
return frames;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 解压全部帧并把明文拼回去。 */
|
|
64
|
+
export function decodeLog(buf) {
|
|
65
|
+
const frames = scanZstdFrames(buf);
|
|
66
|
+
const parts = frames.map((f) => zstdDecompressSync(buf.subarray(f.start, f.end)));
|
|
67
|
+
return Buffer.concat(parts).toString('utf8');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 按 DSH 存储层的写法重新编码:header 独占第一帧(加载器对此有断言),
|
|
72
|
+
* 其后每个事件批压一帧。
|
|
73
|
+
*/
|
|
74
|
+
export function encodeLog(headerLine, eventLines, perFrame = 400) {
|
|
75
|
+
const frames = [zstdCompressSync(Buffer.from(headerLine + NL))];
|
|
76
|
+
for (let i = 0; i < eventLines.length; i += perFrame) {
|
|
77
|
+
const chunk = eventLines.slice(i, i + perFrame).join(NL) + NL;
|
|
78
|
+
frames.push(zstdCompressSync(Buffer.from(chunk)));
|
|
79
|
+
}
|
|
80
|
+
return Buffer.concat(frames);
|
|
81
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"meta":{"title":"Subagent Management","description":"List, delete, and auto-clean subagents"}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"meta":{"title":"子智能体管理","description":"查看、逐个删除与自动清理子智能体"}}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-plugin-yanyan-subagent",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "子智能体管理:查看、彻底删除(日志根除)、批量清理空闲与自动清理、清理残留行",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"icon": "icon.svg",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./lib/index.js",
|
|
10
|
+
"./client": "./lib/client.js",
|
|
11
|
+
"./package.json": "./package.json",
|
|
12
|
+
"./locale/*.json": "./locale/*.json",
|
|
13
|
+
"./icon": "./icon.svg"
|
|
14
|
+
},
|
|
15
|
+
"files": ["lib/index.js","lib/client.js","lib/log-rewrite.js","lib/zstd-frames.js","lib/legacy-hidden-ids.json","cordis.patch.yml","icon.svg","locale/en.json","locale/zh.json","README.md","README.zh.md"],
|
|
16
|
+
"keywords": ["deepseek-harness","dsh-plugin","subagent","cleanup","session-log","cordis"],
|
|
17
|
+
"author": "sweet560",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/sweet560/dsh-plugin-yanyan-toolkit.git",
|
|
22
|
+
"directory": "packages/dsh-plugin-yanyan-subagent"
|
|
23
|
+
},
|
|
24
|
+
"engines": { "node": "^22.19.0 || >=24.0.0" },
|
|
25
|
+
"scripts": { "test": "node --test test/*.test.mjs" },
|
|
26
|
+
"dsh": {
|
|
27
|
+
"engines": { "dsh": ">=0.2.2-rc.2" },
|
|
28
|
+
"bundle": { "patch": "./cordis.patch.yml" },
|
|
29
|
+
"client": { "inject": ["locale","sessions","slots"], "platform": "web" },
|
|
30
|
+
"manifestVersion": 1
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Yanyan Toolkit - Plugin Update
|
|
2
|
+
|
|
3
|
+
The first plugin of the "Yanyan Toolkit" aggregate bundle. It places a "Check updates" button beside the "Installed" group heading on the DSH plugins settings page, checks npm-installed and local `link:` / `file:` directory plugins for newer versions, and offers a per-row update action.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Registry plugins: read the installed version and compare it with the npm registry `latest` version.
|
|
8
|
+
- Local plugins (`link:` / `file:`): for git repositories, fetch and compare local HEAD with `origin/<branch>`; without `.git`, honestly report "local source, cannot auto-update".
|
|
9
|
+
- GitHub sources are best-effort; when they cannot be compared they are reported as "update GitHub sources on the plugin detail page" without blocking other checks.
|
|
10
|
+
- Updatable rows prefer the official `pluginManager` service `updateBundle` (built-in automatic restore on failure/cancel, never removes first); when that API is absent it falls back to an over-install and only uses a two-phase remove+install when `already-installed`, with a hard guarantee of restoring the original version. Local git sources run `git fetch` + `git merge --ff-only`.
|
|
11
|
+
- This package and the aggregate package are never updated; official `@deepseek-ai/*` packages are skipped.
|
|
12
|
+
|
|
13
|
+
## Constraints
|
|
14
|
+
|
|
15
|
+
- Pure JavaScript (ESM host half + `__ModuleLoader__` client half); no React, TypeScript, JSX, build tools, or third-party dependencies.
|
|
16
|
+
- UI copy is Chinese; no emoji in code or docs.
|
|
17
|
+
- All same-origin loopback routes pass the `ctx.connection.requestRejection` admission gate.
|
|
18
|
+
- Updates take effect after a page refresh or a DSH restart.
|
|
19
|
+
|
|
20
|
+
## Tests
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
node --test test/*.test.mjs
|
|
24
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# 研研工具包 - 插件更新
|
|
2
|
+
|
|
3
|
+
聚合插件包「研研工具包」的第一个插件。在 DSH 设置页「已安装插件」分组标题右侧放置一个「检查更新」按钮,检查线上 npm 插件与本地 `link:` / `file:` 目录插件版本,并逐行提供更新操作。
|
|
4
|
+
|
|
5
|
+
## 功能
|
|
6
|
+
|
|
7
|
+
- 检查线上插件(npm 安装):读取已装版本并与 npm registry 的 latest 版本比对。
|
|
8
|
+
- 检查本地插件(`link:` / `file:` 目录):若是 git 仓库,fetch 后比对本地 HEAD 与 `origin/<branch>`;无 `.git` 时如实显示「本地源码,无法自动更新」。
|
|
9
|
+
- GitHub 源尽力而为比对,取不到时显示「GitHub 源请在插件详情页更新」,不阻断其它插件检查。
|
|
10
|
+
- 可更新行点击「更新」优先复用官方 `pluginManager` 服务的 `updateBundle`(内建失败/取消自动装回原版本,绝不先卸载);无该 API 时降级为覆盖安装、仅在 `already-installed` 时两阶段并硬保证回滚装回原版本,本地 git 源执行 `git fetch` + `git merge --ff-only`。
|
|
11
|
+
- 禁止更新本包与聚合包自身,跳过 `@deepseek-ai/*` 官方包。
|
|
12
|
+
|
|
13
|
+
## 约束
|
|
14
|
+
|
|
15
|
+
- 纯 JavaScript(ESM host 半区 + `__ModuleLoader__` client 半区),不依赖 React、不使用 TypeScript / JSX / 构建工具、不引第三方依赖。
|
|
16
|
+
- 所有 UI 文案中文,代码与文档不使用 emoji。
|
|
17
|
+
- 所有同源 loopback 路由经 `ctx.connection.requestRejection` 准入裁决。
|
|
18
|
+
- 更新动作需要刷新页面或重启 DSH 后生效。
|
|
19
|
+
|
|
20
|
+
## 测试
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
node --test test/*.test.mjs
|
|
24
|
+
```
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="插件更新">
|
|
2
|
+
<rect x="6" y="6" width="52" height="52" rx="12" fill="none" stroke="currentColor" stroke-width="4"/>
|
|
3
|
+
<path d="M22 34 L30 42 L44 24" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
4
|
+
<path d="M20 18 A16 16 0 0 1 44 18" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" opacity="0.55"/>
|
|
5
|
+
</svg>
|