Source snapshot: 97d7f0fae96dc04b7ddad56fc1db6a108ed662cc [SEC-CLEAN] · pre-push-clean v1.0 · 109处敏感信息已自动转乱码
332 lines
11 KiB
Markdown
332 lines
11 KiB
Markdown
# GHCS v0.2 · api_keys.py · API密钥管理后端
|
||
|
||
```python
|
||
"""
|
||
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](http://main.py)
|
||
|
||
打开这个文件:
|
||
|
||
```bash
|
||
code ~/guanghu-dev/ghcs-mono/backend/main.py
|
||
```
|
||
|
||
找到 `from auth import get_current_user` 这一行,在它**下面**加一行:
|
||
|
||
```python
|
||
from api_keys import router as api_keys_router
|
||
```
|
||
|
||
找到 `app.include_router(servers_router)` 这一行(或其他 `include_router` 行),在它**下面**加一行:
|
||
|
||
```python
|
||
app.include_router(api_keys_router)
|
||
```
|
||
|
||
改完 ⌘S 保存,就两行。
|
||
|
||
### Step 3 · 验证
|
||
|
||
```bash
|
||
cd ~/guanghu-dev/ghcs-mono/backend
|
||
python3 -c "import api_keys; print('OK:', len(api_keys.router.routes), 'routes')"
|
||
```
|
||
|
||
### Step 4 · 重启
|
||
|
||
```bash
|
||
pkill -f "uvicorn main:app"
|
||
uvicorn main:app --host 127.0.0.1 --port 5002 &
|
||
``` |