qwenproxy-cli 1.0.32 → 1.2.0
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/README.es.md +267 -0
- package/README.md +174 -783
- package/README.pt-BR.md +1031 -0
- package/bin/qwenproxy.js +7 -3
- package/bin/update.d.ts +11 -0
- package/bin/update.js +155 -0
- package/package.json +4 -4
- package/src/api/models.ts +18 -2
- package/src/api/server.ts +18 -2
- package/src/core/accounts.ts +130 -0
- package/src/core/config.ts +96 -5
- package/src/core/model-alias.ts +17 -0
- package/src/index.ts +3 -0
- package/src/reset-cooldowns.ts +5 -1
- package/src/routes/chat/context.ts +9 -9
- package/src/routes/chat/index.ts +15 -6
- package/src/services/qwen-chat-pool.ts +4 -5
- package/src/services/qwen.ts +60 -27
- package/src/sync/claude-code.ts +41 -4
- package/src/sync/cline.ts +34 -4
- package/src/sync/codex.ts +29 -4
- package/src/sync/index.ts +98 -70
- package/src/sync/omp.ts +23 -4
- package/src/sync/opencode.ts +28 -4
- package/src/sync/utils.ts +32 -3
- package/src/sync/zed.ts +28 -4
- package/src/sync-clients.ts +3 -4
- package/src/tui/app.ts +27 -10
- package/src/tui/index.ts +3 -0
- package/src/tui/proxy-client.ts +20 -16
- package/src/tui/screen.ts +10 -3
- package/src/tui/settings.ts +2 -0
- package/src/tui/theme.ts +2 -0
- package/src/tui/types.ts +1 -0
- package/src/tui/views/accounts-view.ts +343 -23
- package/src/tui/views/chat-view.ts +302 -71
- package/src/tui/views/status-view.ts +99 -15
- package/src/tui/views/sync-view.ts +87 -32
- package/src/update-cli.ts +10 -154
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { config, type ChatMode } from "../../core/config.ts";
|
|
1
|
+
import { config, type ChatMode, isStatelessChatMode } from "../../core/config.ts";
|
|
2
2
|
import { ContextLengthExceededError, ValidationError } from "../../core/errors.ts";
|
|
3
3
|
import { getModelContextWindow } from "../../core/model-registry.ts";
|
|
4
4
|
import {
|
|
@@ -61,16 +61,16 @@ export async function buildFinalContext(
|
|
|
61
61
|
|
|
62
62
|
const modelContextWindow = getModelContextWindow(modelId);
|
|
63
63
|
const useThreadNative = true;
|
|
64
|
-
const
|
|
64
|
+
const isStateless = isStatelessChatMode(chatMode);
|
|
65
65
|
// A continuation is ANY evidence of a prior turn, not just a plain
|
|
66
66
|
// role:"assistant" message. Tool-loop clients (Zed/Cline) can send history
|
|
67
67
|
// with tool/function responses or assistant tool_calls but WITHOUT a plain
|
|
68
68
|
// assistant entry; misclassifying those as a new session forced the FULL
|
|
69
69
|
// history to be re-sent on every request (and every chat_in_progress retry)
|
|
70
70
|
// instead of the thread-native delta.
|
|
71
|
-
// In temp
|
|
72
|
-
// is always sent and no thread state is
|
|
73
|
-
const isNewSession =
|
|
71
|
+
// In stateless modes (stateless, stateless-temp, temp) EVERY request is a
|
|
72
|
+
// new chat, so the whole history is always sent and no thread state is consulted.
|
|
73
|
+
const isNewSession = isStateless
|
|
74
74
|
? true
|
|
75
75
|
: !messages.some(isContinuationMessage);
|
|
76
76
|
const completeInstructions = [systemPrompt.trim(), toolInstructions.trim()]
|
|
@@ -83,7 +83,7 @@ export async function buildFinalContext(
|
|
|
83
83
|
// OR: this is a continuation (has assistant messages in history)
|
|
84
84
|
// This prevents new IDE chats from accidentally reusing old Qwen chats
|
|
85
85
|
// while still allowing continuations without explicit session_id
|
|
86
|
-
const allowThreadReuse =
|
|
86
|
+
const allowThreadReuse = isStateless
|
|
87
87
|
? false
|
|
88
88
|
: useThreadNative && (hasExplicitConversationKey || !isNewSession); // has assistant messages = continuation of existing chat
|
|
89
89
|
|
|
@@ -91,7 +91,7 @@ export async function buildFinalContext(
|
|
|
91
91
|
// an explicit conversation key. Otherwise, generate an ephemeral ID for
|
|
92
92
|
// logging/metrics only (not used for thread reuse). Temp mode never persists
|
|
93
93
|
// a thread, so it has no session id.
|
|
94
|
-
const sessionId =
|
|
94
|
+
const sessionId = isStateless
|
|
95
95
|
? null
|
|
96
96
|
: (conversationKey || useThreadNative)
|
|
97
97
|
? deriveSessionId(
|
|
@@ -110,7 +110,7 @@ export async function buildFinalContext(
|
|
|
110
110
|
// Thread-native: send full history when Qwen has no context yet, but preserve
|
|
111
111
|
// tool-result deltas because the upstream parent chain already owns the call.
|
|
112
112
|
// Temp mode: always send the FULL history (OpenAI standard).
|
|
113
|
-
const baseActivePrompt =
|
|
113
|
+
const baseActivePrompt = isStateless
|
|
114
114
|
? prompt
|
|
115
115
|
: (!existingThread && !hasTrailingToolResult ? prompt : currentPrompt) ||
|
|
116
116
|
prompt;
|
|
@@ -179,7 +179,7 @@ export async function buildFinalContext(
|
|
|
179
179
|
isNewSession,
|
|
180
180
|
useThreadNative,
|
|
181
181
|
// Thread state is only persisted in thread mode (temp chats are ephemeral).
|
|
182
|
-
updateLogicalThread:
|
|
182
|
+
updateLogicalThread: isStateless ? false : useThreadNative,
|
|
183
183
|
chatMode,
|
|
184
184
|
isThinkingModel,
|
|
185
185
|
estimatedTokens,
|
package/src/routes/chat/index.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
handleChatCompletionsError,
|
|
21
21
|
type AssistantCompleteEvent,
|
|
22
22
|
} from "./streaming.ts";
|
|
23
|
-
import { config, type ChatMode } from "../../core/config.ts";
|
|
23
|
+
import { config, type ChatMode, normalizeChatMode } from "../../core/config.ts";
|
|
24
24
|
import { logger } from "../../core/logger.ts";
|
|
25
25
|
import { metrics } from "../../core/metrics.ts";
|
|
26
26
|
import { getContextMeterHeaders, type ContextMeterMode } from "../../services/context-meter.ts";
|
|
@@ -50,11 +50,20 @@ function formatTimingHeader(timings: Record<string, number>): string {
|
|
|
50
50
|
* else silently uses the configured default.
|
|
51
51
|
*/
|
|
52
52
|
function resolveChatMode(headerValue: string | undefined): ChatMode {
|
|
53
|
-
if (headerValue
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
if (headerValue) {
|
|
54
|
+
const m = headerValue.trim().toLowerCase().replace(/_/g, "-");
|
|
55
|
+
if (
|
|
56
|
+
m === "thread" ||
|
|
57
|
+
m === "thread-temp" ||
|
|
58
|
+
m === "temp-thread" ||
|
|
59
|
+
m === "stateless" ||
|
|
60
|
+
m === "stateles" ||
|
|
61
|
+
m === "stateless-temp" ||
|
|
62
|
+
m === "stateles-temp" ||
|
|
63
|
+
m === "temp"
|
|
64
|
+
) {
|
|
65
|
+
return normalizeChatMode(m);
|
|
66
|
+
}
|
|
58
67
|
}
|
|
59
68
|
return config.qwen.chatMode;
|
|
60
69
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import crypto from "crypto";
|
|
2
2
|
import { getQwenHeaders, isAuthMockEnabled } from "./auth-playwright.ts";
|
|
3
|
-
import { config, type ChatMode } from "../core/config.ts";
|
|
3
|
+
import { config, type ChatMode, isLocalChatMode, isStatelessChatMode } from "../core/config.ts";
|
|
4
4
|
import { logger, isToolcallDebugEnabled } from "../core/logger.ts";
|
|
5
5
|
import {
|
|
6
6
|
computeQuotaCooldownMs,
|
|
@@ -83,9 +83,8 @@ export function buildChatNewBody(
|
|
|
83
83
|
project_id: "",
|
|
84
84
|
timestamp: Date.now(),
|
|
85
85
|
chat_type: "t2t",
|
|
86
|
-
// thread
|
|
87
|
-
chat_mode:
|
|
88
|
-
chatMode === "temp" || chatMode === "temp-thread" ? "local" : "normal",
|
|
86
|
+
// normal: thread & stateless (persisted in Qwen); local: thread-temp & stateless-temp (ephemeral)
|
|
87
|
+
chat_mode: isLocalChatMode(chatMode) ? "local" : "normal",
|
|
89
88
|
};
|
|
90
89
|
}
|
|
91
90
|
|
|
@@ -201,7 +200,7 @@ export async function acquireNewQwenChatSession(
|
|
|
201
200
|
accountId?: string,
|
|
202
201
|
chatMode: ChatMode = "thread",
|
|
203
202
|
): Promise<{ chatId: string; leasedFromPool: boolean }> {
|
|
204
|
-
if (isQwenChatPoolEnabled() && chatMode
|
|
203
|
+
if (isQwenChatPoolEnabled() && !isStatelessChatMode(chatMode)) {
|
|
205
204
|
const key = chatPoolKey(accountId, model);
|
|
206
205
|
const pooled = precreatedChatSessions.get(key);
|
|
207
206
|
const chatId = pooled?.shift();
|
package/src/services/qwen.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from "../core/errors.ts";
|
|
13
13
|
import { buildQwenRequestHeaders } from "./qwen-headers.ts";
|
|
14
14
|
import { qwenOrigin, qwenUrl } from "./qwen-url.ts";
|
|
15
|
-
import { config, type ChatMode } from "../core/config.ts";
|
|
15
|
+
import { config, type ChatMode, isLocalChatMode } from "../core/config.ts";
|
|
16
16
|
import { logger } from "../core/logger.ts";
|
|
17
17
|
import { estimateTokenCount } from "../utils/context-truncation.ts";
|
|
18
18
|
import type {
|
|
@@ -440,6 +440,14 @@ const modelsCache = new Map<
|
|
|
440
440
|
{ models: PublicQwenModel[]; fetchedAt: number }
|
|
441
441
|
>();
|
|
442
442
|
|
|
443
|
+
export function getAnyCachedQwenModels(): PublicQwenModel[] | undefined {
|
|
444
|
+
for (const entry of modelsCache.values()) {
|
|
445
|
+
if (entry.models && entry.models.length > 0) {
|
|
446
|
+
return entry.models;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return undefined;
|
|
450
|
+
}
|
|
443
451
|
const nativeToolsDisabled = new Set<string>();
|
|
444
452
|
const disablingNativeToolsInProgress = new Set<string>();
|
|
445
453
|
const lastSyncedPersonalizationHashes = new Map<string, string>();
|
|
@@ -492,6 +500,25 @@ function setPersonalizationHashInDb(accountId: string, hash: string): void {
|
|
|
492
500
|
);
|
|
493
501
|
}
|
|
494
502
|
}
|
|
503
|
+
export function clearPersonalizationDbCache(accountId?: string): number {
|
|
504
|
+
try {
|
|
505
|
+
const db = getDatabase();
|
|
506
|
+
if (accountId) {
|
|
507
|
+
const info = db
|
|
508
|
+
.prepare("DELETE FROM personalization_cache WHERE account_id = ?")
|
|
509
|
+
.run(accountId);
|
|
510
|
+
lastSyncedPersonalizationHashes.delete(accountId);
|
|
511
|
+
activePersonalizationByAccount.delete(accountId);
|
|
512
|
+
return info.changes;
|
|
513
|
+
}
|
|
514
|
+
const info = db.prepare("DELETE FROM personalization_cache").run();
|
|
515
|
+
lastSyncedPersonalizationHashes.clear();
|
|
516
|
+
activePersonalizationByAccount.clear();
|
|
517
|
+
return info.changes;
|
|
518
|
+
} catch {
|
|
519
|
+
return 0;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
495
522
|
|
|
496
523
|
function shortContentHash(value: string): string {
|
|
497
524
|
return crypto.createHash("sha256").update(value).digest("hex").slice(0, 12);
|
|
@@ -666,23 +693,14 @@ export function buildQwenSettingsUpdatePayload(
|
|
|
666
693
|
currentSettings: any,
|
|
667
694
|
instruction: string,
|
|
668
695
|
): Record<string, unknown> {
|
|
669
|
-
// The real client (HAR networkv2) POSTs ONLY `{personalization: {...}}` to
|
|
670
|
-
// /api/v2/users/user/settings/update. Live probes confirmed the personalization
|
|
671
|
-
// object accepts the GET-personalization spread + enable_for_new_chat, but the
|
|
672
|
-
// FULL-settings spread this used to send (ui/memory/tools_enabled + every GET
|
|
673
|
-
// field like tts_speaker_v2, code_settings, manage_cookies) is rejected with
|
|
674
|
-
// RequestValidationError. Safe-settings are applied by disableNativeTools as
|
|
675
|
-
// their own combined partial POST (probe-accepted). NOTE: the persistent
|
|
676
|
-
// RequestValidationError that haunted the sync was NOT the payload — it was a
|
|
677
|
-
// missing Content-Type header (attemptPost received the raw getQwenHeaders
|
|
678
|
-
// map); the body was not parsed as a JSON object ("Field '': Input should be
|
|
679
|
-
// a valid dictionary...").
|
|
680
696
|
const currentPersonalization =
|
|
681
697
|
currentSettings?.personalization &&
|
|
682
698
|
typeof currentSettings.personalization === "object"
|
|
683
699
|
? currentSettings.personalization
|
|
684
700
|
: {};
|
|
685
701
|
|
|
702
|
+
const hasInstruction = instruction.trim().length > 0;
|
|
703
|
+
|
|
686
704
|
return {
|
|
687
705
|
personalization: {
|
|
688
706
|
...currentPersonalization,
|
|
@@ -693,7 +711,7 @@ export function buildQwenSettingsUpdatePayload(
|
|
|
693
711
|
: currentPersonalization.description,
|
|
694
712
|
style: null,
|
|
695
713
|
instruction,
|
|
696
|
-
enable_for_new_chat:
|
|
714
|
+
enable_for_new_chat: hasInstruction,
|
|
697
715
|
},
|
|
698
716
|
};
|
|
699
717
|
}
|
|
@@ -1519,7 +1537,12 @@ export async function syncQwenRequestPersonalization(
|
|
|
1519
1537
|
}
|
|
1520
1538
|
|
|
1521
1539
|
// 2. Check DB cache (survives restarts) (skipped on forceSync)
|
|
1522
|
-
|
|
1540
|
+
const isEmptyInstruction = instruction.trim().length === 0;
|
|
1541
|
+
|
|
1542
|
+
// 2. Check DB cache (survives restarts) (skipped on forceSync)
|
|
1543
|
+
// For empty instructions, do not blindly trust DB cache without verifying
|
|
1544
|
+
// because external agents or web sessions might have altered personalization.
|
|
1545
|
+
if (!bypassCache && syncHash && !cachedHash && !isEmptyInstruction) {
|
|
1523
1546
|
const dbHash = getPersonalizationHashFromDb(cacheKey);
|
|
1524
1547
|
if (dbHash === syncHash) {
|
|
1525
1548
|
lastSyncedPersonalizationHashes.set(cacheKey, syncHash);
|
|
@@ -1528,7 +1551,6 @@ export async function syncQwenRequestPersonalization(
|
|
|
1528
1551
|
return true;
|
|
1529
1552
|
}
|
|
1530
1553
|
}
|
|
1531
|
-
|
|
1532
1554
|
let existing = { chars: null, bytes: null, hash: null } as ReturnType<
|
|
1533
1555
|
typeof textSize
|
|
1534
1556
|
>;
|
|
@@ -1544,7 +1566,9 @@ export async function syncQwenRequestPersonalization(
|
|
|
1544
1566
|
);
|
|
1545
1567
|
currentSettings = existingJson?.data ?? null;
|
|
1546
1568
|
payload = buildQwenSettingsUpdatePayload(currentSettings, instruction);
|
|
1547
|
-
|
|
1569
|
+
const existingInstruction = existingJson?.data?.personalization?.instruction;
|
|
1570
|
+
const existingEnabled = existingJson?.data?.personalization?.enable_for_new_chat === true;
|
|
1571
|
+
existing = textSize(existingInstruction);
|
|
1548
1572
|
const existingSafeSettingsApplied =
|
|
1549
1573
|
existingJson?.data?.ui?.largeTextAsFile === false &&
|
|
1550
1574
|
existingJson?.data?.ui?.splitLargeChunks === false &&
|
|
@@ -1554,8 +1578,12 @@ export async function syncQwenRequestPersonalization(
|
|
|
1554
1578
|
existingJson?.data?.memory?.enable_history_memory === false &&
|
|
1555
1579
|
existingJson?.data?.tools_enabled?.web_search === false &&
|
|
1556
1580
|
existingJson?.data?.tools_enabled?.code_interpreter === false;
|
|
1557
|
-
|
|
1558
|
-
|
|
1581
|
+
const isEmptyAndCleared =
|
|
1582
|
+
isEmptyInstruction &&
|
|
1583
|
+
(!existingInstruction || existingInstruction.trim().length === 0) &&
|
|
1584
|
+
!existingEnabled;
|
|
1585
|
+
const isContentMatched = existing.hash !== null && existing.hash === sent.hash;
|
|
1586
|
+
if ((isContentMatched || isEmptyAndCleared) && existingSafeSettingsApplied) {
|
|
1559
1587
|
setPersonalizationHashInDb(cacheKey, syncHash);
|
|
1560
1588
|
rememberActivePersonalization(
|
|
1561
1589
|
cacheKey,
|
|
@@ -1669,6 +1697,7 @@ export async function syncQwenRequestPersonalization(
|
|
|
1669
1697
|
typeof textSize
|
|
1670
1698
|
>;
|
|
1671
1699
|
|
|
1700
|
+
let verifyData: any = null;
|
|
1672
1701
|
if (config.qwen.personalizationVerifyGet) {
|
|
1673
1702
|
const { json: verifyJson } =
|
|
1674
1703
|
await requestQwenPersonalizationInBrowser(
|
|
@@ -1677,11 +1706,18 @@ export async function syncQwenRequestPersonalization(
|
|
|
1677
1706
|
"/api/v2/users/user/settings",
|
|
1678
1707
|
requestHeaders,
|
|
1679
1708
|
);
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
const
|
|
1709
|
+
verifyData = verifyJson?.data?.personalization;
|
|
1710
|
+
stored = textSize(verifyData?.instruction);
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
const matchReturned =
|
|
1714
|
+
(isEmptyInstruction && (!returnedInstruction || returned.chars === 0)) ||
|
|
1715
|
+
(returned.hash !== null && returned.hash === sent.hash);
|
|
1716
|
+
const matchStored =
|
|
1717
|
+
stored.hash === null
|
|
1718
|
+
? null
|
|
1719
|
+
: (isEmptyInstruction && (!verifyData?.instruction || stored.chars === 0)) ||
|
|
1720
|
+
stored.hash === sent.hash;
|
|
1685
1721
|
const applied = matchReturned || matchStored === true;
|
|
1686
1722
|
if (syncHash && applied) {
|
|
1687
1723
|
lastSyncedPersonalizationHashes.set(cacheKey, syncHash);
|
|
@@ -2757,10 +2793,7 @@ async function createQwenStreamInternal(
|
|
|
2757
2793
|
chatId: chatSessionId || null,
|
|
2758
2794
|
parentId: actualParentId ?? "",
|
|
2759
2795
|
chat_id: chatSessionId || null,
|
|
2760
|
-
chat_mode:
|
|
2761
|
-
options?.chatMode === "temp" || options?.chatMode === "temp-thread"
|
|
2762
|
-
? "local"
|
|
2763
|
-
: "normal",
|
|
2796
|
+
chat_mode: isLocalChatMode(options?.chatMode) ? "local" : "normal",
|
|
2764
2797
|
model: model,
|
|
2765
2798
|
parent_id: actualParentId,
|
|
2766
2799
|
messages: [
|
package/src/sync/claude-code.ts
CHANGED
|
@@ -33,12 +33,14 @@ export function syncClaudeCode(options: SyncOptions): ClientSyncResult {
|
|
|
33
33
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: "qwen3.7-plus",
|
|
34
34
|
ANTHROPIC_DEFAULT_OPUS_MODEL: model,
|
|
35
35
|
CLAUDE_CODE_MAX_CONTEXT_TOKENS: "1000000",
|
|
36
|
+
CLAUDE_CODE_DISABLE_ARTIFACT: "1",
|
|
36
37
|
};
|
|
37
38
|
|
|
38
39
|
const updatedSettings = {
|
|
39
40
|
...existingSettings,
|
|
40
41
|
env,
|
|
41
42
|
model,
|
|
43
|
+
enableArtifact: false,
|
|
42
44
|
};
|
|
43
45
|
|
|
44
46
|
fs.writeFileSync(filePath, JSON.stringify(updatedSettings, null, 2) + "\n", "utf-8");
|
|
@@ -63,13 +65,48 @@ export function syncClaudeCode(options: SyncOptions): ClientSyncResult {
|
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
export function restoreClaudeCode(filePath: string, backupPath?: string): ClientSyncResult {
|
|
66
|
-
const
|
|
68
|
+
const restoredFromBackup = restoreFromBackup(filePath, backupPath);
|
|
69
|
+
|
|
70
|
+
let manuallyCleaned = false;
|
|
71
|
+
if (fs.existsSync(filePath)) {
|
|
72
|
+
try {
|
|
73
|
+
const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
74
|
+
if (data.env && (data.env.ANTHROPIC_BASE_URL?.includes("7936") || data.env.ANTHROPIC_AUTH_TOKEN === "sk-qwenproxy-local" || data.env.ANTHROPIC_MODEL?.includes("qwen"))) {
|
|
75
|
+
delete data.env.ANTHROPIC_BASE_URL;
|
|
76
|
+
delete data.env.ANTHROPIC_AUTH_TOKEN;
|
|
77
|
+
delete data.env.ANTHROPIC_MODEL;
|
|
78
|
+
delete data.env.ANTHROPIC_CUSTOM_MODEL_OPTION;
|
|
79
|
+
delete data.env.ANTHROPIC_CUSTOM_MODEL_OPTION_NAME;
|
|
80
|
+
delete data.env.ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION;
|
|
81
|
+
delete data.env.ANTHROPIC_DEFAULT_SONNET_MODEL;
|
|
82
|
+
delete data.env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
|
|
83
|
+
delete data.env.ANTHROPIC_DEFAULT_OPUS_MODEL;
|
|
84
|
+
delete data.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS;
|
|
85
|
+
delete data.env.CLAUDE_CODE_DISABLE_ARTIFACT;
|
|
86
|
+
delete data.enableArtifact;
|
|
87
|
+
if (data.model && data.model.toLowerCase().includes("qwen")) {
|
|
88
|
+
delete data.model;
|
|
89
|
+
}
|
|
90
|
+
if (Object.keys(data.env).length === 0) {
|
|
91
|
+
delete data.env;
|
|
92
|
+
}
|
|
93
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
94
|
+
manuallyCleaned = true;
|
|
95
|
+
}
|
|
96
|
+
} catch {}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const success = restoredFromBackup || manuallyCleaned;
|
|
67
100
|
return {
|
|
68
101
|
client: "claude-code",
|
|
69
102
|
filePath,
|
|
70
103
|
backupPath,
|
|
71
|
-
success
|
|
72
|
-
action:
|
|
73
|
-
message:
|
|
104
|
+
success,
|
|
105
|
+
action: success ? "restored" : "failed",
|
|
106
|
+
message: success
|
|
107
|
+
? restoredFromBackup
|
|
108
|
+
? "Restored Claude Code settings from backup"
|
|
109
|
+
: "Removed QwenProxy configuration from Claude Code settings"
|
|
110
|
+
: "Backup file not found",
|
|
74
111
|
};
|
|
75
112
|
}
|
package/src/sync/cline.ts
CHANGED
|
@@ -98,13 +98,43 @@ export function syncCline(options: SyncOptions): ClientSyncResult {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
export function restoreCline(filePath: string, backupPath?: string): ClientSyncResult {
|
|
101
|
-
const
|
|
101
|
+
const restoredFromBackup = restoreFromBackup(filePath, backupPath);
|
|
102
|
+
|
|
103
|
+
let manuallyCleaned = false;
|
|
104
|
+
if (fs.existsSync(filePath)) {
|
|
105
|
+
try {
|
|
106
|
+
const db = new Database(filePath);
|
|
107
|
+
const rows = db
|
|
108
|
+
.prepare("SELECT key, value FROM ItemTable WHERE key = 'saoudrizwan.claude-dev' OR key = 'ZooCodeOrganization.zoo-code'")
|
|
109
|
+
.all() as Array<{ key: string; value: string }>;
|
|
110
|
+
|
|
111
|
+
for (const row of rows) {
|
|
112
|
+
try {
|
|
113
|
+
const parsed = JSON.parse(row.value);
|
|
114
|
+
if (parsed.openAiBaseUrl?.includes("7936") || parsed.openAiModelId?.includes("qwen")) {
|
|
115
|
+
delete parsed.openAiBaseUrl;
|
|
116
|
+
delete parsed.openAiApiKey;
|
|
117
|
+
delete parsed.openAiModelId;
|
|
118
|
+
db.prepare("UPDATE ItemTable SET value = ? WHERE key = ?").run(JSON.stringify(parsed), row.key);
|
|
119
|
+
manuallyCleaned = true;
|
|
120
|
+
}
|
|
121
|
+
} catch {}
|
|
122
|
+
}
|
|
123
|
+
db.close();
|
|
124
|
+
} catch {}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const success = restoredFromBackup || manuallyCleaned;
|
|
102
128
|
return {
|
|
103
129
|
client: "cline",
|
|
104
130
|
filePath,
|
|
105
131
|
backupPath,
|
|
106
|
-
success
|
|
107
|
-
action:
|
|
108
|
-
message:
|
|
132
|
+
success,
|
|
133
|
+
action: success ? "restored" : "failed",
|
|
134
|
+
message: success
|
|
135
|
+
? restoredFromBackup
|
|
136
|
+
? "Restored Cline settings from backup"
|
|
137
|
+
: "Removed QwenProxy configuration from Cline settings"
|
|
138
|
+
: "Backup file not found",
|
|
109
139
|
};
|
|
110
140
|
}
|
package/src/sync/codex.ts
CHANGED
|
@@ -111,13 +111,38 @@ experimental_bearer_token = "${apiKey}"
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
export function restoreCodex(filePath: string, backupPath?: string): ClientSyncResult {
|
|
114
|
-
const
|
|
114
|
+
const restoredFromBackup = restoreFromBackup(filePath, backupPath);
|
|
115
|
+
|
|
116
|
+
// If backup was restored but still had qwenproxy (or if no backup was found),
|
|
117
|
+
// strip the QwenProxy provider block cleanly so the file is guaranteed un-synced.
|
|
118
|
+
let manuallyCleaned = false;
|
|
119
|
+
if (fs.existsSync(filePath)) {
|
|
120
|
+
try {
|
|
121
|
+
let content = fs.readFileSync(filePath, "utf-8");
|
|
122
|
+
if (content.includes("[model_providers.qwenproxy]") || /^model_provider\s*=\s*["']qwenproxy["']/m.test(content)) {
|
|
123
|
+
const providerRegex = /\[model_providers\.qwenproxy\][\s\S]*?(?=(?:^\[|\Z))/m;
|
|
124
|
+
content = content.replace(providerRegex, "").trimEnd();
|
|
125
|
+
content = content.replace(/^model_provider\s*=\s*["']qwenproxy["']\r?\n?/m, "");
|
|
126
|
+
if (/^model\s*=\s*["']qwen/m.test(content)) {
|
|
127
|
+
content = content.replace(/^model\s*=\s*["']qwen[^"']*["']\r?\n?/m, "");
|
|
128
|
+
}
|
|
129
|
+
fs.writeFileSync(filePath, content.trimEnd() + "\n", "utf-8");
|
|
130
|
+
manuallyCleaned = true;
|
|
131
|
+
}
|
|
132
|
+
} catch {}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const success = restoredFromBackup || manuallyCleaned;
|
|
115
136
|
return {
|
|
116
137
|
client: "codex",
|
|
117
138
|
filePath,
|
|
118
139
|
backupPath,
|
|
119
|
-
success
|
|
120
|
-
action:
|
|
121
|
-
message:
|
|
140
|
+
success,
|
|
141
|
+
action: success ? "restored" : "failed",
|
|
142
|
+
message: success
|
|
143
|
+
? restoredFromBackup
|
|
144
|
+
? "Restored Codex config from backup"
|
|
145
|
+
: "Removed QwenProxy configuration from Codex config"
|
|
146
|
+
: "Backup file not found",
|
|
122
147
|
};
|
|
123
148
|
}
|