5d2bced39f
- PROGRESS.md: update to 2026-05-29 with security hardening (T-005), 4-frontend architecture, AI provider refactoring, discovery features, landing page/referral/quota, desktop layout, admin AI management - AGENTS.md: add AI provider list (Alibaba/NVIDIA, removed Claude/DeepL/Local), DB-driven config, CSRF/rate-limit/CORS notes, admin_ai reload quirk - .env.example: sync with actual config, replace deprecated providers with current Sensenova/OpencodeGo/NVIDIA/Spark/Alibaba - docs/PROJECT_STATUS.md: archive (fully superseded by PROGRESS.md) - Remove generated JS files (_bing_search.js, _batch_search.js) - Remove empty directories (data/corpus, data/models) - Remove backend/.coverage (test artifact) - Fix services/.gitignore to cover _bing_search.js - Include pending AI provider DB admin feature (admin_ai, AIProvider model, AIProviders.vue, migration) and T-008 test report
170 lines
5.3 KiB
Python
170 lines
5.3 KiB
Python
from typing import Optional
|
|
from pydantic import BaseModel
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select
|
|
from app.database import get_db
|
|
from app.api.v1.deps import get_current_user
|
|
from app.models.ai_provider import AIProvider
|
|
from app.ai.router import get_ai_router
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
async def require_admin(current_user: dict = Depends(get_current_user)) -> dict:
|
|
if current_user.get("role") != "admin":
|
|
raise HTTPException(status_code=403, detail="Admin access required")
|
|
return current_user
|
|
|
|
|
|
class AIProviderCreate(BaseModel):
|
|
name: str
|
|
provider_type: str
|
|
api_key: Optional[str] = None
|
|
api_secret: Optional[str] = None
|
|
base_url: Optional[str] = None
|
|
model_name: str = "deepseek-v4-flash"
|
|
extra_config: Optional[dict] = None
|
|
priority: int = 0
|
|
enabled: bool = True
|
|
|
|
|
|
class AIProviderUpdate(BaseModel):
|
|
name: Optional[str] = None
|
|
api_key: Optional[str] = None
|
|
api_secret: Optional[str] = None
|
|
base_url: Optional[str] = None
|
|
model_name: Optional[str] = None
|
|
extra_config: Optional[dict] = None
|
|
priority: Optional[int] = None
|
|
enabled: Optional[bool] = None
|
|
|
|
|
|
PROVIDER_TYPE_LABELS = {
|
|
"sensenova": "Sensenova (商汤)",
|
|
"opencode_go": "OpencodeGo",
|
|
"nvidia": "NVIDIA",
|
|
"spark": "讯飞 Spark",
|
|
"alibaba-mt": "阿里翻译",
|
|
}
|
|
|
|
|
|
@router.get("/ai-providers")
|
|
async def list_providers(
|
|
page: int = Query(1, ge=1),
|
|
size: int = Query(50, ge=1, le=100),
|
|
_: dict = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
result = await db.execute(
|
|
select(AIProvider).order_by(AIProvider.priority).offset((page - 1) * size).limit(size)
|
|
)
|
|
providers = result.scalars().all()
|
|
total_result = await db.execute(select(AIProvider))
|
|
total = len(total_result.scalars().all())
|
|
return {
|
|
"items": [
|
|
{
|
|
"id": str(p.id),
|
|
"name": p.name,
|
|
"provider_type": p.provider_type,
|
|
"type_label": PROVIDER_TYPE_LABELS.get(p.provider_type, p.provider_type),
|
|
"api_key": p.api_key[:8] + "..." if p.api_key and len(p.api_key) > 8 else (p.api_key or ""),
|
|
"api_secret": bool(p.api_secret),
|
|
"base_url": p.base_url,
|
|
"model_name": p.model_name,
|
|
"extra_config": p.extra_config,
|
|
"priority": p.priority,
|
|
"enabled": p.enabled,
|
|
"created_at": p.created_at.isoformat() if p.created_at else None,
|
|
"updated_at": p.updated_at.isoformat() if p.updated_at else None,
|
|
}
|
|
for p in providers
|
|
],
|
|
"total": total,
|
|
"page": page,
|
|
"size": size,
|
|
}
|
|
|
|
|
|
@router.post("/ai-providers")
|
|
async def create_provider(
|
|
data: AIProviderCreate,
|
|
_: dict = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
provider = AIProvider(
|
|
name=data.name,
|
|
provider_type=data.provider_type,
|
|
api_key=data.api_key,
|
|
api_secret=data.api_secret,
|
|
base_url=data.base_url,
|
|
model_name=data.model_name,
|
|
extra_config=data.extra_config or {},
|
|
priority=data.priority,
|
|
enabled=data.enabled,
|
|
)
|
|
db.add(provider)
|
|
await db.commit()
|
|
await db.refresh(provider)
|
|
await get_ai_router().reload_from_db(db)
|
|
return {"id": str(provider.id), "message": "AI provider created"}
|
|
|
|
|
|
@router.put("/ai-providers/{provider_id}")
|
|
async def update_provider(
|
|
provider_id: str,
|
|
data: AIProviderUpdate,
|
|
_: dict = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
result = await db.execute(select(AIProvider).where(AIProvider.id == provider_id))
|
|
provider = result.scalar_one_or_none()
|
|
if not provider:
|
|
raise HTTPException(status_code=404, detail="AI provider not found")
|
|
|
|
update_data = data.model_dump(exclude_unset=True)
|
|
for key, value in update_data.items():
|
|
setattr(provider, key, value)
|
|
await db.commit()
|
|
await db.refresh(provider)
|
|
|
|
await get_ai_router().reload_from_db(db)
|
|
return {"id": str(provider.id), "message": "AI provider updated"}
|
|
|
|
|
|
@router.delete("/ai-providers/{provider_id}")
|
|
async def delete_provider(
|
|
provider_id: str,
|
|
_: dict = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
result = await db.execute(select(AIProvider).where(AIProvider.id == provider_id))
|
|
provider = result.scalar_one_or_none()
|
|
if not provider:
|
|
raise HTTPException(status_code=404, detail="AI provider not found")
|
|
await db.delete(provider)
|
|
await db.commit()
|
|
|
|
await get_ai_router().reload_from_db(db)
|
|
return {"message": "AI provider deleted"}
|
|
|
|
|
|
@router.post("/ai-providers/reload")
|
|
async def reload_providers(
|
|
_: dict = Depends(require_admin),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
count = await get_ai_router().reload_from_db(db)
|
|
return {"message": f"AI providers reloaded, {count} providers active"}
|
|
|
|
|
|
@router.get("/ai-providers/status")
|
|
async def get_provider_status(
|
|
_: dict = Depends(require_admin),
|
|
):
|
|
router = get_ai_router()
|
|
active = list(router.providers.keys())
|
|
routing = router.routing_rules
|
|
return {"active_providers": active, "routing_rules": routing, "provider_count": len(active)}
|