qwenproxy-cli 1.0.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 (109) hide show
  1. package/LICENSE +14 -0
  2. package/README.md +907 -0
  3. package/bin/qwenproxy.js +141 -0
  4. package/package.json +78 -0
  5. package/src/api/error-classifier.ts +159 -0
  6. package/src/api/error-helpers.ts +118 -0
  7. package/src/api/models.ts +261 -0
  8. package/src/api/server.ts +859 -0
  9. package/src/cache/memory-cache.ts +385 -0
  10. package/src/clean-cache.ts +204 -0
  11. package/src/core/account-concurrency.ts +671 -0
  12. package/src/core/account-manager.ts +297 -0
  13. package/src/core/account-priority.ts +163 -0
  14. package/src/core/accounts.ts +186 -0
  15. package/src/core/config.ts +383 -0
  16. package/src/core/crypto-utils.ts +79 -0
  17. package/src/core/database.ts +276 -0
  18. package/src/core/errors.ts +118 -0
  19. package/src/core/logger.ts +269 -0
  20. package/src/core/memory-usage.ts +84 -0
  21. package/src/core/metrics.ts +291 -0
  22. package/src/core/model-alias.ts +77 -0
  23. package/src/core/model-registry.ts +544 -0
  24. package/src/core/mutex.ts +119 -0
  25. package/src/core/paths.ts +199 -0
  26. package/src/core/prompt-limits.ts +214 -0
  27. package/src/core/reasoning-effort.ts +102 -0
  28. package/src/core/stream-registry.ts +96 -0
  29. package/src/core/waf-isolation.ts +117 -0
  30. package/src/core/watchdog.ts +195 -0
  31. package/src/delete-chats.ts +23 -0
  32. package/src/index.ts +64 -0
  33. package/src/login.ts +147 -0
  34. package/src/reset-cooldowns.ts +11 -0
  35. package/src/routes/anthropic/index.ts +355 -0
  36. package/src/routes/anthropic/translate.ts +522 -0
  37. package/src/routes/anthropic/types.ts +154 -0
  38. package/src/routes/anthropic/validation.ts +144 -0
  39. package/src/routes/chat/account.ts +1817 -0
  40. package/src/routes/chat/context.ts +241 -0
  41. package/src/routes/chat/errors.ts +85 -0
  42. package/src/routes/chat/helpers.ts +268 -0
  43. package/src/routes/chat/index.ts +618 -0
  44. package/src/routes/chat/media.ts +285 -0
  45. package/src/routes/chat/retry-policy.ts +754 -0
  46. package/src/routes/chat/stop.ts +98 -0
  47. package/src/routes/chat/streaming.ts +2710 -0
  48. package/src/routes/chat/validation.ts +526 -0
  49. package/src/routes/chat.ts +2 -0
  50. package/src/routes/completions.ts +290 -0
  51. package/src/routes/images.ts +139 -0
  52. package/src/routes/responses/adapter.ts +503 -0
  53. package/src/routes/responses/index.ts +405 -0
  54. package/src/routes/responses/state.ts +230 -0
  55. package/src/routes/responses/streaming.ts +528 -0
  56. package/src/routes/responses/types.ts +285 -0
  57. package/src/routes/responses/validation.ts +202 -0
  58. package/src/routes/upload.ts +731 -0
  59. package/src/routes/videos.ts +214 -0
  60. package/src/services/auth-playwright.ts +173 -0
  61. package/src/services/captcha-coordinator.ts +161 -0
  62. package/src/services/captcha-solver.ts +553 -0
  63. package/src/services/chat-cleanup.ts +80 -0
  64. package/src/services/context-meter.ts +317 -0
  65. package/src/services/fingerprint.ts +242 -0
  66. package/src/services/human-behavior.ts +173 -0
  67. package/src/services/media-generation.ts +1748 -0
  68. package/src/services/playwright.ts +2800 -0
  69. package/src/services/qwen-chat-pool.ts +345 -0
  70. package/src/services/qwen-errors.ts +133 -0
  71. package/src/services/qwen-headers.ts +79 -0
  72. package/src/services/qwen-thread-state.ts +393 -0
  73. package/src/services/qwen-url.ts +19 -0
  74. package/src/services/qwen.ts +3126 -0
  75. package/src/services/session-keeper.ts +88 -0
  76. package/src/services/token-estimation-metrics.ts +118 -0
  77. package/src/sync/claude-code.ts +75 -0
  78. package/src/sync/codex.ts +123 -0
  79. package/src/sync/index.ts +362 -0
  80. package/src/sync/omp.ts +105 -0
  81. package/src/sync/opencode.ts +214 -0
  82. package/src/sync/types.ts +53 -0
  83. package/src/sync/utils.ts +27 -0
  84. package/src/sync-clients.ts +189 -0
  85. package/src/tools/instructions.ts +137 -0
  86. package/src/tools/manifest.ts +81 -0
  87. package/src/tools/parser.ts +2989 -0
  88. package/src/tools/toolcall-tags.ts +142 -0
  89. package/src/tools/types.ts +53 -0
  90. package/src/tui/app.ts +264 -0
  91. package/src/tui/index.ts +61 -0
  92. package/src/tui/markdown.ts +258 -0
  93. package/src/tui/proxy-client.ts +326 -0
  94. package/src/tui/screen.ts +278 -0
  95. package/src/tui/server-manager.ts +270 -0
  96. package/src/tui/theme.ts +432 -0
  97. package/src/tui/types.ts +33 -0
  98. package/src/tui/views/accounts-view.ts +656 -0
  99. package/src/tui/views/chat-view.ts +823 -0
  100. package/src/tui/views/logs-view.ts +413 -0
  101. package/src/tui/views/status-view.ts +204 -0
  102. package/src/tui/views/storage-view.ts +291 -0
  103. package/src/tui/views/sync-view.ts +409 -0
  104. package/src/types/ali-oss.d.ts +32 -0
  105. package/src/utils/context-truncation.ts +84 -0
  106. package/src/utils/json.ts +380 -0
  107. package/src/utils/session-id.ts +37 -0
  108. package/src/utils/tool-call-guard.ts +85 -0
  109. package/src/utils/types.ts +109 -0
