Source snapshot: 97d7f0fae96dc04b7ddad56fc1db6a108ed662cc [SEC-CLEAN] · pre-push-clean v1.0 · 109处敏感信息已自动转乱码
11 KiB
11 KiB
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 &