@pedrofariasx/qwenproxy 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.
Files changed (59) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +292 -0
  3. package/bin/qwenproxy.mjs +11 -0
  4. package/package.json +56 -0
  5. package/src/api/models.ts +183 -0
  6. package/src/api/server.ts +126 -0
  7. package/src/cache/memory-cache.ts +186 -0
  8. package/src/core/account-manager.ts +132 -0
  9. package/src/core/accounts.ts +78 -0
  10. package/src/core/config.ts +91 -0
  11. package/src/core/database.ts +92 -0
  12. package/src/core/logger.ts +96 -0
  13. package/src/core/metrics.ts +169 -0
  14. package/src/core/model-registry.ts +30 -0
  15. package/src/core/stream-registry.ts +40 -0
  16. package/src/core/watchdog.ts +130 -0
  17. package/src/index.ts +7 -0
  18. package/src/linter/extraction-engine.ts +165 -0
  19. package/src/linter/index.ts +258 -0
  20. package/src/linter/repair-normalize.ts +245 -0
  21. package/src/linter/safety-gate.ts +219 -0
  22. package/src/linter/streaming-state-machine.ts +252 -0
  23. package/src/linter/structural-parser.ts +352 -0
  24. package/src/linter/types.ts +74 -0
  25. package/src/login.ts +228 -0
  26. package/src/routes/chat.ts +801 -0
  27. package/src/routes/upload.ts +700 -0
  28. package/src/services/playwright.ts +778 -0
  29. package/src/services/qwen.ts +500 -0
  30. package/src/tests/advanced.test.ts +227 -0
  31. package/src/tests/agenticStress.test.ts +360 -0
  32. package/src/tests/concurrency.test.ts +103 -0
  33. package/src/tests/concurrentChat.test.ts +71 -0
  34. package/src/tests/delta.test.ts +63 -0
  35. package/src/tests/index.test.ts +356 -0
  36. package/src/tests/jsonFix.test.ts +98 -0
  37. package/src/tests/linter.test.ts +151 -0
  38. package/src/tests/parallel.test.ts +42 -0
  39. package/src/tests/parser.test.ts +89 -0
  40. package/src/tests/rotation.test.ts +45 -0
  41. package/src/tests/streamingOptimizations.test.ts +328 -0
  42. package/src/tests/structureVerification.test.ts +176 -0
  43. package/src/tools/ast.ts +15 -0
  44. package/src/tools/coercion.ts +67 -0
  45. package/src/tools/confidence.ts +48 -0
  46. package/src/tools/detector.ts +40 -0
  47. package/src/tools/executor.ts +236 -0
  48. package/src/tools/parser.ts +446 -0
  49. package/src/tools/pipeline.ts +122 -0
  50. package/src/tools/registry-runtime.ts +34 -0
  51. package/src/tools/registry.ts +142 -0
  52. package/src/tools/repair.ts +42 -0
  53. package/src/tools/schema.ts +285 -0
  54. package/src/tools/types.ts +104 -0
  55. package/src/tools/validator.ts +33 -0
  56. package/src/utils/context-truncation.ts +61 -0
  57. package/src/utils/json.ts +114 -0
  58. package/src/utils/qwen-stream-parser.ts +286 -0
  59. package/src/utils/types.ts +101 -0
