diff --git a/backend/app/main.py b/backend/app/main.py
index d624002d..52be5308 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 0c4c064b..90cbb2ed 100644
--- a/frontend/src/components/SignalDashboard.vue
+++ b/frontend/src/components/SignalDashboard.vue
@@ -54,11 +54,43 @@
{{ analysisStatus }}
-
+
+
+
+ 🌐 {{ llmServiceName }} 云端服务
+
+
+ 🏠 本地 Ollama
+
+
+
{{ ollamaReady ? '🤖' : '⚠️' }}
- {{ ollamaReady ? '启动分析' : '启动分析 (未检测到 Ollama)' }}
+ {{ ollamaReady ? '启动分析' : (providerType === 'cloud' ? '启动分析' : '启动分析 (未就绪)') }}
+
+
+
+
+ 启动 Ollama
+
+
+ 停止
+
+
+
+
🔄
@@ -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