qwenproxy-cli 1.0.25 → 1.0.27
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 +85 -4
- package/src/core/config.ts +9 -2
- package/src/core/metrics.ts +6 -1
- package/src/core/paths.ts +7 -0
- package/src/core/server-log-buffer.ts +105 -0
- package/src/routes/chat/account.ts +9 -2
- package/src/routes/chat/index.ts +6 -0
- package/src/services/chat-cleanup.ts +175 -3
- package/src/services/qwen-thread-state.ts +37 -0
- package/src/services/qwen.ts +110 -0
- package/src/sync/index.ts +47 -11
- package/src/tools/parser.ts +4 -1
- package/src/tui/app.ts +29 -20
- package/src/tui/index.ts +2 -2
- package/src/tui/proxy-client.ts +58 -4
- package/src/tui/server-manager.ts +46 -1
- package/src/tui/settings.ts +100 -0
- package/src/tui/theme.ts +15 -9
- package/src/tui/types.ts +19 -0
- package/src/tui/views/accounts-view.ts +50 -3
- package/src/tui/views/chat-view.ts +41 -2
- package/src/tui/views/logs-view.ts +22 -18
- package/src/tui/views/status-view.ts +180 -41
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qwenproxy-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.27",
|
|
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/tui-settings.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/tui-settings.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
|
@@ -22,6 +22,11 @@ import { AuthError, NotFoundError } from "../core/errors.js";
|
|
|
22
22
|
import type { QwenAccount } from "../core/accounts.js";
|
|
23
23
|
import { isAuthMockEnabled } from "../services/auth-playwright.js";
|
|
24
24
|
|
|
25
|
+
import {
|
|
26
|
+
hookServerConsoleForLogging,
|
|
27
|
+
getServerLogHistory,
|
|
28
|
+
subscribeServerLogStream,
|
|
29
|
+
} from "../core/server-log-buffer.ts";
|
|
25
30
|
// Module-level state (initialized in startServer)
|
|
26
31
|
let cache: MemoryCache | undefined;
|
|
27
32
|
let watchdog: Watchdog | undefined;
|
|
@@ -131,12 +136,23 @@ app.use("*", async (c, next) => {
|
|
|
131
136
|
String(Math.max(0, ratelimit.tokens - 1)),
|
|
132
137
|
);
|
|
133
138
|
c.header("x-ratelimit-reset-tokens", "0");
|
|
134
|
-
|
|
135
|
-
|
|
139
|
+
const isProbe =
|
|
140
|
+
c.req.path === "/health" ||
|
|
141
|
+
c.req.path === "/metrics" ||
|
|
142
|
+
c.req.path === "/logs" ||
|
|
143
|
+
c.req.path.startsWith("/logs") ||
|
|
144
|
+
c.req.path.startsWith("/diagnostics") ||
|
|
145
|
+
c.req.path === "/favicon.ico";
|
|
146
|
+
|
|
147
|
+
if (!isProbe) {
|
|
148
|
+
metrics.increment("requests.total");
|
|
149
|
+
}
|
|
136
150
|
const start = Date.now();
|
|
137
151
|
await next();
|
|
138
152
|
const duration = Date.now() - start;
|
|
139
|
-
|
|
153
|
+
if (!isProbe) {
|
|
154
|
+
metrics.histogram("latency.request", duration);
|
|
155
|
+
}
|
|
140
156
|
c.header("X-Response-Time", `${duration}ms`);
|
|
141
157
|
c.header("openai-processing-ms", String(duration));
|
|
142
158
|
});
|
|
@@ -272,6 +288,22 @@ app.get("/health", async (c) => {
|
|
|
272
288
|
activeAccounts: (await import("../services/playwright.js")).getActivePlaywrightAccountIds(),
|
|
273
289
|
metrics: {
|
|
274
290
|
cache: await cache?.getStats(),
|
|
291
|
+
requestsTotal: Number(metrics.get("requests.total")?.value ?? 0),
|
|
292
|
+
requestsErrors: Number(metrics.get("requests.errors")?.value ?? 0),
|
|
293
|
+
latencyAvgMs: (() => {
|
|
294
|
+
const hist = metrics.get("latency.request")?.value;
|
|
295
|
+
if (hist && typeof hist === "object" && (hist as any).count > 0) {
|
|
296
|
+
return Math.round((hist as any).sum / (hist as any).count);
|
|
297
|
+
}
|
|
298
|
+
return 0;
|
|
299
|
+
})(),
|
|
300
|
+
deltasCount: Number(metrics.get("requests.delta")?.value ?? 0),
|
|
301
|
+
fullReplaysCount: Number(metrics.get("requests.full")?.value ?? 0),
|
|
302
|
+
toolCallsCount: Number(metrics.get("toolcalls.total")?.value ?? 0),
|
|
303
|
+
toolCallsRecovered: Number(metrics.get("toolcalls.recovered")?.value ?? 0),
|
|
304
|
+
captchasDetected: Number(metrics.get("captcha.challenges.detected")?.value ?? 0),
|
|
305
|
+
captchasSolved: Number(metrics.get("captcha.solves.succeeded")?.value ?? 0),
|
|
306
|
+
chatsCleaned: Number(metrics.get("chats.cleaned")?.value ?? 0),
|
|
275
307
|
},
|
|
276
308
|
});
|
|
277
309
|
});
|
|
@@ -305,9 +337,54 @@ app.get("/metrics", (c) => {
|
|
|
305
337
|
});
|
|
306
338
|
});
|
|
307
339
|
|
|
340
|
+
app.get("/logs", (c) => {
|
|
341
|
+
return c.json(getServerLogHistory());
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
app.get("/logs/live", (c) => {
|
|
345
|
+
const encoder = new TextEncoder();
|
|
346
|
+
return c.body(
|
|
347
|
+
new ReadableStream({
|
|
348
|
+
start(controller) {
|
|
349
|
+
const past = getServerLogHistory();
|
|
350
|
+
for (const entry of past) {
|
|
351
|
+
try {
|
|
352
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(entry)}\n\n`));
|
|
353
|
+
} catch {}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const unsubscribe = subscribeServerLogStream((entry) => {
|
|
357
|
+
try {
|
|
358
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(entry)}\n\n`));
|
|
359
|
+
} catch {
|
|
360
|
+
unsubscribe();
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
c.req.raw.signal.addEventListener("abort", () => {
|
|
365
|
+
unsubscribe();
|
|
366
|
+
});
|
|
367
|
+
},
|
|
368
|
+
}),
|
|
369
|
+
200,
|
|
370
|
+
{
|
|
371
|
+
"Content-Type": "text/event-stream",
|
|
372
|
+
"Cache-Control": "no-cache",
|
|
373
|
+
Connection: "keep-alive",
|
|
374
|
+
},
|
|
375
|
+
);
|
|
376
|
+
});
|
|
377
|
+
|
|
308
378
|
app.onError((err, c) => {
|
|
309
379
|
const requestId = c.req.header("X-Request-Id") || "unknown";
|
|
310
|
-
|
|
380
|
+
const isProbe =
|
|
381
|
+
c.req.path === "/health" ||
|
|
382
|
+
c.req.path === "/metrics" ||
|
|
383
|
+
c.req.path.startsWith("/diagnostics") ||
|
|
384
|
+
c.req.path === "/favicon.ico";
|
|
385
|
+
if (!isProbe) {
|
|
386
|
+
metrics.increment("requests.errors");
|
|
387
|
+
}
|
|
311
388
|
logger.error("API Error", {
|
|
312
389
|
requestId,
|
|
313
390
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -599,6 +676,7 @@ export async function startServer(options?: {
|
|
|
599
676
|
installSignalHandlers?: boolean;
|
|
600
677
|
showBanner?: boolean;
|
|
601
678
|
}): Promise<StartedServerInfo> {
|
|
679
|
+
hookServerConsoleForLogging();
|
|
602
680
|
if (server) {
|
|
603
681
|
if (options?.installSignalHandlers !== false) installSignalHandlers();
|
|
604
682
|
return buildStartedServerInfo();
|
|
@@ -814,6 +892,9 @@ export async function startServer(options?: {
|
|
|
814
892
|
await import("../core/account-concurrency.ts");
|
|
815
893
|
startLeaseSweepTimer();
|
|
816
894
|
|
|
895
|
+
const { scheduleStartupChatCleanup } =
|
|
896
|
+
await import("../services/chat-cleanup.ts");
|
|
897
|
+
scheduleStartupChatCleanup();
|
|
817
898
|
const serverInstance = serve({
|
|
818
899
|
fetch: app.fetch,
|
|
819
900
|
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,
|
package/src/core/metrics.ts
CHANGED
|
@@ -32,8 +32,13 @@ export class Metrics extends EventEmitter {
|
|
|
32
32
|
// Core request metrics
|
|
33
33
|
["requests.total", "counter", "Total requests processed"],
|
|
34
34
|
["requests.errors", "counter", "Total request errors"],
|
|
35
|
+
["requests.delta", "counter", "Thread-native delta requests"],
|
|
36
|
+
["requests.full", "counter", "Full context replay or initial requests"],
|
|
35
37
|
["latency.request", "histogram", "Request latency (ms)"],
|
|
36
|
-
|
|
38
|
+
["toolcalls.total", "counter", "Total tool calls processed"],
|
|
39
|
+
["toolcalls.malformed", "counter", "Malformed tool calls detected"],
|
|
40
|
+
["toolcalls.recovered", "counter", "Malformed tool calls recovered/healed"],
|
|
41
|
+
["chats.cleaned", "counter", "Remote chats automatically cleaned"],
|
|
37
42
|
// Stream metrics
|
|
38
43
|
["streams.active", "gauge", "Active SSE streams"],
|
|
39
44
|
["streams.errors", "counter", "Stream errors"],
|
package/src/core/paths.ts
CHANGED
|
@@ -170,6 +170,13 @@ export function getSyncStatePath(customDataDir?: string): string {
|
|
|
170
170
|
return path.join(customDataDir || getDataDir(), "sync-state.json");
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
/**
|
|
174
|
+
* Path to TUI user preferences file (model, effort, last tab, etc.).
|
|
175
|
+
*/
|
|
176
|
+
export function getTuiSettingsPath(customDataDir?: string): string {
|
|
177
|
+
return path.join(customDataDir || getDataDir(), "tui-settings.json");
|
|
178
|
+
}
|
|
179
|
+
|
|
173
180
|
/**
|
|
174
181
|
* Path to user configuration .env file (either local or in global data dir).
|
|
175
182
|
*/
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QwenProxy - Server Log Circular Buffer & Live SSE Broadcast
|
|
3
|
+
* Captures server logs and streams them to connected TUI instances.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { stripAnsi } from "../tui/theme.ts";
|
|
7
|
+
|
|
8
|
+
export interface ServerLogMessage {
|
|
9
|
+
time: string;
|
|
10
|
+
level: "INFO" | "WARN" | "ERROR";
|
|
11
|
+
message: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const MAX_LOG_HISTORY = 400;
|
|
15
|
+
const logHistory: ServerLogMessage[] = [];
|
|
16
|
+
const subscribers = new Set<(entry: ServerLogMessage) => void>();
|
|
17
|
+
let consoleHooked = false;
|
|
18
|
+
|
|
19
|
+
export function recordServerLog(level: "INFO" | "WARN" | "ERROR", text: string): void {
|
|
20
|
+
if (!text) return;
|
|
21
|
+
const clean = stripAnsi(text).trim();
|
|
22
|
+
if (!clean) return;
|
|
23
|
+
|
|
24
|
+
const time = new Date().toLocaleTimeString("pt-BR", {
|
|
25
|
+
hour: "2-digit",
|
|
26
|
+
minute: "2-digit",
|
|
27
|
+
second: "2-digit",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const lines = clean.split(/\r?\n/);
|
|
31
|
+
for (const raw of lines) {
|
|
32
|
+
const line = raw.trim();
|
|
33
|
+
if (!line) continue;
|
|
34
|
+
// Filter out raw ASCII box frames and border rows
|
|
35
|
+
if (/^[+\-=#]{5,}$/.test(line)) continue;
|
|
36
|
+
if (/^\|\s*\|$/.test(line)) continue;
|
|
37
|
+
if (line.startsWith("|") && line.endsWith("|")) continue;
|
|
38
|
+
|
|
39
|
+
// Filter out box remnants
|
|
40
|
+
if (
|
|
41
|
+
line === "QwenProxy" ||
|
|
42
|
+
line === "OpenAI & Anthropic Compatible API" ||
|
|
43
|
+
/^Endpoint\s+http/i.test(line) ||
|
|
44
|
+
/^Port\s+\d+/i.test(line) ||
|
|
45
|
+
/^Accounts\s+\d+\/\d+/i.test(line) ||
|
|
46
|
+
/^API Key\s+/i.test(line) ||
|
|
47
|
+
/^Status\s+●/i.test(line)
|
|
48
|
+
) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const entry: ServerLogMessage = { time, level, message: line };
|
|
53
|
+
logHistory.push(entry);
|
|
54
|
+
if (logHistory.length > MAX_LOG_HISTORY) {
|
|
55
|
+
logHistory.shift();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const sub of subscribers) {
|
|
59
|
+
try {
|
|
60
|
+
sub(entry);
|
|
61
|
+
} catch {}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function getServerLogHistory(): ServerLogMessage[] {
|
|
67
|
+
return logHistory.slice();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function subscribeServerLogStream(cb: (entry: ServerLogMessage) => void): () => void {
|
|
71
|
+
subscribers.add(cb);
|
|
72
|
+
return () => {
|
|
73
|
+
subscribers.delete(cb);
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function hookServerConsoleForLogging(): void {
|
|
78
|
+
if (consoleHooked) return;
|
|
79
|
+
consoleHooked = true;
|
|
80
|
+
|
|
81
|
+
const origLog = console.log;
|
|
82
|
+
const origInfo = console.info;
|
|
83
|
+
const origWarn = console.warn;
|
|
84
|
+
const origError = console.error;
|
|
85
|
+
|
|
86
|
+
console.log = (...args: any[]) => {
|
|
87
|
+
origLog(...args);
|
|
88
|
+
recordServerLog("INFO", args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" "));
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
console.info = (...args: any[]) => {
|
|
92
|
+
origInfo(...args);
|
|
93
|
+
recordServerLog("INFO", args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" "));
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
console.warn = (...args: any[]) => {
|
|
97
|
+
origWarn(...args);
|
|
98
|
+
recordServerLog("WARN", args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" "));
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
console.error = (...args: any[]) => {
|
|
102
|
+
origError(...args);
|
|
103
|
+
recordServerLog("ERROR", args.map((a) => (typeof a === "string" ? a : JSON.stringify(a))).join(" "));
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -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(
|
package/src/routes/chat/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from "./streaming.ts";
|
|
23
23
|
import { config, type ChatMode } from "../../core/config.ts";
|
|
24
24
|
import { logger } from "../../core/logger.ts";
|
|
25
|
+
import { metrics } from "../../core/metrics.ts";
|
|
25
26
|
import { getContextMeterHeaders, type ContextMeterMode } from "../../services/context-meter.ts";
|
|
26
27
|
import {
|
|
27
28
|
getLogicalThreadState,
|
|
@@ -277,6 +278,11 @@ export async function chatCompletions(c: Context) {
|
|
|
277
278
|
console.log(
|
|
278
279
|
`📤 [${routeLabel}] Request | req=${reqId} | ${streamResult.activeAccountLabel} | ${body.model} | ${replayed ? parsed.messageCount : msgCount} msg(s) | ${replayed ? fullPromptForRequest.length : finalPrompt.length} chars${replayed ? " | full-replay" : ""} | chat=${streamResult.uiSessionId.substring(0, 12)}${declaredTools.length ? ` | ${declaredTools.length} tool(s)` : ""}${files.length ? ` | ${files.length} file(s)` : ""} | +${Date.now() - reqStartedAt}ms`,
|
|
279
280
|
);
|
|
281
|
+
if (replayed || !ctx.existingThread) {
|
|
282
|
+
metrics.increment("requests.full");
|
|
283
|
+
} else {
|
|
284
|
+
metrics.increment("requests.delta");
|
|
285
|
+
}
|
|
280
286
|
|
|
281
287
|
const onAssistantComplete: ((event: AssistantCompleteEvent) => Promise<void> | void) | undefined = undefined;
|
|
282
288
|
|
|
@@ -3,14 +3,25 @@ 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
|
+
import { metrics } from "../core/metrics.ts";
|
|
24
|
+
|
|
14
25
|
export interface DeleteChatsResult {
|
|
15
26
|
attempted: number;
|
|
16
27
|
succeeded: number;
|
|
@@ -109,10 +120,171 @@ export async function deleteChatsForConfiguredAccounts(keepBrowserOpen = false):
|
|
|
109
120
|
});
|
|
110
121
|
}
|
|
111
122
|
}
|
|
112
|
-
|
|
113
123
|
return {
|
|
114
124
|
attempted: accounts.length,
|
|
115
125
|
succeeded,
|
|
116
126
|
mode: "accounts",
|
|
117
127
|
};
|
|
118
128
|
}
|
|
129
|
+
|
|
130
|
+
interface OrphanChatEntry {
|
|
131
|
+
accountId: string;
|
|
132
|
+
chatId: string;
|
|
133
|
+
reason: string;
|
|
134
|
+
enqueuedAt: number;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const orphanChatQueue: OrphanChatEntry[] = [];
|
|
138
|
+
let isProcessingQueue = false;
|
|
139
|
+
|
|
140
|
+
export function getOrphanQueueLengthForTests(): number {
|
|
141
|
+
return orphanChatQueue.length;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Enqueues an abandoned/orphan chat for asynchronous deletion in background.
|
|
146
|
+
* Never blocks the calling request and never deletes a chat actively in use.
|
|
147
|
+
*/
|
|
148
|
+
export function enqueueOrphanChatDeletion(
|
|
149
|
+
accountId: string,
|
|
150
|
+
chatId: string,
|
|
151
|
+
reason = "orphan",
|
|
152
|
+
): void {
|
|
153
|
+
if (!config.qwen.autoCleanOrphanChats || !accountId || !chatId) return;
|
|
154
|
+
if (orphanChatQueue.some((e) => e.accountId === accountId && e.chatId === chatId)) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
orphanChatQueue.push({
|
|
158
|
+
accountId,
|
|
159
|
+
chatId,
|
|
160
|
+
reason,
|
|
161
|
+
enqueuedAt: Date.now(),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (!isProcessingQueue) {
|
|
165
|
+
const timer = setTimeout(() => {
|
|
166
|
+
void processOrphanChatQueue().catch(() => {});
|
|
167
|
+
}, 1_000);
|
|
168
|
+
timer.unref?.();
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function processOrphanChatQueue(): Promise<void> {
|
|
173
|
+
if (isProcessingQueue) return;
|
|
174
|
+
isProcessingQueue = true;
|
|
175
|
+
try {
|
|
176
|
+
while (orphanChatQueue.length > 0) {
|
|
177
|
+
const entry = orphanChatQueue[0];
|
|
178
|
+
|
|
179
|
+
// Never delete a chat that is currently actively bound to an ongoing session
|
|
180
|
+
if (isChatSessionActive(entry.chatId)) {
|
|
181
|
+
orphanChatQueue.shift();
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// If the account is actively serving a stream, postpone deletion
|
|
186
|
+
if (!isAuthMockEnabled() && isAccountBusy(entry.accountId)) {
|
|
187
|
+
await sleep(2_000);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const ok = await deleteSingleQwenChat(entry.accountId, entry.chatId);
|
|
193
|
+
if (ok) {
|
|
194
|
+
metrics.increment("chats.cleaned");
|
|
195
|
+
logger.debug("[ChatCleanup] Deleted orphan chat", {
|
|
196
|
+
accountId: entry.accountId,
|
|
197
|
+
chatId: entry.chatId,
|
|
198
|
+
reason: entry.reason,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
} catch (err) {
|
|
202
|
+
logger.debug("[ChatCleanup] Failed to delete orphan chat", {
|
|
203
|
+
accountId: entry.accountId,
|
|
204
|
+
chatId: entry.chatId,
|
|
205
|
+
error: err instanceof Error ? err.message : String(err),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
orphanChatQueue.shift();
|
|
210
|
+
if (orphanChatQueue.length > 0) {
|
|
211
|
+
await sleep(300);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
} finally {
|
|
215
|
+
isProcessingQueue = false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function drainOrphanChatQueueForTests(): Promise<void> {
|
|
220
|
+
await processOrphanChatQueue();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Scan an account's remote chat list and delete chats older than maxAgeHours,
|
|
225
|
+
* skipping any chat that is currently actively bound to a session.
|
|
226
|
+
*/
|
|
227
|
+
export async function cleanOldChatsForAccount(
|
|
228
|
+
accountId: string,
|
|
229
|
+
maxAgeHours = config.qwen.autoCleanChatMaxAgeHours,
|
|
230
|
+
): Promise<number> {
|
|
231
|
+
try {
|
|
232
|
+
const chats = await fetchRemoteQwenChats(accountId);
|
|
233
|
+
if (!Array.isArray(chats) || chats.length === 0) return 0;
|
|
234
|
+
|
|
235
|
+
const maxAgeMs = maxAgeHours * 60 * 60 * 1000;
|
|
236
|
+
const now = Date.now();
|
|
237
|
+
let cleaned = 0;
|
|
238
|
+
|
|
239
|
+
for (const chat of chats) {
|
|
240
|
+
if (!chat?.id) continue;
|
|
241
|
+
if (isChatSessionActive(chat.id)) continue;
|
|
242
|
+
|
|
243
|
+
const updatedMs =
|
|
244
|
+
typeof chat.updated_at === "number"
|
|
245
|
+
? (chat.updated_at > 1e11 ? chat.updated_at : chat.updated_at * 1000)
|
|
246
|
+
: new Date(chat.updated_at).getTime();
|
|
247
|
+
|
|
248
|
+
if (now - updatedMs >= maxAgeMs) {
|
|
249
|
+
const ok = await deleteSingleQwenChat(accountId, chat.id);
|
|
250
|
+
if (ok) {
|
|
251
|
+
cleaned++;
|
|
252
|
+
metrics.increment("chats.cleaned");
|
|
253
|
+
await sleep(300);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (cleaned > 0) {
|
|
259
|
+
console.log(
|
|
260
|
+
`🧹 [ChatCleanup] Remotely cleaned ${cleaned} old chat(s) (> ${maxAgeHours}h) for account ${accountId}`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
return cleaned;
|
|
264
|
+
} catch (error) {
|
|
265
|
+
logger.debug("[ChatCleanup] cleanOldChatsForAccount error", {
|
|
266
|
+
accountId,
|
|
267
|
+
error: error instanceof Error ? error.message : String(error),
|
|
268
|
+
});
|
|
269
|
+
return 0;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Startup hook: runs 15s after startup in the background, cleaning old chats
|
|
275
|
+
* for active accounts without holding up the server or user requests.
|
|
276
|
+
*/
|
|
277
|
+
export function scheduleStartupChatCleanup(): void {
|
|
278
|
+
if (!config.qwen.autoCleanChatsOnStartup || isAuthMockEnabled()) return;
|
|
279
|
+
|
|
280
|
+
const timer = setTimeout(async () => {
|
|
281
|
+
try {
|
|
282
|
+
const activeIds = getActivePlaywrightAccountIds();
|
|
283
|
+
for (const accountId of activeIds) {
|
|
284
|
+
await cleanOldChatsForAccount(accountId);
|
|
285
|
+
await sleep(1_000);
|
|
286
|
+
}
|
|
287
|
+
} catch {}
|
|
288
|
+
}, 15_000);
|
|
289
|
+
timer.unref?.();
|
|
290
|
+
}
|
|
@@ -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,
|