@@ -0,0 +1,352 @@
1
+ /*
2
+ * Layer 2: Structural AST Parser (Tolerant)
3
+ * Builds partial AST from JSON-like structures without strict parsing
4
+ */
5
+
6
+ import type { PartialASTNode, ToolCallSource } from './types'
7
+
8
+ export interface StructuralParseResult {
9
+ ast: PartialASTNode | null
10
+ confidence: number
11
+ errors: string[]
12
+ sourceHint: ToolCallSource
13
+ }
14
+
15
+ export class StructuralParser {
16
+ private pos = 0
17
+ private text = ''
18
+ private errors: string[] = []
19
+
20
+ parse(text: string, sourceHint: ToolCallSource = 'unknown'): StructuralParseResult {
21
+ this.pos = 0
22
+ this.text = text.trim()
23
+ this.errors = []
24
+
25
+ if (this.text.length === 0) {
26
+ return { ast: null, confidence: 0, errors: ['Empty input'], sourceHint }
27
+ }
28
+
29
+ const nodes: PartialASTNode[] = []
30
+
31
+ while (this.pos < this.text.length) {
32
+ this.skipWhitespace()
33
+ if (this.pos >= this.text.length) break
34
+ const node = this.parseValue()
35
+ if (node) nodes.push(node)
36
+ }
37
+
38
+ if (nodes.length === 0) {
39
+ return { ast: null, confidence: 0, errors: ['No parseable content'], sourceHint }
40
+ }
41
+
42
+ const root: PartialASTNode = {
43
+ type: 'object',
44
+ raw: this.text,
45
+ confidence: this.calculateConfidence(nodes),
46
+ }
47
+ if (nodes.length === 1 && nodes[0].type === 'object' && nodes[0].value !== undefined) {
48
+ root.value = nodes[0].value
49
+ root.children = nodes[0].children
50
+ } else {
51
+ root.children = nodes
52
+ }
53
+
54
+ return { ast: root, confidence: root.confidence, errors: this.errors, sourceHint }
55
+ }
56
+
57
+ private parseValue(): PartialASTNode | undefined {
58
+ const char = this.peek()
59
+ if (char === '{') return this.parseObject()
60
+ if (char === '[') return this.parseArray()
61
+ if (char === '"' || char === "'") return this.parseString()
62
+ if (this.isNull(char)) return this.parseNull()
63
+ if (this.isBoolean(char)) return this.parseBoolean()
64
+ if (this.isNumberStart(char)) return this.parseNumber()
65
+ if (this.isUnquotedKey(char)) return this.parseUnquotedValue()
66
+ if (this.isReActAction(char)) return this.parseReActBlock()
67
+ return undefined
68
+ }
69
+
70
+ private parseObject(): PartialASTNode {
71
+ const start = this.pos
72
+ this.expect('{')
73
+ const children: PartialASTNode[] = []
74
+
75
+ while (this.pos < this.text.length) {
76
+ this.skipWhitespace()
77
+ if (this.peek() === '}') { this.pos++; break }
78
+
79
+ const key = this.parseKey()
80
+ if (!key) { this.pos++; this.errors.push(`Expected key at position ${this.pos}`); continue }
81
+
82
+ this.skipWhitespace()
83
+ this.expectOptional(':')
84
+ this.skipWhitespace()
85
+
86
+ let value: PartialASTNode | undefined
87
+ const ch = this.peek()
88
+ if (ch === '"' || ch === "'") value = this.parseString()
89
+ else if (ch === '{') value = this.parseObject()
90
+ else if (ch === '[') value = this.parseArray()
91
+ else value = this.parseScalar()
92
+
93
+ if (value) {
94
+ children.push({
95
+ type: 'object',
96
+ value: value.value,
97
+ raw: this.text.slice(start, this.pos),
98
+ confidence: 1,
99
+ children: [key, value],
100
+ })
101
+ }
102
+
103
+ this.skipWhitespace()
104
+ if (this.expectOptional(',')) {
105
+ this.skipWhitespace()
106
+ if (this.peek() === '}') break
107
+ }
108
+ }
109
+
110
+ const value: Record<string, unknown> = {}
111
+ for (const child of children) {
112
+ if (child.children && child.children.length >= 2) {
113
+ const key = child.children[0].value
114
+ const val = child.children[1].value
115
+ if (key !== undefined) value[key as string] = val
116
+ }
117
+ }
118
+
119
+ return {
120
+ type: 'object',
121
+ value,
122
+ raw: this.text.slice(start, this.pos),
123
+ confidence: this.calculateObjectConfidence(children),
124
+ children,
125
+ }
126
+ }
127
+
128
+ private parseArray(): PartialASTNode {
129
+ const start = this.pos
130
+ this.expect('[')
131
+ const children: PartialASTNode[] = []
132
+
133
+ while (this.pos < this.text.length) {
134
+ this.skipWhitespace()
135
+ if (this.peek() === ']') { this.pos++; break }
136
+ const value = this.parseValue()
137
+ if (value) children.push(value)
138
+ this.skipWhitespace()
139
+ this.expectOptional(',')
140
+ }
141
+
142
+ return {
143
+ type: 'array',
144
+ raw: this.text.slice(start, this.pos),
145
+ confidence: children.length > 0 ? 0.9 : 0.5,
146
+ children,
147
+ }
148
+ }
149
+
150
+ private parseString(): PartialASTNode {
151
+ const start = this.pos
152
+ const quote = this.peek()
153
+ this.expect(quote as '"' | "'")
154
+ let value = ''
155
+ try {
156
+ value = this.readStringContent(quote)
157
+ } catch {
158
+ this.errors.push(`Unterminated string at position ${start}`)
159
+ const content = this.text.slice(start + 1)
160
+ const idx = content.split('').findIndex((c, i) => c === quote && (i === 0 || content[i - 1] !== '\\'))
161
+ value = idx === -1 ? content : content.slice(0, idx)
162
+ this.pos = this.text.length
163
+ }
164
+ return { type: 'string', value, raw: this.text.slice(start, this.pos), confidence: 0.95 }
165
+ }
166
+
167
+ private readStringContent(quote: string): string {
168
+ let value = ''
169
+ while (this.pos < this.text.length) {
170
+ const char = this.text[this.pos++]
171
+ if (char === '\\' && this.pos < this.text.length) {
172
+ const next = this.text[this.pos++]
173
+ value += this.resolveEscapeSequence(char, next)
174
+ continue
175
+ }
176
+ if (char === quote) break
177
+ value += char
178
+ }
179
+ return value
180
+ }
181
+
182
+ private resolveEscapeSequence(_backslash: string, char: string): string {
183
+ const map: Record<string, string> = { n: '\n', t: '\t', r: '\r', b: '\b', f: '\f', '"': '"', "'": "'", '\\': '\\', '0': '\0' }
184
+ return map[char] ?? _backslash + char
185
+ }
186
+
187
+ private parseKey(): PartialASTNode | undefined {
188
+ const start = this.pos
189
+ if (this.peek() === '"' || this.peek() === "'") {
190
+ const key = this.parseString()
191
+ return { type: 'string', value: key.value, raw: key.raw, confidence: 1 }
192
+ }
193
+
194
+ let key = ''
195
+ while (this.pos < this.text.length) {
196
+ const c = this.text[this.pos]
197
+ if (/[a-zA-Z0-9_\-]/.test(c)) { key += c; this.pos++ }
198
+ else break
199
+ }
200
+
201
+ if (key.length === 0) { this.pos = start; return undefined }
202
+ }
203
+
204
+ private parseScalar(): PartialASTNode | undefined {
205
+ const start = this.pos
206
+ let value = ''
207
+ while (this.pos < this.text.length) {
208
+ const c = this.text[this.pos]
209
+ if (c === ',' || c === '}' || c === ']' || c === '\n') break
210
+ value += c
211
+ this.pos++
212
+ }
213
+ value = value.trim()
214
+
215
+ if (value === 'true') return { type: 'boolean', value: true, raw: value, confidence: 1 }
216
+ if (value === 'false') return { type: 'boolean', value: false, raw: value, confidence: 1 }
217
+ if (value === 'null' || value === 'undefined') return { type: 'null', value: null, raw: value, confidence: 0.9 }
218
+ const num = Number(value)
219
+ if (!isNaN(num) && isFinite(num)) return { type: 'number', value: num, raw: value, confidence: 0.9 }
220
+ if (value.startsWith('"') && value.endsWith('"')) return { type: 'string', value: value.slice(1, -1), raw: value, confidence: 0.85 }
221
+ if (value.startsWith("'") && value.endsWith("'")) return { type: 'string', value: value.slice(1, -1), raw: value, confidence: 0.8 }
222
+ return { type: 'unknown', value, raw: value, confidence: 0.3 }
223
+ }
224
+
225
+ private parseUnquotedValue(): PartialASTNode | undefined {
226
+ const start = this.pos
227
+ let value = ''
228
+ while (this.pos < this.text.length) {
229
+ const c = this.text[this.pos]
230
+ if (/[a-zA-Z0-9_\- ]/.test(c)) { value += c; this.pos++ }
231
+ else break
232
+ }
233
+ if (value.length === 0) return undefined
234
+ return { type: 'unknown', value: value.trim(), raw: value, confidence: 0.5 }
235
+ }
236
+
237
+ private parseNull(): PartialASTNode | undefined {
238
+ if (this.text.slice(this.pos, this.pos + 4) === 'null') {
239
+ this.pos += 4
240
+ return { type: 'null', value: null, raw: 'null', confidence: 1 }
241
+ }
242
+ return undefined
243
+ }
244
+
245
+ private parseBoolean(): PartialASTNode | undefined {
246
+ if (this.text.slice(this.pos, this.pos + 4) === 'true') {
247
+ this.pos += 4
248
+ return { type: 'boolean', value: true, raw: 'true', confidence: 1 }
249
+ }
250
+ if (this.text.slice(this.pos, this.pos + 5) === 'false') {
251
+ this.pos += 5
252
+ return { type: 'boolean', value: false, raw: 'false', confidence: 1 }
253
+ }
254
+ return undefined
255
+ }
256
+
257
+ private parseNumber(): PartialASTNode | undefined {
258
+ const start = this.pos
259
+ let value = ''
260
+ while (this.pos < this.text.length) {
261
+ const c = this.text[this.pos]
262
+ if (/[0-9.\-eE+]/.test(c)) { value += c; this.pos++ }
263
+ else break
264
+ }
265
+ if (value.length === 0) return undefined
266
+ const num = Number(value)
267
+ if (!isNaN(num) && isFinite(num)) return { type: 'number', value: num, raw: value, confidence: 0.9 }
268
+ return { type: 'unknown', value, raw: value, confidence: 0.3 }
269
+ }
270
+
271
+ private parseReActBlock(): PartialASTNode | undefined {
272
+ const start = this.pos
273
+ let action = ''
274
+ let input = ''
275
+
276
+ const actionMatch = this.text.slice(this.pos).match(/^Action:\s*(\w+)/i)
277
+ if (!actionMatch) return undefined
278
+ action = actionMatch[1]
279
+ this.pos += actionMatch[0].length
280
+ this.skipWhitespace()
281
+
282
+ const inputMatch = this.text.slice(this.pos).match(/^Action Input:\s*(\{[\s\S]*)/i)
283
+ if (inputMatch) {
284
+ input = inputMatch[1].trim()
285
+ this.pos += inputMatch[0].length
286
+ }
287
+
288
+ return { type: 'object', value: { action, input }, raw: this.text.slice(start, this.pos), confidence: 0.85 }
289
+ }
290
+
291
+ private isReActAction(char: string): boolean {
292
+ return /^Action:|^thought|^Observation|^Final Answer/i.test(this.text.slice(this.pos))
293
+ }
294
+ private isNull(char: string): boolean { return this.text.slice(this.pos, this.pos + 4) === 'null' }
295
+ private isBoolean(char: string): boolean {
296
+ return this.text.slice(this.pos, this.pos + 4) === 'true' || this.text.slice(this.pos, this.pos + 5) === 'false'
297
+ }
298
+ private isNumberStart(char: string): boolean { return /[0-9.\-]/.test(char) }
299
+ private isUnquotedKey(char: string): boolean { return /[a-zA-Z_]/.test(char) && !this.isNull(char) && !this.isReActAction(char) }
300
+
301
+ private skipWhitespace(): void {
302
+ while (this.pos < this.text.length && /\s/.test(this.text[this.pos])) this.pos++
303
+ }
304
+ private peek(): string { return this.text[this.pos] ?? '' }
305
+ private expect(char: string): boolean {
306
+ if (this.peek() === char) { this.pos++; return true }
307
+ this.errors.push(`Expected '${char}' at position ${this.pos}, got '${this.peek()}'`)
308
+ return false
309
+ }
310
+ private expectOptional(char: string): boolean {
311
+ if (this.peek() === char) { this.pos++; return true }
312
+ return false
313
+ }
314
+
315
+ private calculateConfidence(nodes: PartialASTNode[]): number {
316
+ if (nodes.length === 0) return 0
317
+ const avg = nodes.reduce((a, n) => a + n.confidence, 0) / nodes.length
318
+ return Math.max(0, Math.min(1, avg - Math.min(this.errors.length * 0.1, 0.5)))
319
+ }
320
+
321
+ private calculateObjectConfidence(children: PartialASTNode[]): number {
322
+ if (children.length === 0) return 0.1
323
+ let confidence = 0.7
324
+ if (children.some(c => c.children && c.children[0]?.type === 'string')) confidence += 0.2
325
+ if (this.errors.length === 0) confidence += 0.1
326
+ if (children.length >= 2) confidence += 0.05
327
+ return Math.min(0.99, confidence)
328
+ }
329
+
330
+ static extractJsonFromText(text: string): string[] {
331
+ const results: string[] = []
332
+ let depth = 0, start = -1, inString = false, escape = false
333
+
334
+ for (let i = 0; i < text.length; i++) {
335
+ const char = text[i]
336
+ if (escape) { escape = false; continue }
337
+ if (char === '\\' && inString) { escape = true; continue }
338
+ if (char === '"' && !escape) { inString = !inString; continue }
339
+ if (inString) continue
340
+
341
+ if (char === '{' || char === '[') {
342
+ if (depth === 0) start = i
343
+ depth++
344
+ } else if (char === '}' || char === ']') {
345
+ depth--
346
+ if (depth === 0 && start !== -1) { results.push(text.slice(start, i + 1)); start = -1 }
347
+ }
348
+ }
349
+
350
+ return results
351
+ }
352
+ }
@@ -0,0 +1,74 @@
1
+ /*
2
+ * File: types.ts
3
+ * Project: qwenproxy
4
+ * UltraToolCallLinter - Core type definitions
5
+ */
6
+
7
+ export type ToolCallSource = 'openai' | 'claude' | 'gemini' | 'react' | 'unknown'
8
+
9
+ export interface CanonicalToolCall {
10
+ tool: string
11
+ input: Record<string, unknown>
12
+ meta?: {
13
+ source: ToolCallSource
14
+ confidence: number
15
+ repaired: boolean
16
+ }
17
+ }
18
+
19
+ export interface ParserState {
20
+ buffer: string
21
+ insideCodeBlock: boolean
22
+ braceDepth: number
23
+ bracketDepth: number
24
+ inString: boolean
25
+ escapeNext: boolean
26
+ potentialToolStart: boolean
27
+ }
28
+
29
+ export interface PartialASTNode {
30
+ type: 'object' | 'array' | 'string' | 'number' | 'boolean' | 'null' | 'unknown'
31
+ value?: unknown
32
+ raw: string
33
+ confidence: number
34
+ children?: PartialASTNode[]
35
+ }
36
+
37
+ export interface RawToolCandidate {
38
+ source: ToolCallSource
39
+ raw: Record<string, unknown>
40
+ rawString: string
41
+ confidence: number
42
+ }
43
+
44
+ export interface ParseResult {
45
+ text: string
46
+ toolCalls: CanonicalToolCall[]
47
+ errors: string[]
48
+ confidence: number
49
+ }
50
+
51
+ export interface SecurityViolation {
52
+ type: 'shell_injection' | 'filesystem_access' | 'ssrf' | 'prompt_injection' | 'encoded_payload'
53
+ field: string
54
+ value: string
55
+ detail: string
56
+ }
57
+
58
+ export interface ToolRegistry {
59
+ [name: string]: {
60
+ name: string
61
+ description: string
62
+ parameters: Record<string, unknown>
63
+ required?: string[]
64
+ strict?: boolean
65
+ }
66
+ }
67
+
68
+ export interface ToolDefinition {
69
+ name: string
70
+ description: string
71
+ parameters: Record<string, unknown>
72
+ required?: string[]
73
+ strict?: boolean
74
+ }
package/src/login.ts ADDED
@@ -0,0 +1,228 @@
1
+ import { addAccount, removeAccount, listAccounts, getAccountCredentials, QwenAccount } from './core/accounts.ts'
2
+ import { initPlaywrightForAccount, closePlaywrightForAccount, BrowserType, launchManualLoginAccount, extractAccountInfoFromContext } from './services/playwright.ts'
3
+ import * as readline from 'readline'
4
+ import * as dotenv from 'dotenv'
5
+
6
+ dotenv.config()
7
+
8
+ const rl = readline.createInterface({
9
+ input: process.stdin,
10
+ output: process.stdout,
11
+ })
12
+
13
+ function askQuestion(query: string): Promise<string> {
14
+ return new Promise((resolve) => {
15
+ rl.question(query, (answer) => {
16
+ resolve(answer.trim())
17
+ })
18
+ })
19
+ }
20
+
21
+ function clear() {
22
+ process.stdout.write('\x1Bc')
23
+ }
24
+
25
+ async function showMenu() {
26
+ let browserType: BrowserType = 'chromium'
27
+ const browserArg = process.argv.find(arg => arg.startsWith('--browser='))
28
+ if (browserArg) {
29
+ browserType = browserArg.split('=')[1] as BrowserType
30
+ } else if (process.env.BROWSER) {
31
+ browserType = process.env.BROWSER as BrowserType
32
+ }
33
+
34
+ while (true) {
35
+ const accounts = listAccounts()
36
+ clear()
37
+ console.log('=== QwenProxy Account Manager ===\n')
38
+
39
+ if (accounts.length > 0) {
40
+ console.log(`Configured accounts (${accounts.length}):\n`)
41
+ for (let i = 0; i < accounts.length; i++) {
42
+ console.log(` [${i + 1}] ${accounts[i].email} (ID: ${accounts[i].id})`)
43
+ }
44
+ } else {
45
+ console.log('No accounts configured yet.\n')
46
+ }
47
+
48
+ console.log('\nOptions:')
49
+ console.log(' [A] Add account (with credentials)')
50
+ console.log(' [M] Add account (manual browser login)')
51
+ if (accounts.length > 0) {
52
+ console.log(' [R] Remove an account')
53
+ console.log(' [L] Login all accounts')
54
+ }
55
+ console.log(' [Q] Quit\n')
56
+
57
+ const choice = (await askQuestion('Select an option: ')).toUpperCase()
58
+
59
+ if (choice === 'Q') {
60
+ rl.close()
61
+ process.exit(0)
62
+ }
63
+
64
+ if (choice === 'A') {
65
+ await addAccountFlow()
66
+ continue
67
+ }
68
+
69
+ if (choice === 'M') {
70
+ await addAccountManualFlow(browserType)
71
+ continue
72
+ }
73
+
74
+ if (choice === 'R' && accounts.length > 0) {
75
+ await removeAccountFlow()
76
+ continue
77
+ }
78
+
79
+ if (choice === 'L' && accounts.length > 0) {
80
+ await loginAllAccounts(browserType)
81
+ rl.close()
82
+ return
83
+ }
84
+ }
85
+ }
86
+
87
+ async function addAccountFlow() {
88
+ clear()
89
+ console.log('=== Add New Account ===\n')
90
+ const email = await askQuestion('Email: ')
91
+ if (!email) {
92
+ console.log('Email is required.')
93
+ await askQuestion('Press Enter to continue...')
94
+ return
95
+ }
96
+ const password = await askQuestion('Password: ')
97
+ if (!password) {
98
+ console.log('Password is required.')
99
+ await askQuestion('Press Enter to continue...')
100
+ return
101
+ }
102
+
103
+ try {
104
+ const account = addAccount(email, password)
105
+ console.log(`\nAccount added: ${account.email} (${account.id})`)
106
+ } catch (err: any) {
107
+ console.log(`\nError: ${err.message}`)
108
+ }
109
+
110
+ await askQuestion('Press Enter to continue...')
111
+ }
112
+
113
+ async function removeAccountFlow() {
114
+ const accounts = listAccounts()
115
+ if (accounts.length === 0) return
116
+
117
+ clear()
118
+ console.log('=== Remove Account ===\n')
119
+
120
+ for (let i = 0; i < accounts.length; i++) {
121
+ console.log(` [${i + 1}] ${accounts[i].email} (ID: ${accounts[i].id})`)
122
+ }
123
+
124
+ const input = await askQuestion('\nSelect account number to remove (or 0 to cancel): ')
125
+ const idx = parseInt(input) - 1
126
+
127
+ if (isNaN(idx) || idx < 0 || idx >= accounts.length) {
128
+ console.log(input !== '0' ? 'Invalid selection.' : 'Cancelled.')
129
+ await askQuestion('Press Enter to continue...')
130
+ return
131
+ }
132
+
133
+ const account = accounts[idx]
134
+ const confirm = await askQuestion(`\nRemove ${account.email}? (y/N): `)
135
+ if (confirm.toLowerCase() === 'y') {
136
+ if (removeAccount(account.id)) {
137
+ console.log(`Account ${account.email} removed.`)
138
+ } else {
139
+ console.log('Failed to remove account.')
140
+ }
141
+ } else {
142
+ console.log('Cancelled.')
143
+ }
144
+
145
+ await askQuestion('Press Enter to continue...')
146
+ }
147
+
148
+ async function loginAllAccounts(browserType: BrowserType) {
149
+ const accounts = listAccounts()
150
+ if (accounts.length === 0) return
151
+
152
+ clear()
153
+ console.log(`Logging in ${accounts.length} account(s) using ${browserType}...\n`)
154
+
155
+ for (let i = 0; i < accounts.length; i++) {
156
+ const account = accounts[i]
157
+ const creds = getAccountCredentials(account.id)
158
+ if (!creds || creds.password === '***') {
159
+ console.log(`[Login] Skipping ${account.email} - no credentials available`)
160
+ continue
161
+ }
162
+ console.log(`[Login] Processing account: ${account.email}`)
163
+ try {
164
+ const fullAccount: QwenAccount = {
165
+ id: creds.id,
166
+ email: creds.email,
167
+ password: creds.password,
168
+ }
169
+ await initPlaywrightForAccount(fullAccount, true, browserType)
170
+ console.log(`[Login] Account ${account.email} session saved.`)
171
+ await closePlaywrightForAccount(account.id)
172
+ } catch (err: any) {
173
+ console.error(`[Login] Failed to login ${account.email}: ${err.message}`)
174
+ }
175
+ }
176
+
177
+ console.log('\n[Login] All accounts processed.')
178
+ await askQuestion('Press Enter to continue...')
179
+ }
180
+
181
+ async function addAccountManualFlow(browserType: BrowserType) {
182
+ clear()
183
+ console.log('=== Add Account (Manual Login) ===\n')
184
+ console.log('A browser window will open. Please login to Qwen manually.')
185
+ console.log('Once logged in, close the browser window or press Ctrl+C here.\n')
186
+ await askQuestion('Press Enter to open the browser...')
187
+
188
+ const crypto = await import('crypto')
189
+ const accountId = crypto.randomUUID()
190
+
191
+ const { context, page } = await launchManualLoginAccount(accountId, browserType)
192
+
193
+ console.log('\nBrowser opened. Waiting for you to login...')
194
+
195
+ let loggedIn = false
196
+ while (!loggedIn) {
197
+ await new Promise(resolve => setTimeout(resolve, 2000))
198
+ const { hasSession } = await extractAccountInfoFromContext(page)
199
+ if (hasSession) {
200
+ loggedIn = true
201
+ }
202
+ }
203
+
204
+ console.log('\nLogin detected! Extracting account info...')
205
+
206
+ const extractedEmail = await askQuestion('Enter the email for this account: ')
207
+ if (!extractedEmail) {
208
+ console.log('Email is required.')
209
+ await context.close()
210
+ await askQuestion('Press Enter to continue...')
211
+ return
212
+ }
213
+
214
+ try {
215
+ const account = addAccount(extractedEmail, '', accountId)
216
+ console.log(`\nAccount added: ${account.email} (${account.id})`)
217
+ } catch (err: any) {
218
+ console.log(`\nError: ${err.message}`)
219
+ }
220
+
221
+ await context.close()
222
+ await askQuestion('Press Enter to continue...')
223
+ }
224
+
225
+ showMenu().catch(err => {
226
+ console.error(err)
227
+ process.exit(1)
228
+ })