chore: post-deployment cleanup and docs update

- Make AI routing rules DB-driven (read from system_configs, removed from config.py)
- Add translation quota tracking to LLM translation (OpenAIProvider)
- Add Alibaba MT ECS RAM role support (STS token, no AccessKey needed)
- Fix admin sidebar link for AI模型配置 page
- Fix Quota.vue API path (quotas → translation-quotas)
- Fix login auto-redirect to dashboard
- Add provider dropdown selects to AI routing config UI
- Clean up stale ai_provider_* system_configs records
- Remove OpencodeGo, Spark providers (code + DB)
- Update deploy config: nginx port 8000, systemd cwd
This commit is contained in:
TradeMate Dev
2026-06-02 15:40:02 +08:00
parent fa3050a17c
commit f17a6ccbac
28 changed files with 1140 additions and 209 deletions
+59 -30
View File
@@ -1,17 +1,26 @@
from typing import Dict, Any, Optional, List
from app.ai.base import AIProvider
from app.ai.providers import SparkProvider, SensenovaProvider, OpencodeGoProvider, NvidiaProvider, AlibabaMTProvider
from app.config import settings
from app.ai.providers import SensenovaProvider, NvidiaProvider, AlibabaMTProvider
from app.ai.trade_corpus import TradeCorpus
from app.config import settings
import logging
logger = logging.getLogger(__name__)
DEFAULT_ROUTING: Dict[str, dict] = {
"translate": {"primary": "sensenova", "fallback": ["alibaba-mt", "nvidia"]},
"reply": {"primary": "sensenova", "fallback": ["nvidia"]},
"marketing": {"primary": "sensenova", "fallback": ["nvidia"]},
"extract": {"primary": "sensenova", "fallback": ["nvidia"]},
"quotation": {"primary": "sensenova", "fallback": ["nvidia"]},
"chat": {"primary": "sensenova", "fallback": ["nvidia"]},
}
class AIRouter:
def __init__(self):
self.providers: Dict[str, AIProvider] = {}
self.routing_rules = settings.AI_ROUTING
self.routing_rules = dict(DEFAULT_ROUTING)
self.corpus = TradeCorpus()
async def reload_from_db(self, db_session) -> int:
@@ -38,8 +47,47 @@ class AIRouter:
else:
logger.warning("No enabled AI providers found in DB")
await self._load_routing_rules(db_session)
return len(rows)
async def _load_routing_rules(self, db_session):
from app.models.system_config import SystemConfig
from sqlalchemy import select
# Try consolidated key first
result = await db_session.execute(
select(SystemConfig).where(SystemConfig.key == "ai_routing")
)
cfg = result.scalar_one_or_none()
if cfg and isinstance(cfg.value, dict):
self.routing_rules = {**DEFAULT_ROUTING, **cfg.value}
logger.info("Loaded routing rules from system_configs (ai_routing)")
return
# Fallback: load individual per-task keys
task_keys = {
"translate": "ai_provider_translate",
"reply": "ai_provider_reply",
"marketing": "ai_provider_marketing",
"extract": "ai_provider_extract",
"quotation": "ai_provider_quotation",
}
loaded = {}
for task, key in task_keys.items():
result = await db_session.execute(
select(SystemConfig).where(SystemConfig.key == key)
)
cfg = result.scalar_one_or_none()
if cfg and isinstance(cfg.value, dict):
loaded[task] = cfg.value
if loaded:
self.routing_rules = {**DEFAULT_ROUTING, **loaded}
logger.info(f"Loaded routing rules from system_configs (individual keys): {list(loaded.keys())}")
else:
self.routing_rules = dict(DEFAULT_ROUTING)
logger.info("No routing rules in system_configs, using defaults")
async def seed_from_env(self, db_session) -> int:
from app.models.ai_provider import AIProvider
@@ -53,34 +101,19 @@ class AIRouter:
base_url=settings.SENSENOVA_BASE_URL,
model_name=settings.SENSENOVA_MODEL, priority=0, enabled=True,
))
if settings.OPENCODE_GO_API_KEY:
seeds.append(AIProvider(
name="OpencodeGo", provider_type="opencode_go",
api_key=settings.OPENCODE_GO_API_KEY,
base_url=settings.OPENCODE_GO_BASE_URL,
model_name=settings.OPENCODE_GO_MODEL, priority=1, enabled=True,
))
if settings.NVIDIA_API_KEY:
seeds.append(AIProvider(
name="NVIDIA", provider_type="nvidia",
api_key=settings.NVIDIA_API_KEY,
base_url=settings.NVIDIA_BASE_URL,
model_name=settings.NVIDIA_MODEL, priority=2, enabled=True,
))
if settings.IFLYTEK_API_KEY:
seeds.append(AIProvider(
name="讯飞 Spark", provider_type="spark",
api_key=settings.IFLYTEK_API_KEY,
base_url=settings.IFLYTEK_API_BASE,
model_name=settings.IFLYTEK_MODEL, priority=3, enabled=True,
))
if settings.ALIBABA_ACCESS_KEY_ID and settings.ALIBABA_ACCESS_KEY_SECRET:
seeds.append(AIProvider(
name="阿里翻译", provider_type="alibaba-mt",
api_key=settings.ALIBABA_ACCESS_KEY_ID,
api_secret=settings.ALIBABA_ACCESS_KEY_SECRET,
model_name="alibaba-mt", priority=4, enabled=True,
model_name=settings.NVIDIA_MODEL, priority=1, enabled=True,
))
seeds.append(AIProvider(
name="阿里翻译", provider_type="alibaba-mt",
api_key=settings.ALIBABA_ACCESS_KEY_ID or "",
api_secret=settings.ALIBABA_ACCESS_KEY_SECRET or "",
model_name="alibaba-mt", priority=3, enabled=True,
))
for p in seeds:
db_session.add(p)
@@ -99,12 +132,8 @@ class AIRouter:
t = p.provider_type
if t == "sensenova":
return SensenovaProvider(api_key=p.api_key, model=p.model_name, base_url=p.base_url)
elif t == "opencode_go":
return OpencodeGoProvider(api_key=p.api_key, model=p.model_name, base_url=p.base_url)
elif t == "nvidia":
return NvidiaProvider(api_key=p.api_key, model=p.model_name, base_url=p.base_url)
elif t == "spark":
return SparkProvider(api_key=p.api_key, model=p.model_name, base_url=p.base_url)
elif t == "alibaba-mt":
return AlibabaMTProvider(access_key_id=p.api_key, access_key_secret=p.api_secret or "")
else:
@@ -117,7 +146,7 @@ class AIRouter:
def get_providers_for_task(self, task_type: str) -> List[AIProvider]:
rules = self.routing_rules.get(
task_type,
{"primary": "sensenova", "fallback": ["opencode_go"]},
{"primary": "sensenova", "fallback": ["nvidia"]},
)
ordered = []
seen = set()