NodeRunLog.vue
5.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
<template>
<div class="run-log">
<!-- 双轨(§三.4 AgentChat.vue 的 live map + history list):
进行中的这次 attempt 走 live 卡片持续追加;已完成的 attempt 落成历史卡片,
外观一致、只少一个 LIVE 徽标,切换时内容不丢(两轨的数据来源是同一个事件缓冲) -->
<div v-for="c in cards" :key="c.key" class="log-card" :class="c.status">
<div class="log-card-head" @click="toggle(c.key)">
<span class="ico">{{ icon(c.status) }}</span>
<span class="who">attempt #{{ c.attempt + 1 }}</span>
<span v-if="c.live" class="badge-live">LIVE</span>
<span class="cnt">{{ c.lines.length }} 条</span>
<span class="grow" />
<span class="tog">{{ collapsed[c.key] ? '▶' : '▼' }}</span>
</div>
<div v-if="!collapsed[c.key]" class="log-card-body terminal">
<div v-for="(l, i) in c.lines" :key="i" class="line">
<span class="ts">{{ l.ts }}</span>
<span class="kind">[{{ l.kind }}]</span>
<span class="txt">{{ l.text }}</span>
</div>
<div v-if="!c.lines.length" class="muted">等待输出…</div>
</div>
</div>
<!-- 这一轨是**引擎事件**(node.status / agent.call / node.output …)。
§四.B.3 第 3 条还要求展示 agent 调用内部的对话与工具调用记录,那份数据目前
只活在引擎进程内(AgentManager.callAgent 的 onAcpEvent / onData 回调),
没有任何通道送到浏览器 —— 标清楚,不拿引擎事件冒充它。 -->
<p class="track-note">
引擎事件轨。<b>agent 调用内部</b>的对话 / 工具调用记录尚未透出(需引擎把 onAcpEvent 转成
<code>agent.chunk</code> / <code>agent.tool</code> 事件),到位后在这里并轨显示。
</p>
<p v-if="!cards.length" class="muted pad">
这个节点还没有事件。<br>
事件来自 <code>GET /api/graph-runs/:id/stream</code>(带 <code>lastEventId=0</code> 全量重放)。
</p>
</div>
</template>
<script setup>
import { computed, reactive } from 'vue';
const props = defineProps({
node: { type: Object, default: null },
events: { type: Array, default: () => [] }, // GraphRunPane 的 SSE 缓冲(历史 + 实时同一条轨)
});
const collapsed = reactive({});
function toggle(k) { collapsed[k] = !collapsed[k]; }
/** 事件按 attempt 分组成卡片;当前 attempt 且节点在跑 = live 卡片,其余 = 历史卡片 */
const cards = computed(() => {
const n = props.node;
if (!n) return [];
const groups = new Map();
for (const e of props.events) {
const d = e.data || {};
const ref = d.node_run_id || d.ref;
if (ref !== n.id) continue;
const attempt = d.attempt != null ? d.attempt : (n.attempt || 0);
if (!groups.has(attempt)) groups.set(attempt, []);
groups.get(attempt).push({
ts: fmtTs(e.ts),
kind: e.kind,
text: d.summary || d.to || JSON.stringify(stripNoise(d)),
});
}
const live = n.status === 'running';
return [...groups.entries()]
.sort((a, b) => b[0] - a[0])
.map(([attempt, lines]) => ({
key: `${n.id}-${attempt}`,
attempt,
lines,
live: live && attempt === (n.attempt || 0),
status: live && attempt === (n.attempt || 0) ? 'running' : n.status,
}));
});
function stripNoise(d) {
const { run_id, root_run_id, node_run_id, ts, ...rest } = d || {};
return rest;
}
function fmtTs(ts) {
const d = new Date(typeof ts === 'string' ? Date.parse(ts) : ts);
return Number.isNaN(d.getTime()) ? '--:--:--' : d.toTimeString().slice(0, 8);
}
function icon(s) {
return s === 'running' ? '🚀' : s === 'success' ? '✅' : s === 'fail' ? '❌' : s === 'blocked' ? '⛔' : '⏳';
}
</script>
<style scoped>
.run-log { display:flex; flex-direction:column; gap:8px; }
.log-card { border:1px solid #e6e6ea; border-radius:6px; overflow:hidden; }
.log-card.running { border-color:#93b4e0; }
.log-card.fail { border-color:#e0a0a0; }
.log-card-head { display:flex; align-items:center; gap:8px; padding:6px 10px; background:#f7f7f9; cursor:pointer; font-size:12px; }
.grow { flex:1; }
.who { font-weight:600; }
.cnt { color:#999; font-size:11px; }
.badge-live { background:#d33; color:#fff; font-size:9px; padding:1px 5px; border-radius:7px; font-weight:700; }
.tog { color:#888; }
.log-card-body { max-height:280px; overflow-y:auto; padding:8px 10px; }
.terminal { background:#0f1115; color:#e6e6e6; font-family: ui-monospace, monospace; font-size:11px; }
.line { display:flex; gap:7px; margin-bottom:3px; }
.ts { color:#7da; flex:0 0 60px; }
.kind { color:#ffce6a; flex:0 0 auto; }
.txt { color:#bbd; overflow-wrap:anywhere; flex:1; }
.track-note { font-size:11px; color:#8a6d3b; background:#fdf8ec; border:1px solid #f0e2c0; border-radius:5px; padding:6px 8px; margin:0; line-height:1.6; }
.track-note code { background:#fff; border:1px solid #eadfc2; padding:0 4px; border-radius:3px; }
.muted { color:#999; font-size:12px; }
.muted code { background:#f0f0f2; padding:1px 4px; border-radius:3px; font-size:11px; }
.pad { padding:14px 4px; line-height:1.7; }
</style>