Files
trade-assistant/backend/app/api/v1/admin_ai.py
T
TradeMate Dev 5d2bced39f docs: update project docs and clean up redundant files
- 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
2026-05-29 11:15:33 +08:00

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)}