qwenproxy-cli 1.2.10 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/api/server.ts +14 -2
- package/src/core/accounts.ts +118 -17
- package/src/core/local-auth.ts +52 -0
- package/src/core/paths.ts +3 -0
- package/src/login.ts +55 -8
- package/src/routes/chat/errors.ts +8 -0
- package/src/routes/chat/retry-policy.ts +7 -0
- package/src/routes/chat/streaming.ts +42 -3
- package/src/routes/upload.ts +51 -12
- package/src/services/auth-playwright.ts +5 -28
- package/src/services/playwright.ts +338 -112
- package/src/services/qwen.ts +2 -2
- package/src/sync/codex.ts +154 -65
- package/src/sync/index.ts +7 -2
- package/src/tools/parser.ts +103 -0
- package/src/tools/toolcall-tags.ts +2 -2
- package/src/tui/server-manager.ts +6 -0
- package/src/tui/views/accounts-view.ts +54 -6
- package/src/tui/views/logs-view.ts +4 -20
- package/src/tui/views/status-view.ts +496 -3
- package/src/tui/views/sync-view.ts +22 -10
- package/src/utils/jwt.ts +40 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qwenproxy-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "High-performance OpenAI & Anthropic compatible API gateway for Qwen with multi-account rotation, interactive TUI, and resilient tool calling.",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"bin": {
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"reset": "npx tsx src/reset-cooldowns.ts",
|
|
27
27
|
"purge": "npx tsx src/delete-chats.ts",
|
|
28
28
|
"test": "npm run test:mock && npm run test:live",
|
|
29
|
-
"test:mock": "tsx --test --test-concurrency=1 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chat-cleanup-auto.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/local-auth.test.ts src/tests/safe-remote-url.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/sync-top10-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/tui-settings.test.ts src/tests/personalization-deadline.test.ts src/tests/update-cli.test.ts src/tests/batch-accounts.test.ts src/tests/shared-browser.test.ts src/tests/auth-credential-classification.test.ts src/tests/responses-api.test.ts src/tests/media-generation.test.ts src/tests/infer-tool-name.test.ts src/tests/native-session-headers.test.ts src/tests/omni-thinking.test.ts src/tests/protect-stuck-mutex.test.ts src/tests/stream-error-notice.test.ts src/tests/parser-deep-coverage.test.ts src/tests/tui-logs-copy.test.ts",
|
|
30
|
-
"test:mock:fast": "tsx --test --test-concurrency=2 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chat-cleanup-auto.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/local-auth.test.ts src/tests/safe-remote-url.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/sync-top10-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/tui-settings.test.ts src/tests/personalization-deadline.test.ts src/tests/update-cli.test.ts src/tests/batch-accounts.test.ts src/tests/shared-browser.test.ts src/tests/auth-credential-classification.test.ts src/tests/responses-api.test.ts src/tests/media-generation.test.ts src/tests/infer-tool-name.test.ts src/tests/native-session-headers.test.ts src/tests/omni-thinking.test.ts src/tests/protect-stuck-mutex.test.ts src/tests/stream-error-notice.test.ts src/tests/parser-deep-coverage.test.ts src/tests/tui-logs-copy.test.ts",
|
|
29
|
+
"test:mock": "tsx --test --test-concurrency=1 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chat-cleanup-auto.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/local-auth.test.ts src/tests/safe-remote-url.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/sync-top10-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/tui-settings.test.ts src/tests/personalization-deadline.test.ts src/tests/update-cli.test.ts src/tests/batch-accounts.test.ts src/tests/shared-browser.test.ts src/tests/auth-credential-classification.test.ts src/tests/responses-api.test.ts src/tests/media-generation.test.ts src/tests/infer-tool-name.test.ts src/tests/native-session-headers.test.ts src/tests/omni-thinking.test.ts src/tests/protect-stuck-mutex.test.ts src/tests/stream-error-notice.test.ts src/tests/parser-deep-coverage.test.ts src/tests/tui-logs-copy.test.ts src/tests/auth-jwt-and-validation.test.ts src/tests/tui-port-and-key-modal.test.ts",
|
|
30
|
+
"test:mock:fast": "tsx --test --test-concurrency=2 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chat-cleanup-auto.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/local-auth.test.ts src/tests/safe-remote-url.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/sync-top10-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/tui-settings.test.ts src/tests/personalization-deadline.test.ts src/tests/update-cli.test.ts src/tests/batch-accounts.test.ts src/tests/shared-browser.test.ts src/tests/auth-credential-classification.test.ts src/tests/responses-api.test.ts src/tests/media-generation.test.ts src/tests/infer-tool-name.test.ts src/tests/native-session-headers.test.ts src/tests/omni-thinking.test.ts src/tests/protect-stuck-mutex.test.ts src/tests/stream-error-notice.test.ts src/tests/parser-deep-coverage.test.ts src/tests/tui-logs-copy.test.ts src/tests/auth-jwt-and-validation.test.ts src/tests/tui-port-and-key-modal.test.ts",
|
|
31
31
|
"test:live": "tsx --test src/tests/concurrentChat.test.ts src/tests/agenticStress.test.ts",
|
|
32
32
|
"benchmark:proxy": "npx tsx src/benchmarks/proxy-baseline.ts",
|
|
33
33
|
"typecheck": "tsc --noEmit"
|
package/src/api/server.ts
CHANGED
|
@@ -172,7 +172,13 @@ app.use("*", async (c, next) => {
|
|
|
172
172
|
c.req.path === "/logs" ||
|
|
173
173
|
c.req.path.startsWith("/logs") ||
|
|
174
174
|
c.req.path.startsWith("/diagnostics") ||
|
|
175
|
-
c.req.path === "/favicon.ico"
|
|
175
|
+
c.req.path === "/favicon.ico" ||
|
|
176
|
+
c.req.path === "/v1/models" ||
|
|
177
|
+
c.req.path.startsWith("/v1/models/") ||
|
|
178
|
+
c.req.path === "/models" ||
|
|
179
|
+
c.req.path.startsWith("/models/") ||
|
|
180
|
+
c.req.path === "/v1/chat/mode" ||
|
|
181
|
+
c.req.path === "/chat/mode";
|
|
176
182
|
|
|
177
183
|
if (!isProbe) {
|
|
178
184
|
metrics.increment("requests.total");
|
|
@@ -445,7 +451,13 @@ app.onError((err, c) => {
|
|
|
445
451
|
c.req.path === "/health" ||
|
|
446
452
|
c.req.path === "/metrics" ||
|
|
447
453
|
c.req.path.startsWith("/diagnostics") ||
|
|
448
|
-
c.req.path === "/favicon.ico"
|
|
454
|
+
c.req.path === "/favicon.ico" ||
|
|
455
|
+
c.req.path === "/v1/models" ||
|
|
456
|
+
c.req.path.startsWith("/v1/models/") ||
|
|
457
|
+
c.req.path === "/models" ||
|
|
458
|
+
c.req.path.startsWith("/models/") ||
|
|
459
|
+
c.req.path === "/v1/chat/mode" ||
|
|
460
|
+
c.req.path === "/chat/mode";
|
|
449
461
|
if (!isProbe) {
|
|
450
462
|
metrics.increment("requests.errors");
|
|
451
463
|
}
|
package/src/core/accounts.ts
CHANGED
|
@@ -4,7 +4,8 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { getDatabase } from "./database.ts";
|
|
6
6
|
import { decrypt, encrypt } from "./crypto-utils.ts";
|
|
7
|
-
import { getAccountProfilePath, getProfilesDir } from "./paths.ts";
|
|
7
|
+
import { getAccountProfilePath, getProfilesDir, isRunningUnderNodeTest } from "./paths.ts";
|
|
8
|
+
import { updateEnvVariable } from "./local-auth.ts";
|
|
8
9
|
|
|
9
10
|
export interface QwenAccount {
|
|
10
11
|
id: string;
|
|
@@ -14,6 +15,29 @@ export interface QwenAccount {
|
|
|
14
15
|
cooldown_reason?: string | null;
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
export function isPlaceholderAccountEmail(email: string, password?: string): boolean {
|
|
19
|
+
if (!email || typeof email !== "string") return false;
|
|
20
|
+
const clean = email.trim().toLowerCase();
|
|
21
|
+
if (
|
|
22
|
+
clean === "email1@example.com" ||
|
|
23
|
+
clean === "email2@example.com" ||
|
|
24
|
+
clean === "user@example.com" ||
|
|
25
|
+
clean === "test@example.com" ||
|
|
26
|
+
clean === "your-email@example.com" ||
|
|
27
|
+
clean.startsWith("email1@") ||
|
|
28
|
+
clean.startsWith("email2@")
|
|
29
|
+
) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
if (
|
|
33
|
+
password &&
|
|
34
|
+
(password === "password1" || password === "password2" || password === "changeme")
|
|
35
|
+
) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
17
41
|
function generateId(email: string): string {
|
|
18
42
|
return crypto
|
|
19
43
|
.createHash("md5")
|
|
@@ -59,10 +83,12 @@ function parseEnvAccounts(): QwenAccount[] {
|
|
|
59
83
|
}
|
|
60
84
|
const email = clean(trimmed.substring(0, colonIdx));
|
|
61
85
|
const password = clean(trimmed.substring(colonIdx + 1));
|
|
62
|
-
if (!email || !password) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
86
|
+
if (!email || !password || isPlaceholderAccountEmail(email, password)) {
|
|
87
|
+
if (!email || !password) {
|
|
88
|
+
console.warn(
|
|
89
|
+
`[Accounts] Invalid QWEN_ACCOUNTS entry at index ${index}: "${trimmed}"`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
66
92
|
return null;
|
|
67
93
|
}
|
|
68
94
|
return {
|
|
@@ -79,6 +105,17 @@ let lastSyncTime = 0;
|
|
|
79
105
|
const SYNC_INTERVAL = 30_000;
|
|
80
106
|
|
|
81
107
|
function syncEnvAccounts(): void {
|
|
108
|
+
const db = getDatabase();
|
|
109
|
+
// Automatically purge any dummy placeholder accounts that may have been seeded from .env.example
|
|
110
|
+
try {
|
|
111
|
+
db.prepare(`
|
|
112
|
+
DELETE FROM accounts
|
|
113
|
+
WHERE lower(email) IN ('email1@example.com', 'email2@example.com', 'user@example.com', 'test@example.com', 'your-email@example.com')
|
|
114
|
+
OR lower(email) LIKE 'email1@%'
|
|
115
|
+
OR lower(email) LIKE 'email2@%'
|
|
116
|
+
`).run();
|
|
117
|
+
} catch {}
|
|
118
|
+
|
|
82
119
|
const envAccounts = process.env.QWEN_ACCOUNTS || "";
|
|
83
120
|
const now = Date.now();
|
|
84
121
|
if (envAccounts === lastSyncedEnv && now - lastSyncTime < SYNC_INTERVAL)
|
|
@@ -90,7 +127,6 @@ function syncEnvAccounts(): void {
|
|
|
90
127
|
const accounts = parseEnvAccounts();
|
|
91
128
|
if (accounts.length === 0) return;
|
|
92
129
|
|
|
93
|
-
const db = getDatabase();
|
|
94
130
|
const upsert = db.prepare(`
|
|
95
131
|
INSERT INTO accounts (id, email, password) VALUES (?, ?, ?)
|
|
96
132
|
ON CONFLICT(email) DO UPDATE SET password = excluded.password, updated_at = datetime('now')
|
|
@@ -124,15 +160,17 @@ function getCachedAccounts(): QwenAccount[] {
|
|
|
124
160
|
)
|
|
125
161
|
.all() as QwenAccount[];
|
|
126
162
|
|
|
127
|
-
accountsCache = rows
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
password =
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
163
|
+
accountsCache = rows
|
|
164
|
+
.filter((row) => !isPlaceholderAccountEmail(row.email))
|
|
165
|
+
.map((row) => {
|
|
166
|
+
let password = "";
|
|
167
|
+
try {
|
|
168
|
+
password = decrypt(row.password);
|
|
169
|
+
} catch {
|
|
170
|
+
password = "";
|
|
171
|
+
}
|
|
172
|
+
return { ...row, password };
|
|
173
|
+
});
|
|
136
174
|
accountsCacheTime = now;
|
|
137
175
|
return accountsCache;
|
|
138
176
|
}
|
|
@@ -316,22 +354,85 @@ export function addAccount(
|
|
|
316
354
|
return newAccount;
|
|
317
355
|
}
|
|
318
356
|
|
|
319
|
-
function wipeAccountSessionFiles(id: string): void {
|
|
357
|
+
export function wipeAccountSessionFiles(id: string): void {
|
|
320
358
|
const profilePath = getAccountProfilePath(id);
|
|
321
359
|
try {
|
|
322
360
|
fs.rmSync(profilePath, { recursive: true, force: true });
|
|
323
|
-
} catch {
|
|
361
|
+
} catch (error) {
|
|
362
|
+
const message =
|
|
363
|
+
error instanceof Error ? error.message : String(error ?? "");
|
|
364
|
+
if (
|
|
365
|
+
message.includes("EPERM") ||
|
|
366
|
+
message.includes("EBUSY") ||
|
|
367
|
+
message.includes("Permission denied")
|
|
368
|
+
) {
|
|
369
|
+
try {
|
|
370
|
+
const stalePath = `${profilePath}.stale-${Date.now()}`;
|
|
371
|
+
fs.renameSync(profilePath, stalePath);
|
|
372
|
+
} catch {}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
324
375
|
const siblingState = path.join(getProfilesDir(), `${id}_state.json`);
|
|
325
376
|
try {
|
|
326
377
|
fs.rmSync(siblingState, { force: true });
|
|
327
378
|
} catch {}
|
|
328
379
|
}
|
|
329
380
|
|
|
381
|
+
export function removeAccountFromEnv(emailToRemove: string): void {
|
|
382
|
+
const target = emailToRemove.trim().toLowerCase();
|
|
383
|
+
const rawEnv = process.env.QWEN_ACCOUNTS;
|
|
384
|
+
if (!rawEnv) return;
|
|
385
|
+
|
|
386
|
+
const clean = (s: string) => s.trim().replace(/^[,;\s"'`]+|[,;\s"'`]+$/g, "").trim();
|
|
387
|
+
const lines = rawEnv.split(/[\r\n;]+/);
|
|
388
|
+
const remaining: string[] = [];
|
|
389
|
+
|
|
390
|
+
for (const rawLine of lines) {
|
|
391
|
+
const line = clean(rawLine);
|
|
392
|
+
if (!line) continue;
|
|
393
|
+
|
|
394
|
+
if (line.includes(",") && (line.match(/@/g) || []).length > 1) {
|
|
395
|
+
for (const seg of line.split(",")) {
|
|
396
|
+
const trimmed = clean(seg);
|
|
397
|
+
if (!trimmed) continue;
|
|
398
|
+
const colonIdx = trimmed.indexOf(":");
|
|
399
|
+
const email = clean(colonIdx !== -1 ? trimmed.substring(0, colonIdx) : trimmed);
|
|
400
|
+
if (email.toLowerCase() !== target && !isPlaceholderAccountEmail(email)) {
|
|
401
|
+
remaining.push(trimmed);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
} else {
|
|
405
|
+
const colonIdx = line.indexOf(":");
|
|
406
|
+
const email = clean(colonIdx !== -1 ? line.substring(0, colonIdx) : line);
|
|
407
|
+
if (email.toLowerCase() !== target && !isPlaceholderAccountEmail(email)) {
|
|
408
|
+
remaining.push(line);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const newEnvValue = remaining.join(",");
|
|
414
|
+
process.env.QWEN_ACCOUNTS = newEnvValue;
|
|
415
|
+
lastSyncedEnv = newEnvValue;
|
|
416
|
+
lastSyncTime = Date.now();
|
|
417
|
+
|
|
418
|
+
if (!isRunningUnderNodeTest()) {
|
|
419
|
+
try {
|
|
420
|
+
updateEnvVariable("QWEN_ACCOUNTS", newEnvValue);
|
|
421
|
+
} catch {}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
330
425
|
export function removeAccount(id: string): boolean {
|
|
331
426
|
const db = getDatabase();
|
|
427
|
+
const account = db
|
|
428
|
+
.prepare("SELECT email FROM accounts WHERE id = ?")
|
|
429
|
+
.get(id) as { email: string } | undefined;
|
|
332
430
|
const result = db.prepare("DELETE FROM accounts WHERE id = ?").run(id);
|
|
333
431
|
if (result.changes > 0) {
|
|
334
432
|
wipeAccountSessionFiles(id);
|
|
433
|
+
if (account?.email) {
|
|
434
|
+
removeAccountFromEnv(account.email);
|
|
435
|
+
}
|
|
335
436
|
}
|
|
336
437
|
invalidateAccountsCache();
|
|
337
438
|
return result.changes > 0;
|
package/src/core/local-auth.ts
CHANGED
|
@@ -43,6 +43,58 @@ export function assertBindAllowed(host: string, apiKey: string): void {
|
|
|
43
43
|
);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
export function updateEnvVariable(
|
|
47
|
+
key: string,
|
|
48
|
+
value: string,
|
|
49
|
+
envPath = getEnvFilePath(),
|
|
50
|
+
): void {
|
|
51
|
+
try {
|
|
52
|
+
const existing = fs.existsSync(envPath)
|
|
53
|
+
? fs.readFileSync(envPath, "utf-8")
|
|
54
|
+
: "";
|
|
55
|
+
const regex = new RegExp(`^([ \\t]*)${key}[ \\t]*=.*$`, "m");
|
|
56
|
+
const assignment = regex.exec(existing);
|
|
57
|
+
|
|
58
|
+
let updated: string;
|
|
59
|
+
if (assignment) {
|
|
60
|
+
const start = assignment.index;
|
|
61
|
+
const end = start + assignment[0].length;
|
|
62
|
+
updated =
|
|
63
|
+
existing.slice(0, start) +
|
|
64
|
+
`${assignment[1]}${key}=${value}` +
|
|
65
|
+
existing.slice(end);
|
|
66
|
+
} else {
|
|
67
|
+
const lineEnding = existing.includes("\r\n") ? "\r\n" : "\n";
|
|
68
|
+
const prefix =
|
|
69
|
+
existing.length === 0 || existing.endsWith("\n") ? "" : lineEnding;
|
|
70
|
+
updated = `${existing}${prefix}${key}=${value}${lineEnding}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fs.writeFileSync(envPath, updated, { encoding: "utf-8", mode: 0o600 });
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.warn(
|
|
76
|
+
`⚠️ [Config] Could not update ${key} in .env:`,
|
|
77
|
+
err instanceof Error ? err.message : String(err),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function persistServerPort(
|
|
83
|
+
port: number,
|
|
84
|
+
envPath = getEnvFilePath(),
|
|
85
|
+
): void {
|
|
86
|
+
updateEnvVariable("PORT", String(port), envPath);
|
|
87
|
+
process.env.PORT = String(port);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function persistCustomApiKey(
|
|
91
|
+
apiKey: string,
|
|
92
|
+
envPath = getEnvFilePath(),
|
|
93
|
+
): void {
|
|
94
|
+
updateEnvVariable("API_KEY", apiKey.trim(), envPath);
|
|
95
|
+
process.env.API_KEY = apiKey.trim();
|
|
96
|
+
}
|
|
97
|
+
|
|
46
98
|
export function persistApiKey(
|
|
47
99
|
apiKey: string,
|
|
48
100
|
envPath = getEnvFilePath(),
|
package/src/core/paths.ts
CHANGED
|
@@ -181,6 +181,9 @@ export function getTuiSettingsPath(customDataDir?: string): string {
|
|
|
181
181
|
* Path to user configuration .env file (either local or in global data dir).
|
|
182
182
|
*/
|
|
183
183
|
export function getEnvFilePath(customDataDir?: string): string {
|
|
184
|
+
if (isRunningUnderNodeTest()) {
|
|
185
|
+
return path.join(customDataDir || getDataDir(), ".env");
|
|
186
|
+
}
|
|
184
187
|
const localEnv = path.resolve(".env");
|
|
185
188
|
if (fs.existsSync(localEnv)) {
|
|
186
189
|
return localEnv;
|
package/src/login.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
1
2
|
import {
|
|
2
3
|
addAccount,
|
|
3
4
|
removeAccount,
|
|
@@ -76,30 +77,76 @@ async function addAccountFlow() {
|
|
|
76
77
|
clear();
|
|
77
78
|
console.log("=== Add New Account ===\n");
|
|
78
79
|
const email = await askQuestion("Email: ");
|
|
79
|
-
if (!email) {
|
|
80
|
+
if (!email || !email.trim()) {
|
|
80
81
|
console.log("Email is required.");
|
|
81
82
|
await askQuestion("Press Enter to continue...");
|
|
82
83
|
return;
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
const password = await askQuestion("Password: ");
|
|
86
|
-
if (!password) {
|
|
87
|
+
if (!password || !password.trim()) {
|
|
87
88
|
console.log("Password is required.");
|
|
88
89
|
await askQuestion("Press Enter to continue...");
|
|
89
90
|
return;
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
const trimmedEmail = email.trim();
|
|
94
|
+
const trimmedPassword = password.trim();
|
|
95
|
+
|
|
96
|
+
const existingAccounts = listAccounts();
|
|
97
|
+
if (
|
|
98
|
+
existingAccounts.some(
|
|
99
|
+
(a) => a.email.toLowerCase() === trimmedEmail.toLowerCase(),
|
|
100
|
+
)
|
|
101
|
+
) {
|
|
102
|
+
console.log(
|
|
103
|
+
`\n❌ Error: An account with email "${trimmedEmail}" already exists.`,
|
|
104
|
+
);
|
|
105
|
+
await askQuestion("Press Enter to continue...");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const tempId = crypto.randomUUID();
|
|
110
|
+
console.log(
|
|
111
|
+
`\n⏳ Validating credentials for ${maskEmail(trimmedEmail)} with Qwen via Playwright...`,
|
|
112
|
+
);
|
|
113
|
+
|
|
92
114
|
let account: QwenAccount | null = null;
|
|
93
115
|
try {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
116
|
+
const { validateAccountLogin } = await import("./services/playwright.ts");
|
|
117
|
+
const { wipeAccountSessionFiles } = await import("./core/accounts.ts");
|
|
118
|
+
const { clearAccountCooldown } = await import("./core/account-manager.ts");
|
|
119
|
+
const { config } = await import("./core/config.ts");
|
|
120
|
+
|
|
121
|
+
const ok = await validateAccountLogin(
|
|
122
|
+
{ id: tempId, email: trimmedEmail, password: trimmedPassword },
|
|
123
|
+
config.playwright.headless,
|
|
124
|
+
config.playwright.browser,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
if (ok) {
|
|
128
|
+
account = addAccount(trimmedEmail, trimmedPassword, tempId);
|
|
129
|
+
console.log(
|
|
130
|
+
`\n✅ Account verified and saved: ${maskEmail(account.email)} (${account.id})`,
|
|
131
|
+
);
|
|
132
|
+
} else {
|
|
133
|
+
wipeAccountSessionFiles(tempId);
|
|
134
|
+
clearAccountCooldown(tempId);
|
|
135
|
+
console.log(
|
|
136
|
+
`\n❌ Authentication failed: Invalid credentials or unhandled login challenge.`,
|
|
137
|
+
);
|
|
138
|
+
console.log("The account was NOT saved.");
|
|
139
|
+
}
|
|
97
140
|
} catch (err: any) {
|
|
98
|
-
|
|
99
|
-
|
|
141
|
+
const { wipeAccountSessionFiles } = await import("./core/accounts.ts");
|
|
142
|
+
const { clearAccountCooldown } = await import("./core/account-manager.ts");
|
|
143
|
+
wipeAccountSessionFiles(tempId);
|
|
144
|
+
clearAccountCooldown(tempId);
|
|
145
|
+
console.log(`\n❌ Validation error: ${err?.message || String(err)}`);
|
|
146
|
+
console.log("The account was NOT saved.");
|
|
100
147
|
}
|
|
101
148
|
|
|
102
|
-
await askQuestion("
|
|
149
|
+
await askQuestion("\nPress Enter to continue...");
|
|
103
150
|
}
|
|
104
151
|
|
|
105
152
|
async function removeAccountFlow() {
|
|
@@ -34,6 +34,14 @@ export function parseQwenErrorPayload(
|
|
|
34
34
|
|
|
35
35
|
try {
|
|
36
36
|
const payload = JSON.parse(text);
|
|
37
|
+
if (payload && (payload.update_member || payload.data?.update_member)) {
|
|
38
|
+
return {
|
|
39
|
+
code: "MembershipLimit",
|
|
40
|
+
details: "Account membership limit reached",
|
|
41
|
+
message: "Qwen upstream error: MembershipLimit: Account membership limit reached",
|
|
42
|
+
status: 429,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
37
45
|
if (payload && payload.success === false) {
|
|
38
46
|
const code = payload.data?.code || payload.code || "UpstreamError";
|
|
39
47
|
const details =
|
|
@@ -242,6 +242,8 @@ export function isQuotaLikeError(err: unknown): boolean {
|
|
|
242
242
|
return (
|
|
243
243
|
code === "quota_limit" ||
|
|
244
244
|
code === "ratelimited" ||
|
|
245
|
+
code === "membership_limit" ||
|
|
246
|
+
code === "membershiplimit" ||
|
|
245
247
|
message.includes("quota_limit") ||
|
|
246
248
|
message.includes("quota exceeded") ||
|
|
247
249
|
message.includes("allocated quota") ||
|
|
@@ -253,11 +255,16 @@ export function isQuotaLikeError(err: unknown): boolean {
|
|
|
253
255
|
message.includes("rate increased too quickly") ||
|
|
254
256
|
message.includes("upper limit for today's usage") ||
|
|
255
257
|
message.includes("you've reached the upper limit") ||
|
|
258
|
+
message.includes("membership_limit") ||
|
|
259
|
+
message.includes("membership limit") ||
|
|
260
|
+
message.includes("update_member") ||
|
|
261
|
+
message.includes("update member") ||
|
|
256
262
|
// Accept local rate_limit code only when message also looks like quota/rate
|
|
257
263
|
(code === "rate_limit_exceeded" &&
|
|
258
264
|
(message.includes("quota") ||
|
|
259
265
|
message.includes("rate") ||
|
|
260
266
|
message.includes("limit") ||
|
|
267
|
+
message.includes("membership") ||
|
|
261
268
|
message.includes("demanda") ||
|
|
262
269
|
message.includes("demand")))
|
|
263
270
|
);
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
setToolCapNotice,
|
|
19
19
|
} from "../../services/qwen.ts";
|
|
20
20
|
import { acquireUpstreamStream } from "./account.ts";
|
|
21
|
-
import { markAccountRateLimited } from "../../core/account-manager.ts";
|
|
21
|
+
import { markAccountRateLimited, computeQuotaCooldownMs } from "../../core/account-manager.ts";
|
|
22
22
|
import {
|
|
23
23
|
clearTemporaryBusy,
|
|
24
24
|
markAccountTemporarilyBusy,
|
|
@@ -438,6 +438,20 @@ export async function processNonStreamingResponse(
|
|
|
438
438
|
) {
|
|
439
439
|
const delta = chunk.choices[0].delta;
|
|
440
440
|
|
|
441
|
+
if (delta.extra?.update_member || chunk.update_member) {
|
|
442
|
+
throw toRetryableStreamError(
|
|
443
|
+
"membership_limit",
|
|
444
|
+
"Qwen upstream membership limit reached (update_member); rotating account",
|
|
445
|
+
{
|
|
446
|
+
switchAccount: true,
|
|
447
|
+
forceNewChat: true,
|
|
448
|
+
reason: "membership_limit",
|
|
449
|
+
accountCooldownMs: computeQuotaCooldownMs(Date.now()),
|
|
450
|
+
accountCooldownReason: "MembershipLimit",
|
|
451
|
+
},
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
441
455
|
if (isThinkingPhase(delta.phase)) {
|
|
442
456
|
isThinkingChunk = true;
|
|
443
457
|
const formattedSummary = extractThinkingContent(delta);
|
|
@@ -952,6 +966,11 @@ export async function processStreamingResponse(
|
|
|
952
966
|
|
|
953
967
|
// Release the lease immediately. The stop request below is best-effort
|
|
954
968
|
// and must never hold the account slot or block the next tool turn.
|
|
969
|
+
if (onStreamComplete) {
|
|
970
|
+
try {
|
|
971
|
+
onStreamComplete();
|
|
972
|
+
} catch {}
|
|
973
|
+
}
|
|
955
974
|
retryContext.releaseAccountLease?.();
|
|
956
975
|
retryContext.releaseAccountLease = null;
|
|
957
976
|
removeStream(completionId);
|
|
@@ -1739,6 +1758,20 @@ export async function processStreamingResponse(
|
|
|
1739
1758
|
) {
|
|
1740
1759
|
const delta = chunk.choices[0].delta;
|
|
1741
1760
|
|
|
1761
|
+
if (delta.extra?.update_member || chunk.update_member) {
|
|
1762
|
+
throw toRetryableStreamError(
|
|
1763
|
+
"membership_limit",
|
|
1764
|
+
"Qwen upstream membership limit reached (update_member); rotating account",
|
|
1765
|
+
{
|
|
1766
|
+
switchAccount: true,
|
|
1767
|
+
forceNewChat: true,
|
|
1768
|
+
reason: "membership_limit",
|
|
1769
|
+
accountCooldownMs: computeQuotaCooldownMs(Date.now()),
|
|
1770
|
+
accountCooldownReason: "MembershipLimit",
|
|
1771
|
+
},
|
|
1772
|
+
);
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1742
1775
|
// Qwen streams may end with a {"status":"finished",
|
|
1743
1776
|
// "phase":"answer"} delta and NO trailing [DONE]. Treat it as
|
|
1744
1777
|
// the terminal event so we don't wait on the keep-alive
|
|
@@ -2624,11 +2657,17 @@ export async function processStreamingResponse(
|
|
|
2624
2657
|
}
|
|
2625
2658
|
|
|
2626
2659
|
// Release locks now that the stream is fully done
|
|
2627
|
-
if (onStreamComplete)
|
|
2660
|
+
if (onStreamComplete) {
|
|
2661
|
+
try {
|
|
2662
|
+
onStreamComplete();
|
|
2663
|
+
} catch {}
|
|
2664
|
+
}
|
|
2628
2665
|
|
|
2629
2666
|
// Release account lease from transparent retry if active
|
|
2630
2667
|
if (retryContext.releaseAccountLease) {
|
|
2631
|
-
|
|
2668
|
+
try {
|
|
2669
|
+
retryContext.releaseAccountLease();
|
|
2670
|
+
} catch {}
|
|
2632
2671
|
retryContext.releaseAccountLease = null;
|
|
2633
2672
|
}
|
|
2634
2673
|
}
|