@yxzpro/agent-acta 2.14.4
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/LICENSE +21 -0
- package/LOGFORMATS.md +960 -0
- package/README.md +238 -0
- package/REFERENCE.md +1428 -0
- package/agent-acta-page.html +1998 -0
- package/agent-acta-server.mjs +1066 -0
- package/agent-acta-widget.html +724 -0
- package/archive.mjs +335 -0
- package/core/log.mjs +20 -0
- package/core/service.mjs +4285 -0
- package/hooks/cursor-usage-hook.mjs +89 -0
- package/mcp-server.mjs +372 -0
- package/package.json +68 -0
- package/page/dialog-add-agent.js +65 -0
- package/page/dialog-alert.js +159 -0
- package/page/dialog-archive.js +342 -0
- package/page/dialog-compare.js +238 -0
- package/page/dialog-ctxw.js +101 -0
- package/page/dialog-diag.js +72 -0
- package/page/dialog-heatmap.js +215 -0
- package/page/dialog-selftest.js +202 -0
- package/page/dialog-sess-timeline.js +108 -0
- package/page/dialog-stats.js +694 -0
- package/page/dialog-usage.js +158 -0
- package/page/log-card.js +277 -0
- package/page/log-view.js +273 -0
- package/page/sess-view.js +382 -0
- package/page/shared.js +408 -0
- package/page/sidebar.js +129 -0
- package/page/style.css +973 -0
- package/page/topbar.js +61 -0
- package/parsers/atomcode.mjs +321 -0
- package/parsers/buddy.mjs +272 -0
- package/parsers/buddyext.mjs +582 -0
- package/parsers/claude.mjs +496 -0
- package/parsers/cline.mjs +468 -0
- package/parsers/codearts.mjs +96 -0
- package/parsers/codex.mjs +188 -0
- package/parsers/comate.mjs +195 -0
- package/parsers/copilot.mjs +308 -0
- package/parsers/cursor.mjs +493 -0
- package/parsers/devindb.mjs +358 -0
- package/parsers/discovery.mjs +1230 -0
- package/parsers/doubao.mjs +406 -0
- package/parsers/dsh.mjs +377 -0
- package/parsers/gemini.mjs +267 -0
- package/parsers/generic.mjs +494 -0
- package/parsers/hermes.mjs +398 -0
- package/parsers/kilo.mjs +42 -0
- package/parsers/kimi.mjs +249 -0
- package/parsers/mimocode.mjs +531 -0
- package/parsers/minimax.mjs +503 -0
- package/parsers/openclaw.mjs +490 -0
- package/parsers/opencode.mjs +465 -0
- package/parsers/shared.mjs +418 -0
- package/parsers/trace.mjs +66 -0
- package/parsers/tracecode.mjs +348 -0
- package/parsers/traedb.mjs +528 -0
- package/parsers/zcode.mjs +356 -0
- package/plugin/client.js +51 -0
- package/plugin/cordis.patch.yml +5 -0
- package/plugin/plugin.mjs +220 -0
- package/search.mjs +554 -0
- package/tools/grab-trae-key.ps1 +265 -0
- package/vendor/element-plus-icons.js +3 -0
- package/vendor/element-plus.css +1 -0
- package/vendor/element-plus.dark.css +1 -0
- package/vendor/element-plus.full.min.js +65 -0
- package/vendor/element-plus.zh-cn.js +2 -0
- package/vendor/svg/antgroup-color.svg +1 -0
- package/vendor/svg/bytedance-color.svg +1 -0
- package/vendor/svg/chatglm-color.svg +1 -0
- package/vendor/svg/claude-color.svg +1 -0
- package/vendor/svg/claudecode-color.svg +1 -0
- package/vendor/svg/cline.svg +1 -0
- package/vendor/svg/codebuddy-color.svg +1 -0
- package/vendor/svg/codebuddy.svg +1 -0
- package/vendor/svg/codex-color.svg +1 -0
- package/vendor/svg/copilot.svg +1 -0
- package/vendor/svg/cursor.svg +1 -0
- package/vendor/svg/deepseek-color.svg +1 -0
- package/vendor/svg/deepseek-harness-whale.svg +8 -0
- package/vendor/svg/doubao-color.svg +1 -0
- package/vendor/svg/gemini-color.svg +1 -0
- package/vendor/svg/glmv-color.svg +1 -0
- package/vendor/svg/grok.svg +1 -0
- package/vendor/svg/hermesagent.svg +1 -0
- package/vendor/svg/huawei-color.svg +1 -0
- package/vendor/svg/hunyuan-color.svg +1 -0
- package/vendor/svg/kilocode-avatar.svg +1 -0
- package/vendor/svg/kilocode.svg +1 -0
- package/vendor/svg/kimi-color.svg +1 -0
- package/vendor/svg/kimi.svg +1 -0
- package/vendor/svg/logo-AtomGit-G-red.svg +3 -0
- package/vendor/svg/logo-AtomGit-dark.svg +4 -0
- package/vendor/svg/logo-AtomGit-light.svg +4 -0
- package/vendor/svg/meta-color.svg +1 -0
- package/vendor/svg/mimo.svg +66 -0
- package/vendor/svg/minimax-code-logo.svg +8 -0
- package/vendor/svg/minimax-color.svg +1 -0
- package/vendor/svg/moonshot.svg +1 -0
- package/vendor/svg/nex-color.svg +1 -0
- package/vendor/svg/nex.svg +1 -0
- package/vendor/svg/nvidia-color.svg +1 -0
- package/vendor/svg/openai.svg +1 -0
- package/vendor/svg/openclaw-color.svg +1 -0
- package/vendor/svg/openclaw.svg +1 -0
- package/vendor/svg/opencode.svg +1 -0
- package/vendor/svg/openrouter-color.svg +1 -0
- package/vendor/svg/qoder-color.svg +1 -0
- package/vendor/svg/qwen-color.svg +1 -0
- package/vendor/svg/sensenova-color.svg +1 -0
- package/vendor/svg/stepfun.svg +1 -0
- package/vendor/svg/trae-color.svg +1 -0
- package/vendor/svg/traework-v2.svg +1 -0
- package/vendor/svg/wenxin-color.svg +1 -0
- package/vendor/svg/windsurf.svg +1 -0
- package/vendor/svg/workbuddy-color.svg +29 -0
- package/vendor/svg/xiaomimimo.svg +1 -0
- package/vendor/svg/yuanbao-color.svg +1 -0
- package/vendor/svg/yuanbao.svg +1 -0
- package/vendor/svg/zai.svg +1 -0
- package/vendor/svg/zhipu-color.svg +1 -0
- package/vendor/vue.global.prod.js +9 -0
- package/widget/main.js +202 -0
- package/widget/package.json +7 -0
- package/widget/preload.js +11 -0
package/archive.mjs
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
// ---------------- 归档(历史条目冻结) ----------------
|
|
2
|
+
// 为什么要有它:有些 agent 自己删日志 —— codearts 的 %APPDATA%/codearts-agent/User/logs 只留
|
|
3
|
+
// 约 30 天,产品一删就永久没有,面板再怎么扫也扫不回来(本机历史月已出现空档)。归档把
|
|
4
|
+
// 「已经结束、且发生在今天之前」的**条目 + 详情快照**按 <agent>/<YYYY-MM-DD>.jsonl 冻在本地:
|
|
5
|
+
// 源文件被产品删掉之后,历史在归档里仍然查得到。
|
|
6
|
+
//
|
|
7
|
+
// 三条硬设计(与实时数据/扫描链的边界,别越线):
|
|
8
|
+
// 1) **只冻结、不回喂**。不做「把归档目录当扫描根」那套 —— 扫描、快照、聚合、会话、热力图
|
|
9
|
+
// 一条链都不动;归档以独立只读视图(/api/archive/*)呈现。否则同一轮会既算实时又算归档。
|
|
10
|
+
// 2) **幂等靠合并,不靠重建**。重跑某一天时先读回旧文件、再按 id 合并覆盖,绝不用「当前内存
|
|
11
|
+
// 里的条目」重建整天文件 —— entries 有 LRU 上限(MAX_ENTRIES),内存里没有 ≠ 没归档过,
|
|
12
|
+
// 重建的那一版会把已归档但已被淘汰的历史整段抹掉(这是本模块最危险的坑)。
|
|
13
|
+
// 3) **口径升级不重放**。条目级快照按落盘当时的口径冻结,PARSER_REV 变了也不回头改老归档:
|
|
14
|
+
// 源文件很可能已经被产品删了,想重放也没有原料。代价是归档条目与实时条目可能口径不同,
|
|
15
|
+
// 页面上要写清楚(见 REFERENCE.md「归档」一节)。
|
|
16
|
+
//
|
|
17
|
+
// 依赖由主进程注入(initArchive):entries / srcs 是扫描的共享 Map,dayKey 与 entryContent 都
|
|
18
|
+
// 在主文件里(entryContent 要读回源文件重建正文),本模块不 import 主文件、避免循环依赖。
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
|
|
22
|
+
let DEPS = null; // { dataDir, entries, srcs, entryContent, dayKey, parserRev }
|
|
23
|
+
export function archiveRoot() { return path.join(DEPS.dataDir, 'archive'); }
|
|
24
|
+
|
|
25
|
+
// 单条记录里详情数组的上限:一轮真能堆上百次工具调用,全存进去归档文件会畸形膨胀,
|
|
26
|
+
// 而页面上的归档视图是「翻历史用的」,超出的部分只记个数(detailMore)即可。
|
|
27
|
+
const MAX_DETAIL_ITEMS = 40;
|
|
28
|
+
|
|
29
|
+
export function initArchive(d) { DEPS = d; }
|
|
30
|
+
|
|
31
|
+
const agentDir = agent => path.join(archiveRoot(), agent);
|
|
32
|
+
const dayFile = (agent, day) => path.join(agentDir(agent), day + '.jsonl');
|
|
33
|
+
const isDayName = s => /^\d{4}-\d{2}-\d{2}$/.test(s);
|
|
34
|
+
|
|
35
|
+
// 归档目录里绝对不能出现的字符(agent 名来自 config,理论上可控;一旦有人手工写个带路径的名字,
|
|
36
|
+
// 不挡就会把文件写到归档目录之外去)。只允许字母数字与 . _ - ,其余一律换成 _。
|
|
37
|
+
const safeAgent = n => String(n || '').replace(/[^A-Za-z0-9._-]/g, '_');
|
|
38
|
+
|
|
39
|
+
// POSIX 权限(R34):归档文件里是**完整对话快照**,多用户机器上不该默认可读,所以新建的按私有建
|
|
40
|
+
//(目录 0700 / 文件 0600,win32 上 mode 无副作用 —— 那边的 chmod 只管只读位)。
|
|
41
|
+
// **只在新文件上生效,不 chmod 已有文件**:归档目录是用户可见的数据,有人可能故意放在共享位置
|
|
42
|
+
// 让同事读(或在网络上备份),悄悄把老文件改成 0600 会砸掉那种用法。老文件要收紧就显式来:
|
|
43
|
+
// chmod -R go-rwx ~/.agent-acta
|
|
44
|
+
// 与 search.mjs 的 saveSearch 是同一套做法,各写一遍是有意的(archive 不 import search,反之亦然)。
|
|
45
|
+
const POSIX = process.platform !== 'win32';
|
|
46
|
+
const NEW_FILE_MODE = POSIX ? { mode: 0o600 } : undefined;
|
|
47
|
+
function ensureDir(p) { try { fs.mkdirSync(p, POSIX ? { recursive: true, mode: 0o700 } : { recursive: true }); } catch {} }
|
|
48
|
+
|
|
49
|
+
function readDay(agent, day) {
|
|
50
|
+
const fp = dayFile(agent, day);
|
|
51
|
+
const map = new Map();
|
|
52
|
+
let raw;
|
|
53
|
+
try { raw = fs.readFileSync(fp, 'utf8'); } catch { return map; }
|
|
54
|
+
for (const line of raw.split('\n')) {
|
|
55
|
+
if (!line) continue;
|
|
56
|
+
try { const o = JSON.parse(line); if (o && o.id) map.set(o.id, o); } catch {}
|
|
57
|
+
}
|
|
58
|
+
return map;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function writeDay(agent, day, map) {
|
|
62
|
+
ensureDir(agentDir(agent));
|
|
63
|
+
const fp = dayFile(agent, day);
|
|
64
|
+
const lines = [...map.values()].sort((a, b) => (a.time || 0) - (b.time || 0)).map(o => JSON.stringify(o));
|
|
65
|
+
const body = lines.length ? lines.join('\n') + '\n' : '';
|
|
66
|
+
fs.writeFileSync(fp + '.tmp', body, NEW_FILE_MODE);
|
|
67
|
+
fs.renameSync(fp + '.tmp', fp);
|
|
68
|
+
return { count: lines.length, bytes: Buffer.byteLength(body) };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ---------------- manifest(清单) ----------------
|
|
72
|
+
// 页面清单接口每次请求都去数一遍每个 jsonl 的行数太亏(几十个文件、几 MB),所以归档时顺手
|
|
73
|
+
// 把 {条数, 字节} 记在 manifest 里;清单接口以 manifest 为主、再用 readdir 校正文件是否还在
|
|
74
|
+
// (用户手工删过文件时不至于报出不存在的条目)。
|
|
75
|
+
const MANIFEST = () => path.join(archiveRoot(), 'manifest.json');
|
|
76
|
+
function loadManifest() {
|
|
77
|
+
try { const j = JSON.parse(fs.readFileSync(MANIFEST(), 'utf8')); if (j && j.agents) return j; } catch {}
|
|
78
|
+
return { v: 1, updatedAt: 0, agents: {} };
|
|
79
|
+
}
|
|
80
|
+
function saveManifest(m) {
|
|
81
|
+
ensureDir(archiveRoot());
|
|
82
|
+
m.updatedAt = Date.now();
|
|
83
|
+
const fp = MANIFEST();
|
|
84
|
+
try { fs.writeFileSync(fp + '.tmp', JSON.stringify(m), NEW_FILE_MODE); fs.renameSync(fp + '.tmp', fp); } catch {}
|
|
85
|
+
}
|
|
86
|
+
// 按磁盘实况校正 manifest:文件没了就划掉,manifest 里没有但盘上有的(手工拷进来的)补一条
|
|
87
|
+
// count:-1(条数未知,页面显示为「?」,不假装是 0)。
|
|
88
|
+
function resolveManifest() {
|
|
89
|
+
const m = loadManifest();
|
|
90
|
+
const out = [];
|
|
91
|
+
let agents = [];
|
|
92
|
+
try { agents = fs.readdirSync(archiveRoot(), { withFileTypes: true }).filter(d => d.isDirectory()).map(d => d.name); } catch {}
|
|
93
|
+
for (const agent of agents) {
|
|
94
|
+
let names = [];
|
|
95
|
+
try { names = fs.readdirSync(path.join(archiveRoot(), agent)); } catch {}
|
|
96
|
+
for (const n of names) {
|
|
97
|
+
if (!n.endsWith('.jsonl') || n.endsWith('.tmp')) continue;
|
|
98
|
+
const day = n.slice(0, -6);
|
|
99
|
+
if (!isDayName(day)) continue;
|
|
100
|
+
const rec = m.agents?.[agent]?.[day];
|
|
101
|
+
let bytes = rec ? rec.bytes : 0;
|
|
102
|
+
try { bytes = fs.statSync(path.join(archiveRoot(), agent, n)).size; } catch {}
|
|
103
|
+
out.push({ agent, day, count: rec ? rec.count : -1, bytes });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
out.sort((a, b) => (a.day === b.day ? a.agent.localeCompare(b.agent) : b.day.localeCompare(a.day)));
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---------------- 归档一轮 ----------------
|
|
111
|
+
// 选哪些条目:已结束(finished 不为 false)且发生在**今天 0 点之前**。进行中的轮不冻 ——
|
|
112
|
+
// 冻下来的是半截数据(token 还没累完),而它下一轮扫就会补齐。
|
|
113
|
+
// opts.skip = 不归档的 agent 名(config.archive.skip,见 REFERENCE「历史归档」):
|
|
114
|
+
// 有的 agent 的日志本来就不值得留(量小/无正文/临时跑跑),开关只停**未来**的归档,
|
|
115
|
+
// **绝不碰已经冻好的文件** —— 开关是省磁盘的,不是删数据的入口。
|
|
116
|
+
// maxPerRun > 0 时只处理最老的 N 条(给服务内的后台轮用,避免一次跑几分钟)。
|
|
117
|
+
export async function archiveRun(opts) {
|
|
118
|
+
const o = opts || {};
|
|
119
|
+
const now = Date.now();
|
|
120
|
+
const todayStart = (() => { const d = new Date(now); d.setHours(0, 0, 0, 0); return d.getTime(); })();
|
|
121
|
+
const skipSet = new Set((o.skip || []).map(s => String(s)));
|
|
122
|
+
const all = [];
|
|
123
|
+
let skipped = 0;
|
|
124
|
+
for (const [id, e] of DEPS.entries) {
|
|
125
|
+
if (!e || !e.agent || !e.time || e.time >= todayStart) continue;
|
|
126
|
+
if (e.finished === false) continue;
|
|
127
|
+
if (skipSet.size && skipSet.has(e.agent)) { skipped++; continue; }
|
|
128
|
+
all.push([id, e]);
|
|
129
|
+
}
|
|
130
|
+
all.sort((a, b) => (a[1].time || 0) - (b[1].time || 0));
|
|
131
|
+
const capped = o.maxPerRun > 0 ? all.slice(0, o.maxPerRun) : all;
|
|
132
|
+
|
|
133
|
+
const stat = { scanned: all.length, processed: capped.length, archived: 0, updated: 0, unchanged: 0, noDetail: 0, days: 0, bytes: 0, failed: 0, capped: capped.length < all.length, skipped };
|
|
134
|
+
const groups = new Map(); // "agent|day" -> {agent, day, ids:[[id,e]]}
|
|
135
|
+
for (const [id, e] of capped) {
|
|
136
|
+
const agent = safeAgent(e.agent), day = DEPS.dayKey(e.time);
|
|
137
|
+
const k = agent + '|' + day;
|
|
138
|
+
if (!groups.has(k)) groups.set(k, { agent, day, rows: [] });
|
|
139
|
+
groups.get(k).rows.push([id, e]);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const manifest = loadManifest();
|
|
143
|
+
let tick = makeTick();
|
|
144
|
+
let done = 0;
|
|
145
|
+
const progress = () => {
|
|
146
|
+
// 首次归档一台积压几千条的机器要跑几分钟(每条都要回读源文件重建正文),
|
|
147
|
+
// 全程静默会让人以为卡死了,所以每 500 条报一次进度。
|
|
148
|
+
if (o.log && ++done % 500 === 0) o.log('归档中… ' + done + '/' + stat.processed);
|
|
149
|
+
};
|
|
150
|
+
for (const g of groups.values()) {
|
|
151
|
+
const existing = readDay(g.agent, g.day);
|
|
152
|
+
const isNewDay = !fs.existsSync(dayFile(g.agent, g.day));
|
|
153
|
+
let changed = false;
|
|
154
|
+
for (const [id, e] of g.rows) {
|
|
155
|
+
const prev = existing.get(id);
|
|
156
|
+
// 同一条已经冻过且当时取到了详情 → 不再重读源文件(一轮几百条,重读是白花的时间)。
|
|
157
|
+
if (prev && prev.detail) { stat.unchanged++; progress(); continue; }
|
|
158
|
+
const rec = buildRecord(id, e, prev);
|
|
159
|
+
existing.set(id, rec);
|
|
160
|
+
if (prev) stat.updated++; else stat.archived++;
|
|
161
|
+
if (!rec.detail) stat.noDetail++;
|
|
162
|
+
changed = true;
|
|
163
|
+
progress();
|
|
164
|
+
await tick();
|
|
165
|
+
}
|
|
166
|
+
if (!changed) continue;
|
|
167
|
+
if (o.dry) { stat.days++; continue; }
|
|
168
|
+
try {
|
|
169
|
+
const w = writeDay(g.agent, g.day, existing);
|
|
170
|
+
(manifest.agents[g.agent] ||= {})[g.day] = { count: w.count, bytes: w.bytes };
|
|
171
|
+
stat.days++; stat.bytes += w.bytes;
|
|
172
|
+
if (isNewDay) o.log && o.log('archive: ' + g.agent + '/' + g.day + '(' + w.count + ' 条,' + fmtBytes(w.bytes) + ')');
|
|
173
|
+
} catch (err) { stat.failed++; o.log && o.log('archive: 写 ' + g.agent + '/' + g.day + ' 失败:' + err.message); }
|
|
174
|
+
}
|
|
175
|
+
if (!o.dry && stat.days) saveManifest(manifest);
|
|
176
|
+
return stat;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 一条归档记录 = 列表要用的统计字段 + 详情快照。统计字段逐个显式列,不用 {...e} 整对象摊开:
|
|
180
|
+
// entry 里将来多出内部字段(如缓存用的临时字段)时,不至于悄悄冻进归档、以后想删都删不掉。
|
|
181
|
+
function buildRecord(id, e, prev) {
|
|
182
|
+
const src = DEPS.srcs.get(id);
|
|
183
|
+
const rec = {
|
|
184
|
+
v: 1, id, agent: e.agent,
|
|
185
|
+
kind: src ? src.kind : (prev?.kind || ''),
|
|
186
|
+
project: e.project || '', session: e.session || '',
|
|
187
|
+
time: e.time || 0, day: DEPS.dayKey(e.time),
|
|
188
|
+
dur: e.dur || 0, status: e.status || 'ok',
|
|
189
|
+
tin: e.tin || 0, tout: e.tout || 0, tcache: e.tcache || 0, total: e.total || 0,
|
|
190
|
+
ctx: e.ctx || 0, rounds: e.rounds || 0, tools: e.tools || 0, calls: e.calls || 0,
|
|
191
|
+
models: e.models || [], preview: e.preview || '',
|
|
192
|
+
finished: e.finished !== false, aborted: !!e.aborted,
|
|
193
|
+
toolNames: e.toolNames || {},
|
|
194
|
+
rev: src ? (DEPS.parserRev[src.kind] ?? null) : (prev?.rev ?? null),
|
|
195
|
+
};
|
|
196
|
+
if (prev && prev.detail) return { ...rec, detail: prev.detail, detailMore: prev.detailMore };
|
|
197
|
+
const d = detailOf(id);
|
|
198
|
+
if (d) { rec.detail = d.detail; rec.detailMore = d.more; }
|
|
199
|
+
return rec;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 详情快照:拿实时详情那条路(entryContent,full=false)现取一次,截断口径与非 full 的页面一致。
|
|
203
|
+
// 取不到(源文件已被产品删掉 / 该 kind 不支持回读)不是错误 —— 归档照样记统计,只是没有正文,
|
|
204
|
+
// 页面上会显示「归档时未取到详情」。
|
|
205
|
+
function detailOf(id) {
|
|
206
|
+
let d = null;
|
|
207
|
+
try { d = DEPS.entryContent(id, false); } catch { d = null; }
|
|
208
|
+
if (!d) return null;
|
|
209
|
+
// ⚠ 主文件的 entryContent 读不到源文件时**不返回 null**,而是吞掉异常回一句占位串
|
|
210
|
+
//(catch 里硬编码的 '[内容读取失败: …]')。不认出来的话,归档会把这条占位串当成用户的真实输入
|
|
211
|
+
// 冻进文件 —— 而归档恰恰是「以后再也补不回来」的那一份:源文件当时读不到,留下来的却是句误导人的话。
|
|
212
|
+
// 当「归档时未取到详情」处理(走已有的 noDetail 口径),与上面那句「取不到不是错误」保持一致。
|
|
213
|
+
if (typeof d.user === 'string' && d.user.startsWith('[内容读取失败')) return null;
|
|
214
|
+
const more = {};
|
|
215
|
+
const cut = (arr, key) => {
|
|
216
|
+
if (!Array.isArray(arr)) return [];
|
|
217
|
+
if (arr.length <= MAX_DETAIL_ITEMS) return arr;
|
|
218
|
+
more[key] = arr.length - MAX_DETAIL_ITEMS;
|
|
219
|
+
return arr.slice(0, MAX_DETAIL_ITEMS);
|
|
220
|
+
};
|
|
221
|
+
const out = {};
|
|
222
|
+
if (d.user) out.user = String(d.user);
|
|
223
|
+
if (d.assistant) out.assistant = String(d.assistant);
|
|
224
|
+
if (d.note) out.note = d.note;
|
|
225
|
+
if (d.callsNote) out.callsNote = d.callsNote;
|
|
226
|
+
out.tools = cut(d.tools, 'tools');
|
|
227
|
+
out.calls = cut(d.calls, 'calls');
|
|
228
|
+
if (Array.isArray(d.events) && d.events.length) out.events = cut(d.events, 'events');
|
|
229
|
+
return { detail: out, more: Object.keys(more).length ? more : undefined };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 并发让出:entryContent 是同步读文件,一轮几千条会把事件循环占死(页面就转圈)。
|
|
233
|
+
// 攒够 ~40ms 才 let 一次(与首扫分片同一套预算,见主文件 SLICE_BUDGET_MS 的说明)。
|
|
234
|
+
function makeTick(budgetMs) {
|
|
235
|
+
let deadline = Date.now() + (budgetMs || 40);
|
|
236
|
+
return async function tick() {
|
|
237
|
+
if (Date.now() < deadline) return;
|
|
238
|
+
await new Promise(r => setImmediate(r));
|
|
239
|
+
deadline = Date.now() + (budgetMs || 40);
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ---------------- 查询(页面只读视图直接用) ----------------
|
|
244
|
+
export function archiveIndex() {
|
|
245
|
+
const rows = resolveManifest();
|
|
246
|
+
const agents = {};
|
|
247
|
+
let totalCount = 0, totalBytes = 0;
|
|
248
|
+
for (const r of rows) {
|
|
249
|
+
const a = agents[r.agent] || (agents[r.agent] = { agent: r.agent, days: 0, count: 0, bytes: 0, lastDay: '' });
|
|
250
|
+
a.days++; a.bytes += r.bytes;
|
|
251
|
+
if (r.count >= 0) a.count += r.count; else a.unknown = true;
|
|
252
|
+
if (r.day > a.lastDay) a.lastDay = r.day;
|
|
253
|
+
totalBytes += r.bytes; if (r.count >= 0) totalCount += r.count;
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
root: archiveRoot(), days: rows, agents: Object.values(agents).sort((x, y) => y.lastDay.localeCompare(x.lastDay)),
|
|
257
|
+
totals: { agents: Object.keys(agents).length, days: rows.length, count: totalCount, bytes: totalBytes },
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// 列条目:agent/day 至少给一个;q 为关键词(对 preview/user/assistant/session 做包含匹配,
|
|
262
|
+
// 与主列表的「关键词」口径一致 —— 都是简单的包含,不做分词/正则)。
|
|
263
|
+
export function archiveEntries({ agent, day, q, limit, offset } = {}) {
|
|
264
|
+
const lim = Math.min(Math.max(Number(limit) || 200, 1), 2000);
|
|
265
|
+
const off = Math.max(Number(offset) || 0, 0);
|
|
266
|
+
const kw = String(q || '').trim().toLowerCase();
|
|
267
|
+
const rows = resolveManifest().filter(r => (!agent || r.agent === agent) && (!day || r.day === day));
|
|
268
|
+
const hit = [];
|
|
269
|
+
let scanned = 0, total = 0;
|
|
270
|
+
for (const r of rows) {
|
|
271
|
+
const list = readDay(r.agent, r.day);
|
|
272
|
+
for (const rec of list.values()) {
|
|
273
|
+
scanned++;
|
|
274
|
+
if (kw && !matchKw(rec, kw)) continue;
|
|
275
|
+
total++;
|
|
276
|
+
if (total <= off || hit.length >= lim) continue;
|
|
277
|
+
hit.push({ ...rec, detail: undefined, detailMore: undefined, hasDetail: !!rec.detail });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
hit.sort((a, b) => (b.time || 0) - (a.time || 0));
|
|
281
|
+
return { list: hit, total, scanned, offset: off, limit: lim };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function matchKw(rec, kw) {
|
|
285
|
+
const hay = [rec.preview, rec.session, rec.project, rec.detail?.user, rec.detail?.assistant,
|
|
286
|
+
(rec.models || []).join(' '), Object.keys(rec.toolNames || {}).join(' ')].join('\n').toLowerCase();
|
|
287
|
+
return hay.includes(kw);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function archiveEntry(agent, day, id) {
|
|
291
|
+
const rec = readDay(safeAgent(agent), day).get(id);
|
|
292
|
+
return rec || null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ---------------- 保留策略 ----------------
|
|
296
|
+
// 默认「永久保留」:归档的意义就是对抗产品删日志,自动删归档等于又制造一次空档。
|
|
297
|
+
// 想限量的机器给 config.archive.maxDays(按日期保留最近 N 天)/ maxMB(整目录上限,超了从最老的日期文件开始删)。
|
|
298
|
+
// 删除只针对**整天文件**,不做单条裁剪 —— 归档的价值在「那天发生过什么」,留半天的残卷没意义。
|
|
299
|
+
export function pruneArchive({ maxDays, maxMB, dry, log } = {}) {
|
|
300
|
+
const rows = resolveManifest();
|
|
301
|
+
const removed = [];
|
|
302
|
+
if (maxDays > 0) {
|
|
303
|
+
const cutoff = (() => { const d = new Date(); d.setHours(0, 0, 0, 0); d.setDate(d.getDate() - maxDays + 1); return DEPS.dayKey(d.getTime()); })();
|
|
304
|
+
for (const r of rows) if (r.day < cutoff) removed.push(r);
|
|
305
|
+
}
|
|
306
|
+
if (maxMB > 0) {
|
|
307
|
+
const keep = new Set(removed.map(r => r.agent + '|' + r.day));
|
|
308
|
+
const rest = rows.filter(r => !keep.has(r.agent + '|' + r.day)).sort((a, b) => a.day.localeCompare(b.day));
|
|
309
|
+
let total = rest.reduce((s, r) => s + r.bytes, 0);
|
|
310
|
+
const cap = maxMB * 1024 * 1024;
|
|
311
|
+
for (const r of rest) { if (total <= cap) break; total -= r.bytes; removed.push(r); }
|
|
312
|
+
}
|
|
313
|
+
if (!removed.length) return { removed: 0, bytes: 0 };
|
|
314
|
+
let bytes = 0;
|
|
315
|
+
for (const r of removed) {
|
|
316
|
+
bytes += r.bytes;
|
|
317
|
+
if (dry) continue;
|
|
318
|
+
try { fs.unlinkSync(path.join(archiveRoot(), r.agent, r.day + '.jsonl')); } catch {}
|
|
319
|
+
}
|
|
320
|
+
if (!dry) {
|
|
321
|
+
const m = loadManifest();
|
|
322
|
+
for (const r of removed) { if (m.agents?.[r.agent]) delete m.agents[r.agent][r.day]; }
|
|
323
|
+
saveManifest(m);
|
|
324
|
+
for (const r of removed) { try { if (!fs.readdirSync(path.join(archiveRoot(), r.agent)).length) fs.rmdirSync(path.join(archiveRoot(), r.agent)); } catch {} }
|
|
325
|
+
}
|
|
326
|
+
log && log('archive: 按保留策略删除 ' + removed.length + ' 个日期文件(' + fmtBytes(bytes) + ')');
|
|
327
|
+
return { removed: removed.length, bytes };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function fmtBytes(n) {
|
|
331
|
+
if (!n) return '0B';
|
|
332
|
+
if (n < 1024) return n + 'B';
|
|
333
|
+
if (n < 1024 * 1024) return (n / 1024).toFixed(1) + 'KB';
|
|
334
|
+
return (n / 1024 / 1024).toFixed(1) + 'MB';
|
|
335
|
+
}
|
package/core/log.mjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// 诊断行的唯一出口(2026-09-29 拆掉全局 console 补丁之后立的)。
|
|
2
|
+
//
|
|
3
|
+
// 原来 core/service.mjs 里有一行 `console.log = (...a) => console.error(...a)` —— 改的是**整个进程**
|
|
4
|
+
// 的 console 对象。MCP 里没问题(独立进程、stdout 是协议通道),但 DSH 插件一旦被加载,**宿主自己的**
|
|
5
|
+
// console.log 也跟着被我们改道到 stderr:宿主日志采集器会因此少掉全部 info 行。那是越界的副作用,
|
|
6
|
+
// 而且症状落在宿主身上(我们的代码一个字节没改,宿主的日志先少了一半),所以换成这个模块内的出口。
|
|
7
|
+
//
|
|
8
|
+
// 为什么是**单独一个叶子模块**,而不是 core/service.mjs 里的一个函数:parsers/ 下也要用它
|
|
9
|
+
//(mimocode 有 3 处 console.log),而 core/service.mjs 正是 import parsers 的那一方 ——
|
|
10
|
+
// 从 parsers 反向 import 它就成了循环依赖。这里零 import,谁都能用。
|
|
11
|
+
//
|
|
12
|
+
// 去向由 core/service.mjs 在模块体里定一次 —— 「被当库 import 还是当入口跑」的判据只有那一份
|
|
13
|
+
//(MODE 那一段,hosted 也是从那儿切过去的),两边各算一遍判据就会漂。规则:
|
|
14
|
+
// cli → stdout —— 终端体验与搬家前一致,--status / --doctor 的球与文字走原来的流
|
|
15
|
+
// 其余 → stderr —— lib 的 stdout 是 MCP over stdio 的**协议通道**(混进一行「[index] loaded 1234
|
|
16
|
+
// states」就是一条解析不了的坏消息);hosted 的 stdout 归宿主,插件往里写诊断行同样是污染。
|
|
17
|
+
// 日志在 stderr 里一行不少。
|
|
18
|
+
let toStderr = false;
|
|
19
|
+
export function routeLogsToStderr(on) { toStderr = !!on; }
|
|
20
|
+
export function log(...a) { if (toStderr) console.error(...a); else console.log(...a); }
|