feat: WeChat Pay integration, translation quota management, login UX fixes
- WeChat Pay APIv3 integration (JSAPI + Native) with cert-based auth - TranslationQuota model + admin management UI (配额 tab) - Alibaba MT provider now checks quota before translation - Fix: admin tabs scrollable on mobile, remove header-card - Fix: profile/login navigation - logout stays on profile, login returns to profile - Fix: login form now visible by default (no extra click to show) - Fix: home page translate link uses navigateTo (was switchTab to non-tabBar page) - Add .coverage and apiclient_key.pem to gitignore
This commit is contained in:
@@ -6,5 +6,6 @@ from .spark import SparkProvider
|
||||
from .sensenova import SensenovaProvider
|
||||
from .opencode_go import OpencodeGoProvider
|
||||
from .nvidia import NvidiaProvider
|
||||
from .alibaba import AlibabaMTProvider
|
||||
|
||||
__all__ = ["OpenAIProvider", "ClaudeProvider", "DeepLProvider", "LocalProvider", "SparkProvider", "SensenovaProvider", "OpencodeGoProvider", "NvidiaProvider"]
|
||||
__all__ = ["OpenAIProvider", "ClaudeProvider", "DeepLProvider", "LocalProvider", "SparkProvider", "SensenovaProvider", "OpencodeGoProvider", "NvidiaProvider", "AlibabaMTProvider"]
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
from typing import Dict, Any, Optional
|
||||
from aliyunsdkcore.client import AcsClient
|
||||
from aliyunsdkalimt.request.v20181012 import TranslateGeneralRequest, TranslateECommerceRequest
|
||||
from app.services.translation_quota import TranslationQuotaService
|
||||
from app.database import AsyncSessionLocal
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
ALIBABA_LANG_MAP = {
|
||||
"zh": "zh", "en": "en", "ja": "ja", "ko": "ko",
|
||||
"fr": "fr", "de": "de", "es": "es", "pt": "pt",
|
||||
"ru": "ru", "ar": "ar", "th": "th", "vi": "vi",
|
||||
"id": "id", "ms": "ms", "tl": "tl", "hi": "hi",
|
||||
}
|
||||
|
||||
|
||||
class AlibabaMTProvider:
|
||||
def __init__(self, access_key_id: str, access_key_secret: str,
|
||||
region_id: str = "cn-hangzhou"):
|
||||
self.client = AcsClient(access_key_id, access_key_secret, region_id)
|
||||
self._name = "alibaba-mt"
|
||||
|
||||
async def translate(self, text: str, source_lang: Optional[str],
|
||||
target_lang: str, context: Optional[str] = None) -> Dict[str, Any]:
|
||||
src = source_lang if source_lang and source_lang != "auto" else "auto"
|
||||
tgt = ALIBABA_LANG_MAP.get(target_lang[:2].lower(), "en")
|
||||
|
||||
async with AsyncSessionLocal() as db:
|
||||
quota_svc = TranslationQuotaService(db)
|
||||
|
||||
for version in ("ecommerce", "general"):
|
||||
if not await quota_svc.check_quota(version):
|
||||
logger.info(f"Quota [{version}] exhausted or disabled, trying next")
|
||||
continue
|
||||
|
||||
result = await self._do_translate(version, text, src, tgt)
|
||||
if result and result.get("translated_text"):
|
||||
await quota_svc.consume(version, len(text))
|
||||
await db.commit()
|
||||
result["provider"] = f"{self.name}-{version}"
|
||||
return result
|
||||
|
||||
raise Exception("Alibaba MT: both versions quota exhausted or API failed")
|
||||
|
||||
async def _do_translate(self, version: str, text: str, src: str,
|
||||
tgt: str) -> Optional[Dict[str, Any]]:
|
||||
try:
|
||||
if version == "ecommerce":
|
||||
req = TranslateECommerceRequest.TranslateECommerceRequest()
|
||||
else:
|
||||
req = TranslateGeneralRequest.TranslateGeneralRequest()
|
||||
|
||||
req.set_FormatType("text")
|
||||
req.set_Scene(version)
|
||||
req.set_SourceLanguage(src)
|
||||
req.set_TargetLanguage(tgt)
|
||||
req.set_SourceText(text)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
body = await loop.run_in_executor(None, self.client.do_action_with_exception, req)
|
||||
resp = json.loads(body)
|
||||
data = resp.get("Data", {})
|
||||
translated = data.get("Translated", "")
|
||||
detected = data.get("DetectedLanguage", src)
|
||||
|
||||
if translated:
|
||||
logger.info(f"Alibaba MT [{version}] ok: {text[:20]}... -> {translated[:20]}...")
|
||||
return {
|
||||
"translated_text": translated,
|
||||
"provider": f"{self.name}-{version}",
|
||||
"detected_source_lang": detected,
|
||||
"char_count": len(text),
|
||||
"cost": 0,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning(f"Alibaba MT [{version}] failed: {e}")
|
||||
|
||||
return None
|
||||
|
||||
async def reply(self, *args, **kwargs) -> Dict[str, Any]:
|
||||
raise NotImplementedError("Alibaba MT does not support reply generation")
|
||||
|
||||
async def generate_marketing(self, *args, **kwargs) -> Dict[str, Any]:
|
||||
raise NotImplementedError("Alibaba MT does not support marketing generation")
|
||||
|
||||
async def extract_info(self, text: str, schema: Dict[str, Any]) -> Dict[str, Any]:
|
||||
raise NotImplementedError("Alibaba MT does not support info extraction")
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def cost_per_1k_tokens(self) -> float:
|
||||
return 0
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Dict, Any, Optional, List
|
||||
from app.ai.base import AIProvider
|
||||
from app.ai.providers import OpenAIProvider, ClaudeProvider, DeepLProvider, LocalProvider, SparkProvider, SensenovaProvider, OpencodeGoProvider, NvidiaProvider
|
||||
from app.ai.providers import OpenAIProvider, ClaudeProvider, DeepLProvider, LocalProvider, SparkProvider, SensenovaProvider, OpencodeGoProvider, NvidiaProvider, AlibabaMTProvider
|
||||
from app.config import settings
|
||||
from app.ai.trade_corpus import TradeCorpus
|
||||
import logging
|
||||
@@ -81,6 +81,16 @@ class AIRouter:
|
||||
except Exception as e:
|
||||
logger.warning(f"Spark init failed: {e}")
|
||||
|
||||
if settings.ALIBABA_ACCESS_KEY_ID and settings.ALIBABA_ACCESS_KEY_SECRET:
|
||||
try:
|
||||
self.providers["alibaba-mt"] = AlibabaMTProvider(
|
||||
access_key_id=settings.ALIBABA_ACCESS_KEY_ID,
|
||||
access_key_secret=settings.ALIBABA_ACCESS_KEY_SECRET,
|
||||
)
|
||||
logger.info("Alibaba MT provider ready")
|
||||
except Exception as e:
|
||||
logger.warning(f"Alibaba MT init failed: {e}")
|
||||
|
||||
if settings.LOCAL_MODEL_ENABLED:
|
||||
try:
|
||||
self.providers["local"] = LocalProvider(model_url=settings.LOCAL_MODEL_URL)
|
||||
|
||||
Reference in New Issue
Block a user