@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.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/LOGFORMATS.md +960 -0
  3. package/README.md +238 -0
  4. package/REFERENCE.md +1428 -0
  5. package/agent-acta-page.html +1998 -0
  6. package/agent-acta-server.mjs +1066 -0
  7. package/agent-acta-widget.html +724 -0
  8. package/archive.mjs +335 -0
  9. package/core/log.mjs +20 -0
  10. package/core/service.mjs +4285 -0
  11. package/hooks/cursor-usage-hook.mjs +89 -0
  12. package/mcp-server.mjs +372 -0
  13. package/package.json +68 -0
  14. package/page/dialog-add-agent.js +65 -0
  15. package/page/dialog-alert.js +159 -0
  16. package/page/dialog-archive.js +342 -0
  17. package/page/dialog-compare.js +238 -0
  18. package/page/dialog-ctxw.js +101 -0
  19. package/page/dialog-diag.js +72 -0
  20. package/page/dialog-heatmap.js +215 -0
  21. package/page/dialog-selftest.js +202 -0
  22. package/page/dialog-sess-timeline.js +108 -0
  23. package/page/dialog-stats.js +694 -0
  24. package/page/dialog-usage.js +158 -0
  25. package/page/log-card.js +277 -0
  26. package/page/log-view.js +273 -0
  27. package/page/sess-view.js +382 -0
  28. package/page/shared.js +408 -0
  29. package/page/sidebar.js +129 -0
  30. package/page/style.css +973 -0
  31. package/page/topbar.js +61 -0
  32. package/parsers/atomcode.mjs +321 -0
  33. package/parsers/buddy.mjs +272 -0
  34. package/parsers/buddyext.mjs +582 -0
  35. package/parsers/claude.mjs +496 -0
  36. package/parsers/cline.mjs +468 -0
  37. package/parsers/codearts.mjs +96 -0
  38. package/parsers/codex.mjs +188 -0
  39. package/parsers/comate.mjs +195 -0
  40. package/parsers/copilot.mjs +308 -0
  41. package/parsers/cursor.mjs +493 -0
  42. package/parsers/devindb.mjs +358 -0
  43. package/parsers/discovery.mjs +1230 -0
  44. package/parsers/doubao.mjs +406 -0
  45. package/parsers/dsh.mjs +377 -0
  46. package/parsers/gemini.mjs +267 -0
  47. package/parsers/generic.mjs +494 -0
  48. package/parsers/hermes.mjs +398 -0
  49. package/parsers/kilo.mjs +42 -0
  50. package/parsers/kimi.mjs +249 -0
  51. package/parsers/mimocode.mjs +531 -0
  52. package/parsers/minimax.mjs +503 -0
  53. package/parsers/openclaw.mjs +490 -0
  54. package/parsers/opencode.mjs +465 -0
  55. package/parsers/shared.mjs +418 -0
  56. package/parsers/trace.mjs +66 -0
  57. package/parsers/tracecode.mjs +348 -0
  58. package/parsers/traedb.mjs +528 -0
  59. package/parsers/zcode.mjs +356 -0
  60. package/plugin/client.js +51 -0
  61. package/plugin/cordis.patch.yml +5 -0
  62. package/plugin/plugin.mjs +220 -0
  63. package/search.mjs +554 -0
  64. package/tools/grab-trae-key.ps1 +265 -0
  65. package/vendor/element-plus-icons.js +3 -0
  66. package/vendor/element-plus.css +1 -0
  67. package/vendor/element-plus.dark.css +1 -0
  68. package/vendor/element-plus.full.min.js +65 -0
  69. package/vendor/element-plus.zh-cn.js +2 -0
  70. package/vendor/svg/antgroup-color.svg +1 -0
  71. package/vendor/svg/bytedance-color.svg +1 -0
  72. package/vendor/svg/chatglm-color.svg +1 -0
  73. package/vendor/svg/claude-color.svg +1 -0
  74. package/vendor/svg/claudecode-color.svg +1 -0
  75. package/vendor/svg/cline.svg +1 -0
  76. package/vendor/svg/codebuddy-color.svg +1 -0
  77. package/vendor/svg/codebuddy.svg +1 -0
  78. package/vendor/svg/codex-color.svg +1 -0
  79. package/vendor/svg/copilot.svg +1 -0
  80. package/vendor/svg/cursor.svg +1 -0
  81. package/vendor/svg/deepseek-color.svg +1 -0
  82. package/vendor/svg/deepseek-harness-whale.svg +8 -0
  83. package/vendor/svg/doubao-color.svg +1 -0
  84. package/vendor/svg/gemini-color.svg +1 -0
  85. package/vendor/svg/glmv-color.svg +1 -0
  86. package/vendor/svg/grok.svg +1 -0
  87. package/vendor/svg/hermesagent.svg +1 -0
  88. package/vendor/svg/huawei-color.svg +1 -0
  89. package/vendor/svg/hunyuan-color.svg +1 -0
  90. package/vendor/svg/kilocode-avatar.svg +1 -0
  91. package/vendor/svg/kilocode.svg +1 -0
  92. package/vendor/svg/kimi-color.svg +1 -0
  93. package/vendor/svg/kimi.svg +1 -0
  94. package/vendor/svg/logo-AtomGit-G-red.svg +3 -0
  95. package/vendor/svg/logo-AtomGit-dark.svg +4 -0
  96. package/vendor/svg/logo-AtomGit-light.svg +4 -0
  97. package/vendor/svg/meta-color.svg +1 -0
  98. package/vendor/svg/mimo.svg +66 -0
  99. package/vendor/svg/minimax-code-logo.svg +8 -0
  100. package/vendor/svg/minimax-color.svg +1 -0
  101. package/vendor/svg/moonshot.svg +1 -0
  102. package/vendor/svg/nex-color.svg +1 -0
  103. package/vendor/svg/nex.svg +1 -0
  104. package/vendor/svg/nvidia-color.svg +1 -0
  105. package/vendor/svg/openai.svg +1 -0
  106. package/vendor/svg/openclaw-color.svg +1 -0
  107. package/vendor/svg/openclaw.svg +1 -0
  108. package/vendor/svg/opencode.svg +1 -0
  109. package/vendor/svg/openrouter-color.svg +1 -0
  110. package/vendor/svg/qoder-color.svg +1 -0
  111. package/vendor/svg/qwen-color.svg +1 -0
  112. package/vendor/svg/sensenova-color.svg +1 -0
  113. package/vendor/svg/stepfun.svg +1 -0
  114. package/vendor/svg/trae-color.svg +1 -0
  115. package/vendor/svg/traework-v2.svg +1 -0
  116. package/vendor/svg/wenxin-color.svg +1 -0
  117. package/vendor/svg/windsurf.svg +1 -0
  118. package/vendor/svg/workbuddy-color.svg +29 -0
  119. package/vendor/svg/xiaomimimo.svg +1 -0
  120. package/vendor/svg/yuanbao-color.svg +1 -0
  121. package/vendor/svg/yuanbao.svg +1 -0
  122. package/vendor/svg/zai.svg +1 -0
  123. package/vendor/svg/zhipu-color.svg +1 -0
  124. package/vendor/vue.global.prod.js +9 -0
  125. package/widget/main.js +202 -0
  126. package/widget/package.json +7 -0
  127. package/widget/preload.js +11 -0
