Files
happy-life-star/docs/superpowers/specs/2026-05-24-ai-call-log-detail-design.md
T
peanut 90fca4922e docs: 修复第二轮 spec review 问题
- 分页返回类型改为 PageResult
- JSON 高亮改为 highlight.js
- 补充 Service 接口变更、前端类型定义、分页适配
- 修正展开行预览截断逻辑
- 明确旧接口共存策略、下拉数据来源、时间边界
- 补充 Request 存放路径、keyword 空值处理

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

15 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 格式化使用 highlight.js 轻量库(项目已可通过 npm 安装),对 JSON 语言进行语法高亮,无需手写解析逻辑。配合深色主题 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 选择起止时间。

  • "近7天":今天 00:00:00 往前推 6 天(含今天)
  • "近30天":今天 00:00:00 往前推 29 天(含今天)
  • 自定义:用户选择起止日期,开始时间为所选日期 00:00:00,结束时间为所选日期 23:59:59

下拉数据来源: 场景、服务商、接口下拉选项复用页面已加载的 providers / endpoints / scenes 数据(loadAll() 中已有),无需额外接口。

展开行预览逻辑

function previewText(jsonStr: string, maxLen = 200): string {
  if (!jsonStr) return '-'
  if (jsonStr.length <= maxLen) return jsonStr
  // 优先在空白字符处截断,避免截断在转义序列中间
  const truncated = jsonStr.slice(0, maxLen)
  const lastSpace = truncated.search(/\s+(?!.*\s)/)
  return lastSpace > maxLen * 0.5 ? truncated.slice(0, lastSpace) + '...' : truncated + '...'
}

类型定义

web-admin/src/types/aiconfig.ts 中新增:

export interface LogQueryParams {
  status?: string
  sceneCode?: string
  providerCode?: string
  endpointCode?: string
  startTime?: string
  endTime?: string
  keyword?: string
  pageNum?: number
  pageSize?: number
}

export interface PageResult<T> {
  records: T[]
  total: number
  pageNum: number
  pageSize: number
}

AiRoutingList.vue 中的 logs 数据从 AiCallLog[] 改为分页结构:const logs = ref<PageResult<AiCallLog>>({ records: [], total: 0, pageNum: 1, pageSize: 20 }),表格数据源绑定 logs.value.records

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 中使用,无其他调用方。新接口使用 POST,旧接口使用 GET,路径相同方法不同,技术上可共存。实现时直接替换前端调用即可,旧 GET 接口可保留或后续清理。

后端 API 设计

接口定义

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

Request 对象

存放路径:backend-single/src/main/java/com/emotion/dto/request/ai/AiCallLogQueryRequest.java

@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 接口变更

AiCallLogService 接口中新增方法:

PageResult<AiCallLog> query(AiCallLogQueryRequest request);

latest(Integer limit) 方法保留(前端旧接口调用或后台其他功能可能依赖)。

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);

    // keyword 为空字符串或纯空格时不执行 LIKE 搜索(isNotBlank 已处理)
    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_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