qwenproxy-cli 1.0.26 → 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 +82 -4
- 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/index.ts +6 -0
- package/src/services/chat-cleanup.ts +3 -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/chat-cleanup-auto.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/personalization-deadline.test.ts",
|
|
28
|
-
"test:mock:fast": "tsx --test --test-concurrency=2 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chat-cleanup-auto.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/personalization-deadline.test.ts",
|
|
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();
|
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
|
+
}
|
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
|
|
|
@@ -20,6 +20,7 @@ import { config } from "../core/config.ts";
|
|
|
20
20
|
import { isAccountBusy } from "../core/account-concurrency.ts";
|
|
21
21
|
import { isChatSessionActive } from "./qwen-thread-state.ts";
|
|
22
22
|
import { sleep } from "./human-behavior.ts";
|
|
23
|
+
import { metrics } from "../core/metrics.ts";
|
|
23
24
|
|
|
24
25
|
export interface DeleteChatsResult {
|
|
25
26
|
attempted: number;
|
|
@@ -190,6 +191,7 @@ async function processOrphanChatQueue(): Promise<void> {
|
|
|
190
191
|
try {
|
|
191
192
|
const ok = await deleteSingleQwenChat(entry.accountId, entry.chatId);
|
|
192
193
|
if (ok) {
|
|
194
|
+
metrics.increment("chats.cleaned");
|
|
193
195
|
logger.debug("[ChatCleanup] Deleted orphan chat", {
|
|
194
196
|
accountId: entry.accountId,
|
|
195
197
|
chatId: entry.chatId,
|
|
@@ -247,6 +249,7 @@ export async function cleanOldChatsForAccount(
|
|
|
247
249
|
const ok = await deleteSingleQwenChat(accountId, chat.id);
|
|
248
250
|
if (ok) {
|
|
249
251
|
cleaned++;
|
|
252
|
+
metrics.increment("chats.cleaned");
|
|
250
253
|
await sleep(300);
|
|
251
254
|
}
|
|
252
255
|
}
|
package/src/sync/index.ts
CHANGED
|
@@ -115,8 +115,12 @@ export function inspectClientSyncStatus(
|
|
|
115
115
|
const data = JSON.parse(raw);
|
|
116
116
|
const url = data?.env?.ANTHROPIC_BASE_URL || "";
|
|
117
117
|
const model = data?.env?.ANTHROPIC_MODEL || data?.model || "";
|
|
118
|
+
const isLocalHost =
|
|
119
|
+
url.includes(String(port)) ||
|
|
120
|
+
url.includes(`127.0.0.1:${port}`) ||
|
|
121
|
+
url.includes(`localhost:${port}`);
|
|
118
122
|
const isSynced =
|
|
119
|
-
|
|
123
|
+
isLocalHost &&
|
|
120
124
|
(model.toLowerCase().includes("qwen") || Boolean(data?.env?.ANTHROPIC_AUTH_TOKEN));
|
|
121
125
|
return {
|
|
122
126
|
id,
|
|
@@ -132,34 +136,66 @@ export function inspectClientSyncStatus(
|
|
|
132
136
|
const isProviderActive = /^model_provider\s*=\s*["']qwenproxy["']/m.test(raw);
|
|
133
137
|
const modelMatch = raw.match(/^model\s*=\s*["']([^"']+)["']/m);
|
|
134
138
|
const model = modelMatch ? modelMatch[1] : undefined;
|
|
139
|
+
const urlMatch = raw.match(/\[model_providers\.qwenproxy\][\s\S]*?base_url\s*=\s*["']([^"']+)["']/);
|
|
140
|
+
const url = urlMatch ? urlMatch[1] : "";
|
|
141
|
+
const isLocalHost = Boolean(
|
|
142
|
+
url && (url.includes(String(port)) || url.includes(`127.0.0.1:${port}`) || url.includes(`localhost:${port}`)),
|
|
143
|
+
);
|
|
135
144
|
return {
|
|
136
145
|
id,
|
|
137
146
|
installed: true,
|
|
138
|
-
synced: hasProvider && isProviderActive,
|
|
147
|
+
synced: hasProvider && isProviderActive && isLocalHost,
|
|
139
148
|
model,
|
|
140
149
|
};
|
|
141
150
|
}
|
|
142
151
|
|
|
143
152
|
if (id === "opencode") {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
153
|
+
let isSynced = false;
|
|
154
|
+
try {
|
|
155
|
+
const data = JSON.parse(raw);
|
|
156
|
+
const provider = data?.provider?.qwenproxy;
|
|
157
|
+
const url = provider?.options?.baseURL || "";
|
|
158
|
+
const isLocalHost =
|
|
159
|
+
url.includes(String(port)) ||
|
|
160
|
+
url.includes(`127.0.0.1:${port}`) ||
|
|
161
|
+
url.includes(`localhost:${port}`);
|
|
162
|
+
isSynced = Boolean(provider && isLocalHost);
|
|
163
|
+
} catch {
|
|
164
|
+
const qwenBlockMatch = raw.match(/"qwenproxy"\s*:\s*\{[\s\S]*?"baseURL"\s*:\s*"([^"]+)"/);
|
|
165
|
+
const url = qwenBlockMatch ? qwenBlockMatch[1] : "";
|
|
166
|
+
isSynced = Boolean(
|
|
167
|
+
url &&
|
|
168
|
+
(url.includes(String(port)) ||
|
|
169
|
+
url.includes(`127.0.0.1:${port}`) ||
|
|
170
|
+
url.includes(`localhost:${port}`)),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
147
173
|
return {
|
|
148
174
|
id,
|
|
149
175
|
installed: true,
|
|
150
|
-
synced:
|
|
176
|
+
synced: isSynced,
|
|
151
177
|
};
|
|
152
178
|
}
|
|
153
179
|
|
|
154
180
|
if (id === "omp") {
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
181
|
+
const ompMatch = raw.match(/^[ \t]*qwenproxy:\s*\r?\n((?:[ \t]{4,}.*\r?\n?)*)/m);
|
|
182
|
+
let isSynced = false;
|
|
183
|
+
let url: string | undefined;
|
|
184
|
+
if (ompMatch) {
|
|
185
|
+
const urlMatch = ompMatch[1].match(/baseUrl:\s*(\S+)/);
|
|
186
|
+
url = urlMatch ? urlMatch[1].replace(/['"]/g, "") : undefined;
|
|
187
|
+
isSynced = Boolean(
|
|
188
|
+
url &&
|
|
189
|
+
(url.includes(String(port)) ||
|
|
190
|
+
url.includes(`127.0.0.1:${port}`) ||
|
|
191
|
+
url.includes(`localhost:${port}`)),
|
|
192
|
+
);
|
|
193
|
+
}
|
|
159
194
|
return {
|
|
160
195
|
id,
|
|
161
196
|
installed: true,
|
|
162
|
-
synced:
|
|
197
|
+
synced: isSynced,
|
|
198
|
+
url,
|
|
163
199
|
};
|
|
164
200
|
}
|
|
165
201
|
} catch {
|
package/src/tools/parser.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import { robustParseJSON, computeMissingJsonClosingTokens } from "../utils/json.ts";
|
|
3
3
|
import { logger, isToolcallDebugEnabled } from "../core/logger.js";
|
|
4
|
+
import { metrics } from "../core/metrics.ts";
|
|
4
5
|
import type { ParsedToolCall } from "./types";
|
|
5
6
|
import type { FunctionToolDefinition } from "./types";
|
|
6
7
|
import {
|
|
@@ -1501,7 +1502,7 @@ export class StreamingToolParser {
|
|
|
1501
1502
|
}
|
|
1502
1503
|
|
|
1503
1504
|
this.emittedCallKeys.add(key);
|
|
1504
|
-
|
|
1505
|
+
metrics.increment("toolcalls.total");
|
|
1505
1506
|
const incremental = this.activeIncrementalToolCall;
|
|
1506
1507
|
const matchesIncrementalCall =
|
|
1507
1508
|
incremental?.name === tc.name && incremental.startEmitted;
|
|
@@ -1725,6 +1726,7 @@ export class StreamingToolParser {
|
|
|
1725
1726
|
failureReason: options.failureReason,
|
|
1726
1727
|
recoveryAttempts: options.recoveryAttempts,
|
|
1727
1728
|
});
|
|
1729
|
+
metrics.increment("toolcalls.malformed");
|
|
1728
1730
|
}
|
|
1729
1731
|
|
|
1730
1732
|
private extractUndeclaredNamesFromContent(text: string): string[] {
|
|
@@ -1989,6 +1991,7 @@ export class StreamingToolParser {
|
|
|
1989
1991
|
}
|
|
1990
1992
|
}
|
|
1991
1993
|
if (recovered) {
|
|
1994
|
+
metrics.increment("toolcalls.recovered");
|
|
1992
1995
|
if (isToolcallDebugEnabled()) {
|
|
1993
1996
|
logger.debug("[parser] flush: recovery successful", {
|
|
1994
1997
|
name: recovered.name,
|
package/src/tui/app.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { SyncView } from "./views/sync-view.ts";
|
|
|
26
26
|
import { StorageView } from "./views/storage-view.ts";
|
|
27
27
|
import { AccountsView } from "./views/accounts-view.ts";
|
|
28
28
|
import { LogsView } from "./views/logs-view.ts";
|
|
29
|
+
import { loadTuiSettings, saveTuiSettings } from "./settings.ts";
|
|
29
30
|
export class TuiApp {
|
|
30
31
|
private screen: Screen;
|
|
31
32
|
private views: TuiView[] = [];
|
|
@@ -36,7 +37,7 @@ export class TuiApp {
|
|
|
36
37
|
private pollInterval: NodeJS.Timeout | null = null;
|
|
37
38
|
private statusSnapshot: ProxyStatusSnapshot | null = null;
|
|
38
39
|
private renderScheduled = false;
|
|
39
|
-
constructor(initialTab
|
|
40
|
+
constructor(initialTab?: number) {
|
|
40
41
|
this.screen = new Screen();
|
|
41
42
|
|
|
42
43
|
this.views = [
|
|
@@ -47,10 +48,33 @@ export class TuiApp {
|
|
|
47
48
|
new AccountsView(),
|
|
48
49
|
new LogsView(),
|
|
49
50
|
];
|
|
50
|
-
|
|
51
|
+
|
|
52
|
+
let resolvedTab = initialTab;
|
|
53
|
+
if (!resolvedTab || isNaN(resolvedTab)) {
|
|
54
|
+
const saved = loadTuiSettings();
|
|
55
|
+
if (saved.lastTab && saved.lastTab >= 1 && saved.lastTab <= 6) {
|
|
56
|
+
resolvedTab = saved.lastTab;
|
|
57
|
+
} else {
|
|
58
|
+
resolvedTab = 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const tabIdx = Math.max(0, Math.min(this.views.length - 1, resolvedTab - 1));
|
|
51
63
|
this.activeViewIndex = tabIdx;
|
|
52
64
|
}
|
|
53
65
|
|
|
66
|
+
private switchTab(newIdx: number): void {
|
|
67
|
+
if (newIdx === this.activeViewIndex || !this.views[newIdx]) return;
|
|
68
|
+
const activeView = this.views[this.activeViewIndex];
|
|
69
|
+
if (activeView.onDeactivate) activeView.onDeactivate();
|
|
70
|
+
this.activeViewIndex = newIdx;
|
|
71
|
+
this.hoveredTabIndex = null;
|
|
72
|
+
const nextView = this.views[this.activeViewIndex];
|
|
73
|
+
if (nextView.onActivate) nextView.onActivate();
|
|
74
|
+
saveTuiSettings({ lastTab: newIdx + 1 });
|
|
75
|
+
this.requestRender();
|
|
76
|
+
}
|
|
77
|
+
|
|
54
78
|
private getTabAtCol(col: number): number | null {
|
|
55
79
|
let curCol = 3;
|
|
56
80
|
for (let i = 0; i < this.views.length; i++) {
|
|
@@ -149,12 +173,7 @@ export class TuiApp {
|
|
|
149
173
|
}
|
|
150
174
|
if (key.name === "click") {
|
|
151
175
|
if (tabIdx !== null && tabIdx !== this.activeViewIndex && this.views[tabIdx]) {
|
|
152
|
-
|
|
153
|
-
this.activeViewIndex = tabIdx;
|
|
154
|
-
this.hoveredTabIndex = null;
|
|
155
|
-
const nextView = this.views[this.activeViewIndex];
|
|
156
|
-
if (nextView.onActivate) nextView.onActivate();
|
|
157
|
-
this.requestRender();
|
|
176
|
+
this.switchTab(tabIdx);
|
|
158
177
|
return;
|
|
159
178
|
}
|
|
160
179
|
}
|
|
@@ -189,12 +208,7 @@ export class TuiApp {
|
|
|
189
208
|
// Tab for cycling tabs across views (including from Chat view when no modal is open!)
|
|
190
209
|
if (key.name === "tab" && !isModalOpen) {
|
|
191
210
|
const newIdx = (this.activeViewIndex + 1) % this.views.length;
|
|
192
|
-
|
|
193
|
-
this.activeViewIndex = newIdx;
|
|
194
|
-
this.hoveredTabIndex = null;
|
|
195
|
-
const nextView = this.views[this.activeViewIndex];
|
|
196
|
-
if (nextView.onActivate) nextView.onActivate();
|
|
197
|
-
this.requestRender();
|
|
211
|
+
this.switchTab(newIdx);
|
|
198
212
|
return;
|
|
199
213
|
}
|
|
200
214
|
// Direct tab switching with numbers 1..6 only when NOT typing text in an input
|
|
@@ -207,12 +221,7 @@ export class TuiApp {
|
|
|
207
221
|
if (!key.ctrl && !key.meta && ["1", "2", "3", "4", "5", "6"].includes(key.name)) {
|
|
208
222
|
const newIdx = parseInt(key.name, 10) - 1;
|
|
209
223
|
if (newIdx !== this.activeViewIndex && this.views[newIdx]) {
|
|
210
|
-
|
|
211
|
-
this.activeViewIndex = newIdx;
|
|
212
|
-
this.hoveredTabIndex = null;
|
|
213
|
-
const nextView = this.views[this.activeViewIndex];
|
|
214
|
-
if (nextView.onActivate) nextView.onActivate();
|
|
215
|
-
this.requestRender();
|
|
224
|
+
this.switchTab(newIdx);
|
|
216
225
|
return;
|
|
217
226
|
}
|
|
218
227
|
}
|
package/src/tui/index.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { TuiApp } from "./app.ts";
|
|
7
7
|
|
|
8
|
-
function parseInitialTab(): number {
|
|
8
|
+
function parseInitialTab(): number | undefined {
|
|
9
9
|
const args = process.argv.slice(2);
|
|
10
10
|
const tabArgIdx = args.findIndex((a) => a === "--tab" || a === "-t");
|
|
11
11
|
if (tabArgIdx !== -1 && args[tabArgIdx + 1]) {
|
|
@@ -21,7 +21,7 @@ function parseInitialTab(): number {
|
|
|
21
21
|
return parseInt(firstNumeric, 10);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
return
|
|
24
|
+
return undefined;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
async function main() {
|