Skip to content

Commit 4f68548

Browse files
fix: melhorar mensagens de erro quando token OAuth expira
Substitui erros brutos com JSON da API por mensagens amigáveis em pt-BR com instruções claras de como re-autenticar. Mudanças principais: - Criado src/errors.ts com classes QwenAuthError e QwenApiError - Classificação automática por HTTP status (401/403, 429, 5xx) - Detalhes técnicos só aparecem com OPENCODE_QWEN_DEBUG=1 - Quando refresh falha, não continua com token expirado - console.error trocado por logTechnicalDetail condicional
1 parent e4913af commit 4f68548

6 files changed

Lines changed: 106 additions & 14 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "opencode-qwencode-auth",
3-
"version": "1.0.1",
3+
"version": "1.1.0",
44
"description": "Qwen OAuth authentication plugin for OpenCode - Access Qwen3-Coder models with your qwen.ai account",
55
"module": "index.ts",
66
"type": "module",

src/errors.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* Erros customizados do plugin Qwen Auth
3+
*
4+
* Fornece mensagens amigáveis para o usuário em vez de JSON bruto da API.
5+
* Detalhes técnicos só aparecem com OPENCODE_QWEN_DEBUG=1.
6+
*/
7+
8+
const REAUTH_HINT =
9+
'Execute "npx opencode-qwencode-auth" ou "qwen-code auth login" para re-autenticar.';
10+
11+
// ============================================
12+
// Erro de Autenticação
13+
// ============================================
14+
15+
export type AuthErrorKind = 'token_expired' | 'refresh_failed' | 'auth_required';
16+
17+
const AUTH_MESSAGES: Record<AuthErrorKind, string> = {
18+
token_expired: `[Qwen] Token expirado. ${REAUTH_HINT}`,
19+
refresh_failed: `[Qwen] Falha ao renovar token. ${REAUTH_HINT}`,
20+
auth_required: `[Qwen] Autenticacao necessaria. ${REAUTH_HINT}`,
21+
};
22+
23+
export class QwenAuthError extends Error {
24+
public readonly kind: AuthErrorKind;
25+
public readonly technicalDetail?: string;
26+
27+
constructor(kind: AuthErrorKind, technicalDetail?: string) {
28+
super(AUTH_MESSAGES[kind]);
29+
this.name = 'QwenAuthError';
30+
this.kind = kind;
31+
this.technicalDetail = technicalDetail;
32+
}
33+
}
34+
35+
// ============================================
36+
// Erro de API
37+
// ============================================
38+
39+
function classifyApiStatus(statusCode: number): string {
40+
if (statusCode === 401 || statusCode === 403) {
41+
return `[Qwen] Token invalido ou expirado. ${REAUTH_HINT}`;
42+
}
43+
if (statusCode === 429) {
44+
return '[Qwen] Limite de requisicoes atingido. Aguarde alguns minutos antes de tentar novamente.';
45+
}
46+
if (statusCode >= 500) {
47+
return `[Qwen] Servidor Qwen indisponivel (erro ${statusCode}). Tente novamente em alguns minutos.`;
48+
}
49+
return `[Qwen] Erro na API Qwen (${statusCode}). Verifique sua conexao e tente novamente.`;
50+
}
51+
52+
export class QwenApiError extends Error {
53+
public readonly statusCode: number;
54+
public readonly technicalDetail?: string;
55+
56+
constructor(statusCode: number, technicalDetail?: string) {
57+
super(classifyApiStatus(statusCode));
58+
this.name = 'QwenApiError';
59+
this.statusCode = statusCode;
60+
this.technicalDetail = technicalDetail;
61+
}
62+
}
63+
64+
// ============================================
65+
// Helper de log condicional
66+
// ============================================
67+
68+
/**
69+
* Loga detalhes técnicos apenas quando debug está ativo.
70+
*/
71+
export function logTechnicalDetail(detail: string): void {
72+
if (process.env.OPENCODE_QWEN_DEBUG === '1') {
73+
console.debug('[Qwen Debug]', detail);
74+
}
75+
}

src/index.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ import {
2323
tokenResponseToCredentials,
2424
refreshAccessToken,
2525
} from './qwen/oauth.js';
26+
import { logTechnicalDetail } from './errors.js';
27+
export { QwenAuthError, QwenApiError } from './errors.js';
28+
export type { AuthErrorKind } from './errors.js';
2629

2730
// ============================================
2831
// Helpers
@@ -102,14 +105,25 @@ export const QwenAuthPlugin = async (_input: unknown) => {
102105
accessToken = refreshed.accessToken;
103106
saveCredentials(refreshed);
104107
} catch (e) {
105-
console.error('[Qwen] Token refresh failed:', e);
108+
const detail = e instanceof Error ? e.message : String(e);
109+
logTechnicalDetail(`Token refresh falhou: ${detail}`);
110+
// Não continuar com token expirado - tentar fallback
111+
accessToken = undefined;
106112
}
107113
}
108114

