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
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
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)}
|
||||
Reference in New Issue
Block a user