@hung319/opencode-qwen 1.1.0 → 1.1.5

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/README.md CHANGED
@@ -26,7 +26,7 @@ Or add to your `opencode.json` with specific version:
26
26
 
27
27
  ```json
28
28
  {
29
- "plugin": ["@hung319/opencode-qwen@1.0.0"]
29
+ "plugin": ["@hung319/opencode-qwen@1.1.5"]
30
30
  }
31
31
  ```
32
32
 
@@ -154,9 +154,11 @@ const apiKeyData=getApiKeyData();if(!apiKeyData)return;copyToClipboard(apiKeyDat
154
154
  - **GitHub Repository**: https://github.com/hung319/opencode-qwen
155
155
  - **Issues**: https://github.com/hung319/opencode-qwen/issues
156
156
 
157
- ## License
157
+ ## Features Added
158
158
 
159
- MIT
159
+ - **Email extraction from validation API**: Now uses actual user email from API response instead of default 'qwen-token-user'
160
+ - **Token validation improvements**: Better handling of validation and timeout mechanisms
161
+ - **Enhanced user experience**: More personalized account management with real email addresses
160
162
 
161
163
  ## License
162
164
 
package/dist/plugin.js CHANGED
@@ -133,13 +133,16 @@ export const createQwenPlugin = (id) => async ({ client, directory }) => {
133
133
  loader: async (getAuth, provider) => {
134
134
  await getAuth();
135
135
  const am = await AccountManager.loadFromDisk(config.account_selection_strategy);
136
- // Auto-refresh tokens for all accounts on startup
136
+ // Auto-refresh tokens for all accounts on startup with timeout
137
137
  const accounts = am.getAccounts();
138
138
  for (const account of accounts) {
139
139
  if (account.apiKey) {
140
140
  try {
141
141
  logger.log(`Auto-refreshing token for account: ${account.email}`);
142
- const refreshResult = await refreshToken(account.apiKey);
142
+ // Use a timeout to prevent hanging during token refresh
143
+ const refreshPromise = refreshToken(account.apiKey);
144
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Token refresh timed out')), 15000));
145
+ const refreshResult = await Promise.race([refreshPromise, timeoutPromise]);
143
146
  // Update the account with the new token
144
147
  account.apiKey = refreshResult.apiKey;
145
148
  if (refreshResult.expiresAt) {
@@ -392,8 +395,11 @@ export const createQwenPlugin = (id) => async ({ client, directory }) => {
392
395
  break;
393
396
  }
394
397
  try {
395
- const validation = await validateToken(token);
396
- const email = await promptEmail();
398
+ // Add timeout to prevent hanging during validation
399
+ const validationPromise = validateToken(token);
400
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Token validation timed out')), 15000));
401
+ const validation = await Promise.race([validationPromise, timeoutPromise]);
402
+ const email = validation.email || await promptEmail();
397
403
  accounts.push({ apiKey: token, email });
398
404
  const isFirstAccount = accounts.length === 1;
399
405
  const am = await AccountManager.loadFromDisk(config.account_selection_strategy);
@@ -434,7 +440,12 @@ export const createQwenPlugin = (id) => async ({ client, directory }) => {
434
440
  callback: async () => ({ type: 'failed' })
435
441
  });
436
442
  }
437
- break;
443
+ // Allow user to try another token instead of breaking completely
444
+ const addAnother = await promptAddAnotherAccount(accounts.length);
445
+ if (!addAnother) {
446
+ break;
447
+ }
448
+ continue; // Continue to ask for another token
438
449
  }
439
450
  }
440
451
  const primary = accounts[0];
@@ -50,15 +50,16 @@ export interface QwenModelsResponse {
50
50
  data: QwenModel[];
51
51
  }
52
52
  /**
53
- * Fetch models from Qwen API
54
- * @param apiKey - API key or token
55
- */
56
- export declare function fetchModelsFromAPI(apiKey: string): Promise<QwenModel[] | null>;
53
+ * Fetch models from Qwen API
54
+ * @param apiKey - API key or token
55
+ * @param signal - Optional AbortSignal for cancellation
56
+ */
57
+ export declare function fetchModelsFromAPI(apiKey: string, signal?: AbortSignal): Promise<QwenModel[] | null>;
57
58
  /**
58
59
  * Transform Qwen models to OpenCode format
59
60
  */
60
61
  export declare function transformModelsToOpenCode(models: QwenModel[]): Record<string, any>;
61
62
  /**
62
- * Get cached models or fetch new ones
63
- */
64
- export declare function getModels(apiKey: string | undefined): Promise<Record<string, any>>;
63
+ * Get cached models or fetch new ones
64
+ */
65
+ export declare function getModels(apiKey: string | undefined, signal?: AbortSignal): Promise<Record<string, any>>;
@@ -4,10 +4,11 @@
4
4
  */
5
5
  import * as logger from '../plugin/logger.js';
6
6
  /**
7
- * Fetch models from Qwen API
8
- * @param apiKey - API key or token
9
- */
10
- export async function fetchModelsFromAPI(apiKey) {
7
+ * Fetch models from Qwen API
8
+ * @param apiKey - API key or token
9
+ * @param signal - Optional AbortSignal for cancellation
10
+ */
11
+ export async function fetchModelsFromAPI(apiKey, signal) {
11
12
  try {
12
13
  const headers = {
13
14
  'Content-Type': 'application/json',
@@ -17,6 +18,7 @@ export async function fetchModelsFromAPI(apiKey) {
17
18
  const response = await fetch(`${process.env.QWEN_BASE_URL || 'https://qwen.aikit.club'}/v1/models`, {
18
19
  method: 'GET',
19
20
  headers,
21
+ signal
20
22
  });
21
23
  if (!response.ok) {
22
24
  logger.warn(`Failed to fetch models: ${response.status} ${response.statusText}`);
@@ -27,6 +29,10 @@ export async function fetchModelsFromAPI(apiKey) {
27
29
  return data.data || null;
28
30
  }
29
31
  catch (error) {
32
+ if (error.name === 'AbortError') {
33
+ logger.warn('Model fetching was aborted (timeout or cancellation)');
34
+ throw error; // Re-throw AbortError so it can be handled upstream
35
+ }
30
36
  logger.warn(`Error fetching models from API: ${error.message}`);
31
37
  return null;
32
38
  }
@@ -98,14 +104,22 @@ export function transformModelsToOpenCode(models) {
98
104
  return result;
99
105
  }
100
106
  /**
101
- * Get cached models or fetch new ones
102
- */
103
- export async function getModels(apiKey) {
107
+ * Get cached models or fetch new ones
108
+ */
109
+ export async function getModels(apiKey, signal) {
104
110
  // Try to fetch from API if credentials available
105
111
  if (apiKey) {
106
- const apiModels = await fetchModelsFromAPI(apiKey);
107
- if (apiModels) {
108
- return transformModelsToOpenCode(apiModels);
112
+ try {
113
+ const apiModels = await fetchModelsFromAPI(apiKey, signal);
114
+ if (apiModels) {
115
+ return transformModelsToOpenCode(apiModels);
116
+ }
117
+ }
118
+ catch (error) {
119
+ if (error.name === 'AbortError') {
120
+ throw error; // Re-throw AbortError so it can be handled upstream
121
+ }
122
+ logger.warn(`Error in getModels: ${error.message}`);
109
123
  }
110
124
  }
111
125
  // Return empty object if unable to fetch
@@ -4,10 +4,22 @@ export interface QwenTokenResult {
4
4
  authMethod: 'token';
5
5
  expiresAt?: number;
6
6
  }
7
- export declare function validateToken(apiKey: string): Promise<QwenTokenResult>;
8
7
  export interface QwenRefreshResponse {
9
8
  timestamp: number;
10
9
  expires_at: string;
11
10
  access_token: string;
12
11
  }
12
+ export interface QwenValidateResponse {
13
+ id: string;
14
+ email: string;
15
+ name: string;
16
+ role: string;
17
+ profile_image_url?: string;
18
+ tier: string;
19
+ token_type: string;
20
+ permissions: any;
21
+ timestamp: number;
22
+ access_token: string;
23
+ }
24
+ export declare function validateToken(apiKey: string): Promise<QwenTokenResult>;
13
25
  export declare function refreshToken(currentToken: string): Promise<QwenTokenResult>;
@@ -1,85 +1,130 @@
1
1
  import { QWEN_CONSTANTS } from '../constants';
2
2
  export async function validateToken(apiKey) {
3
3
  // First validate using header-based auth
4
- const response = await fetch(`${QWEN_CONSTANTS.BASE_URL}/models`, {
5
- headers: {
6
- Authorization: `Bearer ${apiKey}`,
7
- 'User-Agent': QWEN_CONSTANTS.USER_AGENT
8
- }
9
- });
10
- if (!response.ok) {
11
- // Try validation endpoint with token in request body as fallback
12
- const validateResponse = await fetch(QWEN_CONSTANTS.VALIDATE_URL, {
13
- method: 'POST',
4
+ // Use a minimal fetch that only checks if the token is valid
5
+ try {
6
+ const controller = new AbortController();
7
+ const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
8
+ const response = await fetch(`${QWEN_CONSTANTS.BASE_URL}/models`, {
14
9
  headers: {
15
- 'Content-Type': 'application/json',
10
+ Authorization: `Bearer ${apiKey}`,
16
11
  'User-Agent': QWEN_CONSTANTS.USER_AGENT
17
12
  },
18
- body: JSON.stringify({ token: apiKey })
13
+ signal: controller.signal
19
14
  });
20
- if (!validateResponse.ok) {
21
- const errorText = await validateResponse.text().catch(() => '');
22
- throw new Error(`Token validation failed: ${validateResponse.status} - ${errorText}`);
15
+ clearTimeout(timeoutId);
16
+ if (!response.ok) {
17
+ // Try validation endpoint with token in request body as fallback
18
+ const validateController = new AbortController();
19
+ const validateTimeoutId = setTimeout(() => validateController.abort(), 10000); // 10 second timeout
20
+ const validateResponse = await fetch(QWEN_CONSTANTS.VALIDATE_URL, {
21
+ method: 'POST',
22
+ headers: {
23
+ 'Content-Type': 'application/json',
24
+ 'User-Agent': QWEN_CONSTANTS.USER_AGENT
25
+ },
26
+ body: JSON.stringify({ token: apiKey }),
27
+ signal: validateController.signal
28
+ });
29
+ clearTimeout(validateTimeoutId);
30
+ if (!validateResponse.ok) {
31
+ const errorText = await validateResponse.text().catch(() => '');
32
+ throw new Error(`Token validation failed: ${validateResponse.status} - ${errorText}`);
33
+ }
34
+ // Extract email from the validation response
35
+ const validationData = await validateResponse.json();
36
+ const email = validationData.email || 'qwen-token-user';
37
+ return {
38
+ apiKey,
39
+ email,
40
+ authMethod: 'token'
41
+ };
23
42
  }
24
- }
25
- // Try to get user info if possible, but don't fail if not available
26
- let email;
27
- try {
28
- // Attempt to get more user information using models endpoint
29
- const modelsResponse = await fetch(`${QWEN_CONSTANTS.BASE_URL}/models`, {
30
- headers: {
31
- Authorization: `Bearer ${apiKey}`,
32
- 'User-Agent': QWEN_CONSTANTS.USER_AGENT
43
+ else {
44
+ // If the models endpoint worked, try to get user info from validation endpoint anyway
45
+ const validateController = new AbortController();
46
+ const validateTimeoutId = setTimeout(() => validateController.abort(), 10000); // 10 second timeout
47
+ const validateResponse = await fetch(QWEN_CONSTANTS.VALIDATE_URL, {
48
+ method: 'POST',
49
+ headers: {
50
+ 'Content-Type': 'application/json',
51
+ 'User-Agent': QWEN_CONSTANTS.USER_AGENT
52
+ },
53
+ body: JSON.stringify({ token: apiKey }),
54
+ signal: validateController.signal
55
+ });
56
+ clearTimeout(validateTimeoutId);
57
+ if (validateResponse.ok) {
58
+ // Extract email from the validation response
59
+ const validationData = await validateResponse.json();
60
+ const email = validationData.email || 'qwen-token-user';
61
+ return {
62
+ apiKey,
63
+ email,
64
+ authMethod: 'token'
65
+ };
66
+ }
67
+ else {
68
+ return {
69
+ apiKey,
70
+ email: 'qwen-token-user',
71
+ authMethod: 'token'
72
+ };
33
73
  }
34
- });
35
- if (modelsResponse.ok) {
36
- const modelsData = await modelsResponse.json();
37
- // If the API returns user-specific data, extract it here
38
74
  }
39
75
  }
40
- catch (e) {
41
- // If we can't get user details, that's OK - we still have a valid token
42
- console.debug('Could not fetch user details, but token is valid');
76
+ catch (error) {
77
+ if (error.name === 'AbortError') {
78
+ throw new Error('Token validation timed out. Please check your connection.');
79
+ }
80
+ throw error;
43
81
  }
44
- return {
45
- apiKey,
46
- email: email || 'qwen-token-user',
47
- authMethod: 'token'
48
- };
49
82
  }
50
83
  export async function refreshToken(currentToken) {
51
- const response = await fetch(QWEN_CONSTANTS.REFRESH_URL, {
52
- method: 'POST',
53
- headers: {
54
- 'Content-Type': 'application/json',
55
- 'User-Agent': QWEN_CONSTANTS.USER_AGENT
56
- },
57
- body: JSON.stringify({ token: currentToken })
58
- });
59
- if (!response.ok) {
60
- const errorText = await response.text().catch(() => '');
61
- throw new Error(`Token refresh failed: ${response.status} - ${errorText}`);
62
- }
63
- const data = await response.json();
64
- if (!data.access_token) {
65
- throw new Error('Refresh token response did not contain new access token');
84
+ try {
85
+ const controller = new AbortController();
86
+ const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
87
+ const response = await fetch(QWEN_CONSTANTS.REFRESH_URL, {
88
+ method: 'POST',
89
+ headers: {
90
+ 'Content-Type': 'application/json',
91
+ 'User-Agent': QWEN_CONSTANTS.USER_AGENT
92
+ },
93
+ body: JSON.stringify({ token: currentToken }),
94
+ signal: controller.signal
95
+ });
96
+ clearTimeout(timeoutId);
97
+ if (!response.ok) {
98
+ const errorText = await response.text().catch(() => '');
99
+ throw new Error(`Token refresh failed: ${response.status} - ${errorText}`);
100
+ }
101
+ const data = await response.json();
102
+ if (!data.access_token) {
103
+ throw new Error('Refresh token response did not contain new access token');
104
+ }
105
+ // Validate the new token and return with expiration information
106
+ const validatedToken = await validateToken(data.access_token);
107
+ // Try to extract expiration time from the response
108
+ let expiresAt;
109
+ if (data.expires_at) {
110
+ // Try to parse the expiration time from the string format
111
+ // Format: "2026-03-15 01:46:05 UTC (2026-03-15 07:16:05 IST, UTC+05:30)"
112
+ const match = data.expires_at.match(/(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) UTC/);
113
+ if (match && match[1]) {
114
+ const dateStr = match[1] + ' UTC';
115
+ const expiresDate = new Date(dateStr);
116
+ expiresAt = expiresDate.getTime();
117
+ }
118
+ }
119
+ return {
120
+ ...validatedToken,
121
+ ...(expiresAt && { expiresAt })
122
+ };
66
123
  }
67
- // Validate the new token and return with expiration information
68
- const validatedToken = await validateToken(data.access_token);
69
- // Try to extract expiration time from the response
70
- let expiresAt;
71
- if (data.expires_at) {
72
- // Try to parse the expiration time from the string format
73
- // Format: "2026-03-15 01:46:05 UTC (2026-03-15 07:16:05 IST, UTC+05:30)"
74
- const match = data.expires_at.match(/(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) UTC/);
75
- if (match && match[1]) {
76
- const dateStr = match[1] + ' UTC';
77
- const expiresDate = new Date(dateStr);
78
- expiresAt = expiresDate.getTime();
124
+ catch (error) {
125
+ if (error.name === 'AbortError') {
126
+ throw new Error('Token refresh timed out');
79
127
  }
128
+ throw error;
80
129
  }
81
- return {
82
- ...validatedToken,
83
- ...(expiresAt && { expiresAt })
84
- };
85
130
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hung319/opencode-qwen",
3
- "version": "1.1.0",
3
+ "version": "1.1.5",
4
4
  "description": "OpenCode plugin for Qwen API providing access to Qwen AI models with auto-config and token management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",