From f98617a70ac9548cf80bea23a746834243f4e84e Mon Sep 17 00:00:00 2001 From: Peanut Date: Sun, 24 May 2026 11:30:34 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20AI=20=E8=B0=83=E7=94=A8=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E8=AF=A6=E6=83=85=E6=9F=A5=E7=9C=8B=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- .../2026-05-24-ai-call-log-detail-design.md | 228 ++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-24-ai-call-log-detail-design.md diff --git a/docs/superpowers/specs/2026-05-24-ai-call-log-detail-design.md b/docs/superpowers/specs/2026-05-24-ai-call-log-detail-design.md new file mode 100644 index 0000000..6ed4241 --- /dev/null +++ b/docs/superpowers/specs/2026-05-24-ai-call-log-detail-design.md @@ -0,0 +1,228 @@ +--- +author: Peanut +created_at: 2026-05-24 +purpose: AI 调用日志详情查看功能设计文档 +--- + +# AI 调用日志详情查看功能设计 + +## 背景 + +后台管理 `web-admin` 中 AI 配置管理的「调用日志」页签目前仅展示基础列表信息(调用时间、场景、服务商、接口、状态、片段数、首字耗时、总耗时、错误码)。入参(`inputText`)、出参(`outputText`)、错误详情(`errorMessage`)等字段已有存储但完全隐藏,不利于后期统计和问题排查。 + +## 目标 + +1. 在调用日志列表中支持展开行快速预览入参/出参 +2. 提供详情弹窗,完整展示所有字段,入参和出参需 JSON 格式化高亮显示 +3. 支持一键复制入参/出参 JSON 内容 +4. 后端支持分页、多条件筛选、关键词搜索入参出参内容 + +## 交互设计 + +### 列表页(调用日志 Tab) + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ [筛选栏] 状态[全部▼] 场景[全部▼] 时间[近7天▼] 关键词[____] [搜索] │ +├─────────────────────────────────────────────────────────────────────┤ +│ ▼ 调用时间 场景 服务商 接口 状态 ... │ +│ ───────────────────────────────────────────────────────────────── │ +│ ▶ 2025-05-24 script_gen dify dify.xxx 成功 ... │ +│ ▼ 2025-05-24 emotion coze coze.summary 失败 ... │ +│ ┌────────────────────────────────────────────────────────────┐ │ +│ │ 入参预览: {"messages":[{"role":"user","content":"..."}]} │ │ +│ │ 出参预览: AI 分析完成,用户情绪状态为... │ │ +│ │ [查看完整详情] │ │ +│ └────────────────────────────────────────────────────────────┘ │ +│ ▶ 2025-05-23 short_story dify dify.story 成功 ... │ +├─────────────────────────────────────────────────────────────────────┤ +│ 共 156 条 [1][2][3]...[16] 每页 10/20/50 条 │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +**展开行交互:** +- 点击行首展开/收起图标切换展开状态 +- 展开区域显示入参和出参的前 200 字符预览(超长截断 + `...`) +- 提供「查看完整详情」按钮,点击打开详情弹窗 + +### 详情弹窗 + +``` +┌────────────────────────────────────────────────────────────┐ +│ 调用详情 [×] │ +├────────────────────────────────────────────────────────────┤ +│ 基本信息 │ +│ ┌────────────┬────────────┬────────────┬────────────┐ │ +│ │ 调用时间 │ 场景 │ 服务商 │ 接口 │ │ +│ │ 状态 │ 首字耗时 │ 总耗时 │ 片段数 │ │ +│ └────────────┴────────────┴────────────┴────────────┘ │ +│ │ +│ 入参 [📋 复制] │ +│ ┌────────────────────────────────────────────────────┐ │ +│ │ { │ │ +│ │ "messages": [ │ │ +│ │ { "role": "user", "content": "我最近很焦虑..." }│ │ +│ │ ] │ │ +│ │ } │ │ +│ └────────────────────────────────────────────────────┘ │ +│ │ +│ 出参 [📋 复制] │ +│ ┌────────────────────────────────────────────────────┐ │ +│ │ { │ │ +│ │ "response": "我理解你的感受..." │ │ +│ │ } │ │ +│ └────────────────────────────────────────────────────┘ │ +│ │ +│ 错误信息(仅失败时显示) │ +│ ┌────────────────────────────────────────────────────┐ │ +│ │ AI_STREAM_INTERRUPTED: 连接超时 │ │ +│ └────────────────────────────────────────────────────┘ │ +├────────────────────────────────────────────────────────────┤ +│ [关闭] │ +└────────────────────────────────────────────────────────────┘ +``` + +**弹窗特性:** +- 入参/出参使用代码高亮 + 等宽字体,支持纵向滚动 +- 每个代码块右上角有「复制」按钮,点击复制原始 JSON 到剪贴板 +- 错误信息区域仅在 `status === 'failed'` 时显示 +- 弹窗宽度 800px + +## 前端组件设计 + +### 文件变更 + +| 文件 | 说明 | +|------|------| +| `web-admin/src/views/aiconfig/AiRoutingList.vue` | 修改:增加展开行、筛选栏、分页逻辑 | +| `web-admin/src/views/aiconfig/components/AiCallLogDetailDialog.vue` | 新增:详情弹窗 | +| `web-admin/src/components/JsonViewer.vue` | 新增:通用 JSON 格式化展示组件 | + +### JSON 高亮方案 + +不引入额外依赖,使用原生 `JSON.stringify(obj, null, 2)` + CSS 样式实现代码高亮: + +- 字符串值 → `#a5d6a7`(浅绿) +- 数字/布尔 → `#90caf9`(浅蓝) +- key → `#ce93d8`(浅紫) +- 标点符号 → `#b0bec5`(灰) + +背景使用深色主题(`#1e1e1e`),与当前管理后台暗色风格一致。 + +### 筛选栏参数 + +```typescript +interface LogQueryParams { + status?: string // running / success / failed + sceneCode?: string + providerCode?: string + endpointCode?: string + startTime?: string + endTime?: string + keyword?: string // 搜索入参/出参内容 + pageNum?: number + pageSize?: number +} +``` + +### 展开行预览逻辑 + +```typescript +function previewText(jsonStr: string, maxLen = 200): string { + if (!jsonStr) return '-' + return jsonStr.length > maxLen ? jsonStr.slice(0, maxLen) + '...' : jsonStr +} +``` + +### API 变更 + +```typescript +// 原接口 +export function listAiCallLogs(limit = 50) { ... } + +// 新接口 +export function queryAiCallLogs(params: LogQueryParams) { + return request({ url: '/ai/call-logs', method: 'post', data: params }) +} +``` + +## 后端 API 设计 + +### 接口定义 + +```java +@PostMapping("/call-logs") +@Operation(summary = "分页查询 AI 调用日志") +public Result> queryCallLogs(@RequestBody @Valid AiCallLogQueryRequest request) +``` + +### Request 对象 + +```java +public class AiCallLogQueryRequest { + private String status; + private String sceneCode; + private String providerCode; + private String endpointCode; + private LocalDateTime startTime; + private LocalDateTime endTime; + private String keyword; + private Integer pageNum = 1; + private Integer pageSize = 20; +} +``` + +### Service 查询逻辑 + +```java +@Override +public Page query(AiCallLogQueryRequest request) { + Page page = new Page<>(request.getPageNum(), request.getPageSize()); + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + + wrapper.eq(AiCallLog::getIsDeleted, 0) + .eq(StringUtils.isNotBlank(request.getStatus()), AiCallLog::getStatus, request.getStatus()) + .eq(StringUtils.isNotBlank(request.getSceneCode()), AiCallLog::getSceneCode, request.getSceneCode()) + .eq(StringUtils.isNotBlank(request.getProviderCode()), AiCallLog::getProviderCode, request.getProviderCode()) + .eq(StringUtils.isNotBlank(request.getEndpointCode()), AiCallLog::getEndpointCode, request.getEndpointCode()) + .ge(request.getStartTime() != null, AiCallLog::getCreateTime, request.getStartTime()) + .le(request.getEndTime() != null, AiCallLog::getCreateTime, request.getEndTime()) + .orderByDesc(AiCallLog::getCreateTime); + + if (StringUtils.isNotBlank(request.getKeyword())) { + wrapper.and(w -> w.like(AiCallLog::getInputText, request.getKeyword()) + .or() + .like(AiCallLog::getOutputText, request.getKeyword())); + } + + return page(page, wrapper); +} +``` + +## 性能考虑 + +- `input_text` 和 `output_text` 可能存储大 JSON(聊天记录),LIKE 搜索在数据量极大时会变慢 +- 当前日志量不大,先使用 LIKE。后续如果数据量增长,可升级为: + - MySQL 全文索引(FULLTEXT) + - 或限制 keyword 搜索只查询近 N 天数据 + +## 数据模型 + +复用现有 `AiCallLog` 实体,无需新增表或字段。详情弹窗展示字段: + +| 字段 | 说明 | +|------|------| +| createTime | 调用时间 | +| sceneCode | 场景编码 | +| providerCode | 服务商编码 | +| endpointCode | 接口编码 | +| status | 状态 | +| firstTokenMs | 首字耗时 | +| durationMs | 总耗时 | +| streamChunks | 片段数 | +| inputText | 入参(JSON 格式化) | +| outputText | 出参(JSON 格式化) | +| errorCode | 错误码 | +| errorMessage | 错误信息 | +| userId | 用户 ID | +| requestId | 请求 ID |