|
|
|
@@ -690,7 +690,8 @@ function canOpenSettings(msg: AiMessage): boolean {
|
|
|
|
|
* 错误是否可重试(显隐「重试」按钮)。
|
|
|
|
|
* Fatal 类错误(鉴权/Provider 配置错——4xx/401/403/invalid_api_key)重试必再败,
|
|
|
|
|
* 隐藏重试按钮,引导用户走「去设置」。network/timeout/unknown 为瞬时错误,可重试。
|
|
|
|
|
* retryable 推断依据后端 error_type(本批前端推断;后端 AiStreamRetry/AiError 加 retryable 字段下批 df-ai 增强)。
|
|
|
|
|
* error_type 由后端 emit 点分类(2026-10-04 起 emit_fatal_error 对模型不存在/令牌无权/
|
|
|
|
|
* 参数拒绝类 400 归 provider_config);后端判不了的真 Unknown 仍可重试,维持现状。
|
|
|
|
|
*/
|
|
|
|
|
function canRetry(msg: AiMessage): boolean {
|
|
|
|
|
// 重试过渡气泡(isRetry)非终态错误,不提供重试入口(终态 AiError 才有重试按钮)
|
|
|
|
@@ -1077,11 +1078,22 @@ const renderItems = computed<RenderItem[]>(() => {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
flushBucket()
|
|
|
|
|
// S1 子代理完成卡追加在消息流末尾:子代理在父轮内运行,完成语义上晚于当轮内容;
|
|
|
|
|
// S1 子代理完成卡:按完成时刻插到所属轮次桶尾,不恒贴消息流最底部——恒贴时用户继续对话,
|
|
|
|
|
// 早前轮次的卡被顶到后面内容之下,与所属轮次脱钩。归桶判据:finishedAt 落在哪桶的跨度内
|
|
|
|
|
// (trace.startTs ≤ finishedAt < 下一桶),卡即插到该桶 trace 卡之后与其同层(spawn 就发生
|
|
|
|
|
// 在这个桶里;桶内末条答复的落库时刻必晚于 finishedAt,时间比较天然落在这里)。
|
|
|
|
|
// 无时间语义的项(step/timesep/sep/agentFin)不作锚点,防插进时间线分隔的语义缝隙。
|
|
|
|
|
// 逐卡一项(key 按 agentId 稳定),同会话多子代理多卡共存,Vue diff 复用不闪。
|
|
|
|
|
// 真相源是 aiShared.convAgentFinished(per-conv),切会话重进仍可见,非组件局部状态。
|
|
|
|
|
for (const card of getConvAgentFinished(store.state.activeConversationId)) {
|
|
|
|
|
items.push({ kind: 'agentFin', key: 'agentfin-' + card.agentId, card })
|
|
|
|
|
const cards = [...getConvAgentFinished(store.state.activeConversationId)].sort((a, b) => a.finishedAt - b.finishedAt)
|
|
|
|
|
for (const card of cards) {
|
|
|
|
|
let at = items.length
|
|
|
|
|
for (let i = items.length - 1; i >= 0; i--) {
|
|
|
|
|
const it = items[i]
|
|
|
|
|
const ts = it.kind === 'trace' ? it.startTs : it.kind === 'msg' ? it.msg.timestamp : 0
|
|
|
|
|
if (ts && ts <= card.finishedAt) { at = i + 1; break }
|
|
|
|
|
}
|
|
|
|
|
items.splice(at, 0, { kind: 'agentFin', key: 'agentfin-' + card.agentId, card })
|
|
|
|
|
}
|
|
|
|
|
return items
|
|
|
|
|
})
|
|
|
|
@@ -1094,18 +1106,49 @@ function agentFinTone(status: AgentFinishedCard['status']): 'ok' | 'warn' | 'err
|
|
|
|
|
if (status === 'failed') return 'err'
|
|
|
|
|
return 'ok'
|
|
|
|
|
}
|
|
|
|
|
// 状态词独立于颜色透出(色弱可用);键与后端 SubAgentStatus::as_str 字面量一一对应
|
|
|
|
|
function agentFinStatusText(status: AgentFinishedCard['status']): string {
|
|
|
|
|
if (status === 'truncated') return t('ai.agentFinished.truncated')
|
|
|
|
|
if (status === 'failed') return t('ai.agentFinished.failed')
|
|
|
|
|
// 状态词独立于颜色透出(色弱可用);键与后端 SubAgentStatus::as_str 字面量一一对应。
|
|
|
|
|
// stop_kind 结构化优先(根治启发式):后端终态枚举直达语义,截断三细分各自透出;
|
|
|
|
|
// 未知值兜底中性 completed 词不误报。无字段(旧后端)回退 summary 启发式:用户停止
|
|
|
|
|
// 级联取消子代理时 stop_reason=「已被取消(...)」并经 summary 终止说明透出(后端两处
|
|
|
|
|
// 取消检查点均写此中文字面量),按该特征分流为「已取消」;无此特征才是系统截断。
|
|
|
|
|
function agentFinStatusText(card: AgentFinishedCard): string {
|
|
|
|
|
const k = card.stop_kind
|
|
|
|
|
if (k) {
|
|
|
|
|
if (k === 'cancelled') return t('ai.agentFinished.cancelled')
|
|
|
|
|
if (k === 'max_rounds') return t('ai.agentFinished.truncMaxRounds')
|
|
|
|
|
if (k === 'wall_clock') return t('ai.agentFinished.truncWallClock')
|
|
|
|
|
if (k === 'token_budget') return t('ai.agentFinished.truncTokenBudget')
|
|
|
|
|
if (k === 'empty_convergence') return t('ai.agentFinished.noReport')
|
|
|
|
|
if (k === 'error') return t('ai.agentFinished.failed')
|
|
|
|
|
return t('ai.agentFinished.completed')
|
|
|
|
|
}
|
|
|
|
|
if (card.status === 'truncated') {
|
|
|
|
|
return card.summary.includes('已被取消')
|
|
|
|
|
? t('ai.agentFinished.cancelled')
|
|
|
|
|
: t('ai.agentFinished.truncated')
|
|
|
|
|
}
|
|
|
|
|
if (card.status === 'failed') return t('ai.agentFinished.failed')
|
|
|
|
|
return t('ai.agentFinished.completed')
|
|
|
|
|
}
|
|
|
|
|
// 启动失败型子代理(并发占满/父会话已删/provider 缺失)在进循环前就返回,子会话未落库,
|
|
|
|
|
// 点卡 peek 必报「对话不存在」死胡同;rounds=0 是前端可得的最稳特征(进循环后的失败
|
|
|
|
|
// 也至少有一轮落库记录,可正常 peek)。
|
|
|
|
|
function agentFinNoTrace(card: AgentFinishedCard): boolean {
|
|
|
|
|
return card.status === 'failed' && card.rounds === 0
|
|
|
|
|
}
|
|
|
|
|
// 摘要超长截断:单行细条承载,压平空白后按 160 字截断(对齐轨迹卡 500 字口径的轻量版),
|
|
|
|
|
// 全文经 title hover 兜底不丢信息。
|
|
|
|
|
function agentFinSummary(summary: string): string {
|
|
|
|
|
const s = (summary || '').replace(/\s+/g, ' ').trim()
|
|
|
|
|
return s.length > 160 ? s.slice(0, 160) + '…' : s
|
|
|
|
|
}
|
|
|
|
|
// 被拒工具 chip(P2-1):前 2 枚枚举 + 「+N」收尾防长清单撑爆细条,完整清单经 hover title 查看
|
|
|
|
|
const AGENT_FIN_REJECTED_MAX = 2
|
|
|
|
|
function agentFinRejectedText(list: string[]): string {
|
|
|
|
|
const shown = list.slice(0, AGENT_FIN_REJECTED_MAX).join(' / ')
|
|
|
|
|
const rest = list.length - AGENT_FIN_REJECTED_MAX
|
|
|
|
|
return t('ai.agentFinished.rejected', { tools: rest > 0 ? `${shown} +${rest}` : shown })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── S3 子代理轨迹浮层(完成卡点击展开,只读) ──
|
|
|
|
|
// 状态全部组件局部:浮层关即弃、不缓存轨迹(每次点开重新 peek,宁少勿预留),
|
|
|
|
@@ -1114,10 +1157,17 @@ interface AgentPeekMsg {
|
|
|
|
|
role: 'user' | 'assistant' | 'tool' | 'system'
|
|
|
|
|
text: string
|
|
|
|
|
toolNames: string[]
|
|
|
|
|
// deny envelope 消息标记:命中子代理拒绝信封的 tool 结果,渲染为红字人话而非原始 JSON
|
|
|
|
|
denied?: boolean
|
|
|
|
|
}
|
|
|
|
|
const agentPeekVisible = ref(false)
|
|
|
|
|
const agentPeekLoading = ref(false)
|
|
|
|
|
// 加载中的卡 id:spinner 挂在具体完成卡上而非浮层(浮层此时尚未打开)
|
|
|
|
|
const agentPeekLoadingId = ref<string | null>(null)
|
|
|
|
|
const agentPeekMsgs = ref<AgentPeekMsg[]>([])
|
|
|
|
|
// 浮层头身份标识:agent-{n} 尾段 + 子会话标题(IPC title,子代理常为空则不显)+ 任务书首行,
|
|
|
|
|
// 多代理场景辨认「在看谁」;关浮层即弃
|
|
|
|
|
const agentPeekSubject = ref({ agent: '', convTitle: '', task: '', taskFull: '' })
|
|
|
|
|
// 单条内容截断上限:tool 结果(文件全文/长输出)可能巨大,简版渲染按 4000 字截断兜底,
|
|
|
|
|
// 完整数据仍在库,不做全文渲染(不要求富文本)。
|
|
|
|
|
const AGENT_PEEK_TEXT_MAX = 4000
|
|
|
|
@@ -1125,40 +1175,104 @@ const AGENT_PEEK_TEXT_MAX = 4000
|
|
|
|
|
/**
|
|
|
|
|
* 完成卡点击:peek 子会话轨迹并开浮层。
|
|
|
|
|
* agentId 即子会话 conv id(形态 {parent}::agent-{n},后端 spawn_agent 落库 origin='sub_agent')。
|
|
|
|
|
* 加载中/已开时忽略重复点击(防连点重复 IPC);失败走既有 toast 通道给可见反馈,不开空浮层。
|
|
|
|
|
* 启动失败无轨迹卡直接忽略(agentFinNoTrace);加载中/已开时忽略重复点击(防连点重复 IPC);
|
|
|
|
|
* 失败走既有 toast 通道给可见反馈,不开空浮层。
|
|
|
|
|
*/
|
|
|
|
|
async function openAgentTrace(card: AgentFinishedCard): Promise<void> {
|
|
|
|
|
if (agentFinNoTrace(card)) return
|
|
|
|
|
if (agentPeekVisible.value || agentPeekLoading.value) return
|
|
|
|
|
agentPeekLoading.value = true
|
|
|
|
|
agentPeekLoadingId.value = card.agentId
|
|
|
|
|
try {
|
|
|
|
|
const res = await aiApi.peekConversation(card.agentId)
|
|
|
|
|
const raw: any[] = typeof res.messages === 'string' ? JSON.parse(res.messages) : (res.messages || [])
|
|
|
|
|
// tool_call_id → 工具名映射:tool 消息只带 id,名字在前条 assistant.tool_calls 里
|
|
|
|
|
const toolNameById = new Map<string, string>()
|
|
|
|
|
for (const m of raw) {
|
|
|
|
|
if (Array.isArray(m.tool_calls)) {
|
|
|
|
|
for (const tc of m.tool_calls) {
|
|
|
|
|
if (tc?.id && tc?.function?.name) toolNameById.set(String(tc.id), String(tc.function.name))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
agentPeekMsgs.value = raw.map((m) => {
|
|
|
|
|
const text = String(m.content || '')
|
|
|
|
|
const rawText = String(m.content || '')
|
|
|
|
|
const role = m.role === 'user' || m.role === 'assistant' || m.role === 'tool' || m.role === 'system'
|
|
|
|
|
? m.role
|
|
|
|
|
: 'assistant'
|
|
|
|
|
return {
|
|
|
|
|
const msg: AgentPeekMsg = {
|
|
|
|
|
role,
|
|
|
|
|
text: text.length > AGENT_PEEK_TEXT_MAX ? text.slice(0, AGENT_PEEK_TEXT_MAX) + '…' : text,
|
|
|
|
|
text: '',
|
|
|
|
|
toolNames: Array.isArray(m.tool_calls)
|
|
|
|
|
? m.tool_calls.map((tc: any) => tc?.function?.name || '').filter(Boolean)
|
|
|
|
|
: [],
|
|
|
|
|
}
|
|
|
|
|
if (role === 'tool') {
|
|
|
|
|
// 回显工具名 chip(与前条 assistant 消息的调用 chip 对得上,行内可读)
|
|
|
|
|
const callName = toolNameById.get(String(m.tool_call_id || ''))
|
|
|
|
|
if (callName) msg.toolNames.push(callName)
|
|
|
|
|
// deny envelope 先解析后截断(信封很小不会被截),命中转人话不走原文
|
|
|
|
|
const deny = parseDenyEnvelope(rawText)
|
|
|
|
|
if (deny) {
|
|
|
|
|
msg.denied = true
|
|
|
|
|
const risk = agentPeekRiskText(deny.risk_level)
|
|
|
|
|
msg.text = t('ai.agentTrace.deniedTool', { tool: deny.tool_name || callName || '?' })
|
|
|
|
|
+ (risk ? `(${risk})` : '')
|
|
|
|
|
+ (deny.message ? ` · ${deny.message}` : '')
|
|
|
|
|
return msg
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
msg.text = rawText.length > AGENT_PEEK_TEXT_MAX ? rawText.slice(0, AGENT_PEEK_TEXT_MAX) + '…' : rawText
|
|
|
|
|
return msg
|
|
|
|
|
})
|
|
|
|
|
// 任务行取子会话首条 user 消息(即任务书)首行,取不到回退卡摘要;title 挂全文
|
|
|
|
|
const firstUser = raw.find((m) => m.role === 'user')
|
|
|
|
|
const taskLine = String(firstUser?.content || '').split('\n').map((s: string) => s.trim()).filter(Boolean)[0] || ''
|
|
|
|
|
const full = taskLine || (card.summary || '').replace(/\s+/g, ' ').trim()
|
|
|
|
|
agentPeekSubject.value = {
|
|
|
|
|
agent: card.agentId.split('::').pop() || card.agentId,
|
|
|
|
|
convTitle: String(res.title || '').trim(),
|
|
|
|
|
task: full.length > 120 ? full.slice(0, 120) + '…' : full,
|
|
|
|
|
taskFull: full,
|
|
|
|
|
}
|
|
|
|
|
agentPeekVisible.value = true
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('[AI] 拉取子代理轨迹失败:', e)
|
|
|
|
|
emit('toast', { msg: t('ai.agentTrace.loadFail'), type: 'error' })
|
|
|
|
|
} finally {
|
|
|
|
|
agentPeekLoading.value = false
|
|
|
|
|
agentPeekLoadingId.value = null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** deny envelope 识别:与后端 is_sub_agent_deny_envelope 同构(status=rejected + reason=sub_agent_denied) */
|
|
|
|
|
function parseDenyEnvelope(text: string): { tool_name?: string; risk_level?: string; message?: string } | null {
|
|
|
|
|
const s = text.trim()
|
|
|
|
|
if (!s.startsWith('{')) return null
|
|
|
|
|
try {
|
|
|
|
|
const v = JSON.parse(s)
|
|
|
|
|
if (v?.status === 'rejected' && v?.reason === 'sub_agent_denied') {
|
|
|
|
|
return { tool_name: v.tool_name, risk_level: v.risk_level, message: v.message }
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// 截断/非 JSON 内容即非信封,按原文渲染
|
|
|
|
|
}
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 风险级别人话化(后端 RiskLevel serde lowercase,字面量 low/medium/high) */
|
|
|
|
|
function agentPeekRiskText(level: unknown): string {
|
|
|
|
|
if (level === 'low') return t('ai.agentTrace.riskLow')
|
|
|
|
|
if (level === 'medium') return t('ai.agentTrace.riskMedium')
|
|
|
|
|
if (level === 'high') return t('ai.agentTrace.riskHigh')
|
|
|
|
|
return ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 关浮层即弃:清空局部消息态,不缓存轨迹 */
|
|
|
|
|
function closeAgentTrace(): void {
|
|
|
|
|
agentPeekVisible.value = false
|
|
|
|
|
agentPeekMsgs.value = []
|
|
|
|
|
agentPeekSubject.value = { agent: '', convTitle: '', task: '', taskFull: '' }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** role → 本地化角色名(简版渲染,无头像气泡) */
|
|
|
|
@@ -1187,6 +1301,8 @@ watch(() => store.state.activeConversationId, () => {
|
|
|
|
|
setFollowing(true)
|
|
|
|
|
// 切会话重置执行轨迹展开态(展开记忆按会话隔离,防跨会话 key 撞车误展开)
|
|
|
|
|
expandedTraceIds.value = new Set()
|
|
|
|
|
// 切会话关子代理轨迹浮层(浮层是组件级单例,不关则旧会话轨迹叠在新会话上)
|
|
|
|
|
closeAgentTrace()
|
|
|
|
|
// BUG-260624-01(消息重叠根治·论证 workflow 完整性高优):MessageList 是持久单实例(AiChat.vue
|
|
|
|
|
// 无 :key/v-if 卸载),实例级 streamingBlocks(:69)跨会话/跨轮残留是消息重叠主根因之一。
|
|
|
|
|
// 钉在「会话切换」这一确切时机清,根治两条残留路径:① switchConversation(useAiConversations)
|
|
|
|
@@ -1211,6 +1327,7 @@ watch(() => store.state.activeConversationId, () => {
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
cancelPendingRaf()
|
|
|
|
|
document.removeEventListener('click', closeTokenPopoverOnOutsideClick)
|
|
|
|
|
document.removeEventListener('keydown', closeAgentTraceOnEsc)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// token 详情面板:点击外部关闭(token 区内的 @click.stop 已阻冒泡,故文档级点击必为外部)
|
|
|
|
@@ -1219,6 +1336,12 @@ function closeTokenPopoverOnOutsideClick(): void {
|
|
|
|
|
}
|
|
|
|
|
document.addEventListener('click', closeTokenPopoverOnOutsideClick)
|
|
|
|
|
|
|
|
|
|
// 子代理轨迹浮层 Esc 关闭(与遮罩点击/X 同为关闭出口;仅浮层开着时消费,不干扰其他快捷键)
|
|
|
|
|
function closeAgentTraceOnEsc(e: KeyboardEvent): void {
|
|
|
|
|
if (e.key === 'Escape' && agentPeekVisible.value) closeAgentTrace()
|
|
|
|
|
}
|
|
|
|
|
document.addEventListener('keydown', closeAgentTraceOnEsc)
|
|
|
|
|
|
|
|
|
|
// Markdown 预热(父原 loadMarkdown 在 onMounted 调,子组件同样幂等——useMarkdown 单例,
|
|
|
|
|
// 多次调用安全,确保子组件挂载即预热)。
|
|
|
|
|
loadMarkdown()
|
|
|
|
@@ -1321,16 +1444,26 @@ defineExpose({
|
|
|
|
|
<div v-else-if="item.kind === 'agentFin'" class="ai-msg-slot">
|
|
|
|
|
<div
|
|
|
|
|
class="ai-agent-fin"
|
|
|
|
|
:class="'ai-agent-fin--' + agentFinTone(item.card.status)"
|
|
|
|
|
:class="['ai-agent-fin--' + agentFinTone(item.card.status), { 'ai-agent-fin--notrace': agentFinNoTrace(item.card) }]"
|
|
|
|
|
role="button"
|
|
|
|
|
:title="$t('ai.agentFinished.viewHint')"
|
|
|
|
|
:title="agentFinNoTrace(item.card) ? $t('ai.agentFinished.noTrace') : $t('ai.agentFinished.viewHint')"
|
|
|
|
|
@click="openAgentTrace(item.card)"
|
|
|
|
|
>
|
|
|
|
|
<span class="ai-agent-fin-dot"></span>
|
|
|
|
|
<span class="ai-agent-fin-label">{{ $t('ai.agentFinished.title') }}</span>
|
|
|
|
|
<span class="ai-agent-fin-status">{{ agentFinStatusText(item.card.status) }}</span>
|
|
|
|
|
<span class="ai-agent-fin-status">{{ agentFinStatusText(item.card) }}</span>
|
|
|
|
|
<!-- 被拒工具 chip(P2-1):非空才渲染,琥珀警示层与 truncated 同色系;
|
|
|
|
|
单枚直显,多枚前 2 枚枚举 + N,完整清单 hover title -->
|
|
|
|
|
<span
|
|
|
|
|
v-if="item.card.rejected_tools?.length"
|
|
|
|
|
class="ai-agent-fin-rejected"
|
|
|
|
|
:title="item.card.rejected_tools.join('\n')"
|
|
|
|
|
>{{ agentFinRejectedText(item.card.rejected_tools) }}</span>
|
|
|
|
|
<span class="ai-agent-fin-summary" :title="item.card.summary">{{ agentFinSummary(item.card.summary) }}</span>
|
|
|
|
|
<span class="ai-agent-fin-meta">{{ $t('ai.agentFinished.meta', { rounds: item.card.rounds, tokens: formatTokensK(item.card.tokens) }) }}</span>
|
|
|
|
|
<!-- 回补卡 rounds/tokens 未知为 0,双零隐藏 meta 防「0 轮 · 0 tokens」噪音 -->
|
|
|
|
|
<span v-if="item.card.rounds || item.card.tokens" class="ai-agent-fin-meta">{{ $t('ai.agentFinished.meta', { rounds: item.card.rounds, tokens: formatTokensK(item.card.tokens) }) }}</span>
|
|
|
|
|
<!-- peek 加载微标:IPC 期间转圈,防「点了没反应」;挂在对应卡上,多卡各自反馈 -->
|
|
|
|
|
<span v-if="agentPeekLoadingId === item.card.agentId" class="ai-agent-fin-spin" aria-hidden="true"></span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 居中时间分隔线(UX 密度改造:相邻消息间隔 ≥30 分钟或跨天;当天只 HH:mm,跨天 MM-DD HH:mm) -->
|
|
|
|
@@ -1584,19 +1717,28 @@ defineExpose({
|
|
|
|
|
<div v-if="agentPeekVisible" class="ai-agent-peek-mask" @click.self="closeAgentTrace">
|
|
|
|
|
<div class="ai-agent-peek" role="dialog" :aria-label="$t('ai.agentTrace.title')">
|
|
|
|
|
<div class="ai-agent-peek-head">
|
|
|
|
|
<span class="ai-agent-peek-title">{{ $t('ai.agentTrace.title') }}</span>
|
|
|
|
|
<div class="ai-agent-peek-head-text">
|
|
|
|
|
<span class="ai-agent-peek-title">{{ $t('ai.agentTrace.title') }}</span>
|
|
|
|
|
<!-- 身份标识行:agent-{n} + 子会话标题 + 任务书首行,多代理场景辨认「在看谁」 -->
|
|
|
|
|
<div class="ai-agent-peek-subject" :title="agentPeekSubject.taskFull">
|
|
|
|
|
<span class="ai-agent-peek-agent">{{ agentPeekSubject.agent }}</span>
|
|
|
|
|
<span v-if="agentPeekSubject.convTitle" class="ai-agent-peek-conv">{{ agentPeekSubject.convTitle }}</span>
|
|
|
|
|
<span v-if="agentPeekSubject.task" class="ai-agent-peek-task">{{ agentPeekSubject.task }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<button class="ai-agent-peek-close" :title="$t('common.close')" @click="closeAgentTrace">
|
|
|
|
|
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="ai-agent-peek-body">
|
|
|
|
|
<div v-if="agentPeekMsgs.length === 0" class="ai-agent-peek-empty">{{ $t('ai.agentTrace.empty') }}</div>
|
|
|
|
|
<!-- 简版卡片流:role 名 + 工具调用名 chips + 纯文本内容(不做完整 markdown 富渲染) -->
|
|
|
|
|
<!-- 简版卡片流:role 名 + 工具调用名 chips + 纯文本内容(不做完整 markdown 富渲染);
|
|
|
|
|
deny envelope 消息(denied)红字人话,不再直出原始 JSON -->
|
|
|
|
|
<div
|
|
|
|
|
v-for="(m, i) in agentPeekMsgs"
|
|
|
|
|
:key="i"
|
|
|
|
|
class="ai-agent-peek-msg"
|
|
|
|
|
:class="'ai-agent-peek-msg--' + m.role"
|
|
|
|
|
:class="['ai-agent-peek-msg--' + m.role, { 'ai-agent-peek-msg--denied': m.denied }]"
|
|
|
|
|
>
|
|
|
|
|
<div class="ai-agent-peek-role">
|
|
|
|
|
<span>{{ agentPeekRoleText(m.role) }}</span>
|
|
|
|
@@ -2450,6 +2592,40 @@ defineExpose({
|
|
|
|
|
font-family: var(--df-font-mono);
|
|
|
|
|
font-size: 10.5px;
|
|
|
|
|
}
|
|
|
|
|
/* 被拒工具 chip(P2-1):琥珀警示层与 truncated 同色系;限宽防长清单挤掉摘要行 */
|
|
|
|
|
.ai-agent-fin-rejected {
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
max-width: 38%;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
padding: 0 6px;
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
font-size: 10.5px;
|
|
|
|
|
background: color-mix(in srgb, var(--df-warning, #f0c75e) 16%, transparent);
|
|
|
|
|
color: var(--df-warning, #f0c75e);
|
|
|
|
|
}
|
|
|
|
|
/* 无轨迹(启动失败)卡:禁点视觉,hover 不提亮(cursor 维持默认) */
|
|
|
|
|
.ai-agent-fin--notrace {
|
|
|
|
|
cursor: default;
|
|
|
|
|
opacity: 0.72;
|
|
|
|
|
}
|
|
|
|
|
.ai-agent-fin--notrace:hover {
|
|
|
|
|
background: color-mix(in srgb, var(--df-text-secondary) 6%, transparent);
|
|
|
|
|
}
|
|
|
|
|
/* peek 拉取中的加载微标(挂在具体卡右端) */
|
|
|
|
|
.ai-agent-fin-spin {
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
width: 10px;
|
|
|
|
|
height: 10px;
|
|
|
|
|
border: 1.5px solid color-mix(in srgb, var(--df-text-secondary) 35%, transparent);
|
|
|
|
|
border-top-color: var(--df-text-secondary);
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
animation: ai-agent-fin-spin 0.8s linear infinite;
|
|
|
|
|
}
|
|
|
|
|
@keyframes ai-agent-fin-spin {
|
|
|
|
|
to { transform: rotate(360deg); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ═══ S3 子代理轨迹浮层(只读,关即弃) ═══ */
|
|
|
|
|
.ai-agent-peek-mask {
|
|
|
|
@@ -2552,4 +2728,50 @@ defineExpose({
|
|
|
|
|
color: var(--df-text-secondary);
|
|
|
|
|
font-size: 11.5px;
|
|
|
|
|
}
|
|
|
|
|
/* 浮层头身份标识行:置于标题下方,窄容器内单行省略 */
|
|
|
|
|
.ai-agent-peek-head-text {
|
|
|
|
|
flex: 1;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 3px;
|
|
|
|
|
}
|
|
|
|
|
.ai-agent-peek-subject {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
min-width: 0;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: var(--df-text-secondary);
|
|
|
|
|
}
|
|
|
|
|
.ai-agent-peek-agent {
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
font-family: var(--df-font-mono);
|
|
|
|
|
font-size: 10.5px;
|
|
|
|
|
color: var(--df-info);
|
|
|
|
|
background: var(--df-info-bg);
|
|
|
|
|
border-radius: var(--df-radius-sm);
|
|
|
|
|
padding: 1px 6px;
|
|
|
|
|
}
|
|
|
|
|
.ai-agent-peek-conv {
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
max-width: 40%;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
color: var(--df-text);
|
|
|
|
|
}
|
|
|
|
|
.ai-agent-peek-task {
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
color: var(--df-text-dim);
|
|
|
|
|
}
|
|
|
|
|
/* deny envelope 消息:红字人话,置于 tool/system 弱化规则之后以覆盖其色 */
|
|
|
|
|
.ai-agent-peek-msg--denied {
|
|
|
|
|
border-left: 2px solid var(--df-danger);
|
|
|
|
|
}
|
|
|
|
|
.ai-agent-peek-msg--denied .ai-agent-peek-text {
|
|
|
|
|
color: var(--df-danger);
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|