Skip to content

Commit 12a6e68

Browse files
committed
fix(gateway): resolve low-risk issues #11-#14
- #11: vision confidence redefined as structured-extraction completeness (text 0.5 + aux fields 0.5) instead of fabricated 0.9/0.3; GLM ASR fabricated 0.9 fixed to 0.0 placeholder - #12: fallback chain budget comments unified to *3.0 matching implementation (was *1.5, misleading maintainers) - #13: key rotation moved to with_retry_and_timeout wrapper so vision/stream/ASR/video paths rotate keys too, not just generate - #14: JWT_SECRET marked as dead config in .env/.env.example with SUPABASE_JWT_ALGORITHM documented; startup warning when legacy JWT_SECRET is set but SUPABASE_JWT_SECRET is empty
1 parent f0b92a9 commit 12a6e68

9 files changed

Lines changed: 50 additions & 10 deletions

File tree

‎server/.env.example‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ REDIS_PASSWORD=
2626
REDIS_URL=redis://localhost:6379
2727

2828
# --- JWT 安全密钥 ---
29-
# 生成方法: openssl rand -hex 32
30-
# 生产环境必须替换为随机字符串
31-
JWT_SECRET=change-this-to-a-random-string-in-production
29+
# ⚠️ GW-2#14: JWT_SECRET 为死配置——网关实际只读取 SUPABASE_JWT_SECRET,
30+
# 按字面配置 JWT_SECRET 会误以为认证密钥已就绪(实际全站 401)
31+
# 生成方法: openssl rand -hex 32(填入下方 SUPABASE_JWT_SECRET)
32+
#JWT_SECRET=change-this-to-a-random-string-in-production
3233

3334
# --- AI 模型 API Key ---
3435
# 通义千问:https://dashscope.aliyun.com
@@ -56,7 +57,10 @@ ALIYUN_ACCESS_KEY_SECRET=
5657

5758
# --- Supabase 配置 ---
5859
# 从 Supabase Dashboard > Settings > API > JWT Settings 获取 JWT Signing Secret
60+
# 网关唯一生效的 JWT 密钥来源(HS256 对称密钥)
5961
SUPABASE_JWT_SECRET=
62+
# JWT 验证算法:HS256(Supabase 默认)/ ES256(仅开启自定义 JWT 时选用)
63+
SUPABASE_JWT_ALGORITHM=HS256
6064
# 项目 URL:从 Supabase Dashboard > Settings > API 获取
6165
SUPABASE_URL=https://your-project-id.supabase.co
6266
# JWKS URL:通常为 <SUPABASE_URL>/auth/v1/.well-known/jwks.json

