Files
happy-life-star/docs/superpowers/specs/2026-05-24-ai-call-log-detail-design.md
T
peanut b9cf39f562 docs: 修复 AI 调用日志设计文档评审问题
- 补充 Request 校验注解和 @Schema
- 修正 keyword 搜索的 MyBatis-Plus 嵌套逻辑
- 明确旧接口废弃策略
- 弹窗布局增加 userId/requestId
- 展开行预览截断逻辑优化
- 补充性能量化阈值
- 增加 JsonViewer props 定义
- 明确时间筛选交互和非法 JSON 处理

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 11:32:50 +08:00

14 KiB
Raw Blame History

author, created_at, purpose
author created_at purpose
Peanut 2026-05-24 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 字符预览(超长截断 + ...
  • 截断逻辑确保不在 Unicode 转义序列中间截断(如 "中"),优先在空白字符处截断,保证预览可读性
  • 提供「查看完整详情」按钮,点击打开详情弹窗

详情弹窗

┌────────────────────────────────────────────────────────────┐
│  调用详情  [×]                                             │
├────────────────────────────────────────────────────────────┤
│  基本信息                                                  │
│  ┌────────────┬────────────┬────────────┬────────────┐    │
│  │ 调用时间   │ 场景       │ 服务商     │ 接口       │    │
│  │ 状态       │ 首字耗时   │ 总耗时     │ 片段数     │    │
│  │ 用户 ID    │ 请求 ID    │            │            │    │
│  └────────────┴────────────┴────────────┴────────────┘    │
│                                                            │
│  入参 [📋 复制]                                            │
│  ┌────────────────────────────────────────────────────┐   │
│  │ {                                                  │   │
│  │   "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 样式实现代码高亮。

JsonViewer.vue Props 接口:

interface JsonViewerProps {
  data: string | object      // JSON 字符串或对象
  title?: string             // 标题,如"入参""出参"
  showCopy?: boolean         // 是否显示复制按钮,默认 true
  maxHeight?: string         // 最大高度,默认 "300px"
}

配色方案:

  • 字符串值 → #a5d6a7(浅绿)
  • 数字/布尔 → #90caf9(浅蓝)
  • key → #ce93d8(浅紫)
  • 标点符号 → #b0bec5(灰)

背景使用深色主题(#1e1e1e),与当前管理后台暗色风格一致。

非法 JSON 处理:inputText/outputText 不是合法 JSON,直接展示原始文本(等宽字体、自动换行),不报错。

筛选栏参数

interface LogQueryParams {
  status?: string        // running / success / failed
  sceneCode?: string
  providerCode?: string
  endpointCode?: string
  startTime?: string     // ISO 8601 格式,如 "2026-05-17T00:00:00"
  endTime?: string       // ISO 8601 格式,如 "2026-05-24T23:59:59"
  keyword?: string       // 搜索入参/出参内容
  pageNum?: number
  pageSize?: number
}

时间筛选交互: 前端筛选栏提供快捷选项(近7天、近30天、自定义范围),由前端组件将快捷选项转换为 startTime/endTime 后传给后端。自定义范围时使用 Element Plus el-date-picker 选择起止时间。

展开行预览逻辑

function previewText(jsonStr: string, maxLen = 200): string {
  if (!jsonStr) return '-'
  return jsonStr.length > maxLen ? jsonStr.slice(0, maxLen) + '...' : jsonStr
}

API 变更

// 原接口(废弃,仅 AiRoutingList.vue 中使用,直接替换)
export function listAiCallLogs(limit = 50) { ... }

// 新接口
export function queryAiCallLogs(params: LogQueryParams) {
  return request({ url: '/ai/call-logs', method: 'post', data: params })
}

兼容性说明: 经检查,listAiCallLogs 仅在 AiRoutingList.vue 的调用日志 Tab 中使用,无其他调用方。本次直接替换,不保留旧接口。

后端 API 设计

接口定义

@PostMapping("/call-logs")
@Operation(summary = "分页查询 AI 调用日志")
public Result<Page<AiCallLog>> queryCallLogs(@RequestBody @Valid AiCallLogQueryRequest request)

Request 对象

@Schema(description = "AI 调用日志查询请求")
public class AiCallLogQueryRequest {

    @Schema(description = "状态:running / success / failed")
    private String status;

    @Schema(description = "场景编码")
    private String sceneCode;

    @Schema(description = "服务商编码")
    private String providerCode;

    @Schema(description = "接口编码")
    private String endpointCode;

    @Schema(description = "开始时间")
    private LocalDateTime startTime;

    @Schema(description = "结束时间")
    private LocalDateTime endTime;

    @Schema(description = "入参/出参关键词搜索")
    @Size(max = 200, message = "关键词长度不能超过 200")
    private String keyword;

    @Schema(description = "页码", example = "1")
    @Min(value = 1, message = "页码必须大于 0")
    private Integer pageNum = 1;

    @Schema(description = "每页条数", example = "20")
    @Min(value = 1, message = "每页条数必须大于 0")
    @Max(value = 100, message = "每页条数不能超过 100")
    private Integer pageSize = 20;
}

Service 查询逻辑

@Override
public Page<AiCallLog> query(AiCallLogQueryRequest request) {
    Page<AiCallLog> page = new Page<>(request.getPageNum(), request.getPageSize());
    LambdaQueryWrapper<AiCallLog> 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.nested(i -> i.like(AiCallLog::getInputText, request.getKeyword())
                                       .or()
                                       .like(AiCallLog::getOutputText, request.getKeyword())));
    }

    return page(page, wrapper);
}

性能考虑

  • input_textoutput_text 可能存储大 JSON(聊天记录),LIKE 搜索在数据量极大时会变慢
  • 量化阈值: 当日志表数据量超过 10 万条,或 keyword 搜索响应时间超过 500ms 时,启动优化评估:
    • 方案 1:为 input_textoutput_text 增加 MySQL 全文索引(FULLTEXT
    • 方案 2:限制 keyword 搜索只查询近 30 天的数据(配合 create_time 索引)
  • 当前日志量不大,先使用 LIKE。

数据模型

复用现有 AiCallLog 实体,无需新增表或字段。详情弹窗展示字段:

字段 说明
createTime 调用时间
sceneCode 场景编码
providerCode 服务商编码
endpointCode 接口编码
status 状态
firstTokenMs 首字耗时
durationMs 总耗时
streamChunks 片段数
inputText 入参(JSON 格式化)
outputText 出参(JSON 格式化)
errorCode 错误码
errorMessage 错误信息
userId 用户 ID
requestId 请求 ID