Update backend schema

This commit is contained in:
Xin Wang
2026-02-08 14:26:19 +08:00
parent b9a315177a
commit 727fe8a997
16 changed files with 1532 additions and 94 deletions

View File

@@ -1,7 +1,10 @@
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, DeclarativeBase
import os
DATABASE_URL = "sqlite:///./data/app.db"
# 使用绝对路径
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DATABASE_URL = f"sqlite:///{os.path.join(BASE_DIR, 'data', 'app.db')}"
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

View File

@@ -42,31 +42,4 @@ def root():
@app.get("/health")
def health():
return {"status": "ok"}
# 初始化默认数据
@app.on_event("startup")
def init_default_data():
from sqlalchemy.orm import Session
from .db import SessionLocal
from .models import Voice
db = SessionLocal()
try:
# 检查是否已有数据
if db.query(Voice).count() == 0:
# 插入默认声音
voices = [
Voice(id="v1", name="Xiaoyun", vendor="Ali", gender="Female", language="zh", description="Gentle and professional."),
Voice(id="v2", name="Kevin", vendor="Volcano", gender="Male", language="en", description="Deep and authoritative."),
Voice(id="v3", name="Abby", vendor="Minimax", gender="Female", language="en", description="Cheerful and lively."),
Voice(id="v4", name="Guang", vendor="Ali", gender="Male", language="zh", description="Standard newscast style."),
Voice(id="v5", name="Doubao", vendor="Volcano", gender="Female", language="zh", description="Cute and young."),
]
for v in voices:
db.add(v)
db.commit()
print("✅ 默认声音数据已初始化")
finally:
db.close()
return {"status": "ok"}

View File

