@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
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ---------------- AgentActa × Cursor:逐轮 token 采集 hook ----------------
|
|
3
|
+
// 由页面侧栏 cursor 行那个「注入」按钮写进 ~/.cursor/hooks.json(见服务端 /api/cursor-hook)。
|
|
4
|
+
// Cursor 在每一轮 agent loop 结束时 spawn 本脚本、把事件 JSON 从 stdin 递进来,本脚本把**用量字段**
|
|
5
|
+
// 追加到 ~/.agent-acta/cursor-usage.jsonl,解析器(parsers/cursor.mjs)再把它按会话/轮配对贴回卡片。
|
|
6
|
+
//
|
|
7
|
+
// 为什么非得走 hook:cursor 的转录文件(agent-transcripts/*.jsonl)里**只有 role + text**,
|
|
8
|
+
// 连时间戳都没有;IDE 的 state.vscdb 里 usageData 是空对象、tokenCount 恒为 0。
|
|
9
|
+
// 逐轮用量只在这条 hook 载荷里出现(实测字段见下),没有第二条本地来源。
|
|
10
|
+
//
|
|
11
|
+
// 载荷(stop / afterAgentResponse 同值,只挑 stop 当唯一来源,免得同一个 generation 记两遍):
|
|
12
|
+
// { hook_event_name, conversation_id, generation_id, model, model_id, model_params,
|
|
13
|
+
// status, loop_count, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens }
|
|
14
|
+
// 三条实测口径(都写进解析器注释了,这里只落原始值,不做换算):
|
|
15
|
+
// 1) input_tokens **含** cache_read + cache_write,要减掉才是不重复的「输入」;
|
|
16
|
+
// 2) 这几个数是**这一轮**的累计(一轮里所有模型调用加总),不是单次调用;
|
|
17
|
+
// 3) token 字段是可选的 —— 老版本 Cursor 没有,缺了就整条不落(宁可不记,也不落一排假的 0)。
|
|
18
|
+
//
|
|
19
|
+
// 落盘格式(一行一条,供解析器读,不要手改):
|
|
20
|
+
// {"v":1,"ev":"stop","conv":"<会话id>","gen":"<本轮id>","ts":<毫秒>,"model":"<model_id>",
|
|
21
|
+
// "status":"completed","in":N,"out":N,"cr":N,"cw":N}
|
|
22
|
+
// · conv 就是转录所在目录名(会话 id),两边靠它对上;gen 用来在同一会话里去重(自动续跑会让
|
|
23
|
+
// stop 触发多次、值相同);ts = 本脚本落盘时刻,解析器按它把用量分派到对应轮。
|
|
24
|
+
//
|
|
25
|
+
// 三条硬约束(hook 是**旁路**,绝不能拖累 Cursor):
|
|
26
|
+
// 1) **fail-open**:任何异常都吞掉、永远 exit 0、stdout 只吐一个 `{}`。
|
|
27
|
+
// 非权限类 hook(stop 就是)退出码非 0 时 Cursor 照常继续,但会记一条 hook 失败日志 ——
|
|
28
|
+
// 我们不想让用户在自己的 Cursor 日志里看到我们的报错。
|
|
29
|
+
// 2) **绝不阻塞**:stdin 最多等 5s(hooks.json 里给的 timeout 是 10s),到点就按「没数据」收工。
|
|
30
|
+
// 3) **只追加、不重写**:追加式落盘,单行写入;并发(多个 Cursor 窗口)也只是多几行,解析器去重。
|
|
31
|
+
import fs from 'node:fs';
|
|
32
|
+
import os from 'node:os';
|
|
33
|
+
import path from 'node:path';
|
|
34
|
+
|
|
35
|
+
const OUT_DIR = path.join(os.homedir(), '.agent-acta');
|
|
36
|
+
const OUT = path.join(OUT_DIR, 'cursor-usage.jsonl');
|
|
37
|
+
const STDIN_WAIT_MS = 5000;
|
|
38
|
+
const STDIN_MAX = 256 * 1024; // 载荷只有几百字节;给个上限纯粹是防呆
|
|
39
|
+
|
|
40
|
+
const str = v => (typeof v === 'string' ? v : '');
|
|
41
|
+
const num = v => (typeof v === 'number' && Number.isFinite(v) && v >= 0 ? Math.round(v) : null);
|
|
42
|
+
|
|
43
|
+
function readStdin() {
|
|
44
|
+
return new Promise(resolve => {
|
|
45
|
+
let buf = '', done = false;
|
|
46
|
+
const fin = () => { if (done) return; done = true; clearTimeout(timer); resolve(buf); };
|
|
47
|
+
const timer = setTimeout(fin, STDIN_WAIT_MS);
|
|
48
|
+
try {
|
|
49
|
+
process.stdin.setEncoding('utf8');
|
|
50
|
+
process.stdin.on('data', c => { if (buf.length < STDIN_MAX) buf += c; });
|
|
51
|
+
process.stdin.on('end', fin);
|
|
52
|
+
process.stdin.on('error', fin);
|
|
53
|
+
} catch { fin(); }
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function main() {
|
|
58
|
+
const raw = await readStdin();
|
|
59
|
+
// 必须剥 BOM:Cursor(Windows)往 stdin 递的 JSON 以 UTF-8 BOM 开头(\uFEFF),
|
|
60
|
+
// 直接 JSON.parse 会抛 "Unexpected token '\uFEFF'",整个脚本静默不落盘 —— 实测踩过。
|
|
61
|
+
let j = null;
|
|
62
|
+
try { j = JSON.parse(raw.replace(/^\uFEFF/, '').trim()); } catch { return; } // 半截/非法 JSON:当没数据
|
|
63
|
+
if (!j || typeof j !== 'object') return;
|
|
64
|
+
|
|
65
|
+
const rec = {
|
|
66
|
+
v: 1,
|
|
67
|
+
ev: str(j.hook_event_name),
|
|
68
|
+
conv: str(j.conversation_id),
|
|
69
|
+
gen: str(j.generation_id),
|
|
70
|
+
ts: Date.now(),
|
|
71
|
+
// model_id 是真模型名(grok-4.6);model 是带模式后缀的档位名(cursor-grok-4.6-high-fast),
|
|
72
|
+
// 只有当 model_id 缺失时才拿它顶上。
|
|
73
|
+
model: str(j.model_id) || str(j.model),
|
|
74
|
+
status: str(j.status),
|
|
75
|
+
in: num(j.input_tokens), out: num(j.output_tokens),
|
|
76
|
+
cr: num(j.cache_read_tokens), cw: num(j.cache_write_tokens),
|
|
77
|
+
};
|
|
78
|
+
if (!rec.conv) return; // 没有会话 id:配不上任何轮
|
|
79
|
+
if (rec.in == null && rec.out == null && rec.cr == null && rec.cw == null) return; // 没有用量字段(老版本)
|
|
80
|
+
|
|
81
|
+
fs.mkdirSync(OUT_DIR, { recursive: true });
|
|
82
|
+
fs.appendFileSync(OUT, JSON.stringify(rec) + '\n', 'utf8');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
main().catch(() => {}).then(() => {
|
|
86
|
+
// stdout 是给 Cursor 的协议通道(stop 只认 followup_message,这里什么都不返回),
|
|
87
|
+
// 写完再退,免得管道里还没刷出去就被 exit 截掉。
|
|
88
|
+
process.stdout.write('{}\n', () => process.exit(0));
|
|
89
|
+
});
|
package/mcp-server.mjs
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// agentacta-mcp —— 把 AgentActa 的**只读**查询面包成 MCP server(stdio 传输),让 agent 自己问自己的日志。
|
|
3
|
+
//
|
|
4
|
+
// 为什么要有它:面板只能「看」,看的前提是人已经知道要看哪一列。而复盘类的提问天然是问句
|
|
5
|
+
// (「我上周最慢的一轮是哪一轮」「哪个项目 token 涨得最凶」),拿界面去答就得人肉翻筛子。
|
|
6
|
+
// 接成 MCP 之后 claude / cursor / qoder 这些客户端可以直接问,答的每一句都能回溯到条目 id。
|
|
7
|
+
//
|
|
8
|
+
// 三条硬边界(与项目「本地只读」的定位一致):
|
|
9
|
+
// 1) **只读**。数据全部来自 `agent-acta-server.mjs` 导出的 LIBRO(只读库模式:不落索引、不写配置、
|
|
10
|
+
// 不搬数据、不起 HTTP、不抢端口、不挂定时器),工具面里也没有任何写入口。
|
|
11
|
+
// 2) **不新增网络出口**。传输只有 stdio —— 客户端 fork 本进程、我们只往 stdout 写协议消息。
|
|
12
|
+
// stdout 是协议通道,所以一行诊断都不许往那儿打(服务端那侧的诊断在只读库模式下走它自己的
|
|
13
|
+
// log() 出口 → stderr,见 core/log.mjs —— 2026-09-29 之前是靠改全局 console 实现的,现在不是了)。
|
|
14
|
+
// 3) **不美化口径**。返回的字段与页面/HTTP 接口逐字同源(同一个 filterEntries / dailyAgg / analyzeAgg),
|
|
15
|
+
// 截断与上限都要在返回里说清楚(truncated / dropped),宁少不假。
|
|
16
|
+
//
|
|
17
|
+
// 协议:JSON-RPC 2.0 over stdio,一行一条消息(MCP stdio 传输的现行帧格式)。
|
|
18
|
+
// 手工对接(不装客户端也能验):
|
|
19
|
+
// printf '%s\n%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}' '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
|
|
20
|
+
// | node mcp-server.mjs
|
|
21
|
+
import { LIBRO } from './agent-acta-server.mjs';
|
|
22
|
+
|
|
23
|
+
const PROTOCOL_SUPPORTED = ['2025-06-18', '2025-03-26', '2024-11-05'];
|
|
24
|
+
const PROTOCOL_LATEST = PROTOCOL_SUPPORTED[0];
|
|
25
|
+
// 长驻的 MCP 进程要有新日志可问,但每问一次就全量走一遍源目录太憨(本机 5.2K 个目录项):
|
|
26
|
+
// 超过这个年龄才补扫一轮。客户端连着用就是「最多滞后 15 秒」,急用可以显式传 refresh:true。
|
|
27
|
+
const REFRESH_TTL_MS = Number(process.env.AGENT_LOG_MCP_TTL_MS || 15000);
|
|
28
|
+
const PREVIEW_CHARS = 200; // 列表里的用户输入摘要(条目里本来就存了 300 字,别再放大)
|
|
29
|
+
const DEFAULT_LIMIT = 50;
|
|
30
|
+
const MAX_LIMIT = 500;
|
|
31
|
+
const DEFAULT_DAYS = 90; // 按天聚合默认只回最近 90 天(全量 401 天对问句没用,还容易顶到响应上限)
|
|
32
|
+
const MAX_CHARS = Number(process.env.AGENT_LOG_MCP_MAX_CHARS || 220000);
|
|
33
|
+
|
|
34
|
+
const log = (...a) => { try { process.stderr.write('[agentacta-mcp] ' + a.join(' ') + '\n'); } catch {} };
|
|
35
|
+
const send = msg => process.stdout.write(JSON.stringify(msg) + '\n');
|
|
36
|
+
const reply = (id, result) => send({ jsonrpc: '2.0', id, result });
|
|
37
|
+
const replyErr = (id, code, message) => send({ jsonrpc: '2.0', id, error: { code, message } });
|
|
38
|
+
|
|
39
|
+
let lastScanAt = 0;
|
|
40
|
+
function ensureFresh(force) {
|
|
41
|
+
if (!force && lastScanAt && Date.now() - lastScanAt < REFRESH_TTL_MS) return;
|
|
42
|
+
const r = LIBRO.refresh();
|
|
43
|
+
lastScanAt = Date.now();
|
|
44
|
+
log('补扫一轮:' + r.scanMs + 'ms / 内存 ' + r.entries + ' 条');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------------- 参数与形状 ----------------
|
|
48
|
+
const num = (v, dflt, min, max) => {
|
|
49
|
+
const n = Number(v);
|
|
50
|
+
if (!Number.isFinite(n)) return dflt;
|
|
51
|
+
return Math.min(max, Math.max(min, Math.trunc(n)));
|
|
52
|
+
};
|
|
53
|
+
const str = v => (v == null ? '' : String(v));
|
|
54
|
+
// 时间范围与页面/HTTP 同一套:range ∈ today|7d|30d,或显式 from/to(YYYY-MM-DD,本地自然日)。
|
|
55
|
+
// 两者同给时显式区间优先 —— 这条口径在 filterEntries 里,这里不另判一次。
|
|
56
|
+
const filters = a => [str(a.agent), str(a.project), str(a.range), str(a.from), str(a.to)];
|
|
57
|
+
const FILTER_PROPS = {
|
|
58
|
+
agent: { type: 'string', description: '只看某个 agent(claude / codex / cursor / trae / qoder-cn …,见 overview)。留空 = 全部' },
|
|
59
|
+
project: { type: 'string', description: '只看某个项目(页面侧栏那个归一化后的名字)。留空 = 全部' },
|
|
60
|
+
range: { type: 'string', enum: ['today', '7d', '30d'], description: '相对范围;与 from/to 二选一,同给时以 from/to 为准' },
|
|
61
|
+
from: { type: 'string', description: '起始自然日 YYYY-MM-DD(本地时区)' },
|
|
62
|
+
to: { type: 'string', description: '结束自然日 YYYY-MM-DD(含当天)' },
|
|
63
|
+
refresh: { type: 'boolean', description: '先补扫一轮再回答(默认只在超过 15 秒没扫时才扫)。刚产生的新日志问不到时用它' },
|
|
64
|
+
};
|
|
65
|
+
const fmtTime = t => {
|
|
66
|
+
if (!t) return null;
|
|
67
|
+
const d = new Date(t), p = n => String(n).padStart(2, '0');
|
|
68
|
+
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds());
|
|
69
|
+
};
|
|
70
|
+
const clip = (s, n) => { const t = String(s == null ? '' : s); return t.length > n ? t.slice(0, n) + '…' : t; };
|
|
71
|
+
|
|
72
|
+
// 一条轮次(卡片)的稳定子集:字段名与页面/接口一致,不改名、不算第二套口径。
|
|
73
|
+
function turnRow(e, withPreview) {
|
|
74
|
+
const row = {
|
|
75
|
+
id: e.id, at: fmtTime(e.time), time: e.time, agent: e.agent, project: e.project,
|
|
76
|
+
session: e.session || null, dur: e.dur || 0, status: e.status || 'ok',
|
|
77
|
+
models: e.models || [], tin: e.tin || 0, tout: e.tout || 0, tcache: e.tcache || 0, total: e.total || 0,
|
|
78
|
+
calls: e.calls || 0, tools: e.tools || 0,
|
|
79
|
+
};
|
|
80
|
+
// token 恒 0 不等于没用量:qoder 走 credits、cursor 的转录里根本没有 usage 字段(见 LOGFORMATS.md)。
|
|
81
|
+
// 这几个键是 undefined 时 JSON 会自动丢掉,所以只在该有条目带它们时才出现在返回里。
|
|
82
|
+
for (const k of ['ctx', 'ctxUsed', 'ctxRatio', 'credits', 'compacts']) if (e[k]) row[k] = e[k];
|
|
83
|
+
if (e.aborted) row.aborted = true; // 用户打断的一轮:与 status:'error' 是两回事
|
|
84
|
+
if (withPreview) row.preview = clip(e.preview, PREVIEW_CHARS);
|
|
85
|
+
return row;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 响应上限兜底:模型上下文被一次工具调用灌爆是真事故,所以宁可如实截短,也别返回一坨被客户端掐掉的 JSON。
|
|
89
|
+
// 只削「行数组」(entries / days / sessions / slowest / models / agents 这些),标量键不动。
|
|
90
|
+
function shrinkToFit(payload) {
|
|
91
|
+
let text = JSON.stringify(payload);
|
|
92
|
+
if (text.length <= MAX_CHARS) return payload;
|
|
93
|
+
const lists = Object.keys(payload).filter(k => Array.isArray(payload[k]) && payload[k].length > 1);
|
|
94
|
+
for (const k of lists) {
|
|
95
|
+
const n = Math.max(1, Math.floor(payload[k].length * 0.6));
|
|
96
|
+
if (n === payload[k].length) continue;
|
|
97
|
+
payload[k] = payload[k].slice(0, n);
|
|
98
|
+
payload.truncated = (payload.truncated || []) .concat(k + ' 只保留前 ' + n + ' 条(响应上限 ' + MAX_CHARS + ' 字符)');
|
|
99
|
+
text = JSON.stringify(payload);
|
|
100
|
+
if (text.length <= MAX_CHARS) return payload;
|
|
101
|
+
}
|
|
102
|
+
return payload;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------------- 工具面(七个,全只读) ----------------
|
|
106
|
+
const TOOLS = [
|
|
107
|
+
{
|
|
108
|
+
name: 'overview',
|
|
109
|
+
description: '先问这个:本机接了哪些 agent、各有多少条、内存窗口多大、归档里还有多少(回答"看不到"之前要用它定位是没扫到还是已被产品删掉)',
|
|
110
|
+
inputSchema: { type: 'object', properties: {} },
|
|
111
|
+
run: () => {
|
|
112
|
+
const { agents, projects } = LIBRO.counts();
|
|
113
|
+
const confs = LIBRO.agentsList();
|
|
114
|
+
const rows = confs.map(c => ({
|
|
115
|
+
agent: c.name, kind: c.kind, entries: agents[c.name] || 0,
|
|
116
|
+
enabled: c.enabled !== false, missing: !!c.missing, source: c.source,
|
|
117
|
+
})).sort((a, b) => b.entries - a.entries || a.agent.localeCompare(b.agent));
|
|
118
|
+
return {
|
|
119
|
+
version: LIBRO.version, build: LIBRO.build, dataDir: LIBRO.dataDir,
|
|
120
|
+
entries: Object.values(agents).reduce((s, n) => s + n, 0),
|
|
121
|
+
agents: rows,
|
|
122
|
+
projects: Object.entries(projects).sort((a, b) => b[1] - a[1]).slice(0, 30)
|
|
123
|
+
.map(([project, count]) => ({ project, count })),
|
|
124
|
+
projectsTotal: Object.keys(projects).length,
|
|
125
|
+
notes: [
|
|
126
|
+
'内存窗口有条数上限(LRU 淘汰最旧、每个 agent 至少保留最近 200 条),窗口外的历史在归档里:页面「历史归档」可查(本工具面暂未提供归档查询)',
|
|
127
|
+
'token 为 0 不代表没消耗:qoder 走 credits、cursor 的转录里没有 usage 字段、部分 agent 只给模型名不给逐次用量',
|
|
128
|
+
'preview 只是用户输入的前 300 字;正文与工具入参出参要用 get_entry',
|
|
129
|
+
],
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'search_entries',
|
|
135
|
+
description: '按 agent/项目/时间范围列轮次卡片(一行 = 一轮请求),可按 preview 文本粗筛;要正文与工具输出用 get_entry',
|
|
136
|
+
inputSchema: {
|
|
137
|
+
type: 'object', properties: {
|
|
138
|
+
...FILTER_PROPS,
|
|
139
|
+
text: { type: 'string', description: '在「用户输入摘要」里做子串匹配(大小写不敏感)。⚠ 不是全文检索:助手正文与工具输出不在索引里' },
|
|
140
|
+
status: { type: 'string', enum: ['any', 'ok', 'error'], description: '只要成功轮 / 只要失败轮(默认 any)' },
|
|
141
|
+
slowest: { type: 'boolean', description: '按耗时降序给(默认按时间倒序)' },
|
|
142
|
+
limit: { type: 'integer', minimum: 1, maximum: MAX_LIMIT, description: '最多返回多少条(默认 50)' },
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
run: a => {
|
|
146
|
+
const { list, rb } = LIBRO.filterEntries(...filters(a));
|
|
147
|
+
const kw = str(a.text).trim().toLowerCase();
|
|
148
|
+
let rows = list;
|
|
149
|
+
if (kw) rows = rows.filter(e => String(e.preview || '').toLowerCase().includes(kw));
|
|
150
|
+
if (a.status && a.status !== 'any') rows = rows.filter(e => (e.status || 'ok') === a.status);
|
|
151
|
+
const sortedRows = a.slowest ? [...rows].sort((x, y) => (y.dur || 0) - (x.dur || 0)) : rows;
|
|
152
|
+
const limit = num(a.limit, DEFAULT_LIMIT, 1, MAX_LIMIT);
|
|
153
|
+
return {
|
|
154
|
+
rangeLabel: rb ? rb.label : '全部时间',
|
|
155
|
+
matched: rows.length, returned: Math.min(limit, sortedRows.length),
|
|
156
|
+
order: a.slowest ? 'dur desc' : 'time desc',
|
|
157
|
+
entries: sortedRows.slice(0, limit).map(e => turnRow(e, true)),
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: 'get_entry',
|
|
163
|
+
description: '取一轮的详情:用户输入、助手正文、工具调用(入参/出参/是否报错)、逐次调用;id 来自 search_entries 或 slowest_turns',
|
|
164
|
+
inputSchema: {
|
|
165
|
+
type: 'object', properties: {
|
|
166
|
+
id: { type: 'string', description: '条目 id(形如 agent:file:turn)' },
|
|
167
|
+
full: { type: 'boolean', description: 'true = 不截断长正文与工具输出(响应会明显变大)' },
|
|
168
|
+
},
|
|
169
|
+
required: ['id'],
|
|
170
|
+
},
|
|
171
|
+
run: a => {
|
|
172
|
+
const e = LIBRO.entry(str(a.id));
|
|
173
|
+
if (!e) return { ok: false, error: '没有这个条目 id(可能已被内存窗口淘汰;用 search_entries 重新取)' };
|
|
174
|
+
const content = LIBRO.entryContent(str(a.id), !!a.full) || { note: '详情取不到:源文件已变化或已被产品删除(这一轮的列表字段仍然有效)' };
|
|
175
|
+
return { ok: true, entry: turnRow(e, true), src: LIBRO.srcOf(str(a.id)) || null, content };
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: 'list_sessions',
|
|
180
|
+
description: '按会话归组给轮次级汇总(一个会话 = 同一 agent + 项目 + session 标识),用来回答「那次会话烧了多少 / 花了多久 / 有没有失败轮」',
|
|
181
|
+
inputSchema: {
|
|
182
|
+
type: 'object', properties: {
|
|
183
|
+
...FILTER_PROPS,
|
|
184
|
+
limit: { type: 'integer', minimum: 1, maximum: MAX_LIMIT, description: '最多返回多少个会话(默认 30,按最后活跃倒序)' },
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
run: a => {
|
|
188
|
+
const r = LIBRO.sessionsAgg(...filters(a), num(a.limit, 30, 1, MAX_LIMIT));
|
|
189
|
+
return {
|
|
190
|
+
rangeLabel: r.range, matched: r.count, returned: r.sessions.length,
|
|
191
|
+
sessions: r.sessions.map(s => ({
|
|
192
|
+
session: s.key, agent: s.agent, project: s.project, name: s.name || null,
|
|
193
|
+
turns: s.turns, firstAt: fmtTime(s.from), lastAt: fmtTime(s.to),
|
|
194
|
+
dur: s.dur, tin: s.tin, tout: s.tout, tcache: s.tcache, total: s.total,
|
|
195
|
+
calls: s.calls, tools: s.tools, models: s.models, status: s.status, hasTimeline: !!s.hasTimeline,
|
|
196
|
+
})),
|
|
197
|
+
};
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: 'usage_stats',
|
|
202
|
+
description: '用量聚合:by=day 给自然日序列(含空天,时间轴不被压扁),by=model 给按模型的 token/耗时/条数;回答「这周比上周多花了多少」这类对比',
|
|
203
|
+
inputSchema: {
|
|
204
|
+
type: 'object', properties: {
|
|
205
|
+
...FILTER_PROPS,
|
|
206
|
+
by: { type: 'string', enum: ['day', 'model'], description: '分桶维度(默认 day)' },
|
|
207
|
+
last: { type: 'integer', minimum: 1, maximum: 400, description: 'by=day 只回最近 N 个自然日(默认 90,更早的如实标 dropped)' },
|
|
208
|
+
top: { type: 'integer', minimum: 1, maximum: 200, description: 'by=model 只回前 N 个模型(默认 20,按合计 token 倒序)' },
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
run: a => {
|
|
212
|
+
const f = filters(a);
|
|
213
|
+
if (str(a.by) === 'model') {
|
|
214
|
+
const m = LIBRO.modelAgg(...f);
|
|
215
|
+
const top = num(a.top, 20, 1, 200);
|
|
216
|
+
// 行里的 label 是「组内条目最多的写法」(显示名),names 是同一模型的全部写法(大小写差异已归并)
|
|
217
|
+
const models = m.models.slice(0, top).map(x => ({
|
|
218
|
+
model: x.label, spellings: x.names.length > 1 ? x.names : undefined,
|
|
219
|
+
count: x.count, tin: x.tin, tout: x.tout, tcache: x.tcache, total: x.total,
|
|
220
|
+
dur: x.dur, calls: x.calls, tools: x.tools, credits: x.credits || 0,
|
|
221
|
+
agents: x.agents,
|
|
222
|
+
}));
|
|
223
|
+
return {
|
|
224
|
+
by: 'model', range: m.range, matched: m.count,
|
|
225
|
+
dropped: Math.max(0, m.models.length - models.length),
|
|
226
|
+
noModel: m.noModel, noModelTotal: m.noModelTotal, multi: m.multi,
|
|
227
|
+
totals: m.totals, models,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
const d = LIBRO.dailyAgg(...f);
|
|
231
|
+
const last = num(a.last, DEFAULT_DAYS, 1, 400);
|
|
232
|
+
const days = d.days.slice(-last).map(x => ({
|
|
233
|
+
day: x.day, count: x.count, tin: x.tin, tout: x.tout, tcache: x.tcache, total: x.total,
|
|
234
|
+
dur: x.dur, maxDur: x.maxDur, calls: x.calls, tools: x.tools, agents: x.agents,
|
|
235
|
+
}));
|
|
236
|
+
return {
|
|
237
|
+
by: 'day', rangeLabel: d.label, from: d.from, to: d.to,
|
|
238
|
+
dropped: Math.max(0, d.days.length - days.length), daysCapped: d.daysCapped || 0,
|
|
239
|
+
days, totals: d.totals,
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
name: 'slowest_turns',
|
|
245
|
+
description: '时延画像:最慢轮 TopN + p50/p95(同一套筛选口径,不吃条/页窗口),用来找「哪一轮卡住了」',
|
|
246
|
+
inputSchema: {
|
|
247
|
+
type: 'object', properties: {
|
|
248
|
+
...FILTER_PROPS,
|
|
249
|
+
topN: { type: 'integer', minimum: 1, maximum: 100, description: '最慢前 N(默认 10)' },
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
run: a => {
|
|
253
|
+
const r = LIBRO.analyzeAgg(...filters(a), num(a.topN, 10, 1, 100));
|
|
254
|
+
return {
|
|
255
|
+
matched: r.count, p50: r.p50, p95: r.p95, maxDur: r.maxDur,
|
|
256
|
+
slowest: r.slowest.map(e => ({ ...turnRow(e, true), at: fmtTime(e.time) })),
|
|
257
|
+
};
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
name: 'tool_fails',
|
|
262
|
+
description: '工具失败画像:哪个工具最常失败 / 超时(按失败次数倒序,附失败率与样本下限)+ 逐 agent 的覆盖度。' +
|
|
263
|
+
'超时算失败(fail = err + timeout);软失败(rg 没搜到这类被判定为正常的结果)单列一档、不进失败率;' +
|
|
264
|
+
'源里没有超时/软失败这一层的 agent 出 null 而不是 0 —— 别把「源里没这层」读成「从没超时」',
|
|
265
|
+
inputSchema: {
|
|
266
|
+
type: 'object', properties: {
|
|
267
|
+
...FILTER_PROPS,
|
|
268
|
+
top: { type: 'integer', minimum: 1, maximum: 100, description: '只回前 N 个工具行(默认 10,按失败次数倒序)' },
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
run: a => {
|
|
272
|
+
const r = LIBRO.analyzeAgg(...filters(a), num(a.top, 10, 1, 100));
|
|
273
|
+
const tf = r.toolFails;
|
|
274
|
+
const unknown = tf.agents.reduce((s, c) => s + (c.unknown || 0), 0);
|
|
275
|
+
const sig = tf.agents.filter(c => c.tsSig).map(c => c.agent);
|
|
276
|
+
const notes = [];
|
|
277
|
+
if (!tf.agents.length) notes.push('这次筛选里没有带工具调用的轮 ⇒ 画像是空的,别读成「零失败」');
|
|
278
|
+
if (tf.building) notes.push('后台正在建索引:画像不全,「已核对」还会往上涨,别现在下结论');
|
|
279
|
+
if (unknown > 0) notes.push('有 ' + unknown + ' 轮带工具调用但还没索引到:它们既不算分子也不算分母' +
|
|
280
|
+
'(画像读的是搜索分片,本进程只读、不建索引,常驻服务建好那份才看得见)');
|
|
281
|
+
// 只列**带**这一层的 agent(通常两三家):列反了就是每次调用都甩一长串名字,把 context 占掉。
|
|
282
|
+
// 逐 agent 的 tsSig 在 agents[] 里都有,这里只是把结论先说出来。
|
|
283
|
+
if (tf.agents.length && sig.length < tf.agents.length) notes.push(sig.length
|
|
284
|
+
? '这些 agent 源里带超时/软失败这一层:' + sig.join('、') +
|
|
285
|
+
';其余 agent 没有超时/软失败这一层,它们的 timeout/soft 出 null(不是 0)'
|
|
286
|
+
: '本次筛选里的 agent 都没有超时/软失败这一层(源里没这个信息),timeout/soft 全是 null —— 不是 0');
|
|
287
|
+
notes.push('fail = err + timeout(超时算失败);soft 单列、不进失败率;total 低于 minTotal 的行 lowDenom=1,率别当结论');
|
|
288
|
+
return {
|
|
289
|
+
matched: r.count, minTotal: tf.minTotal, totals: tf.totals,
|
|
290
|
+
totalKinds: tf.totalKinds, truncated: tf.truncated,
|
|
291
|
+
rows: tf.rows, agents: tf.agents, building: tf.building, notes,
|
|
292
|
+
};
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
];
|
|
296
|
+
|
|
297
|
+
// ---------------- 协议处理 ----------------
|
|
298
|
+
let clientInfo = null;
|
|
299
|
+
|
|
300
|
+
function handle(msg) {
|
|
301
|
+
const { id, method, params } = msg;
|
|
302
|
+
switch (method) {
|
|
303
|
+
case 'initialize': {
|
|
304
|
+
const want = str(params?.protocolVersion);
|
|
305
|
+
const protocolVersion = PROTOCOL_SUPPORTED.includes(want) ? want : PROTOCOL_LATEST;
|
|
306
|
+
clientInfo = params?.clientInfo || null;
|
|
307
|
+
log('握手:客户端 ' + (clientInfo?.name || '?') + ' ' + (clientInfo?.version || '?') + ',协议 ' + protocolVersion);
|
|
308
|
+
return reply(id, {
|
|
309
|
+
protocolVersion,
|
|
310
|
+
capabilities: { tools: { listChanged: false } },
|
|
311
|
+
serverInfo: { name: 'agent-acta', version: LIBRO.version, title: 'AgentActa 本地 agent 日志(只读)' },
|
|
312
|
+
instructions: '数据是本机各 AI agent 的会话日志(只读)。回答任何一条结论都要能指回条目 id:先 overview 看清有哪些 agent,' +
|
|
313
|
+
'再 search_entries / usage_stats / slowest_turns / tool_fails 收窄,最后 get_entry 取证。' +
|
|
314
|
+
'token 为 0 别直接说「没花钱」(见 overview.notes);问「哪个工具最常超时/失败」用 tool_fails,' +
|
|
315
|
+
'它的 timeout/soft 为 null 表示源里没这一层、不是 0。',
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
case 'notifications/initialized':
|
|
319
|
+
case 'notifications/cancelled':
|
|
320
|
+
return; // 通知不带 id,也不该有响应
|
|
321
|
+
case 'ping':
|
|
322
|
+
return reply(id, {});
|
|
323
|
+
case 'tools/list':
|
|
324
|
+
return reply(id, { tools: TOOLS.map(t => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })) });
|
|
325
|
+
case 'tools/call': {
|
|
326
|
+
const name = str(params?.name);
|
|
327
|
+
const tool = TOOLS.find(t => t.name === name);
|
|
328
|
+
if (!tool) return replyErr(id, -32602, '没有这个工具:' + name);
|
|
329
|
+
const args = params?.arguments || {};
|
|
330
|
+
try {
|
|
331
|
+
ensureFresh(args.refresh === true);
|
|
332
|
+
const out = shrinkToFit(tool.run(args));
|
|
333
|
+
return reply(id, { content: [{ type: 'text', text: JSON.stringify(out, null, 1) }], isError: false });
|
|
334
|
+
} catch (e) {
|
|
335
|
+
// 工具执行失败按 MCP 口径回 isError(不是 JSON-RPC error):让模型看见原因、自己改问法
|
|
336
|
+
log('工具 ' + name + ' 失败:' + e.message);
|
|
337
|
+
return reply(id, { content: [{ type: 'text', text: JSON.stringify({ ok: false, error: e.message }) }], isError: true });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
default:
|
|
341
|
+
// 未实现的可选面(resources / prompts / logging)一律明确拒绝,别静默吞:
|
|
342
|
+
// 客户端拿不到答复会重试到超时,报错反而一眼看出「这个 server 只提供 tools」。
|
|
343
|
+
if (id == null) return;
|
|
344
|
+
return replyErr(id, -32601, '不支持的方法:' + method + '(本 server 只提供 tools:' + TOOLS.map(t => t.name).join(', ') + ')');
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ---------------- 主流程 ----------------
|
|
349
|
+
// 先扫一轮再挂 stdin:扫描期间进来的消息由内核缓冲,Node 在读侧没挂 handler 时不会丢,
|
|
350
|
+
// 这样模型的第一条 initialize 不会撞上「还没数据」。
|
|
351
|
+
const opened = await LIBRO.open();
|
|
352
|
+
lastScanAt = Date.now();
|
|
353
|
+
log('只读库就绪:' + opened.agents + ' 个 agent / ' + opened.entries + ' 条 / 首扫 ' + opened.scanMs + 'ms / 数据目录 ' + opened.dataDir);
|
|
354
|
+
|
|
355
|
+
let buf = '';
|
|
356
|
+
process.stdin.setEncoding('utf8');
|
|
357
|
+
process.stdin.on('data', chunk => {
|
|
358
|
+
buf += chunk;
|
|
359
|
+
let i;
|
|
360
|
+
while ((i = buf.indexOf('\n')) >= 0) {
|
|
361
|
+
const line = buf.slice(0, i).trim();
|
|
362
|
+
buf = buf.slice(i + 1);
|
|
363
|
+
if (!line) continue;
|
|
364
|
+
let msg;
|
|
365
|
+
try { msg = JSON.parse(line); } catch { log('忽略一行非 JSON(' + line.length + ' 字符)'); continue; }
|
|
366
|
+
if (Array.isArray(msg)) { for (const m of msg) handle(m); continue; } // 批量请求
|
|
367
|
+
handle(msg);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
process.stdin.on('end', () => process.exit(0)); // 客户端关掉管道 = 本进程没有存在的理由
|
|
371
|
+
process.on('SIGTERM', () => process.exit(0));
|
|
372
|
+
process.on('SIGINT', () => process.exit(0));
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yxzpro/agent-acta",
|
|
3
|
+
"version": "2.14.4",
|
|
4
|
+
"author": "elegant001",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://atomgit.com/elegant001/agent-acta.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://atomgit.com/elegant001/agent-acta#readme",
|
|
10
|
+
"description": "本地 AI agent 请求日志面板:聚合 atomcode / codebuddy / workbuddy / claude / codex / codearts / gemini / cursor / kimi / dsh 等本地日志,看每轮耗时、token、缓存、上下文与工具调用",
|
|
11
|
+
"bin": {
|
|
12
|
+
"agentacta": "./agent-acta-server.mjs",
|
|
13
|
+
"agentacta-mcp": "./mcp-server.mjs"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"agent-acta-server.mjs",
|
|
17
|
+
"core/",
|
|
18
|
+
"mcp-server.mjs",
|
|
19
|
+
"archive.mjs",
|
|
20
|
+
"search.mjs",
|
|
21
|
+
"agent-acta-page.html",
|
|
22
|
+
"agent-acta-widget.html",
|
|
23
|
+
"page/",
|
|
24
|
+
"widget/",
|
|
25
|
+
"hooks/",
|
|
26
|
+
"parsers/",
|
|
27
|
+
"vendor/",
|
|
28
|
+
"tools/",
|
|
29
|
+
"REFERENCE.md",
|
|
30
|
+
"LOGFORMATS.md",
|
|
31
|
+
"plugin/"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=22.5"
|
|
35
|
+
},
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"keywords": [
|
|
38
|
+
"agent",
|
|
39
|
+
"log",
|
|
40
|
+
"local",
|
|
41
|
+
"dashboard",
|
|
42
|
+
"atomcode",
|
|
43
|
+
"claude",
|
|
44
|
+
"codex",
|
|
45
|
+
"kimi",
|
|
46
|
+
"dsh",
|
|
47
|
+
"dsh-plugin"
|
|
48
|
+
],
|
|
49
|
+
"exports": {
|
|
50
|
+
".": "./plugin/plugin.mjs",
|
|
51
|
+
"./client": "./plugin/client.js",
|
|
52
|
+
"./cordis.patch.yml": "./plugin/cordis.patch.yml",
|
|
53
|
+
"./package.json": "./package.json"
|
|
54
|
+
},
|
|
55
|
+
"dsh": {
|
|
56
|
+
"bundle": {
|
|
57
|
+
"patch": "./plugin/cordis.patch.yml"
|
|
58
|
+
},
|
|
59
|
+
"client": {
|
|
60
|
+
"platform": "web",
|
|
61
|
+
"immediately": true,
|
|
62
|
+
"inject": [
|
|
63
|
+
"@deepseek-ai/dsh-client-ui-sidebar",
|
|
64
|
+
"@deepseek-ai/dsh-client-ui-layout"
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// 添加 agent 弹层(R25 批 1 第二个组件)。
|
|
2
|
+
// 契约见 page/shared.js 头部;顶层只许有那一处 AACTA 赋值,Vue / Element Plus 的 API 一律写全名。
|
|
3
|
+
//
|
|
4
|
+
// 数据进出:
|
|
5
|
+
// · visible ← 根(侧栏 agent 列表右上角的「+」)
|
|
6
|
+
// · getJSON ← 根 provide(POST 也要它:404 得能点亮版本红条,口径只有根里一份)
|
|
7
|
+
// · 添加成功后 emit('added') —— 重新拉 agents 与快照是根的活儿(那是根的 all/agents 状态),
|
|
8
|
+
// 组件不碰:它只负责「把这一条 POST 出去、把失败原因摆出来」
|
|
9
|
+
window.AACTA = window.AACTA || {};
|
|
10
|
+
AACTA.DialogAddAgent = {
|
|
11
|
+
name: 'aa-dialog-add-agent',
|
|
12
|
+
template: `
|
|
13
|
+
<el-dialog :model-value="visible" @update:model-value="v => $emit('update:visible', v)" title="添加 agent" width="460px" append-to-body>
|
|
14
|
+
<el-form label-position="top" @submit.prevent>
|
|
15
|
+
<el-form-item label="Agent 名称">
|
|
16
|
+
<el-input v-model="addForm.name" placeholder="如 claude、trae、cursor…" @keyup.enter="submitAdd"></el-input>
|
|
17
|
+
<div class="form-tip">留空路径时按平台自动探测:~/.名称(全平台)、%APPDATA%(Win)、Application Support(mac)、~/.config(Linux/HarmonyOS)</div>
|
|
18
|
+
<div class="form-tip">已知 agent(claude / codex / cursor / trae / traework / qoder / opencode / kilo / gemini / copilot / windsurf / codearts / kimi / dsh / zcode / doubao / hermes / devin / minimax / mimocode / openclaw / comate / cline)服务启动时会自动发现,无需手动添加。</div>
|
|
19
|
+
<div class="form-tip">codebuddy 一家有三份落盘,<b>侧栏只有一行 codebuddy</b>:CLI 的 ~/.codebuddy/projects 与 genie 扩展(VSCode / JetBrains / CodeBuddy CN 应用共用)的 %LOCALAPPDATA%\CodeBuddyExtension\Data 都并到这一行;%APPDATA%\CodeBuddy CN 那个目录只有 IDE 缓存、没有会话正文。</div>
|
|
20
|
+
<div class="form-tip">qwen / hunyuan / grok 目前<b>只有图标、没有解析器</b>:不会被自动发现,手动添加也会因识别不到日志结构而失败(R16 遗留)。</div>
|
|
21
|
+
</el-form-item>
|
|
22
|
+
<el-form-item label="日志路径(可选)">
|
|
23
|
+
<el-input v-model="addForm.path" placeholder="手动指定根目录,或 sessions / projects 目录" @keyup.enter="submitAdd"></el-input>
|
|
24
|
+
</el-form-item>
|
|
25
|
+
<el-alert v-if="addError" :title="addError" type="error" :closable="false" show-icon></el-alert>
|
|
26
|
+
<div v-if="addTried.length" class="tried">
|
|
27
|
+
<div class="tried-t">逐条试过的路径与各自的原因(可整段复制给帮你排查的人):</div>
|
|
28
|
+
<div v-for="t in addTried" :key="t" class="tried-p">{{ t }}</div>
|
|
29
|
+
</div>
|
|
30
|
+
</el-form>
|
|
31
|
+
<template #footer>
|
|
32
|
+
<el-button @click="$emit('update:visible', false)">取消</el-button>
|
|
33
|
+
<el-button type="primary" :loading="addLoading" @click="submitAdd">添加</el-button>
|
|
34
|
+
</template>
|
|
35
|
+
</el-dialog>`,
|
|
36
|
+
props: {
|
|
37
|
+
visible: Boolean,
|
|
38
|
+
},
|
|
39
|
+
emits: ['update:visible', 'added'],
|
|
40
|
+
setup(props, { emit }) {
|
|
41
|
+
const getJSON = Vue.inject('getJSON');
|
|
42
|
+
const addForm = Vue.reactive({ name: '', path: '' });
|
|
43
|
+
const addError = Vue.ref('');
|
|
44
|
+
const addTried = Vue.ref([]);
|
|
45
|
+
const addLoading = Vue.ref(false);
|
|
46
|
+
async function submitAdd() {
|
|
47
|
+
if (!addForm.name.trim()) { addError.value = '请填写 agent 名称'; return; }
|
|
48
|
+
addError.value = ''; addTried.value = []; addLoading.value = true;
|
|
49
|
+
try {
|
|
50
|
+
const j = await getJSON('/api/agents', {
|
|
51
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
52
|
+
body: JSON.stringify({ name: addForm.name, path: addForm.path || undefined }),
|
|
53
|
+
});
|
|
54
|
+
if (!j.ok) { addError.value = j.error || '添加失败'; addTried.value = j.tried || []; return; }
|
|
55
|
+
ElementPlus.ElMessage.success('已添加 ' + j.name + '(' + j.kind + ' 格式 · ' + (j.sessions || j.traces) + ')');
|
|
56
|
+
emit('update:visible', false);
|
|
57
|
+
emit('added'); // 拉新数据是根的活儿(改前是这儿直接调 loadAgents/loadSnapshot)
|
|
58
|
+
} catch (e) { addError.value = e.message; }
|
|
59
|
+
finally { addLoading.value = false; }
|
|
60
|
+
}
|
|
61
|
+
// 每次打开都是干净的表单(改前由 openAdd 重置;现在开与关都在根,重置挂在这儿)
|
|
62
|
+
Vue.watch(() => props.visible, v => { if (v) { addForm.name = ''; addForm.path = ''; addError.value = ''; addTried.value = []; } });
|
|
63
|
+
return { addForm, addError, addTried, addLoading, submitAdd };
|
|
64
|
+
}
|
|
65
|
+
};
|