‎server/ai-gateway/chains/transcribe_chain.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ async def run(
9494
# 后处理
9595
result = self._postprocess_result(result)
9696
result.setdefault("language", language)
97+
# GW-2#11: ASR API 不返回置信度,0.0 为占位常量而非测量值
9798
result.setdefault("confidence", 0.0)
9899
result.setdefault("model", self.model)
99100

‎server/ai-gateway/config/fallback.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
熵减 AI 网关 — Provider Fallback 链与调用编排
33
44
@ai-context: 主 Provider 失败时按 PROVIDER_FALLBACK_CHAIN 依次降级。整条链
5-
共享 TIMEOUT_CONFIG*1.5 的总预算,每次切换前扣除已耗时,避免叠加超时。
5+
共享 TIMEOUT_CONFIG*3.0 的总预算(GW-2#12: 实现为 *3.0——约 3 个备选
6+
Provider × 每次调用超时上限的兑底语义;注释曾写 *1.5 与实现漂移),
7+
每次切换前扣除已耗时,避免叠加超时。
68
call_with_fallback(dict)/ call_with_fallback_stream(AsyncGenerator)各有
79
带用户 Key 的变体:用户 Key 优先,失败再降级服务端链。_resolve_model_name
810
在 fallback 到备选 Provider 时按 主slot→功能slot→通用slot→首个模型 逐级回退。

‎server/ai-gateway/config/limits.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
44
@ai-context: 按 feature 维度配置超时(秒)与每日频率上限。多模态/视频分析
55
成本高、耗时长,故超时与限流都显著高于文本类功能。call_with_fallback 以
6-
TIMEOUT_CONFIG * 1.5 作为整条 fallback 链的总预算。
6+
TIMEOUT_CONFIG * 3.0 作为整条 fallback 链的总预算(GW-2#12: 与实现一致,
7+
约 3 个备选 Provider × 单次超时的兑底语义)。
78
"""
89

910
# ============================================================

‎server/ai-gateway/middleware/auth.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,16 @@ def _jwt_verification_configured() -> bool:
9191
stacklevel=2,
9292
)
9393

94+
# GW-2#14: 死配置检测——存在非空 JWT_SECRET 而 SUPABASE_JWT_SECRET 为空时
95+
# 打印告警:运维按字面配置 JWT_SECRET 会误以为认证密钥已就绪(实际从未生效)
96+
_legacy_jwt_secret = os.getenv("JWT_SECRET", "")
97+
if _legacy_jwt_secret and not APP_CONFIG.get("jwt_secret", ""):
98+
logger.warning(
99+
"检测到 JWT_SECRET 已配置但 SUPABASE_JWT_SECRET 为空:"
100+
"网关只读取 SUPABASE_JWT_SECRET,JWT_SECRET 是死配置不会生效,"
101+
"请将密钥迁移到 SUPABASE_JWT_SECRET(或删除 JWT_SECRET 避免误导)。"
102+
)
103+
94104
# 启动日志:输当前 JWT 验证算法与配置状态(GW-2#1: 避免"已配置"假象)
95105
logger.info(
96106
"JWT 验证算法: %s (验证材料已配置=%s)",

‎server/ai-gateway/providers/base_provider.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,19 @@ async def wrapper(self, *args, **kwargs):
6565
# 未指定 feature 时取配置最大值,确保不会误杀慢请求
6666
timeout = max(TIMEOUT_CONFIG.values()) if TIMEOUT_CONFIG else 30
6767

68+
# GW-2#13: Key 轮询统一入口——原实现只在各 Provider 的 generate
69+
# 方法内手动调用 _rotate_api_key,视觉/流式/ASR/视频路径从不轮换,
70+
# 多 Key 配置形同虚设(RPM 压力全压主 Key,主 Key 熔断后无 Key 可换)。
71+
# 此处对所有被装饰方法统一轮询;无 KeyPool/单 Key 时内部短路返回。
72+
# FallbackProvider 等无装饰器方法不受影响。
73+
try:
74+
rotate = getattr(self, "_rotate_api_key", None)
75+
if rotate is not None:
76+
await rotate()
77+
except Exception:
78+
# 轮询失败不阻断业务调用(保持原行为)
79+
pass
80+
6881
last_error: Exception | None = None
6982
for attempt in range(max_retries + 1):
7083
try:

‎server/ai-gateway/providers/glm_provider.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,10 @@ async def transcribe(
173173
"text": text,
174174
"segments": [],
175175
"language": language,
176-
"confidence": 0.9,
176+
# GW-2#11: OpenAI 兼容 ASR 响应不提供置信度字段,
177+
# 原硬编码 0.9 是编造数据——改为 0.0 明确表示"无置信度数据",
178+
# 客户端不应据此做阈值过滤(与 QwenProvider GW-M15 对齐)
179+
"confidence": 0.0,
177180
"model": model,
178181
"latency_ms": latency_ms,
179182
}

‎server/ai-gateway/routers/transcribe.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ class TranscribeResponse(BaseModel):
4848
text: str = Field(..., description="转写文本")
4949
segments: list[TranscribeSegment] = Field(default_factory=list, description="时间分段列表")
5050
language: str = Field(..., description="检测到的语言")
51-
confidence: float = Field(..., description="置信度 0-1")
51+
# GW-2#11: ASR API 不提供置信度,恒为占位 0.0——前端不应据此做阈值过滤
52+
confidence: float = Field(default=0.0, description="置信度占位(ASR API 不提供,恒为 0)")
5253
model_used: str = Field(..., description="使用的模型名称")
5354
processing_time_ms: int = Field(..., description="请求耗时(毫秒)")
5455
warning: str | None = Field(default=None, description="降级提示(fallback 时透传,客户端据此识别失败)")

‎server/ai-gateway/routers/vision.py‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,14 @@ async def _run_chain(provider, model_name):
135135
if not isinstance(concepts, list):
136136
concepts = []
137137

138-
# 置信度评估:基于提取内容是否非空
139-
has_content = bool(text.strip())
140-
confidence = 0.9 if has_content else 0.3
138+
# GW-2#11: confidence 语义定义为"结构化提取完整度"而非模型置信度——
139+
# 原实现按文本是否非空硬编码 0.9/0.3,数值无统计意义且误导前端
140+
#(低质量识别也显示 0.9)。完整度 = text 权重 0.5 + 辅助字段
141+
#(公式/图表/要点/代码/概念)非空比例权重 0.5,反映解析覆盖度
142+
filled_text = 1.0 if text.strip() else 0.0
143+
aux_fields = [formulas, diagrams, key_points, code_blocks, concepts]
144+
aux_filled = sum(1 for v in aux_fields if v) / len(aux_fields)
145+
confidence = round(filled_text * 0.5 + aux_filled * 0.5, 2)
141146

142147
# 实际使用的模式
143148
effective_mode = result.get("mode", body.mode or "auto")

0 commit comments

Comments
 (0)