Guanghu Domestic Migration a27e87cb99 chore: import sanitized domestic snapshot for REPO-007
Source snapshot: 97d7f0fae96dc04b7ddad56fc1db6a108ed662cc

[SEC-CLEAN] · pre-push-clean v1.0 · 109处敏感信息已自动转乱码
2026-07-17 15:59:55 +08:00

11 KiB
Raw Permalink Blame History

GHCS v0.2 · api_keys.py · API密钥管理后端

"""
API密钥管理模块 · GHCS v0.2
支持: 多供应商 · 多Key · 用量统计 · 费用估算 · 超限告警 · 自动切换
"""
import json
import os
from datetime import datetime
from typing import Optional
from pathlib import Path
from base64 import b64encode, b64decode

from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel

from auth import get_current_user

router = APIRouter(prefix="/api/keys", tags=["api-keys"])

DATA_DIR = Path(__file__).parent
KEYS_FILE = DATA_DIR / "api_keys.json"
USAGE_FILE = DATA_DIR / "api_usage.json"

SUPPLIERS = {
    "deepseek": {
        "name": "DeepSeek", "color": "#4F46E5",
        "pricing_input": 0.001, "pricing_output": 0.002,
    },
    "qwen": {
        "name": "通义千问", "color": "#0EA5E9",
        "pricing_input": 0.0005, "pricing_output": 0.002,
    },
    "glm": {
        "name": "智谱GLM", "color": "#8B5CF6",
        "pricing_input": 0.001, "pricing_output": 0.001,
    },
    "moonshot": {
        "name": "Moonshot", "color": "#10B981",
        "pricing_input": 0.001, "pricing_output": 0.002,
    },
    "other": {
        "name": "其他", "color": "#6B7280",
        "pricing_input": 0.001, "pricing_output": 0.002,
    },
}

SECRET = os.environ.get("GHCS_SECRET", "guanghu-ghcs-default-secret-key")

def _mask_key(key: str) -> str:
    """XOR混淆存储 · 防一眼可见"""
    k = SECRET.encode()
    d = key.encode()
    r = bytes(a ^ k[i % len(k)] for i, a in enumerate(d))
    return b64encode(r).decode()

def _unmask_key(masked: str) -> str:
    """还原"""
    k = SECRET.encode()
    d = b64decode(masked)
    r = bytes(a ^ k[i % len(k)] for i, a in enumerate(d))
    return r.decode()

class ApiKeyCreate(BaseModel):
    supplier: str
    key_name: str
    api_key: str
    monthly_budget: Optional[float] = None

class ApiKeyUpdate(BaseModel):
    key_name: Optional[str] = None
    api_key: Optional[str] = None
    is_active: Optional[bool] = None
    monthly_budget: Optional[float] = None

def _read_keys():
    if not KEYS_FILE.exists():
        return []
    with open(KEYS_FILE, "r") as f:
        return json.load(f)

def _write_keys(data):
    with open(KEYS_FILE, "w") as f:
        json.dump(data, f, indent=2, ensure_ascii=False)

def _read_usage():
    if not USAGE_FILE.exists():
        return {}
    with open(USAGE_FILE, "r") as f:
        return json.load(f)

def _write_usage(data):
    with open(USAGE_FILE, "w") as f:
        json.dump(data, f, indent=2, ensure_ascii=False)

def _make_id():
    import uuid
    return uuid.uuid4().hex[:12]

def _mask_display(api_key: str) -> str:
    if len(api_key) <= 8:
        return "****"
    return api_key[:4] + "****" + api_key[-4:]

def _estimate_cost(supplier: str, tokens: int) -> float:
    """估算费用假设50%输入+50%输出)"""
    cfg = SUPPLIERS.get(supplier, SUPPLIERS["other"])
    half = tokens / 2
    cost_in = (half / 1000) * cfg["pricing_input"]
    cost_out = (half / 1000) * cfg["pricing_output"]
    return round(cost_in + cost_out, 4)

