opencode-qwencode-auth 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/errors.ts +75 -0
- package/src/index.ts +16 -2
- package/src/plugin/auth.ts +3 -2
- package/src/plugin/client.ts +6 -5
- package/src/qwen/oauth.ts +5 -4
package/package.json
CHANGED
package/src/errors.ts
ADDED
|
@@ -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
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,9 @@ import {
|
|
|
23
23
|
tokenResponseToCredentials,
|
|
24
24
|
refreshAccessToken,
|
|
25
25
|
} from './qwen/oauth.js';
|
|
26
|
+
import { logTechnicalDetail } from './errors.js';
|
|
27
|
+
export { QwenAuthError, QwenApiError } from './errors.js';
|
|
28
|
+
export type { AuthErrorKind } from './errors.js';
|
|
26
29
|
|
|
27
30
|
// ============================================
|
|
28
31
|
// Helpers
|
|
@@ -102,14 +105,25 @@ export const QwenAuthPlugin = async (_input: unknown) => {
|
|
|
102
105
|
accessToken = refreshed.accessToken;
|
|
103
106
|
saveCredentials(refreshed);
|
|
104
107
|
} catch (e) {
|
|
105
|
-
|
|
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;
|
|
106
112
|
}
|
|
107
113
|
}
|
|
108
114
|
|
|
109
115
|
// Fallback para credenciais do qwen-code
|
|
110
116
|
if (!accessToken) {
|
|
111
117
|
const creds = checkExistingCredentials();
|
|
112
|
-
if (creds)
|
|
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
|
+
}
|
|
113
127
|
}
|
|
114
128
|
|
|
115
129
|
if (!accessToken) return null;
|
package/src/plugin/auth.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
|
10
10
|
|
|
11
11
|
import type { QwenCredentials } from '../types.js';
|
|
12
12
|
import { refreshAccessToken, isCredentialsExpired } from '../qwen/oauth.js';
|
|
13
|
+
import { logTechnicalDetail } from '../errors.js';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Get the path to the credentials file
|
|
@@ -58,7 +59,7 @@ export function loadCredentials(): QwenCredentials | null {
|
|
|
58
59
|
|
|
59
60
|
return null;
|
|
60
61
|
} catch (error) {
|
|
61
|
-
|
|
62
|
+
logTechnicalDetail(`Erro ao carregar credenciais: ${error}`);
|
|
62
63
|
return null;
|
|
63
64
|
}
|
|
64
65
|
}
|
|
@@ -102,7 +103,7 @@ export async function getValidCredentials(): Promise<QwenCredentials | null> {
|
|
|
102
103
|
credentials = await refreshAccessToken(credentials.refreshToken);
|
|
103
104
|
saveCredentials(credentials);
|
|
104
105
|
} catch (error) {
|
|
105
|
-
|
|
106
|
+
logTechnicalDetail(`Falha no refresh: ${error}`);
|
|
106
107
|
return null;
|
|
107
108
|
}
|
|
108
109
|
}
|
package/src/plugin/client.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
StreamChunk
|
|
13
13
|
} from '../types.js';
|
|
14
14
|
import { getValidCredentials, loadCredentials, isCredentialsExpired } from './auth.js';
|
|
15
|
+
import { QwenAuthError, QwenApiError } from '../errors.js';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* QwenClient - Makes authenticated API calls to Qwen
|
|
@@ -66,7 +67,7 @@ export class QwenClient {
|
|
|
66
67
|
*/
|
|
67
68
|
private getAuthHeader(): string {
|
|
68
69
|
if (!this.credentials) {
|
|
69
|
-
throw new
|
|
70
|
+
throw new QwenAuthError('auth_required');
|
|
70
71
|
}
|
|
71
72
|
return `Bearer ${this.credentials.accessToken}`;
|
|
72
73
|
}
|
|
@@ -87,7 +88,7 @@ export class QwenClient {
|
|
|
87
88
|
if (!this.credentials) {
|
|
88
89
|
const initialized = await this.initialize();
|
|
89
90
|
if (!initialized) {
|
|
90
|
-
throw new
|
|
91
|
+
throw new QwenAuthError('auth_required');
|
|
91
92
|
}
|
|
92
93
|
}
|
|
93
94
|
|
|
@@ -106,7 +107,7 @@ export class QwenClient {
|
|
|
106
107
|
if (!response.ok) {
|
|
107
108
|
const errorText = await response.text();
|
|
108
109
|
this.log('API Error:', response.status, errorText);
|
|
109
|
-
throw new
|
|
110
|
+
throw new QwenApiError(response.status, errorText);
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
const data = await response.json();
|
|
@@ -122,7 +123,7 @@ export class QwenClient {
|
|
|
122
123
|
if (!this.credentials) {
|
|
123
124
|
const initialized = await this.initialize();
|
|
124
125
|
if (!initialized) {
|
|
125
|
-
throw new
|
|
126
|
+
throw new QwenAuthError('auth_required');
|
|
126
127
|
}
|
|
127
128
|
}
|
|
128
129
|
|
|
@@ -142,7 +143,7 @@ export class QwenClient {
|
|
|
142
143
|
if (!response.ok) {
|
|
143
144
|
const errorText = await response.text();
|
|
144
145
|
this.log('API Error:', response.status, errorText);
|
|
145
|
-
throw new
|
|
146
|
+
throw new QwenApiError(response.status, errorText);
|
|
146
147
|
}
|
|
147
148
|
|
|
148
149
|
const reader = response.body?.getReader();
|
package/src/qwen/oauth.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { randomBytes, createHash, randomUUID } from 'node:crypto';
|
|
|
9
9
|
|
|
10
10
|
import { QWEN_OAUTH_CONFIG } from '../constants.js';
|
|
11
11
|
import type { QwenCredentials } from '../types.js';
|
|
12
|
+
import { QwenAuthError, logTechnicalDetail } from '../errors.js';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Device authorization response from Qwen OAuth
|
|
@@ -87,9 +88,8 @@ export async function requestDeviceAuthorization(
|
|
|
87
88
|
|
|
88
89
|
if (!response.ok) {
|
|
89
90
|
const errorData = await response.text();
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
);
|
|
91
|
+
logTechnicalDetail(`Device auth HTTP ${response.status}: ${errorData}`);
|
|
92
|
+
throw new QwenAuthError('auth_required', `HTTP ${response.status}: ${errorData}`);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
const result = await response.json() as DeviceAuthorizationResponse;
|
|
@@ -193,7 +193,8 @@ export async function refreshAccessToken(refreshToken: string): Promise<QwenCred
|
|
|
193
193
|
|
|
194
194
|
if (!response.ok) {
|
|
195
195
|
const errorText = await response.text();
|
|
196
|
-
|
|
196
|
+
logTechnicalDetail(`Token refresh HTTP ${response.status}: ${errorText}`);
|
|
197
|
+
throw new QwenAuthError('refresh_failed', `HTTP ${response.status}: ${errorText}`);
|
|
197
198
|
}
|
|
198
199
|
|
|
199
200
|
const data = await response.json() as TokenResponse;
|