|
20 | 20 | aggregate, |
21 | 21 | ) |
22 | 22 | from ..config import live |
| 23 | +from ..db.crypto import CredentialDecryptError |
23 | 24 | from ..db.repo import CredentialRepository |
24 | 25 | from ..provider.base import ( |
25 | 26 | QUEUE_DEFAULT_WAIT_SECONDS, |
@@ -612,7 +613,7 @@ def terminal(message: str, code: str) -> bytes: |
612 | 613 | # 会让三次排队就把整个请求打成 503。 |
613 | 614 | credential_id, credential_data = pinned |
614 | 615 | pinned = None |
615 | | - if self._deps.credentials.credential_data(credential_id) is None: |
| 616 | + if self._safe_credential_data(credential_id) is None: |
616 | 617 | continue |
617 | 618 | provider_id = self._deps.credentials.provider_of(credential_id) |
618 | 619 | state.provider, state.credential_id = provider_id or "-", credential_id |
@@ -1005,7 +1006,7 @@ async def _complete_model(self, request: ChatRequest, target: ModelTarget, |
1005 | 1006 | if pinned is not None: |
1006 | 1007 | credential_id, credential_data = pinned |
1007 | 1008 | pinned = None |
1008 | | - if self._deps.credentials.credential_data(credential_id) is None: |
| 1009 | + if self._safe_credential_data(credential_id) is None: |
1009 | 1010 | continue |
1010 | 1011 | provider_id = self._deps.credentials.provider_of(credential_id) |
1011 | 1012 | last_provider, last_credential = provider_id or "-", credential_id |
@@ -1256,10 +1257,33 @@ async def _pick(self, target: ModelTarget, tried: set[str], |
1256 | 1257 | return None |
1257 | 1258 | waited = True |
1258 | 1259 | continue |
1259 | | - credential_data = self._deps.credentials.credential_data(credential_id) |
| 1260 | + credential_data = self._safe_credential_data(credential_id) |
1260 | 1261 | if credential_data is not None: |
1261 | 1262 | return credential_id, credential_data |
1262 | | - tried.add(credential_id) # 并发删除:标记后重选 |
| 1263 | + tried.add(credential_id) # 并发删除 / 解不开:标记后重选 |
| 1264 | + |
| 1265 | + def _safe_credential_data(self, credential_id: str) -> dict[str, Any] | None: |
| 1266 | + """取凭证明文;**解不开**时跳过该凭证而不是让整个请求失败。 |
| 1267 | +
|
| 1268 | + 解密失败(APP_SECRET 换过 / 密文损坏)早先直接冒泡 → 一条坏凭证 |
| 1269 | + 就让**整个聊天请求**回 500(实测 2026-10-26 14:48: |
| 1270 | + `/v1/chat/completions` 500,栈顶「凭证解密失败:APP_SECRET 是否被 |
| 1271 | + 更换过?」),其余好凭证白白连坐。 |
| 1272 | +
|
| 1273 | + 这里吞掉它并返回 None(= 「这条凭证用不了」,与并发删除同义)→ |
| 1274 | + 选号继续找下一个号。单条探测端点仍把该错误如实报给管理员 |
| 1275 | + (见 `webapp.handlers` 的可行动 500),两处口径不冲突:一个是 |
| 1276 | + 「这个号不能用,换一个」,一个是「请处理这个号」。 |
| 1277 | +
|
| 1278 | + 走 `credential_data`(而不是仓储的 `credential_data_or_skip`): |
| 1279 | + 测试替身只实现前者,后者是仓储层的具体能力。 |
| 1280 | + """ |
| 1281 | + try: |
| 1282 | + return self._deps.credentials.credential_data(credential_id) |
| 1283 | + except CredentialDecryptError: |
| 1284 | + logger.error("凭证解密失败(凭证 %s):APP_SECRET 是否被更换过?" |
| 1285 | + "该凭证已跳过,其余凭证不受影响", credential_id) |
| 1286 | + return None |
1263 | 1287 |
|
1264 | 1288 | async def _wait_short_cooldown(self, target: ModelTarget, |
1265 | 1289 | tried: set[str]) -> bool: |
|
0 commit comments