From 4396c090daf6002326a6cad9f6685b339d4f149e Mon Sep 17 00:00:00 2001 From: Good-design-999 <283691239+Good-design-999@users.noreply.github.com> Date: Sun, 4 Oct 2026 00:07:21 +0800 Subject: [PATCH] Surface credential sync failures in the management UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A failed credential sync only produced a generic "操作失败" message and the dashboard never rendered the recorded reason, so an account whose refresh token was rejected looked identical to a healthy disabled one. - Expose the ledger's last sync error as sync_error in the credential inventory (field was already whitelisted, never populated). - Show last_error_code / last_failure_at and sync_error under 认证健康. - Include the exception type in manual action failures instead of the generic message only. --- app/credential_actions.py | 4 ++-- app/gateway_management.py | 1 + web/src/pages/Credentials.tsx | 20 ++++++++++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/app/credential_actions.py b/app/credential_actions.py index 4141b06..3226ffa 100644 --- a/app/credential_actions.py +++ b/app/credential_actions.py @@ -50,11 +50,11 @@ def run(gateway, action, identity=None, *, consent_revision=None): result.update(travel=followup, checkin_ok=result["ok"], ok=result["ok"] if followup.get("buddy_blocked") else result["ok"] and followup["ok"], message=result["message"] + ";" + followup["message"]) - except Exception: + except Exception as error: # Upstream exception text may contain headers or credential file paths. if action == "checkin" and result["ok"]: result["checkin_ok"] = True - result.update(ok=False, message="操作失败,保留已有数据;请检查账号状态后重试") + result.update(ok=False, message=f"操作失败({type(error).__name__}),保留已有数据;请检查账号状态后重试") results.append(result) _audit(config, result, started) response = {"ok": bool(results) and all(r["ok"] for r in results), "results": results} diff --git a/app/gateway_management.py b/app/gateway_management.py index c21ee33..8293bba 100644 --- a/app/gateway_management.py +++ b/app/gateway_management.py @@ -59,6 +59,7 @@ def admin_credential_inventory(self): "http_403" if entry.get("last_error") == "backend HTTP 403" else "credential_error" if entry.get("last_error") else None), last_failure_at=entry.get("last_failure_at"), + sync_error=(balance.get("error") or entry.get("last_error") or None), cooldowns=cooldowns, credits=balance.get("credits") or None, sync_pending=path in pool._sync_pending or path in pool._syncing or path in pool._sync_retry, catalog_ready=(self.CONFIG.get("account_catalogs") or {}).get(identity, {}).get("models") is not None, diff --git a/web/src/pages/Credentials.tsx b/web/src/pages/Credentials.tsx index 3a87787..5de8bac 100644 --- a/web/src/pages/Credentials.tsx +++ b/web/src/pages/Credentials.tsx @@ -37,6 +37,15 @@ function expiry(value: unknown, milliseconds = false) { ? new Date(milliseconds ? value : value * 1000).toLocaleString("zh-CN") : text(value); } +const lastErrorLabels: Record = { + http_401: "上游 401(登录态被拒)", + http_403: "上游 403(无权限)", + credential_error: "凭证同步失败(详见操作结果)", +}; +function lastErrorLabel(value: unknown) { + const key = text(value); + return lastErrorLabels[key] ?? key; +} export { safeOAuthUrl } from "../OAuth"; function boundModels(credential: Credential): string[] { return Array.isArray(credential.bindings) @@ -430,6 +439,17 @@ export function Credentials() { : text(c.health)} {c.token_expired === true ? "Token 已过期" : ""} + {c.last_error_code != null && ( + + 最近错误:{text(lastErrorLabel(c.last_error_code))} + {number(c.last_failure_at) !== null + ? ` · ${expiry(c.last_failure_at)}` + : ""} + + )} + {typeof c.sync_error === "string" && c.sync_error !== "" && ( + 失败原因:{text(c.sync_error)} + )} {cooldowns ? (