@router.get("/suppliers")
def list_suppliers():
    """列出所有可用供应商及其状态"""
    keys = _read_keys()
    result = []
    for sk, sv in SUPPLIERS.items():
        supplier_keys = [k for k in keys if k["supplier"] == sk]
        result.append({
            "key": sk, "name": sv["name"], "color": sv["color"],
            "key_count": len(supplier_keys),
            "active_count": sum(1 for k in supplier_keys if k.get("is_active", True)),
        })
    return {"suppliers": result}

@router.get("/list")
def list_keys(supplier: Optional[str] = None):
    """列出所有密钥"""
    keys = _read_keys()
    if supplier:
        keys = [k for k in keys if k["supplier"] == supplier]
    result = []
    for k in keys:
        actual_key = _unmask_key(k["api_key_masked"])
        result.append({
            "id": k["id"], "supplier": k["supplier"],
            "supplier_name": SUPPLIERS.get(k["supplier"], {}).get("name", k["supplier"]),
            "key_name": k["key_name"],
            "api_key_masked": _mask_display(actual_key),
            "is_active": k.get("is_active", True),
            "monthly_budget": k.get("monthly_budget"),
            "created_at": k.get("created_at", ""),
            "updated_at": k.get("updated_at", ""),
        })
    return {"keys": result}

@router.post("/add")
def add_key(body: ApiKeyCreate, user: str = Depends(get_current_user)):
    """添加新密钥"""
    if body.supplier not in SUPPLIERS:
        raise HTTPException(400, f"未知供应商: {body.supplier}")
    keys = _read_keys()
    now = datetime.now().isoformat()
    new_key = {
        "id": _make_id(), "supplier": body.supplier,
        "key_name": body.key_name,
        "api_key_masked": _mask_key(body.api_key),
        "is_active": True, "monthly_budget": body.monthly_budget,
        "created_at": now, "updated_at": now,
    }
    keys.append(new_key)
    _write_keys(keys)
    return {"ok": True, "id": new_key["id"],
            "key_name": body.key_name,
            "api_key_masked": _mask_display(body.api_key)}

@router.put("/{key_id}")
def update_key(key_id: str, body: ApiKeyUpdate, user: str = Depends(get_current_user)):
    """更新密钥"""
    keys = _read_keys()
    for k in keys:
        if k["id"] == key_id:
            if body.key_name is not None:
                k["key_name"] = body.key_name
            if body.api_key is not None:
                k["api_key_masked"] = _mask_key(body.api_key)
            if body.is_active is not None:
                k["is_active"] = body.is_active
            if body.monthly_budget is not None:
                k["monthly_budget"] = body.monthly_budget
            k["updated_at"] = datetime.now().isoformat()
            _write_keys(keys)
            return {"ok": True, "id": key_id}
    raise HTTPException(404, f"密钥不存在: {key_id}")

@router.delete("/{key_id}")
def delete_key(key_id: str, user: str = Depends(get_current_user)):
    """删除密钥"""
    keys = _read_keys()
    new_keys = [k for k in keys if k["id"] != key_id]
    if len(new_keys) == len(keys):
        raise HTTPException(404, f"密钥不存在: {key_id}")
    _write_keys(new_keys)
    return {"ok": True}

@router.get("/usage")
def get_usage(supplier: Optional[str] = None):
    """获取用量统计"""
    keys = _read_keys()
    usage = _read_usage()
    result = []
    for k in keys:
        if supplier and k["supplier"] != supplier:
            continue
        key_usage = usage.get(k["id"], {})
        total_calls = key_usage.get("total_calls", 0)
        total_tokens = key_usage.get("total_tokens", 0)
        estimated_cost = _estimate_cost(k["supplier"], total_tokens)
        budget = k.get("monthly_budget")
        budget_percent = None
        if budget and budget > 0:
            budget_percent = round(estimated_cost / budget * 100, 1)
        result.append({
            "id": k["id"], "supplier": k["supplier"],
            "key_name": k["key_name"], "total_calls": total_calls,
            "total_tokens": total_tokens, "estimated_cost": estimated_cost,
            "monthly_budget": budget, "budget_used_percent": budget_percent,
            "is_active": k.get("is_active", True),
        })
    total_cost = sum(r["estimated_cost"] for r in result)
    total_calls = sum(r["total_calls"] for r in result)
    return {"items": result, "total_cost": round(total_cost, 2),
            "total_calls": total_calls}

