qwenproxy-cli 1.2.3 → 1.2.5
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 +26 -1
- package/src/routes/anthropic/index.ts +12 -1
- package/src/routes/chat/streaming.ts +13 -3
- package/src/tools/parser.ts +35 -13
- package/src/tui/server-manager.ts +7 -5
- package/src/tui/theme.ts +87 -55
- package/src/tui/views/logs-view.ts +135 -116
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qwenproxy-cli",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.5",
|
|
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",
|
|
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",
|
|
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();
|
|
@@ -60,6 +84,7 @@ export function recordServerLog(level: "INFO" | "WARN" | "ERROR", text: string):
|
|
|
60
84
|
if (logHistory.length > MAX_LOG_HISTORY) {
|
|
61
85
|
logHistory.shift();
|
|
62
86
|
}
|
|
87
|
+
appendServerLogToFile(time, level, line);
|
|
63
88
|
|
|
64
89
|
for (const sub of subscribers) {
|
|
65
90
|
try {
|
|
@@ -261,7 +261,18 @@ app.post("/v1/messages", async (c) => {
|
|
|
261
261
|
`event: message_stop\ndata: ${JSON.stringify({ type: "message_stop" })}\n\n`,
|
|
262
262
|
);
|
|
263
263
|
} catch (error: any) {
|
|
264
|
-
|
|
264
|
+
const isClientAbort =
|
|
265
|
+
c.req.raw.signal.aborted ||
|
|
266
|
+
error?.name === "AbortError" ||
|
|
267
|
+
error?.code === "ECONNRESET" ||
|
|
268
|
+
(typeof error?.message === "string" &&
|
|
269
|
+
(error.message.includes("prematurely closed") ||
|
|
270
|
+
error.message.includes("aborted") ||
|
|
271
|
+
error.message.includes("canceled") ||
|
|
272
|
+
error.message.includes("cancelled")));
|
|
273
|
+
if (!isClientAbort) {
|
|
274
|
+
console.error("❌ [Anthropic] Stream error:", error?.message || error);
|
|
275
|
+
}
|
|
265
276
|
try {
|
|
266
277
|
await write(
|
|
267
278
|
`event: error\ndata: ${JSON.stringify({
|
|
@@ -1485,6 +1485,7 @@ export async function processStreamingResponse(
|
|
|
1485
1485
|
}
|
|
1486
1486
|
|
|
1487
1487
|
const previousUiSessionId = currentUiSessionId;
|
|
1488
|
+
const previousAccountId = currentAccountId;
|
|
1488
1489
|
currentAccountId = newStreamResult.activeAccountId;
|
|
1489
1490
|
currentUiSessionId = newStreamResult.uiSessionId;
|
|
1490
1491
|
retryContext.releaseAccountLease =
|
|
@@ -1517,9 +1518,18 @@ export async function processStreamingResponse(
|
|
|
1517
1518
|
});
|
|
1518
1519
|
}
|
|
1519
1520
|
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1521
|
+
const switched =
|
|
1522
|
+
previousUiSessionId !== currentUiSessionId ||
|
|
1523
|
+
previousAccountId !== currentAccountId;
|
|
1524
|
+
if (switched) {
|
|
1525
|
+
console.log(
|
|
1526
|
+
`🔄 [Chat] Stream recovery switched account | old=${previousUiSessionId.substring(0, 12)} | new=${currentUiSessionId.substring(0, 12)} | account=${currentAccountId}`,
|
|
1527
|
+
);
|
|
1528
|
+
} else {
|
|
1529
|
+
console.log(
|
|
1530
|
+
`🔄 [Chat] Stream recovery resumed | chat=${currentUiSessionId.substring(0, 12)} | account=${currentAccountId}`,
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
1523
1533
|
reader = newStreamResult.stream.getReader();
|
|
1524
1534
|
activeReader = reader;
|
|
1525
1535
|
return true;
|
package/src/tools/parser.ts
CHANGED
|
@@ -2453,18 +2453,24 @@ export class StreamingToolParser {
|
|
|
2453
2453
|
recoveryAttempts,
|
|
2454
2454
|
});
|
|
2455
2455
|
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2456
|
+
if (isToolcallDebugEnabled()) {
|
|
2457
|
+
logger.warn(
|
|
2458
|
+
`[parser] Dropping malformed tool call (${t.length} chars): ${t.substring(0, 80).replace(/\n/g, " ")}...`,
|
|
2459
|
+
{
|
|
2460
|
+
toolName: droppedToolName,
|
|
2461
|
+
category: "malformed",
|
|
2462
|
+
contentLength: t.length,
|
|
2463
|
+
content: t.substring(0, 2000),
|
|
2464
|
+
failureReason: "all recovery stages failed to produce valid JSON",
|
|
2465
|
+
recoveryAttempts,
|
|
2466
|
+
declaredTools: [...this.declaredToolNames].slice(0, 10),
|
|
2467
|
+
},
|
|
2468
|
+
);
|
|
2469
|
+
} else {
|
|
2470
|
+
logger.warn(
|
|
2471
|
+
`[parser] Dropping malformed tool call (${t.length} chars)${droppedToolName ? ` [${droppedToolName}]` : ""}: ${t.substring(0, 80).replace(/\n/g, " ")}...`,
|
|
2472
|
+
);
|
|
2473
|
+
}
|
|
2468
2474
|
if (
|
|
2469
2475
|
this.emittedToolCallCount === 0 &&
|
|
2470
2476
|
this.pendingLeadIn.trim().length > 0
|
|
@@ -3018,8 +3024,24 @@ export class StreamingToolParser {
|
|
|
3018
3024
|
private parseToolCall(parsed: any): ParsedToolCall | null {
|
|
3019
3025
|
if (!parsed || typeof parsed !== "object") return null;
|
|
3020
3026
|
|
|
3021
|
-
|
|
3027
|
+
let name =
|
|
3022
3028
|
parsed.name || parsed.function?.name || parsed.tool_name || parsed.tool;
|
|
3029
|
+
if (!name || typeof name !== "string" || name.length === 0) {
|
|
3030
|
+
const candidateArgs =
|
|
3031
|
+
parsed.arguments ||
|
|
3032
|
+
parsed.function?.arguments ||
|
|
3033
|
+
parsed.args ||
|
|
3034
|
+
parsed.parameters ||
|
|
3035
|
+
parsed.input ||
|
|
3036
|
+
parsed;
|
|
3037
|
+
const parsedCandidateArgs =
|
|
3038
|
+
typeof candidateArgs === "string"
|
|
3039
|
+
? parseJsonishString(candidateArgs) ?? {}
|
|
3040
|
+
: typeof candidateArgs === "object" && candidateArgs !== null
|
|
3041
|
+
? candidateArgs
|
|
3042
|
+
: {};
|
|
3043
|
+
name = inferToolNameFromParameters(parsedCandidateArgs, this.tools);
|
|
3044
|
+
}
|
|
3023
3045
|
if (!name || typeof name !== "string" || name.length === 0) return null;
|
|
3024
3046
|
|
|
3025
3047
|
// Drop hallucinated tool calls where the model split a value vertically
|
|
@@ -6,14 +6,14 @@
|
|
|
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
|
-
|
|
9
|
+
import { recordServerLog } from "../core/server-log-buffer.ts";
|
|
10
10
|
|
|
11
|
+
export type ServerLifecycleState = "offline" | "warming" | "online" | "error";
|
|
11
12
|
export interface ServerLogEntry {
|
|
12
13
|
time: string;
|
|
13
14
|
level: "INFO" | "WARN" | "ERROR";
|
|
14
15
|
message: string;
|
|
15
16
|
}
|
|
16
|
-
|
|
17
17
|
export class ServerManager {
|
|
18
18
|
private static instance: ServerManager | null = null;
|
|
19
19
|
|
|
@@ -127,19 +127,21 @@ export class ServerManager {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
this.logEntries.push({ time, level, message: line });
|
|
130
|
-
if (this.logEntries.length >
|
|
130
|
+
if (this.logEntries.length > 2000) {
|
|
131
131
|
this.logEntries.shift();
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
const levelTag = level === "ERROR" ? "[ERR]" : level === "WARN" ? "[WARN]" : "";
|
|
135
135
|
const formatted = `[${time}] ${levelTag ? levelTag + " " : ""}${line}`;
|
|
136
136
|
this.logBuffer.push(formatted);
|
|
137
|
-
if (this.logBuffer.length >
|
|
137
|
+
if (this.logBuffer.length > 2000) {
|
|
138
138
|
this.logBuffer.shift();
|
|
139
139
|
}
|
|
140
|
+
try {
|
|
141
|
+
recordServerLog(level, line);
|
|
142
|
+
} catch {}
|
|
140
143
|
}
|
|
141
144
|
}
|
|
142
|
-
|
|
143
145
|
public interceptLogs(): void {
|
|
144
146
|
if (this.intercepted) return;
|
|
145
147
|
this.intercepted = true;
|
package/src/tui/theme.ts
CHANGED
|
@@ -191,67 +191,81 @@ export function stripAnsi(str: string): string {
|
|
|
191
191
|
}
|
|
192
192
|
const graphemeSegmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
|
|
193
193
|
|
|
194
|
+
function singleGraphemeWidth(segment: string): number {
|
|
195
|
+
const code = segment.codePointAt(0) || 0;
|
|
196
|
+
// Standalone zero-width characters (variation selectors, zero-width space/joiner)
|
|
197
|
+
if (
|
|
198
|
+
segment === "\u200b" ||
|
|
199
|
+
segment === "\u200c" ||
|
|
200
|
+
segment === "\u200d" ||
|
|
201
|
+
(code >= 0xfe00 && code <= 0xfe0f && segment.length === 1)
|
|
202
|
+
) {
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
// Graphemes containing variation selector 16 (emoji presentation)
|
|
206
|
+
if (segment.includes("\ufe0f")) {
|
|
207
|
+
return 2;
|
|
208
|
+
}
|
|
209
|
+
// Specific BMP emojis and symbols that occupy 2 visual terminal cells (e.g. ⚠️, ⏱, ⚡, ✅, ❌, ✨, ☕, ⚙)
|
|
210
|
+
const isBmpEmoji =
|
|
211
|
+
code === 0x26a0 || // ⚠️ (WARNING SIGN)
|
|
212
|
+
code === 0x23f1 || // ⏱ (STOPWATCH)
|
|
213
|
+
code === 0x23f0 || // ⏰
|
|
214
|
+
code === 0x23f3 || // ⏳
|
|
215
|
+
code === 0x231a || // ⌚
|
|
216
|
+
code === 0x231b || // ⌛
|
|
217
|
+
code === 0x2705 || // ✅
|
|
218
|
+
code === 0x2728 || // ✨
|
|
219
|
+
code === 0x274c || // ❌
|
|
220
|
+
code === 0x274e || // ❎
|
|
221
|
+
code === 0x2753 || // ❓
|
|
222
|
+
code === 0x2757 || // ❗
|
|
223
|
+
code === 0x2b50 || // ⭐
|
|
224
|
+
code === 0x2b55 || // ⭕
|
|
225
|
+
code === 0x26a1 || // ⚡
|
|
226
|
+
code === 0x2615 || // ☕
|
|
227
|
+
code === 0x2699 || // ⚙
|
|
228
|
+
code === 0x2709; // ✉
|
|
229
|
+
|
|
230
|
+
// Common emoji and CJK full-width ranges (SMP Emojis 0x1f300 - 0x1faff)
|
|
231
|
+
if (
|
|
232
|
+
isBmpEmoji ||
|
|
233
|
+
(code >= 0x1100 && code <= 0x115f) ||
|
|
234
|
+
(code >= 0x2e80 && code <= 0xa4cf && code !== 0x303f) ||
|
|
235
|
+
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
236
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
237
|
+
(code >= 0xfe10 && code <= 0xfe19) ||
|
|
238
|
+
(code >= 0xfe30 && code <= 0xfe6f) ||
|
|
239
|
+
(code >= 0xff01 && code <= 0xff60) ||
|
|
240
|
+
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
241
|
+
(code >= 0x1f300 && code <= 0x1faff)
|
|
242
|
+
) {
|
|
243
|
+
return 2;
|
|
244
|
+
}
|
|
245
|
+
return 1;
|
|
246
|
+
}
|
|
247
|
+
|
|
194
248
|
/**
|
|
195
249
|
* Computes visual display width of string, accounting for wide characters,
|
|
196
250
|
* grapheme clusters, variation selectors (VS16), and terminal-wide emojis.
|
|
197
251
|
*/
|
|
198
252
|
export function stringWidth(str: string): number {
|
|
253
|
+
if (!str) return 0;
|
|
254
|
+
// Fast path for pure ASCII strings without ANSI escapes
|
|
255
|
+
let isAscii = true;
|
|
256
|
+
for (let i = 0; i < str.length; i++) {
|
|
257
|
+
const c = str.charCodeAt(i);
|
|
258
|
+
if (c < 32 || c > 126) {
|
|
259
|
+
isAscii = false;
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (isAscii) return str.length;
|
|
264
|
+
|
|
199
265
|
const clean = stripAnsi(str).replace(/\t/g, " ").replace(/\r/g, "");
|
|
200
266
|
let width = 0;
|
|
201
267
|
for (const { segment } of graphemeSegmenter.segment(clean)) {
|
|
202
|
-
|
|
203
|
-
// Standalone zero-width characters (variation selectors, zero-width space/joiner)
|
|
204
|
-
if (
|
|
205
|
-
segment === "\u200b" ||
|
|
206
|
-
segment === "\u200c" ||
|
|
207
|
-
segment === "\u200d" ||
|
|
208
|
-
(code >= 0xfe00 && code <= 0xfe0f && segment.length === 1)
|
|
209
|
-
) {
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
// Graphemes containing variation selector 16 (emoji presentation)
|
|
213
|
-
if (segment.includes("\ufe0f")) {
|
|
214
|
-
width += 2;
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
// Specific BMP emojis and symbols that occupy 2 visual terminal cells (e.g. ⚠️, ⏱, ⚡, ✅, ❌, ✨, ☕, ⚙)
|
|
218
|
-
const isBmpEmoji =
|
|
219
|
-
code === 0x26a0 || // ⚠️ (WARNING SIGN)
|
|
220
|
-
code === 0x23f1 || // ⏱ (STOPWATCH)
|
|
221
|
-
code === 0x23f0 || // ⏰
|
|
222
|
-
code === 0x23f3 || // ⏳
|
|
223
|
-
code === 0x231a || // ⌚
|
|
224
|
-
code === 0x231b || // ⌛
|
|
225
|
-
code === 0x2705 || // ✅
|
|
226
|
-
code === 0x2728 || // ✨
|
|
227
|
-
code === 0x274c || // ❌
|
|
228
|
-
code === 0x274e || // ❎
|
|
229
|
-
code === 0x2753 || // ❓
|
|
230
|
-
code === 0x2757 || // ❗
|
|
231
|
-
code === 0x2b50 || // ⭐
|
|
232
|
-
code === 0x2b55 || // ⭕
|
|
233
|
-
code === 0x26a1 || // ⚡
|
|
234
|
-
code === 0x2615 || // ☕
|
|
235
|
-
code === 0x2699 || // ⚙
|
|
236
|
-
code === 0x2709; // ✉
|
|
237
|
-
|
|
238
|
-
// Common emoji and CJK full-width ranges (SMP Emojis 0x1f300 - 0x1faff)
|
|
239
|
-
if (
|
|
240
|
-
isBmpEmoji ||
|
|
241
|
-
(code >= 0x1100 && code <= 0x115f) ||
|
|
242
|
-
(code >= 0x2e80 && code <= 0xa4cf && code !== 0x303f) ||
|
|
243
|
-
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
244
|
-
(code >= 0xf900 && code <= 0xfaff) ||
|
|
245
|
-
(code >= 0xfe10 && code <= 0xfe19) ||
|
|
246
|
-
(code >= 0xfe30 && code <= 0xfe6f) ||
|
|
247
|
-
(code >= 0xff01 && code <= 0xff60) ||
|
|
248
|
-
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
249
|
-
(code >= 0x1f300 && code <= 0x1faff)
|
|
250
|
-
) {
|
|
251
|
-
width += 2;
|
|
252
|
-
} else {
|
|
253
|
-
width += 1;
|
|
254
|
-
}
|
|
268
|
+
width += singleGraphemeWidth(segment);
|
|
255
269
|
}
|
|
256
270
|
return width;
|
|
257
271
|
}
|
|
@@ -261,8 +275,26 @@ export function stringWidth(str: string): number {
|
|
|
261
275
|
*/
|
|
262
276
|
export function truncate(str: string, maxWidth: number, ellipsis = "…"): string {
|
|
263
277
|
if (maxWidth <= 0) return "";
|
|
278
|
+
if (!str) return "";
|
|
279
|
+
|
|
280
|
+
// Fast path for pure ASCII strings
|
|
281
|
+
let isAscii = true;
|
|
282
|
+
for (let i = 0; i < str.length; i++) {
|
|
283
|
+
const c = str.charCodeAt(i);
|
|
284
|
+
if (c < 32 || c > 126) {
|
|
285
|
+
isAscii = false;
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (isAscii) {
|
|
290
|
+
if (str.length <= maxWidth) return str;
|
|
291
|
+
const targetW = Math.max(0, maxWidth - 1);
|
|
292
|
+
return str.slice(0, targetW) + ellipsis;
|
|
293
|
+
}
|
|
294
|
+
|
|
264
295
|
const clean = stripAnsi(str).replace(/\t/g, " ").replace(/\r/g, "");
|
|
265
|
-
|
|
296
|
+
const cleanW = stringWidth(clean);
|
|
297
|
+
if (cleanW <= maxWidth) return str;
|
|
266
298
|
|
|
267
299
|
const ellipsisW = stringWidth(ellipsis);
|
|
268
300
|
const targetW = Math.max(0, maxWidth - ellipsisW);
|
|
@@ -270,7 +302,7 @@ export function truncate(str: string, maxWidth: number, ellipsis = "…"): strin
|
|
|
270
302
|
let currentW = 0;
|
|
271
303
|
let result = "";
|
|
272
304
|
for (const { segment } of graphemeSegmenter.segment(clean)) {
|
|
273
|
-
const segW =
|
|
305
|
+
const segW = singleGraphemeWidth(segment);
|
|
274
306
|
if (currentW + segW > targetW) break;
|
|
275
307
|
currentW += segW;
|
|
276
308
|
result += segment;
|
|
@@ -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 } from "../../core/paths.ts";
|
|
12
13
|
export class LogsView implements TuiView {
|
|
13
14
|
public readonly id = "logs";
|
|
14
15
|
public readonly title = "Logs";
|
|
@@ -89,58 +90,56 @@ export class LogsView implements TuiView {
|
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
public handleKey(key: KeyEvent): boolean | void {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (key.mouse.row >= 3 && key.mouse.row <= 6) {
|
|
93
|
+
// Mouse hover or click on chips (only compute chips when mouse is on chip rows 3 to 6)
|
|
94
|
+
if ((key.name === "hover" || key.name === "click") && key.mouse) {
|
|
95
|
+
if (key.mouse.row === 3) {
|
|
96
|
+
const rawCount = ServerManager.getInstance().getLogEntries(this.filter).length;
|
|
97
|
+
const { chips } = this.getChips(rawCount);
|
|
98
98
|
const col = key.mouse.col;
|
|
99
|
-
let target: "all" | "warn" | "error" | "copy" | "clear" | null = null;
|
|
100
|
-
for (const c of chips) {
|
|
101
|
-
if (col >= c.startCol && col <= c.endCol) {
|
|
102
|
-
target = c.id;
|
|
103
|
-
break;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
if (this.hoveredChip !== target) {
|
|
107
|
-
this.hoveredChip = target;
|
|
108
|
-
return true;
|
|
109
|
-
}
|
|
110
|
-
} else if (this.hoveredChip !== null) {
|
|
111
|
-
this.hoveredChip = null;
|
|
112
|
-
return true;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
99
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return true;
|
|
100
|
+
if (key.name === "hover") {
|
|
101
|
+
let target: "all" | "warn" | "error" | "copy" | "clear" | null = null;
|
|
102
|
+
for (const c of chips) {
|
|
103
|
+
if (col >= c.startCol && col <= c.endCol) {
|
|
104
|
+
target = c.id;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
124
107
|
}
|
|
125
|
-
if (
|
|
126
|
-
this.
|
|
108
|
+
if (this.hoveredChip !== target) {
|
|
109
|
+
this.hoveredChip = target;
|
|
127
110
|
return true;
|
|
128
111
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
112
|
+
} else if (key.name === "click") {
|
|
113
|
+
for (const c of chips) {
|
|
114
|
+
if (col >= c.startCol && col <= c.endCol) {
|
|
115
|
+
if (c.id === "all") {
|
|
116
|
+
this.setFilter("all");
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
if (c.id === "warn") {
|
|
120
|
+
this.setFilter("warn");
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
if (c.id === "error") {
|
|
124
|
+
this.setFilter("error");
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
if (c.id === "copy") {
|
|
128
|
+
this.copyLogs(false);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
if (c.id === "clear") {
|
|
132
|
+
ServerManager.getInstance().clearLogs();
|
|
133
|
+
this.scrollOffset = 0;
|
|
134
|
+
this.selectedLogIndex = null;
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
142
138
|
}
|
|
143
139
|
}
|
|
140
|
+
} else if (this.hoveredChip !== null) {
|
|
141
|
+
this.hoveredChip = null;
|
|
142
|
+
return true;
|
|
144
143
|
}
|
|
145
144
|
}
|
|
146
145
|
|
|
@@ -149,7 +148,7 @@ export class LogsView implements TuiView {
|
|
|
149
148
|
return (
|
|
150
149
|
col >= this.lastWidth - 3 &&
|
|
151
150
|
col <= this.lastWidth &&
|
|
152
|
-
row >=
|
|
151
|
+
row >= 6 &&
|
|
153
152
|
row <= 6 + this.lastVisibleCapacity
|
|
154
153
|
);
|
|
155
154
|
};
|
|
@@ -167,7 +166,7 @@ export class LogsView implements TuiView {
|
|
|
167
166
|
if (key.name === "click" && key.mouse && isMouseOnScrollbar(key.mouse.col, key.mouse.row)) {
|
|
168
167
|
if (this.lastMaxOffset > 0 && this.lastVisibleCapacity > 0) {
|
|
169
168
|
this.isDraggingScrollbar = true;
|
|
170
|
-
const r = key.mouse.row -
|
|
169
|
+
const r = key.mouse.row - 6;
|
|
171
170
|
const pct = Math.max(0, Math.min(1, r / Math.max(1, this.lastVisibleCapacity - 1)));
|
|
172
171
|
const targetScrollFromTop = Math.round(pct * this.lastMaxOffset);
|
|
173
172
|
this.scrollOffset = Math.max(0, Math.min(this.lastMaxOffset, this.lastMaxOffset - targetScrollFromTop));
|
|
@@ -179,7 +178,7 @@ export class LogsView implements TuiView {
|
|
|
179
178
|
// Scrollbar Drag (Hold and Move)
|
|
180
179
|
if (key.name === "drag" && key.mouse) {
|
|
181
180
|
if (this.isDraggingScrollbar && this.lastMaxOffset > 0 && this.lastVisibleCapacity > 0) {
|
|
182
|
-
const r = key.mouse.row -
|
|
181
|
+
const r = key.mouse.row - 6;
|
|
183
182
|
const pct = Math.max(0, Math.min(1, r / Math.max(1, this.lastVisibleCapacity - 1)));
|
|
184
183
|
const targetScrollFromTop = Math.round(pct * this.lastMaxOffset);
|
|
185
184
|
this.scrollOffset = Math.max(0, Math.min(this.lastMaxOffset, this.lastMaxOffset - targetScrollFromTop));
|
|
@@ -196,9 +195,9 @@ export class LogsView implements TuiView {
|
|
|
196
195
|
}
|
|
197
196
|
}
|
|
198
197
|
|
|
199
|
-
// Mouse click on log rows (terminal row
|
|
200
|
-
if (key.name === "click" && key.mouse && key.mouse.row >=
|
|
201
|
-
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;
|
|
202
201
|
if (rowOffset >= 0 && rowOffset < this.lastVisibleCount) {
|
|
203
202
|
const clickedIdx = this.lastStartIndex + rowOffset;
|
|
204
203
|
if (this.selectedLogIndex === clickedIdx) {
|
|
@@ -232,16 +231,16 @@ export class LogsView implements TuiView {
|
|
|
232
231
|
return true;
|
|
233
232
|
}
|
|
234
233
|
|
|
235
|
-
// Copy logs with 'y' or 'Y'
|
|
234
|
+
// Copy logs with 'y' or 'Y' (always copies full log)
|
|
236
235
|
if ((key.name === "y" || key.name === "Y") && !key.ctrl) {
|
|
237
|
-
this.copyLogs();
|
|
236
|
+
this.copyLogs(false);
|
|
238
237
|
return true;
|
|
239
238
|
}
|
|
240
239
|
|
|
241
|
-
// Enter copies selected
|
|
240
|
+
// Enter copies selected single line if a row is selected
|
|
242
241
|
if (key.name === "return" || key.name === "enter") {
|
|
243
242
|
if (this.selectedLogIndex !== null) {
|
|
244
|
-
this.copyLogs();
|
|
243
|
+
this.copyLogs(true);
|
|
245
244
|
return true;
|
|
246
245
|
}
|
|
247
246
|
}
|
|
@@ -351,39 +350,11 @@ export class LogsView implements TuiView {
|
|
|
351
350
|
? theme.bgHover(" [ C ] Limpar ")
|
|
352
351
|
: theme.muted(" [ C ] Limpar ");
|
|
353
352
|
|
|
354
|
-
const
|
|
355
|
-
|
|
356
|
-
if (rawEntries.length === 0) {
|
|
357
|
-
formattedLines.push("");
|
|
358
|
-
formattedLines.push(
|
|
359
|
-
theme.muted(` Nenhum log registrado para o filtro atual [${this.filter}].`),
|
|
360
|
-
);
|
|
361
|
-
formattedLines.push(
|
|
362
|
-
theme.muted(" Eventos de inicialização, requisições e alertas do proxy aparecerão aqui."),
|
|
363
|
-
);
|
|
364
|
-
} else {
|
|
365
|
-
for (const entry of rawEntries) {
|
|
366
|
-
if (!entry || !entry.message || !entry.message.trim()) continue;
|
|
367
|
-
let tag = theme.dim("[INFO]");
|
|
368
|
-
if (entry.level === "WARN") {
|
|
369
|
-
tag = theme.yellow("[WARN]");
|
|
370
|
-
} else if (entry.level === "ERROR") {
|
|
371
|
-
tag = theme.red("[ERR]");
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
const prefix = `${theme.dim(entry.time)} ${tag} `;
|
|
375
|
-
const prefixW = stringWidth(prefix);
|
|
376
|
-
const maxMsgW = Math.max(20, innerW - prefixW - 4);
|
|
377
|
-
const truncatedMsg = truncate(entry.message, maxMsgW);
|
|
378
|
-
formattedLines.push(`${prefix}${truncatedMsg}`);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
this.lastTotalCount = formattedLines.length;
|
|
353
|
+
const total = rawEntries.length;
|
|
354
|
+
this.lastTotalCount = total;
|
|
383
355
|
|
|
384
356
|
// Scroll Window with top margin breathing room (2 rows reserved at the top)
|
|
385
357
|
const visibleCapacity = Math.max(1, contentH - 4);
|
|
386
|
-
const total = formattedLines.length;
|
|
387
358
|
const maxOffset = Math.max(0, total - visibleCapacity);
|
|
388
359
|
this.lastWidth = width;
|
|
389
360
|
this.lastHeight = height;
|
|
@@ -394,10 +365,10 @@ export class LogsView implements TuiView {
|
|
|
394
365
|
const scrollFromTop = maxOffset - clampedOffset;
|
|
395
366
|
|
|
396
367
|
const startIndex = Math.max(0, total - visibleCapacity - clampedOffset);
|
|
397
|
-
const
|
|
368
|
+
const visibleEntries = total === 0 ? [] : rawEntries.slice(startIndex, startIndex + visibleCapacity);
|
|
398
369
|
|
|
399
370
|
this.lastStartIndex = startIndex;
|
|
400
|
-
this.lastVisibleCount =
|
|
371
|
+
this.lastVisibleCount = visibleEntries.length;
|
|
401
372
|
|
|
402
373
|
// Scrollbar calculation
|
|
403
374
|
const hasScrollbar = total > visibleCapacity;
|
|
@@ -416,33 +387,64 @@ export class LogsView implements TuiView {
|
|
|
416
387
|
const finalRows: string[] = ["", ""];
|
|
417
388
|
const boxInnerW = Math.max(1, width - 2);
|
|
418
389
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
390
|
+
if (total === 0) {
|
|
391
|
+
finalRows.push("");
|
|
392
|
+
finalRows.push(
|
|
393
|
+
theme.muted(` Nenhum log registrado para o filtro atual [${this.filter}].`),
|
|
394
|
+
);
|
|
395
|
+
finalRows.push(
|
|
396
|
+
theme.muted(" Eventos de inicialização, requisições e alertas do proxy aparecerão aqui."),
|
|
397
|
+
);
|
|
398
|
+
while (finalRows.length < visibleCapacity + 2) {
|
|
399
|
+
finalRows.push("");
|
|
400
|
+
}
|
|
401
|
+
} else {
|
|
402
|
+
for (let r = 0; r < visibleCapacity; r++) {
|
|
403
|
+
if (r < visibleEntries.length) {
|
|
404
|
+
const actualIdx = startIndex + r;
|
|
405
|
+
const entry = visibleEntries[r];
|
|
406
|
+
let formattedLine = "";
|
|
407
|
+
if (entry && entry.message && entry.message.trim()) {
|
|
408
|
+
let tag = theme.dim("[INFO]");
|
|
409
|
+
if (entry.level === "WARN") {
|
|
410
|
+
tag = theme.yellow("[WARN]");
|
|
411
|
+
} else if (entry.level === "ERROR") {
|
|
412
|
+
tag = theme.red("[ERR]");
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const prefix = `${theme.dim(entry.time)} ${tag} `;
|
|
416
|
+
const prefixW = 16;
|
|
417
|
+
const maxMsgW = Math.max(20, innerW - prefixW - 4);
|
|
418
|
+
const truncatedMsg = truncate(entry.message, maxMsgW);
|
|
419
|
+
formattedLine = `${prefix}${truncatedMsg}`;
|
|
420
|
+
}
|
|
429
421
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
let scrollChar: string;
|
|
434
|
-
if (isThumb) {
|
|
435
|
-
scrollChar = isHighlighted ? `\x1b[48;2;45;35;85m\x1b[38;2;0;255;255m\x1b[1m█\x1b[0m` : theme.cyan("█");
|
|
422
|
+
let styledText = formattedLine;
|
|
423
|
+
if (this.selectedLogIndex === actualIdx) {
|
|
424
|
+
styledText = theme.bgSelected(`▸ ${stripAnsi(formattedLine)} `);
|
|
436
425
|
} else {
|
|
437
|
-
|
|
426
|
+
styledText = ` ${formattedLine}`;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (hasScrollbar) {
|
|
430
|
+
const isThumb = r >= thumbTop && r < thumbTop + thumbSize;
|
|
431
|
+
const isHighlighted = this.isScrollbarHovered || this.isDraggingScrollbar;
|
|
432
|
+
let scrollChar: string;
|
|
433
|
+
if (isThumb) {
|
|
434
|
+
scrollChar = isHighlighted
|
|
435
|
+
? `\x1b[48;2;45;35;85m\x1b[38;2;0;255;255m\x1b[1m█\x1b[0m`
|
|
436
|
+
: theme.cyan("█");
|
|
437
|
+
} else {
|
|
438
|
+
scrollChar = isHighlighted ? theme.cyan("│") : theme.dark("│");
|
|
439
|
+
}
|
|
440
|
+
const padded = pad(styledText, boxInnerW - 1);
|
|
441
|
+
finalRows.push(`${padded}${scrollChar}`);
|
|
442
|
+
} else {
|
|
443
|
+
finalRows.push(styledText);
|
|
438
444
|
}
|
|
439
|
-
const padded = pad(styledText, boxInnerW - 1);
|
|
440
|
-
finalRows.push(`${padded}${scrollChar}`);
|
|
441
445
|
} else {
|
|
442
|
-
finalRows.push(
|
|
446
|
+
finalRows.push("");
|
|
443
447
|
}
|
|
444
|
-
} else {
|
|
445
|
-
finalRows.push("");
|
|
446
448
|
}
|
|
447
449
|
}
|
|
448
450
|
|
|
@@ -457,22 +459,39 @@ export class LogsView implements TuiView {
|
|
|
457
459
|
});
|
|
458
460
|
return box;
|
|
459
461
|
}
|
|
460
|
-
|
|
462
|
+
|
|
463
|
+
private copyLogs(singleLineOnly = false): void {
|
|
461
464
|
const rawEntries = ServerManager.getInstance()
|
|
462
465
|
.getLogEntries(this.filter)
|
|
463
466
|
.filter((e) => e && e.message && e.message.trim().length > 0);
|
|
464
|
-
if (rawEntries.length === 0) return;
|
|
465
467
|
|
|
466
468
|
let text = "";
|
|
467
|
-
if (this.selectedLogIndex !== null && rawEntries[this.selectedLogIndex]) {
|
|
469
|
+
if (singleLineOnly && this.selectedLogIndex !== null && rawEntries[this.selectedLogIndex]) {
|
|
468
470
|
const entry = rawEntries[this.selectedLogIndex];
|
|
469
471
|
text = `[${entry.time}] [${entry.level}] ${entry.message}`;
|
|
470
472
|
} else {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
473
|
+
// For full copy of "all" filter, prefer the complete persistent file from disk if available
|
|
474
|
+
if (this.filter === "all") {
|
|
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
|
+
}
|
|
474
492
|
}
|
|
475
493
|
|
|
494
|
+
if (!text) return;
|
|
476
495
|
setClipboardText(text);
|
|
477
496
|
this.copyNotification = true;
|
|
478
497
|
if (this.copyTimeout) clearTimeout(this.copyTimeout);
|