qwenproxy-cli 1.1.0 → 1.2.1
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 +1 -3
- package/README.md +1 -3
- package/README.pt-BR.md +2 -4
- package/bin/qwenproxy.js +7 -3
- package/bin/update.d.ts +11 -0
- package/bin/update.js +155 -0
- package/package.json +3 -3
- package/src/api/models.ts +18 -2
- package/src/core/config.ts +1 -1
- package/src/core/model-alias.ts +17 -0
- package/src/reset-cooldowns.ts +5 -1
- package/src/services/captcha-coordinator.ts +1 -1
- package/src/services/playwright.ts +22 -11
- package/src/services/qwen.ts +58 -22
- 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/tools/parser.ts +16 -11
- package/src/tui/app.ts +19 -1
- package/src/tui/proxy-client.ts +17 -14
- package/src/tui/theme.ts +1 -0
- package/src/tui/views/chat-view.ts +97 -61
- package/src/tui/views/status-view.ts +68 -15
- package/src/tui/views/sync-view.ts +87 -32
- package/src/update-cli.ts +10 -154
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
|
}
|
package/src/sync/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
ClientSyncResult,
|
|
11
11
|
SyncAllOptions,
|
|
12
12
|
SyncClientName,
|
|
13
|
+
SyncRecord,
|
|
13
14
|
SyncStateFile,
|
|
14
15
|
} from "./types.ts";
|
|
15
16
|
import { syncClaudeCode, restoreClaudeCode } from "./claude-code.ts";
|
|
@@ -23,6 +24,28 @@ import { syncCline, restoreCline } from "./cline.ts";
|
|
|
23
24
|
import { syncZed, restoreZed } from "./zed.ts";
|
|
24
25
|
import { syncAider, restoreAider } from "./aider.ts";
|
|
25
26
|
|
|
27
|
+
export {
|
|
28
|
+
syncClaudeCode,
|
|
29
|
+
restoreClaudeCode,
|
|
30
|
+
syncCodex,
|
|
31
|
+
restoreCodex,
|
|
32
|
+
syncOpenCode,
|
|
33
|
+
restoreOpenCode,
|
|
34
|
+
syncOmp,
|
|
35
|
+
restoreOmp,
|
|
36
|
+
syncHermes,
|
|
37
|
+
restoreHermes,
|
|
38
|
+
syncOpenClaw,
|
|
39
|
+
restoreOpenClaw,
|
|
40
|
+
syncKilo,
|
|
41
|
+
restoreKilo,
|
|
42
|
+
syncCline,
|
|
43
|
+
restoreCline,
|
|
44
|
+
syncZed,
|
|
45
|
+
restoreZed,
|
|
46
|
+
syncAider,
|
|
47
|
+
restoreAider,
|
|
48
|
+
};
|
|
26
49
|
export function resolveApiKey(overrideKey?: string, configKey?: string): string {
|
|
27
50
|
if (overrideKey && overrideKey.trim().length > 0) {
|
|
28
51
|
return overrideKey.trim();
|
|
@@ -646,16 +669,29 @@ export function syncAllClients(options: SyncAllOptions = {}): SyncAllResult {
|
|
|
646
669
|
}
|
|
647
670
|
}
|
|
648
671
|
|
|
649
|
-
// Persist sync state
|
|
672
|
+
// Persist sync state (merge with existing state if present)
|
|
650
673
|
try {
|
|
651
674
|
fs.mkdirSync(path.dirname(stateFilePath), { recursive: true });
|
|
675
|
+
let existingClients: SyncStateFile["clients"] = {};
|
|
676
|
+
if (fs.existsSync(stateFilePath)) {
|
|
677
|
+
try {
|
|
678
|
+
const raw = fs.readFileSync(stateFilePath, "utf-8");
|
|
679
|
+
const parsed = JSON.parse(raw);
|
|
680
|
+
if (parsed?.clients && typeof parsed.clients === "object") {
|
|
681
|
+
existingClients = parsed.clients;
|
|
682
|
+
}
|
|
683
|
+
} catch {}
|
|
684
|
+
}
|
|
652
685
|
const stateContent: SyncStateFile = {
|
|
653
686
|
version: 1,
|
|
654
687
|
updatedAt: new Date().toISOString(),
|
|
655
688
|
apiKey,
|
|
656
689
|
port,
|
|
657
690
|
host,
|
|
658
|
-
clients:
|
|
691
|
+
clients: {
|
|
692
|
+
...existingClients,
|
|
693
|
+
...stateRecords,
|
|
694
|
+
},
|
|
659
695
|
};
|
|
660
696
|
fs.writeFileSync(stateFilePath, JSON.stringify(stateContent, null, 2) + "\n", "utf-8");
|
|
661
697
|
} catch (err) {
|
|
@@ -670,87 +706,79 @@ export interface RestoreAllResult {
|
|
|
670
706
|
details: ClientSyncResult[];
|
|
671
707
|
}
|
|
672
708
|
|
|
673
|
-
export
|
|
709
|
+
export interface RestoreAllOptions {
|
|
710
|
+
stateFilePath?: string;
|
|
711
|
+
targets?: SyncClientName[];
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
export function restoreAllClients(options: RestoreAllOptions = {}): RestoreAllResult {
|
|
715
|
+
const defaultPaths = getDefaultPaths();
|
|
674
716
|
const stateFilePath = options.stateFilePath || getDefaultStateFilePath();
|
|
675
717
|
const details: ClientSyncResult[] = [];
|
|
676
718
|
let restoredCount = 0;
|
|
677
719
|
|
|
678
|
-
|
|
679
|
-
|
|
720
|
+
let state: SyncStateFile | null = null;
|
|
721
|
+
if (fs.existsSync(stateFilePath)) {
|
|
722
|
+
try {
|
|
723
|
+
const raw = fs.readFileSync(stateFilePath, "utf-8");
|
|
724
|
+
state = JSON.parse(raw);
|
|
725
|
+
} catch {}
|
|
680
726
|
}
|
|
681
727
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
if (state.clients.claudeCode?.backupPath) {
|
|
687
|
-
const res = restoreClaudeCode(state.clients.claudeCode.filePath, state.clients.claudeCode.backupPath);
|
|
688
|
-
details.push(res);
|
|
689
|
-
if (res.success) restoredCount++;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
if (state.clients.codex?.backupPath) {
|
|
693
|
-
const res = restoreCodex(state.clients.codex.filePath, state.clients.codex.backupPath);
|
|
694
|
-
details.push(res);
|
|
695
|
-
if (res.success) restoredCount++;
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
if (state.clients.openCode?.backupPath) {
|
|
699
|
-
const res = restoreOpenCode(state.clients.openCode.filePath, state.clients.openCode.backupPath);
|
|
700
|
-
details.push(res);
|
|
701
|
-
if (res.success) restoredCount++;
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
if (state.clients.omp?.backupPath) {
|
|
705
|
-
const res = restoreOmp(state.clients.omp.filePath, state.clients.omp.backupPath);
|
|
706
|
-
details.push(res);
|
|
707
|
-
if (res.success) restoredCount++;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
if (state.clients.hermes?.backupPath) {
|
|
711
|
-
const res = restoreHermes(state.clients.hermes.filePath, state.clients.hermes.backupPath);
|
|
712
|
-
details.push(res);
|
|
713
|
-
if (res.success) restoredCount++;
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
if (state.clients.openClaw?.backupPath) {
|
|
717
|
-
const res = restoreOpenClaw(state.clients.openClaw.filePath, state.clients.openClaw.backupPath);
|
|
718
|
-
details.push(res);
|
|
719
|
-
if (res.success) restoredCount++;
|
|
720
|
-
}
|
|
728
|
+
const shouldRestore = (client: SyncClientName) => {
|
|
729
|
+
if (!options.targets || options.targets.length === 0) return true;
|
|
730
|
+
return options.targets.includes(client);
|
|
731
|
+
};
|
|
721
732
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
733
|
+
const restoreClientsList: Array<{
|
|
734
|
+
id: SyncClientName;
|
|
735
|
+
stateKey: keyof SyncStateFile["clients"];
|
|
736
|
+
defaultPath: string;
|
|
737
|
+
stateRecord?: SyncRecord;
|
|
738
|
+
restoreFn: (filePath: string, backupPath?: string) => ClientSyncResult;
|
|
739
|
+
}> = [
|
|
740
|
+
{ id: "claude-code", stateKey: "claudeCode", defaultPath: defaultPaths.claudeCode, stateRecord: state?.clients?.claudeCode, restoreFn: restoreClaudeCode },
|
|
741
|
+
{ id: "codex", stateKey: "codex", defaultPath: defaultPaths.codex, stateRecord: state?.clients?.codex, restoreFn: restoreCodex },
|
|
742
|
+
{ id: "opencode", stateKey: "openCode", defaultPath: defaultPaths.openCode, stateRecord: state?.clients?.openCode, restoreFn: restoreOpenCode },
|
|
743
|
+
{ id: "omp", stateKey: "omp", defaultPath: defaultPaths.omp, stateRecord: state?.clients?.omp, restoreFn: restoreOmp },
|
|
744
|
+
{ id: "hermes", stateKey: "hermes", defaultPath: defaultPaths.hermes, stateRecord: state?.clients?.hermes, restoreFn: restoreHermes },
|
|
745
|
+
{ id: "openclaw", stateKey: "openClaw", defaultPath: defaultPaths.openClaw, stateRecord: state?.clients?.openClaw, restoreFn: restoreOpenClaw },
|
|
746
|
+
{ id: "kilo", stateKey: "kilo", defaultPath: defaultPaths.kilo, stateRecord: state?.clients?.kilo, restoreFn: restoreKilo },
|
|
747
|
+
{ id: "cline", stateKey: "cline", defaultPath: defaultPaths.cline, stateRecord: state?.clients?.cline, restoreFn: restoreCline },
|
|
748
|
+
{ id: "zed", stateKey: "zed", defaultPath: defaultPaths.zed, stateRecord: state?.clients?.zed, restoreFn: restoreZed },
|
|
749
|
+
{ id: "aider", stateKey: "aider", defaultPath: defaultPaths.aider, stateRecord: state?.clients?.aider, restoreFn: restoreAider },
|
|
750
|
+
];
|
|
727
751
|
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
}
|
|
752
|
+
for (const c of restoreClientsList) {
|
|
753
|
+
if (!shouldRestore(c.id)) continue;
|
|
754
|
+
const filePath = c.stateRecord?.filePath || c.defaultPath;
|
|
755
|
+
const backupPath = c.stateRecord?.backupPath;
|
|
733
756
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
if (res.success) restoredCount++;
|
|
738
|
-
}
|
|
757
|
+
const canAttempt = options.stateFilePath
|
|
758
|
+
? Boolean(c.stateRecord)
|
|
759
|
+
: Boolean(c.stateRecord || fs.existsSync(filePath));
|
|
739
760
|
|
|
740
|
-
if (
|
|
741
|
-
const res =
|
|
761
|
+
if (canAttempt) {
|
|
762
|
+
const res = c.restoreFn(filePath, backupPath);
|
|
742
763
|
details.push(res);
|
|
743
|
-
if (res.success)
|
|
764
|
+
if (res.success) {
|
|
765
|
+
restoredCount++;
|
|
766
|
+
if (state?.clients) {
|
|
767
|
+
delete state.clients[c.stateKey];
|
|
768
|
+
delete (state.clients as any)[c.id];
|
|
769
|
+
}
|
|
770
|
+
}
|
|
744
771
|
}
|
|
745
|
-
|
|
746
|
-
|
|
772
|
+
}
|
|
773
|
+
// Update or delete state file
|
|
774
|
+
if (stateFilePath && fs.existsSync(stateFilePath)) {
|
|
747
775
|
try {
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
776
|
+
if (state && state.clients && Object.keys(state.clients).length > 0) {
|
|
777
|
+
fs.writeFileSync(stateFilePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
778
|
+
} else {
|
|
779
|
+
fs.unlinkSync(stateFilePath);
|
|
780
|
+
}
|
|
781
|
+
} catch {}
|
|
754
782
|
}
|
|
755
783
|
|
|
756
784
|
return { restoredCount, details };
|
package/src/sync/omp.ts
CHANGED
|
@@ -99,13 +99,32 @@ export function syncOmp(options: SyncOptions): ClientSyncResult {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
export function restoreOmp(filePath: string, backupPath?: string): ClientSyncResult {
|
|
102
|
-
const
|
|
102
|
+
const restoredFromBackup = restoreFromBackup(filePath, backupPath);
|
|
103
|
+
|
|
104
|
+
let manuallyCleaned = false;
|
|
105
|
+
if (fs.existsSync(filePath)) {
|
|
106
|
+
try {
|
|
107
|
+
let content = fs.readFileSync(filePath, "utf-8");
|
|
108
|
+
if (content.includes("qwenproxy:")) {
|
|
109
|
+
const regex = /^\s*qwenproxy:\s*\r?\n(?:^[ \t].*\r?\n?)*/m;
|
|
110
|
+
content = content.replace(regex, "");
|
|
111
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
112
|
+
manuallyCleaned = true;
|
|
113
|
+
}
|
|
114
|
+
} catch {}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const success = restoredFromBackup || manuallyCleaned;
|
|
103
118
|
return {
|
|
104
119
|
client: "omp",
|
|
105
120
|
filePath,
|
|
106
121
|
backupPath,
|
|
107
|
-
success
|
|
108
|
-
action:
|
|
109
|
-
message:
|
|
122
|
+
success,
|
|
123
|
+
action: success ? "restored" : "failed",
|
|
124
|
+
message: success
|
|
125
|
+
? restoredFromBackup
|
|
126
|
+
? "Restored OMP models config from backup"
|
|
127
|
+
: "Removed QwenProxy configuration from OMP config"
|
|
128
|
+
: "Backup file not found",
|
|
110
129
|
};
|
|
111
130
|
}
|
package/src/sync/opencode.ts
CHANGED
|
@@ -206,13 +206,37 @@ export function syncOpenCode(options: SyncOptions): ClientSyncResult {
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
export function restoreOpenCode(filePath: string, backupPath?: string): ClientSyncResult {
|
|
209
|
-
const
|
|
209
|
+
const restoredFromBackup = restoreFromBackup(filePath, backupPath);
|
|
210
|
+
|
|
211
|
+
let manuallyCleaned = false;
|
|
212
|
+
if (fs.existsSync(filePath)) {
|
|
213
|
+
try {
|
|
214
|
+
const data = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
215
|
+
if (data.provider && data.provider.qwenproxy) {
|
|
216
|
+
delete data.provider.qwenproxy;
|
|
217
|
+
if (data.model && data.model.includes("qwen")) {
|
|
218
|
+
delete data.model;
|
|
219
|
+
}
|
|
220
|
+
if (Object.keys(data.provider).length === 0) {
|
|
221
|
+
delete data.provider;
|
|
222
|
+
}
|
|
223
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
224
|
+
manuallyCleaned = true;
|
|
225
|
+
}
|
|
226
|
+
} catch {}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const success = restoredFromBackup || manuallyCleaned;
|
|
210
230
|
return {
|
|
211
231
|
client: "opencode",
|
|
212
232
|
filePath,
|
|
213
233
|
backupPath,
|
|
214
|
-
success
|
|
215
|
-
action:
|
|
216
|
-
message:
|
|
234
|
+
success,
|
|
235
|
+
action: success ? "restored" : "failed",
|
|
236
|
+
message: success
|
|
237
|
+
? restoredFromBackup
|
|
238
|
+
? "Restored OpenCode config from backup"
|
|
239
|
+
: "Removed QwenProxy configuration from OpenCode config"
|
|
240
|
+
: "Backup file not found",
|
|
217
241
|
};
|
|
218
242
|
}
|
package/src/sync/utils.ts
CHANGED
|
@@ -13,13 +13,42 @@ export function createTimestampBackup(filePath: string): string {
|
|
|
13
13
|
return backupPath;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
export function findLatestBackup(filePath: string): string | undefined {
|
|
17
|
+
const dir = path.dirname(filePath);
|
|
18
|
+
const ext = path.extname(filePath);
|
|
19
|
+
const base = path.basename(filePath, ext);
|
|
20
|
+
if (!fs.existsSync(dir)) return undefined;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const files = fs.readdirSync(dir);
|
|
24
|
+
const candidates = files
|
|
25
|
+
.filter((f) => f.startsWith(base) && f.includes("qwenproxy") && f.endsWith(".bak"))
|
|
26
|
+
.map((f) => path.join(dir, f))
|
|
27
|
+
.sort((a, b) => {
|
|
28
|
+
try {
|
|
29
|
+
return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs;
|
|
30
|
+
} catch {
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return candidates[0];
|
|
36
|
+
} catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
16
41
|
export function restoreFromBackup(filePath: string, backupPath?: string): boolean {
|
|
17
|
-
|
|
42
|
+
const targetBackup = (backupPath && fs.existsSync(backupPath))
|
|
43
|
+
? backupPath
|
|
44
|
+
: findLatestBackup(filePath);
|
|
45
|
+
|
|
46
|
+
if (!targetBackup || !fs.existsSync(targetBackup)) {
|
|
18
47
|
return false;
|
|
19
48
|
}
|
|
20
|
-
fs.copyFileSync(
|
|
49
|
+
fs.copyFileSync(targetBackup, filePath);
|
|
21
50
|
try {
|
|
22
|
-
fs.unlinkSync(
|
|
51
|
+
fs.unlinkSync(targetBackup);
|
|
23
52
|
} catch {
|
|
24
53
|
// Ignore cleanup error
|
|
25
54
|
}
|
package/src/sync/zed.ts
CHANGED
|
@@ -198,13 +198,37 @@ export function syncZed(options: SyncOptions): ClientSyncResult {
|
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
export function restoreZed(filePath: string, backupPath?: string): ClientSyncResult {
|
|
201
|
-
const
|
|
201
|
+
const restoredFromBackup = restoreFromBackup(filePath, backupPath);
|
|
202
|
+
|
|
203
|
+
let manuallyCleaned = false;
|
|
204
|
+
if (fs.existsSync(filePath)) {
|
|
205
|
+
try {
|
|
206
|
+
let content = fs.readFileSync(filePath, "utf-8");
|
|
207
|
+
if (content.includes("127.0.0.1:7936") || content.includes("qwen3.8-max")) {
|
|
208
|
+
const data = parseJsonWithComments(content);
|
|
209
|
+
if (data.language_models?.openai?.api_url?.includes("7936")) {
|
|
210
|
+
delete data.language_models.openai;
|
|
211
|
+
if (Object.keys(data.language_models).length === 0) {
|
|
212
|
+
delete data.language_models;
|
|
213
|
+
}
|
|
214
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
215
|
+
manuallyCleaned = true;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} catch {}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const success = restoredFromBackup || manuallyCleaned;
|
|
202
222
|
return {
|
|
203
223
|
client: "zed",
|
|
204
224
|
filePath,
|
|
205
225
|
backupPath,
|
|
206
|
-
success
|
|
207
|
-
action:
|
|
208
|
-
message:
|
|
226
|
+
success,
|
|
227
|
+
action: success ? "restored" : "failed",
|
|
228
|
+
message: success
|
|
229
|
+
? restoredFromBackup
|
|
230
|
+
? "Restored Zed settings from backup"
|
|
231
|
+
: "Removed QwenProxy configuration from Zed settings"
|
|
232
|
+
: "Backup file not found",
|
|
209
233
|
};
|
|
210
234
|
}
|
package/src/sync-clients.ts
CHANGED
|
@@ -73,8 +73,8 @@ Exemplos:
|
|
|
73
73
|
npm run sync zed # Sincroniza apenas o Zed Editor
|
|
74
74
|
npm run sync aider # Sincroniza apenas o Aider
|
|
75
75
|
npm run sync claude codex # Sincroniza múltiplos clientes específicos
|
|
76
|
-
npm run sync --
|
|
77
|
-
npm run sync --
|
|
76
|
+
npm run sync --list # Lista status de detecção de todos os 10 clientes (ou qpx sync --list)
|
|
77
|
+
npm run sync --restore # Restaura as configurações originais (ou qpx sync --restore)
|
|
78
78
|
|
|
79
79
|
Opções:
|
|
80
80
|
--client <nome> Nome do cliente (hermes, opencode, claude, openclaw, kilo, cline, omp, codex, zed, aider)
|
|
@@ -193,8 +193,7 @@ async function main() {
|
|
|
193
193
|
console.log("--------------------------------------------------");
|
|
194
194
|
console.log(`✨ ${count} cliente(s) sincronizado(s) com zero perda de outras configs/provedores!`);
|
|
195
195
|
console.log("💡 Para desfazer e restaurar a qualquer momento:");
|
|
196
|
-
console.log(" npm run sync --
|
|
197
|
-
console.log("==================================================\n");
|
|
196
|
+
console.log(" qpx sync --restore (ou npm run sync --restore)");
|
|
198
197
|
}
|
|
199
198
|
|
|
200
199
|
main().catch((err) => {
|
package/src/tools/parser.ts
CHANGED
|
@@ -2043,18 +2043,23 @@ export class StreamingToolParser {
|
|
|
2043
2043
|
recoveryAttempts: truncRecoveryAttempts,
|
|
2044
2044
|
});
|
|
2045
2045
|
logger.warn(
|
|
2046
|
-
|
|
2047
|
-
{
|
|
2048
|
-
toolName,
|
|
2049
|
-
category: "truncated",
|
|
2050
|
-
contentLength: trimmed.length,
|
|
2051
|
-
content: trimmed.substring(0, 2000),
|
|
2052
|
-
failureReason:
|
|
2053
|
-
"stream ended before tool_call closing tag; content too incomplete to reconstruct",
|
|
2054
|
-
recoveryAttempts: truncRecoveryAttempts,
|
|
2055
|
-
emittedToolCallsSoFar: this.emittedToolCallCount,
|
|
2056
|
-
},
|
|
2046
|
+
`[parser] Dropping unrecoverable unclosed tool call (${toolName || "unknown"}) at end of stream: stream ended before closing tag (${trimmed.length} chars)`,
|
|
2057
2047
|
);
|
|
2048
|
+
if (isToolcallDebugEnabled()) {
|
|
2049
|
+
logger.debug(
|
|
2050
|
+
"[parser] Unclosed tool call payload details",
|
|
2051
|
+
{
|
|
2052
|
+
toolName,
|
|
2053
|
+
category: "truncated",
|
|
2054
|
+
contentLength: trimmed.length,
|
|
2055
|
+
content: trimmed.substring(0, 2000),
|
|
2056
|
+
failureReason:
|
|
2057
|
+
"stream ended before tool_call closing tag; content too incomplete to reconstruct",
|
|
2058
|
+
recoveryAttempts: truncRecoveryAttempts,
|
|
2059
|
+
emittedToolCallsSoFar: this.emittedToolCallCount,
|
|
2060
|
+
},
|
|
2061
|
+
);
|
|
2062
|
+
}
|
|
2058
2063
|
if (
|
|
2059
2064
|
this.emittedToolCallCount === 0 &&
|
|
2060
2065
|
this.pendingLeadIn.trim().length > 0
|
package/src/tui/app.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { Screen, type KeyEvent } from "./screen.ts";
|
|
6
6
|
import type { TuiView, ProxyStatusSnapshot } from "./types.ts";
|
|
7
7
|
import { theme, glyphs, drawBox, stringWidth } from "./theme.ts";
|
|
8
|
-
import { fetchProxyStatus } from "./proxy-client.ts";
|
|
8
|
+
import { fetchProxyStatus, fetchLiveModels, getCachedLiveModels } from "./proxy-client.ts";
|
|
9
9
|
import { ServerManager } from "./server-manager.ts";
|
|
10
10
|
import fs from "node:fs";
|
|
11
11
|
import path from "node:path";
|
|
@@ -128,10 +128,28 @@ export class TuiApp {
|
|
|
128
128
|
} catch {}
|
|
129
129
|
|
|
130
130
|
// Background polling every 1s for live status updates and model catalog synchronization
|
|
131
|
+
let pollCount = 0;
|
|
131
132
|
this.pollInterval = setInterval(async () => {
|
|
132
133
|
if (!this.isRunning) return;
|
|
134
|
+
pollCount++;
|
|
133
135
|
try {
|
|
134
136
|
this.statusSnapshot = await fetchProxyStatus();
|
|
137
|
+
|
|
138
|
+
// Sync live models catalog when server is running:
|
|
139
|
+
// Try every 5s until first successful catalog load, then refresh periodically every 30s
|
|
140
|
+
const hasLiveModels = getCachedLiveModels() !== null;
|
|
141
|
+
const shouldSync = !hasLiveModels ? pollCount % 5 === 1 : pollCount % 30 === 1;
|
|
142
|
+
if (shouldSync) {
|
|
143
|
+
const models = await fetchLiveModels(hasLiveModels);
|
|
144
|
+
if (models.length > 0) {
|
|
145
|
+
for (const view of this.views) {
|
|
146
|
+
if ("refreshModels" in view && typeof (view as any).refreshModels === "function") {
|
|
147
|
+
void (view as any).refreshModels();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
135
153
|
this.requestRender();
|
|
136
154
|
} catch {}
|
|
137
155
|
}, 1000);
|