feat(cloud-status): classify blocked accounts and surface validation state

This commit is contained in:
Draculabo committed 2026-04-11 16:46:36 +08:00
1 parent bcdf2a6c24
commit 388fbcb2a9
4 files changed
+137 -8

No files matched your search

+73
View File
@@ -0,0 +1,73 @@
export const RATE_LIMIT_HINTS = [
'resource_exhausted',
'too many requests',
'429',
'rate limit',
'rate-limited',
'risk control',
'risk-controlled',
'frozen',
] as const;
export const OAUTH_HINTS = [
'unauthorized_client',
'invalid_client',
'invalid_grant',
'oauth client',
'not authorized',
'token expired',
'reauth',
'verify your account',
'further action is required',
'validation required',
'validation_url',
'appeal_url',
] as const;
function includesAny(text: string, hints: readonly string[]): boolean {
return hints.some((hint) => text.includes(hint));
}
export function isRateLimitReason(reason: string): boolean {
return includesAny(reason.toLowerCase(), RATE_LIMIT_HINTS);
}
export function isOAuthReauthReason(reason: string): boolean {
return includesAny(reason.toLowerCase(), OAUTH_HINTS);
}
export function extractErrorMessage(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
if (typeof error === 'string') {
return error;
}
return String(error);
}
export function classifyAccountStatusFromError(
error: unknown,
): { status: 'rate_limited' | 'expired'; reason: string } | null {
const reason = extractErrorMessage(error).trim();
if (!reason) {
return null;
}
const normalizedReason = reason.toLowerCase();
if (isRateLimitReason(normalizedReason)) {
return { status: 'rate_limited', reason };
}
if (isOAuthReauthReason(normalizedReason)) {
return { status: 'expired', reason };
}
if (normalizedReason.includes('forbidden')) {
return { status: 'rate_limited', reason };
}
if (normalizedReason.includes('unauthorized')) {
return { status: 'expired', reason };
}
return null;
}