feat: 多平台 Coding Plan 统一管理系统初始实现

- 支持 MiniMax/OpenAI/Google Gemini/智谱/Kimi 五个平台
- 插件化 Provider 架构,自动发现注册
- 多维度 QuotaRule 额度追踪(固定间隔/自然周期/API同步/手动)
- OpenAI + Anthropic 兼容 API 代理,SSE 流式转发
- Model 路由表 + 额度耗尽自动 fallback
- 多媒体任务队列(图片/语音/视频)
- Vue3 + Tailwind 单文件 Web 仪表盘
- Docker 一键部署

Made-with: Cursor
This commit is contained in:
锦麟 王
2026-03-31 15:50:42 +08:00
commit 61ce809634
28 changed files with 2804 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
"""Provider 抽象基类 + 能力枚举"""
from __future__ import annotations
from abc import ABC, abstractmethod
from enum import Enum
from typing import Any, AsyncGenerator
from pydantic import BaseModel
class Capability(str, Enum):
CHAT = "chat"
IMAGE = "image"
VOICE = "voice"
VIDEO = "video"
FILE = "file"
EMBEDDING = "embedding"
class QuotaInfo(BaseModel):
"""Provider 返回的额度信息"""
quota_used: int = 0
quota_remaining: int = 0
quota_total: int = 0
unit: str = "tokens"
raw: dict[str, Any] | None = None
class BaseProvider(ABC):
"""
所有平台适配器的基类。
子类需要设置 name / display_name / capabilities
并实现对应能力的方法。
"""
name: str = ""
display_name: str = ""
capabilities: list[Capability] = []
@abstractmethod
async def chat(
self,
messages: list[dict],
model: str,
plan: dict,
stream: bool = True,
**kwargs,
) -> AsyncGenerator[str, None]:
"""
聊天补全。返回 SSE 格式的 data 行。
每 yield 一次代表一个 SSE event 的 data 字段内容。
"""
yield "" # pragma: no cover
async def generate_image(
self, prompt: str, plan: dict, **kwargs
) -> dict[str, Any]:
raise NotImplementedError(f"{self.name} does not support image generation")
async def generate_voice(
self, text: str, plan: dict, **kwargs
) -> bytes:
raise NotImplementedError(f"{self.name} does not support voice synthesis")
async def generate_video(
self, prompt: str, plan: dict, **kwargs
) -> dict[str, Any]:
raise NotImplementedError(f"{self.name} does not support video generation")
async def query_quota(self, plan: dict) -> QuotaInfo | None:
"""
查询平台额度。返回 None 表示该平台不支持 API 查询,走本地追踪。
"""
return None
def _build_headers(self, plan: dict) -> dict[str, str]:
"""构建请求头: Authorization + extra_headers"""
headers = {"Content-Type": "application/json"}
api_key = plan.get("api_key", "")
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
extra = plan.get("extra_headers") or {}
headers.update(extra)
return headers
def _base_url(self, plan: dict) -> str:
return (plan.get("api_base") or "").rstrip("/")