qwenproxy-cli 1.2.5 → 1.2.7
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 +1 -1
- package/src/core/config.ts +2 -2
- package/src/core/server-log-buffer.ts +7 -0
- package/src/routes/chat/account.ts +31 -33
- package/src/routes/chat/retry-policy.ts +23 -0
- package/src/services/chat-cleanup.ts +10 -3
- package/src/services/playwright.ts +3 -1
- package/src/sync/claude-code.ts +3 -3
- package/src/sync/codex.ts +3 -3
- package/src/sync/index.ts +18 -0
- package/src/sync/kilo.ts +8 -13
- package/src/sync/omp.ts +8 -10
- package/src/sync/openclaw.ts +17 -27
- package/src/sync/opencode.ts +9 -13
- package/src/sync/types.ts +4 -0
- package/src/sync/utils.ts +18 -0
- package/src/sync/zed.ts +45 -53
- package/src/sync-clients.ts +5 -0
- package/src/tui/server-manager.ts +0 -4
- package/src/tui/views/logs-view.ts +12 -12
- package/src/tui/views/sync-view.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qwenproxy-cli",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.7",
|
|
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": {
|
package/src/core/config.ts
CHANGED
|
@@ -174,7 +174,7 @@ const envSchema = z
|
|
|
174
174
|
DELETE_ALL_CHATS_ON_SHUTDOWN: z.string().default("false"),
|
|
175
175
|
AUTO_CLEAN_CHATS_ON_STARTUP: z.string().default("true"),
|
|
176
176
|
AUTO_CLEAN_ORPHAN_CHATS: z.string().default("true"),
|
|
177
|
-
AUTO_CLEAN_CHAT_MAX_AGE_HOURS: z.string().default("
|
|
177
|
+
AUTO_CLEAN_CHAT_MAX_AGE_HOURS: z.string().default("168"),
|
|
178
178
|
// The Baxia WAF scores live page behavior (pointer/scroll events, open
|
|
179
179
|
// session) — an account whose page sits frozen for minutes returns a low
|
|
180
180
|
// trust score and gets TMD-challenged on the next request. On by default;
|
|
@@ -384,7 +384,7 @@ export const config = {
|
|
|
384
384
|
autoCleanOrphanChats: env.AUTO_CLEAN_ORPHAN_CHATS !== "false",
|
|
385
385
|
autoCleanChatMaxAgeHours: Math.max(
|
|
386
386
|
1,
|
|
387
|
-
parseInt(env.AUTO_CLEAN_CHAT_MAX_AGE_HOURS) ||
|
|
387
|
+
parseInt(env.AUTO_CLEAN_CHAT_MAX_AGE_HOURS) || 168,
|
|
388
388
|
),
|
|
389
389
|
sendBxUa: env.QWEN_SEND_BX_UA === "true",
|
|
390
390
|
/** Deployed web bundle version sent as the `version` API header. */
|
|
@@ -79,6 +79,13 @@ export function recordServerLog(level: "INFO" | "WARN" | "ERROR", text: string):
|
|
|
79
79
|
.replace(/^(?:\[?(?:INFO|WARN|WARNING|ERROR|ERR|DEBUG)\]?\s+)+/i, "")
|
|
80
80
|
.replace(/([\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{2300}-\u{23FF}]\uFE0F?)\s{2,}/gu, "$1 ");
|
|
81
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
|
+
|
|
82
89
|
const entry: ServerLogMessage = { time, level, message: line };
|
|
83
90
|
logHistory.push(entry);
|
|
84
91
|
if (logHistory.length > MAX_LOG_HISTORY) {
|
|
@@ -1150,27 +1150,19 @@ async function tryCreateStreamWithRetry(
|
|
|
1150
1150
|
const hasRequestPersonalization =
|
|
1151
1151
|
params.requestPersonalizationInstruction !== null &&
|
|
1152
1152
|
params.requestPersonalizationInstruction !== undefined;
|
|
1153
|
-
const releasePersonalization = hasRequestPersonalization
|
|
1154
|
-
? await acquirePersonalizationLock(currentAccountId)
|
|
1155
|
-
: null;
|
|
1156
|
-
// A same-session retry (or client disconnect) can abort this request
|
|
1157
|
-
// while the personalization sync is still stuck on a hung page op
|
|
1158
|
-
// (closed Playwright context / WAF). The sync never resolves, so the
|
|
1159
|
-
// finally below would not run and the mutex would stay held for
|
|
1160
|
-
// minutes, blocking the retry until its 60s acquire timeout fires.
|
|
1161
|
-
// Release the lock immediately on abort instead.
|
|
1162
|
-
const onPersonalizationAbort = () => releasePersonalization?.();
|
|
1163
|
-
if (combinedSignal.aborted) {
|
|
1164
|
-
onPersonalizationAbort();
|
|
1165
|
-
} else {
|
|
1166
|
-
combinedSignal.addEventListener("abort", onPersonalizationAbort, {
|
|
1167
|
-
once: true,
|
|
1168
|
-
});
|
|
1169
|
-
}
|
|
1170
1153
|
let result: Awaited<ReturnType<typeof createQwenStream>>;
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1154
|
+
let promptForUpstream = effectivePrompt;
|
|
1155
|
+
if (hasRequestPersonalization) {
|
|
1156
|
+
const releasePersonalization = await acquirePersonalizationLock(currentAccountId);
|
|
1157
|
+
const onPersonalizationAbort = () => releasePersonalization();
|
|
1158
|
+
if (combinedSignal.aborted) {
|
|
1159
|
+
onPersonalizationAbort();
|
|
1160
|
+
} else {
|
|
1161
|
+
combinedSignal.addEventListener("abort", onPersonalizationAbort, {
|
|
1162
|
+
once: true,
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
try {
|
|
1174
1166
|
// Let the hash-based cache in syncQwenRequestPersonalization decide
|
|
1175
1167
|
// whether to actually POST. A new chat does not imply the account's
|
|
1176
1168
|
// global settings were reset — only session refresh or profile reset
|
|
@@ -1244,12 +1236,25 @@ async function tryCreateStreamWithRetry(
|
|
|
1244
1236
|
`personalization sync not confirmed for ${currentAccountEmail}: ${syncFailure ?? "settings response did not confirm the instruction"}`,
|
|
1245
1237
|
);
|
|
1246
1238
|
}
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1239
|
+
} finally {
|
|
1240
|
+
combinedSignal.removeEventListener("abort", onPersonalizationAbort);
|
|
1241
|
+
releasePersonalization();
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
if (logger.isLevelEnabled("info")) {
|
|
1245
|
+
console.log(
|
|
1246
|
+
`⏱️ [Chat] Acquire: sync | account=${currentAccountEmail} | +${Date.now() - acquireStartedAt}ms`,
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
if (combinedSignal.aborted) {
|
|
1251
|
+
accountLease?.release();
|
|
1252
|
+
return {
|
|
1253
|
+
success: false,
|
|
1254
|
+
error: new ClientAbortedError("client aborted during personalization sync"),
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1253
1258
|
|
|
1254
1259
|
assertPromptWithinLimits(
|
|
1255
1260
|
promptForUpstream,
|
|
@@ -1350,13 +1355,6 @@ async function tryCreateStreamWithRetry(
|
|
|
1350
1355
|
},
|
|
1351
1356
|
};
|
|
1352
1357
|
}
|
|
1353
|
-
} finally {
|
|
1354
|
-
combinedSignal.removeEventListener(
|
|
1355
|
-
"abort",
|
|
1356
|
-
onPersonalizationAbort,
|
|
1357
|
-
);
|
|
1358
|
-
releasePersonalization?.();
|
|
1359
|
-
}
|
|
1360
1358
|
|
|
1361
1359
|
// Client cancelled (or a same-session retry superseded us) during the
|
|
1362
1360
|
// (potentially slow) personalization sync. Bail before createQwenStream
|
|
@@ -220,6 +220,7 @@ export function isAccountInitializationError(err: unknown): boolean {
|
|
|
220
220
|
return (
|
|
221
221
|
code === "acquire_deadline" ||
|
|
222
222
|
message.includes("acquire deadline") ||
|
|
223
|
+
message.includes("header capture timed out") ||
|
|
223
224
|
message.includes("header capture returned incomplete anti-fraud headers") ||
|
|
224
225
|
message.includes("required qwen anti-fraud headers are unavailable") ||
|
|
225
226
|
message.includes("playwright not initialized for account") ||
|
|
@@ -479,6 +480,28 @@ export function classifyRetryAction(
|
|
|
479
480
|
});
|
|
480
481
|
}
|
|
481
482
|
|
|
483
|
+
// Upstream 401 / Unauthorized on chat creation or API requests:
|
|
484
|
+
// Account session is invalid or expired. Cool down account with AuthInitFailed so the
|
|
485
|
+
// proxy rotates to a valid account instead of looping endlessly on 503s.
|
|
486
|
+
if (
|
|
487
|
+
code === "createchatinvalidresponse" ||
|
|
488
|
+
code === "createchatfailed" ||
|
|
489
|
+
code === "unauthorized" ||
|
|
490
|
+
message.includes("401 não autorizado") ||
|
|
491
|
+
message.includes("não tem permissão para acessar") ||
|
|
492
|
+
message.includes("401 unauthorized") ||
|
|
493
|
+
message.includes('"code":"unauthorized"') ||
|
|
494
|
+
message.includes('"code": "unauthorized"')
|
|
495
|
+
) {
|
|
496
|
+
return makeRetryAction("account_initialization_failed", {
|
|
497
|
+
switchAccount: true,
|
|
498
|
+
forceNewChat: true,
|
|
499
|
+
retryWithFullPrompt: true,
|
|
500
|
+
retryAfterMs: Math.min(baseDelayMs, 1_000),
|
|
501
|
+
accountCooldownMs: config.concurrency.initFailureCooldownMs,
|
|
502
|
+
accountCooldownReason: "AuthInitFailed",
|
|
503
|
+
});
|
|
504
|
+
}
|
|
482
505
|
// Specialized recoveries first (even if wrapped as RetryableQwenStreamError)
|
|
483
506
|
// Corrupted chat history must win over broad "invalid input" matches.
|
|
484
507
|
// Try a fresh chat on the SAME account first — the corruption is in the
|
|
@@ -14,11 +14,11 @@ import {
|
|
|
14
14
|
closeAllPlaywright,
|
|
15
15
|
getActivePlaywrightAccountIds,
|
|
16
16
|
} from "./playwright.ts";
|
|
17
|
+
import { isChatSessionActive } from "./qwen-thread-state.ts";
|
|
18
|
+
import { hasActiveAccountLease, isAccountBusy } from "../core/account-concurrency.ts";
|
|
17
19
|
import { isAuthMockEnabled } from "./auth-playwright.ts";
|
|
18
20
|
import { maskEmail, logger } from "../core/logger.ts";
|
|
19
21
|
import { config } from "../core/config.ts";
|
|
20
|
-
import { isAccountBusy } from "../core/account-concurrency.ts";
|
|
21
|
-
import { isChatSessionActive } from "./qwen-thread-state.ts";
|
|
22
22
|
import { sleep } from "./human-behavior.ts";
|
|
23
23
|
import { metrics } from "../core/metrics.ts";
|
|
24
24
|
|
|
@@ -246,11 +246,16 @@ export async function cleanOldChatsForAccount(
|
|
|
246
246
|
: new Date(chat.updated_at).getTime();
|
|
247
247
|
|
|
248
248
|
if (now - updatedMs >= maxAgeMs) {
|
|
249
|
+
// Cooperative yield: if account becomes active with a request, stop cleanup immediately
|
|
250
|
+
if (hasActiveAccountLease(accountId) || isAccountBusy(accountId)) {
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
|
|
249
254
|
const ok = await deleteSingleQwenChat(accountId, chat.id);
|
|
250
255
|
if (ok) {
|
|
251
256
|
cleaned++;
|
|
252
257
|
metrics.increment("chats.cleaned");
|
|
253
|
-
await sleep(
|
|
258
|
+
await sleep(150);
|
|
254
259
|
}
|
|
255
260
|
}
|
|
256
261
|
}
|
|
@@ -281,6 +286,8 @@ export function scheduleStartupChatCleanup(): void {
|
|
|
281
286
|
try {
|
|
282
287
|
const activeIds = getActivePlaywrightAccountIds();
|
|
283
288
|
for (const accountId of activeIds) {
|
|
289
|
+
// Yield to active requests: skip if account is busy serving a stream or lease
|
|
290
|
+
if (hasActiveAccountLease(accountId) || isAccountBusy(accountId)) continue;
|
|
284
291
|
await cleanOldChatsForAccount(accountId);
|
|
285
292
|
await sleep(1_000);
|
|
286
293
|
}
|
|
@@ -2606,7 +2606,9 @@ async function refreshHeadersInternal(
|
|
|
2606
2606
|
),
|
|
2607
2607
|
});
|
|
2608
2608
|
const url = page.url();
|
|
2609
|
-
|
|
2609
|
+
const isAuthUrl = url.includes("auth") || url.includes("login");
|
|
2610
|
+
const isLoggedIn = isAuthUrl ? false : await isPageLoggedIn(page, 5_000);
|
|
2611
|
+
if (isAuthUrl || !isLoggedIn) {
|
|
2610
2612
|
console.warn(
|
|
2611
2613
|
`⚠️ [Playwright] Session expired during refresh for ${accountId}, re-authenticating...`,
|
|
2612
2614
|
);
|
package/src/sync/claude-code.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import type { ClientSyncResult, SyncOptions } from "./types.ts";
|
|
4
|
-
import { createTimestampBackup, restoreFromBackup } from "./utils.ts";
|
|
4
|
+
import { createTimestampBackup, restoreFromBackup, formatModelDisplayName } from "./utils.ts";
|
|
5
5
|
|
|
6
6
|
export function syncClaudeCode(options: SyncOptions): ClientSyncResult {
|
|
7
7
|
const { filePath, apiKey, baseUrl, model = "qwen3.8-max" } = options;
|
|
@@ -27,8 +27,8 @@ export function syncClaudeCode(options: SyncOptions): ClientSyncResult {
|
|
|
27
27
|
ANTHROPIC_AUTH_TOKEN: apiKey,
|
|
28
28
|
ANTHROPIC_MODEL: model,
|
|
29
29
|
ANTHROPIC_CUSTOM_MODEL_OPTION: model,
|
|
30
|
-
ANTHROPIC_CUSTOM_MODEL_OPTION_NAME:
|
|
31
|
-
ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION:
|
|
30
|
+
ANTHROPIC_CUSTOM_MODEL_OPTION_NAME: formatModelDisplayName(model),
|
|
31
|
+
ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION: `QwenProxy ${model}`,
|
|
32
32
|
ANTHROPIC_DEFAULT_SONNET_MODEL: model,
|
|
33
33
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: "qwen3.7-plus",
|
|
34
34
|
ANTHROPIC_DEFAULT_OPUS_MODEL: model,
|
package/src/sync/codex.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import type { ClientSyncResult, SyncOptions } from "./types.ts";
|
|
4
|
-
import { createTimestampBackup, restoreFromBackup } from "./utils.ts";
|
|
4
|
+
import { createTimestampBackup, restoreFromBackup, formatModelDisplayName } from "./utils.ts";
|
|
5
5
|
|
|
6
6
|
function updateTopLevelKey(content: string, key: string, value: string | number): string {
|
|
7
7
|
// Find first section header [section]
|
|
@@ -75,8 +75,8 @@ experimental_bearer_token = "${apiKey}"
|
|
|
75
75
|
catalog.models.unshift({
|
|
76
76
|
...template,
|
|
77
77
|
slug: model,
|
|
78
|
-
display_name:
|
|
79
|
-
description: `QwenProxy ${model}
|
|
78
|
+
display_name: formatModelDisplayName(model),
|
|
79
|
+
description: `QwenProxy ${model}`,
|
|
80
80
|
context_window: 1000000,
|
|
81
81
|
max_context_window: 1000000,
|
|
82
82
|
});
|
package/src/sync/index.ts
CHANGED
|
@@ -462,6 +462,9 @@ export function syncAllClients(options: SyncAllOptions = {}): SyncAllResult {
|
|
|
462
462
|
const apiKey = resolveApiKey(options.apiKey, config.apiKey);
|
|
463
463
|
const { anthropicBaseUrl, openaiBaseUrl } = resolveBaseUrls(port, host);
|
|
464
464
|
const stateFilePath = options.stateFilePath || getDefaultStateFilePath();
|
|
465
|
+
const selectedModel = options.model || "qwen3.8-max";
|
|
466
|
+
const allModels = options.models && options.models.length > 0 ? options.models : undefined;
|
|
467
|
+
const syncModels = options.syncAllModels !== false && allModels ? allModels : [selectedModel];
|
|
465
468
|
|
|
466
469
|
const results: SyncAllResult = {
|
|
467
470
|
apiKey,
|
|
@@ -483,6 +486,7 @@ export function syncAllClients(options: SyncAllOptions = {}): SyncAllResult {
|
|
|
483
486
|
filePath: paths.claudeCode,
|
|
484
487
|
apiKey,
|
|
485
488
|
baseUrl: anthropicBaseUrl,
|
|
489
|
+
model: selectedModel,
|
|
486
490
|
});
|
|
487
491
|
results.clients.claudeCode = claudeRes;
|
|
488
492
|
if (claudeRes.success && claudeRes.backupPath) {
|
|
@@ -503,6 +507,7 @@ export function syncAllClients(options: SyncAllOptions = {}): SyncAllResult {
|
|
|
503
507
|
apiKey,
|
|
504
508
|
baseUrl: openaiBaseUrl,
|
|
505
509
|
setActive: options.setActive ?? true,
|
|
510
|
+
model: selectedModel,
|
|
506
511
|
});
|
|
507
512
|
results.clients.codex = codexRes;
|
|
508
513
|
if (codexRes.success && codexRes.backupPath) {
|
|
@@ -522,6 +527,8 @@ export function syncAllClients(options: SyncAllOptions = {}): SyncAllResult {
|
|
|
522
527
|
filePath: paths.openCode,
|
|
523
528
|
apiKey,
|
|
524
529
|
baseUrl: openaiBaseUrl,
|
|
530
|
+
model: selectedModel,
|
|
531
|
+
models: syncModels,
|
|
525
532
|
});
|
|
526
533
|
results.clients.openCode = openCodeRes;
|
|
527
534
|
if (openCodeRes.success && openCodeRes.backupPath) {
|
|
@@ -541,6 +548,8 @@ export function syncAllClients(options: SyncAllOptions = {}): SyncAllResult {
|
|
|
541
548
|
filePath: paths.omp,
|
|
542
549
|
apiKey,
|
|
543
550
|
baseUrl: openaiBaseUrl,
|
|
551
|
+
model: selectedModel,
|
|
552
|
+
models: syncModels,
|
|
544
553
|
});
|
|
545
554
|
results.clients.omp = ompRes;
|
|
546
555
|
if (ompRes.success && ompRes.backupPath) {
|
|
@@ -560,6 +569,7 @@ export function syncAllClients(options: SyncAllOptions = {}): SyncAllResult {
|
|
|
560
569
|
filePath: paths.hermes,
|
|
561
570
|
apiKey,
|
|
562
571
|
baseUrl: openaiBaseUrl,
|
|
572
|
+
model: selectedModel,
|
|
563
573
|
});
|
|
564
574
|
results.clients.hermes = hermesRes;
|
|
565
575
|
if (hermesRes.success && hermesRes.backupPath) {
|
|
@@ -579,6 +589,8 @@ export function syncAllClients(options: SyncAllOptions = {}): SyncAllResult {
|
|
|
579
589
|
filePath: paths.openClaw,
|
|
580
590
|
apiKey,
|
|
581
591
|
baseUrl: openaiBaseUrl,
|
|
592
|
+
model: selectedModel,
|
|
593
|
+
models: syncModels,
|
|
582
594
|
});
|
|
583
595
|
results.clients.openClaw = openClawRes;
|
|
584
596
|
if (openClawRes.success && openClawRes.backupPath) {
|
|
@@ -599,6 +611,8 @@ export function syncAllClients(options: SyncAllOptions = {}): SyncAllResult {
|
|
|
599
611
|
apiKey,
|
|
600
612
|
baseUrl: openaiBaseUrl,
|
|
601
613
|
setActive: options.setActive ?? true,
|
|
614
|
+
model: selectedModel,
|
|
615
|
+
models: syncModels,
|
|
602
616
|
});
|
|
603
617
|
results.clients.kilo = kiloRes;
|
|
604
618
|
if (kiloRes.success && kiloRes.backupPath) {
|
|
@@ -618,6 +632,7 @@ export function syncAllClients(options: SyncAllOptions = {}): SyncAllResult {
|
|
|
618
632
|
filePath: paths.cline,
|
|
619
633
|
apiKey,
|
|
620
634
|
baseUrl: openaiBaseUrl,
|
|
635
|
+
model: selectedModel,
|
|
621
636
|
});
|
|
622
637
|
results.clients.cline = clineRes;
|
|
623
638
|
if (clineRes.success && clineRes.backupPath) {
|
|
@@ -638,6 +653,8 @@ export function syncAllClients(options: SyncAllOptions = {}): SyncAllResult {
|
|
|
638
653
|
apiKey,
|
|
639
654
|
baseUrl: openaiBaseUrl,
|
|
640
655
|
setActive: options.setActive ?? true,
|
|
656
|
+
model: selectedModel,
|
|
657
|
+
models: syncModels,
|
|
641
658
|
});
|
|
642
659
|
results.clients.zed = zedRes;
|
|
643
660
|
if (zedRes.success && zedRes.backupPath) {
|
|
@@ -657,6 +674,7 @@ export function syncAllClients(options: SyncAllOptions = {}): SyncAllResult {
|
|
|
657
674
|
filePath: paths.aider,
|
|
658
675
|
apiKey,
|
|
659
676
|
baseUrl: openaiBaseUrl,
|
|
677
|
+
model: selectedModel,
|
|
660
678
|
});
|
|
661
679
|
results.clients.aider = aiderRes;
|
|
662
680
|
if (aiderRes.success && aiderRes.backupPath) {
|
package/src/sync/kilo.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import type { ClientSyncResult, SyncOptions } from "./types.ts";
|
|
4
|
-
import { createTimestampBackup, restoreFromBackup } from "./utils.ts";
|
|
4
|
+
import { createTimestampBackup, restoreFromBackup, formatModelDisplayName } from "./utils.ts";
|
|
5
5
|
|
|
6
6
|
function findKeyObjectSpan(content: string, key: string): { start: number; end: number; hasTrailingComma: boolean } | null {
|
|
7
7
|
const regex = new RegExp(`"${key}"\\s*:\\s*\\{`);
|
|
@@ -90,21 +90,16 @@ function buildKiloProviderObject(
|
|
|
90
90
|
baseUrl: string,
|
|
91
91
|
apiKey: string,
|
|
92
92
|
primaryModel: string = "qwen3.8-max",
|
|
93
|
+
models?: string[],
|
|
93
94
|
): Record<string, any> {
|
|
94
95
|
const modelsObj: Record<string, any> = {};
|
|
95
|
-
const modelList =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
96
|
+
const modelList = Array.from(
|
|
97
|
+
new Set([primaryModel, ...(models && models.length > 0 ? models : [primaryModel, "qwen3.7-plus"])].filter(Boolean)),
|
|
98
|
+
);
|
|
99
99
|
|
|
100
100
|
for (const m of modelList) {
|
|
101
101
|
modelsObj[m] = {
|
|
102
|
-
name:
|
|
103
|
-
m === "qwen3.8-max"
|
|
104
|
-
? "Qwen 3.8 Max"
|
|
105
|
-
: m === "qwen3.7-plus"
|
|
106
|
-
? "Qwen 3.7 Plus"
|
|
107
|
-
: m,
|
|
102
|
+
name: formatModelDisplayName(m),
|
|
108
103
|
limit: { context: 1048576, output: 65536 },
|
|
109
104
|
modalities: { input: ["text", "image"], output: ["text"] },
|
|
110
105
|
reasoning: true,
|
|
@@ -128,7 +123,7 @@ function buildKiloProviderObject(
|
|
|
128
123
|
}
|
|
129
124
|
|
|
130
125
|
export function syncKilo(options: SyncOptions): ClientSyncResult {
|
|
131
|
-
const { filePath, apiKey, baseUrl, model = "qwen3.8-max", setActive = true } = options;
|
|
126
|
+
const { filePath, apiKey, baseUrl, model = "qwen3.8-max", models, setActive = true } = options;
|
|
132
127
|
try {
|
|
133
128
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
134
129
|
|
|
@@ -140,7 +135,7 @@ export function syncKilo(options: SyncOptions): ClientSyncResult {
|
|
|
140
135
|
content = fs.readFileSync(filePath, "utf-8");
|
|
141
136
|
}
|
|
142
137
|
|
|
143
|
-
const providerObj = buildKiloProviderObject(baseUrl, apiKey, model);
|
|
138
|
+
const providerObj = buildKiloProviderObject(baseUrl, apiKey, model, models);
|
|
144
139
|
const providerJson = JSON.stringify(providerObj, null, 6)
|
|
145
140
|
.split("\n")
|
|
146
141
|
.map((line, idx) => (idx === 0 ? line : ` ${line}`))
|
package/src/sync/omp.ts
CHANGED
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import type { ClientSyncResult, SyncOptions } from "./types.ts";
|
|
4
|
-
import { createTimestampBackup, restoreFromBackup } from "./utils.ts";
|
|
4
|
+
import { createTimestampBackup, restoreFromBackup, formatModelDisplayName } from "./utils.ts";
|
|
5
5
|
|
|
6
6
|
function buildOmpProviderYaml(
|
|
7
7
|
baseUrl: string,
|
|
8
8
|
apiKey: string,
|
|
9
9
|
primaryModel: string = "qwen3.8-max",
|
|
10
|
+
models?: string[],
|
|
10
11
|
): string {
|
|
11
|
-
const modelList =
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
15
|
-
|
|
12
|
+
const modelList = Array.from(
|
|
13
|
+
new Set([primaryModel, ...(models && models.length > 0 ? models : [primaryModel, "qwen3.7-plus"])].filter(Boolean)),
|
|
14
|
+
);
|
|
16
15
|
const formattedModels = modelList
|
|
17
16
|
.map(
|
|
18
17
|
(m) => ` - id: ${m}
|
|
19
|
-
name: ${m
|
|
18
|
+
name: ${formatModelDisplayName(m).replace(/\s+/g, "")}
|
|
20
19
|
input: [text, image]
|
|
21
20
|
contextWindow: 1000000
|
|
22
21
|
maxTokens: 131072
|
|
@@ -41,7 +40,7 @@ ${formattedModels}
|
|
|
41
40
|
}
|
|
42
41
|
|
|
43
42
|
export function syncOmp(options: SyncOptions): ClientSyncResult {
|
|
44
|
-
const { filePath, apiKey, baseUrl, model = "qwen3.8-max" } = options;
|
|
43
|
+
const { filePath, apiKey, baseUrl, model = "qwen3.8-max", models } = options;
|
|
45
44
|
try {
|
|
46
45
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
47
46
|
|
|
@@ -53,8 +52,7 @@ export function syncOmp(options: SyncOptions): ClientSyncResult {
|
|
|
53
52
|
content = fs.readFileSync(filePath, "utf-8");
|
|
54
53
|
}
|
|
55
54
|
|
|
56
|
-
const providerBlock = buildOmpProviderYaml(baseUrl, apiKey, model);
|
|
57
|
-
|
|
55
|
+
const providerBlock = buildOmpProviderYaml(baseUrl, apiKey, model, models);
|
|
58
56
|
if (!content.trim()) {
|
|
59
57
|
content = `providers:\n${providerBlock}`;
|
|
60
58
|
} else {
|
package/src/sync/openclaw.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import type { ClientSyncResult, SyncOptions } from "./types.ts";
|
|
4
|
-
import { createTimestampBackup, restoreFromBackup } from "./utils.ts";
|
|
4
|
+
import { createTimestampBackup, restoreFromBackup, formatModelDisplayName } from "./utils.ts";
|
|
5
5
|
|
|
6
6
|
function findKeyObjectSpan(content: string, key: string): { start: number; end: number; hasTrailingComma: boolean } | null {
|
|
7
7
|
const regex = new RegExp(`"${key}"\\s*:\\s*\\{`);
|
|
@@ -90,41 +90,31 @@ function buildOpenClawProviderObject(
|
|
|
90
90
|
baseUrl: string,
|
|
91
91
|
apiKey: string,
|
|
92
92
|
model: string = "qwen3.8-max",
|
|
93
|
+
models?: string[],
|
|
93
94
|
): Record<string, any> {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (model !== "qwen3.7-plus") {
|
|
107
|
-
models.push({
|
|
108
|
-
id: "qwen3.7-plus",
|
|
109
|
-
name: "Qwen 3.7 Plus",
|
|
110
|
-
reasoning: true,
|
|
111
|
-
supportsReasoningEffort: true,
|
|
112
|
-
supportedReasoningEfforts: ["low", "medium", "high"],
|
|
113
|
-
contextWindow: 1000000,
|
|
114
|
-
maxTokens: 65536,
|
|
115
|
-
});
|
|
116
|
-
}
|
|
95
|
+
const modelList = Array.from(
|
|
96
|
+
new Set([model, ...(models && models.length > 0 ? models : [model, "qwen3.7-plus"])].filter(Boolean)),
|
|
97
|
+
);
|
|
98
|
+
const modelEntries = modelList.map((m) => ({
|
|
99
|
+
id: m,
|
|
100
|
+
name: formatModelDisplayName(m),
|
|
101
|
+
reasoning: true,
|
|
102
|
+
supportsReasoningEffort: true,
|
|
103
|
+
supportedReasoningEfforts: ["low", "medium", "high"],
|
|
104
|
+
contextWindow: 1000000,
|
|
105
|
+
maxTokens: 65536,
|
|
106
|
+
}));
|
|
117
107
|
|
|
118
108
|
return {
|
|
119
109
|
baseUrl,
|
|
120
110
|
apiKey,
|
|
121
111
|
api: "openai-completions",
|
|
122
|
-
models,
|
|
112
|
+
models: modelEntries,
|
|
123
113
|
};
|
|
124
114
|
}
|
|
125
115
|
|
|
126
116
|
export function syncOpenClaw(options: SyncOptions): ClientSyncResult {
|
|
127
|
-
const { filePath, apiKey, baseUrl, model = "qwen3.8-max", reasoningEffort = "high" } = options;
|
|
117
|
+
const { filePath, apiKey, baseUrl, model = "qwen3.8-max", models, reasoningEffort = "high" } = options;
|
|
128
118
|
try {
|
|
129
119
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
130
120
|
|
|
@@ -136,7 +126,7 @@ export function syncOpenClaw(options: SyncOptions): ClientSyncResult {
|
|
|
136
126
|
content = fs.readFileSync(filePath, "utf-8");
|
|
137
127
|
}
|
|
138
128
|
|
|
139
|
-
const providerObj = buildOpenClawProviderObject(baseUrl, apiKey, model);
|
|
129
|
+
const providerObj = buildOpenClawProviderObject(baseUrl, apiKey, model, models);
|
|
140
130
|
const providerJson = JSON.stringify(providerObj, null, 6)
|
|
141
131
|
.split("\n")
|
|
142
132
|
.map((line, idx) => (idx === 0 ? line : ` ${line}`))
|
package/src/sync/opencode.ts
CHANGED
|
@@ -1,27 +1,22 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import type { ClientSyncResult, SyncOptions } from "./types.ts";
|
|
4
|
-
import { createTimestampBackup, restoreFromBackup } from "./utils.ts";
|
|
4
|
+
import { createTimestampBackup, restoreFromBackup, formatModelDisplayName } from "./utils.ts";
|
|
5
5
|
|
|
6
6
|
function buildOpenCodeProviderObject(
|
|
7
7
|
baseUrl: string,
|
|
8
8
|
apiKey: string,
|
|
9
9
|
primaryModel: string = "qwen3.8-max",
|
|
10
|
+
models?: string[],
|
|
10
11
|
): Record<string, any> {
|
|
11
12
|
const modelsObj: Record<string, any> = {};
|
|
12
|
-
const modelList =
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
13
|
+
const modelList = Array.from(
|
|
14
|
+
new Set([primaryModel, ...(models && models.length > 0 ? models : [primaryModel, "qwen3.7-plus"])].filter(Boolean)),
|
|
15
|
+
);
|
|
16
16
|
|
|
17
17
|
for (const m of modelList) {
|
|
18
18
|
modelsObj[m] = {
|
|
19
|
-
name:
|
|
20
|
-
m === "qwen3.8-max"
|
|
21
|
-
? "Qwen 3.8 Max"
|
|
22
|
-
: m === "qwen3.7-plus"
|
|
23
|
-
? "Qwen 3.7 Plus"
|
|
24
|
-
: m,
|
|
19
|
+
name: formatModelDisplayName(m),
|
|
25
20
|
limit: { context: 1048576, output: 65536 },
|
|
26
21
|
modalities: { input: ["text", "image"], output: ["text"] },
|
|
27
22
|
reasoning: true,
|
|
@@ -127,8 +122,9 @@ function findKeyObjectSpan(content: string, key: string): { start: number; end:
|
|
|
127
122
|
}
|
|
128
123
|
|
|
129
124
|
export function syncOpenCode(options: SyncOptions): ClientSyncResult {
|
|
130
|
-
const { filePath, apiKey, baseUrl, model = "qwen3.8-max" } = options;
|
|
125
|
+
const { filePath, apiKey, baseUrl, model = "qwen3.8-max", models } = options;
|
|
131
126
|
try {
|
|
127
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
132
128
|
let backupPath: string | undefined;
|
|
133
129
|
let content = "";
|
|
134
130
|
|
|
@@ -137,7 +133,7 @@ export function syncOpenCode(options: SyncOptions): ClientSyncResult {
|
|
|
137
133
|
content = fs.readFileSync(filePath, "utf-8");
|
|
138
134
|
}
|
|
139
135
|
|
|
140
|
-
const providerObj = buildOpenCodeProviderObject(baseUrl, apiKey, model);
|
|
136
|
+
const providerObj = buildOpenCodeProviderObject(baseUrl, apiKey, model, models);
|
|
141
137
|
const providerJson = JSON.stringify(providerObj, null, 6)
|
|
142
138
|
.split("\n")
|
|
143
139
|
.map((line, idx) => (idx === 0 ? line : ` ${line}`))
|
package/src/sync/types.ts
CHANGED
|
@@ -26,6 +26,7 @@ export interface SyncOptions {
|
|
|
26
26
|
apiKey: string;
|
|
27
27
|
baseUrl: string;
|
|
28
28
|
model?: string;
|
|
29
|
+
models?: string[];
|
|
29
30
|
setActive?: boolean;
|
|
30
31
|
reasoningEffort?: "low" | "medium" | "high" | "none";
|
|
31
32
|
modelSettingsPath?: string;
|
|
@@ -38,6 +39,9 @@ export interface SyncAllOptions {
|
|
|
38
39
|
setActive?: boolean;
|
|
39
40
|
stateFilePath?: string;
|
|
40
41
|
targets?: SyncClientName[];
|
|
42
|
+
model?: string;
|
|
43
|
+
models?: string[];
|
|
44
|
+
syncAllModels?: boolean;
|
|
41
45
|
customPaths?: {
|
|
42
46
|
claudeCode?: string;
|
|
43
47
|
codex?: string;
|
package/src/sync/utils.ts
CHANGED
|
@@ -54,3 +54,21 @@ export function restoreFromBackup(filePath: string, backupPath?: string): boolea
|
|
|
54
54
|
}
|
|
55
55
|
return true;
|
|
56
56
|
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Format internal model slug to clean human-readable display name without bulky suffixes.
|
|
60
|
+
* E.g. "qwen3.8-max" -> "Qwen 3.8 Max", "qwen3.8-omni-flash" -> "Qwen 3.8 Omni Flash"
|
|
61
|
+
*/
|
|
62
|
+
export function formatModelDisplayName(model: string): string {
|
|
63
|
+
if (model === "qwen3.8-max") return "Qwen 3.8 Max";
|
|
64
|
+
if (model === "qwen3.8-omni-flash") return "Qwen 3.8 Omni Flash";
|
|
65
|
+
if (model === "qwen3.7-plus") return "Qwen 3.7 Plus";
|
|
66
|
+
if (model === "qwen3.6-plus") return "Qwen 3.6 Plus";
|
|
67
|
+
return model
|
|
68
|
+
.replace(/^qwen/i, "Qwen ")
|
|
69
|
+
.split(/[-_]/)
|
|
70
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
71
|
+
.join(" ")
|
|
72
|
+
.replace(/\s+/g, " ")
|
|
73
|
+
.trim();
|
|
74
|
+
}
|
package/src/sync/zed.ts
CHANGED
|
@@ -64,10 +64,15 @@ function parseJsonWithComments(raw: string): Record<string, any> {
|
|
|
64
64
|
return JSON.parse(cleaned);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
function buildZedAvailableModels(primaryModel: string = "qwen3.8-max"): any[] {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
function buildZedAvailableModels(primaryModel: string = "qwen3.8-max", models?: string[]): any[] {
|
|
68
|
+
const modelList = Array.from(
|
|
69
|
+
new Set([primaryModel, ...(models && models.length > 0 ? models : [primaryModel, "qwen3.7-plus"])].filter(Boolean)),
|
|
70
|
+
);
|
|
71
|
+
const zedModels: any[] = [];
|
|
72
|
+
|
|
73
|
+
for (const m of modelList) {
|
|
74
|
+
zedModels.push({
|
|
75
|
+
name: m,
|
|
71
76
|
max_tokens: 1000000,
|
|
72
77
|
max_output_tokens: 131072,
|
|
73
78
|
max_completion_tokens: 131072,
|
|
@@ -79,59 +84,46 @@ function buildZedAvailableModels(primaryModel: string = "qwen3.8-max"): any[] {
|
|
|
79
84
|
chat_completions: true,
|
|
80
85
|
interleaved_reasoning: true,
|
|
81
86
|
},
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
name: `${primaryModel}-thinking`,
|
|
85
|
-
max_tokens: 1000000,
|
|
86
|
-
max_output_tokens: 131072,
|
|
87
|
-
max_completion_tokens: 131072,
|
|
88
|
-
capabilities: {
|
|
89
|
-
tools: true,
|
|
90
|
-
images: true,
|
|
91
|
-
parallel_tool_calls: true,
|
|
92
|
-
prompt_cache_key: true,
|
|
93
|
-
chat_completions: true,
|
|
94
|
-
interleaved_reasoning: true,
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
name: `${primaryModel}-fast`,
|
|
99
|
-
max_tokens: 1000000,
|
|
100
|
-
max_output_tokens: 131072,
|
|
101
|
-
max_completion_tokens: 131072,
|
|
102
|
-
capabilities: {
|
|
103
|
-
tools: true,
|
|
104
|
-
images: true,
|
|
105
|
-
parallel_tool_calls: true,
|
|
106
|
-
prompt_cache_key: true,
|
|
107
|
-
chat_completions: true,
|
|
108
|
-
interleaved_reasoning: true,
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
];
|
|
112
|
-
|
|
113
|
-
if (primaryModel !== "qwen3.7-plus") {
|
|
114
|
-
models.push({
|
|
115
|
-
name: "qwen3.7-plus",
|
|
116
|
-
max_tokens: 1000000,
|
|
117
|
-
max_output_tokens: 65536,
|
|
118
|
-
max_completion_tokens: 65536,
|
|
119
|
-
capabilities: {
|
|
120
|
-
tools: true,
|
|
121
|
-
images: true,
|
|
122
|
-
parallel_tool_calls: true,
|
|
123
|
-
prompt_cache_key: true,
|
|
124
|
-
chat_completions: true,
|
|
125
|
-
interleaved_reasoning: true,
|
|
126
|
-
},
|
|
127
87
|
});
|
|
88
|
+
if (m === primaryModel) {
|
|
89
|
+
zedModels.push(
|
|
90
|
+
{
|
|
91
|
+
name: `${m}-thinking`,
|
|
92
|
+
max_tokens: 1000000,
|
|
93
|
+
max_output_tokens: 131072,
|
|
94
|
+
max_completion_tokens: 131072,
|
|
95
|
+
capabilities: {
|
|
96
|
+
tools: true,
|
|
97
|
+
images: true,
|
|
98
|
+
parallel_tool_calls: true,
|
|
99
|
+
prompt_cache_key: true,
|
|
100
|
+
chat_completions: true,
|
|
101
|
+
interleaved_reasoning: true,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: `${m}-fast`,
|
|
106
|
+
max_tokens: 1000000,
|
|
107
|
+
max_output_tokens: 131072,
|
|
108
|
+
max_completion_tokens: 131072,
|
|
109
|
+
capabilities: {
|
|
110
|
+
tools: true,
|
|
111
|
+
images: true,
|
|
112
|
+
parallel_tool_calls: true,
|
|
113
|
+
prompt_cache_key: true,
|
|
114
|
+
chat_completions: true,
|
|
115
|
+
interleaved_reasoning: true,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
}
|
|
128
120
|
}
|
|
129
|
-
|
|
130
|
-
return models;
|
|
121
|
+
return zedModels;
|
|
131
122
|
}
|
|
132
123
|
|
|
124
|
+
|
|
133
125
|
export function syncZed(options: SyncOptions): ClientSyncResult {
|
|
134
|
-
const { filePath, baseUrl, model = "qwen3.8-max", setActive = true } = options;
|
|
126
|
+
const { filePath, baseUrl, model = "qwen3.8-max", models, setActive = true } = options;
|
|
135
127
|
try {
|
|
136
128
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
137
129
|
|
|
@@ -153,7 +145,7 @@ export function syncZed(options: SyncOptions): ClientSyncResult {
|
|
|
153
145
|
|
|
154
146
|
openaiCompatible.QwenProxy = {
|
|
155
147
|
api_url: baseUrl,
|
|
156
|
-
available_models: buildZedAvailableModels(model),
|
|
148
|
+
available_models: buildZedAvailableModels(model, models),
|
|
157
149
|
};
|
|
158
150
|
|
|
159
151
|
const updatedSettings: Record<string, any> = {
|
package/src/sync-clients.ts
CHANGED
|
@@ -19,6 +19,7 @@ function parseArgs() {
|
|
|
19
19
|
host?: string;
|
|
20
20
|
setActive: boolean;
|
|
21
21
|
targets: SyncClientName[];
|
|
22
|
+
model?: string;
|
|
22
23
|
} = {
|
|
23
24
|
restore: false,
|
|
24
25
|
list: false,
|
|
@@ -43,6 +44,8 @@ function parseArgs() {
|
|
|
43
44
|
options.host = args[++i];
|
|
44
45
|
} else if (arg === "--no-active") {
|
|
45
46
|
options.setActive = false;
|
|
47
|
+
} else if ((arg === "--model" || arg === "-m") && args[i + 1]) {
|
|
48
|
+
options.model = args[++i];
|
|
46
49
|
} else if (arg === "--client" && args[i + 1]) {
|
|
47
50
|
const normalized = normalizeClientName(args[++i]);
|
|
48
51
|
if (normalized) options.targets.push(normalized);
|
|
@@ -78,6 +81,7 @@ Exemplos:
|
|
|
78
81
|
|
|
79
82
|
Opções:
|
|
80
83
|
--client <nome> Nome do cliente (hermes, opencode, claude, openclaw, kilo, cline, omp, codex, zed, aider)
|
|
84
|
+
--model <modelo> Modelo padrão a configurar (padrão: qwen3.8-max)
|
|
81
85
|
--api-key <chave> Sobrescrever chave de API (padrão: lê do .env ou usa sk-qwenproxy-local)
|
|
82
86
|
--port <porta> Sobrescrever porta do servidor (padrão: lê do .env ou usa 7936)
|
|
83
87
|
--host <host> Sobrescrever host do servidor (padrão: 127.0.0.1)
|
|
@@ -165,6 +169,7 @@ async function main() {
|
|
|
165
169
|
host: options.host,
|
|
166
170
|
setActive: options.setActive,
|
|
167
171
|
targets: options.targets.length > 0 ? options.targets : undefined,
|
|
172
|
+
model: options.model,
|
|
168
173
|
});
|
|
169
174
|
|
|
170
175
|
console.log(`🔑 Chave API: ${result.apiKey}`);
|
|
@@ -6,7 +6,6 @@
|
|
|
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
|
-
import { recordServerLog } from "../core/server-log-buffer.ts";
|
|
10
9
|
|
|
11
10
|
export type ServerLifecycleState = "offline" | "warming" | "online" | "error";
|
|
12
11
|
export interface ServerLogEntry {
|
|
@@ -137,9 +136,6 @@ export class ServerManager {
|
|
|
137
136
|
if (this.logBuffer.length > 2000) {
|
|
138
137
|
this.logBuffer.shift();
|
|
139
138
|
}
|
|
140
|
-
try {
|
|
141
|
-
recordServerLog(level, line);
|
|
142
|
-
} catch {}
|
|
143
139
|
}
|
|
144
140
|
}
|
|
145
141
|
public interceptLogs(): void {
|
|
@@ -9,7 +9,7 @@ import type { KeyEvent } from "../screen.ts";
|
|
|
9
9
|
import { theme, drawBox, stringWidth, truncate, pad, stripAnsi, setClipboardText } from "../theme.ts";
|
|
10
10
|
import { ServerManager } from "../server-manager.ts";
|
|
11
11
|
import { loadTuiSettings, saveTuiSettings } from "../settings.ts";
|
|
12
|
-
import { getServerLogFilePath } from "../../core/paths.ts";
|
|
12
|
+
import { getServerLogFilePath, isRunningUnderNodeTest } from "../../core/paths.ts";
|
|
13
13
|
export class LogsView implements TuiView {
|
|
14
14
|
public readonly id = "logs";
|
|
15
15
|
public readonly title = "Logs";
|
|
@@ -70,8 +70,8 @@ export class LogsView implements TuiView {
|
|
|
70
70
|
chips.push({
|
|
71
71
|
id: d.id,
|
|
72
72
|
label: d.label,
|
|
73
|
-
startCol
|
|
74
|
-
endCol
|
|
73
|
+
startCol,
|
|
74
|
+
endCol,
|
|
75
75
|
});
|
|
76
76
|
currentCol += w;
|
|
77
77
|
}
|
|
@@ -90,9 +90,9 @@ export class LogsView implements TuiView {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
public handleKey(key: KeyEvent): boolean | void {
|
|
93
|
-
// Mouse hover or click on chips (
|
|
93
|
+
// Mouse hover or click on chips (terminal row 4 is the top border of the logs box where chips are rendered)
|
|
94
94
|
if ((key.name === "hover" || key.name === "click") && key.mouse) {
|
|
95
|
-
if (key.mouse.row ===
|
|
95
|
+
if (key.mouse.row === 4) {
|
|
96
96
|
const rawCount = ServerManager.getInstance().getLogEntries(this.filter).length;
|
|
97
97
|
const { chips } = this.getChips(rawCount);
|
|
98
98
|
const col = key.mouse.col;
|
|
@@ -148,7 +148,7 @@ export class LogsView implements TuiView {
|
|
|
148
148
|
return (
|
|
149
149
|
col >= this.lastWidth - 3 &&
|
|
150
150
|
col <= this.lastWidth &&
|
|
151
|
-
row >=
|
|
151
|
+
row >= 7 &&
|
|
152
152
|
row <= 6 + this.lastVisibleCapacity
|
|
153
153
|
);
|
|
154
154
|
};
|
|
@@ -166,7 +166,7 @@ export class LogsView implements TuiView {
|
|
|
166
166
|
if (key.name === "click" && key.mouse && isMouseOnScrollbar(key.mouse.col, key.mouse.row)) {
|
|
167
167
|
if (this.lastMaxOffset > 0 && this.lastVisibleCapacity > 0) {
|
|
168
168
|
this.isDraggingScrollbar = true;
|
|
169
|
-
const r = key.mouse.row -
|
|
169
|
+
const r = key.mouse.row - 7;
|
|
170
170
|
const pct = Math.max(0, Math.min(1, r / Math.max(1, this.lastVisibleCapacity - 1)));
|
|
171
171
|
const targetScrollFromTop = Math.round(pct * this.lastMaxOffset);
|
|
172
172
|
this.scrollOffset = Math.max(0, Math.min(this.lastMaxOffset, this.lastMaxOffset - targetScrollFromTop));
|
|
@@ -178,7 +178,7 @@ export class LogsView implements TuiView {
|
|
|
178
178
|
// Scrollbar Drag (Hold and Move)
|
|
179
179
|
if (key.name === "drag" && key.mouse) {
|
|
180
180
|
if (this.isDraggingScrollbar && this.lastMaxOffset > 0 && this.lastVisibleCapacity > 0) {
|
|
181
|
-
const r = key.mouse.row -
|
|
181
|
+
const r = key.mouse.row - 7;
|
|
182
182
|
const pct = Math.max(0, Math.min(1, r / Math.max(1, this.lastVisibleCapacity - 1)));
|
|
183
183
|
const targetScrollFromTop = Math.round(pct * this.lastMaxOffset);
|
|
184
184
|
this.scrollOffset = Math.max(0, Math.min(this.lastMaxOffset, this.lastMaxOffset - targetScrollFromTop));
|
|
@@ -195,9 +195,9 @@ export class LogsView implements TuiView {
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
// Mouse click on log rows (terminal row
|
|
199
|
-
if (key.name === "click" && key.mouse && key.mouse.row >=
|
|
200
|
-
const rowOffset = key.mouse.row -
|
|
198
|
+
// Mouse click on log rows (terminal row 7+, accounting for 2-line top margin: row 5 & 6 are margin)
|
|
199
|
+
if (key.name === "click" && key.mouse && key.mouse.row >= 7) {
|
|
200
|
+
const rowOffset = key.mouse.row - 7;
|
|
201
201
|
if (rowOffset >= 0 && rowOffset < this.lastVisibleCount) {
|
|
202
202
|
const clickedIdx = this.lastStartIndex + rowOffset;
|
|
203
203
|
if (this.selectedLogIndex === clickedIdx) {
|
|
@@ -471,7 +471,7 @@ export class LogsView implements TuiView {
|
|
|
471
471
|
text = `[${entry.time}] [${entry.level}] ${entry.message}`;
|
|
472
472
|
} else {
|
|
473
473
|
// For full copy of "all" filter, prefer the complete persistent file from disk if available
|
|
474
|
-
if (this.filter === "all") {
|
|
474
|
+
if (this.filter === "all" && !isRunningUnderNodeTest()) {
|
|
475
475
|
try {
|
|
476
476
|
const logPath = getServerLogFilePath();
|
|
477
477
|
if (fs.existsSync(logPath)) {
|
|
@@ -299,6 +299,9 @@ export class SyncView implements TuiView {
|
|
|
299
299
|
try {
|
|
300
300
|
const res = syncAllClients({
|
|
301
301
|
targets: selectedTargets,
|
|
302
|
+
model: currentModel,
|
|
303
|
+
models: this.availableModels,
|
|
304
|
+
syncAllModels: this.syncAllModels,
|
|
302
305
|
});
|
|
303
306
|
|
|
304
307
|
let successCount = 0;
|