@@ -1,6 +1,6 @@
from datetime import datetime
from typing import List, Optional
from sqlalchemy import String, Integer, DateTime, Text, Float, ForeignKey, JSON
from sqlalchemy import String, Integer, DateTime, Text, Float, ForeignKey, JSON, Enum
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .db import Base
@@ -15,18 +15,72 @@ class User(Base):
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
# ============ Voice ============
class Voice(Base):
__tablename__ = "voices"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
user_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey("users.id"), index=True, nullable=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
vendor: Mapped[str] = mapped_column(String(64), nullable=False)
gender: Mapped[str] = mapped_column(String(32), nullable=False)
language: Mapped[str] = mapped_column(String(16), nullable=False)
description: Mapped[str] = mapped_column(String(255), nullable=False)
voice_params: Mapped[dict] = mapped_column(JSON, default=dict)
model: Mapped[Optional[str]] = mapped_column(String(128), nullable=True) # 厂商语音模型标识
voice_key: Mapped[Optional[str]] = mapped_column(String(128), nullable=True) # 厂商voice_key
speed: Mapped[float] = mapped_column(Float, default=1.0)
gain: Mapped[int] = mapped_column(Integer, default=0)
pitch: Mapped[int] = mapped_column(Integer, default=0)
enabled: Mapped[bool] = mapped_column(default=True)
is_system: Mapped[bool] = mapped_column(default=False)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
user = relationship("User", foreign_keys=[user_id])
# ============ LLM Model ============
class LLMModel(Base):
__tablename__ = "llm_models"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), index=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
vendor: Mapped[str] = mapped_column(String(64), nullable=False)
type: Mapped[str] = mapped_column(String(32), nullable=False) # text/embedding/rerank
base_url: Mapped[str] = mapped_column(String(512), nullable=False)
api_key: Mapped[str] = mapped_column(String(512), nullable=False)
model_name: Mapped[Optional[str]] = mapped_column(String(128), nullable=True)
temperature: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
context_length: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
enabled: Mapped[bool] = mapped_column(default=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
user = relationship("User")
# ============ ASR Model ============
class ASRModel(Base):
__tablename__ = "asr_models"
id: Mapped[str] = mapped_column(String(64), primary_key=True)
user_id: Mapped[int] = mapped_column(Integer, ForeignKey("users.id"), index=True)
name: Mapped[str] = mapped_column(String(128), nullable=False)
vendor: Mapped[str] = mapped_column(String(64), nullable=False)
language: Mapped[str] = mapped_column(String(32), nullable=False) # zh/en/Multi-lingual
base_url: Mapped[str] = mapped_column(String(512), nullable=False)
api_key: Mapped[str] = mapped_column(String(512), nullable=False)
model_name: Mapped[Optional[str]] = mapped_column(String(128), nullable=True)
hotwords: Mapped[dict] = mapped_column(JSON, default=list)
enable_punctuation: Mapped[bool] = mapped_column(default=True)
enable_normalization: Mapped[bool] = mapped_column(default=True)
enabled: Mapped[bool] = mapped_column(default=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
user = relationship("User")
# ============ Assistant ============
class Assistant(Base):
__tablename__ = "assistants"
@@ -46,6 +100,11 @@ class Assistant(Base):
config_mode: Mapped[str] = mapped_column(String(32), default="platform")
api_url: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
api_key: Mapped[Optional[str]] = mapped_column(String(255), nullable=True)
# 模型关联
llm_model_id: Mapped[Optional[str]] = mapped_column(String(64), nullable=True)
asr_model_id: Mapped[Optional[str]] = mapped_column(String(64), nullable=True)
embedding_model_id: Mapped[Optional[str]] = mapped_column(String(64), nullable=True)
rerank_model_id: Mapped[Optional[str]] = mapped_column(String(64), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
@@ -53,6 +112,7 @@ class Assistant(Base):
call_records = relationship("CallRecord", back_populates="assistant")
# ============ Knowledge Base ============
class KnowledgeBase(Base):
__tablename__ = "knowledge_bases"
@@ -92,6 +152,7 @@ class KnowledgeDocument(Base):
kb = relationship("KnowledgeBase", back_populates="documents")
# ============ Workflow ============
class Workflow(Base):
__tablename__ = "workflows"
@@ -108,6 +169,7 @@ class Workflow(Base):
user = relationship("User")
# ============ Call Record ============
class CallRecord(Base):
__tablename__ = "call_records"

View File

@@ -1,24 +1,203 @@
from datetime import datetime
from enum import Enum
from typing import List, Optional
from pydantic import BaseModel
# ============ Enums ============
class AssistantConfigMode(str, Enum):
PLATFORM = "platform"
DIFY = "dify"
FASTGPT = "fastgpt"
NONE = "none"
class LLMModelType(str, Enum):
TEXT = "text"
EMBEDDING = "embedding"
RERANK = "rerank"
class ASRLanguage(str, Enum):
ZH = "zh"
EN = "en"
MULTILINGUAL = "Multi-lingual"
class VoiceGender(str, Enum):
MALE = "Male"
FEMALE = "Female"
class CallRecordSource(str, Enum):
DEBUG = "debug"
EXTERNAL = "external"
class CallRecordStatus(str, Enum):
CONNECTED = "connected"
MISSED = "missed"
FAILED = "failed"
# ============ Voice ============
class VoiceBase(BaseModel):
name: str
vendor: str
gender: str
language: str
description: str
gender: str # "Male" | "Female"
language: str # "zh" | "en"
description: str = ""
class VoiceCreate(VoiceBase):
model: str # 厂商语音模型标识
voice_key: str # 厂商voice_key
speed: float = 1.0
gain: int = 0
pitch: int = 0
enabled: bool = True
class VoiceUpdate(BaseModel):
name: Optional[str] = None
description: Optional[str] = None
model: Optional[str] = None
voice_key: Optional[str] = None
speed: Optional[float] = None
gain: Optional[int] = None
pitch: Optional[int] = None
enabled: Optional[bool] = None
class VoiceOut(VoiceBase):
id: str
user_id: Optional[int] = None
model: Optional[str] = None
voice_key: Optional[str] = None
speed: float = 1.0
gain: int = 0
pitch: int = 0
enabled: bool = True
is_system: bool = False
created_at: Optional[datetime] = None
class Config:
from_attributes = True
class VoicePreviewRequest(BaseModel):
text: str
speed: Optional[float] = None
gain: Optional[int] = None
pitch: Optional[int] = None
class VoicePreviewResponse(BaseModel):
success: bool
audio_url: Optional[str] = None
duration_ms: Optional[int] = None
error: Optional[str] = None
# ============ LLM Model ============
class LLMModelBase(BaseModel):
name: str
vendor: str
type: LLMModelType
base_url: str
api_key: str
model_name: Optional[str] = None
temperature: Optional[float] = None
context_length: Optional[int] = None
enabled: bool = True
class LLMModelCreate(LLMModelBase):
pass
class LLMModelUpdate(BaseModel):
name: Optional[str] = None
base_url: Optional[str] = None
api_key: Optional[str] = None
model_name: Optional[str] = None
temperature: Optional[float] = None
context_length: Optional[int] = None
enabled: Optional[bool] = None
class LLMModelOut(LLMModelBase):
id: str
user_id: int
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
class LLMModelTestResponse(BaseModel):
success: bool
latency_ms: Optional[int] = None
message: Optional[str] = None
# ============ ASR Model ============
class ASRModelBase(BaseModel):
name: str
vendor: str
language: str # "zh" | "en" | "Multi-lingual"
base_url: str
api_key: str
model_name: Optional[str] = None
enabled: bool = True
class ASRModelCreate(ASRModelBase):
hotwords: List[str] = []
enable_punctuation: bool = True
enable_normalization: bool = True
class ASRModelUpdate(BaseModel):
name: Optional[str] = None
language: Optional[str] = None
base_url: Optional[str] = None
api_key: Optional[str] = None
model_name: Optional[str] = None
hotwords: Optional[List[str]] = None
enable_punctuation: Optional[bool] = None
enable_normalization: Optional[bool] = None
enabled: Optional[bool] = None
class ASRModelOut(ASRModelBase):
id: str
user_id: int
hotwords: List[str] = []
enable_punctuation: bool = True
enable_normalization: bool = True
created_at: Optional[datetime] = None
class Config:
from_attributes = True
class ASRTestRequest(BaseModel):
audio_url: Optional[str] = None
audio_data: Optional[str] = None # base64 encoded
class ASRTestResponse(BaseModel):
success: bool
transcript: Optional[str] = None
language: Optional[str] = None
confidence: Optional[float] = None
duration_ms: Optional[int] = None
latency_ms: Optional[int] = None
error: Optional[str] = None
# ============ Assistant ============
class AssistantBase(BaseModel):
name: str
@@ -34,25 +213,56 @@ class AssistantBase(BaseModel):
configMode: str = "platform"
apiUrl: Optional[str] = None
apiKey: Optional[str] = None
# 模型关联
llmModelId: Optional[str] = None
asrModelId: Optional[str] = None
embeddingModelId: Optional[str] = None
rerankModelId: Optional[str] = None
class AssistantCreate(AssistantBase):
pass
class AssistantUpdate(AssistantBase):
class AssistantUpdate(BaseModel):
name: Optional[str] = None
opener: Optional[str] = None
prompt: Optional[str] = None
knowledgeBaseId: Optional[str] = None
language: Optional[str] = None
voice: Optional[str] = None
speed: Optional[float] = None
hotwords: Optional[List[str]] = None
tools: Optional[List[str]] = None
interruptionSensitivity: Optional[int] = None
configMode: Optional[str] = None
apiUrl: Optional[str] = None
apiKey: Optional[str] = None
llmModelId: Optional[str] = None
asrModelId: Optional[str] = None
embeddingModelId: Optional[str] = None
rerankModelId: Optional[str] = None
class AssistantOut(AssistantBase):
id: str
callCount: int = 0
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
class Config:
from_attributes = True
class AssistantStats(BaseModel):
assistant_id: str
total_calls: int = 0
connected_calls: int = 0
missed_calls: int = 0
avg_duration_seconds: float = 0.0
today_calls: int = 0
# ============ Knowledge Base ============
class KnowledgeDocument(BaseModel):
id: str
@@ -196,6 +406,7 @@ class TranscriptSegment(BaseModel):
endMs: int
durationMs: Optional[int] = None
audioUrl: Optional[str] = None
emotion: Optional[str] = None
class CallRecordCreate(BaseModel):
@@ -208,6 +419,9 @@ class CallRecordUpdate(BaseModel):
status: Optional[str] = None
summary: Optional[str] = None
duration_seconds: Optional[int] = None
ended_at: Optional[str] = None
cost: Optional[float] = None
metadata: Optional[dict] = None
class CallRecordOut(BaseModel):
@@ -220,6 +434,9 @@ class CallRecordOut(BaseModel):
ended_at: Optional[str] = None
duration_seconds: Optional[int] = None
summary: Optional[str] = None
cost: float = 0.0
metadata: dict = {}
created_at: Optional[datetime] = None
transcripts: List[TranscriptSegment] = []
class Config:
@@ -246,6 +463,19 @@ class TranscriptOut(TranscriptCreate):
from_attributes = True
# ============ History Stats ============
class HistoryStats(BaseModel):
total_calls: int = 0
connected_calls: int = 0
missed_calls: int = 0
failed_calls: int = 0
avg_duration_seconds: float = 0.0
total_cost: float = 0.0
by_status: dict = {}
by_source: dict = {}
daily_trend: List[dict] = []
# ============ Dashboard ============
class DashboardStats(BaseModel):
totalCalls: int
@@ -269,3 +499,9 @@ class ListResponse(BaseModel):
page: int
limit: int
list: List
class SearchResult(BaseModel):
id: str
started_at: str
matched_content: Optional[str] = None