@router.post("/usage/record")
def record_usage(key_id: str, tokens: int, calls: int = 1):
    """记录API调用由api-router调用"""
    usage = _read_usage()
    if key_id not in usage:
        usage[key_id] = {"total_calls": 0, "total_tokens": 0, "daily": {}}
    usage[key_id]["total_calls"] += calls
    usage[key_id]["total_tokens"] += tokens
    today = datetime.now().strftime("%Y-%m-%d")
    if today not in usage[key_id]["daily"]:
        usage[key_id]["daily"][today] = {"calls": 0, "tokens": 0}
    usage[key_id]["daily"][today]["calls"] += calls
    usage[key_id]["daily"][today]["tokens"] += tokens
    _write_usage(usage)
    return {"ok": True}

@router.get("/active-key")
def get_active_key(supplier: str):
    """获取指定供应商的活跃密钥供api-router调用"""
    keys = _read_keys()
    supplier_keys = [
        k for k in keys
        if k["supplier"] == supplier and k.get("is_active", True)
    ]
    if not supplier_keys:
        raise HTTPException(404, f"供应商 {supplier} 无活跃密钥")
    k = supplier_keys[0]
    return {
        "supplier": k["supplier"],
        "api_key": _unmask_key(k["api_key_masked"]),
        "key_name": k["key_name"],
    }

@router.get("/health-check")
def health_check():
    """检查所有供应商的密钥健康状态"""
    keys = _read_keys()
    usage = _read_usage()
    alerts = []
    for k in keys:
        if not k.get("is_active", True):
            continue
        budget = k.get("monthly_budget")
        key_usage = usage.get(k["id"], {})
        total_tokens = key_usage.get("total_tokens", 0)
        cost = _estimate_cost(k["supplier"], total_tokens)
        if budget and budget > 0:
            percent = cost / budget * 100
            if percent >= 100:
                alerts.append({
                    "level": "red", "key_id": k["id"],
                    "key_name": k["key_name"], "supplier": k["supplier"],
                    "message": f"已超出月度预算!已用 {cost:.2f} / {budget:.0f} 元",
                })
            elif percent >= 80:
                alerts.append({
                    "level": "yellow", "key_id": k["id"],
                    "key_name": k["key_name"], "supplier": k["supplier"],
                    "message": f"月度预算即将耗尽:{cost:.2f} / {budget:.0f} 元 ({percent:.0f}%)",
                })
    return {"alerts": alerts, "ok": len(alerts) == 0}

🔧 部署步骤

Step 1 · 保存文件

复制上面代码 → VS Code打开 ~/guanghu-dev/ghcs-mono/backend/api_keys.py → 全选粘贴 → ⌘S

Step 2 · 改 main.py

打开这个文件:

code ~/guanghu-dev/ghcs-mono/backend/main.py

找到 from auth import get_current_user 这一行,在它下面加一行:

from api_keys import router as api_keys_router

找到 app.include_router(servers_router) 这一行(或其他 include_router 行),在它下面加一行:

app.include_router(api_keys_router)

改完 ⌘S 保存,就两行。

Step 3 · 验证

cd ~/guanghu-dev/ghcs-mono/backend
python3 -c "import api_keys; print('OK:', len(api_keys.router.routes), 'routes')"

Step 4 · 重启

pkill -f "uvicorn main:app"
uvicorn main:app --host 127.0.0.1 --port 5002 &