109115
// Fallback para credenciais do qwen-code
110116
if (!accessToken) {
111117
const creds = checkExistingCredentials();
112-
if (creds) accessToken = creds.accessToken;
118+
if (creds) {
119+
accessToken = creds.accessToken;
120+
} else {
121+
console.warn(
122+
'[Qwen] Token expirado e sem credenciais alternativas. ' +
123+
'Execute "npx opencode-qwencode-auth" ou "qwen-code auth login" para re-autenticar.'
124+
);
125+
return null;
126+
}
113127
}
114128

115129
if (!accessToken) return null;

src/plugin/auth.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
1010

1111
import type { QwenCredentials } from '../types.js';
1212
import { refreshAccessToken, isCredentialsExpired } from '../qwen/oauth.js';
13+
import { logTechnicalDetail } from '../errors.js';
1314

1415
/**
1516
* Get the path to the credentials file
@@ -58,7 +59,7 @@ export function loadCredentials(): QwenCredentials | null {
5859

5960
return null;
6061
} catch (error) {
61-
console.error('Error loading Qwen credentials:', error);
62+
logTechnicalDetail(`Erro ao carregar credenciais: ${error}`);
6263
return null;
6364
}
6465
}
@@ -102,7 +103,7 @@ export async function getValidCredentials(): Promise<QwenCredentials | null> {
102103
credentials = await refreshAccessToken(credentials.refreshToken);
103104
saveCredentials(credentials);
104105
} catch (error) {
105-
console.error('Failed to refresh token:', error);
106+
logTechnicalDetail(`Falha no refresh: ${error}`);
106107
return null;
107108
}
108109
}

src/plugin/client.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
StreamChunk
1313
} from '../types.js';
1414
import { getValidCredentials, loadCredentials, isCredentialsExpired } from './auth.js';
15+
import { QwenAuthError, QwenApiError } from '../errors.js';
1516

1617
/**
1718
* QwenClient - Makes authenticated API calls to Qwen
@@ -66,7 +67,7 @@ export class QwenClient {
6667
*/
6768
private getAuthHeader(): string {
6869
if (!this.credentials) {
69-
throw new Error('Not authenticated. Please run the OAuth flow first.');
70+
throw new QwenAuthError('auth_required');
7071
}
7172
return `Bearer ${this.credentials.accessToken}`;
7273
}
@@ -87,7 +88,7 @@ export class QwenClient {
8788
if (!this.credentials) {
8889
const initialized = await this.initialize();
8990
if (!initialized) {
90-
throw new Error('No valid Qwen credentials found. Please authenticate first.');
91+
throw new QwenAuthError('auth_required');
9192
}
9293
}
9394

@@ -106,7 +107,7 @@ export class QwenClient {
106107
if (!response.ok) {
107108
const errorText = await response.text();
108109
this.log('API Error:', response.status, errorText);
109-
throw new Error(`Qwen API error: ${response.status} - ${errorText}`);
110+
throw new QwenApiError(response.status, errorText);
110111
}
111112

112113
const data = await response.json();
@@ -122,7 +123,7 @@ export class QwenClient {
122123
if (!this.credentials) {
123124
const initialized = await this.initialize();
124125
if (!initialized) {
125-
throw new Error('No valid Qwen credentials found. Please authenticate first.');
126+
throw new QwenAuthError('auth_required');
126127
}
127128
}
128129

@@ -142,7 +143,7 @@ export class QwenClient {
142143
if (!response.ok) {
143144
const errorText = await response.text();
144145
this.log('API Error:', response.status, errorText);
145-
throw new Error(`Qwen API error: ${response.status} - ${errorText}`);
146+
throw new QwenApiError(response.status, errorText);
146147
}
147148

148149
const reader = response.body?.getReader();

src/qwen/oauth.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { randomBytes, createHash, randomUUID } from 'node:crypto';
99

1010
import { QWEN_OAUTH_CONFIG } from '../constants.js';
1111
import type { QwenCredentials } from '../types.js';
12+
import { QwenAuthError, logTechnicalDetail } from '../errors.js';
1213

1314
/**
1415
* Device authorization response from Qwen OAuth
@@ -87,9 +88,8 @@ export async function requestDeviceAuthorization(
8788

8889
if (!response.ok) {
8990
const errorData = await response.text();
90-
throw new Error(
91-
`Device authorization failed: ${response.status} ${response.statusText}. Response: ${errorData}`
92-
);
91+
logTechnicalDetail(`Device auth HTTP ${response.status}: ${errorData}`);
92+
throw new QwenAuthError('auth_required', `HTTP ${response.status}: ${errorData}`);
9393
}
9494

9595
const result = await response.json() as DeviceAuthorizationResponse;
@@ -193,7 +193,8 @@ export async function refreshAccessToken(refreshToken: string): Promise<QwenCred
193193

194194
if (!response.ok) {
195195
const errorText = await response.text();
196-
throw new Error(`Token refresh failed: ${response.status} - ${errorText}`);
196+
logTechnicalDetail(`Token refresh HTTP ${response.status}: ${errorText}`);
197+
throw new QwenAuthError('refresh_failed', `HTTP ${response.status}: ${errorText}`);
197198
}
198199

199200
const data = await response.json() as TokenResponse;

0 commit comments

Comments
 (0)