@@ -0,0 +1,393 @@
1
+ import { getDatabase } from "../core/database.ts";
2
+ import { logger } from "../core/logger.ts";
3
+ import { isAuthMockEnabled } from "./auth-playwright.ts";
4
+
5
+ interface SessionEntry {
6
+ accountId: string;
7
+ parentId: string | null;
8
+ timestamp: number;
9
+ }
10
+
11
+ export interface LogicalThreadEntry {
12
+ accountId: string;
13
+ chatSessionId: string;
14
+ parentId: string | null;
15
+ instructionsSent: boolean;
16
+ timestamp: number;
17
+ }
18
+
19
+ const sessionStates: Map<string, SessionEntry> =
20
+ (globalThis as any)._sessionStates || new Map();
21
+ (globalThis as any)._sessionStates = sessionStates;
22
+
23
+ // In-memory cache for logical thread states (backed by SQLite)
24
+ const logicalThreadStates: Map<string, LogicalThreadEntry> =
25
+ (globalThis as any)._logicalThreadStates || new Map();
26
+ (globalThis as any)._logicalThreadStates = logicalThreadStates;
27
+
28
+ const SESSION_TTL_MS = 24 * 60 * 60 * 1000;
29
+
30
+ /**
31
+ * Debounce window before dirty logical thread states are upserted to SQLite in
32
+ * a single transaction. The tool-call loop calls updateLogicalThreadState on
33
+ * every turn (~30+ synchronous writes per conversation otherwise). The
34
+ * in-memory cache stays the authoritative source within the process, so reads
35
+ * never observe the delay; flushLogicalThreadState() runs on shutdown.
36
+ */
37
+ const LOGICAL_THREAD_FLUSH_DELAY_MS = 300;
38
+
39
+ const logicalThreadDirty: Set<string> =
40
+ (globalThis as any)._logicalThreadDirty || new Set();
41
+ (globalThis as any)._logicalThreadDirty = logicalThreadDirty;
42
+
43
+ // Pending "tool-call cap reached" notices, keyed by logical session id. Set when
44
+ // a turn is closed early at the per-turn tool-call cap; consumed by the NEXT
45
+ // turn of the same session so the model is told that calls beyond the cap were
46
+ // not executed (and it can re-issue them). In-memory only: a restart drops the
47
+ // notice, which is acceptable — the model simply continues without the hint.
48
+ const toolCapNotices: Map<string, number> =
49
+ (globalThis as any)._toolCapNotices || new Map();
50
+ (globalThis as any)._toolCapNotices = toolCapNotices;
51
+
52
+ /** Record that `logicalSessionId` hit the per-turn tool-call cap this turn. */
53
+ export function setToolCapNotice(
54
+ logicalSessionId: string | null | undefined,
55
+ ): void {
56
+ if (!logicalSessionId) return;
57
+ toolCapNotices.set(logicalSessionId, Date.now());
58
+ }
59
+
60
+ /**
61
+ * Consume (read + clear) the pending tool-cap notice for `logicalSessionId`.
62
+ * Returns true when the previous turn of this session was closed early at the
63
+ * cap, so the caller can inject a notice into the current turn's prompt.
64
+ */
65
+ export function consumeToolCapNotice(
66
+ logicalSessionId: string | null | undefined,
67
+ ): boolean {
68
+ if (!logicalSessionId) return false;
69
+ const ts = toolCapNotices.get(logicalSessionId);
70
+ if (ts === undefined) return false;
71
+ toolCapNotices.delete(logicalSessionId);
72
+ return Date.now() - ts <= SESSION_TTL_MS;
73
+ }
74
+
75
+ let logicalThreadFlushTimer: NodeJS.Timeout | null = null;
76
+
77
+ function scheduleLogicalThreadFlush(): void {
78
+ if (logicalThreadFlushTimer) return;
79
+ logicalThreadFlushTimer = setTimeout(() => {
80
+ logicalThreadFlushTimer = null;
81
+ flushLogicalThreadState();
82
+ }, LOGICAL_THREAD_FLUSH_DELAY_MS);
83
+ logicalThreadFlushTimer.unref?.();
84
+ }
85
+
86
+ /**
87
+ * Persist every dirty logical thread state to SQLite in one transaction.
88
+ * Exported so the shutdown path can flush before closeDatabase().
89
+ */
90
+ export function flushLogicalThreadState(): void {
91
+ if (logicalThreadFlushTimer) {
92
+ clearTimeout(logicalThreadFlushTimer);
93
+ logicalThreadFlushTimer = null;
94
+ }
95
+ if (isAuthMockEnabled()) return;
96
+ if (logicalThreadDirty.size === 0) return;
97
+
98
+ try {
99
+ const db = getDatabase();
100
+ const upsert = db.prepare(
101
+ `INSERT INTO logical_thread_states (session_id, account_id, chat_session_id, parent_id, instructions_sent, updated_at)
102
+ VALUES (?, ?, ?, ?, ?, datetime('now'))
103
+ ON CONFLICT(session_id) DO UPDATE SET
104
+ account_id = excluded.account_id,
105
+ chat_session_id = excluded.chat_session_id,
106
+ parent_id = excluded.parent_id,
107
+ instructions_sent = excluded.instructions_sent,
108
+ updated_at = datetime('now')`,
109
+ );
110
+ const flush = db.transaction(
111
+ (entries: Array<{
112
+ sessionId: string;
113
+ accountId: string;
114
+ chatSessionId: string;
115
+ parentId: string | null;
116
+ instructionsSent: number;
117
+ }>) => {
118
+ for (const e of entries) {
119
+ upsert.run(
120
+ e.sessionId,
121
+ e.accountId,
122
+ e.chatSessionId,
123
+ e.parentId,
124
+ e.instructionsSent,
125
+ );
126
+ }
127
+ },
128
+ );
129
+
130
+ const entries: Array<{
131
+ sessionId: string;
132
+ accountId: string;
133
+ chatSessionId: string;
134
+ parentId: string | null;
135
+ instructionsSent: number;
136
+ }> = [];
137
+ for (const sessionId of logicalThreadDirty) {
138
+ const entry = logicalThreadStates.get(sessionId);
139
+ // Entries evicted from the cache (clearAllSessionsForAccount / TTL) must
140
+ // NOT be resurrected — the deletion already happened in the cache.
141
+ if (!entry || !entry.chatSessionId) continue;
142
+ entries.push({
143
+ sessionId,
144
+ accountId: entry.accountId,
145
+ chatSessionId: entry.chatSessionId,
146
+ parentId: entry.parentId ?? null,
147
+ instructionsSent: entry.instructionsSent ? 1 : 0,
148
+ });
149
+ }
150
+
151
+ if (entries.length > 0) flush(entries);
152
+ logicalThreadDirty.clear();
153
+ } catch (err) {
154
+ logger.warn(
155
+ "[Qwen] Failed to batch-persist logical thread states to SQLite",
156
+ {
157
+ count: logicalThreadDirty.size,
158
+ error: err instanceof Error ? err.message : String(err),
159
+ },
160
+ );
161
+ logicalThreadDirty.clear();
162
+ }
163
+ }
164
+
165
+ function cleanupStaleSessions() {
166
+ const now = Date.now();
167
+ for (const [key, entry] of sessionStates.entries()) {
168
+ if (now - entry.timestamp > SESSION_TTL_MS) {
169
+ sessionStates.delete(key);
170
+ }
171
+ }
172
+ // Cleanup stale entries from SQLite
173
+ try {
174
+ const db = getDatabase();
175
+ const cutoff = new Date(now - SESSION_TTL_MS).toISOString();
176
+ db.prepare("DELETE FROM logical_thread_states WHERE updated_at < ?").run(
177
+ cutoff,
178
+ );
179
+ } catch (error) {
180
+ logger.warn("Failed to clean up stale logical thread states", { error });
181
+ }
182
+ for (const [key, entry] of logicalThreadStates.entries()) {
183
+ if (now - entry.timestamp > SESSION_TTL_MS) {
184
+ logicalThreadStates.delete(key);
185
+ logicalThreadDirty.delete(key);
186
+ }
187
+ }
188
+ }
189
+
190
+ export function getLogicalThreadState(
191
+ logicalSessionId: string | null | undefined,
192
+ ): LogicalThreadEntry | null {
193
+ if (!logicalSessionId) return null;
194
+
195
+ // Check in-memory cache first
196
+ const cached = logicalThreadStates.get(logicalSessionId);
197
+ if (cached && Date.now() - cached.timestamp <= SESSION_TTL_MS) {
198
+ return cached;
199
+ }
200
+ if (cached) {
201
+ logicalThreadStates.delete(logicalSessionId);
202
+ }
203
+
204
+ if (isAuthMockEnabled()) return null;
205
+
206
+ // Fallback to SQLite
207
+ try {
208
+ const db = getDatabase();
209
+ const row = db
210
+ .prepare(
211
+ "SELECT session_id, account_id, chat_session_id, parent_id, instructions_sent, updated_at FROM logical_thread_states WHERE session_id = ?",
212
+ )
213
+ .get(logicalSessionId) as
214
+ | {
215
+ session_id: string;
216
+ account_id: string;
217
+ chat_session_id: string;
218
+ parent_id: string | null;
219
+ instructions_sent: number;
220
+ updated_at: string;
221
+ }
222
+ | undefined;
223
+
224
+ if (!row) return null;
225
+
226
+ const timestamp = new Date(row.updated_at).getTime();
227
+ if (Date.now() - timestamp > SESSION_TTL_MS) {
228
+ db.prepare("DELETE FROM logical_thread_states WHERE session_id = ?").run(
229
+ logicalSessionId,
230
+ );
231
+ return null;
232
+ }
233
+
234
+ const entry: LogicalThreadEntry = {
235
+ accountId: row.account_id,
236
+ chatSessionId: row.chat_session_id,
237
+ parentId: row.parent_id,
238
+ instructionsSent: row.instructions_sent === 1,
239
+ timestamp,
240
+ };
241
+
242
+ // Populate in-memory cache
243
+ logicalThreadStates.set(logicalSessionId, entry);
244
+ return entry;
245
+ } catch (err) {
246
+ logger.warn("[Qwen] Failed to read logical thread from SQLite", {
247
+ sessionId: logicalSessionId,
248
+ error: err instanceof Error ? err.message : String(err),
249
+ });
250
+ return null;
251
+ }
252
+ }
253
+
254
+ export function updateLogicalThreadState(
255
+ logicalSessionId: string,
256
+ entry: Omit<LogicalThreadEntry, "timestamp" | "instructionsSent"> & {
257
+ instructionsSent?: boolean;
258
+ },
259
+ ): void {
260
+ if (
261
+ !logicalSessionId ||
262
+ entry.chatSessionId === undefined ||
263
+ entry.chatSessionId === null
264
+ )
265
+ return;
266
+ if (logicalThreadStates.size > 10000) cleanupStaleSessions();
267
+ const existing = logicalThreadStates.get(logicalSessionId);
268
+ const merged = {
269
+ ...entry,
270
+ instructionsSent:
271
+ entry.instructionsSent ?? existing?.instructionsSent ?? false,
272
+ timestamp: Date.now(),
273
+ };
274
+
275
+ // Update in-memory cache
276
+ logicalThreadStates.set(logicalSessionId, merged);
277
+
278
+ if (isAuthMockEnabled()) return;
279
+
280
+ // Persist to SQLite with a short debounce: turns in the tool-call loop
281
+ // update this on every stream creation, and coalescing them into one
282
+ // transaction removes ~30+ synchronous writes per conversation. Reads stay
283
+ // cache-first, so runtime behavior is unchanged; the flush runs on shutdown.
284
+ logicalThreadDirty.add(logicalSessionId);
285
+ scheduleLogicalThreadFlush();
286
+ }
287
+
288
+ export function updateLogicalThreadParent(
289
+ logicalSessionId: string | null | undefined,
290
+ parentId: string | null,
291
+ accountId: string,
292
+ chatSessionId: string,
293
+ ): void {
294
+ if (!logicalSessionId || !chatSessionId) return;
295
+ updateLogicalThreadState(logicalSessionId, {
296
+ accountId,
297
+ chatSessionId,
298
+ parentId,
299
+ instructionsSent: true,
300
+ });
301
+ }
302
+
303
+ export function updateSessionParent(
304
+ sessionId: string,
305
+ parentId: string | null,
306
+ accountId?: string,
307
+ ) {
308
+ if (!sessionId) return;
309
+
310
+ if (sessionStates.size > 10000) {
311
+ cleanupStaleSessions();
312
+ }
313
+
314
+ const existing = sessionStates.get(sessionId);
315
+ sessionStates.set(sessionId, {
316
+ accountId: accountId || existing?.accountId || "global",
317
+ parentId,
318
+ timestamp: Date.now(),
319
+ });
320
+ }
321
+
322
+ /**
323
+ * Invalidate the stored parent for a logical thread without forgetting the
324
+ * upstream chat binding. The next request will see a missing parent and must
325
+ * rebuild the upstream chat with full context instead of appending to a
326
+ * possibly corrupted parent chain.
327
+ */
328
+ export function invalidateLogicalThreadParent(
329
+ logicalSessionId: string | null | undefined,
330
+ ): void {
331
+ if (!logicalSessionId) return;
332
+
333
+ const existing = getLogicalThreadState(logicalSessionId);
334
+ if (!existing) return;
335
+
336
+ updateSessionParent(existing.chatSessionId, null, existing.accountId);
337
+ updateLogicalThreadState(logicalSessionId, {
338
+ accountId: existing.accountId,
339
+ chatSessionId: existing.chatSessionId,
340
+ parentId: null,
341
+ instructionsSent: existing.instructionsSent,
342
+ });
343
+ }
344
+
345
+ export function clearAllSessionsForAccount(accountId: string): void {
346
+ let removed = 0;
347
+
348
+ for (const [key, entry] of sessionStates.entries()) {
349
+ if (entry.accountId === accountId) {
350
+ sessionStates.delete(key);
351
+ removed++;
352
+ }
353
+ }
354
+
355
+ for (const [key, entry] of logicalThreadStates.entries()) {
356
+ if (entry.accountId === accountId) {
357
+ logicalThreadStates.delete(key);
358
+ // Drop pending writes too, so a later debounce flush cannot resurrect
359
+ // the just-deleted row.
360
+ logicalThreadDirty.delete(key);
361
+ removed++;
362
+ }
363
+ }
364
+
365
+ // Also clear from SQLite
366
+ try {
367
+ const db = getDatabase();
368
+ const result = db
369
+ .prepare("DELETE FROM logical_thread_states WHERE account_id = ?")
370
+ .run(accountId);
371
+ removed += result.changes;
372
+ } catch {}
373
+
374
+ console.log(
375
+ `🧹 [Qwen] Cleared ${removed} session(s) for account ${accountId}`,
376
+ );
377
+ }
378
+
379
+ export function getSessionParent(
380
+ sessionId: string,
381
+ accountId?: string,
382
+ ): string | null | undefined {
383
+ const entry = sessionStates.get(sessionId);
384
+ if (!entry) return undefined;
385
+ if (Date.now() - entry.timestamp > SESSION_TTL_MS) {
386
+ sessionStates.delete(sessionId);
387
+ return undefined;
388
+ }
389
+ if (accountId && entry.accountId !== accountId) {
390
+ return undefined;
391
+ }
392
+ return entry.parentId;
393
+ }
@@ -0,0 +1,19 @@
1
+ import { config } from "../core/config.ts";
2
+
3
+ /**
4
+ * Build a URL against the configured Qwen web origin.
5
+ *
6
+ * Keeping path joining here prevents one endpoint from silently ignoring
7
+ * QWEN_BASE_URL (which is especially important for local test proxies).
8
+ */
9
+ const QWEN_BASE_URL = config.qwen.baseUrl.trim().replace(/\/+$/, "");
10
+ const QWEN_ORIGIN = new URL(QWEN_BASE_URL).origin;
11
+
12
+ export function qwenUrl(path = ""): string {
13
+ const normalizedPath = path.replace(/^\/+/, "");
14
+ return normalizedPath ? `${QWEN_BASE_URL}/${normalizedPath}` : QWEN_BASE_URL;
15
+ }
16
+
17
+ export function qwenOrigin(): string {
18
+ return QWEN_ORIGIN;
19
+ }