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:
TradeMate Dev
2026-05-29 11:15:33 +08:00
parent c04fa2c19f
commit 5d2bced39f
31 changed files with 1933 additions and 816 deletions
+169
View File
@@ -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)}