qwenproxy-cli 1.0.25 → 1.0.26
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 +3 -0
- package/src/core/config.ts +9 -2
- package/src/routes/chat/account.ts +9 -2
- package/src/services/chat-cleanup.ts +172 -3
- package/src/services/qwen-thread-state.ts +37 -0
- package/src/services/qwen.ts +110 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qwenproxy-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.26",
|
|
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": {
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"reset": "npx tsx src/reset-cooldowns.ts",
|
|
25
25
|
"purge": "npx tsx src/delete-chats.ts",
|
|
26
26
|
"test": "npm run test:mock && npm run test:live",
|
|
27
|
-
"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/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/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/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/personalization-deadline.test.ts",
|
|
28
|
-
"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/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/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/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/personalization-deadline.test.ts",
|
|
27
|
+
"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/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/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/personalization-deadline.test.ts",
|
|
28
|
+
"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/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/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/personalization-deadline.test.ts",
|
|
29
29
|
"test:live": "tsx --test src/tests/concurrentChat.test.ts src/tests/agenticStress.test.ts",
|
|
30
30
|
"benchmark:proxy": "npx tsx src/benchmarks/proxy-baseline.ts",
|
|
31
31
|
"typecheck": "tsc --noEmit"
|
package/src/api/server.ts
CHANGED
|
@@ -814,6 +814,9 @@ export async function startServer(options?: {
|
|
|
814
814
|
await import("../core/account-concurrency.ts");
|
|
815
815
|
startLeaseSweepTimer();
|
|
816
816
|
|
|
817
|
+
const { scheduleStartupChatCleanup } =
|
|
818
|
+
await import("../services/chat-cleanup.ts");
|
|
819
|
+
scheduleStartupChatCleanup();
|
|
817
820
|
const serverInstance = serve({
|
|
818
821
|
fetch: app.fetch,
|
|
819
822
|
port: config.server.port,
|
package/src/core/config.ts
CHANGED
|
@@ -161,7 +161,9 @@ const envSchema = z
|
|
|
161
161
|
CONTEXT_METER_WINDOW_TOKENS: z.string().default("0"),
|
|
162
162
|
CONTEXT_METER_REPORT_USAGE: z.string().default("true"),
|
|
163
163
|
DELETE_ALL_CHATS_ON_SHUTDOWN: z.string().default("false"),
|
|
164
|
-
|
|
164
|
+
AUTO_CLEAN_CHATS_ON_STARTUP: z.string().default("true"),
|
|
165
|
+
AUTO_CLEAN_ORPHAN_CHATS: z.string().default("true"),
|
|
166
|
+
AUTO_CLEAN_CHAT_MAX_AGE_HOURS: z.string().default("24"),
|
|
165
167
|
// The Baxia WAF scores live page behavior (pointer/scroll events, open
|
|
166
168
|
// session) — an account whose page sits frozen for minutes returns a low
|
|
167
169
|
// trust score and gets TMD-challenged on the next request. On by default;
|
|
@@ -367,7 +369,12 @@ export const config = {
|
|
|
367
369
|
parseInt(env.QWEN_MAX_PERSONALIZATION_BYTES),
|
|
368
370
|
),
|
|
369
371
|
deleteAllChatsOnShutdown: env.DELETE_ALL_CHATS_ON_SHUTDOWN === "true",
|
|
370
|
-
|
|
372
|
+
autoCleanChatsOnStartup: env.AUTO_CLEAN_CHATS_ON_STARTUP !== "false",
|
|
373
|
+
autoCleanOrphanChats: env.AUTO_CLEAN_ORPHAN_CHATS !== "false",
|
|
374
|
+
autoCleanChatMaxAgeHours: Math.max(
|
|
375
|
+
1,
|
|
376
|
+
parseInt(env.AUTO_CLEAN_CHAT_MAX_AGE_HOURS) || 24,
|
|
377
|
+
),
|
|
371
378
|
sendBxUa: env.QWEN_SEND_BX_UA === "true",
|
|
372
379
|
/** Deployed web bundle version sent as the `version` API header. */
|
|
373
380
|
webVersion: env.QWEN_WEB_VERSION,
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
} from "../../core/account-concurrency.ts";
|
|
37
37
|
import { isAuthMockEnabled } from "../../services/auth-playwright.ts";
|
|
38
38
|
import { isPlaywrightInitialized, refreshHeaders } from "../../services/playwright.ts";
|
|
39
|
+
import { enqueueOrphanChatDeletion } from "../../services/chat-cleanup.ts";
|
|
39
40
|
import {
|
|
40
41
|
clearAllSessionsForAccount,
|
|
41
42
|
createQwenStream,
|
|
@@ -1720,12 +1721,15 @@ async function tryCreateStreamWithRetry(
|
|
|
1720
1721
|
// Do NOT persist sticky binding until create succeeds — premature empty
|
|
1721
1722
|
// chatSessionId writes make subsequent turns rotate/lose context.
|
|
1722
1723
|
if (params.useThreadNative) {
|
|
1724
|
+
const abandonedChatId = params.existingThread?.chatSessionId;
|
|
1725
|
+
if (abandonedChatId) {
|
|
1726
|
+
enqueueOrphanChatDeletion(currentAccountId, abandonedChatId, policy.reason);
|
|
1727
|
+
}
|
|
1723
1728
|
params.existingThread = null;
|
|
1724
1729
|
params.finalPrompt = params.fullPrompt;
|
|
1725
1730
|
params.messageCount = params.fullMessageCount ?? params.messageCount;
|
|
1726
1731
|
params.forceNewChat = true;
|
|
1727
1732
|
}
|
|
1728
|
-
|
|
1729
1733
|
await new Promise((resolve) =>
|
|
1730
1734
|
setTimeout(
|
|
1731
1735
|
resolve,
|
|
@@ -1749,12 +1753,15 @@ async function tryCreateStreamWithRetry(
|
|
|
1749
1753
|
console.warn(
|
|
1750
1754
|
`🔄 [Chat] Forcing new chat/full context | reason=${policy.reason}`,
|
|
1751
1755
|
);
|
|
1756
|
+
const abandonedChatId = params.existingThread?.chatSessionId;
|
|
1757
|
+
if (abandonedChatId) {
|
|
1758
|
+
enqueueOrphanChatDeletion(currentAccountId, abandonedChatId, policy.reason);
|
|
1759
|
+
}
|
|
1752
1760
|
params.existingThread = null;
|
|
1753
1761
|
params.finalPrompt = params.fullPrompt;
|
|
1754
1762
|
params.messageCount = params.fullMessageCount ?? params.messageCount;
|
|
1755
1763
|
params.forceNewChat = true;
|
|
1756
1764
|
}
|
|
1757
|
-
|
|
1758
1765
|
// Drop files on retry for invalid_input to isolate file-related errors
|
|
1759
1766
|
if (policy.dropFiles && params.allFiles.length > 0) {
|
|
1760
1767
|
console.warn(
|
|
@@ -3,14 +3,24 @@ import {
|
|
|
3
3
|
loadAccounts,
|
|
4
4
|
type QwenAccount,
|
|
5
5
|
} from "../core/accounts.ts";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
deleteAllQwenChats,
|
|
8
|
+
deleteSingleQwenChat,
|
|
9
|
+
fetchRemoteQwenChats,
|
|
10
|
+
} from "./qwen.ts";
|
|
7
11
|
import {
|
|
8
12
|
initPlaywrightForAccount,
|
|
9
13
|
isPlaywrightInitialized,
|
|
10
14
|
closeAllPlaywright,
|
|
15
|
+
getActivePlaywrightAccountIds,
|
|
11
16
|
} from "./playwright.ts";
|
|
12
17
|
import { isAuthMockEnabled } from "./auth-playwright.ts";
|
|
13
|
-
import { maskEmail } from "../core/logger.ts";
|
|
18
|
+
import { maskEmail, logger } from "../core/logger.ts";
|
|
19
|
+
import { config } from "../core/config.ts";
|
|
20
|
+
import { isAccountBusy } from "../core/account-concurrency.ts";
|
|
21
|
+
import { isChatSessionActive } from "./qwen-thread-state.ts";
|
|
22
|
+
import { sleep } from "./human-behavior.ts";
|
|
23
|
+
|
|
14
24
|
export interface DeleteChatsResult {
|
|
15
25
|
attempted: number;
|
|
16
26
|
succeeded: number;
|
|
@@ -109,10 +119,169 @@ export async function deleteChatsForConfiguredAccounts(keepBrowserOpen = false):
|
|
|
109
119
|
});
|
|
110
120
|
}
|
|
111
121
|
}
|
|
112
|
-
|
|
113
122
|
return {
|
|
114
123
|
attempted: accounts.length,
|
|
115
124
|
succeeded,
|
|
116
125
|
mode: "accounts",
|
|
117
126
|
};
|
|
118
127
|
}
|
|
128
|
+
|
|
129
|
+
interface OrphanChatEntry {
|
|
130
|
+
accountId: string;
|
|
131
|
+
chatId: string;
|
|
132
|
+
reason: string;
|
|
133
|
+
enqueuedAt: number;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const orphanChatQueue: OrphanChatEntry[] = [];
|
|
137
|
+
let isProcessingQueue = false;
|
|
138
|
+
|
|
139
|
+
export function getOrphanQueueLengthForTests(): number {
|
|
140
|
+
return orphanChatQueue.length;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Enqueues an abandoned/orphan chat for asynchronous deletion in background.
|
|
145
|
+
* Never blocks the calling request and never deletes a chat actively in use.
|
|
146
|
+
*/
|
|
147
|
+
export function enqueueOrphanChatDeletion(
|
|
148
|
+
accountId: string,
|
|
149
|
+
chatId: string,
|
|
150
|
+
reason = "orphan",
|
|
151
|
+
): void {
|
|
152
|
+
if (!config.qwen.autoCleanOrphanChats || !accountId || !chatId) return;
|
|
153
|
+
if (orphanChatQueue.some((e) => e.accountId === accountId && e.chatId === chatId)) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
orphanChatQueue.push({
|
|
157
|
+
accountId,
|
|
158
|
+
chatId,
|
|
159
|
+
reason,
|
|
160
|
+
enqueuedAt: Date.now(),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (!isProcessingQueue) {
|
|
164
|
+
const timer = setTimeout(() => {
|
|
165
|
+
void processOrphanChatQueue().catch(() => {});
|
|
166
|
+
}, 1_000);
|
|
167
|
+
timer.unref?.();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function processOrphanChatQueue(): Promise<void> {
|
|
172
|
+
if (isProcessingQueue) return;
|
|
173
|
+
isProcessingQueue = true;
|
|
174
|
+
try {
|
|
175
|
+
while (orphanChatQueue.length > 0) {
|
|
176
|
+
const entry = orphanChatQueue[0];
|
|
177
|
+
|
|
178
|
+
// Never delete a chat that is currently actively bound to an ongoing session
|
|
179
|
+
if (isChatSessionActive(entry.chatId)) {
|
|
180
|
+
orphanChatQueue.shift();
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// If the account is actively serving a stream, postpone deletion
|
|
185
|
+
if (!isAuthMockEnabled() && isAccountBusy(entry.accountId)) {
|
|
186
|
+
await sleep(2_000);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const ok = await deleteSingleQwenChat(entry.accountId, entry.chatId);
|
|
192
|
+
if (ok) {
|
|
193
|
+
logger.debug("[ChatCleanup] Deleted orphan chat", {
|
|
194
|
+
accountId: entry.accountId,
|
|
195
|
+
chatId: entry.chatId,
|
|
196
|
+
reason: entry.reason,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
} catch (err) {
|
|
200
|
+
logger.debug("[ChatCleanup] Failed to delete orphan chat", {
|
|
201
|
+
accountId: entry.accountId,
|
|
202
|
+
chatId: entry.chatId,
|
|
203
|
+
error: err instanceof Error ? err.message : String(err),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
orphanChatQueue.shift();
|
|
208
|
+
if (orphanChatQueue.length > 0) {
|
|
209
|
+
await sleep(300);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
} finally {
|
|
213
|
+
isProcessingQueue = false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export async function drainOrphanChatQueueForTests(): Promise<void> {
|
|
218
|
+
await processOrphanChatQueue();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Scan an account's remote chat list and delete chats older than maxAgeHours,
|
|
223
|
+
* skipping any chat that is currently actively bound to a session.
|
|
224
|
+
*/
|
|
225
|
+
export async function cleanOldChatsForAccount(
|
|
226
|
+
accountId: string,
|
|
227
|
+
maxAgeHours = config.qwen.autoCleanChatMaxAgeHours,
|
|
228
|
+
): Promise<number> {
|
|
229
|
+
try {
|
|
230
|
+
const chats = await fetchRemoteQwenChats(accountId);
|
|
231
|
+
if (!Array.isArray(chats) || chats.length === 0) return 0;
|
|
232
|
+
|
|
233
|
+
const maxAgeMs = maxAgeHours * 60 * 60 * 1000;
|
|
234
|
+
const now = Date.now();
|
|
235
|
+
let cleaned = 0;
|
|
236
|
+
|
|
237
|
+
for (const chat of chats) {
|
|
238
|
+
if (!chat?.id) continue;
|
|
239
|
+
if (isChatSessionActive(chat.id)) continue;
|
|
240
|
+
|
|
241
|
+
const updatedMs =
|
|
242
|
+
typeof chat.updated_at === "number"
|
|
243
|
+
? (chat.updated_at > 1e11 ? chat.updated_at : chat.updated_at * 1000)
|
|
244
|
+
: new Date(chat.updated_at).getTime();
|
|
245
|
+
|
|
246
|
+
if (now - updatedMs >= maxAgeMs) {
|
|
247
|
+
const ok = await deleteSingleQwenChat(accountId, chat.id);
|
|
248
|
+
if (ok) {
|
|
249
|
+
cleaned++;
|
|
250
|
+
await sleep(300);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (cleaned > 0) {
|
|
256
|
+
console.log(
|
|
257
|
+
`🧹 [ChatCleanup] Remotely cleaned ${cleaned} old chat(s) (> ${maxAgeHours}h) for account ${accountId}`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
return cleaned;
|
|
261
|
+
} catch (error) {
|
|
262
|
+
logger.debug("[ChatCleanup] cleanOldChatsForAccount error", {
|
|
263
|
+
accountId,
|
|
264
|
+
error: error instanceof Error ? error.message : String(error),
|
|
265
|
+
});
|
|
266
|
+
return 0;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Startup hook: runs 15s after startup in the background, cleaning old chats
|
|
272
|
+
* for active accounts without holding up the server or user requests.
|
|
273
|
+
*/
|
|
274
|
+
export function scheduleStartupChatCleanup(): void {
|
|
275
|
+
if (!config.qwen.autoCleanChatsOnStartup || isAuthMockEnabled()) return;
|
|
276
|
+
|
|
277
|
+
const timer = setTimeout(async () => {
|
|
278
|
+
try {
|
|
279
|
+
const activeIds = getActivePlaywrightAccountIds();
|
|
280
|
+
for (const accountId of activeIds) {
|
|
281
|
+
await cleanOldChatsForAccount(accountId);
|
|
282
|
+
await sleep(1_000);
|
|
283
|
+
}
|
|
284
|
+
} catch {}
|
|
285
|
+
}, 15_000);
|
|
286
|
+
timer.unref?.();
|
|
287
|
+
}
|
|
@@ -376,6 +376,43 @@ export function clearAllSessionsForAccount(accountId: string): void {
|
|
|
376
376
|
);
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
+
/**
|
|
380
|
+
* Returns true if a chatSessionId is currently actively bound to any non-expired session.
|
|
381
|
+
* Prevents auto-cleanup routines from deleting conversations currently in use.
|
|
382
|
+
*/
|
|
383
|
+
export function isChatSessionActive(chatId: string): boolean {
|
|
384
|
+
if (!chatId) return false;
|
|
385
|
+
const now = Date.now();
|
|
386
|
+
for (const entry of logicalThreadStates.values()) {
|
|
387
|
+
if (entry.chatSessionId === chatId && now - entry.timestamp <= SESSION_TTL_MS) {
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const sessionEntry = sessionStates.get(chatId);
|
|
392
|
+
if (sessionEntry && now - sessionEntry.timestamp <= SESSION_TTL_MS) {
|
|
393
|
+
return true;
|
|
394
|
+
}
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Invalidate and remove all local cached/database records for a given chatId.
|
|
400
|
+
*/
|
|
401
|
+
export function removeSessionByChatId(chatId: string): void {
|
|
402
|
+
if (!chatId) return;
|
|
403
|
+
sessionStates.delete(chatId);
|
|
404
|
+
for (const [key, entry] of logicalThreadStates.entries()) {
|
|
405
|
+
if (entry.chatSessionId === chatId) {
|
|
406
|
+
logicalThreadStates.delete(key);
|
|
407
|
+
logicalThreadDirty.delete(key);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
try {
|
|
411
|
+
const db = getDatabase();
|
|
412
|
+
db.prepare("DELETE FROM logical_thread_states WHERE chat_session_id = ?").run(chatId);
|
|
413
|
+
} catch {}
|
|
414
|
+
}
|
|
415
|
+
|
|
379
416
|
export function getSessionParent(
|
|
380
417
|
sessionId: string,
|
|
381
418
|
accountId?: string,
|
package/src/services/qwen.ts
CHANGED
|
@@ -53,6 +53,8 @@ export {
|
|
|
53
53
|
updateSessionParent,
|
|
54
54
|
invalidateLogicalThreadParent,
|
|
55
55
|
clearAllSessionsForAccount,
|
|
56
|
+
removeSessionByChatId,
|
|
57
|
+
isChatSessionActive,
|
|
56
58
|
getSessionParent,
|
|
57
59
|
} from "./qwen-thread-state.ts";
|
|
58
60
|
export type { LogicalThreadEntry } from "./qwen-thread-state.ts";
|
|
@@ -74,6 +76,8 @@ import {
|
|
|
74
76
|
} from "./qwen-errors.ts";
|
|
75
77
|
import {
|
|
76
78
|
clearAllSessionsForAccount,
|
|
79
|
+
removeSessionByChatId,
|
|
80
|
+
isChatSessionActive,
|
|
77
81
|
getSessionParent,
|
|
78
82
|
updateSessionParent,
|
|
79
83
|
} from "./qwen-thread-state.ts";
|
|
@@ -1977,6 +1981,112 @@ export async function deleteAllQwenChats(accountId?: string): Promise<boolean> {
|
|
|
1977
1981
|
return true;
|
|
1978
1982
|
}
|
|
1979
1983
|
|
|
1984
|
+
export interface RemoteQwenChat {
|
|
1985
|
+
id: string;
|
|
1986
|
+
title: string;
|
|
1987
|
+
updated_at: number | string;
|
|
1988
|
+
created_at: number | string;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
/**
|
|
1992
|
+
* Deletes a single chat session by ID on Qwen Web.
|
|
1993
|
+
*/
|
|
1994
|
+
export async function deleteSingleQwenChat(
|
|
1995
|
+
accountId: string | undefined,
|
|
1996
|
+
chatId: string,
|
|
1997
|
+
): Promise<boolean> {
|
|
1998
|
+
if (!chatId) return false;
|
|
1999
|
+
|
|
2000
|
+
if (isAuthMockEnabled()) {
|
|
2001
|
+
const url = qwenUrl(`/api/v2/chats/${encodeURIComponent(chatId)}`);
|
|
2002
|
+
const response = await fetch(url, {
|
|
2003
|
+
method: "DELETE",
|
|
2004
|
+
});
|
|
2005
|
+
removeSessionByChatId(chatId);
|
|
2006
|
+
return response.ok;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
const requestHeaders: Record<string, string> = {
|
|
2010
|
+
source: "web",
|
|
2011
|
+
version: "0.2.89",
|
|
2012
|
+
timezone: new Date().toString().split(" (")[0],
|
|
2013
|
+
"x-request-id": crypto.randomUUID(),
|
|
2014
|
+
Referer: qwenUrl(`/c/${encodeURIComponent(chatId)}`),
|
|
2015
|
+
};
|
|
2016
|
+
|
|
2017
|
+
try {
|
|
2018
|
+
const response = await requestQwenTextInBrowser(
|
|
2019
|
+
accountId,
|
|
2020
|
+
"DELETE",
|
|
2021
|
+
`/api/v2/chats/${encodeURIComponent(chatId)}`,
|
|
2022
|
+
requestHeaders,
|
|
2023
|
+
undefined,
|
|
2024
|
+
{ referrer: qwenUrl(`/c/${encodeURIComponent(chatId)}`), noMutexRecovery: true },
|
|
2025
|
+
);
|
|
2026
|
+
|
|
2027
|
+
const { json: parsed } = await readJsonTextResponse(response, {
|
|
2028
|
+
strict: false,
|
|
2029
|
+
});
|
|
2030
|
+
|
|
2031
|
+
const success = response.ok && parsed?.success === true && parsed?.data?.status === true;
|
|
2032
|
+
if (success) {
|
|
2033
|
+
removeSessionByChatId(chatId);
|
|
2034
|
+
}
|
|
2035
|
+
return success;
|
|
2036
|
+
} catch (error) {
|
|
2037
|
+
logger.debug("[Qwen] deleteSingleQwenChat failed", {
|
|
2038
|
+
accountId,
|
|
2039
|
+
chatId,
|
|
2040
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2041
|
+
});
|
|
2042
|
+
return false;
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
/**
|
|
2047
|
+
* Fetch remote chat list from Qwen Web.
|
|
2048
|
+
*/
|
|
2049
|
+
export async function fetchRemoteQwenChats(
|
|
2050
|
+
accountId?: string,
|
|
2051
|
+
): Promise<RemoteQwenChat[]> {
|
|
2052
|
+
if (isAuthMockEnabled()) {
|
|
2053
|
+
try {
|
|
2054
|
+
const response = await fetch(qwenUrl("/api/v2/chats/?page=1&exclude_project=true"));
|
|
2055
|
+
const json: any = await response.json().catch(() => null);
|
|
2056
|
+
if (json?.success && Array.isArray(json.data)) {
|
|
2057
|
+
return json.data;
|
|
2058
|
+
}
|
|
2059
|
+
} catch {}
|
|
2060
|
+
return [];
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
const requestHeaders: Record<string, string> = {
|
|
2064
|
+
version: "0.2.89",
|
|
2065
|
+
timezone: new Date().toString().split(" (")[0],
|
|
2066
|
+
"x-request-id": crypto.randomUUID(),
|
|
2067
|
+
Referer: qwenUrl("/settings/chats"),
|
|
2068
|
+
};
|
|
2069
|
+
|
|
2070
|
+
try {
|
|
2071
|
+
const response = await requestQwenTextInBrowser(
|
|
2072
|
+
accountId,
|
|
2073
|
+
"GET",
|
|
2074
|
+
"/api/v2/chats/?page=1&exclude_project=true",
|
|
2075
|
+
requestHeaders,
|
|
2076
|
+
undefined,
|
|
2077
|
+
{ referrer: qwenUrl("/settings/chats"), noMutexRecovery: true },
|
|
2078
|
+
);
|
|
2079
|
+
if (!response.ok) return [];
|
|
2080
|
+
const json: any = await response.json().catch(() => null);
|
|
2081
|
+
if (json?.success && Array.isArray(json.data)) {
|
|
2082
|
+
return json.data;
|
|
2083
|
+
}
|
|
2084
|
+
return [];
|
|
2085
|
+
} catch {
|
|
2086
|
+
return [];
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
|
|
1980
2090
|
export async function fetchQwenModels(
|
|
1981
2091
|
accountId?: string,
|
|
1982
2092
|
): Promise<PublicQwenModel[]> {
|