deepdata: core product code (backend, frontend, runtime, deepflow module)
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from .about import router as about_router
|
||||
from app.module.system.interface.models import router as models_router
|
||||
from .sys_param import router as sys_param_router
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router.include_router(about_router)
|
||||
router.include_router(models_router)
|
||||
router.include_router(sys_param_router)
|
||||
@@ -0,0 +1,22 @@
|
||||
from fastapi import APIRouter
|
||||
from typing import Dict, Any
|
||||
from app.core.config import settings
|
||||
from app.module.shared.schema import StandardResponse
|
||||
|
||||
from ..schema import HealthResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/health", response_model=StandardResponse[HealthResponse])
|
||||
async def health_check():
|
||||
"""健康检查端点"""
|
||||
|
||||
return StandardResponse(
|
||||
code="0",
|
||||
message="success",
|
||||
data=HealthResponse(
|
||||
status="healthy",
|
||||
service="Label Studio Adapter",
|
||||
version=settings.app_version
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,76 @@
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
|
||||
from app.core.exception import SuccessResponse
|
||||
from app.module.shared.schema import StandardResponse, PaginatedData
|
||||
from app.module.system.schema.models import (
|
||||
CreateModelRequest,
|
||||
QueryModelRequest,
|
||||
ModelsResponse,
|
||||
ProviderItem,
|
||||
ModelType,
|
||||
)
|
||||
from app.module.system.service.models_service import ModelsService
|
||||
|
||||
router = APIRouter(prefix="/models", tags=["models"])
|
||||
|
||||
|
||||
@router.get("/providers", response_model=StandardResponse[list[ProviderItem]])
|
||||
async def get_providers(svc: ModelsService = Depends()):
|
||||
"""获取厂商列表,与 Java GET /models/providers 一致。"""
|
||||
data = await svc.get_providers()
|
||||
return SuccessResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/list", response_model=StandardResponse[PaginatedData[ModelsResponse]])
|
||||
async def get_models(
|
||||
page: int = Query(0, ge=0, description="页码,从 0 开始"),
|
||||
size: int = Query(20, gt=0, le=2000, description="每页大小"),
|
||||
provider: str | None = Query(None, description="模型提供商"),
|
||||
type: ModelType | None = Query(None, description="模型类型"),
|
||||
isEnabled: bool | None = Query(None, description="是否启用"),
|
||||
isDefault: bool | None = Query(None, description="是否默认"),
|
||||
svc: ModelsService = Depends(),
|
||||
):
|
||||
"""分页查询模型列表,与 Java GET /models/list 一致。"""
|
||||
q = QueryModelRequest(
|
||||
page=page,
|
||||
size=size,
|
||||
provider=provider,
|
||||
type=type,
|
||||
isEnabled=isEnabled,
|
||||
isDefault=isDefault,
|
||||
)
|
||||
data = await svc.get_models(q)
|
||||
return SuccessResponse(data=data)
|
||||
|
||||
|
||||
@router.post("/create", response_model=StandardResponse[ModelsResponse])
|
||||
async def create_model(req: CreateModelRequest, svc: ModelsService = Depends()):
|
||||
"""创建模型配置,与 Java POST /models/create 一致。"""
|
||||
data = await svc.create_model(req)
|
||||
return SuccessResponse(data=data)
|
||||
|
||||
|
||||
@router.get("/{model_id}", response_model=StandardResponse[ModelsResponse])
|
||||
async def get_model_detail(model_id: str, svc: ModelsService = Depends()):
|
||||
"""获取模型详情,与 Java GET /models/{modelId} 一致。"""
|
||||
data = await svc.get_model_detail(model_id)
|
||||
return SuccessResponse(data=data)
|
||||
|
||||
|
||||
@router.put("/{model_id}", response_model=StandardResponse[ModelsResponse])
|
||||
async def update_model(
|
||||
model_id: str,
|
||||
req: CreateModelRequest,
|
||||
svc: ModelsService = Depends(),
|
||||
):
|
||||
"""更新模型配置,与 Java PUT /models/{modelId} 一致。"""
|
||||
data = await svc.update_model(model_id, req)
|
||||
return SuccessResponse(data=data)
|
||||
|
||||
|
||||
@router.delete("/{model_id}", response_model=StandardResponse[None])
|
||||
async def delete_model(model_id: str, svc: ModelsService = Depends()):
|
||||
"""删除模型配置,与 Java DELETE /models/{modelId} 一致。"""
|
||||
await svc.delete_model(model_id)
|
||||
return SuccessResponse(data=None)
|
||||
@@ -0,0 +1,106 @@
|
||||
"""
|
||||
System Parameter API Routes
|
||||
系统参数 REST API 路由
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.module.shared.schema.common import StandardResponse
|
||||
from app.module.system.schema.sys_param import (
|
||||
SysParamDto,
|
||||
UpdateParamValueRequest,
|
||||
CreateSysParamRequest,
|
||||
)
|
||||
from app.module.system.service.sys_param_service import SysParamService
|
||||
|
||||
router = APIRouter(prefix="/sys-param", tags=["System Parameters"])
|
||||
|
||||
|
||||
@router.get(
|
||||
"/list",
|
||||
response_model=StandardResponse[List[SysParamDto]],
|
||||
summary="获取系统参数列表",
|
||||
description="获取所有系统参数配置",
|
||||
)
|
||||
async def list_params(db: AsyncSession = Depends(get_db)):
|
||||
"""获取系统参数列表"""
|
||||
service = SysParamService(db)
|
||||
params = await service.list_params()
|
||||
return StandardResponse(code="0", message="success", data=params)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{param_id}",
|
||||
response_model=StandardResponse[SysParamDto],
|
||||
summary="获取系统参数详情",
|
||||
description="根据ID获取系统参数详情",
|
||||
)
|
||||
async def get_param(
|
||||
param_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取系统参数详情"""
|
||||
service = SysParamService(db)
|
||||
param = await service.get_param_by_id(param_id)
|
||||
if not param:
|
||||
from app.core.exception import BusinessError, ErrorCodes
|
||||
|
||||
raise BusinessError(
|
||||
ErrorCodes.NOT_FOUND, f"System parameter {param_id} not found"
|
||||
)
|
||||
return StandardResponse(code="0", message="success", data=param)
|
||||
|
||||
|
||||
@router.post(
|
||||
"",
|
||||
response_model=StandardResponse[SysParamDto],
|
||||
summary="创建系统参数",
|
||||
description="创建新的系统参数",
|
||||
)
|
||||
async def create_param(
|
||||
request: CreateSysParamRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""创建系统参数"""
|
||||
service = SysParamService(db)
|
||||
param = await service.create_param(request)
|
||||
await db.commit()
|
||||
return StandardResponse(code="0", message="success", data=param)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/{param_id}",
|
||||
response_model=StandardResponse[SysParamDto],
|
||||
summary="更新系统参数值",
|
||||
description="更新指定系统参数的值",
|
||||
)
|
||||
async def update_param_value(
|
||||
param_id: str,
|
||||
request: UpdateParamValueRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""更新系统参数值"""
|
||||
service = SysParamService(db)
|
||||
param = await service.update_param_value(param_id, request.param_value)
|
||||
await db.commit()
|
||||
return StandardResponse(code="0", message="success", data=param)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{param_id}",
|
||||
response_model=StandardResponse[str],
|
||||
summary="删除系统参数",
|
||||
description="删除指定的系统参数",
|
||||
)
|
||||
async def delete_param(
|
||||
param_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""删除系统参数"""
|
||||
service = SysParamService(db)
|
||||
await service.delete_param(param_id)
|
||||
await db.commit()
|
||||
return StandardResponse(code="0", message="success", data=param_id)
|
||||
@@ -0,0 +1,17 @@
|
||||
from .health import HealthResponse
|
||||
from .models import (
|
||||
ModelType,
|
||||
CreateModelRequest,
|
||||
QueryModelRequest,
|
||||
ModelsResponse,
|
||||
ProviderItem,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"HealthResponse",
|
||||
"ModelType",
|
||||
"CreateModelRequest",
|
||||
"QueryModelRequest",
|
||||
"ModelsResponse",
|
||||
"ProviderItem",
|
||||
]
|
||||
@@ -0,0 +1,10 @@
|
||||
from pydantic import Field
|
||||
|
||||
from app.module.shared.schema import BaseResponseModel
|
||||
from app.module.shared.schema import StandardResponse
|
||||
|
||||
class HealthResponse(BaseResponseModel):
|
||||
"""健康检查响应模型"""
|
||||
status: str = Field(..., description="服务状态")
|
||||
service: str = Field(..., description="服务名称")
|
||||
version: str = Field(..., description="应用版本")
|
||||
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
模型配置 DTO:与 Java 版 t_models 接口保持一致。
|
||||
"""
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class ModelType(str, Enum):
|
||||
"""模型类型枚举,与 Java ModelType 一致。"""
|
||||
CHAT = "CHAT"
|
||||
EMBEDDING = "EMBEDDING"
|
||||
|
||||
|
||||
# --- 请求 DTO ---
|
||||
|
||||
|
||||
class CreateModelRequest(BaseModel):
|
||||
"""创建/更新模型配置请求,与 Java CreateModelRequest 一致。"""
|
||||
model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
|
||||
|
||||
modelName: str = Field(..., min_length=1, description="模型名称(如 qwen2)")
|
||||
provider: str = Field(..., min_length=1, description="模型提供商(如 Ollama、OpenAI、DeepSeek)")
|
||||
baseUrl: str = Field(..., min_length=1, description="API 基础地址")
|
||||
apiKey: Optional[str] = Field(None, description="API 密钥(无密钥则为空)")
|
||||
type: ModelType = Field(..., description="模型类型(如 chat、embedding)")
|
||||
isEnabled: Optional[bool] = Field(None, description="是否启用:1-启用,0-禁用")
|
||||
isDefault: Optional[bool] = Field(None, description="是否默认:1-默认,0-非默认")
|
||||
|
||||
|
||||
class QueryModelRequest(BaseModel):
|
||||
"""模型查询请求,与 Java QueryModelRequest + PagingQuery 一致。page 从 0 开始,size 默认 20。"""
|
||||
model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
|
||||
|
||||
page: int = Field(0, ge=0, description="页码,从 0 开始")
|
||||
size: int = Field(20, gt=0, le=2000, description="每页大小")
|
||||
provider: Optional[str] = Field(None, description="模型提供商")
|
||||
type: Optional[ModelType] = Field(None, description="模型类型")
|
||||
isEnabled: Optional[bool] = Field(None, description="是否启用")
|
||||
isDefault: Optional[bool] = Field(None, description="是否默认")
|
||||
|
||||
|
||||
# --- 响应 DTO ---
|
||||
|
||||
|
||||
class ModelsResponse(BaseModel):
|
||||
"""模型配置响应,与 Java Models 实体字段一致。"""
|
||||
model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
|
||||
|
||||
id: str
|
||||
modelName: str
|
||||
provider: str
|
||||
baseUrl: str
|
||||
apiKey: str = ""
|
||||
type: str # "CHAT" | "EMBEDDING"
|
||||
isEnabled: bool = True
|
||||
isDefault: bool = False
|
||||
createdAt: Optional[datetime] = None
|
||||
updatedAt: Optional[datetime] = None
|
||||
createdBy: Optional[str] = None
|
||||
updatedBy: Optional[str] = None
|
||||
|
||||
|
||||
class ProviderItem(BaseModel):
|
||||
"""厂商项,仅含 provider、baseUrl,与 Java getProviders() 返回结构一致。"""
|
||||
model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
|
||||
|
||||
provider: str
|
||||
baseUrl: str
|
||||
@@ -0,0 +1,51 @@
|
||||
"""
|
||||
System Parameter DTOs
|
||||
系统参数数据传输对象
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.module.shared.schema import BaseResponseModel
|
||||
|
||||
|
||||
class SysParamDto(BaseResponseModel):
|
||||
"""系统参数 DTO"""
|
||||
|
||||
id: str = Field(..., description="参数ID")
|
||||
param_value: str = Field(..., description="参数值")
|
||||
param_type: str = Field(
|
||||
default="string", description="参数类型(string、integer、boolean)"
|
||||
)
|
||||
option_list: Optional[str] = Field(None, description="选项列表(JSON格式)")
|
||||
description: Optional[str] = Field(None, description="参数描述")
|
||||
is_built_in: bool = Field(default=False, description="是否内置")
|
||||
can_modify: bool = Field(default=True, description="是否可修改")
|
||||
is_enabled: bool = Field(default=True, description="是否启用")
|
||||
created_at: Optional[datetime] = Field(None, description="创建时间")
|
||||
updated_at: Optional[datetime] = Field(None, description="更新时间")
|
||||
created_by: Optional[str] = Field(None, description="创建者")
|
||||
updated_by: Optional[str] = Field(None, description="更新者")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class UpdateParamValueRequest(BaseResponseModel):
|
||||
"""更新参数值请求"""
|
||||
|
||||
param_value: str = Field(..., description="参数值")
|
||||
|
||||
|
||||
class CreateSysParamRequest(BaseResponseModel):
|
||||
"""创建系统参数请求"""
|
||||
|
||||
id: str = Field(..., description="参数ID")
|
||||
param_value: str = Field(..., description="参数值")
|
||||
param_type: str = Field(default="string", description="参数类型")
|
||||
option_list: Optional[str] = Field(None, description="选项列表(JSON格式)")
|
||||
description: Optional[str] = Field(None, description="参数描述")
|
||||
is_built_in: bool = Field(default=False, description="是否内置")
|
||||
can_modify: bool = Field(default=True, description="是否可修改")
|
||||
is_enabled: bool = Field(default=True, description="是否启用")
|
||||
@@ -0,0 +1,16 @@
|
||||
"""通用系统服务:仅保留与 DB 直接相关的查询。LLM 创建与调用统一使用 app.core.llm.LLMFactory。"""
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.db.models.models import Models
|
||||
|
||||
|
||||
async def get_model_by_id(db: AsyncSession, model_id: str) -> Optional[Models]:
|
||||
"""根据模型ID获取未删除的 Models 记录。"""
|
||||
q = select(Models).where(Models.id == model_id).where(
|
||||
(Models.is_deleted == False) | (Models.is_deleted.is_(None))
|
||||
)
|
||||
result = await db.execute(q)
|
||||
return result.scalar_one_or_none()
|
||||
@@ -0,0 +1,173 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
from collections.abc import Callable, Iterable
|
||||
from pathlib import Path
|
||||
from typing import NamedTuple
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.logging import get_logger
|
||||
from app.module.shared.schedule import Scheduler
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class DiskUsage(NamedTuple):
|
||||
total: int
|
||||
used: int
|
||||
free: int
|
||||
|
||||
|
||||
class LogPvcMonitor:
|
||||
"""Monitor log PVC usage and delete oldest log-like files when it is full."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
root_path: str | Path,
|
||||
threshold: float,
|
||||
delete_batch_size: int,
|
||||
file_suffixes: str | Iterable[str],
|
||||
disk_usage: Callable[[Path], DiskUsage] = shutil.disk_usage,
|
||||
unlink_file: Callable[[Path], None] | None = None,
|
||||
) -> None:
|
||||
self.root_path = Path(root_path)
|
||||
self.threshold = threshold
|
||||
self.delete_batch_size = max(1, delete_batch_size)
|
||||
self.file_suffixes = self._normalize_suffixes(file_suffixes)
|
||||
self._disk_usage = disk_usage
|
||||
self._unlink_file = unlink_file or (lambda path: path.unlink())
|
||||
|
||||
def cleanup_if_needed(self) -> int:
|
||||
usage_ratio = self._get_usage_ratio()
|
||||
if usage_ratio is None:
|
||||
return 0
|
||||
if usage_ratio < self.threshold:
|
||||
logger.debug(
|
||||
"Log PVC usage %.2f%% is below threshold %.2f%%",
|
||||
usage_ratio * 100,
|
||||
self.threshold * 100,
|
||||
)
|
||||
return 0
|
||||
|
||||
total_deleted = 0
|
||||
logger.warning(
|
||||
"Log PVC usage %.2f%% reached threshold %.2f%%, starting cleanup under %s",
|
||||
usage_ratio * 100,
|
||||
self.threshold * 100,
|
||||
self.root_path,
|
||||
)
|
||||
|
||||
while usage_ratio >= self.threshold:
|
||||
candidates = self._find_candidates()
|
||||
if not candidates:
|
||||
logger.warning(
|
||||
"Log PVC usage remains %.2f%%, but no matching log files are available under %s",
|
||||
usage_ratio * 100,
|
||||
self.root_path,
|
||||
)
|
||||
break
|
||||
|
||||
deleted_in_batch = self._delete_batch(candidates[: self.delete_batch_size])
|
||||
total_deleted += deleted_in_batch
|
||||
if deleted_in_batch == 0:
|
||||
logger.warning("Log PVC cleanup made no progress; stopping this cleanup run")
|
||||
break
|
||||
|
||||
next_usage_ratio = self._get_usage_ratio()
|
||||
if next_usage_ratio is None:
|
||||
break
|
||||
usage_ratio = next_usage_ratio
|
||||
|
||||
if total_deleted:
|
||||
logger.info("Deleted %s files during log PVC cleanup", total_deleted)
|
||||
return total_deleted
|
||||
|
||||
def _find_candidates(self) -> list[Path]:
|
||||
candidates: list[tuple[float, str, Path]] = []
|
||||
if not self.file_suffixes:
|
||||
return []
|
||||
|
||||
try:
|
||||
paths = self.root_path.rglob("*")
|
||||
for path in paths:
|
||||
try:
|
||||
if path.is_symlink() or not path.is_file():
|
||||
continue
|
||||
if path.suffix.lower().lstrip(".") not in self.file_suffixes:
|
||||
continue
|
||||
stat_info = path.stat()
|
||||
except OSError as exc:
|
||||
logger.warning("Skipping inaccessible log file candidate %s: %s", path, exc)
|
||||
continue
|
||||
candidates.append((stat_info.st_mtime, str(path), path))
|
||||
except OSError as exc:
|
||||
logger.warning("Failed to scan log PVC path %s: %s", self.root_path, exc)
|
||||
return []
|
||||
|
||||
candidates.sort(key=lambda item: (item[0], item[1]))
|
||||
return [path for _, _, path in candidates]
|
||||
|
||||
def _delete_batch(self, candidates: list[Path]) -> int:
|
||||
deleted = 0
|
||||
for path in candidates:
|
||||
try:
|
||||
self._unlink_file(path)
|
||||
deleted += 1
|
||||
logger.info("Deleted old log file %s", path)
|
||||
except FileNotFoundError:
|
||||
logger.info("Log file already deleted before cleanup: %s", path)
|
||||
except OSError as exc:
|
||||
logger.warning("Failed to delete old log file %s: %s", path, exc)
|
||||
return deleted
|
||||
|
||||
def _get_usage_ratio(self) -> float | None:
|
||||
try:
|
||||
usage = self._disk_usage(self.root_path)
|
||||
except OSError as exc:
|
||||
logger.warning("Failed to inspect log PVC path %s: %s", self.root_path, exc)
|
||||
return None
|
||||
|
||||
if usage.total <= 0:
|
||||
logger.warning("Log PVC path %s has invalid total size: %s", self.root_path, usage.total)
|
||||
return None
|
||||
return usage.used / usage.total
|
||||
|
||||
@staticmethod
|
||||
def _normalize_suffixes(file_suffixes: str | Iterable[str]) -> set[str]:
|
||||
if isinstance(file_suffixes, str):
|
||||
raw_suffixes = file_suffixes.split(",")
|
||||
else:
|
||||
raw_suffixes = file_suffixes
|
||||
return {
|
||||
suffix.strip().lower().lstrip(".")
|
||||
for suffix in raw_suffixes
|
||||
if suffix and suffix.strip().lstrip(".")
|
||||
}
|
||||
|
||||
|
||||
def schedule_log_pvc_monitor(scheduler: Scheduler) -> None:
|
||||
if not settings.log_pvc_monitor_enabled:
|
||||
logger.info("Log PVC monitor is disabled")
|
||||
return
|
||||
|
||||
monitor = LogPvcMonitor(
|
||||
root_path=settings.log_pvc_monitor_path,
|
||||
threshold=settings.log_pvc_monitor_threshold,
|
||||
delete_batch_size=settings.log_pvc_monitor_delete_batch_size,
|
||||
file_suffixes=settings.log_pvc_monitor_file_suffixes,
|
||||
)
|
||||
interval_seconds = max(1, settings.log_pvc_monitor_interval_seconds)
|
||||
scheduler.add_interval_job(
|
||||
job_id="system:log-pvc-monitor",
|
||||
seconds=interval_seconds,
|
||||
func=monitor.cleanup_if_needed,
|
||||
replace_existing=True,
|
||||
max_instances=1,
|
||||
coalesce=True,
|
||||
misfire_grace_time=60,
|
||||
)
|
||||
logger.info(
|
||||
"Scheduled log PVC monitor for %s every %s seconds",
|
||||
settings.log_pvc_monitor_path,
|
||||
interval_seconds,
|
||||
)
|
||||
@@ -0,0 +1,249 @@
|
||||
import math
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import Depends, HTTPException
|
||||
from sqlalchemy import select, func, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.module.shared.llm import LLMFactory
|
||||
from app.core.logging import get_logger
|
||||
from app.db.datascope import DataScopeHandle, SYSTEM_USER
|
||||
from app.db.models.models import Models
|
||||
from app.db.session import get_db
|
||||
from app.module.shared.schema import PaginatedData
|
||||
from app.module.system.schema.models import (
|
||||
CreateModelRequest,
|
||||
QueryModelRequest,
|
||||
ModelsResponse,
|
||||
ProviderItem,
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# 固定厂商列表
|
||||
PROVIDERS = [
|
||||
ProviderItem(provider="ModelEngine", baseUrl="http://nginxservice/open/router/v1"),
|
||||
ProviderItem(provider="Ollama", baseUrl="http://localhost:11434"),
|
||||
ProviderItem(provider="OpenAI", baseUrl="https://api.openai.com/v1"),
|
||||
ProviderItem(provider="DeepSeek", baseUrl="https://api.deepseek.com/v1"),
|
||||
ProviderItem(provider="火山方舟", baseUrl="https://ark.cn-beijing.volces.com/api/v3"),
|
||||
ProviderItem(provider="阿里云百炼", baseUrl="https://dashscope.aliyuncs.com/compatible-mode/v1"),
|
||||
ProviderItem(provider="硅基流动", baseUrl="https://api.siliconflow.cn/v1"),
|
||||
ProviderItem(provider="智谱AI", baseUrl="https://open.bigmodel.cn/api/paas/v4"),
|
||||
ProviderItem(provider="自定义模型", baseUrl=""),
|
||||
]
|
||||
|
||||
|
||||
def _orm_to_response(row: Models) -> ModelsResponse:
|
||||
return ModelsResponse(
|
||||
id=row.id,
|
||||
modelName=row.model_name,
|
||||
provider=row.provider,
|
||||
baseUrl=row.base_url,
|
||||
apiKey=row.api_key or "",
|
||||
type=row.type or "CHAT",
|
||||
isEnabled=bool(row.is_enabled) if row.is_enabled is not None else True,
|
||||
isDefault=bool(row.is_default) if row.is_default is not None else False,
|
||||
createdAt=row.created_at,
|
||||
updatedAt=row.updated_at,
|
||||
createdBy=row.created_by,
|
||||
updatedBy=row.updated_by,
|
||||
)
|
||||
|
||||
|
||||
class ModelsService:
|
||||
"""模型配置服务:db 通过 FastAPI Depends(get_db) 注入,不在路由中传递。"""
|
||||
|
||||
def __init__(self, db: AsyncSession = Depends(get_db)):
|
||||
self.db = db
|
||||
|
||||
async def get_providers(self) -> list[ProviderItem]:
|
||||
"""返回固定厂商列表,与 Java getProviders() 一致。"""
|
||||
return list(PROVIDERS)
|
||||
|
||||
async def get_models(self, q: QueryModelRequest) -> PaginatedData[ModelsResponse]:
|
||||
"""分页查询,支持 provider/type/isEnabled/isDefault;排除已删除;page 从 0 开始。"""
|
||||
query = select(Models).where(
|
||||
(Models.is_deleted == False) | (Models.is_deleted.is_(None))
|
||||
)
|
||||
if q.provider is not None and q.provider != "":
|
||||
query = query.where(Models.provider == q.provider)
|
||||
if q.type is not None:
|
||||
query = query.where(Models.type == q.type.value)
|
||||
if q.isEnabled is not None:
|
||||
query = query.where(Models.is_enabled == q.isEnabled)
|
||||
if q.isDefault is not None:
|
||||
query = query.where(Models.is_default == q.isDefault)
|
||||
|
||||
total = (await self.db.execute(select(func.count()).select_from(query.subquery()))).scalar_one()
|
||||
size = max(1, min(500, q.size))
|
||||
offset = max(0, q.page) * size
|
||||
rows = (
|
||||
await self.db.execute(
|
||||
query.order_by(Models.created_at.desc()).offset(offset).limit(size)
|
||||
)
|
||||
).scalars().all()
|
||||
total_pages = math.ceil(total / size) if total else 0
|
||||
current = max(0, q.page) + 1
|
||||
return PaginatedData(
|
||||
page=current,
|
||||
size=size,
|
||||
total_elements=total,
|
||||
total_pages=total_pages,
|
||||
content=[_orm_to_response(r) for r in rows],
|
||||
)
|
||||
|
||||
async def get_model_detail(self, model_id: str) -> ModelsResponse:
|
||||
"""获取模型详情,已删除或不存在则 404。"""
|
||||
query = select(Models).where(Models.id == model_id).where(
|
||||
(Models.is_deleted == False) | (Models.is_deleted.is_(None))
|
||||
)
|
||||
r = (await self.db.execute(query)).scalar_one_or_none()
|
||||
if not r:
|
||||
raise HTTPException(status_code=404, detail="模型配置不存在")
|
||||
return _orm_to_response(r)
|
||||
|
||||
async def create_model(self, req: CreateModelRequest) -> ModelsResponse:
|
||||
"""创建模型:健康检查后 saveAndSetDefault;isEnabled 恒为 True。
|
||||
若 uk_model_provider 已存在且 is_deleted=True,则恢复并更新相关字段。"""
|
||||
try:
|
||||
LLMFactory.check_health(req.modelName, req.baseUrl, req.apiKey, req.type.value)
|
||||
except Exception as e:
|
||||
logger.error("Model health check failed: model=%s type=%s err=%s", req.modelName, req.type, e)
|
||||
raise HTTPException(status_code=400, detail="模型健康检查失败") from e
|
||||
|
||||
# 检查 uk_model_provider (model_name, provider, created_by) 是否存在且已删除
|
||||
effective_user = (DataScopeHandle.get_user_info() or "").strip() or SYSTEM_USER
|
||||
created_by_match = (
|
||||
(Models.created_by == effective_user)
|
||||
| ((Models.created_by.is_(None)) & (effective_user == SYSTEM_USER))
|
||||
)
|
||||
deleted_row = (
|
||||
await self.db.execute(
|
||||
select(Models).where(
|
||||
Models.model_name == req.modelName,
|
||||
Models.provider == req.provider,
|
||||
created_by_match,
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
|
||||
if deleted_row and deleted_row.is_deleted:
|
||||
# 恢复并更新
|
||||
entity = deleted_row
|
||||
entity.base_url = req.baseUrl
|
||||
entity.api_key = req.apiKey or ""
|
||||
entity.type = req.type.value
|
||||
entity.is_enabled = True
|
||||
entity.is_deleted = False
|
||||
entity.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
|
||||
want_default = req.isDefault if req.isDefault is not None else False
|
||||
if want_default:
|
||||
await self.db.execute(
|
||||
update(Models)
|
||||
.where(Models.type == req.type.value, Models.is_default == True)
|
||||
.values(is_default=False)
|
||||
)
|
||||
entity.is_default = want_default
|
||||
|
||||
self.db.add(entity)
|
||||
await self.db.commit()
|
||||
await self.db.refresh(entity)
|
||||
return _orm_to_response(entity)
|
||||
|
||||
# 正常创建
|
||||
existing = (
|
||||
await self.db.execute(
|
||||
select(Models).where(
|
||||
(Models.is_deleted == False) | (Models.is_deleted.is_(None)),
|
||||
Models.type == req.type.value,
|
||||
Models.is_default == True,
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
|
||||
is_default: bool
|
||||
if existing is None:
|
||||
is_default = True
|
||||
else:
|
||||
await self.db.execute(
|
||||
update(Models)
|
||||
.where(Models.type == req.type.value, Models.is_default == True)
|
||||
.values(is_default=False)
|
||||
)
|
||||
is_default = req.isDefault if req.isDefault is not None else False
|
||||
|
||||
now = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
entity = Models(
|
||||
id=str(uuid.uuid4()),
|
||||
model_name=req.modelName,
|
||||
provider=req.provider,
|
||||
base_url=req.baseUrl,
|
||||
api_key=req.apiKey or "",
|
||||
type=req.type.value,
|
||||
is_enabled=True,
|
||||
is_default=is_default,
|
||||
is_deleted=False,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
self.db.add(entity)
|
||||
await self.db.commit()
|
||||
await self.db.refresh(entity)
|
||||
return _orm_to_response(entity)
|
||||
|
||||
async def update_model(self, model_id: str, req: CreateModelRequest) -> ModelsResponse:
|
||||
"""更新模型:存在性校验、健康检查后 updateAndSetDefault;isEnabled 恒为 True。"""
|
||||
res = await self.db.execute(
|
||||
select(Models).where(Models.id == model_id).where(
|
||||
(Models.is_deleted == False) | (Models.is_deleted.is_(None))
|
||||
)
|
||||
)
|
||||
entity = res.scalar_one_or_none()
|
||||
if not entity:
|
||||
raise HTTPException(status_code=404, detail="模型配置不存在")
|
||||
|
||||
try:
|
||||
LLMFactory.check_health(req.modelName, req.baseUrl, req.apiKey, req.type.value)
|
||||
except Exception as e:
|
||||
logger.error("Model health check failed: model=%s type=%s err=%s", req.modelName, req.type, e)
|
||||
raise HTTPException(status_code=400, detail="模型健康检查失败") from e
|
||||
|
||||
entity.model_name = req.modelName
|
||||
entity.provider = req.provider
|
||||
entity.base_url = req.baseUrl
|
||||
entity.api_key = req.apiKey or ""
|
||||
entity.type = req.type.value
|
||||
entity.is_enabled = True
|
||||
|
||||
want_default = req.isDefault if req.isDefault is not None else False
|
||||
if (entity.is_default is not True) and want_default:
|
||||
await self.db.execute(
|
||||
update(Models)
|
||||
.where(Models.type == req.type.value, Models.is_default == True)
|
||||
.values(is_default=False)
|
||||
)
|
||||
entity.is_default = want_default
|
||||
entity.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
|
||||
await self.db.commit()
|
||||
await self.db.refresh(entity)
|
||||
return _orm_to_response(entity)
|
||||
|
||||
async def delete_model(self, model_id: str) -> None:
|
||||
"""软删除模型配置。"""
|
||||
entity = (
|
||||
await self.db.execute(
|
||||
select(Models).where(Models.id == model_id).where(
|
||||
(Models.is_deleted == False) | (Models.is_deleted.is_(None))
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if not entity:
|
||||
raise HTTPException(status_code=404, detail="模型配置不存在")
|
||||
entity.is_deleted = True
|
||||
entity.updated_at = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||
await self.db.commit()
|
||||
await self.db.refresh(entity)
|
||||
@@ -0,0 +1,141 @@
|
||||
"""
|
||||
System Parameter Service
|
||||
系统参数服务
|
||||
"""
|
||||
|
||||
from typing import List, Optional
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.logging import get_logger
|
||||
from app.core.exception import BusinessError, ErrorCodes
|
||||
from app.db.models.sys_param import SysParam
|
||||
from app.module.system.schema.sys_param import (
|
||||
SysParamDto,
|
||||
UpdateParamValueRequest,
|
||||
CreateSysParamRequest,
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class SysParamService:
|
||||
"""系统参数服务"""
|
||||
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def list_params(self) -> List[SysParamDto]:
|
||||
"""获取系统参数列表"""
|
||||
query = select(SysParam).order_by(SysParam.param_type, SysParam.id)
|
||||
result = await self.db.execute(query)
|
||||
params = result.scalars().all()
|
||||
return [self._orm_to_response(p) for p in params]
|
||||
|
||||
async def get_param_by_id(self, param_id: str) -> Optional[SysParamDto]:
|
||||
"""根据ID获取系统参数"""
|
||||
query = select(SysParam).where(SysParam.id == param_id)
|
||||
result = await self.db.execute(query)
|
||||
param = result.scalar_one_or_none()
|
||||
if not param:
|
||||
return None
|
||||
return self._orm_to_response(param)
|
||||
|
||||
async def update_param_value(self, param_id: str, param_value: str) -> SysParamDto:
|
||||
"""更新系统参数值"""
|
||||
# 查询参数
|
||||
query = select(SysParam).where(SysParam.id == param_id)
|
||||
result = await self.db.execute(query)
|
||||
param = result.scalar_one_or_none()
|
||||
|
||||
if not param:
|
||||
raise BusinessError(
|
||||
ErrorCodes.NOT_FOUND, f"System parameter {param_id} not found"
|
||||
)
|
||||
|
||||
# 检查是否可修改
|
||||
if not param.can_modify:
|
||||
raise BusinessError(
|
||||
ErrorCodes.OPERATION_FAILED,
|
||||
f"System parameter {param_id} cannot be modified",
|
||||
)
|
||||
|
||||
# 更新值
|
||||
param.param_value = param_value
|
||||
await self.db.commit()
|
||||
await self.db.refresh(param)
|
||||
|
||||
logger.info(f"Updated system parameter {param_id} = {param_value}")
|
||||
return self._orm_to_response(param)
|
||||
|
||||
async def create_param(self, request: CreateSysParamRequest) -> SysParamDto:
|
||||
"""创建系统参数"""
|
||||
# 检查是否已存在
|
||||
existing = await self.get_param_by_id(request.id)
|
||||
if existing:
|
||||
raise BusinessError(
|
||||
ErrorCodes.OPERATION_FAILED,
|
||||
f"System parameter {request.id} already exists",
|
||||
)
|
||||
|
||||
# 创建新参数
|
||||
param = SysParam(
|
||||
id=request.id,
|
||||
param_value=request.param_value,
|
||||
param_type=request.param_type,
|
||||
option_list=request.option_list,
|
||||
description=request.description,
|
||||
is_built_in=request.is_built_in,
|
||||
can_modify=request.can_modify,
|
||||
is_enabled=request.is_enabled,
|
||||
)
|
||||
|
||||
self.db.add(param)
|
||||
await self.db.commit()
|
||||
await self.db.refresh(param)
|
||||
|
||||
logger.info(f"Created system parameter {request.id}")
|
||||
return self._orm_to_response(param)
|
||||
|
||||
async def delete_param(self, param_id: str) -> None:
|
||||
"""删除系统参数"""
|
||||
# 查询参数
|
||||
query = select(SysParam).where(SysParam.id == param_id)
|
||||
result = await self.db.execute(query)
|
||||
param = result.scalar_one_or_none()
|
||||
|
||||
if not param:
|
||||
raise BusinessError(
|
||||
ErrorCodes.NOT_FOUND, f"System parameter {param_id} not found"
|
||||
)
|
||||
|
||||
# 检查是否为内置参数
|
||||
if param.is_built_in:
|
||||
raise BusinessError(
|
||||
ErrorCodes.OPERATION_FAILED,
|
||||
f"Cannot delete built-in system parameter {param_id}",
|
||||
)
|
||||
|
||||
# 删除参数
|
||||
await self.db.delete(param)
|
||||
await self.db.commit()
|
||||
|
||||
logger.info(f"Deleted system parameter {param_id}")
|
||||
|
||||
@staticmethod
|
||||
def _orm_to_response(row: SysParam) -> SysParamDto:
|
||||
"""Convert ORM model to response DTO"""
|
||||
return SysParamDto(
|
||||
id=row.id,
|
||||
param_value=row.param_value,
|
||||
param_type=row.param_type or "string",
|
||||
option_list=row.option_list,
|
||||
description=row.description,
|
||||
is_built_in=bool(row.is_built_in) if row.is_built_in is not None else False,
|
||||
can_modify=bool(row.can_modify) if row.can_modify is not None else True,
|
||||
is_enabled=bool(row.is_enabled) if row.is_enabled is not None else True,
|
||||
created_at=row.created_at,
|
||||
updated_at=row.updated_at,
|
||||
created_by=row.created_by,
|
||||
updated_by=row.updated_by,
|
||||
)
|
||||
Reference in New Issue
Block a user