fix: shorten New-API token names
This commit is contained in:
@@ -24,7 +24,7 @@ from app.schemas.upstream import (
|
||||
UpstreamCreate, UpstreamUpdate, UpstreamResponse, SnapshotResponse, TestResult,
|
||||
UpstreamBatchActionItem, UpstreamBatchActionSummary, UpstreamBatchActionResponse,
|
||||
)
|
||||
from app.services.upstream_client import UpstreamClient, UpstreamError, build_snapshot, mask_secret, _extract_key_value
|
||||
from app.services.upstream_client import UpstreamClient, UpstreamError, build_snapshot, build_new_api_token_name, mask_secret, _extract_key_value
|
||||
from app.services.auth_config import MASK, mask_auth_config, normalize_auth_config
|
||||
from app.services.snapshot_service import diff_snapshots
|
||||
from app.services import scheduler as sched_svc
|
||||
@@ -377,10 +377,27 @@ def _ensure_group_key(
|
||||
"""确保一个上游分组有一个 SmartUp 前缀 Key:存在则 upsert,不存在则创建。"""
|
||||
gid = _group_id(group)
|
||||
gname = _group_name(group, gid)
|
||||
# 使用稳定的 upstream_id + group_id 而非可变名称,避免因改名产生重复
|
||||
# 可读 Key 名:{prefix}-{upstream.id}-{安全的分组名}-{group_id}
|
||||
safe_group_name = re.sub(r"[^a-zA-Z0-9\u4e00-\u9fff_-]", "", gname)[:30] if gname else gid
|
||||
stable_name = f"{prefix}-{upstream.id}-{safe_group_name}-{gid}"
|
||||
|
||||
# 根据上游类型决定 token 名格式:
|
||||
# - New-API/Nox-API:用新短名(hash8,≤50 字节)
|
||||
# - Sub2API 等其他上游:保持旧格式({prefix}-{id}-{safename}-{gid})
|
||||
use_short_name = _is_new_api_user_upstream(upstream) or _is_nox_api_upstream(upstream)
|
||||
if use_short_name:
|
||||
try:
|
||||
stable_name = build_new_api_token_name(prefix, upstream.id, gid, gname)
|
||||
except ValueError as exc:
|
||||
# prefix 过长,骨架已超 50 字节,直接返回清晰错误
|
||||
return GeneratedUpstreamKeyResponse(
|
||||
upstream_id=upstream.id,
|
||||
group_id=gid,
|
||||
group_name=gname,
|
||||
key_name=f"{prefix}-{upstream.id}--??",
|
||||
status="failed",
|
||||
error=str(exc),
|
||||
)
|
||||
else:
|
||||
safe_group_name = re.sub(r"[^a-zA-Z0-9\u4e00-\u9fff_-]", "", gname)[:30] if gname else gid
|
||||
stable_name = f"{prefix}-{upstream.id}-{safe_group_name}-{gid}"
|
||||
|
||||
with _generate_key_lock:
|
||||
try:
|
||||
@@ -396,10 +413,18 @@ def _ensure_group_key(
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if row and row.key_id:
|
||||
# 本地已有记录,检查远端是否仍存在
|
||||
if row and row.key_name:
|
||||
# 本地已有记录(无论 key_id 是否存在),优先用本地已存的 key_name 查远端
|
||||
# 这样旧格式 key_name 也能找回远端对应 token,不重复创建
|
||||
lookup_name = row.key_name
|
||||
found_by_name = lookup_name # 记录实际命中的远端名称
|
||||
try:
|
||||
existing = client.find_smartup_group_key(gid, stable_name, prefix)
|
||||
existing = client.find_smartup_group_key(gid, lookup_name, prefix)
|
||||
# 若旧格式未找到,再尝试新格式(同一分组可能已在远端迁移)
|
||||
if existing is None and lookup_name != stable_name:
|
||||
existing = client.find_smartup_group_key(gid, stable_name, prefix)
|
||||
if existing is not None:
|
||||
found_by_name = stable_name
|
||||
except Exception:
|
||||
existing = None
|
||||
if existing:
|
||||
@@ -412,6 +437,10 @@ def _ensure_group_key(
|
||||
row.masked_key = masked
|
||||
elif masked:
|
||||
row.masked_key = str(masked)
|
||||
# P3:若是通过新短名找到(旧名已消失),同步更新本地 key_name
|
||||
# 保证 UI 和后续 lookup 与远端名称一致
|
||||
if found_by_name != lookup_name:
|
||||
row.key_name = found_by_name
|
||||
row.raw_json = json.dumps(existing, ensure_ascii=False)
|
||||
row.status = "exists"
|
||||
row.updated_at = datetime.now(timezone.utc)
|
||||
@@ -422,8 +451,29 @@ def _ensure_group_key(
|
||||
# 远端不存在,需要重新创建
|
||||
row.status = "replaced"
|
||||
|
||||
# 2. 查远端是否有同名 Key(防止并发时另一个请求已创建)
|
||||
existing = client.find_smartup_group_key(gid, stable_name, prefix)
|
||||
# 2. 查远端是否有同名 Key
|
||||
# 对 New-API/Nox 上游:先按旧格式查(本地无记录但远端可能仍是旧格式),
|
||||
# 再按新短名查(并发创建防重);最后才创建。
|
||||
names_to_search: list[str] = []
|
||||
if use_short_name and not row:
|
||||
# 构造旧格式 token 名,尝试导入已存在的旧格式远端 token
|
||||
old_safe = re.sub(r"[^a-zA-Z0-9\u4e00-\u9fff_-]", "", gname)[:30] if gname else gid
|
||||
old_format_name = f"{prefix}-{upstream.id}-{old_safe}-{gid}"
|
||||
if old_format_name != stable_name:
|
||||
names_to_search.append(old_format_name)
|
||||
names_to_search.append(stable_name)
|
||||
|
||||
existing = None
|
||||
found_name = stable_name
|
||||
for candidate_name in names_to_search:
|
||||
try:
|
||||
existing = client.find_smartup_group_key(gid, candidate_name, prefix)
|
||||
except Exception:
|
||||
existing = None
|
||||
if existing:
|
||||
found_name = candidate_name
|
||||
break
|
||||
|
||||
if existing:
|
||||
key_id = str(existing.get("id") or "")
|
||||
key_value = _extract_plaintext_key(existing)
|
||||
@@ -444,7 +494,7 @@ def _ensure_group_key(
|
||||
group_id=gid,
|
||||
group_name=gname,
|
||||
key_id=key_id or None,
|
||||
key_name=stable_name,
|
||||
key_name=found_name,
|
||||
key_value=key_value or "",
|
||||
masked_key=masked,
|
||||
raw_json=json.dumps(existing, ensure_ascii=False),
|
||||
@@ -456,7 +506,7 @@ def _ensure_group_key(
|
||||
db.refresh(row)
|
||||
return _key_response(row, include_value=False)
|
||||
|
||||
# 3. 远端不存在,创建新 Key
|
||||
# 3. 远端不存在,创建新 Key(使用新短名)
|
||||
created = client.create_api_key(
|
||||
stable_name,
|
||||
gid,
|
||||
@@ -502,8 +552,8 @@ def _ensure_group_key(
|
||||
group_name=gname,
|
||||
key_name=stable_name,
|
||||
status="failed",
|
||||
error=str(exc),
|
||||
)
|
||||
error=str(exc),
|
||||
)
|
||||
|
||||
|
||||
@router.post("/{uid}/keys/generate-by-groups", response_model=GenerateKeysByGroupsResponse)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"""Upstream HTTP client — ported from monitor_ai98pro_group_rates.py."""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from typing import Any, Callable, Optional
|
||||
from urllib.parse import urljoin
|
||||
@@ -16,6 +18,60 @@ class UpstreamError(RuntimeError):
|
||||
|
||||
NEW_API_DEFAULT_QUOTA_PER_UNIT = 500000
|
||||
|
||||
# New-API/Nox-API token name 上限(UTF-8 字节)
|
||||
_NEW_API_TOKEN_NAME_MAX_BYTES = 50
|
||||
|
||||
|
||||
def _utf8_safe_truncate(text: str, max_bytes: int) -> str:
|
||||
"""按 UTF-8 字节安全截断字符串,不切坏多字节字符。"""
|
||||
encoded = text.encode("utf-8")
|
||||
if len(encoded) <= max_bytes:
|
||||
return text
|
||||
truncated = encoded[:max_bytes]
|
||||
# 向后退直到合法 UTF-8 边界
|
||||
return truncated.decode("utf-8", errors="ignore")
|
||||
|
||||
|
||||
def build_new_api_token_name(
|
||||
prefix: str,
|
||||
upstream_id: int | str,
|
||||
group_id: str,
|
||||
group_name: str,
|
||||
) -> str:
|
||||
"""构造不超过 50 UTF-8 字节的 New-API/Nox-API token 名。
|
||||
|
||||
格式:{prefix}-{upstream_id}-{短分组名}-{hash8}
|
||||
- 短分组名:去除非字母/数字/汉字/_/- 后,UTF-8 安全截断到剩余空间
|
||||
- hash8:sha1("{upstream_id}:{group_id}")[:8],保证同名分组仍能区分
|
||||
- 若 {prefix}-{upstream_id}-{hash8} 本身超过 50 字节,抛出 ValueError
|
||||
|
||||
示例:
|
||||
SmartUp-7-ClaudeMax智能分组-a1b2c3d4
|
||||
"""
|
||||
hash8 = hashlib.sha1(f"{upstream_id}:{group_id}".encode()).hexdigest()[:8]
|
||||
# 最小骨架:{prefix}-{upstream_id}--{hash8}(含两个分隔符)
|
||||
skeleton = f"{prefix}-{upstream_id}--{hash8}"
|
||||
skeleton_bytes = len(skeleton.encode("utf-8"))
|
||||
if skeleton_bytes > _NEW_API_TOKEN_NAME_MAX_BYTES:
|
||||
raise ValueError(
|
||||
f"token name prefix '{prefix}' 过长,骨架 '{skeleton}' 已达 "
|
||||
f"{skeleton_bytes} 字节(上限 {_NEW_API_TOKEN_NAME_MAX_BYTES})"
|
||||
)
|
||||
|
||||
# 可用于短分组名的字节数
|
||||
safe_name = re.sub(r"[^a-zA-Z0-9\u4e00-\u9fff_-]", "", group_name or group_id)
|
||||
# 骨架含一个额外分隔符,留给 group_name 的字节 = max - len(prefix-upid--hash8) - 1
|
||||
# 即 max - skeleton_bytes(skeleton 已包含两个 `-` 和一个 `-` 给 group_name,
|
||||
# 但 skeleton 里 group_name 位置是空的,所以可用字节 = max - skeleton_bytes + 1
|
||||
# skeleton: prefix-uid--hash8 → prefix-uid-<group>-hash8 多出 group 部分
|
||||
# 可用字节:_NEW_API_TOKEN_NAME_MAX_BYTES - len(f"{prefix}-{upstream_id}-".encode()) - len(f"-{hash8}".encode())
|
||||
base_bytes = len(f"{prefix}-{upstream_id}-".encode("utf-8"))
|
||||
suffix_bytes = len(f"-{hash8}".encode("utf-8"))
|
||||
group_budget = _NEW_API_TOKEN_NAME_MAX_BYTES - base_bytes - suffix_bytes
|
||||
short_name = _utf8_safe_truncate(safe_name, group_budget) if group_budget > 0 else ""
|
||||
|
||||
return f"{prefix}-{upstream_id}-{short_name}-{hash8}"
|
||||
|
||||
|
||||
def _find_token(value: Any) -> str:
|
||||
if isinstance(value, str) and value.count(".") >= 2:
|
||||
|
||||
Reference in New Issue
Block a user