Compare commits

..

9 Commits
master ... main

6 changed files with 120 additions and 66 deletions

View File

@ -1,5 +1,7 @@
from fastapi import FastAPI, Depends, HTTPException, BackgroundTasks from fastapi import FastAPI, Depends, HTTPException, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select from sqlalchemy.future import select
from typing import List from typing import List
@ -1140,3 +1142,60 @@ async def update_ai_config(config: LLMConfig):
"provider": config.provider, "provider": config.provider,
"requires_restart": True "requires_restart": True
} }
# 前端静态文件服务
FRONTEND_DIR = Path("/data/neural-sync/frontend/dist")
@app.get("/")
async def serve_frontend():
"""服务前端首页"""
index_path = FRONTEND_DIR / "index.html"
if index_path.exists():
return FileResponse(index_path)
return {"message": "Frontend not found", "frontend_dir": str(FRONTEND_DIR)}
@app.get("/{path:path}")
async def serve_frontend_paths(path: str):
"""服务前端其他路由"""
# 先尝试直接文件
file_path = FRONTEND_DIR / path
if file_path.exists() and file_path.is_file():
return FileResponse(file_path)
# 回退到 index.html (SPA 路由)
return FileResponse(FRONTEND_DIR / "index.html")
@app.post("/admin/build-frontend")
async def build_frontend(background_tasks: BackgroundTasks):
"""构建前端 (仅用于部署)"""
import subprocess
frontend_path = FRONTEND_DIR.parent
async def run_build():
try:
result = subprocess.run(
["npm", "install"],
cwd=str(frontend_path),
capture_output=True,
text=True,
timeout=300
)
result = subprocess.run(
["npm", "run", "build"],
cwd=str(frontend_path),
capture_output=True,
text=True,
timeout=300
)
return result.returncode == 0, result.stdout, result.stderr
except Exception as e:
return False, "", str(e)
success, stdout, stderr = await run_build()
if success:
return {"status": "success", "message": "前端构建成功"}
else:
return {"status": "error", "message": stderr or "构建失败"}

View File

@ -4,14 +4,18 @@ import uuid
from datetime import datetime from datetime import datetime
from .database import Base from .database import Base
# Compatibility layer for SQLite # Compatibility layer for SQLite and PostgreSQL without pgvector
try: try:
from sqlalchemy.dialects.postgresql import UUID, ARRAY from sqlalchemy.dialects.postgresql import UUID, ARRAY
from pgvector.sqlalchemy import Vector
# Check if we are using SQLite
import os import os
if "sqlite" in os.getenv("DATABASE_URL", ""): if "sqlite" in os.getenv("DATABASE_URL", ""):
raise ImportError("Using SQLite") raise ImportError("Using SQLite")
# Try to import pgvector, if fails use JSON
try:
from pgvector.sqlalchemy import Vector
except ImportError:
# No pgvector, use JSON instead
Vector = JSON
except ImportError: except ImportError:
from sqlalchemy.types import TypeDecorator, CHAR from sqlalchemy.types import TypeDecorator, CHAR
import json import json
@ -38,7 +42,6 @@ except ImportError:
if value is None: return None if value is None: return None
return json.loads(value) return json.loads(value)
# Use JSON for ARRAY in SQLite
ARRAY = JSON ARRAY = JSON
class ContextNode(Base): class ContextNode(Base):

View File

