Files
yunhai-agent-front/src/components/ToolTraceCard.vue
T
2026-10-12 14:45:00 +08:00

342 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
/**
* 工具执行轨迹卡片:让用户看清"智能体到底动了系统的哪个接口、成功没有"。
* 这是把「黑盒 AI」变成「可信执行器」的关键 —— 也让审计有据可查。
* 模块标签沿用侧栏同一套色调,保证"同一个模块在哪里都是同一个颜色"。
*/
import { ref } from "vue";
import { MODULE_LABEL, moduleMeta, moduleUrl } from "@/assets/config/modules";
import type { ToolCallTrace } from "@/types/agent";
defineProps<{ traces: ToolCallTrace[] }>();
const expanded = ref<Record<number, boolean>>({});
function toggle(i: number) {
expanded.value[i] = !expanded.value[i];
}
/** 参数展示:过滤掉内部字段,值过长时截断 */
function prettyArgs(args?: Record<string, any>): string {
if (!args || !Object.keys(args).length) return "(无参数)";
const entries = Object.entries(args).map(([k, v]) => {
let val = typeof v === "object" ? JSON.stringify(v) : String(v);
if (val.length > 80) val = val.slice(0, 80) + "…";
return `${k}: ${val}`;
});
return entries.join(",");
}
function openLanding(t: ToolCallTrace) {
const url = moduleUrl(t.module, t.landingPath);
if (url) window.open(url, "_blank");
}
/**
* 是否显示「去系统里查看」入口。
*
* - 写操作成功:去系统里确认结果(原有行为);
* - **任何失败**:服务端调不通模块用户态接口(下游 code=302,只认浏览器 CAS 会话),
* 此时由用户带自己的登录态打开模块页面,是唯一能看到真实数据的路径 —— 这是「深链兜底」。
*/
function canGo(t: ToolCallTrace): boolean {
if (!t.module) return false;
// 写操作(无论成败)→ 去系统里确认/补办;只读失败(如下游 302)→ 去系统里自己看。
// 只读成功不显示:数据已经在回复里了。
return t.write === true || !t.success;
}
/** 按钮文案:成功是"去确认",失败是"自己去看" */
function goLabel(t: ToolCallTrace): string {
return t.success ? "去系统里查看" : "去系统里自己查看";
}
</script>
<template>
<div class="trace">
<div
v-for="(t, i) in traces"
:key="i"
class="trace__item"
:class="[t.success ? 'trace__item--ok' : 'trace__item--fail']"
>
<div class="trace__head" @click="toggle(i)">
<span class="trace__badge" :class="t.write ? 'trace__badge--write' : 'trace__badge--read'">
{{ t.write ? "操作" : "查询" }}
</span>
<span class="trace__title">{{ t.title }}</span>
<span v-if="t.module" class="trace__module" :class="`tone-${moduleMeta(t.module).tone}`">
{{ MODULE_LABEL[t.module] || t.module }}
</span>
<span class="trace__spacer" />
<span class="trace__status">
<svg v-if="t.success" viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 12.5l5 5 11-11" />
</svg>
<svg v-else viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round">
<path d="M6 6l12 12M18 6L6 18" />
</svg>
<!-- 失败原因是自然语言(可能很长)→ 这里只显示一行省略号,完整内容在「详情」里看 -->
<span class="trace__status-text" :title="t.success ? '' : t.error || ''">
<template v-if="t.success">完成{{ t.costMs ? ` · ${t.costMs}ms` : "" }}</template>
<template v-else>{{ t.error || "失败" }}</template>
</span>
</span>
<span class="trace__toggle">
{{ expanded[i] ? "收起" : "详情" }}
<svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" :class="{ 'trace__chev--up': expanded[i] }">
<path d="M6 9l6 6 6-6" />
</svg>
</span>
</div>
<div v-if="expanded[i]" class="trace__body">
<div v-if="!t.success && t.error" class="trace__row">
<span class="trace__k">原因</span>
<span class="trace__v trace__v--err">{{ t.error }}</span>
</div>
<div class="trace__row">
<span class="trace__k">接口</span>
<code class="trace__code">{{ t.name }}</code>
</div>
<div class="trace__row">
<span class="trace__k">参数</span>
<span class="trace__v">{{ prettyArgs(t.arguments) }}</span>
</div>
</div>
<div v-if="canGo(t)" class="trace__actions">
<button
class="trace__go"
:class="{ 'trace__go--fallback': !t.success }"
type="button"
@click="openLanding(t)"
>
{{ goLabel(t) }}
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12h13M12 6l6 6-6 6" />
</svg>
</button>
<span v-if="!t.success" class="trace__go-hint">
服务端拿不到你在业务系统的登录态,用你自己的登录态打开即可看到真实数据
</span>
</div>
</div>
</div>
</template>
<style scoped>
.trace {
display: flex;
flex-direction: column;
gap: 7px;
margin-top: 9px;
}
.trace__item {
border: 1px solid var(--agent-border);
border-left: 3px solid var(--agent-success);
border-radius: 10px;
background: var(--agent-surface);
padding: 9px 12px;
transition: box-shadow 0.16s ease;
}
.trace__item--fail {
border-left-color: var(--agent-danger);
background: #fffbfb;
}
/* ------------------------------ 头部 ------------------------------ */
.trace__head {
display: flex;
align-items: center;
gap: 9px;
cursor: pointer;
font-size: 12.5px;
min-width: 0;
}
.trace__badge {
flex: none;
padding: 2px 7px;
border-radius: 5px;
font-size: 11px;
font-weight: 500;
letter-spacing: 0.3px;
}
.trace__badge--read {
background: var(--agent-primary-soft);
color: var(--agent-primary-dark);
}
.trace__badge--write {
background: #fdf2e2;
color: var(--agent-warn);
}
.trace__title {
flex: none;
color: var(--agent-text);
font-weight: 550;
}
.trace__module {
flex: none;
padding: 2px 8px;
border-radius: 999px;
font-size: 11px;
color: var(--tone);
background: var(--tone-soft);
}
.trace__spacer {
flex: 1 1 auto;
}
.trace__status {
/* ★ 可收缩:失败原因是自然语言(可能很长),必须让它让步给「详情」按钮,
否则整张卡会被撑破、右侧按钮跑出视野(见 2026-10-10 截图) */
flex: 0 1 auto;
min-width: 0;
max-width: 46%;
display: inline-flex;
align-items: center;
gap: 4px;
color: var(--agent-success);
font-size: 12px;
}
.trace__status > svg {
flex: none;
}
.trace__status-text {
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.trace__item--fail .trace__status {
color: var(--agent-danger);
}
.trace__toggle {
flex: none;
display: inline-flex;
align-items: center;
gap: 3px;
color: var(--agent-text-mute);
font-size: 12px;
transition: color 0.16s ease;
}
.trace__head:hover .trace__toggle {
color: var(--agent-primary);
}
.trace__chev--up {
transform: rotate(180deg);
}
/* ------------------------------ 展开体 ------------------------------ */
.trace__body {
margin-top: 9px;
padding-top: 9px;
border-top: 1px dashed var(--agent-border);
font-size: 12.5px;
color: var(--agent-text-sub);
}
.trace__row {
display: flex;
gap: 9px;
margin-bottom: 5px;
word-break: break-all;
}
.trace__row:last-child {
margin-bottom: 0;
}
.trace__k {
flex: none;
width: 32px;
color: var(--agent-text-mute);
}
.trace__code {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
font-size: 11.5px;
padding: 1px 6px;
border-radius: 5px;
color: var(--agent-primary-dark);
background: var(--agent-primary-soft);
}
.trace__v {
color: var(--agent-text);
}
.trace__v--err {
color: var(--agent-danger, #d4380d);
}
/* ------------------------------ 动作 ------------------------------ */
.trace__actions {
margin-top: 9px;
display: flex;
align-items: center;
gap: 9px;
flex-wrap: wrap;
}
.trace__go {
display: inline-flex;
align-items: center;
gap: 5px;
height: 28px;
padding: 0 12px;
border-radius: 8px;
border: 1px solid var(--agent-primary-line);
background: var(--agent-primary-soft);
color: var(--agent-primary-dark);
font-family: inherit;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: background 0.16s ease, border-color 0.16s ease;
}
.trace__go:hover {
background: #dfeaff;
border-color: var(--agent-primary);
}
/* 深链兜底:查询失败时用稍弱的中性色,与"成功后的去确认"区分开 */
.trace__go--fallback {
border-color: var(--agent-border);
background: #fff;
color: var(--agent-text);
}
.trace__go--fallback:hover {
background: #f6f8fb;
border-color: var(--agent-primary);
color: var(--agent-primary-dark);
}
/* 解释"为什么要去系统里看" —— 一句话说清,避免用户以为智能体坏了 */
.trace__go-hint {
flex: 1 1 auto;
min-width: 0;
font-size: 11.5px;
color: var(--agent-text-mute);
line-height: 1.4;
}
</style>