qwenproxy-cli 1.2.4 → 1.2.6
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/core/accounts.ts +2 -0
- package/src/core/paths.ts +16 -0
- package/src/core/server-log-buffer.ts +33 -1
- package/src/tui/server-manager.ts +3 -5
- package/src/tui/views/logs-view.ts +37 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qwenproxy-cli",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.6",
|
|
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/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",
|
|
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/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",
|
|
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/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/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",
|
|
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/core/accounts.ts
CHANGED
|
@@ -121,6 +121,8 @@ export function loadAccounts(): QwenAccount[] {
|
|
|
121
121
|
export function invalidateAccountsCache(): void {
|
|
122
122
|
accountsCache = null;
|
|
123
123
|
accountsCacheTime = 0;
|
|
124
|
+
lastSyncedEnv = "";
|
|
125
|
+
lastSyncTime = 0;
|
|
124
126
|
}
|
|
125
127
|
export interface BatchAccountEntry {
|
|
126
128
|
email: string;
|
package/src/core/paths.ts
CHANGED
|
@@ -188,6 +188,20 @@ export function getEnvFilePath(customDataDir?: string): string {
|
|
|
188
188
|
return path.join(customDataDir || getDataDir(), ".env");
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Path to logs directory.
|
|
193
|
+
*/
|
|
194
|
+
export function getLogsDir(customDataDir?: string): string {
|
|
195
|
+
return path.join(customDataDir || getDataDir(), "logs");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Path to persistent server log file.
|
|
200
|
+
*/
|
|
201
|
+
export function getServerLogFilePath(customDataDir?: string): string {
|
|
202
|
+
return path.join(getLogsDir(customDataDir), "server.log");
|
|
203
|
+
}
|
|
204
|
+
|
|
191
205
|
/**
|
|
192
206
|
* Ensures all standard directories exist on disk with proper recursive creation.
|
|
193
207
|
*/
|
|
@@ -195,11 +209,13 @@ export function ensureDataDirs(targetDataDir?: string): void {
|
|
|
195
209
|
const root = targetDataDir || getDataDir();
|
|
196
210
|
const dbDir = getDbDir(root);
|
|
197
211
|
const profilesDir = getProfilesDir(root);
|
|
212
|
+
const logsDir = getLogsDir(root);
|
|
198
213
|
|
|
199
214
|
try {
|
|
200
215
|
if (!fs.existsSync(root)) fs.mkdirSync(root, { recursive: true });
|
|
201
216
|
if (!fs.existsSync(dbDir)) fs.mkdirSync(dbDir, { recursive: true });
|
|
202
217
|
if (!fs.existsSync(profilesDir)) fs.mkdirSync(profilesDir, { recursive: true });
|
|
218
|
+
if (!fs.existsSync(logsDir)) fs.mkdirSync(logsDir, { recursive: true });
|
|
203
219
|
} catch (err: any) {
|
|
204
220
|
console.error(`[Paths] Error ensuring data directories at ${root}:`, err?.message || String(err));
|
|
205
221
|
}
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* Captures server logs and streams them to connected TUI instances.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import fs from "node:fs";
|
|
6
7
|
import { stripAnsi } from "../tui/theme.ts";
|
|
8
|
+
import { getServerLogFilePath, ensureDataDirs, isRunningUnderNodeTest } from "./paths.ts";
|
|
7
9
|
|
|
8
10
|
export interface ServerLogMessage {
|
|
9
11
|
time: string;
|
|
@@ -11,11 +13,33 @@ export interface ServerLogMessage {
|
|
|
11
13
|
message: string;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
const MAX_LOG_HISTORY =
|
|
16
|
+
const MAX_LOG_HISTORY = 2000;
|
|
15
17
|
const logHistory: ServerLogMessage[] = [];
|
|
16
18
|
const subscribers = new Set<(entry: ServerLogMessage) => void>();
|
|
17
19
|
let consoleHooked = false;
|
|
18
20
|
|
|
21
|
+
function appendServerLogToFile(time: string, level: string, message: string): void {
|
|
22
|
+
if (isRunningUnderNodeTest()) return;
|
|
23
|
+
try {
|
|
24
|
+
const filePath = getServerLogFilePath();
|
|
25
|
+
if (fs.existsSync(filePath)) {
|
|
26
|
+
const stats = fs.statSync(filePath);
|
|
27
|
+
if (stats.size > 20 * 1024 * 1024) { // Rotate at 20MB
|
|
28
|
+
const oldPath = filePath + ".old";
|
|
29
|
+
try {
|
|
30
|
+
if (fs.existsSync(oldPath)) fs.unlinkSync(oldPath);
|
|
31
|
+
fs.renameSync(filePath, oldPath);
|
|
32
|
+
} catch {}
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
ensureDataDirs();
|
|
36
|
+
}
|
|
37
|
+
const dateStr = new Date().toLocaleDateString("pt-BR");
|
|
38
|
+
const line = `[${dateStr} ${time}] [${level}] ${message}\n`;
|
|
39
|
+
fs.appendFileSync(filePath, line, "utf-8");
|
|
40
|
+
} catch {}
|
|
41
|
+
}
|
|
42
|
+
|
|
19
43
|
export function recordServerLog(level: "INFO" | "WARN" | "ERROR", text: string): void {
|
|
20
44
|
if (!text) return;
|
|
21
45
|
const clean = stripAnsi(text).trim();
|
|
@@ -55,11 +79,19 @@ export function recordServerLog(level: "INFO" | "WARN" | "ERROR", text: string):
|
|
|
55
79
|
.replace(/^(?:\[?(?:INFO|WARN|WARNING|ERROR|ERR|DEBUG)\]?\s+)+/i, "")
|
|
56
80
|
.replace(/([\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{2300}-\u{23FF}]\uFE0F?)\s{2,}/gu, "$1 ");
|
|
57
81
|
if (!line) continue;
|
|
82
|
+
|
|
83
|
+
// Prevent identical consecutive duplicate logs in the same second
|
|
84
|
+
const last = logHistory[logHistory.length - 1];
|
|
85
|
+
if (last && last.time === time && last.level === level && last.message === line) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
58
89
|
const entry: ServerLogMessage = { time, level, message: line };
|
|
59
90
|
logHistory.push(entry);
|
|
60
91
|
if (logHistory.length > MAX_LOG_HISTORY) {
|
|
61
92
|
logHistory.shift();
|
|
62
93
|
}
|
|
94
|
+
appendServerLogToFile(time, level, line);
|
|
63
95
|
|
|
64
96
|
for (const sub of subscribers) {
|
|
65
97
|
try {
|
|
@@ -6,14 +6,13 @@
|
|
|
6
6
|
import { config } from "../core/config.ts";
|
|
7
7
|
import { startServer, stopServer } from "../api/server.ts";
|
|
8
8
|
import { stripAnsi } from "./theme.ts";
|
|
9
|
-
export type ServerLifecycleState = "offline" | "warming" | "online" | "error";
|
|
10
9
|
|
|
10
|
+
export type ServerLifecycleState = "offline" | "warming" | "online" | "error";
|
|
11
11
|
export interface ServerLogEntry {
|
|
12
12
|
time: string;
|
|
13
13
|
level: "INFO" | "WARN" | "ERROR";
|
|
14
14
|
message: string;
|
|
15
15
|
}
|
|
16
|
-
|
|
17
16
|
export class ServerManager {
|
|
18
17
|
private static instance: ServerManager | null = null;
|
|
19
18
|
|
|
@@ -127,19 +126,18 @@ export class ServerManager {
|
|
|
127
126
|
}
|
|
128
127
|
|
|
129
128
|
this.logEntries.push({ time, level, message: line });
|
|
130
|
-
if (this.logEntries.length >
|
|
129
|
+
if (this.logEntries.length > 2000) {
|
|
131
130
|
this.logEntries.shift();
|
|
132
131
|
}
|
|
133
132
|
|
|
134
133
|
const levelTag = level === "ERROR" ? "[ERR]" : level === "WARN" ? "[WARN]" : "";
|
|
135
134
|
const formatted = `[${time}] ${levelTag ? levelTag + " " : ""}${line}`;
|
|
136
135
|
this.logBuffer.push(formatted);
|
|
137
|
-
if (this.logBuffer.length >
|
|
136
|
+
if (this.logBuffer.length > 2000) {
|
|
138
137
|
this.logBuffer.shift();
|
|
139
138
|
}
|
|
140
139
|
}
|
|
141
140
|
}
|
|
142
|
-
|
|
143
141
|
public interceptLogs(): void {
|
|
144
142
|
if (this.intercepted) return;
|
|
145
143
|
this.intercepted = true;
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
* Real-time event log viewer with level filtering (All, Warnings, Errors).
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import fs from "node:fs";
|
|
6
7
|
import type { TuiView } from "../types.ts";
|
|
7
8
|
import type { KeyEvent } from "../screen.ts";
|
|
8
9
|
import { theme, drawBox, stringWidth, truncate, pad, stripAnsi, setClipboardText } from "../theme.ts";
|
|
9
10
|
import { ServerManager } from "../server-manager.ts";
|
|
10
11
|
import { loadTuiSettings, saveTuiSettings } from "../settings.ts";
|
|
11
|
-
|
|
12
|
+
import { getServerLogFilePath, isRunningUnderNodeTest } from "../../core/paths.ts";
|
|
12
13
|
export class LogsView implements TuiView {
|
|
13
14
|
public readonly id = "logs";
|
|
14
15
|
public readonly title = "Logs";
|
|
@@ -91,7 +92,7 @@ export class LogsView implements TuiView {
|
|
|
91
92
|
public handleKey(key: KeyEvent): boolean | void {
|
|
92
93
|
// Mouse hover or click on chips (only compute chips when mouse is on chip rows 3 to 6)
|
|
93
94
|
if ((key.name === "hover" || key.name === "click") && key.mouse) {
|
|
94
|
-
if (key.mouse.row
|
|
95
|
+
if (key.mouse.row === 3) {
|
|
95
96
|
const rawCount = ServerManager.getInstance().getLogEntries(this.filter).length;
|
|
96
97
|
const { chips } = this.getChips(rawCount);
|
|
97
98
|
const col = key.mouse.col;
|
|
@@ -124,7 +125,7 @@ export class LogsView implements TuiView {
|
|
|
124
125
|
return true;
|
|
125
126
|
}
|
|
126
127
|
if (c.id === "copy") {
|
|
127
|
-
this.copyLogs();
|
|
128
|
+
this.copyLogs(false);
|
|
128
129
|
return true;
|
|
129
130
|
}
|
|
130
131
|
if (c.id === "clear") {
|
|
@@ -147,7 +148,7 @@ export class LogsView implements TuiView {
|
|
|
147
148
|
return (
|
|
148
149
|
col >= this.lastWidth - 3 &&
|
|
149
150
|
col <= this.lastWidth &&
|
|
150
|
-
row >=
|
|
151
|
+
row >= 6 &&
|
|
151
152
|
row <= 6 + this.lastVisibleCapacity
|
|
152
153
|
);
|
|
153
154
|
};
|
|
@@ -165,7 +166,7 @@ export class LogsView implements TuiView {
|
|
|
165
166
|
if (key.name === "click" && key.mouse && isMouseOnScrollbar(key.mouse.col, key.mouse.row)) {
|
|
166
167
|
if (this.lastMaxOffset > 0 && this.lastVisibleCapacity > 0) {
|
|
167
168
|
this.isDraggingScrollbar = true;
|
|
168
|
-
const r = key.mouse.row -
|
|
169
|
+
const r = key.mouse.row - 6;
|
|
169
170
|
const pct = Math.max(0, Math.min(1, r / Math.max(1, this.lastVisibleCapacity - 1)));
|
|
170
171
|
const targetScrollFromTop = Math.round(pct * this.lastMaxOffset);
|
|
171
172
|
this.scrollOffset = Math.max(0, Math.min(this.lastMaxOffset, this.lastMaxOffset - targetScrollFromTop));
|
|
@@ -177,7 +178,7 @@ export class LogsView implements TuiView {
|
|
|
177
178
|
// Scrollbar Drag (Hold and Move)
|
|
178
179
|
if (key.name === "drag" && key.mouse) {
|
|
179
180
|
if (this.isDraggingScrollbar && this.lastMaxOffset > 0 && this.lastVisibleCapacity > 0) {
|
|
180
|
-
const r = key.mouse.row -
|
|
181
|
+
const r = key.mouse.row - 6;
|
|
181
182
|
const pct = Math.max(0, Math.min(1, r / Math.max(1, this.lastVisibleCapacity - 1)));
|
|
182
183
|
const targetScrollFromTop = Math.round(pct * this.lastMaxOffset);
|
|
183
184
|
this.scrollOffset = Math.max(0, Math.min(this.lastMaxOffset, this.lastMaxOffset - targetScrollFromTop));
|
|
@@ -194,9 +195,9 @@ export class LogsView implements TuiView {
|
|
|
194
195
|
}
|
|
195
196
|
}
|
|
196
197
|
|
|
197
|
-
// Mouse click on log rows (terminal row
|
|
198
|
-
if (key.name === "click" && key.mouse && key.mouse.row >=
|
|
199
|
-
const rowOffset = key.mouse.row -
|
|
198
|
+
// Mouse click on log rows (terminal row 6+, accounting for 2-line top margin: row 4 & 5 are margin)
|
|
199
|
+
if (key.name === "click" && key.mouse && key.mouse.row >= 6) {
|
|
200
|
+
const rowOffset = key.mouse.row - 6;
|
|
200
201
|
if (rowOffset >= 0 && rowOffset < this.lastVisibleCount) {
|
|
201
202
|
const clickedIdx = this.lastStartIndex + rowOffset;
|
|
202
203
|
if (this.selectedLogIndex === clickedIdx) {
|
|
@@ -230,16 +231,16 @@ export class LogsView implements TuiView {
|
|
|
230
231
|
return true;
|
|
231
232
|
}
|
|
232
233
|
|
|
233
|
-
// Copy logs with 'y' or 'Y'
|
|
234
|
+
// Copy logs with 'y' or 'Y' (always copies full log)
|
|
234
235
|
if ((key.name === "y" || key.name === "Y") && !key.ctrl) {
|
|
235
|
-
this.copyLogs();
|
|
236
|
+
this.copyLogs(false);
|
|
236
237
|
return true;
|
|
237
238
|
}
|
|
238
239
|
|
|
239
|
-
// Enter copies selected
|
|
240
|
+
// Enter copies selected single line if a row is selected
|
|
240
241
|
if (key.name === "return" || key.name === "enter") {
|
|
241
242
|
if (this.selectedLogIndex !== null) {
|
|
242
|
-
this.copyLogs();
|
|
243
|
+
this.copyLogs(true);
|
|
243
244
|
return true;
|
|
244
245
|
}
|
|
245
246
|
}
|
|
@@ -458,22 +459,39 @@ export class LogsView implements TuiView {
|
|
|
458
459
|
});
|
|
459
460
|
return box;
|
|
460
461
|
}
|
|
461
|
-
|
|
462
|
+
|
|
463
|
+
private copyLogs(singleLineOnly = false): void {
|
|
462
464
|
const rawEntries = ServerManager.getInstance()
|
|
463
465
|
.getLogEntries(this.filter)
|
|
464
466
|
.filter((e) => e && e.message && e.message.trim().length > 0);
|
|
465
|
-
if (rawEntries.length === 0) return;
|
|
466
467
|
|
|
467
468
|
let text = "";
|
|
468
|
-
if (this.selectedLogIndex !== null && rawEntries[this.selectedLogIndex]) {
|
|
469
|
+
if (singleLineOnly && this.selectedLogIndex !== null && rawEntries[this.selectedLogIndex]) {
|
|
469
470
|
const entry = rawEntries[this.selectedLogIndex];
|
|
470
471
|
text = `[${entry.time}] [${entry.level}] ${entry.message}`;
|
|
471
472
|
} else {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
473
|
+
// For full copy of "all" filter, prefer the complete persistent file from disk if available
|
|
474
|
+
if (this.filter === "all" && !isRunningUnderNodeTest()) {
|
|
475
|
+
try {
|
|
476
|
+
const logPath = getServerLogFilePath();
|
|
477
|
+
if (fs.existsSync(logPath)) {
|
|
478
|
+
const diskContent = fs.readFileSync(logPath, "utf-8");
|
|
479
|
+
if (diskContent && diskContent.trim().length > 0) {
|
|
480
|
+
text = diskContent.trim();
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
} catch {}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Fallback to memory buffer if disk log is empty or on filtered view
|
|
487
|
+
if (!text && rawEntries.length > 0) {
|
|
488
|
+
text = rawEntries
|
|
489
|
+
.map((entry) => `[${entry.time}] [${entry.level}] ${entry.message}`)
|
|
490
|
+
.join("\n");
|
|
491
|
+
}
|
|
475
492
|
}
|
|
476
493
|
|
|
494
|
+
if (!text) return;
|
|
477
495
|
setClipboardText(text);
|
|
478
496
|
this.copyNotification = true;
|
|
479
497
|
if (this.copyTimeout) clearTimeout(this.copyTimeout);
|