RunHistory.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894
  1. <script setup>
  2. import { ref, reactive, onMounted } from 'vue'
  3. import { NEmpty, NSpin, NIcon, NPopconfirm, NButton, useMessage } from 'naive-ui'
  4. import { getWorkflowsWithRuns, getRunDetail, deleteRun } from '../../api/runHistory'
  5. import {
  6. PlayCircleOutline,
  7. ChevronForwardOutline,
  8. TrashOutline,
  9. ChatbubbleEllipsesOutline,
  10. SparklesOutline,
  11. FlaskOutline,
  12. RocketOutline,
  13. ColorWandOutline,
  14. GitBranchOutline,
  15. ExitOutline,
  16. DownloadOutline
  17. } from '@vicons/ionicons5'
  18. import { unwrapNodeOutput, nodeOutputStatus, nodeOutputMessage } from '../../composables/useWorkflowRunner'
  19. const message = useMessage()
  20. const loading = ref(false)
  21. const workflows = ref([]) // [{ workflowId, workflowName, runs }]
  22. const selectedRun = ref(null)
  23. const runDetail = ref(null) // { run, nodes }
  24. const expandedLogs = reactive({})
  25. const expandedContext = reactive({})
  26. /** 思考过程折叠状态:key=nodeId,未设置默认展开 */
  27. const expandedThinking = reactive({})
  28. /** 折叠的智能体(workflowId 集合),默认全部展开 */
  29. const collapsedWfs = reactive({})
  30. function toggleWfCollapse(workflowId) {
  31. collapsedWfs[workflowId] = !collapsedWfs[workflowId]
  32. }
  33. const nodeTypeInfo = {
  34. userInput: { color: '#10B981', typeLabel: '用户输入', icon: ChatbubbleEllipsesOutline },
  35. llm: { color: '#8B5CF6', typeLabel: '大模型', icon: SparklesOutline },
  36. skill: { color: '#06B6D4', typeLabel: '技能', icon: FlaskOutline },
  37. agent: { color: '#F59E0B', typeLabel: '智能体', icon: RocketOutline },
  38. smartAction: { color: '#EC4899', typeLabel: '智能操作', icon: ColorWandOutline },
  39. condition: { color: '#F97316', typeLabel: '条件', icon: GitBranchOutline },
  40. output: { color: '#EF4444', typeLabel: '输出', icon: ExitOutline }
  41. }
  42. onMounted(loadData)
  43. async function loadData() {
  44. loading.value = true
  45. try {
  46. const res = await getWorkflowsWithRuns()
  47. workflows.value = res.data || []
  48. } catch (e) {
  49. message.error('加载运行记录失败')
  50. } finally {
  51. loading.value = false
  52. }
  53. }
  54. async function selectRun(run) {
  55. selectedRun.value = run
  56. // reactive 对象无 .value,需逐键删除以正确重置折叠状态
  57. Object.keys(expandedLogs).forEach(k => delete expandedLogs[k])
  58. Object.keys(expandedContext).forEach(k => delete expandedContext[k])
  59. Object.keys(expandedThinking).forEach(k => delete expandedThinking[k])
  60. try {
  61. const res = await getRunDetail(run.id)
  62. runDetail.value = res.data
  63. } catch (e) {
  64. message.error('加载运行详情失败')
  65. }
  66. }
  67. async function handleDelete(runId) {
  68. try {
  69. await deleteRun(runId)
  70. message.success('已删除')
  71. if (selectedRun.value?.id === runId) {
  72. selectedRun.value = null
  73. runDetail.value = null
  74. }
  75. await loadData()
  76. } catch (e) {
  77. message.error('删除失败')
  78. }
  79. }
  80. function statusText(s) {
  81. return { running: '执行中', success: '成功', failed: '失败', skipped: '跳过' }[s?.toLowerCase()] || s || '未知'
  82. }
  83. function statusClass(s) {
  84. return 'status-' + (s || 'unknown').toLowerCase()
  85. }
  86. function formatTime(t) {
  87. if (!t) return '-'
  88. // 兼容数组和字符串格式
  89. if (Array.isArray(t)) {
  90. const [y, m, d, h, min, s] = t
  91. 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')}`
  92. }
  93. return String(t).replace('T', ' ').substring(0, 19)
  94. }
  95. function duration(run) {
  96. if (!run.startedAt || !run.completedAt) return '-'
  97. let start = new Date(Array.isArray(run.startedAt) ? run.startedAt.join('-') : run.startedAt)
  98. let end = new Date(Array.isArray(run.completedAt) ? run.completedAt.join('-') : run.completedAt)
  99. let diff = Math.round((end - start) / 1000)
  100. if (diff < 60) return diff + ' 秒'
  101. if (diff < 3600) return Math.floor(diff / 60) + ' 分 ' + (diff % 60) + ' 秒'
  102. return Math.floor(diff / 3600) + ' 时 ' + Math.floor((diff % 3600) / 60) + ' 分'
  103. }
  104. function parseJson(str) {
  105. if (!str) return null
  106. try { return JSON.parse(str) } catch { return null }
  107. }
  108. function toggleLogs(nodeId) {
  109. expandedLogs[nodeId] = !expandedLogs[nodeId]
  110. }
  111. function toggleContext(nodeId) {
  112. expandedContext[nodeId] = !expandedContext[nodeId]
  113. }
  114. function toggleThinking(nodeId) {
  115. expandedThinking[nodeId] = !expandedThinking[nodeId]
  116. }
  117. /**
  118. * 将 contextSnapshot 中的值格式化为可显示字符串:
  119. * - 对象/数组:JSON.stringify(缩进 2)
  120. * - 字符串:原样返回,超长时由 CSS 限制高度
  121. * - 其他:String(value)
  122. */
  123. function formatContextValue(value) {
  124. if (value === null || value === undefined) return 'null'
  125. if (typeof value === 'object') return JSON.stringify(value, null, 2)
  126. return String(value)
  127. }
  128. function logTypeIcon(type) {
  129. return { TOOL_CALL: '🔧', TOOL_RESULT: '📋', THINKING: '💭', ERROR: '❌', INFO: 'ℹ️' }[type] || '•'
  130. }
  131. function logTypeLabel(type) {
  132. return { TOOL_CALL: '调用工具', TOOL_RESULT: '工具返回', THINKING: '思考', ERROR: '错误', INFO: '信息' }[type] || type
  133. }
  134. /**
  135. * 从日志数组中提取思考过程内容(THINKING 类型日志)。
  136. * 工作流编辑器的运行结果是实时流式累加,运行记录页则从持久化日志中还原。
  137. */
  138. function extractThinking(logsJson) {
  139. const logs = parseJson(logsJson)
  140. if (!Array.isArray(logs)) return null
  141. const thinking = logs.filter(l => l.type === 'THINKING').map(l => l.message || '').join('')
  142. return thinking || null
  143. }
  144. /**
  145. * 过滤掉 THINKING 类型日志:思考过程已在节点卡片上方集中展示,
  146. * 列表展开时再显示会造成内容重复(流式 token 拼接的多条 message 与上方完整思考重复)。
  147. */
  148. function nonThinkingLogs(logsJson) {
  149. const logs = parseJson(logsJson)
  150. if (!Array.isArray(logs)) return []
  151. return logs.filter(l => l.type !== 'THINKING')
  152. }
  153. /**
  154. * 下载某次运行的工作目录 zip。
  155. * 后端接口:GET /api/workflows/{workflowId}/runs/{runId}/download
  156. * 响应已是 attachment + Content-Disposition,浏览器直接导航即可触发下载,
  157. * 无需经 axios 拦截器(拦截器要求 res.code===200,二进制响应会失败)。
  158. */
  159. function downloadWorkspace(run) {
  160. if (!run?.workflowId || !run?.runId) {
  161. message.error('运行信息缺失,无法下载')
  162. return
  163. }
  164. window.location.href = `/api/workflows/${run.workflowId}/runs/${run.runId}/download`
  165. }
  166. </script>
  167. <template>
  168. <div class="run-history-page">
  169. <div class="page-header">
  170. <h2>运行记录</h2>
  171. </div>
  172. <div v-if="loading" style="text-align: center; padding: 60px;">
  173. <n-spin size="large" />
  174. </div>
  175. <n-empty v-else-if="workflows.length === 0" description="暂无运行记录" style="margin-top: 80px;" />
  176. <div v-else class="history-layout">
  177. <!-- 左侧:工作流列表 -->
  178. <div class="left-panel">
  179. <div v-for="wf in workflows" :key="wf.workflowId" class="wf-group">
  180. <div class="wf-group-title" @click="toggleWfCollapse(wf.workflowId)">
  181. <n-icon size="12" class="wf-collapse-icon" :class="{ collapsed: collapsedWfs[wf.workflowId] }">
  182. <ChevronForwardOutline />
  183. </n-icon>
  184. <n-icon size="14"><PlayCircleOutline /></n-icon>
  185. <span>{{ wf.displayName || wf.workflowName }}</span>
  186. <span class="wf-run-count">{{ wf.runs.length }}</span>
  187. </div>
  188. <div v-if="wf.tags && wf.tags.length" class="wf-tags">
  189. <span v-for="tag in wf.tags" :key="tag.tagId" class="wf-tag">{{ tag.tagName }}</span>
  190. </div>
  191. <div v-show="!collapsedWfs[wf.workflowId]">
  192. <div
  193. v-for="run in wf.runs"
  194. :key="run.id"
  195. class="run-item"
  196. :class="{ active: selectedRun?.id === run.id }"
  197. @click="selectRun(run)"
  198. >
  199. <div class="run-item-top">
  200. <span class="run-status-dot" :class="statusClass(run.status)"></span>
  201. <span class="run-id">#{{ run.runId }}</span>
  202. <span class="run-badge" :class="statusClass(run.status)">{{ statusText(run.status) }}</span>
  203. </div>
  204. <div class="run-item-bottom">
  205. <span>{{ formatTime(run.startedAt) }}</span>
  206. <span v-if="run.completedAt" class="run-duration">{{ duration(run) }}</span>
  207. </div>
  208. <n-popconfirm @positive-click.stop="handleDelete(run.id)">
  209. <template #trigger>
  210. <button class="run-delete-btn" @click.stop>
  211. <n-icon size="12"><TrashOutline /></n-icon>
  212. </button>
  213. </template>
  214. 确定删除此运行记录?
  215. </n-popconfirm>
  216. </div>
  217. </div>
  218. </div>
  219. </div>
  220. <!-- 右侧:运行详情 -->
  221. <div class="right-panel">
  222. <div v-if="!runDetail" class="empty-detail">
  223. <n-empty description="选择左侧运行记录查看详情" />
  224. </div>
  225. <template v-else>
  226. <div class="detail-header">
  227. <div class="detail-title">
  228. <span class="run-status-dot" :class="statusClass(runDetail.run.status)"></span>
  229. 运行 #{{ runDetail.run.runId }}
  230. <span class="run-badge" :class="statusClass(runDetail.run.status)">{{ statusText(runDetail.run.status) }}</span>
  231. </div>
  232. <div class="detail-meta">
  233. <span>开始: {{ formatTime(runDetail.run.startedAt) }}</span>
  234. <span v-if="runDetail.run.completedAt">完成: {{ formatTime(runDetail.run.completedAt) }}</span>
  235. <span v-if="runDetail.run.completedAt">耗时: {{ duration(runDetail.run) }}</span>
  236. </div>
  237. <div v-if="runDetail.run.error" class="detail-error">{{ runDetail.run.error }}</div>
  238. </div>
  239. <!-- 节点结果列表 -->
  240. <div class="detail-nodes">
  241. <div
  242. v-for="node in runDetail.nodes"
  243. :key="node.id"
  244. class="node-result-card"
  245. :style="{ borderLeftColor: nodeTypeInfo[node.nodeType]?.color || '#666' }"
  246. >
  247. <div class="nrc-header" @click="toggleLogs(node.nodeId)" style="cursor: pointer">
  248. <div class="nrc-icon" :style="{ background: nodeTypeInfo[node.nodeType]?.color || '#666' }">
  249. <n-icon size="12"><component :is="nodeTypeInfo[node.nodeType]?.icon || PlayCircleOutline" /></n-icon>
  250. </div>
  251. <div class="nrc-info">
  252. <span class="nrc-label">{{ node.label || node.nodeId }}</span>
  253. <span class="nrc-type">{{ nodeTypeInfo[node.nodeType]?.typeLabel || node.nodeType }}</span>
  254. </div>
  255. <span v-if="node.logs && nonThinkingLogs(node.logs).length" class="nrc-log-count">{{ nonThinkingLogs(node.logs).length }} 条日志</span>
  256. <span class="nrc-status" :class="statusClass(node.status)">{{ statusText(node.status) }}</span>
  257. <span v-if="node.logs && nonThinkingLogs(node.logs).length" class="nrc-expand" :class="{ expanded: expandedLogs[node.nodeId] }">▶</span>
  258. </div>
  259. <div class="nrc-vars" v-if="parseJson(node.output)">
  260. <!-- envelope 失败(status=400)时显示红色徽章 + message -->
  261. <div v-if="nodeOutputStatus(parseJson(node.output)) === 400" class="nrc-env-fail">
  262. <span class="nrc-env-badge failed">失败</span>
  263. <span class="nrc-env-msg">{{ nodeOutputMessage(parseJson(node.output)) }}</span>
  264. </div>
  265. <template v-if="Object.keys(unwrapNodeOutput(parseJson(node.output))).length">
  266. <div class="nrc-var" v-for="(value, key) in unwrapNodeOutput(parseJson(node.output))" :key="key">
  267. <span class="nrc-var-key" :style="{ color: nodeTypeInfo[node.nodeType]?.color || '#666' }">{{ key }}</span>
  268. <span class="nrc-var-value">{{ typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value) }}</span>
  269. </div>
  270. </template>
  271. </div>
  272. <!-- 思考过程(从持久化日志中提取并集中展示),支持折叠 -->
  273. <div v-if="extractThinking(node.logs)" class="nrc-thinking">
  274. <div class="nrc-thinking-header" @click.stop="toggleThinking(node.nodeId)" style="cursor: pointer">
  275. <span class="nrc-thinking-tag">💭 思考过程</span>
  276. <span class="nrc-thinking-static">{{ extractThinking(node.logs).length }} 字</span>
  277. <span class="nrc-thinking-expand" :class="{ expanded: expandedThinking[node.nodeId] !== false }">▶</span>
  278. </div>
  279. <pre v-show="expandedThinking[node.nodeId] !== false" class="nrc-thinking-body">{{ extractThinking(node.logs) }}</pre>
  280. </div>
  281. <!-- 完整上下文快照(节点执行后的全部 variables,debug 视图) -->
  282. <div v-if="parseJson(node.contextSnapshot)" class="nrc-ctx-toggle" @click.stop="toggleContext(node.nodeId)">
  283. <span class="nrc-ctx-icon" :class="{ expanded: expandedContext[node.nodeId] }">▶</span>
  284. <span class="nrc-ctx-label">查看完整上下文({{ Object.keys(parseJson(node.contextSnapshot)).length }} 个变量)</span>
  285. </div>
  286. <div v-if="expandedContext[node.nodeId] && parseJson(node.contextSnapshot)" class="nrc-ctx-panel">
  287. <div class="nrc-ctx-var" v-for="(value, key) in parseJson(node.contextSnapshot)" :key="key">
  288. <span class="nrc-ctx-key">{{ key }}</span>
  289. <span class="nrc-ctx-value">{{ formatContextValue(value) }}</span>
  290. </div>
  291. </div>
  292. <div v-if="node.error" class="nrc-error">{{ node.error }}</div>
  293. <div v-if="expandedLogs[node.nodeId] && nonThinkingLogs(node.logs).length" class="nrc-logs">
  294. <div class="nrc-log" v-for="(log, idx) in nonThinkingLogs(node.logs)" :key="idx" :class="'log-type-' + (log.type || 'info').toLowerCase()">
  295. <span class="nrc-log-icon">{{ logTypeIcon(log.type) }}</span>
  296. <span class="nrc-log-msg">{{ log.message }}</span>
  297. <span class="nrc-log-tag">{{ logTypeLabel(log.type) }}</span>
  298. <div v-if="log.detail" class="nrc-log-detail">{{ log.detail }}</div>
  299. </div>
  300. </div>
  301. </div>
  302. </div>
  303. <!-- 工作空间下载:下载本次运行的工作目录 zip(应用 .agentignore 过滤) -->
  304. <div class="detail-footer">
  305. <n-button quaternary size="small" @click="downloadWorkspace(runDetail.run)">
  306. <template #icon>
  307. <n-icon size="14"><DownloadOutline /></n-icon>
  308. </template>
  309. 下载工作空间
  310. </n-button>
  311. </div>
  312. </template>
  313. </div>
  314. </div>
  315. </div>
  316. </template>
  317. <style scoped>
  318. .run-history-page {
  319. padding: 24px 32px;
  320. height: 100vh;
  321. display: flex;
  322. flex-direction: column;
  323. overflow: hidden;
  324. }
  325. .page-header {
  326. margin-bottom: 20px;
  327. flex-shrink: 0;
  328. }
  329. .page-header h2 {
  330. font-size: 20px;
  331. font-weight: 700;
  332. color: var(--text-primary, #e0e0e0);
  333. margin: 0;
  334. }
  335. .history-layout {
  336. flex: 1;
  337. display: flex;
  338. gap: 20px;
  339. min-height: 0;
  340. }
  341. /* 左侧面板 */
  342. .left-panel {
  343. width: 320px;
  344. flex-shrink: 0;
  345. overflow-y: auto;
  346. padding-right: 8px;
  347. }
  348. .wf-group {
  349. margin-bottom: 20px;
  350. }
  351. .wf-group-title {
  352. display: flex;
  353. align-items: center;
  354. gap: 8px;
  355. font-size: 14px;
  356. font-weight: 600;
  357. color: var(--text-primary, #e0e0e0);
  358. margin-bottom: 8px;
  359. padding: 4px;
  360. cursor: pointer;
  361. user-select: none;
  362. border-radius: 6px;
  363. transition: background 0.15s;
  364. }
  365. .wf-group-title:hover { background: rgba(255,255,255,0.04); }
  366. .wf-collapse-icon {
  367. transition: transform 0.2s;
  368. color: var(--text-tertiary, #888);
  369. }
  370. /* 默认展开:图标旋转 90° 朝下;折叠时复原朝右 */
  371. .wf-collapse-icon:not(.collapsed) { transform: rotate(90deg); }
  372. .wf-run-count {
  373. font-size: 11px;
  374. background: rgba(99, 102, 241, 0.15);
  375. color: #818cf8;
  376. padding: 1px 6px;
  377. border-radius: 8px;
  378. margin-left: auto;
  379. }
  380. .wf-tags {
  381. display: flex;
  382. flex-wrap: wrap;
  383. gap: 6px;
  384. padding: 0 4px 8px;
  385. }
  386. .wf-tag {
  387. font-size: 10px;
  388. padding: 2px 8px;
  389. border: 1px solid rgba(255, 255, 255, 0.35);
  390. border-radius: 10px;
  391. color: rgba(255, 255, 255, 0.6);
  392. white-space: nowrap;
  393. line-height: 1.4;
  394. }
  395. .run-item {
  396. position: relative;
  397. padding: 10px 12px;
  398. border-radius: 8px;
  399. border: 1px solid var(--border-color, rgba(255,255,255,0.08));
  400. margin-bottom: 6px;
  401. cursor: pointer;
  402. transition: all 0.15s;
  403. }
  404. .run-item:hover {
  405. background: rgba(37, 99, 235, 0.06);
  406. border-color: rgba(37, 99, 235, 0.2);
  407. }
  408. .run-item.active {
  409. background: rgba(37, 99, 235, 0.1);
  410. border-color: rgba(37, 99, 235, 0.3);
  411. }
  412. .run-item-top {
  413. display: flex;
  414. align-items: center;
  415. gap: 6px;
  416. margin-bottom: 4px;
  417. }
  418. .run-status-dot {
  419. width: 6px;
  420. height: 6px;
  421. border-radius: 50%;
  422. flex-shrink: 0;
  423. }
  424. .run-status-dot.status-running { background: #818cf8; }
  425. .run-status-dot.status-success { background: #34d399; }
  426. .run-status-dot.status-failed { background: #f87171; }
  427. .run-id {
  428. font-size: 12px;
  429. font-weight: 600;
  430. color: var(--text-primary, #e0e0e0);
  431. font-family: 'Cascadia Code', monospace;
  432. }
  433. .run-badge {
  434. font-size: 10px;
  435. padding: 1px 5px;
  436. border-radius: 4px;
  437. font-weight: 600;
  438. margin-left: auto;
  439. }
  440. .run-badge.status-running { background: rgba(99,102,241,0.15); color: #818cf8; }
  441. .run-badge.status-success { background: rgba(16,185,129,0.15); color: #34d399; }
  442. .run-badge.status-failed { background: rgba(239,68,68,0.15); color: #f87171; }
  443. .run-item-bottom {
  444. display: flex;
  445. align-items: center;
  446. gap: 8px;
  447. font-size: 11px;
  448. color: var(--text-tertiary, #888);
  449. }
  450. .run-duration {
  451. margin-left: auto;
  452. color: var(--text-secondary, #aaa);
  453. }
  454. .run-delete-btn {
  455. position: absolute;
  456. top: 6px;
  457. right: 6px;
  458. background: none;
  459. border: none;
  460. color: var(--text-tertiary, #666);
  461. cursor: pointer;
  462. padding: 2px;
  463. border-radius: 4px;
  464. opacity: 0;
  465. transition: opacity 0.15s;
  466. }
  467. .run-item:hover .run-delete-btn {
  468. opacity: 1;
  469. }
  470. .run-delete-btn:hover {
  471. color: #f87171;
  472. background: rgba(239,68,68,0.1);
  473. }
  474. /* 右侧详情面板 */
  475. .right-panel {
  476. flex: 1;
  477. overflow-y: auto;
  478. min-width: 0;
  479. }
  480. .empty-detail {
  481. display: flex;
  482. align-items: center;
  483. justify-content: center;
  484. height: 100%;
  485. }
  486. .detail-footer {
  487. display: flex;
  488. justify-content: center;
  489. padding: 16px 0 4px;
  490. margin-top: 12px;
  491. border-top: 1px dashed var(--border-color, rgba(255,255,255,0.08));
  492. }
  493. .detail-header {
  494. margin-bottom: 16px;
  495. padding-bottom: 12px;
  496. border-bottom: 1px solid var(--border-color, rgba(255,255,255,0.08));
  497. }
  498. .detail-title {
  499. display: flex;
  500. align-items: center;
  501. gap: 8px;
  502. font-size: 16px;
  503. font-weight: 600;
  504. color: var(--text-primary, #e0e0e0);
  505. margin-bottom: 6px;
  506. }
  507. .detail-meta {
  508. display: flex;
  509. gap: 16px;
  510. font-size: 12px;
  511. color: var(--text-tertiary, #888);
  512. }
  513. .detail-error {
  514. margin-top: 8px;
  515. padding: 8px 12px;
  516. background: rgba(239,68,68,0.1);
  517. border: 1px solid rgba(239,68,68,0.2);
  518. border-radius: 6px;
  519. color: #f87171;
  520. font-size: 12px;
  521. }
  522. /* 节点结果卡片 - 复用 WorkflowEditor 样式 */
  523. .node-result-card {
  524. background: rgba(255,255,255,0.03);
  525. border-radius: 8px;
  526. border-left: 3px solid #666;
  527. margin-bottom: 10px;
  528. overflow: hidden;
  529. }
  530. .nrc-header {
  531. display: flex;
  532. align-items: center;
  533. gap: 8px;
  534. padding: 8px 10px;
  535. border-bottom: 1px solid rgba(255,255,255,0.04);
  536. }
  537. .nrc-icon {
  538. width: 22px;
  539. height: 22px;
  540. display: flex;
  541. align-items: center;
  542. justify-content: center;
  543. border-radius: 5px;
  544. color: #fff;
  545. flex-shrink: 0;
  546. }
  547. .nrc-info {
  548. flex: 1;
  549. min-width: 0;
  550. display: flex;
  551. flex-direction: column;
  552. gap: 1px;
  553. }
  554. .nrc-label {
  555. font-size: 12px;
  556. font-weight: 600;
  557. color: var(--text-primary, #e0e0e0);
  558. white-space: nowrap;
  559. overflow: hidden;
  560. text-overflow: ellipsis;
  561. }
  562. .nrc-type {
  563. font-size: 10px;
  564. color: var(--text-tertiary, #666);
  565. }
  566. .nrc-status {
  567. font-size: 10px;
  568. padding: 2px 6px;
  569. border-radius: 4px;
  570. font-weight: 600;
  571. flex-shrink: 0;
  572. }
  573. .nrc-status.status-running { background: rgba(99,102,241,0.15); color: #818cf8; }
  574. .nrc-status.status-success { background: rgba(16,185,129,0.15); color: #34d399; }
  575. .nrc-status.status-failed { background: rgba(239,68,68,0.15); color: #f87171; }
  576. .nrc-status.status-skipped { background: rgba(107,114,128,0.15); color: #9ca3af; }
  577. .nrc-log-count {
  578. font-size: 10px;
  579. padding: 2px 6px;
  580. border-radius: 4px;
  581. background: rgba(99,102,241,0.1);
  582. color: #818cf8;
  583. flex-shrink: 0;
  584. }
  585. .nrc-expand {
  586. font-size: 10px;
  587. color: var(--text-tertiary, #666);
  588. transition: transform 0.2s;
  589. flex-shrink: 0;
  590. }
  591. .nrc-expand.expanded { transform: rotate(90deg); }
  592. .nrc-vars { padding: 6px 10px; }
  593. /* envelope 失败提示条:status=400 时显示在变量列表顶部 */
  594. .nrc-env-fail {
  595. display: flex;
  596. align-items: center;
  597. gap: 6px;
  598. padding: 4px 8px;
  599. margin-bottom: 6px;
  600. background: rgba(239, 68, 68, 0.08);
  601. border-left: 2px solid #ef4444;
  602. border-radius: 3px;
  603. font-size: 11px;
  604. }
  605. .nrc-env-badge {
  606. font-weight: 600;
  607. padding: 1px 6px;
  608. border-radius: 3px;
  609. flex-shrink: 0;
  610. }
  611. .nrc-env-badge.failed {
  612. background: rgba(239, 68, 68, 0.15);
  613. color: #f87171;
  614. }
  615. .nrc-env-msg {
  616. color: #f87171;
  617. word-break: break-all;
  618. }
  619. .nrc-var {
  620. display: flex;
  621. flex-direction: column;
  622. gap: 2px;
  623. padding: 4px 0;
  624. border-bottom: 1px solid rgba(255,255,255,0.02);
  625. }
  626. .nrc-var:last-child { border-bottom: none; }
  627. .nrc-var-key {
  628. font-family: 'Cascadia Code', 'Fira Code', monospace;
  629. font-size: 11px;
  630. font-weight: 600;
  631. }
  632. .nrc-var-value {
  633. font-family: 'Cascadia Code', 'Fira Code', monospace;
  634. font-size: 11px;
  635. color: var(--text-secondary, #aaa);
  636. white-space: pre-wrap;
  637. word-break: break-all;
  638. max-height: 120px;
  639. overflow-y: auto;
  640. }
  641. /* 思考过程展示(与 WorkflowEditor 实时思考区样式保持一致) */
  642. .nrc-thinking {
  643. margin: 6px 10px;
  644. border-radius: 6px;
  645. background: rgba(168, 85, 247, 0.05);
  646. border: 1px solid rgba(168, 85, 247, 0.2);
  647. overflow: hidden;
  648. }
  649. .nrc-thinking-header {
  650. display: flex;
  651. align-items: center;
  652. gap: 8px;
  653. padding: 5px 10px;
  654. font-size: 11px;
  655. background: rgba(168, 85, 247, 0.08);
  656. }
  657. .nrc-thinking-tag {
  658. color: #c084fc;
  659. font-weight: 600;
  660. }
  661. .nrc-thinking-static {
  662. color: var(--text-tertiary, #888);
  663. font-size: 10px;
  664. }
  665. .nrc-thinking-expand {
  666. margin-left: auto;
  667. font-size: 10px;
  668. color: var(--text-tertiary, #666);
  669. transition: transform 0.2s;
  670. flex-shrink: 0;
  671. }
  672. .nrc-thinking-expand.expanded {
  673. transform: rotate(90deg);
  674. }
  675. .nrc-thinking-body {
  676. margin: 0;
  677. padding: 8px 12px;
  678. max-height: 320px;
  679. overflow: auto;
  680. font-family: 'Cascadia Code', 'Fira Code', monospace;
  681. font-size: 11px;
  682. color: var(--text-secondary, #ccc);
  683. white-space: pre-wrap;
  684. word-break: break-all;
  685. }
  686. .nrc-error {
  687. padding: 6px 10px;
  688. color: #f87171;
  689. font-size: 11px;
  690. }
  691. /* 完整上下文快照折叠区 */
  692. .nrc-ctx-toggle {
  693. display: flex;
  694. align-items: center;
  695. gap: 6px;
  696. padding: 6px 10px;
  697. cursor: pointer;
  698. border-top: 1px solid rgba(255,255,255,0.04);
  699. font-size: 11px;
  700. color: var(--text-secondary, #aaa);
  701. user-select: none;
  702. }
  703. .nrc-ctx-toggle:hover { background: rgba(99,102,241,0.05); }
  704. .nrc-ctx-icon {
  705. font-size: 9px;
  706. color: var(--text-tertiary, #888);
  707. transition: transform 0.2s;
  708. }
  709. .nrc-ctx-icon.expanded { transform: rotate(90deg); }
  710. .nrc-ctx-label { font-weight: 500; }
  711. .nrc-ctx-panel {
  712. padding: 6px 10px;
  713. border-top: 1px solid rgba(255,255,255,0.04);
  714. max-height: 280px;
  715. overflow-y: auto;
  716. background: rgba(0,0,0,0.15);
  717. }
  718. .nrc-ctx-var {
  719. display: flex;
  720. flex-direction: column;
  721. gap: 2px;
  722. padding: 4px 0;
  723. border-bottom: 1px solid rgba(255,255,255,0.02);
  724. }
  725. .nrc-ctx-var:last-child { border-bottom: none; }
  726. .nrc-ctx-key {
  727. font-family: 'Cascadia Code', 'Fira Code', monospace;
  728. font-size: 11px;
  729. font-weight: 600;
  730. color: #818cf8;
  731. }
  732. .nrc-ctx-value {
  733. font-family: 'Cascadia Code', 'Fira Code', monospace;
  734. font-size: 11px;
  735. color: var(--text-secondary, #ccc);
  736. white-space: pre-wrap;
  737. word-break: break-all;
  738. max-height: 80px;
  739. overflow-y: auto;
  740. }
  741. .nrc-logs {
  742. border-top: 1px solid rgba(255,255,255,0.04);
  743. padding: 6px 10px;
  744. max-height: 400px;
  745. overflow-y: auto;
  746. }
  747. .nrc-log {
  748. padding: 4px 0;
  749. border-bottom: 1px solid rgba(255,255,255,0.02);
  750. font-size: 11px;
  751. line-height: 1.5;
  752. }
  753. .nrc-log:last-child { border-bottom: none; }
  754. .nrc-log-icon { margin-right: 4px; }
  755. .nrc-log-tag {
  756. font-size: 10px;
  757. padding: 1px 4px;
  758. border-radius: 3px;
  759. margin-right: 6px;
  760. font-weight: 600;
  761. flex-shrink: 0;
  762. }
  763. .log-type-tool_call .nrc-log-tag { background: rgba(59,130,246,0.15); color: #60a5fa; }
  764. .log-type-tool_result .nrc-log-tag { background: rgba(16,185,129,0.15); color: #34d399; }
  765. .log-type-thinking .nrc-log-tag { background: rgba(168,85,247,0.15); color: #c084fc; }
  766. .log-type-error .nrc-log-tag { background: rgba(239,68,68,0.15); color: #f87171; }
  767. .log-type-info .nrc-log-tag { background: rgba(107,114,128,0.15); color: #9ca3af; }
  768. .nrc-log-msg {
  769. color: var(--text-secondary, #ccc);
  770. word-break: break-all;
  771. }
  772. .nrc-log-detail {
  773. margin-top: 3px;
  774. margin-left: 22px;
  775. font-family: 'Cascadia Code', 'Fira Code', monospace;
  776. font-size: 10px;
  777. color: var(--text-tertiary, #888);
  778. background: rgba(0,0,0,0.2);
  779. padding: 4px 6px;
  780. border-radius: 4px;
  781. white-space: pre-wrap;
  782. word-break: break-all;
  783. max-height: 100px;
  784. overflow-y: auto;
  785. }
  786. </style>