group_keys.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. """
  2. 分组-Key号池管理路由
  3. 支持一个分组绑定多个key,形成号池
  4. """
  5. from __future__ import annotations
  6. from datetime import datetime
  7. from typing import List, Optional
  8. from fastapi import APIRouter, HTTPException
  9. from pydantic import BaseModel
  10. from app.db import get_pool
  11. router = APIRouter(tags=["group_keys"])
  12. class GroupKeyIn(BaseModel):
  13. """添加key到分组"""
  14. group_id: int
  15. api_key_id: int
  16. priority: Optional[int] = 0
  17. weight: Optional[int] = 1
  18. class GroupKeyUpdate(BaseModel):
  19. """更新分组-key关系"""
  20. priority: Optional[int] = None
  21. weight: Optional[int] = None
  22. is_active: Optional[bool] = None
  23. status: Optional[str] = None
  24. status_msg: Optional[str] = None
  25. class GroupKeyOut(BaseModel):
  26. id: int
  27. group_id: int
  28. group_name: str
  29. api_key_id: int
  30. api_key_name: str
  31. api_key_masked: str
  32. priority: int
  33. weight: int
  34. is_active: bool
  35. status: str
  36. status_msg: Optional[str]
  37. last_used_at: Optional[datetime]
  38. total_tokens: int
  39. request_count: int
  40. error_count: int
  41. created_at: datetime
  42. updated_at: datetime
  43. class BatchAddKeysIn(BaseModel):
  44. """批量添加key到分组"""
  45. group_id: int
  46. api_key_ids: List[int]
  47. priority: Optional[int] = 0
  48. weight: Optional[int] = 1
  49. def _mask_key(v: str) -> str:
  50. if len(v) <= 10:
  51. return '••••••••'
  52. return v[:6] + '••••••••' + v[-4:]
  53. @router.get("/group-keys", response_model=List[GroupKeyOut])
  54. async def list_group_keys(group_id: Optional[int] = None) -> List[GroupKeyOut]:
  55. """获取分组-key列表"""
  56. pool = get_pool()
  57. query = """
  58. SELECT gk.id, gk.group_id, mg.name AS group_name,
  59. gk.api_key_id, ak.name AS api_key_name, ak.key_value,
  60. gk.priority, gk.weight, gk.is_active, gk.status, gk.status_msg,
  61. gk.last_used_at, gk.total_tokens, gk.request_count, gk.error_count,
  62. gk.created_at, gk.updated_at
  63. FROM group_keys gk
  64. JOIN model_groups mg ON mg.id = gk.group_id
  65. JOIN api_keys ak ON ak.id = gk.api_key_id
  66. """
  67. if group_id is not None:
  68. query += " WHERE gk.group_id = $1"
  69. rows = await pool.fetch(query, group_id)
  70. else:
  71. rows = await pool.fetch(query)
  72. return [
  73. GroupKeyOut(**{
  74. **dict(r),
  75. "api_key_masked": _mask_key(r["key_value"])
  76. })
  77. for r in rows
  78. ]
  79. @router.post("/group-keys", response_model=GroupKeyOut, status_code=201)
  80. async def add_key_to_group(body: GroupKeyIn) -> GroupKeyOut:
  81. """添加key到分组(号池)"""
  82. pool = get_pool()
  83. # 检查分组是否存在
  84. group = await pool.fetchrow("SELECT id, name FROM model_groups WHERE id = $1", body.group_id)
  85. if not group:
  86. raise HTTPException(status_code=404, detail="分组不存在")
  87. # 检查key是否存在
  88. key = await pool.fetchrow("SELECT id, name, key_value FROM api_keys WHERE id = $1", body.api_key_id)
  89. if not key:
  90. raise HTTPException(status_code=404, detail="API Key不存在")
  91. # 检查是否已存在
  92. existing = await pool.fetchrow(
  93. "SELECT id FROM group_keys WHERE group_id = $1 AND api_key_id = $2",
  94. body.group_id, body.api_key_id
  95. )
  96. if existing:
  97. raise HTTPException(status_code=400, detail="该key已在此分组中")
  98. row = await pool.fetchrow(
  99. """
  100. INSERT INTO group_keys (group_id, api_key_id, priority, weight)
  101. VALUES ($1, $2, $3, $4)
  102. RETURNING id, group_id, api_key_id, priority, weight, is_active, status, status_msg,
  103. last_used_at, total_tokens, request_count, error_count, created_at, updated_at
  104. """,
  105. body.group_id, body.api_key_id, body.priority or 0, body.weight or 1
  106. )
  107. return GroupKeyOut(**{
  108. **dict(row),
  109. "group_name": group["name"],
  110. "api_key_name": key["name"],
  111. "api_key_masked": _mask_key(key["key_value"]),
  112. })
  113. @router.post("/group-keys/batch-add", response_model=List[GroupKeyOut], status_code=201)
  114. async def batch_add_keys_to_group(body: BatchAddKeysIn) -> List[GroupKeyOut]:
  115. """批量添加key到分组"""
  116. pool = get_pool()
  117. # 检查分组
  118. group = await pool.fetchrow("SELECT id, name FROM model_groups WHERE id = $1", body.group_id)
  119. if not group:
  120. raise HTTPException(status_code=404, detail="分组不存在")
  121. results = []
  122. for key_id in body.api_key_ids:
  123. key = await pool.fetchrow("SELECT id, name, key_value FROM api_keys WHERE id = $1", key_id)
  124. if not key:
  125. continue
  126. # 检查是否已存在
  127. existing = await pool.fetchrow(
  128. "SELECT id FROM group_keys WHERE group_id = $1 AND api_key_id = $2",
  129. body.group_id, key_id
  130. )
  131. if existing:
  132. continue
  133. row = await pool.fetchrow(
  134. """
  135. INSERT INTO group_keys (group_id, api_key_id, priority, weight)
  136. VALUES ($1, $2, $3, $4)
  137. RETURNING id, group_id, api_key_id, priority, weight, is_active, status, status_msg,
  138. last_used_at, total_tokens, request_count, error_count, created_at, updated_at
  139. """,
  140. body.group_id, key_id, body.priority or 0, body.weight or 1
  141. )
  142. results.append(GroupKeyOut(**{
  143. **dict(row),
  144. "group_name": group["name"],
  145. "api_key_name": key["name"],
  146. "api_key_masked": _mask_key(key["key_value"]),
  147. }))
  148. return results
  149. @router.put("/group-keys/{id}", response_model=GroupKeyOut)
  150. async def update_group_key(id: int, body: GroupKeyUpdate) -> GroupKeyOut:
  151. """更新分组-key关系"""
  152. pool = get_pool()
  153. existing = await pool.fetchrow(
  154. """
  155. SELECT gk.*, mg.name AS group_name, ak.name AS api_key_name, ak.key_value
  156. FROM group_keys gk
  157. JOIN model_groups mg ON mg.id = gk.group_id
  158. JOIN api_keys ak ON ak.id = gk.api_key_id
  159. WHERE gk.id = $1
  160. """,
  161. id
  162. )
  163. if not existing:
  164. raise HTTPException(status_code=404, detail="记录不存在")
  165. updates = []
  166. values = []
  167. param_idx = 1
  168. if body.priority is not None:
  169. updates.append(f"priority = ${param_idx}")
  170. values.append(body.priority)
  171. param_idx += 1
  172. if body.weight is not None:
  173. updates.append(f"weight = ${param_idx}")
  174. values.append(body.weight)
  175. param_idx += 1
  176. if body.is_active is not None:
  177. updates.append(f"is_active = ${param_idx}")
  178. values.append(body.is_active)
  179. param_idx += 1
  180. if body.status is not None:
  181. updates.append(f"status = ${param_idx}")
  182. values.append(body.status)
  183. param_idx += 1
  184. if body.status_msg is not None:
  185. updates.append(f"status_msg = ${param_idx}")
  186. values.append(body.status_msg)
  187. param_idx += 1
  188. if updates:
  189. updates.append("updated_at = NOW()")
  190. values.append(id)
  191. await pool.execute(
  192. f"UPDATE group_keys SET {', '.join(updates)} WHERE id = ${param_idx}",
  193. *values
  194. )
  195. # 重新获取
  196. row = await pool.fetchrow(
  197. """
  198. SELECT gk.id, gk.group_id, mg.name AS group_name,
  199. gk.api_key_id, ak.name AS api_key_name, ak.key_value,
  200. gk.priority, gk.weight, gk.is_active, gk.status, gk.status_msg,
  201. gk.last_used_at, gk.total_tokens, gk.request_count, gk.error_count,
  202. gk.created_at, gk.updated_at
  203. FROM group_keys gk
  204. JOIN model_groups mg ON mg.id = gk.group_id
  205. JOIN api_keys ak ON ak.id = gk.api_key_id
  206. WHERE gk.id = $1
  207. """,
  208. id
  209. )
  210. return GroupKeyOut(**{
  211. **dict(row),
  212. "api_key_masked": _mask_key(row["key_value"])
  213. })
  214. @router.delete("/group-keys/{id}", status_code=204)
  215. async def remove_key_from_group(id: int):
  216. """从分组中移除key"""
  217. pool = get_pool()
  218. result = await pool.execute("DELETE FROM group_keys WHERE id = $1", id)
  219. if result == "DELETE 0":
  220. raise HTTPException(status_code=404, detail="记录不存在")
  221. @router.get("/groups/{group_id}/pool-summary")
  222. async def get_group_pool_summary(group_id: int):
  223. """获取分组号池摘要(健康度统计)"""
  224. pool = get_pool()
  225. # 检查分组
  226. group = await pool.fetchrow("SELECT id, name FROM model_groups WHERE id = $1", group_id)
  227. if not group:
  228. raise HTTPException(status_code=404, detail="分组不存在")
  229. # 统计
  230. stats = await pool.fetchrow(
  231. """
  232. SELECT
  233. COUNT(*) AS total_keys,
  234. COUNT(*) FILTER (WHERE status = 'active' AND is_active = TRUE) AS active_keys,
  235. COUNT(*) FILTER (WHERE status = 'rate_limited') AS rate_limited_keys,
  236. COUNT(*) FILTER (WHERE status = 'banned') AS banned_keys,
  237. COUNT(*) FILTER (WHERE status = 'failed') AS failed_keys,
  238. SUM(total_tokens) AS total_tokens,
  239. SUM(request_count) AS total_requests,
  240. SUM(error_count) AS total_errors
  241. FROM group_keys
  242. WHERE group_id = $1
  243. """,
  244. group_id
  245. )
  246. # 健康度计算
  247. total = stats["total_keys"] or 0
  248. active = stats["active_keys"] or 0
  249. health_rate = (active / total * 100) if total > 0 else 0
  250. return {
  251. "group_id": group_id,
  252. "group_name": group["name"],
  253. "total_keys": total,
  254. "active_keys": active,
  255. "rate_limited_keys": stats["rate_limited_keys"] or 0,
  256. "banned_keys": stats["banned_keys"] or 0,
  257. "failed_keys": stats["failed_keys"] or 0,
  258. "health_rate": round(health_rate, 2),
  259. "total_tokens": stats["total_tokens"] or 0,
  260. "total_requests": stats["total_requests"] or 0,
  261. "total_errors": stats["total_errors"] or 0,
  262. }
  263. @router.get("/groups/{group_id}/keys")
  264. async def get_group_keys_detail(group_id: int):
  265. """获取分组的key详情列表"""
  266. pool = get_pool()
  267. rows = await pool.fetch(
  268. """
  269. SELECT gk.id, gk.api_key_id, ak.name AS api_key_name, ak.key_value,
  270. gk.priority, gk.weight, gk.is_active, gk.status, gk.status_msg,
  271. gk.last_used_at, gk.total_tokens, gk.request_count, gk.error_count,
  272. gk.created_at, gk.updated_at
  273. FROM group_keys gk
  274. JOIN api_keys ak ON ak.id = gk.api_key_id
  275. WHERE gk.group_id = $1
  276. ORDER BY gk.priority DESC, gk.weight DESC
  277. """,
  278. group_id
  279. )
  280. return [
  281. {
  282. "id": r["id"],
  283. "api_key_id": r["api_key_id"],
  284. "api_key_name": r["api_key_name"],
  285. "api_key_masked": _mask_key(r["key_value"]),
  286. "priority": r["priority"],
  287. "weight": r["weight"],
  288. "is_active": r["is_active"],
  289. "status": r["status"],
  290. "status_msg": r["status_msg"],
  291. "last_used_at": r["last_used_at"].isoformat() if r["last_used_at"] else None,
  292. "total_tokens": r["total_tokens"],
  293. "request_count": r["request_count"],
  294. "error_count": r["error_count"],
  295. "created_at": r["created_at"].isoformat() if r["created_at"] else None,
  296. "updated_at": r["updated_at"].isoformat() if r["updated_at"] else None,
  297. }
  298. for r in rows
  299. ]
  300. # ============================================
  301. # 推送到company_base_pool
  302. # ============================================
  303. @router.post("/groups/{group_id}/push")
  304. async def push_group(group_id: int):
  305. """手动推送单个分组到company_base_pool"""
  306. pool = get_pool()
  307. try:
  308. await push_group_keys_to_pool(pool, group_id, [])
  309. return {"success": True, "message": "推送成功"}
  310. except Exception as e:
  311. raise HTTPException(status_code=500, detail=f"推送失败: {str(e)}")
  312. @router.post("/groups/push-all")
  313. async def push_all_groups():
  314. """手动推送所有分组到company_base_pool"""
  315. pool = get_pool()
  316. try:
  317. await push_all_groups_to_pool(pool)
  318. return {"success": True, "message": "推送完成"}
  319. except Exception as e:
  320. raise HTTPException(status_code=500, detail=f"推送失败: {str(e)}")