|
|
@@ -0,0 +1,607 @@
|
|
|
+<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>
|
|
|
+ </el-card>
|
|
|
+ </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-card>
|
|
|
+ </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-card>
|
|
|
+ </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>
|
|
|
+ <el-button size="mini" @click="testAllHealth">全部检测</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </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>
|
|
|
+
|
|
|
+ <!-- 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="400">
|
|
|
+ <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-card class="test-card" shadow="never">
|
|
|
+ <div slot="header" class="card-header">
|
|
|
+ <span>🧪 快速功能测试</span>
|
|
|
+ <el-button size="mini" type="success" @click="runQuickTests" :loading="quickTesting">一键测试</el-button>
|
|
|
+ </div>
|
|
|
+ <el-row :gutter="12">
|
|
|
+ <el-col :span="6" v-for="t in quickTests" :key="t.key">
|
|
|
+ <el-card shadow="hover" class="test-item" :class="'test-' + t.result">
|
|
|
+ <div class="test-item-header">
|
|
|
+ <span class="test-name">{{ t.name }}</span>
|
|
|
+ <el-tag :type="t.result === 'success' ? 'success' : t.result === 'error' ? 'danger' : 'info'" size="mini">
|
|
|
+ {{ t.result === 'success' ? '✓' : t.result === 'error' ? '✗' : '—' }}
|
|
|
+ </el-tag>
|
|
|
+ </div>
|
|
|
+ <div class="test-item-desc">{{ t.desc }}</div>
|
|
|
+ <div v-if="t.result === 'error'" class="test-item-error">{{ t.errorMsg }}</div>
|
|
|
+ <div v-if="t.result === 'success'" class="test-item-time">{{ t.time }}ms</div>
|
|
|
+ </el-card>
|
|
|
+ </el-col>
|
|
|
+ </el-row>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- 配置面板 -->
|
|
|
+ <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>
|
|
|
+
|
|
|
+ <!-- API 测试对话框 -->
|
|
|
+ <el-dialog :visible.sync="testDialogVisible" :title="'测试: ' + testingEndpoint.path" width="700px" :close-on-click-modal="false">
|
|
|
+ <el-form size="small">
|
|
|
+ <el-form-item label="请求参数">
|
|
|
+ <el-input
|
|
|
+ v-model="testParams"
|
|
|
+ type="textarea"
|
|
|
+ :rows="8"
|
|
|
+ placeholder='请输入 JSON 参数,例如: {"user_id": 1, "query": "你好"}'
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" @click="runTest" :loading="testing">发送请求</el-button>
|
|
|
+ <el-button @click="testDialogVisible = false">关闭</el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <el-divider>响应结果</el-divider>
|
|
|
+ <pre class="response-pre" v-if="testResponse">{{ testResponse }}</pre>
|
|
|
+ <el-alert v-else type="info" :closable="false" description="点击"发送请求"查看响应结果" />
|
|
|
+ </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">
|
|
|
+ <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>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import {
|
|
|
+ getLangGraphHealth,
|
|
|
+ getLangGraphSpec,
|
|
|
+ getLangGraphDetailedHealth,
|
|
|
+ testFoodRecognize,
|
|
|
+ testMenuGenerate,
|
|
|
+ testChat,
|
|
|
+ testRecommend,
|
|
|
+ testQuestionnaireGenerate
|
|
|
+} from '@/api/langgraph'
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'LangGraphAdmin',
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ langgraphUrl: 'https://ai.etotem.com.cn',
|
|
|
+ serviceVersion: '0.3.0',
|
|
|
+ statusText: '检测中...',
|
|
|
+ statusClass: 'status-checking',
|
|
|
+ statusIcon: '🔄',
|
|
|
+ statusTime: '',
|
|
|
+ apiCount: 0,
|
|
|
+ apiEndpoints: [],
|
|
|
+ apiFilter: '',
|
|
|
+ componentList: [],
|
|
|
+ refreshing: false,
|
|
|
+ quickTesting: false,
|
|
|
+ config: {},
|
|
|
+ // 测试对话框
|
|
|
+ testDialogVisible: false,
|
|
|
+ testingEndpoint: {},
|
|
|
+ testParams: '{}',
|
|
|
+ testing: false,
|
|
|
+ testResponse: '',
|
|
|
+ // 日志
|
|
|
+ logList: [],
|
|
|
+ logLoading: false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: (function() {
|
|
|
+ return {
|
|
|
+ filteredApiEndpoints: function() {
|
|
|
+ var self = this
|
|
|
+ if (!self.apiFilter) return self.apiEndpoints
|
|
|
+ var kw = self.apiFilter.toLowerCase()
|
|
|
+ return self.apiEndpoints.filter(function(e) {
|
|
|
+ return e.path.toLowerCase().indexOf(kw) >= 0 || (e.summary && e.summary.toLowerCase().indexOf(kw) >= 0)
|
|
|
+ })
|
|
|
+ },
|
|
|
+ quickTests: function() {
|
|
|
+ return [
|
|
|
+ { key: 'food', name: '食材识别', desc: 'POST /api/v1/food/recognize', result: 'pending', time: 0, errorMsg: '' },
|
|
|
+ { key: 'menu', name: '菜单生成', desc: 'POST /api/v1/menu/generate', result: 'pending', time: 0, errorMsg: '' },
|
|
|
+ { key: 'chat', name: 'AI 对话', desc: 'POST /api/v1/chat', result: 'pending', time: 0, errorMsg: '' },
|
|
|
+ { key: 'questionnaire', name: '问卷生成', desc: 'POST /api/v1/questionnaire/generate', result: 'pending', time: 0, errorMsg: '' }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })(),
|
|
|
+ created: function() {
|
|
|
+ this.refreshAll()
|
|
|
+ this.fetchConfig()
|
|
|
+ this.fetchLogs()
|
|
|
+ // 每 30 秒自动刷新状态
|
|
|
+ this._interval = setInterval(this.refreshAll, 30000)
|
|
|
+ },
|
|
|
+ beforeDestroy: function() {
|
|
|
+ if (this._interval) clearInterval(this._interval)
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ async refreshAll() {
|
|
|
+ this.refreshing = true
|
|
|
+ try {
|
|
|
+ await Promise.all([this.fetchHealth(), this.fetchSpec()])
|
|
|
+ } finally {
|
|
|
+ this.refreshing = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async fetchHealth() {
|
|
|
+ try {
|
|
|
+ var res = await getLangGraphHealth()
|
|
|
+ if (res.status === 'healthy') {
|
|
|
+ this.statusText = '运行中'
|
|
|
+ this.statusClass = 'status-healthy'
|
|
|
+ this.statusIcon = '✅'
|
|
|
+ } else {
|
|
|
+ this.statusText = '未响应'
|
|
|
+ this.statusClass = 'status-unhealthy'
|
|
|
+ this.statusIcon = '❌'
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ this.statusText = '异常'
|
|
|
+ this.statusClass = 'status-unhealthy'
|
|
|
+ this.statusIcon = '❌'
|
|
|
+ }
|
|
|
+ this.statusTime = new Date().toLocaleTimeString()
|
|
|
+ },
|
|
|
+ async fetchSpec() {
|
|
|
+ try {
|
|
|
+ var res = await getLangGraphSpec()
|
|
|
+ var paths = res.data.paths || {}
|
|
|
+ var endpoints = []
|
|
|
+ var self = this
|
|
|
+ Object.keys(paths).forEach(function(path) {
|
|
|
+ var methods = paths[path]
|
|
|
+ Object.keys(methods).forEach(function(method) {
|
|
|
+ var info = methods[method]
|
|
|
+ endpoints.push({
|
|
|
+ method: method.toUpperCase(),
|
|
|
+ path: path,
|
|
|
+ summary: info.summary || info.description || ''
|
|
|
+ })
|
|
|
+ })
|
|
|
+ })
|
|
|
+ endpoints.sort(function(a, b) {
|
|
|
+ if (a.method !== b.method) return a.method.localeCompare(b.method)
|
|
|
+ return a.path.localeCompare(b.path)
|
|
|
+ })
|
|
|
+ this.apiEndpoints = endpoints
|
|
|
+ this.apiCount = endpoints.length
|
|
|
+ this.fetchDetailedHealth()
|
|
|
+ } catch (e) {
|
|
|
+ console.error('获取 API 规范失败', e)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async fetchDetailedHealth() {
|
|
|
+ try {
|
|
|
+ var res = await getLangGraphDetailedHealth()
|
|
|
+ var data = res.data || {}
|
|
|
+ var comps = []
|
|
|
+ var c = data.components || {}
|
|
|
+ if (c.chromadb) {
|
|
|
+ comps.push({ name: 'ChromaDB', status: c.chromadb.status || 'unknown', detail: c.chromadb.path || '' })
|
|
|
+ }
|
|
|
+ if (c.llm) {
|
|
|
+ comps.push({ name: 'LLM', status: c.llm.status || 'unknown', detail: '' })
|
|
|
+ }
|
|
|
+ if (c.java_backend) {
|
|
|
+ comps.push({ name: 'Java 后端', status: c.java_backend.status || 'unknown', detail: c.java_backend.code ? 'HTTP ' + c.java_backend.code : '' })
|
|
|
+ }
|
|
|
+ this.componentList = comps
|
|
|
+ // 更新总状态
|
|
|
+ var allOk = comps.every(function(x) { return x.status === 'ok' })
|
|
|
+ var hasError = comps.some(function(x) { return x.status === 'error' })
|
|
|
+ if (allOk) {
|
|
|
+ this.statusText = '运行中'
|
|
|
+ this.statusClass = 'status-healthy'
|
|
|
+ this.statusIcon = '✅'
|
|
|
+ } else if (hasError) {
|
|
|
+ this.statusText = '部分异常'
|
|
|
+ this.statusClass = 'status-degraded'
|
|
|
+ this.statusIcon = '⚠️'
|
|
|
+ } else {
|
|
|
+ this.statusText = '检测中'
|
|
|
+ this.statusClass = 'status-checking'
|
|
|
+ this.statusIcon = '🔄'
|
|
|
+ }
|
|
|
+ this.statusTime = new Date().toLocaleTimeString()
|
|
|
+ } catch (e) {
|
|
|
+ this.componentList = [{ name: '健康检查', status: 'error', detail: e.message }]
|
|
|
+ }
|
|
|
+ },
|
|
|
+ async fetchConfig() {
|
|
|
+ try {
|
|
|
+ var res = await getLangGraphSpec()
|
|
|
+ // 从 openapi 无法直接获取配置,尝试从 /api/v1/health 获取
|
|
|
+ var h = await getLangGraphDetailedHealth()
|
|
|
+ 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'
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.error('获取配置失败', e)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ openTestDialog: function(endpoint) {
|
|
|
+ this.testingEndpoint = endpoint
|
|
|
+ var defaults = {
|
|
|
+ '/api/v1/food/recognize': JSON.stringify({ image_url: 'https://example.com/food.jpg' }, null, 2),
|
|
|
+ '/api/v1/menu/generate': JSON.stringify({ selected_foods: '[{"name":"鸡肉"}]', participants: '[]', date: '2026-08-08' }, null, 2),
|
|
|
+ '/api/v1/chat': JSON.stringify({ query: '你好', user_id: 1 }, null, 2),
|
|
|
+ '/api/v1/recommend': JSON.stringify({ user_id: 1, query: '推荐早餐', limit: 5 }, null, 2),
|
|
|
+ '/api/v1/questionnaire/generate': JSON.stringify({ member_name: '小明', relationship_type: 'parent' }, null, 2),
|
|
|
+ '/api/v1/tongue/diagnose': '请上传舌苔图片 (MIME 类型表单)'
|
|
|
+ }
|
|
|
+ this.testParams = defaults[endpoint.path] || '{}'
|
|
|
+ this.testResponse = ''
|
|
|
+ this.testDialogVisible = true
|
|
|
+ },
|
|
|
+ async runTest() {
|
|
|
+ var self = this
|
|
|
+ self.testing = true
|
|
|
+ self.testResponse = ''
|
|
|
+ try {
|
|
|
+ var payload = {}
|
|
|
+ if (self.testParams && self.testParams !== '请上传舌苔图片 (MIME 类型表单)') {
|
|
|
+ payload = JSON.parse(self.testParams)
|
|
|
+ }
|
|
|
+ var res = await this._callApi(self.testingEndpoint.path, payload)
|
|
|
+ self.testResponse = JSON.stringify(res.data, null, 2)
|
|
|
+ } catch (e) {
|
|
|
+ self.testResponse = '错误: ' + (e.response ? JSON.stringify(e.response.data) : e.message)
|
|
|
+ } finally {
|
|
|
+ self.testing = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ _callApi: function(path, data) {
|
|
|
+ var base = 'https://ai.etotem.com.cn'
|
|
|
+ var axios = require('axios')
|
|
|
+ return axios.post(base + path, data, { timeout: 60000 })
|
|
|
+ },
|
|
|
+ async runQuickTests() {
|
|
|
+ var self = this
|
|
|
+ self.quickTesting = true
|
|
|
+ var tests = self.quickTests
|
|
|
+ var results = { food: tests[0], menu: tests[1], chat: tests[2], questionnaire: tests[3] }
|
|
|
+
|
|
|
+ // 食材识别
|
|
|
+ var t0 = Date.now()
|
|
|
+ try {
|
|
|
+ var r1 = await testFoodRecognize({ image_url: 'https://example.com/food.jpg' })
|
|
|
+ results.food.result = 'success'
|
|
|
+ results.food.time = Date.now() - t0
|
|
|
+ } catch (e) {
|
|
|
+ results.food.result = 'error'
|
|
|
+ results.food.errorMsg = (e.response && e.response.data && e.response.data.detail) || e.message
|
|
|
+ }
|
|
|
+
|
|
|
+ // 菜单生成
|
|
|
+ t0 = Date.now()
|
|
|
+ try {
|
|
|
+ var r2 = await testMenuGenerate({ selected_foods: '[{"name":"鸡肉"}]', participants: '[]', date: '2026-08-08' })
|
|
|
+ results.menu.result = 'success'
|
|
|
+ results.menu.time = Date.now() - t0
|
|
|
+ } catch (e) {
|
|
|
+ results.menu.result = 'error'
|
|
|
+ results.menu.errorMsg = (e.response && e.response.data && e.response.data.detail) || e.message
|
|
|
+ }
|
|
|
+
|
|
|
+ // 对话
|
|
|
+ t0 = Date.now()
|
|
|
+ try {
|
|
|
+ var r3 = await testChat({ query: '你好', user_id: 1 })
|
|
|
+ results.chat.result = 'success'
|
|
|
+ results.chat.time = Date.now() - t0
|
|
|
+ } catch (e) {
|
|
|
+ results.chat.result = 'error'
|
|
|
+ results.chat.errorMsg = (e.response && e.response.data && e.response.data.detail) || e.message
|
|
|
+ }
|
|
|
+
|
|
|
+ // 问卷
|
|
|
+ t0 = Date.now()
|
|
|
+ try {
|
|
|
+ var r4 = await testQuestionnaireGenerate({ member_name: '小明', relationship_type: 'parent' })
|
|
|
+ results.questionnaire.result = 'success'
|
|
|
+ results.questionnaire.time = Date.now() - t0
|
|
|
+ } catch (e) {
|
|
|
+ results.questionnaire.result = 'error'
|
|
|
+ results.questionnaire.errorMsg = (e.response && e.response.data && e.response.data.detail) || e.message
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新显示
|
|
|
+ tests[0] = results.food
|
|
|
+ tests[1] = results.menu
|
|
|
+ tests[2] = results.chat
|
|
|
+ tests[3] = results.questionnaire
|
|
|
+ self.quickTesting = false
|
|
|
+ },
|
|
|
+ async fetchLogs() {
|
|
|
+ this.logLoading = true
|
|
|
+ try {
|
|
|
+ // 从后端获取日志
|
|
|
+ var res = await this.$axios.get('https://ai.etotem.com.cn/openapi.json', { timeout: 5000 })
|
|
|
+ this.logList = [{ time: new Date().toLocaleTimeString(), level: 'INFO', msg: '服务连接正常,API 端点数: ' + (res.data.paths ? Object.keys(res.data.paths).length : 0) }]
|
|
|
+ } catch (e) {
|
|
|
+ this.logList = [{ time: new Date().toLocaleTimeString(), level: 'ERROR', msg: '无法连接到 LangGraph 服务: ' + e.message }]
|
|
|
+ } finally {
|
|
|
+ this.logLoading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ clearLogs: function() {
|
|
|
+ this.logList = []
|
|
|
+ },
|
|
|
+ testAllHealth: function() {
|
|
|
+ this.fetchHealth()
|
|
|
+ this.fetchDetailedHealth()
|
|
|
+ },
|
|
|
+ getLogLevel: function(log) {
|
|
|
+ if (log.level === 'ERROR' || log.level === 'error') return 'error'
|
|
|
+ if (log.level === 'WARNING' || log.level === 'WARN') return 'warning'
|
|
|
+ return 'info'
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.page-container {
|
|
|
+ padding: 16px;
|
|
|
+}
|
|
|
+.status-row {
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+.status-card {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 12px 16px;
|
|
|
+ cursor: default;
|
|
|
+ transition: all 0.3s;
|
|
|
+}
|
|
|
+.status-card:hover {
|
|
|
+ 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;
|
|
|
+ margin-bottom: 4px;
|
|
|
+}
|
|
|
+.status-value {
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: bold;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+.status-sub {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ margin-top: 2px;
|
|
|
+}
|
|
|
+.card-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+.test-item {
|
|
|
+ cursor: default;
|
|
|
+ transition: all 0.3s;
|
|
|
+}
|
|
|
+.test-item:hover {
|
|
|
+ transform: translateY(-2px);
|
|
|
+}
|
|
|
+.test-item-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+.test-name {
|
|
|
+ font-weight: 600;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+.test-item-desc {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ margin-bottom: 6px;
|
|
|
+}
|
|
|
+.test-item-error {
|
|
|
+ font-size: 11px;
|
|
|
+ color: #f56c6c;
|
|
|
+ margin-top: 4px;
|
|
|
+}
|
|
|
+.test-item-time {
|
|
|
+ font-size: 11px;
|
|
|
+ color: #67c23a;
|
|
|
+ margin-top: 4px;
|
|
|
+}
|
|
|
+.response-pre {
|
|
|
+ background: #f5f7fa;
|
|
|
+ padding: 12px;
|
|
|
+ border-radius: 4px;
|
|
|
+ font-size: 12px;
|
|
|
+ max-height: 400px;
|
|
|
+ overflow-y: auto;
|
|
|
+ white-space: pre-wrap;
|
|
|
+ word-break: break-all;
|
|
|
+}
|
|
|
+.log-container {
|
|
|
+ max-height: 300px;
|
|
|
+ overflow-y: auto;
|
|
|
+ background: #1e1e1e;
|
|
|
+ border-radius: 4px;
|
|
|
+ padding: 12px;
|
|
|
+ font-family: 'Courier New', monospace;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+.log-line {
|
|
|
+ display: flex;
|
|
|
+ gap: 12px;
|
|
|
+ padding: 2px 0;
|
|
|
+ line-height: 1.6;
|
|
|
+}
|
|
|
+.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>
|