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
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/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";
|
|
@@ -26,6 +26,7 @@ import { SyncView } from "./views/sync-view.ts";
|
|
|
26
26
|
import { StorageView } from "./views/storage-view.ts";
|
|
27
27
|
import { AccountsView } from "./views/accounts-view.ts";
|
|
28
28
|
import { LogsView } from "./views/logs-view.ts";
|
|
29
|
+
import { setRuntimeChatMode } from "../core/config.ts";
|
|
29
30
|
import { loadTuiSettings, saveTuiSettings } from "./settings.ts";
|
|
30
31
|
export class TuiApp {
|
|
31
32
|
private screen: Screen;
|
|
@@ -39,6 +40,10 @@ export class TuiApp {
|
|
|
39
40
|
private renderScheduled = false;
|
|
40
41
|
constructor(initialTab?: number) {
|
|
41
42
|
this.screen = new Screen();
|
|
43
|
+
const saved = loadTuiSettings();
|
|
44
|
+
if (saved.chat?.mode) {
|
|
45
|
+
setRuntimeChatMode(saved.chat.mode);
|
|
46
|
+
}
|
|
42
47
|
|
|
43
48
|
this.views = [
|
|
44
49
|
new StatusView(),
|
|
@@ -49,16 +54,10 @@ export class TuiApp {
|
|
|
49
54
|
new LogsView(),
|
|
50
55
|
];
|
|
51
56
|
|
|
52
|
-
let resolvedTab = initialTab;
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
if (saved.lastTab && saved.lastTab >= 1 && saved.lastTab <= 6) {
|
|
56
|
-
resolvedTab = saved.lastTab;
|
|
57
|
-
} else {
|
|
58
|
-
resolvedTab = 1;
|
|
59
|
-
}
|
|
57
|
+
let resolvedTab = initialTab ?? (saved.lastTab && saved.lastTab >= 1 && saved.lastTab <= 6 ? saved.lastTab : 1);
|
|
58
|
+
if (isNaN(resolvedTab)) {
|
|
59
|
+
resolvedTab = 1;
|
|
60
60
|
}
|
|
61
|
-
|
|
62
61
|
const tabIdx = Math.max(0, Math.min(this.views.length - 1, resolvedTab - 1));
|
|
63
62
|
this.activeViewIndex = tabIdx;
|
|
64
63
|
}
|
|
@@ -129,10 +128,28 @@ export class TuiApp {
|
|
|
129
128
|
} catch {}
|
|
130
129
|
|
|
131
130
|
// Background polling every 1s for live status updates and model catalog synchronization
|
|
131
|
+
let pollCount = 0;
|
|
132
132
|
this.pollInterval = setInterval(async () => {
|
|
133
133
|
if (!this.isRunning) return;
|
|
134
|
+
pollCount++;
|
|
134
135
|
try {
|
|
135
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
|
+
|
|
136
153
|
this.requestRender();
|
|
137
154
|
} catch {}
|
|
138
155
|
}, 1000);
|
package/src/tui/index.ts
CHANGED
package/src/tui/proxy-client.ts
CHANGED
|
@@ -190,10 +190,11 @@ export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
|
|
|
190
190
|
const cacheBytesSaved = lastMetricsData?.cache?.bytesSaved;
|
|
191
191
|
|
|
192
192
|
return {
|
|
193
|
-
online,
|
|
193
|
+
online: lastOnlineState,
|
|
194
194
|
port,
|
|
195
195
|
host,
|
|
196
|
-
|
|
196
|
+
chatMode: config.qwen.chatMode as any,
|
|
197
|
+
overallStatus: lastOverallStatus,
|
|
197
198
|
uptimeSeconds,
|
|
198
199
|
rssMb,
|
|
199
200
|
systemMemoryPct,
|
|
@@ -335,18 +336,17 @@ export async function streamChatCompletions(
|
|
|
335
336
|
let cachedLiveModels: string[] | null = null;
|
|
336
337
|
let liveModelsPromise: Promise<string[]> | null = null;
|
|
337
338
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
];
|
|
339
|
+
export { DEFAULT_FALLBACK_MODELS } from "../core/model-alias.ts";
|
|
340
|
+
import { DEFAULT_FALLBACK_MODELS } from "../core/model-alias.ts";
|
|
341
|
+
|
|
342
|
+
export function getCachedLiveModels(): string[] | null {
|
|
343
|
+
return cachedLiveModels;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function resetCachedLiveModelsForTests(): void {
|
|
347
|
+
cachedLiveModels = null;
|
|
348
|
+
liveModelsPromise = null;
|
|
349
|
+
}
|
|
350
350
|
|
|
351
351
|
export async function fetchLiveModels(forceRefresh = false): Promise<string[]> {
|
|
352
352
|
if (!forceRefresh && cachedLiveModels && cachedLiveModels.length > 0) {
|
|
@@ -364,7 +364,9 @@ export async function fetchLiveModels(forceRefresh = false): Promise<string[]> {
|
|
|
364
364
|
|
|
365
365
|
liveModelsPromise = (async () => {
|
|
366
366
|
const controller = new AbortController();
|
|
367
|
-
|
|
367
|
+
// Cold start with Playwright navigation and bx security token acquisition can take 5-12s
|
|
368
|
+
const timeoutMs = cachedLiveModels ? 4000 : 12000;
|
|
369
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
368
370
|
try {
|
|
369
371
|
const resp = await fetch(`http://${host}:${port}/v1/models`, {
|
|
370
372
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
@@ -388,7 +390,9 @@ export async function fetchLiveModels(forceRefresh = false): Promise<string[]> {
|
|
|
388
390
|
}
|
|
389
391
|
}
|
|
390
392
|
}
|
|
391
|
-
} catch {
|
|
393
|
+
} catch {
|
|
394
|
+
// Don't poison cachedLiveModels on network error so subsequent polls can retry
|
|
395
|
+
} finally {
|
|
392
396
|
clearTimeout(timeout);
|
|
393
397
|
liveModelsPromise = null;
|
|
394
398
|
}
|
package/src/tui/screen.ts
CHANGED
|
@@ -47,13 +47,20 @@ export class Screen {
|
|
|
47
47
|
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
48
48
|
return false;
|
|
49
49
|
}
|
|
50
|
-
|
|
50
|
+
try {
|
|
51
|
+
process.title = "QwenProxy";
|
|
52
|
+
} catch {}
|
|
51
53
|
this.active = true;
|
|
52
54
|
this.prevRenderedRows = [];
|
|
53
|
-
// Switch to alternate screen buffer, clear screen, hide cursor,
|
|
55
|
+
// Switch to alternate screen buffer, clear screen, hide cursor, enable mouse tracking, and set terminal title
|
|
54
56
|
ServerManager.getInstance().withTuiRendering(() => {
|
|
55
57
|
process.stdout.write(
|
|
56
|
-
ANSI.enterAltScreen +
|
|
58
|
+
ANSI.enterAltScreen +
|
|
59
|
+
"\x1b[2J" +
|
|
60
|
+
ANSI.cursorHome +
|
|
61
|
+
ANSI.hideCursor +
|
|
62
|
+
ANSI.enableMouse +
|
|
63
|
+
ANSI.setTitle("QwenProxy"),
|
|
57
64
|
);
|
|
58
65
|
});
|
|
59
66
|
// Setup raw keyboard input
|
package/src/tui/settings.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface TuiSettings {
|
|
|
12
12
|
chat?: {
|
|
13
13
|
model?: string;
|
|
14
14
|
effort?: "high" | "medium" | "low";
|
|
15
|
+
mode?: "thread" | "thread-temp" | "stateless" | "stateless-temp";
|
|
15
16
|
};
|
|
16
17
|
logs?: {
|
|
17
18
|
filter?: "all" | "warn" | "error";
|
|
@@ -23,6 +24,7 @@ const defaultSettings: TuiSettings = {
|
|
|
23
24
|
chat: {
|
|
24
25
|
model: "qwen3.8-max",
|
|
25
26
|
effort: "high",
|
|
27
|
+
mode: "thread",
|
|
26
28
|
},
|
|
27
29
|
logs: {
|
|
28
30
|
filter: "all",
|
package/src/tui/theme.ts
CHANGED
|
@@ -18,6 +18,7 @@ export const ANSI = {
|
|
|
18
18
|
exitAltScreen: "\x1b[?1049l",
|
|
19
19
|
enableMouse: "\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1006h",
|
|
20
20
|
disableMouse: "\x1b[?1006l\x1b[?1005l\x1b[?1004l\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?1015l",
|
|
21
|
+
setTitle: (title: string) => `\x1b]0;${title}\x07`,
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
export const theme = {
|
|
@@ -38,6 +39,7 @@ export const theme = {
|
|
|
38
39
|
borderInactive: (s: string) => `\x1b[38;2;53;53;61m${s}\x1b[39m`, // #35353d (Qwen Line Primary Border)
|
|
39
40
|
bgSelected: (s: string) => `\x1b[48;2;45;35;85m\x1b[38;2;247;248;252m${s}\x1b[49m\x1b[39m`, // #2d2355 Deep Violet + #f7f8fc White
|
|
40
41
|
bgHover: (s: string) => `\x1b[48;2;58;46;110m\x1b[38;2;247;248;252m${s}\x1b[49m\x1b[39m`, // #3a2e6e Violet Hover + #f7f8fc White
|
|
42
|
+
bgUserCard: (s: string) => `\x1b[48;2;38;40;58m${s}\x1b[49m`, // #26283a (Catppuccin Surface / Distinct Lighter User Message Card)
|
|
41
43
|
bold: (s: string) => `\x1b[1m${s}\x1b[22m`,
|
|
42
44
|
italic: (s: string) => `\x1b[3m${s}\x1b[23m`,
|
|
43
45
|
dim: (s: string) => `\x1b[2m${s}\x1b[22m`,
|
package/src/tui/types.ts
CHANGED