@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.
- package/LICENSE +13 -0
- package/README.md +292 -0
- package/bin/qwenproxy.mjs +11 -0
- package/package.json +56 -0
- package/src/api/models.ts +183 -0
- package/src/api/server.ts +126 -0
- package/src/cache/memory-cache.ts +186 -0
- package/src/core/account-manager.ts +132 -0
- package/src/core/accounts.ts +78 -0
- package/src/core/config.ts +91 -0
- package/src/core/database.ts +92 -0
- package/src/core/logger.ts +96 -0
- package/src/core/metrics.ts +169 -0
- package/src/core/model-registry.ts +30 -0
- package/src/core/stream-registry.ts +40 -0
- package/src/core/watchdog.ts +130 -0
- package/src/index.ts +7 -0
- package/src/linter/extraction-engine.ts +165 -0
- package/src/linter/index.ts +258 -0
- package/src/linter/repair-normalize.ts +245 -0
- package/src/linter/safety-gate.ts +219 -0
- package/src/linter/streaming-state-machine.ts +252 -0
- package/src/linter/structural-parser.ts +352 -0
- package/src/linter/types.ts +74 -0
- package/src/login.ts +228 -0
- package/src/routes/chat.ts +801 -0
- package/src/routes/upload.ts +700 -0
- package/src/services/playwright.ts +778 -0
- package/src/services/qwen.ts +500 -0
- package/src/tests/advanced.test.ts +227 -0
- package/src/tests/agenticStress.test.ts +360 -0
- package/src/tests/concurrency.test.ts +103 -0
- package/src/tests/concurrentChat.test.ts +71 -0
- package/src/tests/delta.test.ts +63 -0
- package/src/tests/index.test.ts +356 -0
- package/src/tests/jsonFix.test.ts +98 -0
- package/src/tests/linter.test.ts +151 -0
- package/src/tests/parallel.test.ts +42 -0
- package/src/tests/parser.test.ts +89 -0
- package/src/tests/rotation.test.ts +45 -0
- package/src/tests/streamingOptimizations.test.ts +328 -0
- package/src/tests/structureVerification.test.ts +176 -0
- package/src/tools/ast.ts +15 -0
- package/src/tools/coercion.ts +67 -0
- package/src/tools/confidence.ts +48 -0
- package/src/tools/detector.ts +40 -0
- package/src/tools/executor.ts +236 -0
- package/src/tools/parser.ts +446 -0
- package/src/tools/pipeline.ts +122 -0
- package/src/tools/registry-runtime.ts +34 -0
- package/src/tools/registry.ts +142 -0
- package/src/tools/repair.ts +42 -0
- package/src/tools/schema.ts +285 -0
- package/src/tools/types.ts +104 -0
- package/src/tools/validator.ts +33 -0
- package/src/utils/context-truncation.ts +61 -0
- package/src/utils/json.ts +114 -0
- package/src/utils/qwen-stream-parser.ts +286 -0
- 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
|
+
})
|