@ -107,7 +107,7 @@ const extractMetadata = (content: string) => {
// 1. Extract Title (First H1 or first non-empty line) // 1. Extract Title (First H1 or first non-empty line)
const titleMatch = content.match(/^#\s+(.*)/m) const titleMatch = content.match(/^#\s+(.*)/m)
if (titleMatch) { if (titleMatch && titleMatch[1]) {
title = titleMatch[1].trim() title = titleMatch[1].trim()
} else { } else {
const firstLine = lines.find(l => l.trim().length > 0) const firstLine = lines.find(l => l.trim().length > 0)
@ -136,12 +136,12 @@ const extractMetadata = (content: string) => {
// Look for "Core Logic", "Architecture", "Introduction" headers // Look for "Core Logic", "Architecture", "Introduction" headers
const logicRegex = /##\s*(Core Logic|Architecture|Introduction|About|Features)([\s\S]*?)(##|$)/i const logicRegex = /##\s*(Core Logic|Architecture|Introduction|About|Features)([\s\S]*?)(##|$)/i
const logicMatch = content.match(logicRegex) const logicMatch = content.match(logicRegex)
if (logicMatch) { if (logicMatch && logicMatch[2]) {
coreLogic = logicMatch[2].trim().substring(0, 300) + (logicMatch[2].length > 300 ? '...' : '') coreLogic = logicMatch[2].trim().substring(0, 300) + (logicMatch[2].length > 300 ? '...' : '')
} else { } else {
// Fallback: First paragraph after title // Fallback: First paragraph after title
const paragraphs = content.split(/\n\s*\n/) const paragraphs = content.split(/\n\s*\n/)
if (paragraphs.length > 1) { if (paragraphs.length > 1 && paragraphs[1]) {
coreLogic = paragraphs[1].trim().substring(0, 200) + '...' coreLogic = paragraphs[1].trim().substring(0, 200) + '...'
} }
} }
@ -169,7 +169,7 @@ const parsedNodes = computed(() => {
const fetchNodes = async () => { const fetchNodes = async () => {
try { try {
const res = await axios.get('http://127.0.0.1:8000/context_nodes/') const res = await axios.get('/context_nodes/')
nodes.value = res.data nodes.value = res.data
} catch (error) { } catch (error) {
message.error('获取认知节点失败') message.error('获取认知节点失败')
@ -178,7 +178,7 @@ const fetchNodes = async () => {
const createNode = async () => { const createNode = async () => {
try { try {
const res = await axios.post('http://127.0.0.1:8000/context_nodes/', formValue.value) const res = await axios.post('/context_nodes/', formValue.value)
message.success('认知已提交!') message.success('认知已提交!')
nodes.value.push(res.data) nodes.value.push(res.data)
showModal.value = false showModal.value = false

View File

@ -97,7 +97,7 @@ const getStatusLabel = (status: string) => {
const fetchExperiments = async () => { const fetchExperiments = async () => {
try { try {
const res = await axios.get('http://127.0.0.1:8000/experiments/') const res = await axios.get('/experiments/')
experiments.value = res.data experiments.value = res.data
} catch (error) { } catch (error) {
message.error('获取实验列表失败') message.error('获取实验列表失败')
@ -106,7 +106,7 @@ const fetchExperiments = async () => {
const fetchContextNodes = async () => { const fetchContextNodes = async () => {
try { try {
const res = await axios.get('http://127.0.0.1:8000/context_nodes/') const res = await axios.get('/context_nodes/')
contextNodes.value = res.data contextNodes.value = res.data
} catch (error) { } catch (error) {
console.error('Failed to fetch context nodes') console.error('Failed to fetch context nodes')
@ -120,7 +120,7 @@ const openModal = () => {
const createExperiment = async () => { const createExperiment = async () => {
try { try {
const res = await axios.post('http://127.0.0.1:8000/experiments/', formValue.value) const res = await axios.post('/experiments/', formValue.value)
message.success('实验已启动!') message.success('实验已启动!')
experiments.value.push(res.data) experiments.value.push(res.data)
showModal.value = false showModal.value = false

View File

@ -52,7 +52,7 @@
</div> </div>
<!-- 选中提供商说明 --> <!-- 选中提供商说明 -->
<div class="provider-info"> <div class="provider-info" v-if="selectedProvider">
<span class="provider-desc">{{ selectedProvider.desc }}</span> <span class="provider-desc">{{ selectedProvider.desc }}</span>
<span class="provider-pricing">{{ selectedProvider.pricing }}</span> <span class="provider-pricing">{{ selectedProvider.pricing }}</span>
</div> </div>
@ -63,7 +63,7 @@
<h2 class="section-title">连接配置</h2> <h2 class="section-title">连接配置</h2>
<!-- API 密钥配置 --> <!-- API 密钥配置 -->
<div v-if="showApiKey" class="form-group"> <div v-if="showApiKey && selectedProvider" class="form-group">
<label class="form-label">API 密钥</label> <label class="form-label">API 密钥</label>
<input <input
v-model="apiKey" v-model="apiKey"
@ -161,13 +161,6 @@ const KeyIcon = () => h('svg', { viewBox: '0 0 24 24', fill: 'none', stroke: 'cu
h('path', { d: 'M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4' }) h('path', { d: 'M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4' })
]) ])
const ServerIcon = () => h('svg', { viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', 'stroke-width': 1.5 }, [
h('rect', { x: '2', y: '2', width: '20', height: '8', rx: '2', ry: '2' }),
h('rect', { x: '2', y: '14', width: '20', height: '8', rx: '2', ry: '2' }),
h('line', { x1: '6', y1: '6', x2: '6.01', y2: '6' }),
h('line', { x1: '6', y1: '18', x2: '6.01', y2: '18' })
])
// 侧边栏导航 // 侧边栏导航
const navItems = [ const navItems = [
{ key: 'provider', label: '模型服务', icon: CloudIcon }, { key: 'provider', label: '模型服务', icon: CloudIcon },
@ -239,7 +232,7 @@ const selectProvider = (value: string) => {
const testConnection = async () => { const testConnection = async () => {
testing.value = true testing.value = true
try { try {
const response = await axios.get(`${config.ollama_host.replace(':11434', ':8000')}/ai/status`, { timeout: 5000 }) const response = await axios.get(`/ai/status`, { timeout: 5000 })
if (response.data.status === 'ready') { if (response.data.status === 'ready') {
isConnected.value = true isConnected.value = true
statusText.value = `${response.data.model_count} 个模型` statusText.value = `${response.data.model_count} 个模型`
@ -258,7 +251,7 @@ const testConnection = async () => {
const loadConfig = async () => { const loadConfig = async () => {
try { try {
const response = await axios.get('http://127.0.0.1:8000/ai/config') const response = await axios.get('/ai/config')
config.provider = response.data.provider config.provider = response.data.provider
config.ollama_host = response.data.ollama_host config.ollama_host = response.data.ollama_host
} catch {} } catch {}
@ -267,7 +260,7 @@ const loadConfig = async () => {
const saveConfig = async () => { const saveConfig = async () => {
saving.value = true saving.value = true
try { try {
await axios.post('http://127.0.0.1:8000/ai/config', { await axios.post('/ai/config', {
provider: config.provider, provider: config.provider,
deepseek_api_key: config.deepseek_api_key || undefined, deepseek_api_key: config.deepseek_api_key || undefined,
openai_api_key: config.openai_api_key || undefined, openai_api_key: config.openai_api_key || undefined,
@ -291,7 +284,7 @@ const resetConfig = () => {
const restartBackend = async () => { const restartBackend = async () => {
try { try {
await axios.post('http://127.0.0.1:8000/ai/stop') await axios.post('/ai/stop')
saveStatus.value = '后端已停止,请手动启动' saveStatus.value = '后端已停止,请手动启动'
showRestartModal.value = false showRestartModal.value = false
} catch { } catch {

View File

@ -21,11 +21,11 @@
<div class="progress-comparison"> <div class="progress-comparison">
<div class="progress-row"> <div class="progress-row">
<span class="label">我方</span> <span class="label">我方</span>
<n-progress type="line" :percentage="getOurProgress(selectedSignal)" color="#63e2b7" rail-color="rgba(255,255,255,0.1)" height="6" processing /> <n-progress type="line" :percentage="Number(getOurProgress(selectedSignal))" color="#63e2b7" rail-color="rgba(255,255,255,0.1)" :height="6" processing />
</div> </div>
<div class="progress-row"> <div class="progress-row">
<span class="label">竞品</span> <span class="label">竞品</span>
<n-progress type="line" :percentage="getCompetitorProgress(selectedSignal)" color="#f5222d" rail-color="rgba(255,255,255,0.1)" height="6" /> <n-progress type="line" :percentage="Number(getCompetitorProgress(selectedSignal))" color="#f5222d" rail-color="rgba(255,255,255,0.1)" :height="6" />
</div> </div>
<div class="lead-time-text" v-if="selectedSignal"> <div class="lead-time-text" v-if="selectedSignal">
{{ getLeadTimeText(selectedSignal) }} {{ getLeadTimeText(selectedSignal) }}
@ -134,18 +134,16 @@
'new-signal-flash': isNewSignal(signal.latestSignal) 'new-signal-flash': isNewSignal(signal.latestSignal)
}" }"
> >
<template #header> <div class="signal-item-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div style="display: flex; align-items: center; gap: 6px;"> <div style="display: flex; align-items: center; gap: 6px;">
<n-tag :type="getPriorityType(signal.latestSignal.priority)" size="small" round> <n-tag :type="getPriorityType(signal.latestSignal?.priority || 'medium')" size="small" round>
{{ getPriorityLabel(signal.latestSignal.priority) }} {{ getPriorityLabel(signal.latestSignal?.priority || 'medium') }}
</n-tag> </n-tag>
<n-tag :type="getImpactColor(signal.impact_score)" size="small" round>{{ signal.source }}</n-tag> <n-tag :type="getImpactColor(signal.impact_score)" size="small" round>{{ signal.source }}</n-tag>
<n-tag v-if="signal.count > 1" type="info" size="tiny" round>{{ signal.count }}条</n-tag> <n-tag v-if="signal.count > 1" type="info" size="tiny" round>{{ signal.count }}条</n-tag>
</div> </div>
<n-text depth="3" style="font-size: 12px;">{{ formatDate(signal.created_at) }}</n-text> <n-text depth="3" style="font-size: 12px;">{{ formatDate(signal.created_at) }}</n-text>
</div> </div>
</template>
<n-text class="signal-content" :depth="1">{{ signal.content }}</n-text> <n-text class="signal-content" :depth="1">{{ signal.content }}</n-text>
</n-list-item> </n-list-item>
</n-list> </n-list>
@ -493,7 +491,7 @@
:percentage="getTaskProgress(task)" :percentage="getTaskProgress(task)"
:color="getTaskTypeColor(task.type, true)" :color="getTaskTypeColor(task.type, true)"
rail-color="rgba(255,255,255,0.1)" rail-color="rgba(255,255,255,0.1)"
height="4" :height="4"
:show-indicator="false" :show-indicator="false"
style="margin-top: 8px;" style="margin-top: 8px;"
:class="{ 'progress-pulse': taskExecuting }" :class="{ 'progress-pulse': taskExecuting }"
@ -543,7 +541,7 @@
<n-modal v-model:show="showCompetitorModal"> <n-modal v-model:show="showCompetitorModal">
<n-card style="width: 400px" title="⚔️ 设定主要竞品(某漫、某画等)" :bordered="false" size="large" role="dialog" aria-modal="true" class="glass-modal"> <n-card style="width: 400px" title="⚔️ 设定主要竞品(某漫、某画等)" :bordered="false" size="large" role="dialog" aria-modal="true" class="glass-modal">
<n-space vertical> <n-space vertical>
<n-input v-for="(comp, index) in competitors" :key="index" v-model:value="competitors[index]" placeholder="输入竞品名称..." /> <n-input v-for="(_comp, index) in competitors" :key="index" v-model:value="competitors[index]" placeholder="输入竞品名称..." />
</n-space> </n-space>
<template #footer> <template #footer>
<n-button type="primary" block @click="saveCompetitors"> <n-button type="primary" block @click="saveCompetitors">
@ -573,11 +571,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, onUnmounted, reactive, watch, nextTick, computed } from 'vue' import { ref, onMounted, onUnmounted, reactive, watch, computed } from 'vue'
import { import {
NLayout, NLayoutSider, NLayoutContent, NSpace, NCard, NButton, NList, NListItem, NTag, NText, NLayout, NLayoutSider, NLayoutContent, NSpace, NCard, NButton, NList, NListItem, NTag, NText,
NGrid, NGridItem, NDivider, NResult, NCheckbox, NThing, NScrollbar, NSpin, NModal, NForm, NFormItem, NGrid, NGridItem, NScrollbar, NSpin, NModal, NForm, NFormItem,
NInput, useMessage, NEmpty, NProgress, NIcon, NSteps, NStep, NStatistic NInput, useMessage, NProgress, NIcon, NSteps, NStep, NStatistic
} from 'naive-ui' } from 'naive-ui'
import axios from 'axios' import axios from 'axios'
import VChart from 'vue-echarts' import VChart from 'vue-echarts'
@ -859,12 +857,14 @@ const factSnippets: Record<string, { title: string; source: string; content: str
const showFactSnippetModal = (type: string) => { const showFactSnippetModal = (type: string) => {
const snippet = factSnippets[type] || factSnippets['Whitepaper'] const snippet = factSnippets[type] || factSnippets['Whitepaper']
if (snippet) {
currentFactSnippet.value = { currentFactSnippet.value = {
title: snippet.title, title: snippet.title,
source: snippet.source, source: snippet.source,
content: snippet.content content: snippet.content
} }
showFactSnippet.value = true showFactSnippet.value = true
}
} }
// --- Logic --- // --- Logic ---
@ -947,8 +947,8 @@ const getRelatedKnowledgeNodes = () => {
return Array.isArray(knowledgeNodes) ? knowledgeNodes : [] return Array.isArray(knowledgeNodes) ? knowledgeNodes : []
} }
const getTechStackType = (category: string) => { const getTechStackType = (category: string): 'default' | 'error' | 'success' | 'warning' | 'info' | 'primary' => {
const typeMap: Record<string, string> = { const typeMap: Record<string, 'default' | 'error' | 'success' | 'warning' | 'info' | 'primary'> = {
framework: 'success', framework: 'success',
model: 'error', model: 'error',
tool: 'info', tool: 'info',
@ -994,8 +994,8 @@ const getKnowledgeNodeIcon = (category: string) => {
return iconMap[category] || '🧠' return iconMap[category] || '🧠'
} }
const getKnowledgeNodeType = (category: string) => { const getKnowledgeNodeType = (category: string): 'default' | 'error' | 'success' | 'warning' | 'info' | 'primary' => {
const typeMap: Record<string, string> = { const typeMap: Record<string, 'default' | 'error' | 'success' | 'warning' | 'info' | 'primary'> = {
tech_stack: 'success', tech_stack: 'success',
competitor: 'error', competitor: 'error',
market: 'info' market: 'info'
@ -1163,13 +1163,13 @@ const getPriorityLabel = (priority: string) => {
} }
// Voting helpers // Voting helpers
const getModelVoteType = (modelName: string, strategicInfo: any) => { const getModelVoteType = (_modelName: string, strategicInfo: any) => {
// For now, since we don't have individual model results in the response, // For now, since we don't have individual model results in the response,
// we'll return a default type. This could be enhanced if we send individual results. // we'll return a default type. This could be enhanced if we send individual results.
return strategicInfo.is_strategic ? 'success' : 'default' return strategicInfo.is_strategic ? 'success' : 'default'
} }
const getModelVoteLabel = (modelName: string, strategicInfo: any) => { const getModelVoteLabel = (_modelName: string, strategicInfo: any) => {
// Similar to above, placeholder logic // Similar to above, placeholder logic
return strategicInfo.is_strategic ? '战略级' : '非战略' return strategicInfo.is_strategic ? '战略级' : '非战略'
} }
@ -1186,7 +1186,7 @@ const submitFeedback = async (agreed: boolean) => {
const aiPrediction = currentStrategicInfo.value.is_strategic const aiPrediction = currentStrategicInfo.value.is_strategic
const userFeedback = agreed ? aiPrediction : !aiPrediction const userFeedback = agreed ? aiPrediction : !aiPrediction
const response = await axios.post(`http://127.0.0.1:8000/signals/${selectedSignal.value.id}/feedback`, { const response = await axios.post(`/signals/${selectedSignal.value.id}/feedback`, {
signal_id: selectedSignal.value.id, signal_id: selectedSignal.value.id,
was_strategic: userFeedback, was_strategic: userFeedback,
reason: agreed ? 'AI判断准确' : 'AI判断不准确' reason: agreed ? 'AI判断准确' : 'AI判断不准确'
@ -1447,7 +1447,7 @@ watch(selectedSignal, (newVal, oldVal) => {
// Load existing feedback for a signal // Load existing feedback for a signal
const loadSignalFeedback = async (signalId: string) => { const loadSignalFeedback = async (signalId: string) => {
try { try {
const response = await axios.get(`http://127.0.0.1:8000/signals/${signalId}/feedbacks`) const response = await axios.get(`/signals/${signalId}/feedbacks`)
if (response.data && response.data.length > 0) { if (response.data && response.data.length > 0) {
const latestFeedback = response.data[0] const latestFeedback = response.data[0]
signalFeedback.value = latestFeedback signalFeedback.value = latestFeedback
@ -1505,7 +1505,7 @@ const isNewSignal = (signal: any) => {
const fetchSignals = async (isPoll = false) => { const fetchSignals = async (isPoll = false) => {
if (!isPoll) loading.value = true if (!isPoll) loading.value = true
try { try {
const res = await axios.get('http://127.0.0.1:8000/signals/') const res = await axios.get('/signals/')
const newSignals = res.data const newSignals = res.data
// Check for task status updates if polling // Check for task status updates if polling
@ -1579,7 +1579,7 @@ const submitSignal = async () => {
if (!formValue.value.content) return if (!formValue.value.content) return
submitting.value = true submitting.value = true
try { try {
const res = await axios.post('http://127.0.0.1:8000/signals/ingest', formValue.value) const res = await axios.post('/signals/ingest', formValue.value)
message.success('信号已注入,战略评估完成!') message.success('信号已注入,战略评估完成!')
showModal.value = false showModal.value = false
await fetchSignals() await fetchSignals()
@ -1590,7 +1590,7 @@ const submitSignal = async () => {
} }
// 停止 Ollama 节省资源 // 停止 Ollama 节省资源
await axios.post('http://127.0.0.1:8000/ai/stop') await axios.post('/ai/stop')
ollamaReady.value = false ollamaReady.value = false
message.info('🌱 Ollama 已停止,节省资源') message.info('🌱 Ollama 已停止,节省资源')
} catch (error) { } catch (error) {
@ -1603,7 +1603,7 @@ const submitSignal = async () => {
// 检查 AI 服务状态 // 检查 AI 服务状态
const checkOllamaStatus = async () => { const checkOllamaStatus = async () => {
try { try {
const response = await axios.get('http://127.0.0.1:8000/ai/status', { timeout: 3000 }) const response = await axios.get('/ai/status', { timeout: 3000 })
const data = response.data const data = response.data
providerType.value = data.provider_type || 'cloud' providerType.value = data.provider_type || 'cloud'
@ -1617,7 +1617,6 @@ const checkOllamaStatus = async () => {
// 本地 Ollama // 本地 Ollama
ollamaReady.value = data.status === 'ready' ollamaReady.value = data.status === 'ready'
const modelCount = data.model_count || 0 const modelCount = data.model_count || 0
const models = data.available_models || []
if (ollamaReady.value) { if (ollamaReady.value) {
message.success(`✅ Ollama 已就绪 (${modelCount} 个模型)`) message.success(`✅ Ollama 已就绪 (${modelCount} 个模型)`)
@ -1635,7 +1634,7 @@ const checkOllamaStatus = async () => {
const startOllama = async () => { const startOllama = async () => {
startingOllama.value = true startingOllama.value = true
try { try {
const response = await axios.post('http://127.0.0.1:8000/ai/start') const response = await axios.post('/ai/start')
if (response.data.status === 'already_running') { if (response.data.status === 'already_running') {
message.info('Ollama 已在运行') message.info('Ollama 已在运行')
@ -1651,7 +1650,7 @@ const startOllama = async () => {
} else { } else {
message.error(response.data.message) message.error(response.data.message)
} }
} catch (error) { } catch (error: any) {
message.error('启动失败: ' + (error.response?.data?.detail || error.message)) message.error('启动失败: ' + (error.response?.data?.detail || error.message))
} finally { } finally {
startingOllama.value = false startingOllama.value = false
@ -1661,7 +1660,7 @@ const startOllama = async () => {
// 停止本地 Ollama // 停止本地 Ollama
const stopOllama = async () => { const stopOllama = async () => {
try { try {
await axios.post('http://127.0.0.1:8000/ai/stop') await axios.post('/ai/stop')
ollamaReady.value = false ollamaReady.value = false
message.success('✅ Ollama 已停止,内存已释放') message.success('✅ Ollama 已停止,内存已释放')
} catch (error) { } catch (error) {
@ -1692,7 +1691,7 @@ const startAnalysis = async () => {
// 调用后端获取并分析信号 // 调用后端获取并分析信号
analysisStatus.value = '正在获取 HackerNews 信号...' analysisStatus.value = '正在获取 HackerNews 信号...'
const response = await axios.post('http://127.0.0.1:8000/signals/fetch') const response = await axios.post('/signals/fetch')
clearInterval(progressInterval) clearInterval(progressInterval)
analysisProgress.value = 100 analysisProgress.value = 100
@ -1709,7 +1708,7 @@ const startAnalysis = async () => {
// 仅本地 Ollama 需要停止节省资源 // 仅本地 Ollama 需要停止节省资源
if (providerType.value === 'local') { if (providerType.value === 'local') {
analysisStatus.value = '正在停止 Ollama...' analysisStatus.value = '正在停止 Ollama...'
await axios.post('http://127.0.0.1:8000/ai/stop') await axios.post('/ai/stop')
ollamaReady.value = false ollamaReady.value = false
analysisStatus.value = `完成!Ollama 已停止` analysisStatus.value = `完成!Ollama 已停止`
} else { } else {