diff --git a/app/adapters/ai.py b/app/adapters/ai.py new file mode 100644 index 0000000..19e4fba --- /dev/null +++ b/app/adapters/ai.py @@ -0,0 +1,110 @@ +"""AI 平台适配器(验证 Key + 余额/用量查询) + +各 AI 平台 API 差异较大,此处实现 test_connection(验证 Key,附带余额) +与 get_account(余额)。余额接口可能随平台调整,填入真实凭证后以实际返回为准。 +所需配置统一为 {"api_key": "..."}(Minimax 另需 group_id)。 +""" + +import httpx + +from app.adapters.base import AccountInfo, BaseAdapter +from app.adapters.registry import register + + +class _AIBase(BaseAdapter): + """AI 适配器公共逻辑(Bearer Key + JSON GET)""" + + required_config = ["api_key"] + + def _headers(self) -> dict: + return {"Authorization": f"Bearer {self.config.get('api_key', '')}"} + + def _get(self, url: str) -> dict: + with httpx.Client(timeout=30) as client: + resp = client.get(url, headers=self._headers()) + resp.raise_for_status() + return resp.json() + + def _http_error(self, e: httpx.HTTPStatusError) -> dict: + return {"ok": False, "message": f"HTTP {e.response.status_code}:API Key 无效或权限不足"} + + +@register("deepseek-api") +class DeepSeekAdapter(_AIBase): + BASE = "https://api.deepseek.com" + + def _balance(self) -> AccountInfo: + data = self._get(self.BASE + "/user/balance").get("data", {}) + try: + total = float(data.get("total_balance")) if data.get("total_balance") is not None else None + except (TypeError, ValueError): + total = None + return AccountInfo(balance=total, currency="CNY", raw=data) + + def test_connection(self) -> dict: + try: + acc = self._balance() + return {"ok": True, "message": f"连接成功,余额 {acc.balance} CNY"} + except httpx.HTTPStatusError as e: + return self._http_error(e) + except Exception as e: # noqa: BLE001 + return {"ok": False, "message": str(e)} + + def get_account(self) -> AccountInfo: + return self._balance() + + +@register("moonshot-api") +class KimiAdapter(_AIBase): + BASE = "https://api.moonshot.cn/v1" + + def _balance(self) -> AccountInfo: + data = self._get(self.BASE + "/users/me/balance").get("data", {}) + return AccountInfo(balance=data.get("available_balance"), currency="CNY", raw=data) + + def test_connection(self) -> dict: + try: + acc = self._balance() + return {"ok": True, "message": f"连接成功,可用余额 {acc.balance} CNY"} + except httpx.HTTPStatusError as e: + return self._http_error(e) + except Exception as e: # noqa: BLE001 + return {"ok": False, "message": str(e)} + + def get_account(self) -> AccountInfo: + return self._balance() + + +@register("openai-api") +class OpenAIAdapter(_AIBase): + BASE = "https://api.openai.com/v1" + + def test_connection(self) -> dict: + try: + data = self._get(self.BASE + "/models") + count = len(data.get("data", [])) + return {"ok": True, "message": f"连接成功,可用模型 {count} 个"} + except httpx.HTTPStatusError as e: + return self._http_error(e) + except Exception as e: # noqa: BLE001 + return {"ok": False, "message": str(e)} + + def get_account(self) -> AccountInfo: + # OpenAI 余额接口需组织级权限,尝试旧版 billing,失败返回空 + try: + data = self._get("https://api.openai.com/dashboard/billing/credit_grants") + return AccountInfo(balance=data.get("total_available"), currency="USD", raw=data) + except Exception: # noqa: BLE001 + return AccountInfo(balance=None, currency="USD", raw={}) + + +@register("minimax-api") +class MinimaxAdapter(_AIBase): + # Minimax 需 group_id,余额接口因账号类型而异,此处为骨架待完善 + required_config = ["api_key", "group_id"] + BASE = "https://api.minimax.chat/v1" + + def test_connection(self) -> dict: + if not self.config.get("group_id"): + return {"ok": False, "message": "Minimax 需配置 group_id(适配器余额接口待完善)"} + return {"ok": False, "message": "Minimax 适配器余额接口待完善(请提供具体接口文档)"} diff --git a/app/adapters/registry.py b/app/adapters/registry.py index a3669b2..6750d60 100644 --- a/app/adapters/registry.py +++ b/app/adapters/registry.py @@ -26,6 +26,7 @@ def register(*sdk_types: str): def load_all() -> None: """导入所有适配器模块以触发注册(幂等)""" from app.adapters import ( # noqa: F401 + ai, aliyun, cloudflare, digitalocean, @@ -53,6 +54,18 @@ def supported_types() -> list: return sorted(_REGISTRY.keys()) +def sdk_capabilities() -> dict: + """返回 sdk_type -> capabilities 映射(供前端判断测试/同步按钮)""" + load_all() + result = {} + for sdk_type, cls in _REGISTRY.items(): + try: + result[sdk_type] = cls({}).capabilities() + except Exception: # noqa: BLE001 + result[sdk_type] = {} + return result + + def adapter_meta() -> list: """返回所有已注册适配器的元信息(供前端展示所需凭证字段与能力)""" load_all() diff --git a/app/routers/providers.py b/app/routers/providers.py index 185e10c..a989c85 100644 --- a/app/routers/providers.py +++ b/app/routers/providers.py @@ -31,6 +31,7 @@ def adapters() -> dict: return { "adapters": registry.adapter_meta(), "supported_types": registry.supported_types(), + "capabilities": registry.sdk_capabilities(), } diff --git a/static/js/app.js b/static/js/app.js index 6c4aa05..5e62fc9 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -320,7 +320,7 @@ const ProvidersView = {