@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,382 @@
|
|
|
1
|
+
// 会话视图(R25 批 3):左栏会话列表 + 右栏逐轮轨迹 + 逐轮用量图 + 概览条 + 轮详情。
|
|
2
|
+
// 契约见 page/shared.js 头部的「R25 组件文件契约」;Vue 的 API 一律写全名,不解构。
|
|
3
|
+
//
|
|
4
|
+
// 为什么这里**不像弹层那样把数据搬进组件**(批 1 的先例在此不适用,是刻意的):
|
|
5
|
+
// 这个视图是 `v-if`(不变式写着「sess 视图是 v-if,自己两栏内滚」),切去「请求日志」就把整棵子树销毁。
|
|
6
|
+
// 改前那些状态活在根的 setup 里,所以切回来还留着选中的会话、已展开的轮和 /api/entry 缓存
|
|
7
|
+
// (列表另有一支 watch(view) 会重拉)。数据若搬到组件肚子里,这一份状态就跟着子树一起没了 ——
|
|
8
|
+
// 「切个视图丢光展开」是行为变化,纯重构不许。故本组件与顶栏/侧栏同类:**状态全留根,props 收数据、emit 发动作**。
|
|
9
|
+
// props 面宽(30 多个)是 R25 评审时就接受下的代价(文档:「props 面过宽 → 接受,不做自动注入魔法」);
|
|
10
|
+
// 换来的好处是 page-check 能逐个标识符盯住模板 ↔ 可用面,换成一个 `inject` 的状态口袋就全瞎了。
|
|
11
|
+
//
|
|
12
|
+
// 三个 v-model 式回写(sess-search / sess-sort / sess-chart-open)的事件名走 kebab,
|
|
13
|
+
// 靠 Vue emit 的 camelize 兜底对上根里的 onUpdate:xxx(批 2 实测过的那条)。
|
|
14
|
+
window.AACTA = window.AACTA || {};
|
|
15
|
+
AACTA.SessView = {
|
|
16
|
+
name: 'aa-sess-view',
|
|
17
|
+
template: `
|
|
18
|
+
<main class="main sv-main">
|
|
19
|
+
<aside class="sv-side">
|
|
20
|
+
<div class="sv-side-head">
|
|
21
|
+
<el-input :model-value="sessSearch" @update:model-value="v => $emit('update:sess-search', v)" size="small" clearable placeholder="搜索会话 / 项目 / 模型…" :prefix-icon="searchIcon"></el-input>
|
|
22
|
+
<div class="sv-side-row">
|
|
23
|
+
<span class="sv-side-n" title="列表跟随左侧的 agent / 项目 / 时间范围收窄(服务端 /api/sessions 与 /api/daily 同口径)">{{ sessListFiltered.length }} / {{ sessList.length }} 个会话<template v-if="sessListCapped"> · 只取最近 500 个</template></span>
|
|
24
|
+
<el-select :model-value="sessSort" @update:model-value="v => $emit('update:sess-sort', v)" size="small" class="sv-sort" title="排序(客户端排,不重新请求)">
|
|
25
|
+
<el-option label="最近活跃" value="to"></el-option>
|
|
26
|
+
<el-option label="最早开始" value="from"></el-option>
|
|
27
|
+
<el-option label="轮数最多" value="turns"></el-option>
|
|
28
|
+
<el-option label="耗时最长" value="dur"></el-option>
|
|
29
|
+
<el-option label="token 最多" value="total"></el-option>
|
|
30
|
+
</el-select>
|
|
31
|
+
</div>
|
|
32
|
+
<el-alert v-if="sessListError" :title="sessListError" type="error" :closable="false" show-icon></el-alert>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="sv-list" v-loading="sessListLoading">
|
|
35
|
+
<div v-if="!sessListLoading && !sessList.length" class="empty sv-empty">当前范围内没有带会话标识的条目</div>
|
|
36
|
+
<div v-else-if="!sessListFiltered.length" class="empty sv-empty">没有匹配的会话</div>
|
|
37
|
+
<div v-for="s in sessListFiltered" :key="s._sid" class="sv-item" :class="{on: sessPick && sessPick._sid === s._sid, sub: s.parentKey}" @click="$emit('pick-session', s)" :title="s.key">
|
|
38
|
+
<div class="sv-item-1">
|
|
39
|
+
<!-- I16:子 agent 会话缩进 + 折线前缀。判据是**服务端算好的显式谱系**(s.parentKey),
|
|
40
|
+
不是按时间区间猜的 —— 见 server 的 sessionsAgg。认不出父的那些照旧平铺(不硬造层级)。 -->
|
|
41
|
+
<span v-if="s.parentKey" class="sv-sub-branch" title="子 agent 会话(父会话在当前列表里)">└</span>
|
|
42
|
+
<img v-if="agentIcon(s.agent)" class="adot-img" :src="agentIcon(s.agent)">
|
|
43
|
+
<span v-else class="adot" :style="{background: agentColor(s.agent)}"></span>
|
|
44
|
+
<span class="sv-item-t">{{ s.name || s.key }}</span>
|
|
45
|
+
<span v-if="s.status !== 'ok'" class="tag tag-err">有失败轮</span>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="sv-item-2">{{ s.agent }} · {{ s.project || '—' }}<template v-if="s.sub"> · 子 agent</template></div>
|
|
48
|
+
<div class="sv-item-3">
|
|
49
|
+
<span>{{ s.turns }} 轮</span>
|
|
50
|
+
<!-- I16:子树合计只在真有子会话时显示(subSelf=false),免得每行都挂一个和本体一样的数 -->
|
|
51
|
+
<span v-if="!s.subSelf" class="sv-item-sub" :title="'含 ' + s.subKids + ' 个子 agent 会话的合计'">合计 {{ s.subTurns }} 轮 · {{ fmtN(s.subTotal) }} tok</span>
|
|
52
|
+
<span v-else>{{ fmtN(s.total) }} tok</span>
|
|
53
|
+
<span>{{ fmtDurBig(s.dur) }}</span>
|
|
54
|
+
<span class="sv-item-n">{{ fmtAgo(s.to) }}</span>
|
|
55
|
+
</div>
|
|
56
|
+
<div v-if="!s.hasTimeline" class="sv-item-4">
|
|
57
|
+
<span class="mini-tag" title="该 agent 没有逐事件时间轴(缺的是「把逐事件留成一条流」,不是日志里没有时间戳),轮次退回 LLM 调用明细 + 工具调用两块渲染">无逐事件</span>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</aside>
|
|
62
|
+
|
|
63
|
+
<section class="sv-cur">
|
|
64
|
+
<div v-if="sessCurLoading" v-loading="true" style="min-height:180px"></div>
|
|
65
|
+
<div v-else-if="!sessCur" class="empty sv-empty">从左边选一个会话,看它的逐轮轨迹</div>
|
|
66
|
+
<template v-else>
|
|
67
|
+
<div class="sv-head">
|
|
68
|
+
<div style="min-width:0">
|
|
69
|
+
<!-- I14:标题三档兜底。sessPick.name 来自会话列表(/api/sessions);
|
|
70
|
+
sessCur.name 来自 /api/session 的会话级字段 —— 列表那一份拿不到时(如直接带 key 进来,
|
|
71
|
+
sessPick 为空)也不该退回裸 UUID。两者都无才是「这个来源没有标题」 -->
|
|
72
|
+
<h1 class="sv-title">{{ (sessPick && sessPick.name) || sessCur.name || sessCur.session || '(无标题会话)' }}</h1>
|
|
73
|
+
<div class="statline">{{ sessPick.agent }} · {{ sessPick.project || '—' }} · {{ sessCur.session }}</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div class="sess-sum">
|
|
78
|
+
<span><b>{{ sessCur.count }}</b> 轮</span>
|
|
79
|
+
<span>↑{{ fmtN(sessCur.tin) }} · ↓{{ fmtN(sessCur.tout) }} · 缓存 {{ fmtN(sessCur.tcache) }}</span>
|
|
80
|
+
<span>累计 token <b>{{ fmtN(sumTok(sessCur)) }}</b></span>
|
|
81
|
+
<!-- I12 压缩画像:会话级小结(逐轮累加),回答「一共压掉多少 / 等了多久」。
|
|
82
|
+
只有真压过才有这一格;compDrop 只有 claude 源有 ⇒ 没有就不说,不印假 0。 -->
|
|
83
|
+
<span v-if="sessChart && sessChart.comp"
|
|
84
|
+
:title="(sessChart.comp.auto || sessChart.comp.manual) ? '自动压缩 ' + sessChart.comp.auto + ' 次 / 手动压缩 ' + sessChart.comp.manual + ' 次' : ''">
|
|
85
|
+
压缩 <b>{{ sessChart.comp.n }}</b> 次<template v-if="sessChart.comp.drop"> · 丢 {{ fmtN(sessChart.comp.drop) }} tok</template><template v-if="sessChart.comp.ms"> · 等 {{ fmtDurBig(sessChart.comp.ms) }}</template>
|
|
86
|
+
</span>
|
|
87
|
+
<span>总耗时 <b>{{ fmtDurBig(sessCur.dur) }}</b></span>
|
|
88
|
+
<span>{{ fmtTime(sessFrom) }} ~ {{ fmtTime(sessTo) }}</span>
|
|
89
|
+
<span v-if="sessCur.models && sessCur.models.length" class="sess-models">{{ modelText(sessCur.models) }}</span>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<!-- I16 子 agent 拓扑:这一坨的谱系。两件事分开说 ——
|
|
93
|
+
①「我挂在谁下面」(父会话,可点着跳过去);
|
|
94
|
+
②「我下面挂了谁」(子会话清单,每个可点着跳过去)。
|
|
95
|
+
数据全部来自服务端的**显式外键**(dsh 会话头 parentSession / claude 的 subagents/ 目录层级 /
|
|
96
|
+
zcode·opencode 的 session.parent_id / hermes 的 parent_session_id),不是按时间区间猜的。
|
|
97
|
+
两边都认不出来时整块不渲染(不占位、不写「无」)—— 与「拿不到就不印 0」同一条约定。 -->
|
|
98
|
+
<div v-if="sessCur.parent || (sessCur.children && sessCur.children.length)" class="sv-tree">
|
|
99
|
+
<div v-if="sessCur.parent" class="sv-tree-row">
|
|
100
|
+
<span class="sv-tree-k">父会话</span>
|
|
101
|
+
<a class="sv-tree-link" :title="'跳到父会话 ' + sessCur.parent" @click="$emit('open-session', sessCur.parent)">{{ sessCur.parent }}</a>
|
|
102
|
+
<span class="sv-tree-note">这一坨是它派出来的子 agent</span>
|
|
103
|
+
</div>
|
|
104
|
+
<div v-if="sessCur.sub && sessCur.subMode" class="sv-tree-row">
|
|
105
|
+
<span class="sv-tree-k">派活</span>
|
|
106
|
+
<span class="sv-tree-sub">{{ sessCur.sub }}</span>
|
|
107
|
+
<span class="mini-tag" :title="sessCur.subMode === 'one-shot' ? '一次性子任务(跑完就结束)' : '可续的子会话(还能接着对话)'">{{ sessCur.subMode }}</span>
|
|
108
|
+
</div>
|
|
109
|
+
<div v-if="sessCur.children && sessCur.children.length" class="sv-tree-row sv-tree-kids">
|
|
110
|
+
<span class="sv-tree-k">子 agent</span>
|
|
111
|
+
<span class="sv-tree-note">{{ sessCur.children.length }} 个会话 · 合计 {{ sessCur.subTurns }} 轮 / {{ fmtN(sessCur.subTotal) }} tok(含本级)</span>
|
|
112
|
+
<div v-for="c in sessCur.children" :key="c.key" class="sv-tree-kid" :class="{err: c.status !== 'ok'}" @click="$emit('open-session', c.key)" :title="'跳到子会话 ' + c.key">
|
|
113
|
+
<i class="sv-chip" :style="{background: agentColor(sessPick && sessPick.agent)}"></i>
|
|
114
|
+
<span class="sv-tree-kid-t">{{ c.name || c.sub || c.key }}</span>
|
|
115
|
+
<span class="sv-tree-kid-n">{{ c.turns }} 轮 · {{ fmtN(c.total) }} tok</span>
|
|
116
|
+
<span v-if="c.subKids" class="mini-tag" :title="'它还挂着 ' + c.subKids + ' 层后代会话'">+{{ c.subKids }}</span>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<!-- 逐轮用量图:模式由**数据**决定(sessChart 的注释),不按 agent 名硬编码 ——
|
|
122
|
+
token 系画「缓存+输入+输出」堆叠柱,线**只有一根**:拿得到窗口容量的会话画上下文占用率
|
|
123
|
+
(0–100 绝对刻度,顶格=满窗),一根都没有就退回耗时折线;qoder 画积分柱 + 上下文占比线。
|
|
124
|
+
压缩红点两种模式通用(谁落了 compacts 就点谁),无分母时落基线画空心。
|
|
125
|
+
压缩数据自 I12(2026-09-28)起 claude 与 qoder 都会落(tooltip 带自动/手动、丢多少、等多久)。
|
|
126
|
+
全 0(cursor 这类源头就没有用量)时 sessChart 为 null,整块不渲染(卡片上已有「无用量」说明,
|
|
127
|
+
画一排 0 没有意义)。横轴是轮次序号(等宽),真实时间轴在下面概览条里,两图各司其职。
|
|
128
|
+
点柱子 = 定位并展开那一轮,与概览条 sessOvClick 同一语义。 -->
|
|
129
|
+
<div v-if="sessChart" class="sv-chart">
|
|
130
|
+
<div class="sv-chart-h" @click="$emit('update:sess-chart-open', !sessChartOpen)">
|
|
131
|
+
<span class="sess-arrow">{{ sessChartOpen ? '▾' : '▸' }}</span>
|
|
132
|
+
<span>逐轮用量</span>
|
|
133
|
+
<template v-if="sessChart.mode === 'token'">
|
|
134
|
+
<span class="li"><i class="sv-chip" style="background:var(--s-cache)"></i>缓存</span>
|
|
135
|
+
<span class="li"><i class="sv-chip" style="background:var(--s-in)"></i>输入</span>
|
|
136
|
+
<span class="li"><i class="sv-chip" style="background:var(--s-out)"></i>输出</span>
|
|
137
|
+
<template v-if="sessChart.segs.length">
|
|
138
|
+
<span v-if="sessChart.lineKind === 'ctx'" class="li"><i class="sv-chip" style="background:var(--green)"></i>上下文占用率</span>
|
|
139
|
+
<span v-else class="li"><i class="sv-chip" style="background:var(--green)"></i>耗时</span>
|
|
140
|
+
</template>
|
|
141
|
+
</template>
|
|
142
|
+
<template v-else>
|
|
143
|
+
<span class="li"><i class="sv-chip" style="background:var(--s-out)"></i>积分</span>
|
|
144
|
+
<span class="li"><i class="sv-chip" style="background:var(--s-in)"></i>上下文占比</span>
|
|
145
|
+
</template>
|
|
146
|
+
<span v-if="sessChart.hasMark" class="li"><i class="sv-dot"></i>压缩</span>
|
|
147
|
+
<span class="sv-hint">点柱子定位到那一轮</span>
|
|
148
|
+
</div>
|
|
149
|
+
<template v-if="sessChartOpen">
|
|
150
|
+
<div v-if="sessChart.note" class="sv-chart-note">{{ sessChart.note }}</div>
|
|
151
|
+
<div class="sv-chart-body">
|
|
152
|
+
<div v-for="r in sessChart.rows" :key="r.id" class="sv-col" :title="r.tip" @click="$emit('chart-go', r.id)">
|
|
153
|
+
<div class="sv-bar"><i v-for="(seg, si) in r.stack" :key="si" :style="{height: seg.h + '%', background: seg.c}"></i></div>
|
|
154
|
+
<i v-if="r.mark" class="sv-mark" :class="{bare: r.mark.bare}" :style="{bottom: r.mark.y + '%'}"></i>
|
|
155
|
+
</div>
|
|
156
|
+
<svg v-if="sessChart.segs.length" class="sv-line" viewBox="0 0 100 100" preserveAspectRatio="none">
|
|
157
|
+
<polyline v-for="(pts, li) in sessChart.segs" :key="li" :points="pts" fill="none" :stroke="sessChart.lineColor" stroke-width="1.5" vector-effect="non-scaling-stroke" stroke-linejoin="round"/>
|
|
158
|
+
</svg>
|
|
159
|
+
</div>
|
|
160
|
+
<div class="sv-chart-x">
|
|
161
|
+
<span v-for="(r, i) in sessChart.rows" :key="r.id" class="sv-x-tick">{{ i % sessChart.tickEvery === 0 ? '#' + (i + 1) : '' }}</span>
|
|
162
|
+
</div>
|
|
163
|
+
</template>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<!-- hasTimeline 是**服务端**算的(能力名单 EVENT_AGENTS,目前是 dsh / zcode / traedb / opencode / kilo / hermes / devin),页面不硬编码 agent 名单。
|
|
167
|
+
sessHasEvents 是页面侧的自纠正:真加载到 events 就不显示这条告警(防名单漏更新)。
|
|
168
|
+
⚠️ 措辞别写成「这个 agent 的日志没有逐事件时间戳」—— 那是错的:claude/codebuddy/gemini
|
|
169
|
+
的日志里有逐行时间戳,解析器也已经用它算出了每次调用的真实耗时(实测 15/15、5/5、1/1 都有非零 dur),
|
|
170
|
+
只是**算完没留成事件流**。缺的是「留成流」,不是「时间」。 -->
|
|
171
|
+
<el-alert v-if="sessPick && !sessPick.hasTimeline && !sessHasEvents" type="info" :closable="false" show-icon style="margin-bottom:12px"
|
|
172
|
+
title="这个 agent 没有逐事件时间轴"
|
|
173
|
+
description="逐事件流水目前只有 dsh / zcode / traedb(Trae 的 SQLCipher 库)/ opencode / kilo / hermes / devin 的解析器做了保留(服务端 hasTimeline 字段);该 agent 的轮次退回 LLM 调用明细 + 工具调用两块渲染。别的 agent 的日志也带逐行时间戳,只是解析时用来算完每次调用的耗时,没再留成一条事件流。"></el-alert>
|
|
174
|
+
|
|
175
|
+
<!-- 概览条:横轴 = sessSpan([首轮起点, 末轮**结束**],见 sessSpan 的注释),
|
|
176
|
+
宽度 = **真实时间占比**(pctOf),不给任何段画等宽。
|
|
177
|
+
背景轨 = 每一轮(所有轮都有真实 time/dur);前景轨 = 只画**已经展开取过 events 的轮**,
|
|
178
|
+
llm 拆成「等待(ttft)」与「解码(dur)」两段,工具画 call→result 的真实区间。
|
|
179
|
+
**不做滚轮缩放 / 拖动**(既有决策:区间一被随手滚掉,下面的数字就全跟着跳),
|
|
180
|
+
交互只有「点击定位 + 高亮」。 -->
|
|
181
|
+
<div v-if="sessSpan && sessCur.entries.length" class="sv-ov">
|
|
182
|
+
<div class="sv-ov-h">
|
|
183
|
+
<span class="li"><i class="sv-chip" style="background:var(--placeholder)"></i>轮(真实起止)</span>
|
|
184
|
+
<span class="li"><i class="sv-chip" style="background:var(--s-out)"></i>TTFT 等待</span>
|
|
185
|
+
<span class="li"><i class="sv-chip" style="background:var(--s-in)"></i>解码</span>
|
|
186
|
+
<span class="li"><i class="sv-chip" style="background:var(--s-cache)"></i>工具(真实耗时)</span>
|
|
187
|
+
<span class="sv-hint">点一下落在最近的一轮并展开 · 前景轨只画已展开过的轮</span>
|
|
188
|
+
</div>
|
|
189
|
+
<div class="sv-ov-body" @click="e => $emit('ov-click', e)">
|
|
190
|
+
<div class="sv-mtrack">
|
|
191
|
+
<i v-for="seg in sessOvTurns" :key="seg.id" class="sv-mseg" :class="{mgap: seg.mgap}"
|
|
192
|
+
:style="{left: seg.left + '%', width: seg.width + '%', background: seg.err ? 'var(--red)' : 'var(--placeholder)'}"
|
|
193
|
+
:title="seg.tip"></i>
|
|
194
|
+
</div>
|
|
195
|
+
<div class="sv-mtrack">
|
|
196
|
+
<i v-for="(seg, si) in sessOvEvents" :key="si" class="sv-mseg" :class="[seg.cls, {mgap: seg.mgap}]"
|
|
197
|
+
:style="{left: seg.left + '%', width: seg.width + '%'}" :title="seg.tip"></i>
|
|
198
|
+
</div>
|
|
199
|
+
<i v-if="sessCursor != null" class="sv-cur-line" :style="{left: (sessCursor * 100) + '%'}"></i>
|
|
200
|
+
</div>
|
|
201
|
+
<div class="sv-ov-x">
|
|
202
|
+
<span>{{ fmtTime(sessFrom) }}</span>
|
|
203
|
+
<span class="sv-ov-mid">{{ fmtTime((sessFrom + sessTo) / 2) }}</span>
|
|
204
|
+
<span>{{ fmtTime(sessTo) }}</span>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<!-- 逐轮轨迹:默认全折叠,点开哪轮才拉哪轮(懒加载 + sessDetail 缓存,不重复请求) -->
|
|
209
|
+
<div v-for="(tk, i) in sessCur.entries" :key="tk.id" :id="'sv-turn-' + tk.id" class="sess-turn" :class="{err: tk.status !== 'ok'}">
|
|
210
|
+
<div class="sess-turn-head" @click="$emit('toggle-turn', tk)">
|
|
211
|
+
<span class="sn">#{{ i + 1 }}</span>
|
|
212
|
+
<span class="st-preview">{{ tk.preview || '(无文本预览)' }}</span>
|
|
213
|
+
<span v-if="sessEvIndex[tk.id]" class="mini-tag" :title="'该轮有 ' + sessEvIndex[tk.id].length + ' 条逐事件记录'">轨迹 {{ sessEvIndex[tk.id].length }}</span>
|
|
214
|
+
<span v-if="tk.status !== 'ok'" class="tag tag-err">ERR</span>
|
|
215
|
+
<span class="sd">{{ fmtN(sumTok(tk)) }}</span>
|
|
216
|
+
<span class="sd">{{ fmtDur(tk.dur) }}</span>
|
|
217
|
+
<span class="st-time">{{ tk.timeUnknown ? '时间未知' : fmtTime(tk.time) }}</span>
|
|
218
|
+
<span class="sess-arrow">{{ sessOpen.has(tk.id) ? '▾' : '▸' }}</span>
|
|
219
|
+
</div>
|
|
220
|
+
<div v-if="sessOpen.has(tk.id)" class="sess-turn-body" @click.stop>
|
|
221
|
+
<template v-if="sessDetail[tk.id]">
|
|
222
|
+
<!-- 有逐事件时间戳 → 轨迹。事件按真实时间顺序,但**不保证**严格成对
|
|
223
|
+
(step 可能缺 end、result 可能配不上 call),所以每一行都自己撑住,不假设配对。 -->
|
|
224
|
+
<template v-if="sessEvRows[tk.id]">
|
|
225
|
+
<div class="sv-evs-h">逐事件轨迹({{ sessEvRows[tk.id].length }} 条 · 时间为相对本轮起点的偏移)</div>
|
|
226
|
+
<div class="sv-evs">
|
|
227
|
+
<template v-for="row in sessEvRows[tk.id]" :key="row.key">
|
|
228
|
+
<div class="sv-ev" :class="['k-' + row.k, {on: sessHi === row.key, err: row.err}]" @click="$emit('event-click', tk, row)">
|
|
229
|
+
<span class="sv-ev-t">{{ row.rel }}</span>
|
|
230
|
+
<span class="sv-ev-k">{{ row.label }}</span>
|
|
231
|
+
<!-- llm:TTFT / decode 两段迷你条。两个都是 0(失败调用)时画斜纹占位 + 文案里的「?」,
|
|
232
|
+
不画成 0 宽空气,也不编造时长。 -->
|
|
233
|
+
<span v-if="row.k === 'llm'" class="sv-mini" :title="'等待 ' + (row.ev.ttft || 0) + 'ms + 解码 ' + (row.ev.dur || 0) + 'ms(按本轮最长的一次调用归一)'">
|
|
234
|
+
<i v-if="row.bar.w" class="sv-mini-w" :style="{width: row.bar.w + '%'}"></i>
|
|
235
|
+
<i v-if="row.bar.d" class="sv-mini-d" :style="{width: row.bar.d + '%'}"></i>
|
|
236
|
+
<i v-if="row.zero" class="sv-mini-z"></i>
|
|
237
|
+
</span>
|
|
238
|
+
<span v-if="row.k === 'llm'" class="sv-ev-ttft">TTFT {{ row.ev.ttft ? row.ev.ttft + 'ms' : '?' }} │ decode {{ row.ev.dur ? row.ev.dur + 'ms' : '?' }}</span>
|
|
239
|
+
<span class="sv-ev-sub">{{ row.sub }}</span>
|
|
240
|
+
<span v-if="row.tool" class="sess-arrow">{{ sessHi === row.key ? '▾' : '▸' }}</span>
|
|
241
|
+
</div>
|
|
242
|
+
<!-- 工具入参/返回:正文只在 tools[ev.i] 里存一份(事件里不存副本),
|
|
243
|
+
这里按同一套折叠 / 复制渲染;截断时走 sessLoadFull 二次拉取。 -->
|
|
244
|
+
<div v-if="row.tool && sessHi === row.key" class="sv-ev-json" @click.stop>
|
|
245
|
+
<template v-if="row.tool.input">
|
|
246
|
+
<div class="tool-sec-h" @click="$emit('fold-json', tk.id, row.ev.i, 'in')">{{ toolJsonFold.has(tk.id + ':' + row.ev.i + ':in') ? '▸ 入参' : '▾ 入参' }}</div>
|
|
247
|
+
<pre v-show="!toolJsonFold.has(tk.id + ':' + row.ev.i + ':in')">{{ row.tool._inPretty || prettyJson(row.tool.input) }}</pre>
|
|
248
|
+
</template>
|
|
249
|
+
<span v-if="row.tool.inputTrunc && !sessFullIds.has(tk.id)" class="more-btn" @click="$emit('load-full', tk.id)">输入已截断,查看完整内容</span>
|
|
250
|
+
<template v-if="row.tool.output">
|
|
251
|
+
<div class="tool-sec-h" @click="$emit('fold-json', tk.id, row.ev.i, 'out')">{{ toolJsonFold.has(tk.id + ':' + row.ev.i + ':out') ? '▸ 返回' : '▾ 返回' }}</div>
|
|
252
|
+
<pre v-show="!toolJsonFold.has(tk.id + ':' + row.ev.i + ':out')">{{ row.tool._outPretty || prettyJson(row.tool.output) }}</pre>
|
|
253
|
+
</template>
|
|
254
|
+
<span v-if="row.tool.outputTrunc && !sessFullIds.has(tk.id)" class="more-btn" @click="$emit('load-full', tk.id)">输出已截断,查看完整内容</span>
|
|
255
|
+
<span class="tool-copy" @click="copyRaw($event, (row.tool.input || '') + (row.tool.output ? '\\n' + row.tool.output : ''))">复制</span>
|
|
256
|
+
</div>
|
|
257
|
+
</template>
|
|
258
|
+
</div>
|
|
259
|
+
</template>
|
|
260
|
+
|
|
261
|
+
<!-- 没有逐事件(不在 EVENT_AGENTS,或这轮没落盘)→ 退回 LLM 调用明细 + 工具调用两块。
|
|
262
|
+
⚠️ 措辞:缺的是「把逐事件留成一条流」,**不是**「日志里没有时间戳」——
|
|
263
|
+
claude/codebuddy/gemini 的日志有逐行时间戳,下面那张「LLM 调用明细」里的耗时
|
|
264
|
+
就是解析器用它们算出来的(实测 15/15、5/5、1/1 非零)。 -->
|
|
265
|
+
<template v-else>
|
|
266
|
+
<div class="calls-note">这一轮没有逐事件时间轴,退回 LLM 调用明细 + 工具调用两块渲染。</div>
|
|
267
|
+
<template v-if="sessDetail[tk.id].calls && sessDetail[tk.id].calls.length">
|
|
268
|
+
<h4>LLM 调用明细({{ sessDetail[tk.id].calls.length }} 次)</h4>
|
|
269
|
+
<div class="call-list">
|
|
270
|
+
<div v-for="(c, ci) in sessDetail[tk.id].calls" :key="ci" class="call-item">
|
|
271
|
+
<div class="call-row">
|
|
272
|
+
<span class="ci"><i v-if="c.text" class="call-dot" title="这次调用有正文,可点开"></i>#{{ ci + 1 }}</span>
|
|
273
|
+
<span class="cm">{{ c.model || 'generation' }}</span>
|
|
274
|
+
<span v-if="c.tools && c.tools.length" class="call-tools" :title="'这次调用发起了:' + toolText(c.tools, 99)">{{ toolText(c.tools) }}</span>
|
|
275
|
+
<span v-if="c.error" class="cerr">✗ {{ c.error }}</span>
|
|
276
|
+
<span class="cd">{{ fmtDur(c.dur) }}</span>
|
|
277
|
+
<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>
|
|
278
|
+
</div>
|
|
279
|
+
<!-- 同卡片详情:这次调用自己说了什么。折叠开关按 tk.id 独立记,
|
|
280
|
+
与卡片里的 details 缓存不是一份(见 sessLoadFull 上面那段说明) -->
|
|
281
|
+
<template v-if="c.text">
|
|
282
|
+
<div class="tool-sec-h call-out-h" @click="$emit('fold-callout', tk.id, ci)">{{ callOpen.has(tk.id + ':' + ci) ? '▾ 本次输出' : '▸ 本次输出' }}</div>
|
|
283
|
+
<pre v-show="callOpen.has(tk.id + ':' + ci)" class="call-out">{{ c.text }}</pre>
|
|
284
|
+
<span v-if="c.textTrunc && !sessFullIds.has(tk.id)" class="more-btn" @click="$emit('load-full', tk.id)">本次输出已截断,查看完整内容</span>
|
|
285
|
+
</template>
|
|
286
|
+
</div>
|
|
287
|
+
</div>
|
|
288
|
+
</template>
|
|
289
|
+
<div v-else-if="sessDetail[tk.id].callsNote" class="calls-note">{{ sessDetail[tk.id].callsNote }}</div>
|
|
290
|
+
<template v-if="sessDetail[tk.id].tools && sessDetail[tk.id].tools.length">
|
|
291
|
+
<h4>工具调用({{ sessDetail[tk.id].tools.length }})</h4>
|
|
292
|
+
<div v-for="(tt, ti) in sessDetail[tk.id].tools" :key="ti" class="tool-item">
|
|
293
|
+
<div class="tool-head">
|
|
294
|
+
<span class="tname">{{ tt.name }}</span>
|
|
295
|
+
<span v-if="tt.error" class="terr">✗ {{ tt.error }}</span>
|
|
296
|
+
<span v-if="tt.dur != null" class="tdur">{{ fmtDur(tt.dur) }}</span>
|
|
297
|
+
<span class="tool-copy" @click="copyRaw($event, (tt.input || '') + (tt.output ? '\\n' + tt.output : ''))">复制</span>
|
|
298
|
+
</div>
|
|
299
|
+
<template v-if="tt.input">
|
|
300
|
+
<div class="tool-sec-h" @click="$emit('fold-json', tk.id, ti, 'in')">{{ toolJsonFold.has(tk.id + ':' + ti + ':in') ? '▸ 入参' : '▾ 入参' }}</div>
|
|
301
|
+
<pre v-show="!toolJsonFold.has(tk.id + ':' + ti + ':in')">{{ tt._inPretty || prettyJson(tt.input) }}</pre>
|
|
302
|
+
</template>
|
|
303
|
+
<span v-if="tt.inputTrunc && !sessFullIds.has(tk.id)" class="more-btn" @click="$emit('load-full', tk.id)">输入已截断,查看完整内容</span>
|
|
304
|
+
<template v-if="tt.output">
|
|
305
|
+
<div class="tool-sec-h" @click="$emit('fold-json', tk.id, ti, 'out')">{{ toolJsonFold.has(tk.id + ':' + ti + ':out') ? '▸ 返回' : '▾ 返回' }}</div>
|
|
306
|
+
<pre v-show="!toolJsonFold.has(tk.id + ':' + ti + ':out')">{{ tt._outPretty || prettyJson(tt.output) }}</pre>
|
|
307
|
+
</template>
|
|
308
|
+
<span v-if="tt.outputTrunc && !sessFullIds.has(tk.id)" class="more-btn" @click="$emit('load-full', tk.id)">输出已截断,查看完整内容</span>
|
|
309
|
+
</div>
|
|
310
|
+
</template>
|
|
311
|
+
</template>
|
|
312
|
+
|
|
313
|
+
<!-- 非文本 part(zcode 的状态事件 / 推理锚点 / 未知类型):不静默丢弃,
|
|
314
|
+
与时间轴 / 退回两块都并存,所以放在两条渲染路径之外 -->
|
|
315
|
+
<template v-if="sessDetail[tk.id].others && sessDetail[tk.id].others.length">
|
|
316
|
+
<h4>其他事件({{ sessDetail[tk.id].others.length }})</h4>
|
|
317
|
+
<div v-for="(o, oi) in sessDetail[tk.id].others" :key="oi">
|
|
318
|
+
<div class="other-item"><span class="ot">{{ o.type }}</span><span class="od">{{ o.t ? fmtTime(o.t) : '' }}</span></div>
|
|
319
|
+
<pre v-if="o.json" class="other-json">{{ o.json }}</pre>
|
|
320
|
+
</div>
|
|
321
|
+
</template>
|
|
322
|
+
|
|
323
|
+
<!-- 正文两块:轨迹说的是「怎么跑的」,正文说的是「跑了什么」,两条渲染路径都要给 -->
|
|
324
|
+
<h4>用户输入 <span class="copy-btn" title="复制原文" @click="copyRaw($event, sessDetail[tk.id].user)">复制</span></h4>
|
|
325
|
+
<pre>{{ sessDetail[tk.id].user || '(空)' }}</pre>
|
|
326
|
+
<template v-if="sessDetail[tk.id].assistant">
|
|
327
|
+
<h4>AI 输出 <span class="copy-btn" title="复制原文(非渲染后文本)" @click="copyRaw($event, sessDetail[tk.id].assistant)">复制</span></h4>
|
|
328
|
+
<!-- _assistantMd 由 prepDetail 缓存(见卡片详情同款注释) -->
|
|
329
|
+
<div class="md-body" v-html="sessDetail[tk.id]._assistantMd"></div>
|
|
330
|
+
</template>
|
|
331
|
+
</template>
|
|
332
|
+
<el-skeleton v-else :rows="2" animated></el-skeleton>
|
|
333
|
+
<!-- 轮详情底部的「收起」:复用卡片的 .detail-foot(吸底 + 渐变上沿,十几屏高也随时点得到)。
|
|
334
|
+
交互同 collapseCard:折叠后把轮头滚回视野,见 sessCollapseTurn。 -->
|
|
335
|
+
<div class="detail-foot">
|
|
336
|
+
<el-button size="small" @click="$emit('collapse-turn', tk.id, $event)">收起</el-button>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
</template>
|
|
341
|
+
</section>
|
|
342
|
+
</main>`,
|
|
343
|
+
props: {
|
|
344
|
+
// 左栏列表(状态与请求都在根:v-if 销毁子树时这些得活着,见文件头)
|
|
345
|
+
sessSearch: { type: String, default: '' },
|
|
346
|
+
sessSort: { type: String, default: 'to' },
|
|
347
|
+
sessList: { type: Array, default: () => [] },
|
|
348
|
+
sessListFiltered: { type: Array, default: () => [] },
|
|
349
|
+
sessListCapped: Boolean,
|
|
350
|
+
sessListError: { type: String, default: '' },
|
|
351
|
+
sessListLoading: Boolean,
|
|
352
|
+
// 右栏:选中的会话 + /api/session 聚合 + 逐轮懒加载的详情缓存
|
|
353
|
+
sessPick: { type: Object, default: null },
|
|
354
|
+
sessCur: { type: Object, default: null },
|
|
355
|
+
sessCurLoading: Boolean,
|
|
356
|
+
sessOpen: { type: Object, default: () => new Set() },
|
|
357
|
+
sessDetail: { type: Object, default: () => ({}) },
|
|
358
|
+
sessFullIds: { type: Object, default: () => new Set() },
|
|
359
|
+
sessHi: { type: String, default: '' },
|
|
360
|
+
sessCursor: { type: Number, default: null },
|
|
361
|
+
sessSpan: { type: Object, default: null },
|
|
362
|
+
sessFrom: { type: Number, default: 0 },
|
|
363
|
+
sessTo: { type: Number, default: 0 },
|
|
364
|
+
sessHasEvents: Boolean,
|
|
365
|
+
sessOvTurns: { type: Array, default: () => [] },
|
|
366
|
+
sessOvEvents: { type: Array, default: () => [] },
|
|
367
|
+
sessEvIndex: { type: Object, default: () => ({}) },
|
|
368
|
+
sessEvRows: { type: Object, default: () => ({}) },
|
|
369
|
+
sessChart: { type: Object, default: null },
|
|
370
|
+
sessChartOpen: Boolean,
|
|
371
|
+
// 折叠开关两份:与卡片详情共用同一份根状态(同一个 Set 在两个视图里都可能出现)
|
|
372
|
+
toolJsonFold: { type: Object, default: () => new Set() },
|
|
373
|
+
callOpen: { type: Object, default: () => new Set() },
|
|
374
|
+
searchIcon: { type: [Object, Function], default: null },
|
|
375
|
+
},
|
|
376
|
+
emits: ['update:sess-search', 'update:sess-sort', 'update:sess-chart-open', 'pick-session',
|
|
377
|
+
'toggle-turn', 'event-click', 'ov-click', 'chart-go', 'collapse-turn', 'load-full',
|
|
378
|
+
'fold-json', 'fold-callout', 'open-session'],
|
|
379
|
+
setup() {
|
|
380
|
+
return { fmtN, fmtDur, fmtDurBig, fmtTime, fmtAgo, sumTok, modelText, toolText, prettyJson, copyRaw, agentColor, agentIcon };
|
|
381
|
+
}
|
|
382
|
+
};
|