diff --git a/backend/app/main.py b/backend/app/main.py index d624002..52be530 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -833,13 +833,29 @@ async def get_feedback_stats(db: AsyncSession = Depends(get_db)): @app.get("/ai/status") async def get_ai_status(): """ - Check if Ollama AI service is running and available. + 检查 AI 服务状态 + 返回当前提供商类型和运行状态 """ import subprocess import json + import os + provider = os.getenv("LLM_PROVIDER", "ollama").lower() + + # 云端 API 始终就绪 + if provider in ["deepseek", "openai", "qwen"]: + api_key = os.getenv(f"{provider.upper()}_API_KEY") + return { + "status": "ready", + "provider": provider, + "provider_type": "cloud", + "cloud_service": provider, + "api_key_configured": bool(api_key), + "service_name": provider.capitalize() + } + + # 本地 Ollama 需要检查 try: - # Use curl to check Ollama status result = subprocess.run( ["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "http://127.0.0.1:11434/api/tags"], capture_output=True, @@ -848,7 +864,6 @@ async def get_ai_status(): ) if result.stdout.strip() == "200": - # Try to get model list result2 = subprocess.run( ["curl", "-s", "http://127.0.0.1:11434/api/tags"], capture_output=True, @@ -861,19 +876,25 @@ async def get_ai_status(): model_names = [m.get("name", "unknown") for m in models] return { "status": "ready", + "provider": "ollama", + "provider_type": "local", "ollama_running": True, "available_models": model_names, - "model_count": len(model_names) + "model_count": len(model_names), + "service_name": "Ollama" } except Exception as e: print(f"[AI STATUS] Error: {e}") return { "status": "not_ready", + "provider": "ollama", + "provider_type": "local", "ollama_running": False, "available_models": [], "model_count": 0, - "error": "Ollama service not available. Start with: ollama serve" + "service_name": "Ollama", + "error": "Ollama 服务未运行,请点击启动" } @app.post("/ai/stop") @@ -960,6 +981,86 @@ async def stop_ai_service(): } +@app.post("/ai/start") +async def start_ai_service(): + """ + 启动本地 Ollama 服务 + 对于云端 API (DeepSeek/OpenAI/Qwen),返回无需启动 + """ + import subprocess + import os + + provider = os.getenv("LLM_PROVIDER", "ollama").lower() + + # 云端 API 不需要启动 + if provider in ["deepseek", "openai", "qwen"]: + return { + "status": "not_applicable", + "message": f"使用 {provider.upper()} 云端服务,无需启动本地服务", + "provider": provider, + "provider_type": "cloud" + } + + # 检查 Ollama 是否已运行 + try: + result = subprocess.run( + ["lsof", "-ti", ":11434"], + capture_output=True, + timeout=2 + ) + if result.returncode == 0: + return { + "status": "already_running", + "message": "Ollama 已在运行", + "provider": "ollama", + "provider_type": "local" + } + except: + pass + + # 启动 Ollama(尝试多种方式) + started = False + error_msgs = [] + + # 方法1: macOS Ollama.app + try: + subprocess.Popen( + ["open", "-a", "Ollama"], + capture_output=True + ) + started = True + except Exception as e: + error_msgs.append(f"Ollama.app: {e}") + + # 方法2: 命令行 ollama serve + if not started: + try: + subprocess.Popen( + ["ollama", "serve"], + capture_output=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + started = True + except Exception as e: + error_msgs.append(f"ollama serve: {e}") + + if started: + return { + "status": "starting", + "message": "正在启动 Ollama,请稍候...", + "provider": "ollama", + "provider_type": "local" + } + else: + return { + "status": "error", + "message": f"启动失败: {'; '.join(error_msgs)}", + "provider": "ollama", + "provider_type": "local" + } + + # LLM 配置管理 class LLMConfig(BaseModel): provider: str = "deepseek" # ollama, deepseek, openai, qwen diff --git a/frontend/src/components/SignalDashboard.vue b/frontend/src/components/SignalDashboard.vue index 0c4c064..90cbb2e 100644 --- a/frontend/src/components/SignalDashboard.vue +++ b/frontend/src/components/SignalDashboard.vue @@ -54,11 +54,43 @@ {{ analysisStatus }} - + + + + 🌐 {{ llmServiceName }} 云端服务 + + + 🏠 本地 Ollama + + + - {{ ollamaReady ? '启动分析' : '启动分析 (未检测到 Ollama)' }} + {{ ollamaReady ? '启动分析' : (providerType === 'cloud' ? '启动分析' : '启动分析 (未就绪)') }} + + + + + @@ -575,6 +607,11 @@ const analysisProgress = ref(0) const analysisStatus = ref('') const ollamaReady = ref(false) +// AI 服务配置 +const providerType = ref('cloud') // 'cloud' | 'local' +const llmServiceName = ref('') // 'DeepSeek' | 'OpenAI' | 'Qwen' | 'Ollama' +const startingOllama = ref(false) + // Typewriter Effect State const displayedAnalysis = reactive({ situation_analysis: '', @@ -1563,17 +1600,72 @@ const submitSignal = async () => { } } -// 检查 Ollama 状态 +// 检查 AI 服务状态 const checkOllamaStatus = async () => { try { const response = await axios.get('http://127.0.0.1:8000/ai/status', { timeout: 3000 }) - ollamaReady.value = response.data.status === 'ready' - const modelCount = response.data.model_count || 0 - const models = response.data.available_models || [] - message.info(ollamaReady.value ? `✅ Ollama 已就绪 (${modelCount} 个模型: ${models.join(', ')})` : '❌ Ollama 未运行') + + const data = response.data + providerType.value = data.provider_type || 'cloud' + llmServiceName.value = data.service_name || '' + + if (data.provider_type === 'cloud') { + // 云端 API + ollamaReady.value = true + message.success(`✅ 已连接 ${data.service_name} 云端服务`) + } else { + // 本地 Ollama + ollamaReady.value = data.status === 'ready' + const modelCount = data.model_count || 0 + const models = data.available_models || [] + + if (ollamaReady.value) { + message.success(`✅ Ollama 已就绪 (${modelCount} 个模型)`) + } else { + message.warning('⚠️ Ollama 未运行,请点击启动') + } + } } catch (error) { ollamaReady.value = false - message.warning('⚠️ 无法检测 Ollama 状态,请确保后端服务运行中') + message.warning('⚠️ 无法检测 AI 服务状态') + } +} + +// 启动本地 Ollama +const startOllama = async () => { + startingOllama.value = true + try { + const response = await axios.post('http://127.0.0.1:8000/ai/start') + + if (response.data.status === 'already_running') { + message.info('Ollama 已在运行') + ollamaReady.value = true + } else if (response.data.status === 'starting') { + message.info('正在启动 Ollama,请稍候...') + // 等待几秒后检查状态 + setTimeout(async () => { + await checkOllamaStatus() + }, 3000) + } else if (response.data.status === 'not_applicable') { + message.info(response.data.message) + } else { + message.error(response.data.message) + } + } catch (error) { + message.error('启动失败: ' + (error.response?.data?.detail || error.message)) + } finally { + startingOllama.value = false + } +} + +// 停止本地 Ollama +const stopOllama = async () => { + try { + await axios.post('http://127.0.0.1:8000/ai/stop') + ollamaReady.value = false + message.success('✅ Ollama 已停止,内存已释放') + } catch (error) { + message.error('停止失败') } } @@ -1614,12 +1706,15 @@ const startAnalysis = async () => { // 刷新信号列表 await fetchSignals() - // 停止 Ollama 节省资源 - analysisStatus.value = '正在停止 Ollama...' - await axios.post('http://127.0.0.1:8000/ai/stop') - ollamaReady.value = false - - analysisStatus.value = `完成!Ollama 已停止` + // 仅本地 Ollama 需要停止节省资源 + if (providerType.value === 'local') { + analysisStatus.value = '正在停止 Ollama...' + await axios.post('http://127.0.0.1:8000/ai/stop') + ollamaReady.value = false + analysisStatus.value = `完成!Ollama 已停止` + } else { + analysisStatus.value = `分析完成!${llmServiceName.value} 云端服务` + } setTimeout(() => { analyzing.value = false