Просмотр исходного кода

fix(admin): LangGraph 管理页面使用原生 fetch 避免 axios 拦截器问题

iwt 2 месяцев назад
Родитель
Сommit
1d49c22008
3 измененных файлов с 137 добавлено и 95 удалено
  1. 1 1
      cfc-web/.last_build_commit
  2. 2 2
      cfc-web/package-lock.json
  3. 134 92
      cfc-web/src/views/admin/LangGraphAdmin.vue

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-02cafd93db6666e0b99bd69d5d3b197ff061e713
+708f4218575e92b78ae0b5a8b76c1181212aa743

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.865",
+  "version": "1.0.866",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.865",
+      "version": "1.0.866",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",

+ 134 - 92
cfc-web/src/views/admin/LangGraphAdmin.vue

@@ -71,7 +71,7 @@
         <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 :data="filteredApiEndpoints" border stripe size="small" max-height="500">
         <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>
@@ -90,7 +90,7 @@
     <!-- 快速测试面板 -->
     <el-card class="test-card" shadow="never">
       <div slot="header" class="card-header">
-        <span>🧪 快速功能测试</span>
+        <span>🧪 功能测试</span>
         <el-button size="mini" type="success" @click="runQuickTests" :loading="quickTesting">一键测试</el-button>
       </div>
       <el-row :gutter="12">
@@ -176,7 +176,7 @@
           <el-button size="mini" type="danger" @click="clearLogs">清空</el-button>
         </div>
       </div>
-      <div class="log-container">
+      <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>
@@ -189,16 +189,33 @@
 </template>
 
 <script>
-import {
-  getLangGraphHealth,
-  getLangGraphSpec,
-  getLangGraphDetailedHealth,
-  testFoodRecognize,
-  testMenuGenerate,
-  testChat,
-  testRecommend,
-  testQuestionnaireGenerate
-} from '@/api/langgraph'
+const BASE = 'https://ai.etotem.com.cn'
+
+function fetchJSON(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.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 {
   name: 'LangGraphAdmin',
@@ -216,43 +233,47 @@ export default {
       componentList: [],
       refreshing: false,
       quickTesting: false,
-      config: {},
-      // 测试对话框
+      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'
+      },
       testDialogVisible: false,
       testingEndpoint: {},
       testParams: '{}',
       testing: false,
       testResponse: '',
-      // 日志
       logList: [],
-      logLoading: false
+      logLoading: false,
+      _interval: null
     }
   },
-  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: '' }
-        ]
-      }
+  computed: {
+    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() {
@@ -269,8 +290,13 @@ export default {
     },
     async fetchHealth() {
       try {
-        var res = await getLangGraphHealth()
-        if (res.status === 'healthy') {
+        var img = new Image()
+        var healthy = await new Promise(function(resolve) {
+          img.onload = function() { resolve(true) }
+          img.onerror = function() { resolve(false) }
+          img.src = BASE + '/health?t=' + Date.now()
+        })
+        if (healthy) {
           this.statusText = '运行中'
           this.statusClass = 'status-healthy'
           this.statusIcon = '✅'
@@ -288,8 +314,8 @@ export default {
     },
     async fetchSpec() {
       try {
-        var res = await getLangGraphSpec()
-        var paths = res.data.paths || {}
+        var res = await fetchJSON(BASE + '/openapi.json')
+        var paths = res.paths || {}
         var endpoints = []
         var self = this
         Object.keys(paths).forEach(function(path) {
@@ -316,21 +342,20 @@ export default {
     },
     async fetchDetailedHealth() {
       try {
-        var res = await getLangGraphDetailedHealth()
-        var data = res.data || {}
+        var res = await fetchJSON(BASE + '/api/v1/health')
+        var data = res || {}
         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: '' })
+          comps.push({ name: 'LLM', status: c.llm.status || 'unknown', detail: c.llm.message || '' })
         }
         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) {
@@ -351,23 +376,16 @@ export default {
         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)
+    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'
       }
     },
     openTestDialog: function(endpoint) {
@@ -377,8 +395,7 @@ export default {
         '/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 类型表单)'
+        '/api/v1/questionnaire/generate': JSON.stringify({ member_name: '小明', relationship_type: 'parent' }, null, 2)
       }
       this.testParams = defaults[endpoint.path] || '{}'
       this.testResponse = ''
@@ -393,70 +410,87 @@ export default {
         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)
+        var path = self.testingEndpoint.path
+        var res = await fetch(BASE + path, {
+          method: 'POST',
+          headers: { 'Content-Type': 'application/json' },
+          body: JSON.stringify(payload),
+          signal: AbortSignal.timeout(60000)
+        })
+        var data = await res.json()
+        self.testResponse = JSON.stringify(data, null, 2)
       } catch (e) {
-        self.testResponse = '错误: ' + (e.response ? JSON.stringify(e.response.data) : e.message)
+        self.testResponse = '错误: ' + 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 tests = self.quickTests.slice()
       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' })
+        var r1 = await fetch(BASE + '/api/v1/food/recognize', {
+          method: 'POST',
+          headers: { 'Content-Type': 'application/json' },
+          body: JSON.stringify({ image_url: 'https://example.com/food.jpg' })
+        })
+        await r1.json()
         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
+        results.food.errorMsg = e.message
       }
 
-      // 菜单生成
       t0 = Date.now()
       try {
-        var r2 = await testMenuGenerate({ selected_foods: '[{"name":"鸡肉"}]', participants: '[]', date: '2026-08-08' })
+        var r2 = await fetch(BASE + '/api/v1/menu/generate', {
+          method: 'POST',
+          headers: { 'Content-Type': 'application/json' },
+          body: JSON.stringify({ selected_foods: '[{"name":"鸡肉"}]', participants: '[]', date: '2026-08-08' })
+        })
+        await r2.json()
         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
+        results.menu.errorMsg = e.message
       }
 
-      // 对话
       t0 = Date.now()
       try {
-        var r3 = await testChat({ query: '你好', user_id: 1 })
+        var r3 = await fetch(BASE + '/api/v1/chat', {
+          method: 'POST',
+          headers: { 'Content-Type': 'application/json' },
+          body: JSON.stringify({ query: '你好', user_id: 1 })
+        })
+        await r3.json()
         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
+        results.chat.errorMsg = e.message
       }
 
-      // 问卷
       t0 = Date.now()
       try {
-        var r4 = await testQuestionnaireGenerate({ member_name: '小明', relationship_type: 'parent' })
+        var r4 = await fetch(BASE + '/api/v1/questionnaire/generate', {
+          method: 'POST',
+          headers: { 'Content-Type': 'application/json' },
+          body: JSON.stringify({ member_name: '小明', relationship_type: 'parent' })
+        })
+        await r4.json()
         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
+        results.questionnaire.errorMsg = e.message
       }
 
-      // 更新显示
       tests[0] = results.food
       tests[1] = results.menu
       tests[2] = results.chat
@@ -466,11 +500,19 @@ export default {
     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) }]
+        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 }]
+        this.logList = [{
+          time: new Date().toLocaleTimeString(),
+          level: 'ERROR',
+          msg: '无法连接到 LangGraph 服务: ' + e.message
+        }]
       } finally {
         this.logLoading = false
       }
@@ -583,7 +625,7 @@ export default {
   word-break: break-all;
 }
 .log-container {
-  max-height: 300px;
+  max-height: 250px;
   overflow-y: auto;
   background: #1e1e1e;
   border-radius: 4px;