| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894 |
- <script setup>
- import { ref, reactive, onMounted } from 'vue'
- import { NEmpty, NSpin, NIcon, NPopconfirm, NButton, useMessage } from 'naive-ui'
- import { getWorkflowsWithRuns, getRunDetail, deleteRun } from '../../api/runHistory'
- import {
- PlayCircleOutline,
- ChevronForwardOutline,
- TrashOutline,
- ChatbubbleEllipsesOutline,
- SparklesOutline,
- FlaskOutline,
- RocketOutline,
- ColorWandOutline,
- GitBranchOutline,
- ExitOutline,
- DownloadOutline
- } from '@vicons/ionicons5'
- import { unwrapNodeOutput, nodeOutputStatus, nodeOutputMessage } from '../../composables/useWorkflowRunner'
- const message = useMessage()
- const loading = ref(false)
- const workflows = ref([]) // [{ workflowId, workflowName, runs }]
- const selectedRun = ref(null)
- const runDetail = ref(null) // { run, nodes }
- const expandedLogs = reactive({})
- const expandedContext = reactive({})
- /** 思考过程折叠状态:key=nodeId,未设置默认展开 */
- const expandedThinking = reactive({})
- /** 折叠的智能体(workflowId 集合),默认全部展开 */
- const collapsedWfs = reactive({})
- function toggleWfCollapse(workflowId) {
- collapsedWfs[workflowId] = !collapsedWfs[workflowId]
- }
- const nodeTypeInfo = {
- userInput: { color: '#10B981', typeLabel: '用户输入', icon: ChatbubbleEllipsesOutline },
- llm: { color: '#8B5CF6', typeLabel: '大模型', icon: SparklesOutline },
- skill: { color: '#06B6D4', typeLabel: '技能', icon: FlaskOutline },
- agent: { color: '#F59E0B', typeLabel: '智能体', icon: RocketOutline },
- smartAction: { color: '#EC4899', typeLabel: '智能操作', icon: ColorWandOutline },
- condition: { color: '#F97316', typeLabel: '条件', icon: GitBranchOutline },
- output: { color: '#EF4444', typeLabel: '输出', icon: ExitOutline }
- }
- onMounted(loadData)
- async function loadData() {
- loading.value = true
- try {
- const res = await getWorkflowsWithRuns()
- workflows.value = res.data || []
- } catch (e) {
- message.error('加载运行记录失败')
- } finally {
- loading.value = false
- }
- }
- async function selectRun(run) {
- selectedRun.value = run
- // reactive 对象无 .value,需逐键删除以正确重置折叠状态
- Object.keys(expandedLogs).forEach(k => delete expandedLogs[k])
- Object.keys(expandedContext).forEach(k => delete expandedContext[k])
- Object.keys(expandedThinking).forEach(k => delete expandedThinking[k])
- try {
- const res = await getRunDetail(run.id)
- runDetail.value = res.data
- } catch (e) {
- message.error('加载运行详情失败')
- }
- }
- async function handleDelete(runId) {
- try {
- await deleteRun(runId)
- message.success('已删除')
- if (selectedRun.value?.id === runId) {
- selectedRun.value = null
- runDetail.value = null
- }
- await loadData()
- } catch (e) {
- message.error('删除失败')
- }
- }
- function statusText(s) {
- return { running: '执行中', success: '成功', failed: '失败', skipped: '跳过' }[s?.toLowerCase()] || s || '未知'
- }
- function statusClass(s) {
- return 'status-' + (s || 'unknown').toLowerCase()
- }
- function formatTime(t) {
- if (!t) return '-'
- // 兼容数组和字符串格式
- if (Array.isArray(t)) {
- const [y, m, d, h, min, s] = t
- return `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')} ${String(h).padStart(2, '0')}:${String(min).padStart(2, '0')}:${String(s || 0).padStart(2, '0')}`
- }
- return String(t).replace('T', ' ').substring(0, 19)
- }
- function duration(run) {
- if (!run.startedAt || !run.completedAt) return '-'
- let start = new Date(Array.isArray(run.startedAt) ? run.startedAt.join('-') : run.startedAt)
- let end = new Date(Array.isArray(run.completedAt) ? run.completedAt.join('-') : run.completedAt)
- let diff = Math.round((end - start) / 1000)
- if (diff < 60) return diff + ' 秒'
- if (diff < 3600) return Math.floor(diff / 60) + ' 分 ' + (diff % 60) + ' 秒'
- return Math.floor(diff / 3600) + ' 时 ' + Math.floor((diff % 3600) / 60) + ' 分'
- }
- function parseJson(str) {
- if (!str) return null
- try { return JSON.parse(str) } catch { return null }
- }
- function toggleLogs(nodeId) {
- expandedLogs[nodeId] = !expandedLogs[nodeId]
- }
- function toggleContext(nodeId) {
- expandedContext[nodeId] = !expandedContext[nodeId]
- }
- function toggleThinking(nodeId) {
- expandedThinking[nodeId] = !expandedThinking[nodeId]
- }
- /**
- * 将 contextSnapshot 中的值格式化为可显示字符串:
- * - 对象/数组:JSON.stringify(缩进 2)
- * - 字符串:原样返回,超长时由 CSS 限制高度
- * - 其他:String(value)
- */
- function formatContextValue(value) {
- if (value === null || value === undefined) return 'null'
- if (typeof value === 'object') return JSON.stringify(value, null, 2)
- return String(value)
- }
- function logTypeIcon(type) {
- return { TOOL_CALL: '🔧', TOOL_RESULT: '📋', THINKING: '💭', ERROR: '❌', INFO: 'ℹ️' }[type] || '•'
- }
- function logTypeLabel(type) {
- return { TOOL_CALL: '调用工具', TOOL_RESULT: '工具返回', THINKING: '思考', ERROR: '错误', INFO: '信息' }[type] || type
- }
- /**
- * 从日志数组中提取思考过程内容(THINKING 类型日志)。
- * 工作流编辑器的运行结果是实时流式累加,运行记录页则从持久化日志中还原。
- */
- function extractThinking(logsJson) {
- const logs = parseJson(logsJson)
- if (!Array.isArray(logs)) return null
- const thinking = logs.filter(l => l.type === 'THINKING').map(l => l.message || '').join('')
- return thinking || null
- }
- /**
- * 过滤掉 THINKING 类型日志:思考过程已在节点卡片上方集中展示,
- * 列表展开时再显示会造成内容重复(流式 token 拼接的多条 message 与上方完整思考重复)。
- */
- function nonThinkingLogs(logsJson) {
- const logs = parseJson(logsJson)
- if (!Array.isArray(logs)) return []
- return logs.filter(l => l.type !== 'THINKING')
- }
- /**
- * 下载某次运行的工作目录 zip。
- * 后端接口:GET /api/workflows/{workflowId}/runs/{runId}/download
- * 响应已是 attachment + Content-Disposition,浏览器直接导航即可触发下载,
- * 无需经 axios 拦截器(拦截器要求 res.code===200,二进制响应会失败)。
- */
- function downloadWorkspace(run) {
- if (!run?.workflowId || !run?.runId) {
- message.error('运行信息缺失,无法下载')
- return
- }
- window.location.href = `/api/workflows/${run.workflowId}/runs/${run.runId}/download`
- }
- </script>
- <template>
- <div class="run-history-page">
- <div class="page-header">
- <h2>运行记录</h2>
- </div>
- <div v-if="loading" style="text-align: center; padding: 60px;">
- <n-spin size="large" />
- </div>
- <n-empty v-else-if="workflows.length === 0" description="暂无运行记录" style="margin-top: 80px;" />
- <div v-else class="history-layout">
- <!-- 左侧:工作流列表 -->
- <div class="left-panel">
- <div v-for="wf in workflows" :key="wf.workflowId" class="wf-group">
- <div class="wf-group-title" @click="toggleWfCollapse(wf.workflowId)">
- <n-icon size="12" class="wf-collapse-icon" :class="{ collapsed: collapsedWfs[wf.workflowId] }">
- <ChevronForwardOutline />
- </n-icon>
- <n-icon size="14"><PlayCircleOutline /></n-icon>
- <span>{{ wf.displayName || wf.workflowName }}</span>
- <span class="wf-run-count">{{ wf.runs.length }}</span>
- </div>
- <div v-if="wf.tags && wf.tags.length" class="wf-tags">
- <span v-for="tag in wf.tags" :key="tag.tagId" class="wf-tag">{{ tag.tagName }}</span>
- </div>
- <div v-show="!collapsedWfs[wf.workflowId]">
- <div
- v-for="run in wf.runs"
- :key="run.id"
- class="run-item"
- :class="{ active: selectedRun?.id === run.id }"
- @click="selectRun(run)"
- >
- <div class="run-item-top">
- <span class="run-status-dot" :class="statusClass(run.status)"></span>
- <span class="run-id">#{{ run.runId }}</span>
- <span class="run-badge" :class="statusClass(run.status)">{{ statusText(run.status) }}</span>
- </div>
- <div class="run-item-bottom">
- <span>{{ formatTime(run.startedAt) }}</span>
- <span v-if="run.completedAt" class="run-duration">{{ duration(run) }}</span>
- </div>
- <n-popconfirm @positive-click.stop="handleDelete(run.id)">
- <template #trigger>
- <button class="run-delete-btn" @click.stop>
- <n-icon size="12"><TrashOutline /></n-icon>
- </button>
- </template>
- 确定删除此运行记录?
- </n-popconfirm>
- </div>
- </div>
- </div>
- </div>
- <!-- 右侧:运行详情 -->
- <div class="right-panel">
- <div v-if="!runDetail" class="empty-detail">
- <n-empty description="选择左侧运行记录查看详情" />
- </div>
- <template v-else>
- <div class="detail-header">
- <div class="detail-title">
- <span class="run-status-dot" :class="statusClass(runDetail.run.status)"></span>
- 运行 #{{ runDetail.run.runId }}
- <span class="run-badge" :class="statusClass(runDetail.run.status)">{{ statusText(runDetail.run.status) }}</span>
- </div>
- <div class="detail-meta">
- <span>开始: {{ formatTime(runDetail.run.startedAt) }}</span>
- <span v-if="runDetail.run.completedAt">完成: {{ formatTime(runDetail.run.completedAt) }}</span>
- <span v-if="runDetail.run.completedAt">耗时: {{ duration(runDetail.run) }}</span>
- </div>
- <div v-if="runDetail.run.error" class="detail-error">{{ runDetail.run.error }}</div>
- </div>
- <!-- 节点结果列表 -->
- <div class="detail-nodes">
- <div
- v-for="node in runDetail.nodes"
- :key="node.id"
- class="node-result-card"
- :style="{ borderLeftColor: nodeTypeInfo[node.nodeType]?.color || '#666' }"
- >
- <div class="nrc-header" @click="toggleLogs(node.nodeId)" style="cursor: pointer">
- <div class="nrc-icon" :style="{ background: nodeTypeInfo[node.nodeType]?.color || '#666' }">
- <n-icon size="12"><component :is="nodeTypeInfo[node.nodeType]?.icon || PlayCircleOutline" /></n-icon>
- </div>
- <div class="nrc-info">
- <span class="nrc-label">{{ node.label || node.nodeId }}</span>
- <span class="nrc-type">{{ nodeTypeInfo[node.nodeType]?.typeLabel || node.nodeType }}</span>
- </div>
- <span v-if="node.logs && nonThinkingLogs(node.logs).length" class="nrc-log-count">{{ nonThinkingLogs(node.logs).length }} 条日志</span>
- <span class="nrc-status" :class="statusClass(node.status)">{{ statusText(node.status) }}</span>
- <span v-if="node.logs && nonThinkingLogs(node.logs).length" class="nrc-expand" :class="{ expanded: expandedLogs[node.nodeId] }">▶</span>
- </div>
- <div class="nrc-vars" v-if="parseJson(node.output)">
- <!-- envelope 失败(status=400)时显示红色徽章 + message -->
- <div v-if="nodeOutputStatus(parseJson(node.output)) === 400" class="nrc-env-fail">
- <span class="nrc-env-badge failed">失败</span>
- <span class="nrc-env-msg">{{ nodeOutputMessage(parseJson(node.output)) }}</span>
- </div>
- <template v-if="Object.keys(unwrapNodeOutput(parseJson(node.output))).length">
- <div class="nrc-var" v-for="(value, key) in unwrapNodeOutput(parseJson(node.output))" :key="key">
- <span class="nrc-var-key" :style="{ color: nodeTypeInfo[node.nodeType]?.color || '#666' }">{{ key }}</span>
- <span class="nrc-var-value">{{ typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value) }}</span>
- </div>
- </template>
- </div>
- <!-- 思考过程(从持久化日志中提取并集中展示),支持折叠 -->
- <div v-if="extractThinking(node.logs)" class="nrc-thinking">
- <div class="nrc-thinking-header" @click.stop="toggleThinking(node.nodeId)" style="cursor: pointer">
- <span class="nrc-thinking-tag">💭 思考过程</span>
- <span class="nrc-thinking-static">{{ extractThinking(node.logs).length }} 字</span>
- <span class="nrc-thinking-expand" :class="{ expanded: expandedThinking[node.nodeId] !== false }">▶</span>
- </div>
- <pre v-show="expandedThinking[node.nodeId] !== false" class="nrc-thinking-body">{{ extractThinking(node.logs) }}</pre>
- </div>
- <!-- 完整上下文快照(节点执行后的全部 variables,debug 视图) -->
- <div v-if="parseJson(node.contextSnapshot)" class="nrc-ctx-toggle" @click.stop="toggleContext(node.nodeId)">
- <span class="nrc-ctx-icon" :class="{ expanded: expandedContext[node.nodeId] }">▶</span>
- <span class="nrc-ctx-label">查看完整上下文({{ Object.keys(parseJson(node.contextSnapshot)).length }} 个变量)</span>
- </div>
- <div v-if="expandedContext[node.nodeId] && parseJson(node.contextSnapshot)" class="nrc-ctx-panel">
- <div class="nrc-ctx-var" v-for="(value, key) in parseJson(node.contextSnapshot)" :key="key">
- <span class="nrc-ctx-key">{{ key }}</span>
- <span class="nrc-ctx-value">{{ formatContextValue(value) }}</span>
- </div>
- </div>
- <div v-if="node.error" class="nrc-error">{{ node.error }}</div>
- <div v-if="expandedLogs[node.nodeId] && nonThinkingLogs(node.logs).length" class="nrc-logs">
- <div class="nrc-log" v-for="(log, idx) in nonThinkingLogs(node.logs)" :key="idx" :class="'log-type-' + (log.type || 'info').toLowerCase()">
- <span class="nrc-log-icon">{{ logTypeIcon(log.type) }}</span>
- <span class="nrc-log-msg">{{ log.message }}</span>
- <span class="nrc-log-tag">{{ logTypeLabel(log.type) }}</span>
- <div v-if="log.detail" class="nrc-log-detail">{{ log.detail }}</div>
- </div>
- </div>
- </div>
- </div>
- <!-- 工作空间下载:下载本次运行的工作目录 zip(应用 .agentignore 过滤) -->
- <div class="detail-footer">
- <n-button quaternary size="small" @click="downloadWorkspace(runDetail.run)">
- <template #icon>
- <n-icon size="14"><DownloadOutline /></n-icon>
- </template>
- 下载工作空间
- </n-button>
- </div>
- </template>
- </div>
- </div>
- </div>
- </template>
- <style scoped>
- .run-history-page {
- padding: 24px 32px;
- height: 100vh;
- display: flex;
- flex-direction: column;
- overflow: hidden;
- }
- .page-header {
- margin-bottom: 20px;
- flex-shrink: 0;
- }
- .page-header h2 {
- font-size: 20px;
- font-weight: 700;
- color: var(--text-primary, #e0e0e0);
- margin: 0;
- }
- .history-layout {
- flex: 1;
- display: flex;
- gap: 20px;
- min-height: 0;
- }
- /* 左侧面板 */
- .left-panel {
- width: 320px;
- flex-shrink: 0;
- overflow-y: auto;
- padding-right: 8px;
- }
- .wf-group {
- margin-bottom: 20px;
- }
- .wf-group-title {
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 14px;
- font-weight: 600;
- color: var(--text-primary, #e0e0e0);
- margin-bottom: 8px;
- padding: 4px;
- cursor: pointer;
- user-select: none;
- border-radius: 6px;
- transition: background 0.15s;
- }
- .wf-group-title:hover { background: rgba(255,255,255,0.04); }
- .wf-collapse-icon {
- transition: transform 0.2s;
- color: var(--text-tertiary, #888);
- }
- /* 默认展开:图标旋转 90° 朝下;折叠时复原朝右 */
- .wf-collapse-icon:not(.collapsed) { transform: rotate(90deg); }
- .wf-run-count {
- font-size: 11px;
- background: rgba(99, 102, 241, 0.15);
- color: #818cf8;
- padding: 1px 6px;
- border-radius: 8px;
- margin-left: auto;
- }
- .wf-tags {
- display: flex;
- flex-wrap: wrap;
- gap: 6px;
- padding: 0 4px 8px;
- }
- .wf-tag {
- font-size: 10px;
- padding: 2px 8px;
- border: 1px solid rgba(255, 255, 255, 0.35);
- border-radius: 10px;
- color: rgba(255, 255, 255, 0.6);
- white-space: nowrap;
- line-height: 1.4;
- }
- .run-item {
- position: relative;
- padding: 10px 12px;
- border-radius: 8px;
- border: 1px solid var(--border-color, rgba(255,255,255,0.08));
- margin-bottom: 6px;
- cursor: pointer;
- transition: all 0.15s;
- }
- .run-item:hover {
- background: rgba(37, 99, 235, 0.06);
- border-color: rgba(37, 99, 235, 0.2);
- }
- .run-item.active {
- background: rgba(37, 99, 235, 0.1);
- border-color: rgba(37, 99, 235, 0.3);
- }
- .run-item-top {
- display: flex;
- align-items: center;
- gap: 6px;
- margin-bottom: 4px;
- }
- .run-status-dot {
- width: 6px;
- height: 6px;
- border-radius: 50%;
- flex-shrink: 0;
- }
- .run-status-dot.status-running { background: #818cf8; }
- .run-status-dot.status-success { background: #34d399; }
- .run-status-dot.status-failed { background: #f87171; }
- .run-id {
- font-size: 12px;
- font-weight: 600;
- color: var(--text-primary, #e0e0e0);
- font-family: 'Cascadia Code', monospace;
- }
- .run-badge {
- font-size: 10px;
- padding: 1px 5px;
- border-radius: 4px;
- font-weight: 600;
- margin-left: auto;
- }
- .run-badge.status-running { background: rgba(99,102,241,0.15); color: #818cf8; }
- .run-badge.status-success { background: rgba(16,185,129,0.15); color: #34d399; }
- .run-badge.status-failed { background: rgba(239,68,68,0.15); color: #f87171; }
- .run-item-bottom {
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 11px;
- color: var(--text-tertiary, #888);
- }
- .run-duration {
- margin-left: auto;
- color: var(--text-secondary, #aaa);
- }
- .run-delete-btn {
- position: absolute;
- top: 6px;
- right: 6px;
- background: none;
- border: none;
- color: var(--text-tertiary, #666);
- cursor: pointer;
- padding: 2px;
- border-radius: 4px;
- opacity: 0;
- transition: opacity 0.15s;
- }
- .run-item:hover .run-delete-btn {
- opacity: 1;
- }
- .run-delete-btn:hover {
- color: #f87171;
- background: rgba(239,68,68,0.1);
- }
- /* 右侧详情面板 */
- .right-panel {
- flex: 1;
- overflow-y: auto;
- min-width: 0;
- }
- .empty-detail {
- display: flex;
- align-items: center;
- justify-content: center;
- height: 100%;
- }
- .detail-footer {
- display: flex;
- justify-content: center;
- padding: 16px 0 4px;
- margin-top: 12px;
- border-top: 1px dashed var(--border-color, rgba(255,255,255,0.08));
- }
- .detail-header {
- margin-bottom: 16px;
- padding-bottom: 12px;
- border-bottom: 1px solid var(--border-color, rgba(255,255,255,0.08));
- }
- .detail-title {
- display: flex;
- align-items: center;
- gap: 8px;
- font-size: 16px;
- font-weight: 600;
- color: var(--text-primary, #e0e0e0);
- margin-bottom: 6px;
- }
- .detail-meta {
- display: flex;
- gap: 16px;
- font-size: 12px;
- color: var(--text-tertiary, #888);
- }
- .detail-error {
- margin-top: 8px;
- padding: 8px 12px;
- background: rgba(239,68,68,0.1);
- border: 1px solid rgba(239,68,68,0.2);
- border-radius: 6px;
- color: #f87171;
- font-size: 12px;
- }
- /* 节点结果卡片 - 复用 WorkflowEditor 样式 */
- .node-result-card {
- background: rgba(255,255,255,0.03);
- border-radius: 8px;
- border-left: 3px solid #666;
- margin-bottom: 10px;
- overflow: hidden;
- }
- .nrc-header {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 8px 10px;
- border-bottom: 1px solid rgba(255,255,255,0.04);
- }
- .nrc-icon {
- width: 22px;
- height: 22px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 5px;
- color: #fff;
- flex-shrink: 0;
- }
- .nrc-info {
- flex: 1;
- min-width: 0;
- display: flex;
- flex-direction: column;
- gap: 1px;
- }
- .nrc-label {
- font-size: 12px;
- font-weight: 600;
- color: var(--text-primary, #e0e0e0);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- }
- .nrc-type {
- font-size: 10px;
- color: var(--text-tertiary, #666);
- }
- .nrc-status {
- font-size: 10px;
- padding: 2px 6px;
- border-radius: 4px;
- font-weight: 600;
- flex-shrink: 0;
- }
- .nrc-status.status-running { background: rgba(99,102,241,0.15); color: #818cf8; }
- .nrc-status.status-success { background: rgba(16,185,129,0.15); color: #34d399; }
- .nrc-status.status-failed { background: rgba(239,68,68,0.15); color: #f87171; }
- .nrc-status.status-skipped { background: rgba(107,114,128,0.15); color: #9ca3af; }
- .nrc-log-count {
- font-size: 10px;
- padding: 2px 6px;
- border-radius: 4px;
- background: rgba(99,102,241,0.1);
- color: #818cf8;
- flex-shrink: 0;
- }
- .nrc-expand {
- font-size: 10px;
- color: var(--text-tertiary, #666);
- transition: transform 0.2s;
- flex-shrink: 0;
- }
- .nrc-expand.expanded { transform: rotate(90deg); }
- .nrc-vars { padding: 6px 10px; }
- /* envelope 失败提示条:status=400 时显示在变量列表顶部 */
- .nrc-env-fail {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 4px 8px;
- margin-bottom: 6px;
- background: rgba(239, 68, 68, 0.08);
- border-left: 2px solid #ef4444;
- border-radius: 3px;
- font-size: 11px;
- }
- .nrc-env-badge {
- font-weight: 600;
- padding: 1px 6px;
- border-radius: 3px;
- flex-shrink: 0;
- }
- .nrc-env-badge.failed {
- background: rgba(239, 68, 68, 0.15);
- color: #f87171;
- }
- .nrc-env-msg {
- color: #f87171;
- word-break: break-all;
- }
- .nrc-var {
- display: flex;
- flex-direction: column;
- gap: 2px;
- padding: 4px 0;
- border-bottom: 1px solid rgba(255,255,255,0.02);
- }
- .nrc-var:last-child { border-bottom: none; }
- .nrc-var-key {
- font-family: 'Cascadia Code', 'Fira Code', monospace;
- font-size: 11px;
- font-weight: 600;
- }
- .nrc-var-value {
- font-family: 'Cascadia Code', 'Fira Code', monospace;
- font-size: 11px;
- color: var(--text-secondary, #aaa);
- white-space: pre-wrap;
- word-break: break-all;
- max-height: 120px;
- overflow-y: auto;
- }
- /* 思考过程展示(与 WorkflowEditor 实时思考区样式保持一致) */
- .nrc-thinking {
- margin: 6px 10px;
- border-radius: 6px;
- background: rgba(168, 85, 247, 0.05);
- border: 1px solid rgba(168, 85, 247, 0.2);
- overflow: hidden;
- }
- .nrc-thinking-header {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 5px 10px;
- font-size: 11px;
- background: rgba(168, 85, 247, 0.08);
- }
- .nrc-thinking-tag {
- color: #c084fc;
- font-weight: 600;
- }
- .nrc-thinking-static {
- color: var(--text-tertiary, #888);
- font-size: 10px;
- }
- .nrc-thinking-expand {
- margin-left: auto;
- font-size: 10px;
- color: var(--text-tertiary, #666);
- transition: transform 0.2s;
- flex-shrink: 0;
- }
- .nrc-thinking-expand.expanded {
- transform: rotate(90deg);
- }
- .nrc-thinking-body {
- margin: 0;
- padding: 8px 12px;
- max-height: 320px;
- overflow: auto;
- font-family: 'Cascadia Code', 'Fira Code', monospace;
- font-size: 11px;
- color: var(--text-secondary, #ccc);
- white-space: pre-wrap;
- word-break: break-all;
- }
- .nrc-error {
- padding: 6px 10px;
- color: #f87171;
- font-size: 11px;
- }
- /* 完整上下文快照折叠区 */
- .nrc-ctx-toggle {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 6px 10px;
- cursor: pointer;
- border-top: 1px solid rgba(255,255,255,0.04);
- font-size: 11px;
- color: var(--text-secondary, #aaa);
- user-select: none;
- }
- .nrc-ctx-toggle:hover { background: rgba(99,102,241,0.05); }
- .nrc-ctx-icon {
- font-size: 9px;
- color: var(--text-tertiary, #888);
- transition: transform 0.2s;
- }
- .nrc-ctx-icon.expanded { transform: rotate(90deg); }
- .nrc-ctx-label { font-weight: 500; }
- .nrc-ctx-panel {
- padding: 6px 10px;
- border-top: 1px solid rgba(255,255,255,0.04);
- max-height: 280px;
- overflow-y: auto;
- background: rgba(0,0,0,0.15);
- }
- .nrc-ctx-var {
- display: flex;
- flex-direction: column;
- gap: 2px;
- padding: 4px 0;
- border-bottom: 1px solid rgba(255,255,255,0.02);
- }
- .nrc-ctx-var:last-child { border-bottom: none; }
- .nrc-ctx-key {
- font-family: 'Cascadia Code', 'Fira Code', monospace;
- font-size: 11px;
- font-weight: 600;
- color: #818cf8;
- }
- .nrc-ctx-value {
- font-family: 'Cascadia Code', 'Fira Code', monospace;
- font-size: 11px;
- color: var(--text-secondary, #ccc);
- white-space: pre-wrap;
- word-break: break-all;
- max-height: 80px;
- overflow-y: auto;
- }
- .nrc-logs {
- border-top: 1px solid rgba(255,255,255,0.04);
- padding: 6px 10px;
- max-height: 400px;
- overflow-y: auto;
- }
- .nrc-log {
- padding: 4px 0;
- border-bottom: 1px solid rgba(255,255,255,0.02);
- font-size: 11px;
- line-height: 1.5;
- }
- .nrc-log:last-child { border-bottom: none; }
- .nrc-log-icon { margin-right: 4px; }
- .nrc-log-tag {
- font-size: 10px;
- padding: 1px 4px;
- border-radius: 3px;
- margin-right: 6px;
- font-weight: 600;
- flex-shrink: 0;
- }
- .log-type-tool_call .nrc-log-tag { background: rgba(59,130,246,0.15); color: #60a5fa; }
- .log-type-tool_result .nrc-log-tag { background: rgba(16,185,129,0.15); color: #34d399; }
- .log-type-thinking .nrc-log-tag { background: rgba(168,85,247,0.15); color: #c084fc; }
- .log-type-error .nrc-log-tag { background: rgba(239,68,68,0.15); color: #f87171; }
- .log-type-info .nrc-log-tag { background: rgba(107,114,128,0.15); color: #9ca3af; }
- .nrc-log-msg {
- color: var(--text-secondary, #ccc);
- word-break: break-all;
- }
- .nrc-log-detail {
- margin-top: 3px;
- margin-left: 22px;
- font-family: 'Cascadia Code', 'Fira Code', monospace;
- font-size: 10px;
- color: var(--text-tertiary, #888);
- background: rgba(0,0,0,0.2);
- padding: 4px 6px;
- border-radius: 4px;
- white-space: pre-wrap;
- word-break: break-all;
- max-height: 100px;
- overflow-y: auto;
- }
- </style>
|