@@ -0,0 +1,159 @@
1
+ // 异常巡检弹层(R9):配置三条异常规则 + 列出命中条目 + 一键定位回主列表。
2
+ // 契约见 page/shared.js 头部的「R25 组件文件契约」;Vue 的 API 一律写全名,不解构。
3
+ //
4
+ // 数据进出:
5
+ // · visible ← 根(侧栏「异常巡检」项点开 / 弹层关掉时回写)
6
+ // · getJSON ← 根 provide(接口 404 要点亮顶部版本红条,那段逻辑只有根里一份)
7
+ // · 规则与命中条目都归弹层自己(自己拉 /api/alert、自己改)—— 命中集合(哪个 id 算异常)
8
+ // 归根持有(供「只看异常」筛选与定位),弹层只负责读回 / 触发定位;保存后把命中数
9
+ // 上报回根(@summary)好让徽章与「只看异常」即时同步。
10
+ //
11
+ // 规则口径(与服务端一一对应):三条都是「启用 + 阈值」,命中 = 满足阈值即算一条异常;
12
+ // · tokens:单轮 token(total = 输入+输出+缓存)超过阈值;
13
+ // · durMs: 单轮耗时超过阈值(毫秒,轮内首条→末条时间差,含挂机/隔夜);
14
+ // · fail: 按 agent 统计最近 N 轮里失败(status != ok)占比超过阈值(%),
15
+ // 命中的是该 agent 里构成失败的那几轮。
16
+ // 保存即落盘(config.alert)并立刻重算命中,同 scanMs 那套设置机制(服务重启后仍在)。
17
+ window.AACTA = window.AACTA || {};
18
+ AACTA.DialogAlert = {
19
+ name: 'aa-dialog-alert',
20
+ template: `
21
+ <el-dialog :model-value="visible" @update:model-value="v => $emit('update:visible', v)" title="异常巡检" width="min(920px, 95vw)" append-to-body class="al-dialog">
22
+ <div class="al-hint">
23
+ 按三条规则盯住异常日志,命中后侧栏徽章与「只看异常」会标出新出现的异常条目,不用人工翻找。
24
+ 规则与阈值<b>即时生效并持久化</b>(写在 <code>~/.agent-acta/config.json</code>,重启服务仍在;不启用 = 绝不打扰)。
25
+ </div>
26
+ <el-skeleton v-if="al.loading && !al.j" :rows="5" animated></el-skeleton>
27
+ <div v-else-if="al.error" class="al-err">
28
+ <div class="al-errmsg">{{ al.error }}</div>
29
+ <el-button size="small" :loading="al.loading" @click="load()">重试</el-button>
30
+ </div>
31
+ <template v-else>
32
+ <div class="al-rules">
33
+ <div class="al-rule" v-for="r in ruleRows" :key="r.k">
34
+ <el-switch :model-value="r.on" @update:model-value="v => set(r.k, 'on', v)" size="small"></el-switch>
35
+ <div class="al-rule-name">{{ r.label }}</div>
36
+ <div class="al-rule-tip">{{ r.tip }}</div>
37
+ <div class="al-rule-val">
38
+ <template v-if="r.k === 'fail'">
39
+ <el-input-number :model-value="r.val" @update:model-value="v => set(r.k, 'val', v)"
40
+ :min="1" :max="100" size="small" style="width:88px"></el-input-number>
41
+ <span class="al-unit">%</span>
42
+ <span class="al-rule-window">最近
43
+ <el-input-number :model-value="r.window" @update:model-value="v => set(r.k, 'window', v)"
44
+ :min="1" size="small" style="width:64px"></el-input-number> 轮
45
+ </span>
46
+ </template>
47
+ <template v-else>
48
+ <el-input-number :model-value="r.val" @update:model-value="v => set(r.k, 'val', v)"
49
+ :min="1" size="small" style="width:120px" :disabled="!r.on"></el-input-number>
50
+ <span class="al-unit">{{ r.unit }}</span>
51
+ </template>
52
+ </div>
53
+ </div>
54
+ <div class="al-save-row">
55
+ <span class="al-count">当前命中 <b>{{ summary.count }}</b> 条</span>
56
+ <el-button type="primary" size="small" :loading="al.saving" @click="save()">保存规则</el-button>
57
+ </div>
58
+ </div>
59
+
60
+ <div class="al-hits">
61
+ <div class="al-sec-head">命中的异常条目(点行可定位回主列表)</div>
62
+ <el-empty v-if="!summary.count" description="没有命中(规则未启用,或当前内存窗口内没有异常)" :image-size="60"></el-empty>
63
+ <template v-else>
64
+ <div class="al-hit" v-for="h in hits" :key="h.id" @click="locate(h.id)">
65
+ <div class="al-hit-l">
66
+ <span class="al-agent">{{ h.agent }}</span>
67
+ <span class="al-rules-tag">
68
+ <el-tag v-for="r in h.rules" :key="r" size="small" :type="tagType(r)" effect="plain">{{ ruleLabel(r) }}</el-tag>
69
+ </span>
70
+ <span class="al-preview" :title="h.preview">{{ h.preview || '(空)' }}</span>
71
+ </div>
72
+ <div class="al-hit-r">
73
+ <span v-if="h.rules.includes('tokens')" class="al-m">token {{ fmtN(h.total) }}</span>
74
+ <span v-if="h.rules.includes('durMs')" class="al-m">耗时 {{ fmtDur(h.dur) }}</span>
75
+ <span v-if="h.rules.includes('fail')" class="al-m">{{ h.status === 'ok' ? '成功' : '失败' }}</span>
76
+ <span class="al-time">{{ fmtTime(h.time) }}</span>
77
+ </div>
78
+ </div>
79
+ <div v-if="hits.length < summary.count" class="al-more">仅显示最近 {{ hits.length }} 条(共 {{ summary.count }} 条命中)</div>
80
+ </template>
81
+ </div>
82
+ </template>
83
+ </el-dialog>`,
84
+ props: {
85
+ visible: Boolean,
86
+ fmtN: { type: Function, required: true },
87
+ fmtDur: { type: Function, required: true },
88
+ fmtTime: { type: Function, required: true },
89
+ },
90
+ emits: ['update:visible', 'locate', 'summary'],
91
+ inject: ['getJSON'],
92
+ setup(props, { emit }) {
93
+ const al = Vue.reactive({ loading: false, saving: false, error: '', j: null });
94
+ const summary = Vue.reactive({ count: 0, agents: [] });
95
+ const hits = Vue.ref([]);
96
+ // 规则编辑态(默认 = 服务端全关)。直接持有可写拷贝,改完按形状提交给 save。
97
+ const rules = Vue.reactive({
98
+ tokens: { on: false, val: 1000 },
99
+ durMs: { on: false, val: 60000 },
100
+ fail: { on: false, val: 50, window: 50 },
101
+ });
102
+ const ruleRows = Vue.computed(() => [
103
+ { k: 'tokens', on: rules.tokens.on, val: rules.tokens.val, label: '单轮 token 超阈值', tip: 'total = 输入 + 输出 + 缓存', unit: 'token' },
104
+ { k: 'durMs', on: rules.durMs.on, val: rules.durMs.val, label: '单轮耗时超阈值', tip: '轮内首条→末条时间差(含挂机/隔夜)', unit: 'ms' },
105
+ { k: 'fail', on: rules.fail.on, val: rules.fail.val, window: rules.fail.window, label: '失败率超阈值', tip: '某 agent 最近 N 轮中失败占比', unit: '%' },
106
+ ]);
107
+ const ruleLabel = k => ({ tokens: 'token', durMs: '耗时', fail: '失败率' })[k] || k;
108
+ const tagType = k => ({ tokens: 'warning', durMs: 'danger', fail: 'info' })[k] || 'info';
109
+ function set(k, field, v) { rules[k][field] = v; }
110
+
111
+ async function load() {
112
+ al.loading = true; al.error = '';
113
+ try {
114
+ const j = await getJSON('/api/alert');
115
+ al.j = j;
116
+ Object.assign(rules.tokens, j.rules.tokens || { on: false, val: 0 });
117
+ Object.assign(rules.durMs, j.rules.durMs || { on: false, val: 0 });
118
+ Object.assign(rules.fail, j.rules.fail || { on: false, val: 0, window: 50 });
119
+ summary.count = (j.summary && j.summary.count) || 0;
120
+ summary.agents = (j.summary && j.summary.agents) || [];
121
+ hits.value = (j.hits || []).slice(0, 200);
122
+ } catch (e) { al.error = e.message || String(e); }
123
+ al.loading = false;
124
+ }
125
+ async function save() {
126
+ al.saving = true; al.error = '';
127
+ try {
128
+ const body = {
129
+ rules: {
130
+ tokens: { on: rules.tokens.on, val: rules.tokens.on ? rules.tokens.val : 0 },
131
+ durMs: { on: rules.durMs.on, val: rules.durMs.on ? rules.durMs.val : 0 },
132
+ fail: { on: rules.fail.on, val: rules.fail.on ? rules.fail.val : 0, window: rules.fail.window },
133
+ },
134
+ };
135
+ await getJSON('/api/alert', {
136
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
137
+ });
138
+ // 保存后重新拉一次,取最新规则回显 + 命中列表(POST 返回摘要不带 hit 明细)
139
+ const fresh = await getJSON('/api/alert');
140
+ al.j = fresh;
141
+ Object.assign(rules.tokens, fresh.rules.tokens);
142
+ Object.assign(rules.durMs, fresh.rules.durMs);
143
+ Object.assign(rules.fail, fresh.rules.fail);
144
+ summary.count = (fresh.summary && fresh.summary.count) || 0;
145
+ summary.agents = (fresh.summary && fresh.summary.agents) || [];
146
+ hits.value = (fresh.hits || []).slice(0, 200);
147
+ // 让根刷新侧栏徽章 / 「只看异常」集合(服务端已广播 alert,但根也可能在此时未连接 SSE)
148
+ emit('summary', fresh.summary || { count: 0, agents: [] });
149
+ ElMessage.success('异常巡检规则已保存');
150
+ } catch (e) { al.error = e.message || String(e); }
151
+ al.saving = false;
152
+ }
153
+ function locate(id) { emit('locate', id); }
154
+
155
+ Vue.watch(() => props.visible, v => { if (v) load(); });
156
+
157
+ return { al, summary, hits, ruleRows, rules, load, save, locate, set, ruleLabel, tagType, fmtN: props.fmtN, fmtDur: props.fmtDur, fmtTime: props.fmtTime };
158
+ }
159
+ };
@@ -0,0 +1,342 @@
1
+ // R28 历史归档弹层:浏览 ~/.agent-acta/archive 下被冻结的条目(模板与数据全在本文件)。
2
+ // 契约见 page/shared.js 头部「R25 组件文件契约」;Vue / Element Plus 的 API 写全名。
3
+ //
4
+ // 数据进出:
5
+ // · visible ← 根(侧栏「历史归档」项点开 / 弹层自己关闭时回写);getJSON ← 根 provide(404 要点亮版本红条)
6
+ // · 三个只读端点(服务端批 1 已上):
7
+ // /api/archive/index → 清单(root / days[] / agents[] / totals)
8
+ // /api/archive/entries → 条目列表(**不含** detail,只给 hasDetail 标记)
9
+ // /api/archive/entry → 单条冻结的详情快照
10
+ // · 归档是**旁路**:只读、不参与主列表的扫描与查询链路(archive.mjs 头部的第一条约束)。
11
+ // 所以这个弹层与左侧栏的 agent/时间筛选无关,自己一套 selAgent/selDay/q。
12
+ //
13
+ // 与主列表刻意不同的三处(都是归档的固有性质,不是缺功能):
14
+ // · 没有「加载完整内容」:冻下来的就是当时那份(正文 800 字截断、tools/calls/events 各最多 40 项),
15
+ // truncated 的原文随产品删日志一起没了,这里给不出更多 —— 底部那行把它说清楚。
16
+ // · 没有进行中的轮(归档只收已结束且早于今天 0 点的条目),所以状态只有 ok / 失败。
17
+ // · 详情是快照而非重读源文件:源文件删了照样打得开,这正是这个功能存在的理由。
18
+ window.AACTA = window.AACTA || {};
19
+ AACTA.DialogArchive = {
20
+ name: 'aa-dialog-archive',
21
+ template: `
22
+ <el-dialog :model-value="visible" @update:model-value="v => $emit('update:visible', v)" title="历史归档" width="min(1120px, 95vw)" append-to-body class="arc-dialog">
23
+ <el-skeleton v-if="arc.loading && !arc.days.length" :rows="8" animated></el-skeleton>
24
+ <div v-else-if="!arc.days.length" class="empty">
25
+ <el-alert v-if="arc.error" :title="arc.error" type="error" :closable="false" show-icon style="margin-bottom:10px"></el-alert>
26
+ <template v-else>
27
+ 还没有归档数据。归档在服务内每日自动跑一遍,也可以手动执行
28
+ <code>node agent-acta-server.mjs --archive</code>(先 <code>--list</code> 看一眼再跑)。
29
+ </template>
30
+ <el-button size="small" @click="load">重试</el-button>
31
+ </div>
32
+ <div v-else class="arc-wrap">
33
+ <aside class="arc-side">
34
+ <div class="arc-sum">
35
+ <div><b>{{ arc.totals.count }}</b> 条 · <b>{{ arc.totals.days }}</b> 个日期文件</div>
36
+ <div class="arc-sum-2">{{ arc.totals.agents }} 个 agent · {{ arcBytes(arc.totals.bytes) }}</div>
37
+ </div>
38
+ <div class="arc-tree">
39
+ <div class="arc-node lv0" :class="{active: selAgent==='' && selDay===''}" @click="pick('', '')">
40
+ <span class="arc-nm">全部</span><span class="arc-ct">{{ arc.totals.count }}</span>
41
+ </div>
42
+ <template v-for="a in arc.agents" :key="a.agent">
43
+ <div class="arc-node lv1" :class="{active: selAgent===a.agent && selDay===''}" @click="pick(a.agent, '')" :title="a.lastDay ? '最近归档 ' + a.lastDay : ''">
44
+ <!-- 折叠开关单独占一格:点箭头只管收起/展开,点整行是「筛到这个 agent」(顺手展开)。
45
+ 18 个 agent 全展开是 214 行,没有折叠就只能一路滚 -->
46
+ <span class="arc-caret" :class="{open: isOpen(a.agent)}" :title="isOpen(a.agent) ? '收起' : '展开'" @click.stop="toggleAgent(a.agent)">{{ isOpen(a.agent) ? '▾' : '▸' }}</span>
47
+ <img v-if="agentIcon(a.agent)" class="arc-ico" :src="agentIcon(a.agent)">
48
+ <span v-else class="arc-dot" :style="{background: agentColor(a.agent)}"></span>
49
+ <span class="arc-nm">{{ a.agent }}</span>
50
+ <span class="arc-ct">{{ a.unknown ? '?' : a.count }}</span>
51
+ </div>
52
+ <template v-for="d in dayRows(a.agent)" :key="d.day">
53
+ <div v-if="isOpen(a.agent)" class="arc-node lv2" :class="{active: selAgent===a.agent && selDay===d.day}" @click="pick(a.agent, d.day)">
54
+ <span class="arc-nm">{{ d.day }}</span>
55
+ <span class="arc-ct">{{ d.count < 0 ? '?' : d.count }}</span>
56
+ </div>
57
+ </template>
58
+ </template>
59
+ </div>
60
+ <!-- per-agent 归档开关:树里只有「已经有归档」的 agent,而开关要能管到**还没有归档**的那些
61
+ (以后也永远不会有 —— 正是需要它出现在这里的理由),所以名单取自配置(index 的 configAgents),不是树。 -->
62
+ <div class="arc-cfg">
63
+ <div class="arc-cfg-h" @click="cfgOpen = !cfgOpen" :title="cfgOpen ? '收起' : '展开'">
64
+ <span>归档开关</span>
65
+ <span class="arc-cfg-n">{{ arc.skip.length ? '已关 ' + arc.skip.length + ' 个' : '全部开启' }}</span>
66
+ <span class="arc-caret" :class="{open: cfgOpen}">{{ cfgOpen ? '▾' : '▸' }}</span>
67
+ </div>
68
+ <div v-show="cfgOpen" class="arc-cfg-body">
69
+ <div class="arc-cfg-tip">关掉就不再归档这个 agent 的新条目(写进 config.json 的 archive.skip);已经冻好的日期文件保留,下一轮归档起生效。</div>
70
+ <div v-for="a in arc.configAgents" :key="a" class="arc-cfg-row">
71
+ <span class="arc-nm">{{ a }}</span>
72
+ <el-switch size="small" :model-value="!isSkipped(a)" :loading="skipSaving === a"
73
+ :title="isSkipped(a) ? '当前不归档这个 agent(打开即恢复归档)' : '当前会归档这个 agent(关掉即停止归档新条目)'"
74
+ @click.stop @change="v => toggleSkip(a, v)"></el-switch>
75
+ </div>
76
+ <div v-if="!arc.configAgents.length" class="arc-cfg-tip">没有已启用的 agent。</div>
77
+ </div>
78
+ </div>
79
+ </aside>
80
+ <section class="arc-main">
81
+ <!-- 失败不清场:已经拉到的树与列表留在原地,错误只占一条横幅。
82
+ 以前这里是「有 error 就整层换成一条红条」,服务重启/瞬断时会看起来像归档数据没了
83
+ (用户第一次反馈就是打开一会儿报 Failed to fetch 后整层空了)。 -->
84
+ <div v-if="arc.error" class="arc-errbar">
85
+ <span class="arc-errmsg">{{ arc.error }}</span>
86
+ <el-button size="small" @click="load">重试</el-button>
87
+ </div>
88
+ <div class="arc-bar">
89
+ <el-input v-model="q" size="small" clearable placeholder="关键词(预览 / 用户 / 助手 / 会话 / 模型 / 工具)" style="width:300px" @keyup.enter="reload" @clear="reload"></el-input>
90
+ <el-button size="small" type="primary" :loading="arc.listLoading" @click="reload">搜索</el-button>
91
+ <span class="arc-scope">{{ scopeText }}</span>
92
+ </div>
93
+ <div class="arc-list">
94
+ <div v-for="r in arc.list" :key="r.id" class="arc-row" :class="{err: r.status !== 'ok', open: openId === r.id}" @click="toggle(r)">
95
+ <span class="arc-t">{{ fmtTime(r.time) }}</span>
96
+ <span class="arc-pv">{{ r.preview || '(无文本预览)' }}</span>
97
+ <span class="arc-tag">{{ agentLetter(r.agent) }}</span>
98
+ <span v-if="!r.hasDetail" class="arc-nod">无详情</span>
99
+ <span class="arc-kv">↑{{ fmtN(r.tin) }} ↓{{ fmtN(r.tout) }}</span>
100
+ <span v-if="r.tools" class="arc-kv">{{ r.tools }} 工具</span>
101
+ <span v-if="r.calls" class="arc-kv">{{ r.calls }} 次 LLM</span>
102
+ <span v-if="r.dur" class="arc-kv">{{ fmtDur(r.dur) }}</span>
103
+ <span class="arc-arrow">{{ openId === r.id ? '▾' : '▸' }}</span>
104
+ </div>
105
+ <div v-if="!arc.list.length && !arc.listLoading" class="arc-none">这个范围内没有条目{{ q ? '匹配「' + q + '」' : '' }}</div>
106
+ </div>
107
+ <div v-if="arc.total > arc.list.length" class="arc-more">
108
+ <el-button size="small" :loading="arc.listLoading" @click="more">加载更多(已列 {{ arc.list.length }} / 共 {{ arc.total }})</el-button>
109
+ </div>
110
+ <div v-if="openId" class="arc-detail" @click.stop>
111
+ <el-skeleton v-if="arc.detailLoading" :rows="4" animated></el-skeleton>
112
+ <div v-else-if="arc.detailErr" class="arc-derr">{{ arc.detailErr }}</div>
113
+ <template v-else-if="detail">
114
+ <div class="arc-dhead">
115
+ <span class="arc-dtime">{{ fmtTime(detail.time) }}</span>
116
+ <span class="arc-dmeta">{{ detail.project || '(无项目)' }}<template v-if="detail.session"> · {{ detail.session }}</template></span>
117
+ <span class="arc-dmeta">归档于 {{ detail.day }} · 口径 rev {{ detail.rev == null ? '—' : detail.rev }}</span>
118
+ </div>
119
+ <div v-if="!detail.detail" class="arc-none">归档时未取到详情:那会儿源日志就已经读不到了,只剩统计字段。</div>
120
+ <template v-else>
121
+ <h4>用户输入 <span class="copy-btn" title="复制原文" @click="copyRaw($event, detail.detail.user)">复制</span></h4>
122
+ <pre>{{ detail.detail.user || '(空)' }}</pre>
123
+ <template v-if="detail.detail.assistant">
124
+ <h4>AI 输出 <span class="copy-btn" title="复制原文(非渲染后文本)" @click="copyRaw($event, detail.detail.assistant)">复制</span></h4>
125
+ <div class="md-body" v-html="detail.detail._assistantMd"></div>
126
+ </template>
127
+ <template v-if="detail.detail.tools && detail.detail.tools.length">
128
+ <h4>工具调用({{ detail.detail.tools.length }})</h4>
129
+ <div v-for="(t, i) in detail.detail.tools" :key="i" class="tool-item">
130
+ <div class="tool-head">
131
+ <span class="tname">{{ t.name }}</span>
132
+ <span v-if="t.error" class="terr">✗ {{ t.error }}</span>
133
+ <span class="tool-copy" @click="copyRaw($event, (t.input || '') + (t.output ? '\\n' + t.output : ''))">复制</span>
134
+ </div>
135
+ <pre v-if="t.input">{{ t._inPretty || prettyJson(t.input) }}</pre>
136
+ <pre v-if="t.output">{{ t._outPretty || prettyJson(t.output) }}</pre>
137
+ </div>
138
+ </template>
139
+ <template v-if="detail.detail.calls && detail.detail.calls.length">
140
+ <h4>LLM 调用明细({{ detail.detail.calls.length }} 次)</h4>
141
+ <div class="call-list">
142
+ <div v-for="(c, i) in detail.detail.calls" :key="i" class="call-item">
143
+ <div class="call-row">
144
+ <span class="ci">#{{ i + 1 }}</span>
145
+ <span class="cm">{{ c.model || 'generation' }}</span>
146
+ <span v-if="c.tools && c.tools.length" class="call-tools" :title="'这次调用发起了:' + toolText(c.tools, 99)">{{ toolText(c.tools) }}</span>
147
+ <span v-if="c.error" class="cerr">✗ {{ c.error }}</span>
148
+ <span class="cd">{{ fmtDur(c.dur) }}</span>
149
+ <span v-if="c.tin != null && c.model" class="ct">↑{{ fmtN(c.tin) }} · ↓{{ fmtN(c.tout) }}<template v-if="c.tcache"> · 缓存 {{ fmtN(c.tcache) }}</template></span>
150
+ </div>
151
+ <pre v-if="c.text" class="call-out">{{ c.text }}</pre>
152
+ </div>
153
+ </div>
154
+ </template>
155
+ <div v-else-if="detail.detail.callsNote" class="calls-note">{{ detail.detail.callsNote }}</div>
156
+ <template v-if="detail.detail.events && detail.detail.events.length">
157
+ <h4>事件({{ detail.detail.events.length }})</h4>
158
+ <div v-for="(ev, i) in detail.detail.events" :key="i" class="arc-ev">
159
+ <span class="arc-ev-t">{{ fmtTime(ev.time) }}</span>
160
+ <span class="arc-ev-n">{{ ev.name || ev.type || ev.title || '(事件)' }}</span>
161
+ <span v-if="ev.text" class="arc-ev-x">{{ ev.text }}</span>
162
+ </div>
163
+ </template>
164
+ <div v-if="detail.detail.note" class="calls-note">{{ detail.detail.note }}</div>
165
+ <div v-if="thenNote" class="arc-then">{{ thenNote }}</div>
166
+ </template>
167
+ </template>
168
+ </div>
169
+ </section>
170
+ </div>
171
+ <template #footer>
172
+ <span class="arc-foot">归档 = 条目级冻结(统计字段 + 归档当时的详情快照):只读旁路,不并入上方请求列表;正文截断 800 字、工具/调用/事件各最多 40 项 —— 被截掉的那部分原文已随产品删日志一起消失,这里补不回来。</span>
173
+ <el-button @click="$emit('update:visible', false)">关闭</el-button>
174
+ <el-button type="primary" :loading="arc.loading" @click="load">刷新</el-button>
175
+ </template>
176
+ </el-dialog>`,
177
+ props: { visible: Boolean },
178
+ emits: ['update:visible'],
179
+ setup(props) {
180
+ const getJSON = Vue.inject('getJSON');
181
+ const PAGE = 200;
182
+ const arc = Vue.reactive({
183
+ loading: false, error: '', listLoading: false, detailLoading: false, detailErr: '',
184
+ days: [], agents: [], skip: [], configAgents: [], totals: { agents: 0, days: 0, count: 0, bytes: 0 },
185
+ list: [], total: 0, scanned: 0,
186
+ });
187
+ const selAgent = Vue.ref(''), selDay = Vue.ref(''), q = Vue.ref('');
188
+ const openId = Vue.ref(''), detail = Vue.ref(null);
189
+ const cfgOpen = Vue.ref(true); // 开关列表默认展开:它是这个弹层唯一的「写」入口,藏起来等于没有
190
+ const skipSaving = Vue.ref(''); // 正在保存的那个 agent 名(只用来给该行的开关转圈)
191
+
192
+ // 展开状态只记「被收起」的 agent:默认全展开(与加折叠之前一致,18 个 agent 也就 214 行),
193
+ // 收起是用户主动的动作。反过来记「展开的」会让第一次打开的面板变成光秃秃一排 agent 名。
194
+ const closed = Vue.reactive({});
195
+ const isOpen = a => !closed[a];
196
+ function toggleAgent(a) { closed[a] = !closed[a]; }
197
+
198
+ // 取数统一走这里:**只**对网络层失败(fetch 直接 reject)自动重试一次 —— 服务重启/瞬断是
199
+ // 一次性事件,重试一次基本就好了;接口真报错(404/500/非 JSON)不重试,那是版本或数据的问题,
200
+ // 重试只会把错误拖晚 1.2s 才显示。
201
+ const netFail = e => /Failed to fetch|NetworkError|Load failed|fetch failed/i.test((e && e.message) || '');
202
+ async function withRetry(fn) {
203
+ try { return await fn(); }
204
+ catch (e) {
205
+ if (!netFail(e)) throw e;
206
+ await new Promise(r => setTimeout(r, 1200));
207
+ return await fn();
208
+ }
209
+ }
210
+ const errText = (what, e) => what +
211
+ (netFail(e) ? ':连不上服务(可能正在重启或已停止)—— ' : ':') + ((e && e.message) || e);
212
+
213
+ async function load() {
214
+ if (arc.loading) return;
215
+ arc.loading = true; arc.error = '';
216
+ try {
217
+ const j = await withRetry(() => getJSON('/api/archive/index'));
218
+ if (!j.ok) throw new Error(j.error || '清单读取失败');
219
+ arc.days = j.days || [];
220
+ arc.agents = j.agents || [];
221
+ arc.skip = j.skip || [];
222
+ arc.configAgents = j.configAgents || [];
223
+ arc.totals = j.totals || arc.totals;
224
+ // 顺带把当前选中范围的条目也拉一遍:不然打开弹层只有左边一棵树、右边空着,
225
+ // 得先点一下才知道能看什么。全量扫一遍 195 个日期文件实测 ~0.6s,不值得为它加个「请选择」的空态。
226
+ closeDetail();
227
+ loadEntries(0);
228
+ } catch (e) { arc.error = errText('历史归档加载失败', e); }
229
+ finally { arc.loading = false; }
230
+ }
231
+ // 每次打开都回到「全部展开」:el-dialog 默认不销毁内容,组件实例活到整页刷新为止,
232
+ // closed 不清就是在**跨次记忆** —— 上次随手收起的几个 agent 下次打开时静默留着,
233
+ // 看起来就是「一打开全都收起了」。收起只属于本次浏览。
234
+ function resetCollapsed() { for (const k of Object.keys(closed)) delete closed[k]; }
235
+ Vue.watch(() => props.visible, v => { if (v) { resetCollapsed(); load(); } });
236
+
237
+ // 清单里的 days[] 是一串扁平行(agent + day + count + bytes),按 agent 归一次组给模板用
238
+ const dayMap = Vue.computed(() => {
239
+ const m = new Map();
240
+ for (const d of arc.days) {
241
+ if (!m.has(d.agent)) m.set(d.agent, []);
242
+ m.get(d.agent).push(d);
243
+ }
244
+ return m;
245
+ });
246
+ const dayRows = a => dayMap.value.get(a) || [];
247
+
248
+ function pick(agent, day) {
249
+ selAgent.value = agent; selDay.value = day;
250
+ // 点行筛选时顺手把它展开:不然「筛到了但看不见日期」,还得再点一次箭头
251
+ if (agent) closed[agent] = false;
252
+ closeDetail();
253
+ arc.list = []; arc.total = 0; arc.scanned = 0;
254
+ loadEntries(0);
255
+ }
256
+ function reload() { closeDetail(); loadEntries(0); }
257
+ function more() { loadEntries(arc.list.length); }
258
+
259
+ async function loadEntries(off) {
260
+ arc.listLoading = true;
261
+ try {
262
+ const j = await withRetry(() => getJSON('/api/archive/entries?agent=' + encodeURIComponent(selAgent.value)
263
+ + '&day=' + encodeURIComponent(selDay.value)
264
+ + '&q=' + encodeURIComponent(q.value.trim())
265
+ + '&limit=' + PAGE + '&offset=' + off));
266
+ if (!j.ok) throw new Error(j.error || '条目读取失败');
267
+ arc.list = off > 0 ? arc.list.concat(j.list || []) : (j.list || []);
268
+ arc.total = j.total || 0; arc.scanned = j.scanned || 0;
269
+ arc.error = '';
270
+ } catch (e) { arc.error = errText('归档条目加载失败', e); }
271
+ finally { arc.listLoading = false; }
272
+ }
273
+
274
+ function closeDetail() { openId.value = ''; detail.value = null; arc.detailErr = ''; }
275
+
276
+ // ---- per-agent 归档开关(写 config.archive.skip)----
277
+ // 开关的显示态由 arc.skip 推出来(不是 v-model),所以保存失败时它自己会弹回原位 —— 不需要额外的回滚。
278
+ const isSkipped = a => arc.skip.includes(a);
279
+ async function toggleSkip(a, on) {
280
+ skipSaving.value = a;
281
+ try {
282
+ const j = await withRetry(() => getJSON('/api/archive/skip', {
283
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
284
+ body: JSON.stringify({ agent: a, skip: !on }),
285
+ }));
286
+ if (!j.ok) throw new Error(j.error || '保存失败');
287
+ arc.skip = j.skip || [];
288
+ arc.error = '';
289
+ ElementPlus.ElMessage.success(on ? '已恢复归档 ' + a : '已停止归档 ' + a + '(已冻好的文件保留)'); } catch (e) { arc.error = errText('归档开关保存失败', e); }
290
+ finally { skipSaving.value = ''; }
291
+ }
292
+ async function toggle(r) {
293
+ if (openId.value === r.id) { closeDetail(); return; }
294
+ openId.value = r.id; detail.value = null; arc.detailErr = '';
295
+ arc.detailLoading = true;
296
+ try {
297
+ const j = await withRetry(() => getJSON('/api/archive/entry?agent=' + encodeURIComponent(r.agent)
298
+ + '&day=' + encodeURIComponent(r.day) + '&id=' + encodeURIComponent(r.id)));
299
+ if (!j.ok) throw new Error(j.error || '详情读取失败');
300
+ detail.value = prepArcDetail(j.entry);
301
+ } catch (e) { arc.detailErr = errText('归档详情读取失败', e); }
302
+ finally { arc.detailLoading = false; }
303
+ }
304
+ // 与页面 prepDetail 同一套「入库前把贵的渲染算一次」的做法:别在模板里调 renderMd/prettyJson
305
+ function prepArcDetail(rec) {
306
+ const d = rec && rec.detail;
307
+ if (d) {
308
+ if (d.assistant) d._assistantMd = renderMd(d.assistant);
309
+ for (const t of d.tools || []) {
310
+ if (t.input) t._inPretty = prettyJson(t.input);
311
+ if (t.output) t._outPretty = prettyJson(t.output);
312
+ }
313
+ }
314
+ return rec;
315
+ }
316
+
317
+ const scopeText = Vue.computed(() => {
318
+ const scope = (selAgent.value || '全部 agent') + (selDay.value ? ' · ' + selDay.value : '');
319
+ return scope + ' · 命中 ' + arc.total + ' / 扫描 ' + arc.scanned + ' 条';
320
+ });
321
+ // 归档时被 40 项上限砍掉的部分:归档文件里只留了计数(detailMore),原文补不回来,直接说清楚
322
+ const thenNote = Vue.computed(() => {
323
+ const m = detail.value && detail.value.detailMore;
324
+ if (!m) return '';
325
+ const parts = Object.keys(m).map(k => ({ tools: '工具', calls: 'LLM 调用', events: '事件' })[k] + ' ' + m[k] + ' 项');
326
+ return '归档时按每类最多 40 项截断:另有 ' + parts.join('、') + '未入库(原文已随源日志一起消失)。';
327
+ });
328
+
329
+ const arcBytes = n => {
330
+ const v = Number(n) || 0;
331
+ if (v < 1024) return v + 'B';
332
+ if (v < 1024 * 1024) return (v / 1024).toFixed(1) + 'KB';
333
+ if (v < 1024 * 1024 * 1024) return (v / 1048576).toFixed(1) + 'MB';
334
+ return (v / 1073741824).toFixed(2) + 'GB';
335
+ };
336
+
337
+ // 模板只认 props ∪ setup return(page-check.mjs 盯住)—— 下面这串全是模板里出现过的名字
338
+ return { arc, selAgent, selDay, q, openId, detail, load, pick, reload, more, toggle,
339
+ isOpen, toggleAgent, cfgOpen, isSkipped, toggleSkip, skipSaving, dayRows, scopeText, thenNote, arcBytes,
340
+ fmtN, fmtDur, fmtTime, agentLetter, agentColor, agentIcon, toolText, copyRaw, prettyJson };
341
+ }
342
+ };