hedgequantx 2.6.67 → 2.6.69
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/menus/ai-agent.js +231 -46
- package/src/services/ai/oauth-gemini.js +223 -0
- package/src/services/ai/oauth-iflow.js +269 -0
- package/src/services/ai/oauth-openai.js +233 -0
- package/src/services/ai/oauth-qwen.js +279 -0
- package/src/services/ai/providers/index.js +56 -4
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Qwen OAuth Authentication (Device Flow)
|
|
3
|
+
*
|
|
4
|
+
* Implements OAuth 2.0 Device Authorization Grant for Qwen Chat subscription.
|
|
5
|
+
* Based on the public OAuth flow used by Qwen Code CLI.
|
|
6
|
+
*
|
|
7
|
+
* Data source: Qwen OAuth API (https://chat.qwen.ai/api/v1/oauth2)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
const https = require('https');
|
|
12
|
+
|
|
13
|
+
// Public OAuth Client ID (from Qwen CLI)
|
|
14
|
+
const CLIENT_ID = 'f0304373b74a44d2b584a3fb70ca9e56';
|
|
15
|
+
const DEVICE_CODE_URL = 'https://chat.qwen.ai/api/v1/oauth2/device/code';
|
|
16
|
+
const TOKEN_URL = 'https://chat.qwen.ai/api/v1/oauth2/token';
|
|
17
|
+
const SCOPES = 'openid profile email model.completion';
|
|
18
|
+
const GRANT_TYPE = 'urn:ietf:params:oauth:grant-type:device_code';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generate PKCE code verifier and challenge
|
|
22
|
+
* @returns {Object} { verifier: string, challenge: string }
|
|
23
|
+
*/
|
|
24
|
+
const generatePKCE = () => {
|
|
25
|
+
const verifier = crypto.randomBytes(32)
|
|
26
|
+
.toString('base64')
|
|
27
|
+
.replace(/\+/g, '-')
|
|
28
|
+
.replace(/\//g, '_')
|
|
29
|
+
.replace(/=/g, '');
|
|
30
|
+
|
|
31
|
+
const challenge = crypto.createHash('sha256')
|
|
32
|
+
.update(verifier)
|
|
33
|
+
.digest('base64')
|
|
34
|
+
.replace(/\+/g, '-')
|
|
35
|
+
.replace(/\//g, '_')
|
|
36
|
+
.replace(/=/g, '');
|
|
37
|
+
|
|
38
|
+
return { verifier, challenge };
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Make HTTPS request
|
|
43
|
+
*/
|
|
44
|
+
const makeRequest = (urlStr, options) => {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const url = new URL(urlStr);
|
|
47
|
+
const req = https.request({
|
|
48
|
+
hostname: url.hostname,
|
|
49
|
+
port: url.port || 443,
|
|
50
|
+
path: url.pathname + url.search,
|
|
51
|
+
method: options.method || 'POST',
|
|
52
|
+
headers: options.headers || {}
|
|
53
|
+
}, (res) => {
|
|
54
|
+
let data = '';
|
|
55
|
+
res.on('data', chunk => data += chunk);
|
|
56
|
+
res.on('end', () => {
|
|
57
|
+
try {
|
|
58
|
+
const json = JSON.parse(data);
|
|
59
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
60
|
+
resolve(json);
|
|
61
|
+
} else {
|
|
62
|
+
resolve({ ...json, statusCode: res.statusCode }); // Return error response for polling
|
|
63
|
+
}
|
|
64
|
+
} catch (e) {
|
|
65
|
+
reject(new Error(`Invalid JSON response: ${data.substring(0, 200)}`));
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
req.on('error', reject);
|
|
71
|
+
|
|
72
|
+
if (options.body) {
|
|
73
|
+
req.write(options.body);
|
|
74
|
+
}
|
|
75
|
+
req.end();
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Initiate device authorization flow
|
|
81
|
+
* @returns {Promise<Object>} { deviceCode, userCode, verificationUri, verificationUriComplete, expiresIn, interval, verifier }
|
|
82
|
+
*/
|
|
83
|
+
const initiateDeviceFlow = async () => {
|
|
84
|
+
try {
|
|
85
|
+
const pkce = generatePKCE();
|
|
86
|
+
|
|
87
|
+
const body = new URLSearchParams({
|
|
88
|
+
client_id: CLIENT_ID,
|
|
89
|
+
scope: SCOPES,
|
|
90
|
+
code_challenge: pkce.challenge,
|
|
91
|
+
code_challenge_method: 'S256'
|
|
92
|
+
}).toString();
|
|
93
|
+
|
|
94
|
+
const response = await makeRequest(DEVICE_CODE_URL, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: {
|
|
97
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
98
|
+
'Accept': 'application/json'
|
|
99
|
+
},
|
|
100
|
+
body
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (!response.device_code) {
|
|
104
|
+
return {
|
|
105
|
+
type: 'failed',
|
|
106
|
+
error: 'Device code not found in response'
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
type: 'success',
|
|
112
|
+
deviceCode: response.device_code,
|
|
113
|
+
userCode: response.user_code,
|
|
114
|
+
verificationUri: response.verification_uri,
|
|
115
|
+
verificationUriComplete: response.verification_uri_complete,
|
|
116
|
+
expiresIn: response.expires_in,
|
|
117
|
+
interval: response.interval || 5,
|
|
118
|
+
verifier: pkce.verifier
|
|
119
|
+
};
|
|
120
|
+
} catch (error) {
|
|
121
|
+
return {
|
|
122
|
+
type: 'failed',
|
|
123
|
+
error: error.message
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Poll for token (single attempt)
|
|
130
|
+
* @param {string} deviceCode - Device code from initiateDeviceFlow
|
|
131
|
+
* @param {string} verifier - PKCE code verifier
|
|
132
|
+
* @returns {Promise<Object>}
|
|
133
|
+
*/
|
|
134
|
+
const pollForToken = async (deviceCode, verifier) => {
|
|
135
|
+
try {
|
|
136
|
+
const body = new URLSearchParams({
|
|
137
|
+
grant_type: GRANT_TYPE,
|
|
138
|
+
client_id: CLIENT_ID,
|
|
139
|
+
device_code: deviceCode,
|
|
140
|
+
code_verifier: verifier
|
|
141
|
+
}).toString();
|
|
142
|
+
|
|
143
|
+
const response = await makeRequest(TOKEN_URL, {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
headers: {
|
|
146
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
147
|
+
'Accept': 'application/json'
|
|
148
|
+
},
|
|
149
|
+
body
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Check for polling errors (RFC 8628)
|
|
153
|
+
if (response.error) {
|
|
154
|
+
switch (response.error) {
|
|
155
|
+
case 'authorization_pending':
|
|
156
|
+
return { type: 'pending' };
|
|
157
|
+
case 'slow_down':
|
|
158
|
+
return { type: 'slow_down' };
|
|
159
|
+
case 'expired_token':
|
|
160
|
+
return { type: 'failed', error: 'Device code expired. Please restart authentication.' };
|
|
161
|
+
case 'access_denied':
|
|
162
|
+
return { type: 'failed', error: 'Authorization denied by user.' };
|
|
163
|
+
default:
|
|
164
|
+
return { type: 'failed', error: response.error_description || response.error };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Success
|
|
169
|
+
if (response.access_token) {
|
|
170
|
+
return {
|
|
171
|
+
type: 'success',
|
|
172
|
+
access: response.access_token,
|
|
173
|
+
refresh: response.refresh_token,
|
|
174
|
+
resourceUrl: response.resource_url,
|
|
175
|
+
expires: Date.now() + (response.expires_in * 1000)
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { type: 'pending' };
|
|
180
|
+
} catch (error) {
|
|
181
|
+
return {
|
|
182
|
+
type: 'failed',
|
|
183
|
+
error: error.message
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Refresh access token using refresh token
|
|
190
|
+
* @param {string} refreshTokenValue - The refresh token
|
|
191
|
+
* @returns {Promise<Object>}
|
|
192
|
+
*/
|
|
193
|
+
const refreshToken = async (refreshTokenValue) => {
|
|
194
|
+
try {
|
|
195
|
+
const body = new URLSearchParams({
|
|
196
|
+
grant_type: 'refresh_token',
|
|
197
|
+
refresh_token: refreshTokenValue,
|
|
198
|
+
client_id: CLIENT_ID
|
|
199
|
+
}).toString();
|
|
200
|
+
|
|
201
|
+
const response = await makeRequest(TOKEN_URL, {
|
|
202
|
+
method: 'POST',
|
|
203
|
+
headers: {
|
|
204
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
205
|
+
'Accept': 'application/json'
|
|
206
|
+
},
|
|
207
|
+
body
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
if (response.error) {
|
|
211
|
+
return {
|
|
212
|
+
type: 'failed',
|
|
213
|
+
error: response.error_description || response.error
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
type: 'success',
|
|
219
|
+
access: response.access_token,
|
|
220
|
+
refresh: response.refresh_token,
|
|
221
|
+
resourceUrl: response.resource_url,
|
|
222
|
+
expires: Date.now() + (response.expires_in * 1000)
|
|
223
|
+
};
|
|
224
|
+
} catch (error) {
|
|
225
|
+
return {
|
|
226
|
+
type: 'failed',
|
|
227
|
+
error: error.message
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get valid access token (refresh if expired)
|
|
234
|
+
* @param {Object} oauthData - OAuth data { access, refresh, expires }
|
|
235
|
+
* @returns {Promise<Object>}
|
|
236
|
+
*/
|
|
237
|
+
const getValidToken = async (oauthData) => {
|
|
238
|
+
if (!oauthData || !oauthData.refresh) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const expirationBuffer = 5 * 60 * 1000; // 5 minutes
|
|
243
|
+
if (oauthData.expires && oauthData.expires > Date.now() + expirationBuffer) {
|
|
244
|
+
return {
|
|
245
|
+
...oauthData,
|
|
246
|
+
refreshed: false
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const result = await refreshToken(oauthData.refresh);
|
|
251
|
+
if (result.type === 'success') {
|
|
252
|
+
return {
|
|
253
|
+
access: result.access,
|
|
254
|
+
refresh: result.refresh,
|
|
255
|
+
expires: result.expires,
|
|
256
|
+
refreshed: true
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return null;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Check if credentials are OAuth tokens
|
|
265
|
+
*/
|
|
266
|
+
const isOAuthCredentials = (credentials) => {
|
|
267
|
+
return credentials && credentials.oauth && credentials.oauth.refresh;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
module.exports = {
|
|
271
|
+
CLIENT_ID,
|
|
272
|
+
SCOPES,
|
|
273
|
+
generatePKCE,
|
|
274
|
+
initiateDeviceFlow,
|
|
275
|
+
pollForToken,
|
|
276
|
+
refreshToken,
|
|
277
|
+
getValidToken,
|
|
278
|
+
isOAuthCredentials
|
|
279
|
+
};
|
|
@@ -66,12 +66,22 @@ const PROVIDERS = {
|
|
|
66
66
|
|
|
67
67
|
openai: {
|
|
68
68
|
id: 'openai',
|
|
69
|
-
name: 'OPENAI (GPT-4)',
|
|
70
|
-
description: '
|
|
69
|
+
name: 'OPENAI (GPT-4/5)',
|
|
70
|
+
description: 'Plus/Pro or API Key',
|
|
71
71
|
category: 'direct',
|
|
72
72
|
models: [], // Fetched from API at runtime
|
|
73
73
|
defaultModel: null, // Will use first model from API
|
|
74
74
|
options: [
|
|
75
|
+
{
|
|
76
|
+
id: 'oauth_plus',
|
|
77
|
+
label: 'PLUS/PRO SUBSCRIPTION (OAUTH)',
|
|
78
|
+
description: [
|
|
79
|
+
'Login with your ChatGPT account',
|
|
80
|
+
'Unlimited with your plan'
|
|
81
|
+
],
|
|
82
|
+
fields: ['oauth'],
|
|
83
|
+
authType: 'oauth'
|
|
84
|
+
},
|
|
75
85
|
{
|
|
76
86
|
id: 'api_key',
|
|
77
87
|
label: 'API KEY (PAY-PER-USE)',
|
|
@@ -89,11 +99,21 @@ const PROVIDERS = {
|
|
|
89
99
|
gemini: {
|
|
90
100
|
id: 'gemini',
|
|
91
101
|
name: 'GEMINI (GOOGLE)',
|
|
92
|
-
description: '
|
|
102
|
+
description: 'Advanced or API Key',
|
|
93
103
|
category: 'direct',
|
|
94
104
|
models: [], // Fetched from API at runtime
|
|
95
105
|
defaultModel: null, // Will use first model from API
|
|
96
106
|
options: [
|
|
107
|
+
{
|
|
108
|
+
id: 'oauth_advanced',
|
|
109
|
+
label: 'ADVANCED SUBSCRIPTION (OAUTH)',
|
|
110
|
+
description: [
|
|
111
|
+
'Login with your Google account',
|
|
112
|
+
'Unlimited with your plan'
|
|
113
|
+
],
|
|
114
|
+
fields: ['oauth'],
|
|
115
|
+
authType: 'oauth'
|
|
116
|
+
},
|
|
97
117
|
{
|
|
98
118
|
id: 'api_key',
|
|
99
119
|
label: 'API KEY (FREE TIER)',
|
|
@@ -243,15 +263,47 @@ const PROVIDERS = {
|
|
|
243
263
|
endpoint: 'https://api.together.xyz/v1'
|
|
244
264
|
},
|
|
245
265
|
|
|
266
|
+
iflow: {
|
|
267
|
+
id: 'iflow',
|
|
268
|
+
name: 'IFLOW',
|
|
269
|
+
description: 'Subscription (OAuth)',
|
|
270
|
+
category: 'direct',
|
|
271
|
+
models: [], // Fetched from API at runtime
|
|
272
|
+
defaultModel: null, // Will use first model from API
|
|
273
|
+
options: [
|
|
274
|
+
{
|
|
275
|
+
id: 'oauth_sub',
|
|
276
|
+
label: 'SUBSCRIPTION (OAUTH)',
|
|
277
|
+
description: [
|
|
278
|
+
'Login with your iFlow account',
|
|
279
|
+
'Access to DeepSeek, Kimi, GLM & more'
|
|
280
|
+
],
|
|
281
|
+
fields: ['oauth'],
|
|
282
|
+
authType: 'oauth'
|
|
283
|
+
}
|
|
284
|
+
],
|
|
285
|
+
endpoint: 'https://api.iflow.com/v1'
|
|
286
|
+
},
|
|
287
|
+
|
|
246
288
|
|
|
247
289
|
qwen: {
|
|
248
290
|
id: 'qwen',
|
|
249
291
|
name: 'QWEN (ALIBABA)',
|
|
250
|
-
description: '
|
|
292
|
+
description: 'Chat subscription or API Key',
|
|
251
293
|
category: 'direct',
|
|
252
294
|
models: [], // Fetched from API at runtime
|
|
253
295
|
defaultModel: null, // Will use first model from API
|
|
254
296
|
options: [
|
|
297
|
+
{
|
|
298
|
+
id: 'oauth_chat',
|
|
299
|
+
label: 'CHAT SUBSCRIPTION (OAUTH)',
|
|
300
|
+
description: [
|
|
301
|
+
'Login with your Qwen account',
|
|
302
|
+
'Unlimited with your plan'
|
|
303
|
+
],
|
|
304
|
+
fields: ['oauth'],
|
|
305
|
+
authType: 'oauth'
|
|
306
|
+
},
|
|
255
307
|
{
|
|
256
308
|
id: 'api_key',
|
|
257
309
|
label: 'API KEY (DASHSCOPE)',
|