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.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from typing import List
@ -1140,3 +1142,60 @@ async def update_ai_config(config: LLMConfig):
"provider": config.provider,
"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 .database import Base
# Compatibility layer for SQLite
# Compatibility layer for SQLite and PostgreSQL without pgvector
try:
from sqlalchemy.dialects.postgresql import UUID, ARRAY
from pgvector.sqlalchemy import Vector
# Check if we are using SQLite
import os
if "sqlite" in os.getenv("DATABASE_URL", ""):
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:
from sqlalchemy.types import TypeDecorator, CHAR
import json
@ -38,7 +42,6 @@ except ImportError:
if value is None: return None
return json.loads(value)
# Use JSON for ARRAY in SQLite
ARRAY = JSON
class ContextNode(Base):

View File

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

View File

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

View File

@ -52,7 +52,7 @@
</div>
<!-- 选中提供商说明 -->
<div class="provider-info">
<div class="provider-info" v-if="selectedProvider">
<span class="provider-desc">{{ selectedProvider.desc }}</span>
<span class="provider-pricing">{{ selectedProvider.pricing }}</span>
</div>
@ -63,7 +63,7 @@
<h2 class="section-title">连接配置</h2>
<!-- API 密钥配置 -->
<div v-if="showApiKey" class="form-group">
<div v-if="showApiKey && selectedProvider" class="form-group">
<label class="form-label">API 密钥</label>
<input
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' })
])
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 = [
{ key: 'provider', label: '模型服务', icon: CloudIcon },
@ -239,7 +232,7 @@ const selectProvider = (value: string) => {
const testConnection = async () => {
testing.value = true
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') {
isConnected.value = true
statusText.value = `${response.data.model_count} 个模型`
@ -258,7 +251,7 @@ const testConnection = async () => {
const loadConfig = async () => {
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.ollama_host = response.data.ollama_host
} catch {}
@ -267,7 +260,7 @@ const loadConfig = async () => {
const saveConfig = async () => {
saving.value = true
try {
await axios.post('http://127.0.0.1:8000/ai/config', {
await axios.post('/ai/config', {
provider: config.provider,
deepseek_api_key: config.deepseek_api_key || undefined,
openai_api_key: config.openai_api_key || undefined,
@ -291,7 +284,7 @@ const resetConfig = () => {
const restartBackend = async () => {
try {
await axios.post('http://127.0.0.1:8000/ai/stop')
await axios.post('/ai/stop')
saveStatus.value = '后端已停止,请手动启动'
showRestartModal.value = false
} catch {

View File

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