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,117 @@
1
+ /**
2
+ * WAF hard-block contingency (aligned with upstream account-isolation.ts).
3
+ *
4
+ * When a Baxia/TMD challenge could NOT be solved, quarantining the account is
5
+ * not enough: the fingerprint seed is derived from the accountId, so the
6
+ * account returns from cooldown on the SAME device identity the WAF already
7
+ * flagged, and the challenge re-propagates immediately. The contingency here:
8
+ * - escalates the quarantine window per consecutive hard block (×2, capped),
9
+ * - rotates the account's fingerprint seed (fresh device identity),
10
+ * - closes the account's Playwright context so the next use re-initializes
11
+ * with the rotated profile (cookies/storage persist in the profile dir).
12
+ *
13
+ * Strictly per-account: a block on one account never touches another's state.
14
+ */
15
+ import { config } from "./config.ts";
16
+ import { logger } from "./logger.ts";
17
+ import { markAccountRateLimited } from "./account-manager.ts";
18
+ import { rotateFingerprintSeed } from "../services/fingerprint.ts";
19
+
20
+ const ESCALATION_FACTOR = 2;
21
+ const MAX_CONSECUTIVE_FOR_ESCALATION = 4;
22
+
23
+ interface WafBlockState {
24
+ consecutiveHardBlocks: number;
25
+ lastBlockAt: number;
26
+ }
27
+
28
+ const states = new Map<string, WafBlockState>();
29
+
30
+ export interface WafBlockResult {
31
+ cooldownMs: number;
32
+ fingerprintRotated: true;
33
+ escalated: boolean;
34
+ }
35
+
36
+ type ContextResetListener = (accountId: string) => void | Promise<void>;
37
+ let contextResetListener: ContextResetListener | null = null;
38
+
39
+ /**
40
+ * Register the hook that physically resets an account's browser context after
41
+ * a fingerprint rotation. Injected by the Playwright layer so this module
42
+ * stays decoupled from it (and unit-testable without a browser).
43
+ */
44
+ export function setWafContextResetListener(fn: ContextResetListener | null): void {
45
+ contextResetListener = fn;
46
+ }
47
+
48
+ function getState(accountId: string): WafBlockState {
49
+ let state = states.get(accountId);
50
+ if (!state) {
51
+ state = { consecutiveHardBlocks: 0, lastBlockAt: 0 };
52
+ states.set(accountId, state);
53
+ }
54
+ return state;
55
+ }
56
+
57
+ export function getWafHardBlockCount(accountId: string): number {
58
+ return states.get(accountId)?.consecutiveHardBlocks ?? 0;
59
+ }
60
+
61
+ /**
62
+ * Record an unsolvable WAF challenge on `accountId`: quarantine (escalating),
63
+ * rotate the device fingerprint, and reset the browser context.
64
+ */
65
+ export function recordWafHardBlock(accountId: string): WafBlockResult {
66
+ const state = getState(accountId);
67
+ state.consecutiveHardBlocks += 1;
68
+ state.lastBlockAt = Date.now();
69
+
70
+ const base = config.captcha.accountCooldownMs;
71
+ const cap = config.captcha.hardBlockMaxCooldownMs;
72
+ const exponent = Math.min(
73
+ state.consecutiveHardBlocks - 1,
74
+ MAX_CONSECUTIVE_FOR_ESCALATION,
75
+ );
76
+ const cooldownMs = Math.min(cap, base * Math.pow(ESCALATION_FACTOR, exponent));
77
+
78
+ if (cooldownMs > 0) {
79
+ markAccountRateLimited(accountId, cooldownMs, "WafChallenge");
80
+ }
81
+
82
+ rotateFingerprintSeed(accountId);
83
+ logger.warn(
84
+ `[WafIsolation] Hard WAF block on ${accountId}: fingerprint rotated (streak ${state.consecutiveHardBlocks}), quarantined ${Math.round(cooldownMs / 1000)}s`,
85
+ );
86
+
87
+ if (contextResetListener) {
88
+ // Best-effort: the next use re-initializes the context with the rotated
89
+ // profile; a failed close must not abort the quarantine.
90
+ void Promise.resolve(contextResetListener(accountId)).catch((error: unknown) => {
91
+ logger.warn(
92
+ `[WafIsolation] Context reset listener failed for ${accountId}: ${
93
+ error instanceof Error ? error.message : String(error)
94
+ }`,
95
+ );
96
+ });
97
+ }
98
+
99
+ return {
100
+ cooldownMs,
101
+ fingerprintRotated: true,
102
+ escalated: state.consecutiveHardBlocks > 1,
103
+ };
104
+ }
105
+
106
+ /** A successful stream on the account clears the escalation streak. */
107
+ export function noteWafRecovery(accountId: string): void {
108
+ const state = states.get(accountId);
109
+ if (state && state.consecutiveHardBlocks > 0) {
110
+ state.consecutiveHardBlocks = 0;
111
+ }
112
+ }
113
+
114
+ /** Test/admin helper: drop all isolation state for an account. */
115
+ export function clearWafIsolation(accountId: string): void {
116
+ states.delete(accountId);
117
+ }
@@ -0,0 +1,195 @@
1
+ import { EventEmitter } from "events";
2
+ import { config } from "./config.js";
3
+ import { metrics } from "./metrics.js";
4
+ import {
5
+ classifyRamUsage,
6
+ getHeapUsageSnapshot,
7
+ getRssUsageSnapshot,
8
+ type HeapUsageSnapshot,
9
+ type RssUsageSnapshot,
10
+ } from "./memory-usage.js";
11
+
12
+ export type { HeapUsageSnapshot, RssUsageSnapshot };
13
+ export {
14
+ getHeapUsageSnapshot,
15
+ getRssUsageSnapshot,
16
+ getMemoryUsagePct,
17
+ classifyRamUsage,
18
+ } from "./memory-usage.js";
19
+
20
+ export interface HealthStatus {
21
+ ram: "ok" | "warning" | "critical";
22
+ streams: "ok" | "congested" | "blocked";
23
+ overall: "healthy" | "degraded" | "unhealthy";
24
+ heap?: HeapUsageSnapshot;
25
+ /** RSS vs total system memory — the RAM pressure signal used for `ram`. */
26
+ rss?: RssUsageSnapshot;
27
+ }
28
+
29
+ export class Watchdog extends EventEmitter {
30
+ private checkInterval: NodeJS.Timeout | null = null;
31
+ private consecutiveFailures: number = 0;
32
+ private recoveryInProgress: boolean = false;
33
+
34
+ start(): void {
35
+ if (this.checkInterval) return;
36
+
37
+ this.checkInterval = setInterval(() => {
38
+ this.performHealthCheck().catch((error) => {
39
+ this.emit("check:error", error);
40
+ this.consecutiveFailures++;
41
+ });
42
+ }, config.watchdog.checkInterval);
43
+ this.checkInterval.unref?.();
44
+
45
+ this.emit("started");
46
+ }
47
+
48
+ private async performHealthCheck(): Promise<void> {
49
+ const heap = getHeapUsageSnapshot();
50
+ const rss = getRssUsageSnapshot();
51
+ const status: HealthStatus = {
52
+ // RAM pressure is measured from RSS vs total system memory (not the V8
53
+ // heap): Playwright browser processes consume memory outside the heap,
54
+ // so heap-vs-limit misreads real pressure on a VPS.
55
+ ram: classifyRamUsage(
56
+ rss.usagePercent,
57
+ config.watchdog.ram.warningThreshold,
58
+ config.watchdog.ram.criticalThreshold,
59
+ ),
60
+ streams: this.checkStreams(),
61
+ overall: "healthy",
62
+ heap,
63
+ rss,
64
+ };
65
+
66
+ status.overall = this.calculateOverall(status);
67
+
68
+ if (status.overall === "unhealthy") {
69
+ this.consecutiveFailures++;
70
+ if (
71
+ this.consecutiveFailures >=
72
+ config.watchdog.consecutiveFailuresThreshold &&
73
+ !this.recoveryInProgress
74
+ ) {
75
+ await this.triggerRecovery(status);
76
+ }
77
+ } else {
78
+ this.consecutiveFailures = 0;
79
+ }
80
+
81
+ this.emit("health:check", status);
82
+ metrics.gauge(
83
+ "watchdog.ram.status",
84
+ status.ram === "ok" ? 0 : status.ram === "warning" ? 1 : 2,
85
+ );
86
+ metrics.gauge(
87
+ "watchdog.overall",
88
+ status.overall === "healthy" ? 0 : status.overall === "degraded" ? 1 : 2,
89
+ );
90
+ metrics.gauge("memory.heap.limit", heap.heapSizeLimit);
91
+ metrics.gauge("memory.heap.usage_percent", heap.usagePercent);
92
+ metrics.gauge("memory.rss", heap.rss);
93
+ metrics.gauge("memory.rss.usage_percent", rss.usagePercent);
94
+ }
95
+
96
+ private checkStreams(): "ok" | "congested" | "blocked" {
97
+ const activeStreams = metrics.get("streams.active")?.value || 0;
98
+ if (activeStreams > config.watchdog.streams.criticalThreshold)
99
+ return "blocked";
100
+ if (activeStreams > config.watchdog.streams.warningThreshold)
101
+ return "congested";
102
+ return "ok";
103
+ }
104
+
105
+ private calculateOverall(
106
+ status: HealthStatus,
107
+ ): "healthy" | "degraded" | "unhealthy" {
108
+ const critical = ["critical", "blocked"];
109
+ const warning = ["warning", "congested"];
110
+
111
+ const values = [status.ram, status.streams];
112
+ if (values.some((v) => critical.includes(v))) return "unhealthy";
113
+ if (values.some((v) => warning.includes(v))) return "degraded";
114
+ return "healthy";
115
+ }
116
+
117
+ private async triggerRecovery(status: HealthStatus): Promise<void> {
118
+ if (this.recoveryInProgress) return;
119
+ this.recoveryInProgress = true;
120
+
121
+ this.emit("recovery:start", status);
122
+ metrics.increment("watchdog.recovery.triggered");
123
+
124
+ try {
125
+ if (status.ram === "critical") {
126
+ await this.recoverRAM();
127
+ }
128
+ if (status.streams === "blocked") {
129
+ await this.recoverStreams();
130
+ }
131
+
132
+ this.emit("recovery:complete");
133
+ metrics.increment("watchdog.recovery.success");
134
+ } catch (error: any) {
135
+ this.emit("recovery:error", error);
136
+ metrics.increment("watchdog.recovery.failed");
137
+ } finally {
138
+ this.recoveryInProgress = false;
139
+ }
140
+ }
141
+
142
+ private async recoverRAM(): Promise<void> {
143
+ if (global.gc) global.gc();
144
+ await new Promise((resolve) => setTimeout(resolve, 100));
145
+ this.emit("recovery:ram:freed");
146
+ // RAM pressure is now measured from RSS, which Playwright browser processes
147
+ // dominate — GC alone cannot relieve it. Close genuinely parked contexts
148
+ // (idle mutex, no active stream, preserves the max-active-context minimum)
149
+ // so the next health check sees the freed RSS.
150
+ try {
151
+ const { closeIdlePlaywrightAccounts } = await import(
152
+ "../services/playwright.js"
153
+ );
154
+ const closed = await closeIdlePlaywrightAccounts(
155
+ config.sessionKeeper.idleMs,
156
+ );
157
+ if (closed > 0) {
158
+ this.emit("recovery:ram:contexts-closed", closed);
159
+ }
160
+ } catch {
161
+ // Playwright may not be initialized (mock mode / pre-start); RSS relief
162
+ // is best-effort and GC already ran.
163
+ }
164
+ }
165
+
166
+ private async recoverStreams(): Promise<void> {
167
+ this.emit("recovery:streams:throttled");
168
+ }
169
+
170
+ stop(): void {
171
+ if (this.checkInterval) {
172
+ clearInterval(this.checkInterval);
173
+ this.checkInterval = null;
174
+ }
175
+ this.emit("stopped");
176
+ }
177
+
178
+ getStatus(): Promise<HealthStatus> {
179
+ const heap = getHeapUsageSnapshot();
180
+ const rss = getRssUsageSnapshot();
181
+ const status: HealthStatus = {
182
+ ram: classifyRamUsage(
183
+ rss.usagePercent,
184
+ config.watchdog.ram.warningThreshold,
185
+ config.watchdog.ram.criticalThreshold,
186
+ ),
187
+ streams: this.checkStreams(),
188
+ overall: "healthy",
189
+ heap,
190
+ rss,
191
+ };
192
+ status.overall = this.calculateOverall(status);
193
+ return Promise.resolve(status);
194
+ }
195
+ }
@@ -0,0 +1,23 @@
1
+ import "dotenv/config";
2
+ import { deleteChatsForConfiguredAccounts } from "./services/chat-cleanup.ts";
3
+
4
+ async function run(): Promise<void> {
5
+ console.log("🗑️ [DeleteChats] Using Playwright sessions");
6
+
7
+ const result = await deleteChatsForConfiguredAccounts();
8
+ console.log(
9
+ `✅ [DeleteChats] Completed in ${result.mode} mode: ${result.succeeded}/${result.attempted} scope(s) cleared.`,
10
+ );
11
+
12
+ if (result.succeeded !== result.attempted) {
13
+ process.exitCode = 1;
14
+ }
15
+ }
16
+
17
+ run().catch((error) => {
18
+ console.error(
19
+ "[DeleteChats] Fatal error:",
20
+ error instanceof Error ? error.message : String(error),
21
+ );
22
+ process.exit(1);
23
+ });
package/src/index.ts ADDED
@@ -0,0 +1,64 @@
1
+ import dotenv from 'dotenv'
2
+ import fs from 'node:fs'
3
+ import { getEnvFilePath, ensureDataDirs } from './core/paths.ts'
4
+
5
+ // Ensure persistent user data directory exists
6
+ ensureDataDirs()
7
+
8
+ // Load .env from local directory or persistent global OS directory
9
+ const envPath = getEnvFilePath()
10
+ if (fs.existsSync(envPath)) {
11
+ dotenv.config({ path: envPath })
12
+ } else {
13
+ dotenv.config()
14
+ }
15
+ // Prevent benign asynchronous driver/browser teardown exceptions from crashing the server
16
+ process.on('uncaughtException', (error: unknown) => {
17
+ const msg = error instanceof Error ? error.message : String(error)
18
+ if (
19
+ msg.includes('Cannot find parent object') ||
20
+ msg.includes('Target page, context or browser has been closed') ||
21
+ msg.includes('Browser has been closed') ||
22
+ msg.includes('Target closed') ||
23
+ msg.includes('Connection closed')
24
+ ) {
25
+ console.warn(`⚠️ [Playwright] Handled benign driver teardown exception: ${msg}`)
26
+ return
27
+ }
28
+ console.error('❌ [Process] Uncaught Exception:', error)
29
+ })
30
+
31
+ process.on('unhandledRejection', (reason: unknown) => {
32
+ const msg = reason instanceof Error ? reason.message : String(reason)
33
+ if (
34
+ msg.includes('Cannot find parent object') ||
35
+ msg.includes('Target page, context or browser has been closed') ||
36
+ msg.includes('Browser has been closed') ||
37
+ msg.includes('Target closed') ||
38
+ msg.includes('Connection closed')
39
+ ) {
40
+ console.warn(`⚠️ [Playwright] Handled benign driver teardown rejection: ${msg}`)
41
+ return
42
+ }
43
+ console.error('❌ [Process] Unhandled Rejection:', reason)
44
+ })
45
+ import { startServer } from './api/server.js'
46
+ const isTui = process.argv.includes('--tui') || process.env.QWEN_TUI === 'true'
47
+
48
+ if (isTui) {
49
+ const { TuiApp } = await import('./tui/app.ts')
50
+ const app = new TuiApp()
51
+ await app.start()
52
+ } else {
53
+ startServer().catch((error: unknown) => {
54
+ const message = error instanceof Error ? error.message : String(error)
55
+ // Expected configuration errors are already formatted with an emoji and
56
+ // actionable guidance; print only the message to avoid leaking stack traces.
57
+ if (message.includes('[Server]')) {
58
+ console.error(message)
59
+ } else {
60
+ console.error('❌ [Server] Failed to start:', message)
61
+ }
62
+ process.exit(1)
63
+ })
64
+ }
package/src/login.ts ADDED
@@ -0,0 +1,147 @@
1
+ import {
2
+ addAccount,
3
+ removeAccount,
4
+ listAccounts,
5
+ type QwenAccount,
6
+ } from "./core/accounts.ts";
7
+
8
+ import { maskEmail } from "./core/logger.ts";
9
+ import * as readline from "readline";
10
+ import * as dotenv from "dotenv";
11
+
12
+ dotenv.config();
13
+
14
+ const rl = readline.createInterface({
15
+ input: process.stdin,
16
+ output: process.stdout,
17
+ });
18
+
19
+ function askQuestion(query: string): Promise<string> {
20
+ return new Promise((resolve) => {
21
+ rl.question(query, (answer) => {
22
+ resolve(answer.trim());
23
+ });
24
+ });
25
+ }
26
+
27
+ function clear() {
28
+ process.stdout.write("\x1Bc");
29
+ }
30
+
31
+ async function showMenu() {
32
+ while (true) {
33
+ const accounts = listAccounts();
34
+ clear();
35
+ console.log("=== QwenProxy Account Manager ===\n");
36
+ console.log("Auth mode: Playwright (validated on server start)\n");
37
+
38
+ if (accounts.length > 0) {
39
+ console.log(`Configured accounts (${accounts.length}):\n`);
40
+ for (let i = 0; i < accounts.length; i++) {
41
+ console.log(
42
+ ` [${i + 1}] ${accounts[i].email} (ID: ${accounts[i].id})`,
43
+ );
44
+ }
45
+ } else {
46
+ console.log("No accounts configured yet.\n");
47
+ }
48
+
49
+ console.log("\nOptions:");
50
+ console.log(" [A] Add account");
51
+ if (accounts.length > 0) {
52
+ console.log(" [R] Remove an account");
53
+ }
54
+ console.log(" [Q] Quit\n");
55
+
56
+ const choice = (await askQuestion("Select an option: ")).toUpperCase();
57
+
58
+ if (choice === "Q") {
59
+ rl.close();
60
+ process.exit(0);
61
+ }
62
+
63
+ if (choice === "A") {
64
+ await addAccountFlow();
65
+ continue;
66
+ }
67
+
68
+ if (choice === "R" && accounts.length > 0) {
69
+ await removeAccountFlow();
70
+ continue;
71
+ }
72
+ }
73
+ }
74
+
75
+ async function addAccountFlow() {
76
+ clear();
77
+ console.log("=== Add New Account ===\n");
78
+ const email = await askQuestion("Email: ");
79
+ if (!email) {
80
+ console.log("Email is required.");
81
+ await askQuestion("Press Enter to continue...");
82
+ return;
83
+ }
84
+
85
+ const password = await askQuestion("Password: ");
86
+ if (!password) {
87
+ console.log("Password is required.");
88
+ await askQuestion("Press Enter to continue...");
89
+ return;
90
+ }
91
+
92
+ let account: QwenAccount | null = null;
93
+ try {
94
+ account = addAccount(email, password);
95
+ console.log(`Account added: ${maskEmail(account.email)} (${account.id})`);
96
+ console.log("Credentials will be validated by Playwright on server start.");
97
+ } catch (err: any) {
98
+ if (account) removeAccount(account.id);
99
+ console.log(`\nError: ${err.message}`);
100
+ }
101
+
102
+ await askQuestion("Press Enter to continue...");
103
+ }
104
+
105
+ async function removeAccountFlow() {
106
+ const accounts = listAccounts();
107
+ if (accounts.length === 0) return;
108
+
109
+ clear();
110
+ console.log("=== Remove Account ===\n");
111
+
112
+ for (let i = 0; i < accounts.length; i++) {
113
+ console.log(
114
+ ` [${i + 1}] ${maskEmail(accounts[i].email)} (ID: ${accounts[i].id})`,
115
+ );
116
+ }
117
+
118
+ const input = await askQuestion(
119
+ "\nSelect account number to remove (or 0 to cancel): ",
120
+ );
121
+ const idx = parseInt(input) - 1;
122
+
123
+ if (isNaN(idx) || idx < 0 || idx >= accounts.length) {
124
+ console.log(input !== "0" ? "Invalid selection." : "Cancelled.");
125
+ await askQuestion("Press Enter to continue...");
126
+ return;
127
+ }
128
+
129
+ const account = accounts[idx];
130
+ const confirm = await askQuestion(`\nRemove ${account.email}? (y/N): `);
131
+ if (confirm.toLowerCase() === "y") {
132
+ if (removeAccount(account.id)) {
133
+ console.log(`Account ${maskEmail(account.email)} removed.`);
134
+ } else {
135
+ console.log("Failed to remove account.");
136
+ }
137
+ } else {
138
+ console.log("Cancelled.");
139
+ }
140
+
141
+ await askQuestion("Press Enter to continue...");
142
+ }
143
+
144
+ showMenu().catch((err) => {
145
+ console.error(err);
146
+ process.exit(1);
147
+ });
@@ -0,0 +1,11 @@
1
+ import { clearAllAccountCooldowns } from "./core/account-manager.ts";
2
+ import { loadAccounts } from "./core/accounts.ts";
3
+
4
+ function main() {
5
+ const accounts = loadAccounts();
6
+ console.log(`🔍 Checking cooldowns for ${accounts.length} configured account(s)...`);
7
+ const cleared = clearAllAccountCooldowns();
8
+ console.log(`✅ Cooldowns reset successfully: ${cleared} account(s) cleared.`);
9
+ }
10
+
11
+ main();