|
@@ -1,137 +1,179 @@
|
|
|
<template>
|
|
<template>
|
|
|
- <div class="langgraph-admin page-container">
|
|
|
|
|
- <!-- 顶部状态栏 -->
|
|
|
|
|
- <el-row :gutter="16" class="status-row">
|
|
|
|
|
- <el-col :span="6">
|
|
|
|
|
- <el-card :class="['status-card', statusClass]" shadow="hover">
|
|
|
|
|
- <div class="status-icon">{{ statusIcon }}</div>
|
|
|
|
|
- <div class="status-info">
|
|
|
|
|
- <div class="status-label">服务状态</div>
|
|
|
|
|
- <div class="status-value">{{ statusText }}</div>
|
|
|
|
|
- <div class="status-sub">{{ statusTime }}</div>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <div class="lg-admin page-container">
|
|
|
|
|
+ <!-- 顶部状态栏(固定) -->
|
|
|
|
|
+ <el-row :gutter="12" class="top-bar">
|
|
|
|
|
+ <el-col :span="4">
|
|
|
|
|
+ <el-card shadow="hover" class="mini-card" :class="statusClass">
|
|
|
|
|
+ <div class="mini-icon">{{ statusIcon }}</div>
|
|
|
|
|
+ <div class="mini-text">{{ statusText }}</div>
|
|
|
|
|
+ <div class="mini-sub">{{ statusTime }}</div>
|
|
|
</el-card>
|
|
</el-card>
|
|
|
</el-col>
|
|
</el-col>
|
|
|
- <el-col :span="6">
|
|
|
|
|
- <el-card shadow="hover">
|
|
|
|
|
- <div class="status-icon" style="background:linear-gradient(135deg,#667eea,#764ba2)">🔗</div>
|
|
|
|
|
- <div class="status-info">
|
|
|
|
|
- <div class="status-label">服务地址</div>
|
|
|
|
|
- <div class="status-value" style="font-size:13px">{{ langgraphUrl }}</div>
|
|
|
|
|
- <div class="status-sub">端口 9000 · v{{ serviceVersion }}</div>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <el-col :span="4">
|
|
|
|
|
+ <el-card shadow="hover" class="mini-card">
|
|
|
|
|
+ <div class="mini-icon" style="background:linear-gradient(135deg,#667eea,#764ba2)">🔗</div>
|
|
|
|
|
+ <div class="mini-text">{{ langgraphUrl }}</div>
|
|
|
|
|
+ <div class="mini-sub">端口 9000 · v{{ serviceVersion }}</div>
|
|
|
</el-card>
|
|
</el-card>
|
|
|
</el-col>
|
|
</el-col>
|
|
|
- <el-col :span="6">
|
|
|
|
|
- <el-card shadow="hover">
|
|
|
|
|
- <div class="status-icon" style="background:linear-gradient(135deg,#11998e,#38ef7d)">📊</div>
|
|
|
|
|
- <div class="status-info">
|
|
|
|
|
- <div class="status-label">API 端点数</div>
|
|
|
|
|
- <div class="status-value">{{ apiCount }}</div>
|
|
|
|
|
- <div class="status-sub">个可用接口</div>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <el-col :span="4">
|
|
|
|
|
+ <el-card shadow="hover" class="mini-card">
|
|
|
|
|
+ <div class="mini-icon" style="background:linear-gradient(135deg,#11998e,#38ef7d)">📡</div>
|
|
|
|
|
+ <div class="mini-text">{{ apiCount }} 个端点</div>
|
|
|
|
|
+ <div class="mini-sub">API 接口总数</div>
|
|
|
</el-card>
|
|
</el-card>
|
|
|
</el-col>
|
|
</el-col>
|
|
|
- <el-col :span="6">
|
|
|
|
|
- <el-card shadow="hover">
|
|
|
|
|
- <div class="status-icon" style="background:linear-gradient(135deg,#fc4a1a,#f7b733)">⚡</div>
|
|
|
|
|
- <div class="status-info">
|
|
|
|
|
- <div class="status-label">刷新操作</div>
|
|
|
|
|
- <div style="margin-top:6px">
|
|
|
|
|
- <el-button size="mini" type="primary" @click="refreshAll" :loading="refreshing">刷新</el-button>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <el-col :span="4">
|
|
|
|
|
+ <el-card shadow="hover" class="mini-card">
|
|
|
|
|
+ <div class="mini-icon" style="background:linear-gradient(135deg,#fc4a1a,#f7b733)">📋</div>
|
|
|
|
|
+ <div class="mini-text">{{ totalLogs }} 条日志</div>
|
|
|
|
|
+ <div class="mini-sub">请求历史记录</div>
|
|
|
</el-card>
|
|
</el-card>
|
|
|
</el-col>
|
|
</el-col>
|
|
|
|
|
+ <el-col :span="8" class="top-actions">
|
|
|
|
|
+ <el-button size="small" type="primary" @click="refreshAll" :loading="refreshing">🔄 刷新</el-button>
|
|
|
|
|
+ <el-button size="small" @click="fetchLogs">📋 拉取日志</el-button>
|
|
|
|
|
+ <el-button size="small" type="danger" @click="clearLogsConfirm" :disabled="!logList.length">清空日志</el-button>
|
|
|
|
|
+ </el-col>
|
|
|
</el-row>
|
|
</el-row>
|
|
|
|
|
|
|
|
- <!-- 组件健康状态 -->
|
|
|
|
|
- <el-card class="component-card" shadow="never">
|
|
|
|
|
- <div slot="header" class="card-header">
|
|
|
|
|
- <span>🧩 组件健康状态</span>
|
|
|
|
|
- <el-button size="mini" @click="fetchDetailedHealth">重新检测</el-button>
|
|
|
|
|
- </div>
|
|
|
|
|
- <el-table :data="componentList" border stripe size="small">
|
|
|
|
|
- <el-table-column prop="name" label="组件" width="160" />
|
|
|
|
|
- <el-table-column prop="status" label="状态" width="100">
|
|
|
|
|
- <template slot-scope="{ row }">
|
|
|
|
|
- <el-tag :type="row.status === 'ok' ? 'success' : row.status === 'error' ? 'danger' : 'info'" size="small">
|
|
|
|
|
- {{ row.status === 'ok' ? '正常' : row.status === 'error' ? '异常' : '未知' }}
|
|
|
|
|
- </el-tag>
|
|
|
|
|
- </template>
|
|
|
|
|
- </el-table-column>
|
|
|
|
|
- <el-table-column prop="detail" label="详情" />
|
|
|
|
|
- </el-table>
|
|
|
|
|
- </el-card>
|
|
|
|
|
|
|
+ <!-- 标签页 -->
|
|
|
|
|
+ <el-tabs v-model="activeTab" type="border-card" class="main-tabs">
|
|
|
|
|
+
|
|
|
|
|
+ <!-- ===== 概览 ===== -->
|
|
|
|
|
+ <el-tab-pane label="📊 概览" name="overview">
|
|
|
|
|
+ <el-row :gutter="16">
|
|
|
|
|
+ <el-col :span="16">
|
|
|
|
|
+ <el-card shadow="never" class="panel">
|
|
|
|
|
+ <div slot="header"><span>🧩 组件健康状态</span></div>
|
|
|
|
|
+ <el-table :data="componentList" border stripe size="small" max-height="300">
|
|
|
|
|
+ <el-table-column prop="name" label="组件" width="140" />
|
|
|
|
|
+ <el-table-column prop="status" label="状态" width="100">
|
|
|
|
|
+ <template slot-scope="{ row }">
|
|
|
|
|
+ <el-tag :type="row.status === 'ok' ? 'success' : row.status === 'error' ? 'danger' : 'info'" size="small">
|
|
|
|
|
+ {{ row.status === 'ok' ? '正常' : row.status === 'error' ? '异常' : '未知' }}
|
|
|
|
|
+ </el-tag>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="detail" label="详情" show-overflow-tooltip />
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ <el-col :span="8">
|
|
|
|
|
+ <el-card shadow="never" class="panel">
|
|
|
|
|
+ <div slot="header"><span>⚙️ 服务配置</span></div>
|
|
|
|
|
+ <el-descriptions :column="1" border size="small">
|
|
|
|
|
+ <el-descriptions-item label="LLM 模型">{{ config.LLM_MODEL || '-' }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="LLM Base URL">{{ config.LLM_BASE_URL || '-' }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="Embedding 模型">{{ config.EMBEDDING_MODEL || '-' }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="Embedding URL">{{ config.EMBEDDING_BASE_URL || '-' }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="Java 后端">{{ config.JAVA_BASE_URL || '-' }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="Chroma DB">{{ config.CHROMA_DB_PATH || '-' }}</el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="LangSmith 追踪">
|
|
|
|
|
+ <el-tag :type="config.LANGCHAIN_TRACING_V2 === 'true' ? 'success' : 'info'" size="mini">
|
|
|
|
|
+ {{ config.LANGCHAIN_TRACING_V2 === 'true' ? '已启用' : '未启用' }}
|
|
|
|
|
+ </el-tag>
|
|
|
|
|
+ </el-descriptions-item>
|
|
|
|
|
+ <el-descriptions-item label="日志级别">{{ config.LOG_LEVEL || 'info' }}</el-descriptions-item>
|
|
|
|
|
+ </el-descriptions>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ </el-row>
|
|
|
|
|
+ </el-tab-pane>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- ===== API 端点 ===== -->
|
|
|
|
|
+ <el-tab-pane label="📡 API 端点" name="apis">
|
|
|
|
|
+ <div class="tab-toolbar">
|
|
|
|
|
+ <el-input v-model="apiFilter" placeholder="筛选路径或说明..." size="small" prefix-icon="el-icon-search" style="width:280px" clearable />
|
|
|
|
|
+ <el-button size="small" @click="fetchSpec">刷新列表</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-table :data="filteredApiEndpoints" border stripe size="small" max-height="500" style="width:100%">
|
|
|
|
|
+ <el-table-column prop="method" label="方法" width="80" fixed>
|
|
|
|
|
+ <template slot-scope="{ row }">
|
|
|
|
|
+ <el-tag :type="row.method === 'GET' ? 'info' : 'primary'" size="mini">{{ row.method }}</el-tag>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="path" label="路径" min-width="260" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column prop="summary" label="说明" min-width="180" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column label="操作" width="80" fixed="right">
|
|
|
|
|
+ <template slot-scope="{ row }">
|
|
|
|
|
+ <el-button v-if="row.method === 'POST'" type="text" size="mini" @click="openTestDialog(row)">测试</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ </el-tab-pane>
|
|
|
|
|
|
|
|
- <!-- API 端点列表 -->
|
|
|
|
|
- <el-card class="api-card" shadow="never">
|
|
|
|
|
- <div slot="header" class="card-header">
|
|
|
|
|
- <span>📡 API 端点列表 ({{ apiEndpoints.length }})</span>
|
|
|
|
|
- <el-input v-model="apiFilter" placeholder="筛选端点..." size="mini" prefix-icon="el-icon-search" style="width:200px" clearable />
|
|
|
|
|
- </div>
|
|
|
|
|
- <el-table :data="filteredApiEndpoints" border stripe size="small" max-height="700">
|
|
|
|
|
- <el-table-column prop="method" label="方法" width="80">
|
|
|
|
|
- <template slot-scope="{ row }">
|
|
|
|
|
- <el-tag :type="row.method === 'GET' ? 'info' : 'primary'" size="mini">{{ row.method }}</el-tag>
|
|
|
|
|
- </template>
|
|
|
|
|
- </el-table-column>
|
|
|
|
|
- <el-table-column prop="path" label="路径" min-width="280" show-overflow-tooltip />
|
|
|
|
|
- <el-table-column prop="summary" label="说明" min-width="200" show-overflow-tooltip />
|
|
|
|
|
- <el-table-column label="操作" width="100" fixed="right">
|
|
|
|
|
- <template slot-scope="{ row }">
|
|
|
|
|
- <el-button v-if="row.method === 'POST'" type="text" size="small" @click="openTestDialog(row)">测试</el-button>
|
|
|
|
|
- </template>
|
|
|
|
|
- </el-table-column>
|
|
|
|
|
- </el-table>
|
|
|
|
|
- </el-card>
|
|
|
|
|
|
|
+ <!-- ===== 请求历史 ===== -->
|
|
|
|
|
+ <el-tab-pane label="📋 请求历史" name="logs">
|
|
|
|
|
+ <div class="tab-toolbar">
|
|
|
|
|
+ <el-select v-model="logFilterMethod" placeholder="方法筛选" size="small" clearable style="width:100px;margin-right:8px">
|
|
|
|
|
+ <el-option label="GET" value="GET" />
|
|
|
|
|
+ <el-option label="POST" value="POST" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ <el-input v-model="logFilterPath" placeholder="路径筛选..." size="small" prefix-icon="el-icon-search" style="width:200px;margin-right:8px" clearable />
|
|
|
|
|
+ <el-tag size="small" type="info">共 {{ totalLogs }} 条</el-tag>
|
|
|
|
|
+ <el-button size="small" @click="fetchLogs" :loading="logLoading">刷新</el-button>
|
|
|
|
|
+ <el-button size="small" type="danger" @click="clearLogsConfirm" :disabled="!logList.length">清空</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-table :data="filteredLogs" border stripe size="small" max-height="520" style="width:100%">
|
|
|
|
|
+ <el-table-column prop="ts" label="时间" width="70" fixed />
|
|
|
|
|
+ <el-table-column prop="method" label="方法" width="60">
|
|
|
|
|
+ <template slot-scope="{ row }">
|
|
|
|
|
+ <el-tag :type="row.method === 'GET' ? 'info' : 'primary'" size="mini">{{ row.method }}</el-tag>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="path" label="路径" min-width="200" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column prop="status" label="状态" width="60">
|
|
|
|
|
+ <template slot-scope="{ row }">
|
|
|
|
|
+ <el-tag :type="row.status < 400 ? 'success' : 'danger'" size="mini">{{ row.status }}</el-tag>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="duration_ms" label="耗时" width="70">
|
|
|
|
|
+ <template slot-scope="{ row }">
|
|
|
|
|
+ <span :style="{ color: row.duration_ms > 5000 ? '#f56c6c' : '' }">{{ row.duration_ms }}ms</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="client_ip" label="IP" width="90" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column label="操作" width="60" fixed="right">
|
|
|
|
|
+ <template slot-scope="{ row }">
|
|
|
|
|
+ <el-button type="text" size="mini" @click="toggleLogDetail(row)">详情</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ <!-- 详情展开行 -->
|
|
|
|
|
+ <el-table :data="logDetailRows" border stripe size="small" max-height="300" class="log-detail-table" v-if="expandedLog">
|
|
|
|
|
+ <el-table-column label="请求时间" width="100"><template slot-scope="{ row }">{{ row.ts }}</template></el-table-column>
|
|
|
|
|
+ <el-table-column label="方法"><template slot-scope="{ row }">{{ row.method }}</template></el-table-column>
|
|
|
|
|
+ <el-table-column label="路径"><template slot-scope="{ row }">{{ row.path }}</template></el-table-column>
|
|
|
|
|
+ <el-table-column label="状态"><template slot-scope="{ row }"><el-tag :type="row.status < 400 ? 'success' : 'danger'" size="mini">{{ row.status }}</el-tag></template></el-table-column>
|
|
|
|
|
+ <el-table-column label="耗时(ms)"><template slot-scope="{ row }">{{ row.duration_ms }}</template></el-table-column>
|
|
|
|
|
+ <el-table-column label="客户端IP"><template slot-scope="{ row }">{{ row.client_ip }}</template></el-table-column>
|
|
|
|
|
+ <el-table-column label="请求体" min-width="300">
|
|
|
|
|
+ <template slot-scope="{ row }">
|
|
|
|
|
+ <pre class="detail-pre">{{ row.body || '(空)' }}</pre>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="响应体" min-width="300">
|
|
|
|
|
+ <template slot-scope="{ row }">
|
|
|
|
|
+ <pre class="detail-pre">{{ (row.response || '').substring(0, 2000) }}</pre>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="错误" min-width="200" v-if="row.error">
|
|
|
|
|
+ <template slot-scope="{ row }">
|
|
|
|
|
+ <pre class="detail-pre error-pre">{{ row.error }}</pre>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ </el-tab-pane>
|
|
|
|
|
|
|
|
- <!-- 配置面板 -->
|
|
|
|
|
- <el-card class="config-card" shadow="never">
|
|
|
|
|
- <div slot="header" class="card-header">
|
|
|
|
|
- <span>⚙️ 服务配置</span>
|
|
|
|
|
- <el-button size="mini" type="primary" @click="fetchConfig">刷新配置</el-button>
|
|
|
|
|
- </div>
|
|
|
|
|
- <el-descriptions :column="2" border size="small">
|
|
|
|
|
- <el-descriptions-item label="LLM 模型" :span="1">
|
|
|
|
|
- <el-tag size="mini">{{ config.LLM_MODEL || '-' }}</el-tag>
|
|
|
|
|
- </el-descriptions-item>
|
|
|
|
|
- <el-descriptions-item label="LLM 基础 URL" :span="1">
|
|
|
|
|
- {{ config.LLM_BASE_URL || '-' }}
|
|
|
|
|
- </el-descriptions-item>
|
|
|
|
|
- <el-descriptions-item label="Embedding 模型" :span="1">
|
|
|
|
|
- <el-tag size="mini">{{ config.EMBEDDING_MODEL || '-' }}</el-tag>
|
|
|
|
|
- </el-descriptions-item>
|
|
|
|
|
- <el-descriptions-item label="Embedding URL" :span="1">
|
|
|
|
|
- {{ config.EMBEDDING_BASE_URL || '-' }}
|
|
|
|
|
- </el-descriptions-item>
|
|
|
|
|
- <el-descriptions-item label="Java 后端地址" :span="1">
|
|
|
|
|
- {{ config.JAVA_BASE_URL || '-' }}
|
|
|
|
|
- </el-descriptions-item>
|
|
|
|
|
- <el-descriptions-item label="Chroma DB 路径" :span="1">
|
|
|
|
|
- {{ config.CHROMA_DB_PATH || '-' }}
|
|
|
|
|
- </el-descriptions-item>
|
|
|
|
|
- <el-descriptions-item label="LangSmith 追踪" :span="1">
|
|
|
|
|
- <el-tag :type="config.LANGCHAIN_TRACING_V2 === 'true' ? 'success' : 'info'" size="mini">
|
|
|
|
|
- {{ config.LANGCHAIN_TRACING_V2 === 'true' ? '已启用' : '未启用' }}
|
|
|
|
|
- </el-tag>
|
|
|
|
|
- </el-descriptions-item>
|
|
|
|
|
- <el-descriptions-item label="日志级别" :span="1">
|
|
|
|
|
- <el-tag size="mini">{{ config.LOG_LEVEL || 'info' }}</el-tag>
|
|
|
|
|
- </el-descriptions-item>
|
|
|
|
|
- </el-descriptions>
|
|
|
|
|
- </el-card>
|
|
|
|
|
|
|
+ </el-tabs>
|
|
|
|
|
|
|
|
<!-- API 测试对话框 -->
|
|
<!-- API 测试对话框 -->
|
|
|
- <el-dialog :visible.sync="testDialogVisible" :title="'测试: ' + testingEndpoint.path" width="700px" :close-on-click-modal="false">
|
|
|
|
|
|
|
+ <el-dialog :visible.sync="testDialogVisible" :title="'测试: ' + testingEndpoint.path" width="720px" :close-on-click-modal="false">
|
|
|
<el-form size="small">
|
|
<el-form size="small">
|
|
|
<el-form-item label="请求参数">
|
|
<el-form-item label="请求参数">
|
|
|
- <el-input
|
|
|
|
|
- v-model="testParams"
|
|
|
|
|
- type="textarea"
|
|
|
|
|
- :rows="8"
|
|
|
|
|
- placeholder='请输入 JSON 参数,例如: {"user_id": 1, "query": "你好"}'
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ <el-input v-model="testParams" type="textarea" :rows="6"
|
|
|
|
|
+ placeholder='请输入 JSON 参数,例如: {"user_id": 1, "query": "你好"}' />
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
<el-form-item>
|
|
<el-form-item>
|
|
|
<el-button type="primary" @click="runTest" :loading="testing">发送请求</el-button>
|
|
<el-button type="primary" @click="runTest" :loading="testing">发送请求</el-button>
|
|
@@ -142,25 +184,6 @@
|
|
|
<pre class="response-pre" v-if="testResponse">{{ testResponse }}</pre>
|
|
<pre class="response-pre" v-if="testResponse">{{ testResponse }}</pre>
|
|
|
<el-alert v-else type="info" :closable="false" description="点击"发送请求"查看响应结果" />
|
|
<el-alert v-else type="info" :closable="false" description="点击"发送请求"查看响应结果" />
|
|
|
</el-dialog>
|
|
</el-dialog>
|
|
|
-
|
|
|
|
|
- <!-- 日志面板 -->
|
|
|
|
|
- <el-card class="log-card" shadow="never">
|
|
|
|
|
- <div slot="header" class="card-header">
|
|
|
|
|
- <span>📋 最近日志</span>
|
|
|
|
|
- <div>
|
|
|
|
|
- <el-button size="mini" @click="fetchLogs" :loading="logLoading">刷新</el-button>
|
|
|
|
|
- <el-button size="mini" type="danger" @click="clearLogs">清空</el-button>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="log-container" ref="logContainer">
|
|
|
|
|
- <div v-for="(log, i) in logList" :key="i" class="log-line" :class="'log-' + getLogLevel(log)">
|
|
|
|
|
- <span class="log-time">{{ log.time }}</span>
|
|
|
|
|
- <span class="log-level">{{ log.level }}</span>
|
|
|
|
|
- <span class="log-msg">{{ log.msg }}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div v-if="logList.length === 0" class="log-empty">暂无日志</div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </el-card>
|
|
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
@@ -170,44 +193,32 @@ const BASE = 'https://ai.etotem.com.cn'
|
|
|
function fetchJSON(url, options = {}) {
|
|
function fetchJSON(url, options = {}) {
|
|
|
return fetch(url, {
|
|
return fetch(url, {
|
|
|
...options,
|
|
...options,
|
|
|
- headers: {
|
|
|
|
|
- 'Content-Type': 'application/json',
|
|
|
|
|
- ...options.headers
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json', ...options.headers }
|
|
|
}).then(function(res) {
|
|
}).then(function(res) {
|
|
|
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
|
return res.json()
|
|
return res.json()
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function fetchText(url, options = {}) {
|
|
|
|
|
- return fetch(url, {
|
|
|
|
|
- ...options,
|
|
|
|
|
- headers: {
|
|
|
|
|
- 'Content-Type': 'application/json',
|
|
|
|
|
- ...options.headers
|
|
|
|
|
- }
|
|
|
|
|
- }).then(function(res) {
|
|
|
|
|
- if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
|
|
|
- return res.text()
|
|
|
|
|
- })
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
export default {
|
|
export default {
|
|
|
name: 'LangGraphAdmin',
|
|
name: 'LangGraphAdmin',
|
|
|
data() {
|
|
data() {
|
|
|
return {
|
|
return {
|
|
|
|
|
+ activeTab: 'overview',
|
|
|
langgraphUrl: 'https://ai.etotem.com.cn',
|
|
langgraphUrl: 'https://ai.etotem.com.cn',
|
|
|
serviceVersion: '0.3.0',
|
|
serviceVersion: '0.3.0',
|
|
|
|
|
+ // 状态
|
|
|
statusText: '检测中...',
|
|
statusText: '检测中...',
|
|
|
statusClass: 'status-checking',
|
|
statusClass: 'status-checking',
|
|
|
statusIcon: '🔄',
|
|
statusIcon: '🔄',
|
|
|
statusTime: '',
|
|
statusTime: '',
|
|
|
apiCount: 0,
|
|
apiCount: 0,
|
|
|
|
|
+ // API 列表
|
|
|
apiEndpoints: [],
|
|
apiEndpoints: [],
|
|
|
apiFilter: '',
|
|
apiFilter: '',
|
|
|
|
|
+ // 组件健康
|
|
|
componentList: [],
|
|
componentList: [],
|
|
|
- refreshing: false,
|
|
|
|
|
|
|
+ // 配置
|
|
|
config: {
|
|
config: {
|
|
|
LLM_MODEL: 'deepseek',
|
|
LLM_MODEL: 'deepseek',
|
|
|
LLM_BASE_URL: 'http://axonhub.iwintrue.com/v1',
|
|
LLM_BASE_URL: 'http://axonhub.iwintrue.com/v1',
|
|
@@ -218,13 +229,20 @@ export default {
|
|
|
LANGCHAIN_TRACING_V2: 'true',
|
|
LANGCHAIN_TRACING_V2: 'true',
|
|
|
LOG_LEVEL: 'info'
|
|
LOG_LEVEL: 'info'
|
|
|
},
|
|
},
|
|
|
|
|
+ // 请求日志
|
|
|
|
|
+ logList: [],
|
|
|
|
|
+ totalLogs: 0,
|
|
|
|
|
+ logFilterMethod: '',
|
|
|
|
|
+ logFilterPath: '',
|
|
|
|
|
+ logLoading: false,
|
|
|
|
|
+ expandedLog: null,
|
|
|
|
|
+ // 测试
|
|
|
testDialogVisible: false,
|
|
testDialogVisible: false,
|
|
|
testingEndpoint: {},
|
|
testingEndpoint: {},
|
|
|
testParams: '{}',
|
|
testParams: '{}',
|
|
|
testing: false,
|
|
testing: false,
|
|
|
testResponse: '',
|
|
testResponse: '',
|
|
|
- logList: [],
|
|
|
|
|
- logLoading: false,
|
|
|
|
|
|
|
+ refreshing: false,
|
|
|
_interval: null
|
|
_interval: null
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
@@ -236,11 +254,21 @@ export default {
|
|
|
return self.apiEndpoints.filter(function(e) {
|
|
return self.apiEndpoints.filter(function(e) {
|
|
|
return e.path.toLowerCase().indexOf(kw) >= 0 || (e.summary && e.summary.toLowerCase().indexOf(kw) >= 0)
|
|
return e.path.toLowerCase().indexOf(kw) >= 0 || (e.summary && e.summary.toLowerCase().indexOf(kw) >= 0)
|
|
|
})
|
|
})
|
|
|
|
|
+ },
|
|
|
|
|
+ filteredLogs: function() {
|
|
|
|
|
+ var list = this.logList
|
|
|
|
|
+ if (this.logFilterMethod) {
|
|
|
|
|
+ list = list.filter(function(l) { return l.method === this.logFilterMethod }, this)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.logFilterPath) {
|
|
|
|
|
+ var kw = this.logFilterPath.toLowerCase()
|
|
|
|
|
+ list = list.filter(function(l) { return l.path.toLowerCase().indexOf(kw) >= 0 })
|
|
|
|
|
+ }
|
|
|
|
|
+ return list
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
created: function() {
|
|
created: function() {
|
|
|
this.refreshAll()
|
|
this.refreshAll()
|
|
|
- this.fetchLogs()
|
|
|
|
|
this._interval = setInterval(this.refreshAll, 60000)
|
|
this._interval = setInterval(this.refreshAll, 60000)
|
|
|
},
|
|
},
|
|
|
beforeDestroy: function() {
|
|
beforeDestroy: function() {
|
|
@@ -284,7 +312,6 @@ export default {
|
|
|
var res = await fetchJSON(BASE + '/openapi.json')
|
|
var res = await fetchJSON(BASE + '/openapi.json')
|
|
|
var paths = res.paths || {}
|
|
var paths = res.paths || {}
|
|
|
var endpoints = []
|
|
var endpoints = []
|
|
|
- var self = this
|
|
|
|
|
Object.keys(paths).forEach(function(path) {
|
|
Object.keys(paths).forEach(function(path) {
|
|
|
var methods = paths[path]
|
|
var methods = paths[path]
|
|
|
Object.keys(methods).forEach(function(method) {
|
|
Object.keys(methods).forEach(function(method) {
|
|
@@ -323,9 +350,6 @@ export default {
|
|
|
comps.push({ name: 'Java 后端', status: c.java_backend.status || 'unknown', detail: c.java_backend.code ? 'HTTP ' + c.java_backend.code : (c.java_backend.message || '') })
|
|
comps.push({ name: 'Java 后端', status: c.java_backend.status || 'unknown', detail: c.java_backend.code ? 'HTTP ' + c.java_backend.code : (c.java_backend.message || '') })
|
|
|
}
|
|
}
|
|
|
this.componentList = comps
|
|
this.componentList = comps
|
|
|
-
|
|
|
|
|
- // 状态判定:仅当 LLM 异常时才显示异常
|
|
|
|
|
- // Java 后端通过 Docker 容器访问时可能网络不通(内外网 IP 差异),不作为整体状态依据
|
|
|
|
|
var llmOk = !c.llm || c.llm.status === 'ok'
|
|
var llmOk = !c.llm || c.llm.status === 'ok'
|
|
|
var chromaOk = !c.chromadb || c.chromadb.status === 'ok'
|
|
var chromaOk = !c.chromadb || c.chromadb.status === 'ok'
|
|
|
if (llmOk && chromaOk) {
|
|
if (llmOk && chromaOk) {
|
|
@@ -346,18 +370,39 @@ export default {
|
|
|
this.componentList = [{ name: '健康检查', status: 'error', detail: e.message }]
|
|
this.componentList = [{ name: '健康检查', status: 'error', detail: e.message }]
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
- fetchConfig: function() {
|
|
|
|
|
- this.config = {
|
|
|
|
|
- LLM_MODEL: 'deepseek',
|
|
|
|
|
- LLM_BASE_URL: 'http://axonhub.iwintrue.com/v1',
|
|
|
|
|
- EMBEDDING_MODEL: 'BAAI/bge-large-zh-v1.5',
|
|
|
|
|
- EMBEDDING_BASE_URL: 'https://api.siliconflow.cn/v1',
|
|
|
|
|
- JAVA_BASE_URL: 'http://111.227.6.214:9082',
|
|
|
|
|
- CHROMA_DB_PATH: '/data/chroma_db',
|
|
|
|
|
- LANGCHAIN_TRACING_V2: 'true',
|
|
|
|
|
- LOG_LEVEL: 'info'
|
|
|
|
|
|
|
+ // ===== 请求日志 =====
|
|
|
|
|
+ async fetchLogs() {
|
|
|
|
|
+ this.logLoading = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ var res = await fetchJSON(BASE + '/api/v1/logs?limit=200')
|
|
|
|
|
+ this.logList = res.logs || []
|
|
|
|
|
+ this.totalLogs = res.total || 0
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ this.logList = []
|
|
|
|
|
+ this.totalLogs = 0
|
|
|
|
|
+ this.$message.error('获取日志失败: ' + e.message)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ this.logLoading = false
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
|
|
+ async clearLogsConfirm() {
|
|
|
|
|
+ var self = this
|
|
|
|
|
+ this.$confirm('确认清空所有请求日志?', '提示', { type: 'warning' }).then(async function() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ var res = await fetch(BASE + '/api/v1/logs/clear', { method: 'POST' })
|
|
|
|
|
+ var data = await res.json()
|
|
|
|
|
+ self.logList = []
|
|
|
|
|
+ self.totalLogs = 0
|
|
|
|
|
+ self.$message.success('已清空 ' + (data.cleared || 0) + ' 条日志')
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ self.$message.error('清空失败: ' + e.message)
|
|
|
|
|
+ }
|
|
|
|
|
+ }).catch(function() {})
|
|
|
|
|
+ },
|
|
|
|
|
+ toggleLogDetail: function(row) {
|
|
|
|
|
+ this.expandedLog = this.expandedLog === row ? null : row
|
|
|
|
|
+ },
|
|
|
|
|
+ // ===== API 测试 =====
|
|
|
openTestDialog: function(endpoint) {
|
|
openTestDialog: function(endpoint) {
|
|
|
this.testingEndpoint = endpoint
|
|
this.testingEndpoint = endpoint
|
|
|
var defaults = {
|
|
var defaults = {
|
|
@@ -396,34 +441,6 @@ export default {
|
|
|
} finally {
|
|
} finally {
|
|
|
self.testing = false
|
|
self.testing = false
|
|
|
}
|
|
}
|
|
|
- },
|
|
|
|
|
- async fetchLogs() {
|
|
|
|
|
- this.logLoading = true
|
|
|
|
|
- try {
|
|
|
|
|
- var res = await fetch(BASE + '/openapi.json')
|
|
|
|
|
- var data = await res.json()
|
|
|
|
|
- this.logList = [{
|
|
|
|
|
- time: new Date().toLocaleTimeString(),
|
|
|
|
|
- level: 'INFO',
|
|
|
|
|
- msg: '服务连接正常,API 端点数: ' + Object.keys(data.paths || {}).length
|
|
|
|
|
- }]
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- this.logList = [{
|
|
|
|
|
- time: new Date().toLocaleTimeString(),
|
|
|
|
|
- level: 'ERROR',
|
|
|
|
|
- msg: '无法连接到 LangGraph 服务: ' + e.message
|
|
|
|
|
- }]
|
|
|
|
|
- } finally {
|
|
|
|
|
- this.logLoading = false
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- clearLogs: function() {
|
|
|
|
|
- this.logList = []
|
|
|
|
|
- },
|
|
|
|
|
- getLogLevel: function(log) {
|
|
|
|
|
- if (log.level === 'ERROR' || log.level === 'error') return 'error'
|
|
|
|
|
- if (log.level === 'WARNING' || log.level === 'WARN') return 'warning'
|
|
|
|
|
- return 'info'
|
|
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -432,51 +449,77 @@ export default {
|
|
|
<style scoped>
|
|
<style scoped>
|
|
|
.page-container {
|
|
.page-container {
|
|
|
padding: 16px;
|
|
padding: 16px;
|
|
|
|
|
+ background: #f0f2f5;
|
|
|
|
|
+ min-height: 100vh;
|
|
|
}
|
|
}
|
|
|
-.status-row {
|
|
|
|
|
- margin-bottom: 16px;
|
|
|
|
|
|
|
+
|
|
|
|
|
+/* 顶部状态栏 */
|
|
|
|
|
+.top-bar {
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
}
|
|
}
|
|
|
-.status-card {
|
|
|
|
|
|
|
+.mini-card {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
- padding: 12px 16px;
|
|
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ padding: 12px 8px;
|
|
|
|
|
+ text-align: center;
|
|
|
cursor: default;
|
|
cursor: default;
|
|
|
transition: all 0.3s;
|
|
transition: all 0.3s;
|
|
|
|
|
+ border-left: 4px solid transparent;
|
|
|
}
|
|
}
|
|
|
-.status-card:hover {
|
|
|
|
|
|
|
+.mini-card:hover {
|
|
|
transform: translateY(-2px);
|
|
transform: translateY(-2px);
|
|
|
}
|
|
}
|
|
|
-.status-healthy { border-left: 4px solid #67c23a; }
|
|
|
|
|
-.status-unhealthy { border-left: 4px solid #f56c6c; }
|
|
|
|
|
-.status-degraded { border-left: 4px solid #e6a23c; }
|
|
|
|
|
-.status-checking { border-left: 4px solid #409eff; }
|
|
|
|
|
-.status-icon {
|
|
|
|
|
- font-size: 32px;
|
|
|
|
|
- margin-right: 16px;
|
|
|
|
|
-}
|
|
|
|
|
-.status-info {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
-}
|
|
|
|
|
-.status-label {
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
- color: #909399;
|
|
|
|
|
|
|
+.mini-card.status-healthy { border-left-color: #67c23a; }
|
|
|
|
|
+.mini-card.status-unhealthy { border-left-color: #f56c6c; }
|
|
|
|
|
+.mini-card.status-degraded { border-left-color: #e6a23c; }
|
|
|
|
|
+.mini-card.status-checking { border-left-color: #409eff; }
|
|
|
|
|
+.mini-icon {
|
|
|
|
|
+ font-size: 24px;
|
|
|
margin-bottom: 4px;
|
|
margin-bottom: 4px;
|
|
|
}
|
|
}
|
|
|
-.status-value {
|
|
|
|
|
- font-size: 18px;
|
|
|
|
|
- font-weight: bold;
|
|
|
|
|
|
|
+.mini-text {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
color: #303133;
|
|
color: #303133;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ max-width: 100%;
|
|
|
}
|
|
}
|
|
|
-.status-sub {
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
|
|
+.mini-sub {
|
|
|
|
|
+ font-size: 11px;
|
|
|
color: #909399;
|
|
color: #909399;
|
|
|
margin-top: 2px;
|
|
margin-top: 2px;
|
|
|
}
|
|
}
|
|
|
-.card-header {
|
|
|
|
|
|
|
+.top-actions {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ justify-content: flex-start;
|
|
|
|
|
+ padding-left: 16px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 标签页 */
|
|
|
|
|
+.main-tabs {
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+.tab-toolbar {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
- justify-content: space-between;
|
|
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+/* 面板 */
|
|
|
|
|
+.panel {
|
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 响应体 */
|
|
|
.response-pre {
|
|
.response-pre {
|
|
|
background: #f5f7fa;
|
|
background: #f5f7fa;
|
|
|
padding: 12px;
|
|
padding: 12px;
|
|
@@ -487,26 +530,26 @@ export default {
|
|
|
white-space: pre-wrap;
|
|
white-space: pre-wrap;
|
|
|
word-break: break-all;
|
|
word-break: break-all;
|
|
|
}
|
|
}
|
|
|
-.log-container {
|
|
|
|
|
- max-height: 600px;
|
|
|
|
|
- overflow-y: auto;
|
|
|
|
|
|
|
+
|
|
|
|
|
+/* 日志详情 */
|
|
|
|
|
+.log-detail-table {
|
|
|
|
|
+ margin-top: 8px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+.detail-pre {
|
|
|
background: #1e1e1e;
|
|
background: #1e1e1e;
|
|
|
|
|
+ color: #d4d4d4;
|
|
|
|
|
+ padding: 8px;
|
|
|
border-radius: 4px;
|
|
border-radius: 4px;
|
|
|
- padding: 12px;
|
|
|
|
|
- font-family: 'Courier New', monospace;
|
|
|
|
|
- font-size: 12px;
|
|
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ max-height: 200px;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ white-space: pre-wrap;
|
|
|
|
|
+ word-break: break-all;
|
|
|
|
|
+ margin: 0;
|
|
|
}
|
|
}
|
|
|
-.log-line {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- gap: 12px;
|
|
|
|
|
- padding: 2px 0;
|
|
|
|
|
- line-height: 1.6;
|
|
|
|
|
|
|
+.error-pre {
|
|
|
|
|
+ color: #f56c6c;
|
|
|
}
|
|
}
|
|
|
-.log-time { color: #888; min-width: 80px; }
|
|
|
|
|
-.log-level { min-width: 60px; font-weight: bold; }
|
|
|
|
|
-.log-level.ERROR { color: #f56c6c; }
|
|
|
|
|
-.log-level.WARNING { color: #e6a23c; }
|
|
|
|
|
-.log-level.INFO { color: #67c23a; }
|
|
|
|
|
-.log-msg { color: #d4d4d4; flex: 1; }
|
|
|
|
|
-.log-empty { color: #666; text-align: center; padding: 20px; }
|
|
|
|
|
</style>
|
|
</style>
|