@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,202 @@
|
|
|
1
|
+
// 「解析器自检」弹层(R32 / I5):把上一次 `node test/selftest.mjs` 的结果摊开给人看。
|
|
2
|
+
// 契约见 page/shared.js 头部的「R25 组件文件契约」;Vue 的 API 一律写全名,不解构。
|
|
3
|
+
//
|
|
4
|
+
// 数据进出(与 dialog-usage / dialog-diag 同法:开关归根、数据归组件):
|
|
5
|
+
// · visible ← 根(侧栏「解析器自检」项点开 / 弹层关掉时回写)
|
|
6
|
+
// · getJSON ← 根 provide(接口 404 要点亮顶部版本红条,那段逻辑只有根里一份)
|
|
7
|
+
// · 自己拉 /api/selftest(服务端只读 ~/.agent-acta/selftest.json,不 spawn 任何测试)
|
|
8
|
+
//
|
|
9
|
+
// 为什么这里**没有「跑一遍」按钮**(用户 2026-09-22 定的两条口径):
|
|
10
|
+
// 1) 一轮默认清单要串行起停二十来个服务、跑两分钟,副作用挂在一个弹层上 = 谁都不敢刷新;
|
|
11
|
+
// 跑的动作留在终端里,由人决定什么时候花这两分钟。
|
|
12
|
+
// 2) 也不在这儿「拿真日志重解析一遍」—— 那是 `agentacta --doctor` 的活(环境诊断弹层里已经接了)。
|
|
13
|
+
// 这一页保持纯回归结果,不掺真机数据,两边才各自能说清自己在回答什么问题。
|
|
14
|
+
//
|
|
15
|
+
// 三处口径要留意(都是刻意定的,页面只是别把它藏起来):
|
|
16
|
+
// · sameBuild=false 必须显式警告:结果出自另一版代码,一页绿字如果不代表当前这版,比没有自检更危险。
|
|
17
|
+
// · 「这次没跑的」要跟数量与理由(skipped[].why / groups[].note):默认清单只有 23 条,
|
|
18
|
+
// 剩下 13 条要真机数据 / 要 dist 包 / 靶子是常驻服务 —— 不写清楚,页面就显得「测试全过了」。
|
|
19
|
+
// · bench / probe 那几个连失败概念都没有(找不到目录也退 0),永久排除:一个永远绿的检查比没有检查更糟。
|
|
20
|
+
window.AACTA = window.AACTA || {};
|
|
21
|
+
AACTA.DialogSelftest = {
|
|
22
|
+
name: 'aa-dialog-selftest',
|
|
23
|
+
template: `
|
|
24
|
+
<el-dialog :model-value="visible" @update:model-value="v => $emit('update:visible', v)" title="解析器自检" width="min(940px, 95vw)" append-to-body class="sf-dialog">
|
|
25
|
+
<div class="sf-hint">
|
|
26
|
+
这里看的是<b>上一次回归跑的结果</b>:改完解析口径会不会弄坏邻居,不用挨个敲那二十几个脚本。
|
|
27
|
+
这一页<b>只读、不跑测试</b> —— 一轮要串行起停二十来个服务、跑两分钟,得由你在终端里决定什么时候花这两分钟:
|
|
28
|
+
<code>node test/selftest.mjs</code>(<code>--only</code> 挑子集、<code>--group</code> 挑一组、<code>--all</code> 连默认不跑的一起跑)。
|
|
29
|
+
</div>
|
|
30
|
+
<el-skeleton v-if="sf.loading && !sf.j" :rows="6" animated></el-skeleton>
|
|
31
|
+
<div v-else-if="sf.error" class="sf-err">
|
|
32
|
+
<div class="sf-errmsg">{{ sf.error }}</div>
|
|
33
|
+
<el-button size="small" :loading="sf.loading" @click="run()">重试</el-button>
|
|
34
|
+
</div>
|
|
35
|
+
<div v-else-if="!sf.j || !sf.j.ok" class="sf-none">
|
|
36
|
+
<div class="sf-none-t">{{ sf.j && sf.j.error ? sf.j.error : '拿不到结果' }}</div>
|
|
37
|
+
<div class="sf-none-b">
|
|
38
|
+
在仓库的 <code>agent-acta/</code> 目录下跑一次就有内容了(结果写进
|
|
39
|
+
<code>{{ (sf.j && sf.j.file) || '~/.agent-acta/selftest.json' }}</code>,服务只读它):
|
|
40
|
+
</div>
|
|
41
|
+
<div class="sf-cmd">
|
|
42
|
+
<code>node test/selftest.mjs</code>
|
|
43
|
+
<el-button size="small" text @click="copyRaw($event, 'node test/selftest.mjs')">复制</el-button>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<div v-else>
|
|
47
|
+
<!-- build 不同 = 这份绿不代表你现在看的这版代码。它必须排在汇总之前,不然人会先看到「全绿」。 -->
|
|
48
|
+
<div v-if="!sf.j.sameBuild" class="sf-warn">
|
|
49
|
+
这份结果出自 <code>{{ revOf(sf.j.self) }}</code>,当前服务是 <code>{{ revOf(sf.j.server) }}</code>。
|
|
50
|
+
<b>中间改过代码</b>,这一页的绿不代表现在这一版 ——
|
|
51
|
+
要判断这一版,请在终端重跑一次 <code>node test/selftest.mjs</code>。
|
|
52
|
+
<template v-if="sf.j.sameParserRev">(解析器口径倒是一致:改的大概不是解析那层。)</template>
|
|
53
|
+
</div>
|
|
54
|
+
<div v-else-if="!sf.j.sameParserRev" class="sf-warn">
|
|
55
|
+
代码指纹对得上,但<b>解析器口径与结果里那份不一致</b>(各 kind 的 rev 有出入)——
|
|
56
|
+
这份结果不足以说明现在的口径是对的,请重跑。
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div class="sf-sum" :class="{bad: !allGreen}">
|
|
60
|
+
<span class="sf-verdict">
|
|
61
|
+
<i class="sf-vico" :class="allGreen ? 'pass' : 'fail'">{{ allGreen ? '✓' : '✗' }}</i>
|
|
62
|
+
<template v-if="allGreen">{{ sf.j.summary.total }} 项全绿</template>
|
|
63
|
+
<template v-else>{{ bad }} 项不绿 <i>/ {{ sf.j.summary.total }}</i></template>
|
|
64
|
+
</span><i>·</i>
|
|
65
|
+
<span>用时 <b>{{ dur(sf.j.durationMs) }}</b></span><i>·</i>
|
|
66
|
+
<span>{{ ageText }}</span><i>·</i>
|
|
67
|
+
<span>{{ sf.j.rows.length }} 跑过 / {{ sf.j.skipped.length }} 没跑</span>
|
|
68
|
+
<div class="sf-sum-2">
|
|
69
|
+
跑它时 {{ sf.j.self.version || '?' }} / 指纹 <code>{{ sf.j.self.build || '?' }}</code> ·
|
|
70
|
+
这台机器 node {{ sf.j.env.node }} / {{ sf.j.env.platform }} ·
|
|
71
|
+
不绿 {{ sf.j.summary.fail }}、超时 {{ sf.j.summary.timeout }}、runner 没收住 {{ sf.j.summary.error }}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<!-- 分组表来自 runner;万一某行的 group 不在表里(旧结果 / 手改过的文件),它得落在「未分组」里,
|
|
76
|
+
而不是跟着找不到分组的循环被静默跳过 —— 少几行还报「全绿」是这一页最不该犯的错。 -->
|
|
77
|
+
<template v-for="sec in sections" :key="sec.id">
|
|
78
|
+
<div class="sf-cap">
|
|
79
|
+
{{ sec.label }}
|
|
80
|
+
<span class="sf-cap-note">{{ sec.rows.length }} 项 · {{ dur(sec.ms) }}</span>
|
|
81
|
+
<span v-if="sec.bad" class="sf-tag bad">{{ sec.bad }} 项不绿</span>
|
|
82
|
+
</div>
|
|
83
|
+
<div class="sf-list">
|
|
84
|
+
<div v-for="r in sec.rows" :key="r.file" class="sf-row" :class="r.status">
|
|
85
|
+
<span class="sf-st">{{ iconOf(r.status) }}</span>
|
|
86
|
+
<span class="sf-nm">
|
|
87
|
+
<span class="sf-title">{{ r.title }}</span>
|
|
88
|
+
<span class="sf-file">{{ r.file }}</span>
|
|
89
|
+
<span v-if="r.status !== 'pass'" class="sf-tag" :class="r.status">{{ statusText(r.status) }}</span>
|
|
90
|
+
</span>
|
|
91
|
+
<span class="sf-ms">{{ dur(r.ms) }}</span>
|
|
92
|
+
<div v-if="r.status !== 'pass'" class="sf-detail">
|
|
93
|
+
<div v-if="r.tail" class="sf-tail">{{ r.tail }}</div>
|
|
94
|
+
<pre v-if="r.reds && r.reds.length" class="sf-reds">{{ r.reds.join('\\n') }}</pre>
|
|
95
|
+
<div v-if="r.note" class="sf-tail">这一类默认不进清单:{{ r.note }}</div>
|
|
96
|
+
<div v-if="!r.tail && !(r.reds && r.reds.length)" class="sf-tail">(这个脚本没留下可读的失败行,退出码 {{ r.code }};去终端跑它看全文)</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</template>
|
|
101
|
+
|
|
102
|
+
<div v-if="sf.j.skipped.length" class="sf-cap">
|
|
103
|
+
这次没跑的 <span class="sf-cap-note">{{ sf.j.skipped.length }} 项 —— 不是漏了,是默认清单故意不收;每一类为什么不收,runner 随结果一起写了理由</span>
|
|
104
|
+
</div>
|
|
105
|
+
<div v-if="sf.j.skipped.length" class="sf-skip">
|
|
106
|
+
<div v-for="g in skipSections" :key="g.id" class="sf-skip-g">
|
|
107
|
+
<div class="sf-skip-h">{{ g.label }}<span v-if="g.note" class="sf-skip-note">{{ g.note }}</span></div>
|
|
108
|
+
<div v-for="s in g.items" :key="s.file" class="sf-skip-i">
|
|
109
|
+
<code>{{ s.file }}</code><span>{{ s.title }}</span><em>{{ s.why }}</em>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
<template #footer>
|
|
115
|
+
<span class="sf-foot">这一页只读上次结果(服务端不 spawn 测试子进程);要跑请在终端敲上面那条命令。
|
|
116
|
+
结果文件 <code>{{ (sf.j && sf.j.file) || '~/.agent-acta/selftest.json' }}</code> 跟着 HOME 走,
|
|
117
|
+
所以源码树里跑的 runner 和常驻服务读到的是同一份。</span>
|
|
118
|
+
<el-button @click="$emit('update:visible', false)">关闭</el-button>
|
|
119
|
+
<el-button type="primary" :loading="sf.loading" @click="run()">重新读取</el-button>
|
|
120
|
+
</template>
|
|
121
|
+
</el-dialog>`,
|
|
122
|
+
props: { visible: Boolean },
|
|
123
|
+
emits: ['update:visible'],
|
|
124
|
+
setup(props) {
|
|
125
|
+
const getJSON = Vue.inject('getJSON');
|
|
126
|
+
const sf = Vue.reactive({ loading: false, error: '', j: null });
|
|
127
|
+
// 归一化:结果文件是人也能手工碰的东西(runner 正常写出的一定全,缺字段的旧文件/改坏的文件也可能有)。
|
|
128
|
+
// 缺什么就按行本身现算,而不是让模板读到 undefined 整片空白 —— 汇总说「全绿」而实际少了几行,
|
|
129
|
+
// 是这一页最不能犯的错。
|
|
130
|
+
function norm(j) {
|
|
131
|
+
const rows = Array.isArray(j.rows) ? j.rows : [];
|
|
132
|
+
const st = k => rows.filter(r => r.status === k).length;
|
|
133
|
+
j.rows = rows; j.skipped = Array.isArray(j.skipped) ? j.skipped : []; j.groups = Array.isArray(j.groups) ? j.groups : [];
|
|
134
|
+
j.self = j.self || {}; j.env = j.env || {};
|
|
135
|
+
j.summary = j.summary || {};
|
|
136
|
+
j.summary.total = Number(j.summary.total) || rows.length;
|
|
137
|
+
j.summary.pass = st('pass');
|
|
138
|
+
j.summary.fail = st('fail'); j.summary.timeout = st('timeout'); j.summary.error = st('error');
|
|
139
|
+
return j;
|
|
140
|
+
}
|
|
141
|
+
async function run() {
|
|
142
|
+
if (sf.loading) return;
|
|
143
|
+
sf.loading = true;
|
|
144
|
+
try {
|
|
145
|
+
const j = await getJSON('/api/selftest');
|
|
146
|
+
sf.j = norm(j); sf.error = '';
|
|
147
|
+
} catch (e) { sf.error = '自检结果读取失败:' + e.message; }
|
|
148
|
+
finally { sf.loading = false; }
|
|
149
|
+
}
|
|
150
|
+
// 点开才读(一次文件读取,不缓存:每次都想要最新的年龄)
|
|
151
|
+
Vue.watch(() => props.visible, v => { if (v) run(); });
|
|
152
|
+
|
|
153
|
+
// 分组渲染统一走「组 → 行」两张表:先按 runner 给的分组表排,表外那些(旧结果里没这个 id、
|
|
154
|
+
// 或分组表整个没带)兜到「未分组」一节,绝不因为对不上号就少几行。
|
|
155
|
+
const bucketize = (groups, items, groupOf) => {
|
|
156
|
+
const gs = Array.isArray(groups) ? groups : [];
|
|
157
|
+
const known = new Set(gs.map(g => g.id));
|
|
158
|
+
const out = gs.map(g => ({ id: g.id, label: g.label || g.id, note: g.note, items: items.filter(x => groupOf(x) === g.id) }));
|
|
159
|
+
const rest = items.filter(x => !known.has(groupOf(x)));
|
|
160
|
+
if (rest.length) out.push({ id: '__rest', label: '未分组(结果里的 group 对不上分组表)', note: '', items: rest });
|
|
161
|
+
return out.filter(x => x.items.length);
|
|
162
|
+
};
|
|
163
|
+
// 没跑的那批按「为什么这一类不收」成组显示:分组表是 runner 随结果一起发出来的,这里不另立名单
|
|
164
|
+
const skipSections = Vue.computed(() => bucketize(sf.j && sf.j.groups, (sf.j && sf.j.skipped) || [], s => s.group));
|
|
165
|
+
const sections = Vue.computed(() => {
|
|
166
|
+
const j = sf.j; if (!j) return [];
|
|
167
|
+
// 行的 label 用分组表的;一行的 group 缺席时也照样出现(见 bucketize 的兜底)
|
|
168
|
+
return bucketize(j.groups, j.rows, r => r.group).map(sec => {
|
|
169
|
+
const rows = sec.items;
|
|
170
|
+
return { id: sec.id, label: sec.label, rows,
|
|
171
|
+
ms: rows.reduce((s, r) => s + (r.ms || 0), 0),
|
|
172
|
+
bad: rows.filter(r => r.status !== 'pass').length };
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
const allGreen = Vue.computed(() => !!sf.j && !!sf.j.rows.length
|
|
176
|
+
&& sf.j.summary.fail === 0 && sf.j.summary.timeout === 0 && sf.j.summary.error === 0
|
|
177
|
+
&& sf.j.rows.every(r => r.status === 'pass'));
|
|
178
|
+
const bad = Vue.computed(() => (sf.j ? sf.j.summary.total - sf.j.summary.pass : 0));
|
|
179
|
+
const ageText = Vue.computed(() => {
|
|
180
|
+
if (!sf.j) return '—';
|
|
181
|
+
const a = sf.j.ageMs || 0;
|
|
182
|
+
// 年龄超过一天就别报「x 天前」了事:自检是「改完之后跑」的东西,隔天以上本身就说明它旧
|
|
183
|
+
const t = a < 60000 ? '刚刚' : a < 3600000 ? Math.floor(a / 60000) + ' 分钟前'
|
|
184
|
+
: a < 86400000 ? Math.floor(a / 3600000) + ' 小时前' : Math.floor(a / 86400000) + ' 天前';
|
|
185
|
+
return '跑完于 ' + t + '(' + fmtTime(Date.parse(sf.j.at) || Date.now() - a) + ')';
|
|
186
|
+
});
|
|
187
|
+
// 与 runner 里那个 fmtMs 同口径(先四舍五入到整秒再拆分):直接用页面的 fmtDur 会算出「1 分 60 秒」
|
|
188
|
+
const dur = n => {
|
|
189
|
+
const ms = Number(n) || 0;
|
|
190
|
+
if (ms < 1000) return ms + ' ms';
|
|
191
|
+
const tot = Math.round(ms / 1000);
|
|
192
|
+
if (tot < 60) return (ms / 1000).toFixed(1) + ' 秒';
|
|
193
|
+
const m = Math.floor(tot / 60), s = tot % 60;
|
|
194
|
+
return m + ' 分' + (s ? ' ' + s + ' 秒' : '');
|
|
195
|
+
};
|
|
196
|
+
const revOf = o => o && o.build ? o.version + ' / ' + String(o.build).slice(0, 8) : '未知版本';
|
|
197
|
+
const iconOf = st => ({ pass: '✓', fail: '✗', timeout: '✗', error: '!' })[st] || '?';
|
|
198
|
+
const statusText = st => ({ fail: '断言红', timeout: '超时', error: 'runner 没收住' })[st] || st;
|
|
199
|
+
// copyRaw 是 page/shared.js 的顶层函数(与根模板用的是同一份),这里只是让模板能看见它
|
|
200
|
+
return { sf, run, sections, skipSections, allGreen, bad, ageText, dur, revOf, iconOf, statusText, copyRaw };
|
|
201
|
+
}
|
|
202
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// 会话时间线弹层(R25 批 1 第四个组件)。
|
|
2
|
+
// 契约见 page/shared.js 头部「R25 组件文件契约」;顶层只许有那一处 AACTA 赋值,Vue / Element Plus 的 API 写全名。
|
|
3
|
+
//
|
|
4
|
+
// 数据进出 —— 与上下文窗口弹层同理:**轮次数据不算根的**。
|
|
5
|
+
// 改前 sessLoading / sessData / sessExpanded / sessDetails 都摊在根里,但只有这个弹层用;
|
|
6
|
+
// 根的卡片页脚只负责「开哪一个」,所以定成:
|
|
7
|
+
// · visible ← 根(卡片页脚 openSession / 弹层关闭时回写)
|
|
8
|
+
// · req ← 根:{ key, agent, project },点一次卡片页脚递一份(key 为空时根本就没开)
|
|
9
|
+
// · getJSON ← 根 provide(404 要点亮版本红条)
|
|
10
|
+
// · 拉数据、展开态、逐轮详情缓存 —— 都是弹层自己的
|
|
11
|
+
// 轮次正文仍走 /api/entry 懒加载:点开哪轮才取哪轮,大会话不一次读爆(与改前一致)。
|
|
12
|
+
window.AACTA = window.AACTA || {};
|
|
13
|
+
AACTA.DialogSessTimeline = {
|
|
14
|
+
name: 'aa-dialog-sess-timeline',
|
|
15
|
+
template: `
|
|
16
|
+
<el-dialog :model-value="visible" @update:model-value="v => $emit('update:visible', v)" title="会话时间线" width="min(860px, 94vw)" append-to-body class="sess-dialog">
|
|
17
|
+
<div v-if="sessLoading" v-loading="true" style="min-height:200px"></div>
|
|
18
|
+
<template v-else-if="sessData">
|
|
19
|
+
<div class="sess-sum">
|
|
20
|
+
<span><b>{{ sessData.count }}</b> 轮</span>
|
|
21
|
+
<span>↑{{ fmtN(sessData.tin) }} · ↓{{ fmtN(sessData.tout) }} · 缓存 {{ fmtN(sessData.tcache) }}</span>
|
|
22
|
+
<span>合计 {{ fmtDurBig(sessData.dur) }}</span>
|
|
23
|
+
<span>{{ fmtTime(sessData.from) }} ~ {{ fmtTime(sessData.to) }}</span>
|
|
24
|
+
<span v-if="sessData.models.length" class="sess-models">{{ sessData.models.join('、') }}</span>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="sess-list">
|
|
27
|
+
<div v-for="(t, i) in sessData.entries" :key="t.id" class="sess-turn" :class="{err: t.status !== 'ok'}">
|
|
28
|
+
<div class="sess-turn-head" @click="sessToggle(t.id)">
|
|
29
|
+
<span class="sn">#{{ i + 1 }}</span>
|
|
30
|
+
<span class="st-preview">{{ t.preview || '(无文本预览)' }}</span>
|
|
31
|
+
<span v-if="t.status !== 'ok'" class="tag tag-err">ERR</span>
|
|
32
|
+
<span class="sd">{{ fmtDur(t.dur) }}</span>
|
|
33
|
+
<span class="st-time">{{ fmtTime(t.time) }}</span>
|
|
34
|
+
<span class="sess-arrow">{{ sessExpanded.has(t.id) ? '▾' : '▸' }}</span>
|
|
35
|
+
</div>
|
|
36
|
+
<div v-if="sessExpanded.has(t.id)" class="sess-turn-body" @click.stop>
|
|
37
|
+
<template v-if="sessDetailOf(t.id)">
|
|
38
|
+
<h4>用户输入 <span class="copy-btn" @click="copyRaw($event, sessDetailOf(t.id).user)">复制</span></h4>
|
|
39
|
+
<pre>{{ sessDetailOf(t.id).user || '(空)' }}</pre>
|
|
40
|
+
<template v-if="sessDetailOf(t.id).assistant">
|
|
41
|
+
<h4>AI 输出 <span class="copy-btn" @click="copyRaw($event, sessDetailOf(t.id).assistant)">复制</span></h4>
|
|
42
|
+
<div class="md-body" v-html="sessDetailOf(t.id)._assistantMd"></div>
|
|
43
|
+
</template>
|
|
44
|
+
<template v-if="sessDetailOf(t.id).tools && sessDetailOf(t.id).tools.length">
|
|
45
|
+
<h4>工具调用({{ sessDetailOf(t.id).tools.length }})</h4>
|
|
46
|
+
<div v-for="(t2, j) in sessDetailOf(t.id).tools" :key="j" class="tool-item">
|
|
47
|
+
<div class="tool-head"><span class="tname">{{ t2.name }}</span><span v-if="t2.error" class="terr">✗</span></div>
|
|
48
|
+
<pre v-if="t2.input">{{ prettyJson(t2.input) }}</pre>
|
|
49
|
+
<pre v-if="t2.output">{{ prettyJson(t2.output) }}</pre>
|
|
50
|
+
</div>
|
|
51
|
+
</template>
|
|
52
|
+
</template>
|
|
53
|
+
<el-skeleton v-else :rows="2" animated></el-skeleton>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</template>
|
|
58
|
+
<div v-else class="empty">会话数据不可用</div>
|
|
59
|
+
<template #footer>
|
|
60
|
+
<el-button @click="$emit('update:visible', false)">关闭</el-button>
|
|
61
|
+
</template>
|
|
62
|
+
</el-dialog>`,
|
|
63
|
+
props: {
|
|
64
|
+
visible: Boolean,
|
|
65
|
+
// 「开哪一个」:{ key, agent, project }。agent/project 是**可选**的精确化参数 —— session 值跨 agent、
|
|
66
|
+
// 跨项目都不保证唯一(各解析器口径不同,本机实测 atomcode 就有不同项目下的同名 session)。
|
|
67
|
+
// 不传 == 老行为(只按 key 匹配)。project 传原始写法即可:服务端按 projKey 两边归一后比。
|
|
68
|
+
req: { type: Object, default: null },
|
|
69
|
+
},
|
|
70
|
+
emits: ['update:visible'],
|
|
71
|
+
setup(props) {
|
|
72
|
+
const getJSON = Vue.inject('getJSON');
|
|
73
|
+
const sessLoading = Vue.ref(false);
|
|
74
|
+
const sessData = Vue.ref(null); // /api/session 的返回:{count, tin..., from, to, models, entries[]}
|
|
75
|
+
const sessExpanded = Vue.ref(new Set()); // 已展开的轮次 id(点开才拉 /api/entry)
|
|
76
|
+
const sessDetails = Vue.ref({}); // id -> 详情(同 detailOf 的缓存结构,独立一份避免互相污染)
|
|
77
|
+
async function load() {
|
|
78
|
+
const r = props.req || {};
|
|
79
|
+
sessLoading.value = true; sessData.value = null;
|
|
80
|
+
sessExpanded.value = new Set(); sessDetails.value = {};
|
|
81
|
+
try {
|
|
82
|
+
const j = await getJSON('/api/session?key=' + encodeURIComponent(r.key)
|
|
83
|
+
+ (r.agent ? '&agent=' + encodeURIComponent(r.agent) : '')
|
|
84
|
+
+ (r.project ? '&project=' + encodeURIComponent(r.project) : ''));
|
|
85
|
+
sessData.value = j.ok ? j : null;
|
|
86
|
+
if (!j.ok) ElementPlus.ElMessage.error(j.error || '会话加载失败');
|
|
87
|
+
} catch (e) { ElementPlus.ElMessage.error('会话加载失败:' + e.message); }
|
|
88
|
+
finally { sessLoading.value = false; }
|
|
89
|
+
}
|
|
90
|
+
function sessDetailOf(id) { return sessDetails.value[id] || null; }
|
|
91
|
+
async function sessToggle(id) {
|
|
92
|
+
const s = new Set(sessExpanded.value);
|
|
93
|
+
if (s.has(id)) { s.delete(id); sessExpanded.value = s; return; }
|
|
94
|
+
s.add(id); sessExpanded.value = s;
|
|
95
|
+
if (!sessDetails.value[id]) {
|
|
96
|
+
try { sessDetails.value = { ...sessDetails.value, [id]: prepDetail(await getJSON('/api/entry?id=' + encodeURIComponent(id))) }; }
|
|
97
|
+
catch (e) { ElementPlus.ElMessage.error('详情加载失败:' + e.message); }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// 点开卡片页脚才拉(改前是 openSession 顺手 fetch;现在根只置 visible + 递 req)
|
|
101
|
+
Vue.watch(() => props.visible, v => { if (v) load(); });
|
|
102
|
+
// 模板只能看见 setup return 里的键:shared.js 里的格式化函数虽然在本文件的作用域里(经典 script 全局),
|
|
103
|
+
// 但模板编译成 with(_ctx) 后只认 props / return / 已注册组件 / JS 内建(实测 Math、Date、console 能命中,
|
|
104
|
+
// document、window、ElementPlus、fmtN 全是 undefined)。所以模板要用的这几只必须**列进 return**,
|
|
105
|
+
// 否则渲染期抛「fmtN is not a function」,弹层直接打不开。
|
|
106
|
+
return { sessLoading, sessData, sessExpanded, sessDetailOf, sessToggle, fmtN, fmtDur, fmtDurBig, fmtTime, copyRaw, prettyJson };
|
|
107
|
+
}
|
|
108
|
+
};
|