DemandProcessView.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671
  1. <script setup lang="ts">
  2. import { computed, onMounted, ref, watch } from 'vue'
  3. import { fetchAgentOssLogs } from '../api/ossLog'
  4. import type { OssLogItem } from '../types/ossLog'
  5. const items = ref<OssLogItem[]>([])
  6. const agents = ref<string[]>([])
  7. const selectedAgent = ref('')
  8. const keyword = ref('')
  9. const loading = ref(true)
  10. const error = ref<string | null>(null)
  11. let requestId = 0
  12. const filteredItems = computed(() => {
  13. const query = keyword.value.trim().toLocaleLowerCase()
  14. if (!query) return items.value
  15. return items.value.filter((item) =>
  16. [item.log_name, item.agent_name, item.oss_path]
  17. .filter(Boolean)
  18. .join(' ')
  19. .toLocaleLowerCase()
  20. .includes(query),
  21. )
  22. })
  23. onMounted(() => loadLogs())
  24. watch(selectedAgent, () => loadLogs(selectedAgent.value))
  25. async function loadLogs(agentName?: string) {
  26. const currentRequest = ++requestId
  27. loading.value = true
  28. error.value = null
  29. try {
  30. const data = await fetchAgentOssLogs(agentName || null)
  31. if (currentRequest !== requestId) return
  32. items.value = data.items ?? []
  33. if (data.agents?.length) agents.value = data.agents
  34. } catch (e) {
  35. if (currentRequest !== requestId) return
  36. error.value = e instanceof Error ? e.message : String(e)
  37. } finally {
  38. if (currentRequest === requestId) loading.value = false
  39. }
  40. }
  41. function openHtml(item: OssLogItem) {
  42. const url = item.oss_path?.trim()
  43. if (!url) return
  44. window.open(url, '_blank', 'noopener,noreferrer')
  45. }
  46. function displayAgentName(value: string | null): string {
  47. if (!value) return '未标记 Agent'
  48. return value.replace(/_agent$/, '').replaceAll('_', ' ')
  49. }
  50. function formatTime(value: string | null): string {
  51. if (!value) return '—'
  52. const date = new Date(value)
  53. if (Number.isNaN(date.getTime())) return value
  54. return new Intl.DateTimeFormat('zh-CN', {
  55. year: 'numeric',
  56. month: '2-digit',
  57. day: '2-digit',
  58. hour: '2-digit',
  59. minute: '2-digit',
  60. second: '2-digit',
  61. hour12: false,
  62. }).format(date)
  63. }
  64. function agentTone(value: string | null): string {
  65. if (!value) return 'tone-gray'
  66. if (value.includes('find')) return 'tone-orange'
  67. if (value.includes('grade')) return 'tone-violet'
  68. if (value.includes('belong')) return 'tone-blue'
  69. if (value.includes('expand')) return 'tone-green'
  70. if (value.includes('generate')) return 'tone-cyan'
  71. return 'tone-gray'
  72. }
  73. </script>
  74. <template>
  75. <div class="audit-page">
  76. <header class="audit-header">
  77. <div>
  78. <div class="eyebrow"><span /> AGENT TRACEABILITY</div>
  79. <h1>Agent 运行审计</h1>
  80. <p>查看模型输入、思考过程、工具调用与完整运行结果。</p>
  81. </div>
  82. <div class="header-stat">
  83. <strong>{{ filteredItems.length }}</strong>
  84. <span>{{ selectedAgent ? '筛选结果' : '全部记录' }}</span>
  85. </div>
  86. </header>
  87. <section class="filter-bar" aria-label="日志筛选">
  88. <label class="agent-select">
  89. <span class="filter-label">指定 Agent</span>
  90. <span class="select-wrap">
  91. <i class="agent-dot" :class="agentTone(selectedAgent)" />
  92. <select v-model="selectedAgent" aria-label="选择 Agent">
  93. <option value="">全部 Agent</option>
  94. <option v-for="agent in agents" :key="agent" :value="agent">
  95. {{ displayAgentName(agent) }}
  96. </option>
  97. </select>
  98. </span>
  99. </label>
  100. <label class="search-control">
  101. <span class="search-icon" aria-hidden="true" />
  102. <input
  103. v-model="keyword"
  104. type="search"
  105. placeholder="搜索日志名称或 OSS 路径"
  106. aria-label="搜索日志"
  107. />
  108. <button v-if="keyword" type="button" aria-label="清空搜索" @click="keyword = ''">×</button>
  109. </label>
  110. <button class="refresh-button" type="button" :disabled="loading" @click="loadLogs(selectedAgent)">
  111. <span :class="{ spinning: loading }">↻</span>
  112. 刷新
  113. </button>
  114. </section>
  115. <div v-if="error" class="state error">
  116. <strong>日志加载失败</strong>
  117. <span>{{ error }}</span>
  118. <button type="button" @click="loadLogs(selectedAgent)">重新加载</button>
  119. </div>
  120. <section v-else class="table-card">
  121. <div class="table-headline">
  122. <span>
  123. {{ selectedAgent ? displayAgentName(selectedAgent) : '全部 Agent' }}
  124. <b>{{ filteredItems.length }}</b>
  125. </span>
  126. <small>按创建时间倒序 · 点击记录打开 HTML 审计详情</small>
  127. </div>
  128. <div v-if="loading" class="state">
  129. <span class="loader" />
  130. 正在加载审计记录…
  131. </div>
  132. <div v-else-if="!filteredItems.length" class="state empty">
  133. <span class="empty-icon">◎</span>
  134. <strong>{{ keyword ? '没有匹配的日志' : '暂无审计记录' }}</strong>
  135. <small>{{ keyword ? '尝试更换关键词或 Agent' : 'Agent 执行后,日志会出现在这里' }}</small>
  136. </div>
  137. <div v-else class="table-wrap">
  138. <table>
  139. <thead>
  140. <tr>
  141. <th class="col-time">创建时间</th>
  142. <th class="col-agent">Agent</th>
  143. <th class="col-name">日志名称</th>
  144. <th class="col-path">OSS 审计文件</th>
  145. <th class="col-open" aria-label="打开" />
  146. </tr>
  147. </thead>
  148. <tbody>
  149. <tr
  150. v-for="item in filteredItems"
  151. :key="item.id"
  152. class="row"
  153. tabindex="0"
  154. @click="openHtml(item)"
  155. @keydown.enter="openHtml(item)"
  156. >
  157. <td class="time">{{ formatTime(item.create_time) }}</td>
  158. <td>
  159. <span class="agent-badge" :class="agentTone(item.agent_name)">
  160. <i />
  161. {{ displayAgentName(item.agent_name) }}
  162. </span>
  163. </td>
  164. <td class="name">{{ item.log_name || '未命名日志' }}</td>
  165. <td class="path" :title="item.oss_path || ''">{{ item.oss_path || '—' }}</td>
  166. <td class="open-arrow">↗</td>
  167. </tr>
  168. </tbody>
  169. </table>
  170. </div>
  171. </section>
  172. </div>
  173. </template>
  174. <style scoped>
  175. .audit-page {
  176. display: flex;
  177. min-height: 100%;
  178. flex-direction: column;
  179. gap: 18px;
  180. color: #202738;
  181. }
  182. .audit-header {
  183. display: flex;
  184. align-items: flex-end;
  185. justify-content: space-between;
  186. padding: 10px 4px 4px;
  187. }
  188. .eyebrow {
  189. display: flex;
  190. align-items: center;
  191. gap: 8px;
  192. color: #71798b;
  193. font-size: 9px;
  194. font-weight: 800;
  195. letter-spacing: 0.17em;
  196. }
  197. .eyebrow span {
  198. width: 7px;
  199. height: 7px;
  200. border-radius: 50%;
  201. background: #6666d4;
  202. box-shadow: 0 0 0 4px rgba(102, 102, 212, 0.11);
  203. }
  204. .audit-header h1 {
  205. margin: 9px 0 5px;
  206. font-size: 28px;
  207. letter-spacing: -0.04em;
  208. }
  209. .audit-header p {
  210. margin: 0;
  211. color: #858c9b;
  212. font-size: 12px;
  213. }
  214. .header-stat {
  215. display: flex;
  216. align-items: baseline;
  217. gap: 8px;
  218. padding: 0 4px;
  219. }
  220. .header-stat strong {
  221. font-size: 28px;
  222. letter-spacing: -0.05em;
  223. }
  224. .header-stat span {
  225. color: #959ba8;
  226. font-size: 10px;
  227. }
  228. .filter-bar {
  229. display: flex;
  230. min-height: 68px;
  231. align-items: center;
  232. gap: 12px;
  233. padding: 12px 16px;
  234. border: 1px solid #e5e7ec;
  235. border-radius: 15px;
  236. background: rgba(255, 255, 255, 0.88);
  237. }
  238. .agent-select {
  239. display: flex;
  240. min-width: 270px;
  241. align-items: center;
  242. gap: 11px;
  243. }
  244. .filter-label {
  245. color: #7e8594;
  246. font-size: 10px;
  247. font-weight: 700;
  248. white-space: nowrap;
  249. }
  250. .select-wrap {
  251. position: relative;
  252. flex: 1;
  253. }
  254. .select-wrap select {
  255. width: 100%;
  256. height: 40px;
  257. padding: 0 34px 0 31px;
  258. border: 1px solid #dfe2e8;
  259. border-radius: 9px;
  260. appearance: none;
  261. outline: none;
  262. background:
  263. linear-gradient(45deg, transparent 50%, #8d94a2 50%) calc(100% - 17px) 17px / 5px 5px no-repeat,
  264. linear-gradient(135deg, #8d94a2 50%, transparent 50%) calc(100% - 12px) 17px / 5px 5px no-repeat,
  265. #fff;
  266. color: #3f4758;
  267. font-size: 11px;
  268. font-weight: 600;
  269. }
  270. .select-wrap select:focus {
  271. border-color: #7777d8;
  272. box-shadow: 0 0 0 3px rgba(102, 102, 212, 0.09);
  273. }
  274. .agent-dot {
  275. position: absolute;
  276. z-index: 1;
  277. top: 50%;
  278. left: 13px;
  279. width: 7px;
  280. height: 7px;
  281. border-radius: 50%;
  282. transform: translateY(-50%);
  283. }
  284. .search-control {
  285. position: relative;
  286. flex: 1;
  287. }
  288. .search-control input {
  289. width: 100%;
  290. height: 40px;
  291. padding: 0 36px 0 39px;
  292. border: 1px solid #dfe2e8;
  293. border-radius: 9px;
  294. outline: none;
  295. background: #fff;
  296. color: #3f4758;
  297. font-size: 11px;
  298. }
  299. .search-control input:focus {
  300. border-color: #7777d8;
  301. box-shadow: 0 0 0 3px rgba(102, 102, 212, 0.09);
  302. }
  303. .search-control input::placeholder {
  304. color: #a6acb7;
  305. }
  306. .search-icon {
  307. position: absolute;
  308. z-index: 1;
  309. top: 50%;
  310. left: 15px;
  311. width: 12px;
  312. height: 12px;
  313. border: 1.5px solid #8f96a5;
  314. border-radius: 50%;
  315. transform: translateY(-58%);
  316. }
  317. .search-icon::after {
  318. position: absolute;
  319. right: -4px;
  320. bottom: -3px;
  321. width: 5px;
  322. height: 1.5px;
  323. background: #8f96a5;
  324. content: '';
  325. transform: rotate(45deg);
  326. }
  327. .search-control button {
  328. position: absolute;
  329. top: 50%;
  330. right: 10px;
  331. width: 24px;
  332. height: 24px;
  333. border: 0;
  334. border-radius: 6px;
  335. background: transparent;
  336. color: #949aa7;
  337. cursor: pointer;
  338. transform: translateY(-50%);
  339. }
  340. .refresh-button {
  341. display: inline-flex;
  342. height: 40px;
  343. align-items: center;
  344. gap: 7px;
  345. padding: 0 14px;
  346. border: 1px solid #dfe2e8;
  347. border-radius: 9px;
  348. background: #fff;
  349. color: #687082;
  350. font-size: 10px;
  351. font-weight: 700;
  352. cursor: pointer;
  353. }
  354. .refresh-button:disabled {
  355. cursor: wait;
  356. opacity: 0.58;
  357. }
  358. .spinning {
  359. animation: spin 0.75s linear infinite;
  360. }
  361. .table-card {
  362. display: flex;
  363. min-height: 360px;
  364. flex: 1;
  365. flex-direction: column;
  366. overflow: hidden;
  367. border: 1px solid #e4e7ed;
  368. border-radius: 16px;
  369. background: rgba(255, 255, 255, 0.94);
  370. box-shadow: 0 12px 35px rgba(35, 42, 60, 0.035);
  371. }
  372. .table-headline {
  373. display: flex;
  374. min-height: 55px;
  375. align-items: center;
  376. justify-content: space-between;
  377. padding: 0 18px;
  378. border-bottom: 1px solid #eceef2;
  379. }
  380. .table-headline span {
  381. font-size: 11px;
  382. font-weight: 700;
  383. }
  384. .table-headline b {
  385. display: inline-flex;
  386. min-width: 23px;
  387. height: 19px;
  388. align-items: center;
  389. justify-content: center;
  390. margin-left: 7px;
  391. padding: 0 6px;
  392. border-radius: 999px;
  393. background: #eeeeff;
  394. color: #6262ce;
  395. font-size: 9px;
  396. }
  397. .table-headline small {
  398. color: #9ba1ad;
  399. font-size: 9px;
  400. }
  401. .table-wrap {
  402. flex: 1;
  403. overflow: auto;
  404. }
  405. table {
  406. width: 100%;
  407. border-collapse: collapse;
  408. table-layout: fixed;
  409. font-size: 11px;
  410. }
  411. th,
  412. td {
  413. height: 52px;
  414. padding: 0 15px;
  415. border-bottom: 1px solid #f0f1f4;
  416. text-align: left;
  417. vertical-align: middle;
  418. }
  419. th {
  420. position: sticky;
  421. z-index: 1;
  422. top: 0;
  423. height: 40px;
  424. background: #fafafd;
  425. color: #8d94a3;
  426. font-size: 9px;
  427. font-weight: 700;
  428. letter-spacing: 0.03em;
  429. }
  430. .col-time { width: 174px; }
  431. .col-agent { width: 210px; }
  432. .col-name { width: 260px; }
  433. .col-open { width: 44px; }
  434. .row {
  435. outline: none;
  436. cursor: pointer;
  437. transition: background 0.14s;
  438. }
  439. .row:hover,
  440. .row:focus {
  441. background: #f8f8fc;
  442. }
  443. .time {
  444. color: #727a89;
  445. font-variant-numeric: tabular-nums;
  446. }
  447. .agent-badge {
  448. display: inline-flex;
  449. max-width: 100%;
  450. height: 25px;
  451. align-items: center;
  452. gap: 7px;
  453. overflow: hidden;
  454. padding: 0 9px;
  455. border-radius: 7px;
  456. background: var(--tone-soft);
  457. color: var(--tone);
  458. font-size: 9px;
  459. font-weight: 700;
  460. text-overflow: ellipsis;
  461. text-transform: capitalize;
  462. white-space: nowrap;
  463. }
  464. .agent-badge i {
  465. width: 6px;
  466. height: 6px;
  467. flex-shrink: 0;
  468. border-radius: 50%;
  469. background: var(--tone);
  470. }
  471. .tone-blue { --tone: #4d75c8; --tone-soft: #eaf1ff; background: var(--tone, #4d75c8); }
  472. .tone-violet { --tone: #7560c8; --tone-soft: #f0ecff; background: var(--tone, #7560c8); }
  473. .tone-orange { --tone: #c5793d; --tone-soft: #fff0e2; background: var(--tone, #c5793d); }
  474. .tone-green { --tone: #2b9272; --tone-soft: #e5f7f0; background: var(--tone, #2b9272); }
  475. .tone-cyan { --tone: #308ba1; --tone-soft: #e4f5f8; background: var(--tone, #308ba1); }
  476. .tone-gray { --tone: #7d8492; --tone-soft: #eef0f3; background: var(--tone, #7d8492); }
  477. .agent-badge.tone-blue,
  478. .agent-badge.tone-violet,
  479. .agent-badge.tone-orange,
  480. .agent-badge.tone-green,
  481. .agent-badge.tone-cyan,
  482. .agent-badge.tone-gray {
  483. background: var(--tone-soft);
  484. }
  485. .name {
  486. overflow: hidden;
  487. color: #333b4c;
  488. font-weight: 600;
  489. text-overflow: ellipsis;
  490. white-space: nowrap;
  491. }
  492. .path {
  493. overflow: hidden;
  494. color: #89909e;
  495. font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
  496. font-size: 9px;
  497. text-overflow: ellipsis;
  498. white-space: nowrap;
  499. }
  500. .open-arrow {
  501. color: #a4aab5;
  502. font-size: 14px;
  503. }
  504. .state {
  505. display: flex;
  506. min-height: 280px;
  507. align-items: center;
  508. justify-content: center;
  509. gap: 10px;
  510. flex: 1;
  511. color: #838a99;
  512. font-size: 11px;
  513. }
  514. .state.error {
  515. min-height: 260px;
  516. flex-direction: column;
  517. border: 1px solid #f0d9d7;
  518. border-radius: 16px;
  519. background: #fff7f6;
  520. color: #a95752;
  521. }
  522. .state.error strong {
  523. font-size: 13px;
  524. }
  525. .state.error button {
  526. margin-top: 6px;
  527. padding: 7px 12px;
  528. border: 1px solid #e3b9b6;
  529. border-radius: 7px;
  530. background: #fff;
  531. color: #a95752;
  532. cursor: pointer;
  533. }
  534. .state.empty {
  535. flex-direction: column;
  536. }
  537. .state.empty .empty-icon {
  538. color: #7373d5;
  539. font-size: 28px;
  540. }
  541. .state.empty strong {
  542. color: #636b7b;
  543. }
  544. .state.empty small {
  545. color: #a1a7b2;
  546. font-size: 9px;
  547. }
  548. .loader {
  549. width: 15px;
  550. height: 15px;
  551. border: 2px solid #dddff0;
  552. border-top-color: #6666d4;
  553. border-radius: 50%;
  554. animation: spin 0.75s linear infinite;
  555. }
  556. @keyframes spin {
  557. to { transform: rotate(360deg); }
  558. }
  559. @media (max-width: 760px) {
  560. .audit-header {
  561. align-items: flex-start;
  562. flex-direction: column;
  563. gap: 14px;
  564. }
  565. .filter-bar {
  566. align-items: stretch;
  567. flex-direction: column;
  568. }
  569. .agent-select {
  570. min-width: 0;
  571. align-items: flex-start;
  572. flex-direction: column;
  573. }
  574. .select-wrap {
  575. width: 100%;
  576. }
  577. .table-headline {
  578. align-items: flex-start;
  579. flex-direction: column;
  580. justify-content: center;
  581. gap: 4px;
  582. }
  583. .table-wrap {
  584. overflow-x: auto;
  585. }
  586. table {
  587. min-width: 930px;
  588. }
  589. }
  590. </style>