qwenproxy-cli 1.0.25 → 1.0.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/api/server.ts +85 -4
- package/src/core/config.ts +9 -2
- package/src/core/metrics.ts +6 -1
- package/src/core/paths.ts +7 -0
- package/src/core/server-log-buffer.ts +105 -0
- package/src/routes/chat/account.ts +9 -2
- package/src/routes/chat/index.ts +6 -0
- package/src/services/chat-cleanup.ts +175 -3
- package/src/services/qwen-thread-state.ts +37 -0
- package/src/services/qwen.ts +110 -0
- package/src/sync/index.ts +47 -11
- package/src/tools/parser.ts +4 -1
- package/src/tui/app.ts +29 -20
- package/src/tui/index.ts +2 -2
- package/src/tui/proxy-client.ts +58 -4
- package/src/tui/server-manager.ts +46 -1
- package/src/tui/settings.ts +100 -0
- package/src/tui/theme.ts +15 -9
- package/src/tui/types.ts +19 -0
- package/src/tui/views/accounts-view.ts +50 -3
- package/src/tui/views/chat-view.ts +41 -2
- package/src/tui/views/logs-view.ts +22 -18
- package/src/tui/views/status-view.ts +180 -41
package/src/services/qwen.ts
CHANGED
|
@@ -53,6 +53,8 @@ export {
|
|
|
53
53
|
updateSessionParent,
|
|
54
54
|
invalidateLogicalThreadParent,
|
|
55
55
|
clearAllSessionsForAccount,
|
|
56
|
+
removeSessionByChatId,
|
|
57
|
+
isChatSessionActive,
|
|
56
58
|
getSessionParent,
|
|
57
59
|
} from "./qwen-thread-state.ts";
|
|
58
60
|
export type { LogicalThreadEntry } from "./qwen-thread-state.ts";
|
|
@@ -74,6 +76,8 @@ import {
|
|
|
74
76
|
} from "./qwen-errors.ts";
|
|
75
77
|
import {
|
|
76
78
|
clearAllSessionsForAccount,
|
|
79
|
+
removeSessionByChatId,
|
|
80
|
+
isChatSessionActive,
|
|
77
81
|
getSessionParent,
|
|
78
82
|
updateSessionParent,
|
|
79
83
|
} from "./qwen-thread-state.ts";
|
|
@@ -1977,6 +1981,112 @@ export async function deleteAllQwenChats(accountId?: string): Promise<boolean> {
|
|
|
1977
1981
|
return true;
|
|
1978
1982
|
}
|
|
1979
1983
|
|
|
1984
|
+
export interface RemoteQwenChat {
|
|
1985
|
+
id: string;
|
|
1986
|
+
title: string;
|
|
1987
|
+
updated_at: number | string;
|
|
1988
|
+
created_at: number | string;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
/**
|
|
1992
|
+
* Deletes a single chat session by ID on Qwen Web.
|
|
1993
|
+
*/
|
|
1994
|
+
export async function deleteSingleQwenChat(
|
|
1995
|
+
accountId: string | undefined,
|
|
1996
|
+
chatId: string,
|
|
1997
|
+
): Promise<boolean> {
|
|
1998
|
+
if (!chatId) return false;
|
|
1999
|
+
|
|
2000
|
+
if (isAuthMockEnabled()) {
|
|
2001
|
+
const url = qwenUrl(`/api/v2/chats/${encodeURIComponent(chatId)}`);
|
|
2002
|
+
const response = await fetch(url, {
|
|
2003
|
+
method: "DELETE",
|
|
2004
|
+
});
|
|
2005
|
+
removeSessionByChatId(chatId);
|
|
2006
|
+
return response.ok;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
const requestHeaders: Record<string, string> = {
|
|
2010
|
+
source: "web",
|
|
2011
|
+
version: "0.2.89",
|
|
2012
|
+
timezone: new Date().toString().split(" (")[0],
|
|
2013
|
+
"x-request-id": crypto.randomUUID(),
|
|
2014
|
+
Referer: qwenUrl(`/c/${encodeURIComponent(chatId)}`),
|
|
2015
|
+
};
|
|
2016
|
+
|
|
2017
|
+
try {
|
|
2018
|
+
const response = await requestQwenTextInBrowser(
|
|
2019
|
+
accountId,
|
|
2020
|
+
"DELETE",
|
|
2021
|
+
`/api/v2/chats/${encodeURIComponent(chatId)}`,
|
|
2022
|
+
requestHeaders,
|
|
2023
|
+
undefined,
|
|
2024
|
+
{ referrer: qwenUrl(`/c/${encodeURIComponent(chatId)}`), noMutexRecovery: true },
|
|
2025
|
+
);
|
|
2026
|
+
|
|
2027
|
+
const { json: parsed } = await readJsonTextResponse(response, {
|
|
2028
|
+
strict: false,
|
|
2029
|
+
});
|
|
2030
|
+
|
|
2031
|
+
const success = response.ok && parsed?.success === true && parsed?.data?.status === true;
|
|
2032
|
+
if (success) {
|
|
2033
|
+
removeSessionByChatId(chatId);
|
|
2034
|
+
}
|
|
2035
|
+
return success;
|
|
2036
|
+
} catch (error) {
|
|
2037
|
+
logger.debug("[Qwen] deleteSingleQwenChat failed", {
|
|
2038
|
+
accountId,
|
|
2039
|
+
chatId,
|
|
2040
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2041
|
+
});
|
|
2042
|
+
return false;
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
/**
|
|
2047
|
+
* Fetch remote chat list from Qwen Web.
|
|
2048
|
+
*/
|
|
2049
|
+
export async function fetchRemoteQwenChats(
|
|
2050
|
+
accountId?: string,
|
|
2051
|
+
): Promise<RemoteQwenChat[]> {
|
|
2052
|
+
if (isAuthMockEnabled()) {
|
|
2053
|
+
try {
|
|
2054
|
+
const response = await fetch(qwenUrl("/api/v2/chats/?page=1&exclude_project=true"));
|
|
2055
|
+
const json: any = await response.json().catch(() => null);
|
|
2056
|
+
if (json?.success && Array.isArray(json.data)) {
|
|
2057
|
+
return json.data;
|
|
2058
|
+
}
|
|
2059
|
+
} catch {}
|
|
2060
|
+
return [];
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
const requestHeaders: Record<string, string> = {
|
|
2064
|
+
version: "0.2.89",
|
|
2065
|
+
timezone: new Date().toString().split(" (")[0],
|
|
2066
|
+
"x-request-id": crypto.randomUUID(),
|
|
2067
|
+
Referer: qwenUrl("/settings/chats"),
|
|
2068
|
+
};
|
|
2069
|
+
|
|
2070
|
+
try {
|
|
2071
|
+
const response = await requestQwenTextInBrowser(
|
|
2072
|
+
accountId,
|
|
2073
|
+
"GET",
|
|
2074
|
+
"/api/v2/chats/?page=1&exclude_project=true",
|
|
2075
|
+
requestHeaders,
|
|
2076
|
+
undefined,
|
|
2077
|
+
{ referrer: qwenUrl("/settings/chats"), noMutexRecovery: true },
|
|
2078
|
+
);
|
|
2079
|
+
if (!response.ok) return [];
|
|
2080
|
+
const json: any = await response.json().catch(() => null);
|
|
2081
|
+
if (json?.success && Array.isArray(json.data)) {
|
|
2082
|
+
return json.data;
|
|
2083
|
+
}
|
|
2084
|
+
return [];
|
|
2085
|
+
} catch {
|
|
2086
|
+
return [];
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
|
|
1980
2090
|
export async function fetchQwenModels(
|
|
1981
2091
|
accountId?: string,
|
|
1982
2092
|
): Promise<PublicQwenModel[]> {
|
package/src/sync/index.ts
CHANGED
|
@@ -115,8 +115,12 @@ export function inspectClientSyncStatus(
|
|
|
115
115
|
const data = JSON.parse(raw);
|
|
116
116
|
const url = data?.env?.ANTHROPIC_BASE_URL || "";
|
|
117
117
|
const model = data?.env?.ANTHROPIC_MODEL || data?.model || "";
|
|
118
|
+
const isLocalHost =
|
|
119
|
+
url.includes(String(port)) ||
|
|
120
|
+
url.includes(`127.0.0.1:${port}`) ||
|
|
121
|
+
url.includes(`localhost:${port}`);
|
|
118
122
|
const isSynced =
|
|
119
|
-
|
|
123
|
+
isLocalHost &&
|
|
120
124
|
(model.toLowerCase().includes("qwen") || Boolean(data?.env?.ANTHROPIC_AUTH_TOKEN));
|
|
121
125
|
return {
|
|
122
126
|
id,
|
|
@@ -132,34 +136,66 @@ export function inspectClientSyncStatus(
|
|
|
132
136
|
const isProviderActive = /^model_provider\s*=\s*["']qwenproxy["']/m.test(raw);
|
|
133
137
|
const modelMatch = raw.match(/^model\s*=\s*["']([^"']+)["']/m);
|
|
134
138
|
const model = modelMatch ? modelMatch[1] : undefined;
|
|
139
|
+
const urlMatch = raw.match(/\[model_providers\.qwenproxy\][\s\S]*?base_url\s*=\s*["']([^"']+)["']/);
|
|
140
|
+
const url = urlMatch ? urlMatch[1] : "";
|
|
141
|
+
const isLocalHost = Boolean(
|
|
142
|
+
url && (url.includes(String(port)) || url.includes(`127.0.0.1:${port}`) || url.includes(`localhost:${port}`)),
|
|
143
|
+
);
|
|
135
144
|
return {
|
|
136
145
|
id,
|
|
137
146
|
installed: true,
|
|
138
|
-
synced: hasProvider && isProviderActive,
|
|
147
|
+
synced: hasProvider && isProviderActive && isLocalHost,
|
|
139
148
|
model,
|
|
140
149
|
};
|
|
141
150
|
}
|
|
142
151
|
|
|
143
152
|
if (id === "opencode") {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
153
|
+
let isSynced = false;
|
|
154
|
+
try {
|
|
155
|
+
const data = JSON.parse(raw);
|
|
156
|
+
const provider = data?.provider?.qwenproxy;
|
|
157
|
+
const url = provider?.options?.baseURL || "";
|
|
158
|
+
const isLocalHost =
|
|
159
|
+
url.includes(String(port)) ||
|
|
160
|
+
url.includes(`127.0.0.1:${port}`) ||
|
|
161
|
+
url.includes(`localhost:${port}`);
|
|
162
|
+
isSynced = Boolean(provider && isLocalHost);
|
|
163
|
+
} catch {
|
|
164
|
+
const qwenBlockMatch = raw.match(/"qwenproxy"\s*:\s*\{[\s\S]*?"baseURL"\s*:\s*"([^"]+)"/);
|
|
165
|
+
const url = qwenBlockMatch ? qwenBlockMatch[1] : "";
|
|
166
|
+
isSynced = Boolean(
|
|
167
|
+
url &&
|
|
168
|
+
(url.includes(String(port)) ||
|
|
169
|
+
url.includes(`127.0.0.1:${port}`) ||
|
|
170
|
+
url.includes(`localhost:${port}`)),
|
|
171
|
+
);
|
|
172
|
+
}
|
|
147
173
|
return {
|
|
148
174
|
id,
|
|
149
175
|
installed: true,
|
|
150
|
-
synced:
|
|
176
|
+
synced: isSynced,
|
|
151
177
|
};
|
|
152
178
|
}
|
|
153
179
|
|
|
154
180
|
if (id === "omp") {
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
181
|
+
const ompMatch = raw.match(/^[ \t]*qwenproxy:\s*\r?\n((?:[ \t]{4,}.*\r?\n?)*)/m);
|
|
182
|
+
let isSynced = false;
|
|
183
|
+
let url: string | undefined;
|
|
184
|
+
if (ompMatch) {
|
|
185
|
+
const urlMatch = ompMatch[1].match(/baseUrl:\s*(\S+)/);
|
|
186
|
+
url = urlMatch ? urlMatch[1].replace(/['"]/g, "") : undefined;
|
|
187
|
+
isSynced = Boolean(
|
|
188
|
+
url &&
|
|
189
|
+
(url.includes(String(port)) ||
|
|
190
|
+
url.includes(`127.0.0.1:${port}`) ||
|
|
191
|
+
url.includes(`localhost:${port}`)),
|
|
192
|
+
);
|
|
193
|
+
}
|
|
159
194
|
return {
|
|
160
195
|
id,
|
|
161
196
|
installed: true,
|
|
162
|
-
synced:
|
|
197
|
+
synced: isSynced,
|
|
198
|
+
url,
|
|
163
199
|
};
|
|
164
200
|
}
|
|
165
201
|
} catch {
|
package/src/tools/parser.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import { robustParseJSON, computeMissingJsonClosingTokens } from "../utils/json.ts";
|
|
3
3
|
import { logger, isToolcallDebugEnabled } from "../core/logger.js";
|
|
4
|
+
import { metrics } from "../core/metrics.ts";
|
|
4
5
|
import type { ParsedToolCall } from "./types";
|
|
5
6
|
import type { FunctionToolDefinition } from "./types";
|
|
6
7
|
import {
|
|
@@ -1501,7 +1502,7 @@ export class StreamingToolParser {
|
|
|
1501
1502
|
}
|
|
1502
1503
|
|
|
1503
1504
|
this.emittedCallKeys.add(key);
|
|
1504
|
-
|
|
1505
|
+
metrics.increment("toolcalls.total");
|
|
1505
1506
|
const incremental = this.activeIncrementalToolCall;
|
|
1506
1507
|
const matchesIncrementalCall =
|
|
1507
1508
|
incremental?.name === tc.name && incremental.startEmitted;
|
|
@@ -1725,6 +1726,7 @@ export class StreamingToolParser {
|
|
|
1725
1726
|
failureReason: options.failureReason,
|
|
1726
1727
|
recoveryAttempts: options.recoveryAttempts,
|
|
1727
1728
|
});
|
|
1729
|
+
metrics.increment("toolcalls.malformed");
|
|
1728
1730
|
}
|
|
1729
1731
|
|
|
1730
1732
|
private extractUndeclaredNamesFromContent(text: string): string[] {
|
|
@@ -1989,6 +1991,7 @@ export class StreamingToolParser {
|
|
|
1989
1991
|
}
|
|
1990
1992
|
}
|
|
1991
1993
|
if (recovered) {
|
|
1994
|
+
metrics.increment("toolcalls.recovered");
|
|
1992
1995
|
if (isToolcallDebugEnabled()) {
|
|
1993
1996
|
logger.debug("[parser] flush: recovery successful", {
|
|
1994
1997
|
name: recovered.name,
|
package/src/tui/app.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { SyncView } from "./views/sync-view.ts";
|
|
|
26
26
|
import { StorageView } from "./views/storage-view.ts";
|
|
27
27
|
import { AccountsView } from "./views/accounts-view.ts";
|
|
28
28
|
import { LogsView } from "./views/logs-view.ts";
|
|
29
|
+
import { loadTuiSettings, saveTuiSettings } from "./settings.ts";
|
|
29
30
|
export class TuiApp {
|
|
30
31
|
private screen: Screen;
|
|
31
32
|
private views: TuiView[] = [];
|
|
@@ -36,7 +37,7 @@ export class TuiApp {
|
|
|
36
37
|
private pollInterval: NodeJS.Timeout | null = null;
|
|
37
38
|
private statusSnapshot: ProxyStatusSnapshot | null = null;
|
|
38
39
|
private renderScheduled = false;
|
|
39
|
-
constructor(initialTab
|
|
40
|
+
constructor(initialTab?: number) {
|
|
40
41
|
this.screen = new Screen();
|
|
41
42
|
|
|
42
43
|
this.views = [
|
|
@@ -47,10 +48,33 @@ export class TuiApp {
|
|
|
47
48
|
new AccountsView(),
|
|
48
49
|
new LogsView(),
|
|
49
50
|
];
|
|
50
|
-
|
|
51
|
+
|
|
52
|
+
let resolvedTab = initialTab;
|
|
53
|
+
if (!resolvedTab || isNaN(resolvedTab)) {
|
|
54
|
+
const saved = loadTuiSettings();
|
|
55
|
+
if (saved.lastTab && saved.lastTab >= 1 && saved.lastTab <= 6) {
|
|
56
|
+
resolvedTab = saved.lastTab;
|
|
57
|
+
} else {
|
|
58
|
+
resolvedTab = 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const tabIdx = Math.max(0, Math.min(this.views.length - 1, resolvedTab - 1));
|
|
51
63
|
this.activeViewIndex = tabIdx;
|
|
52
64
|
}
|
|
53
65
|
|
|
66
|
+
private switchTab(newIdx: number): void {
|
|
67
|
+
if (newIdx === this.activeViewIndex || !this.views[newIdx]) return;
|
|
68
|
+
const activeView = this.views[this.activeViewIndex];
|
|
69
|
+
if (activeView.onDeactivate) activeView.onDeactivate();
|
|
70
|
+
this.activeViewIndex = newIdx;
|
|
71
|
+
this.hoveredTabIndex = null;
|
|
72
|
+
const nextView = this.views[this.activeViewIndex];
|
|
73
|
+
if (nextView.onActivate) nextView.onActivate();
|
|
74
|
+
saveTuiSettings({ lastTab: newIdx + 1 });
|
|
75
|
+
this.requestRender();
|
|
76
|
+
}
|
|
77
|
+
|
|
54
78
|
private getTabAtCol(col: number): number | null {
|
|
55
79
|
let curCol = 3;
|
|
56
80
|
for (let i = 0; i < this.views.length; i++) {
|
|
@@ -149,12 +173,7 @@ export class TuiApp {
|
|
|
149
173
|
}
|
|
150
174
|
if (key.name === "click") {
|
|
151
175
|
if (tabIdx !== null && tabIdx !== this.activeViewIndex && this.views[tabIdx]) {
|
|
152
|
-
|
|
153
|
-
this.activeViewIndex = tabIdx;
|
|
154
|
-
this.hoveredTabIndex = null;
|
|
155
|
-
const nextView = this.views[this.activeViewIndex];
|
|
156
|
-
if (nextView.onActivate) nextView.onActivate();
|
|
157
|
-
this.requestRender();
|
|
176
|
+
this.switchTab(tabIdx);
|
|
158
177
|
return;
|
|
159
178
|
}
|
|
160
179
|
}
|
|
@@ -189,12 +208,7 @@ export class TuiApp {
|
|
|
189
208
|
// Tab for cycling tabs across views (including from Chat view when no modal is open!)
|
|
190
209
|
if (key.name === "tab" && !isModalOpen) {
|
|
191
210
|
const newIdx = (this.activeViewIndex + 1) % this.views.length;
|
|
192
|
-
|
|
193
|
-
this.activeViewIndex = newIdx;
|
|
194
|
-
this.hoveredTabIndex = null;
|
|
195
|
-
const nextView = this.views[this.activeViewIndex];
|
|
196
|
-
if (nextView.onActivate) nextView.onActivate();
|
|
197
|
-
this.requestRender();
|
|
211
|
+
this.switchTab(newIdx);
|
|
198
212
|
return;
|
|
199
213
|
}
|
|
200
214
|
// Direct tab switching with numbers 1..6 only when NOT typing text in an input
|
|
@@ -207,12 +221,7 @@ export class TuiApp {
|
|
|
207
221
|
if (!key.ctrl && !key.meta && ["1", "2", "3", "4", "5", "6"].includes(key.name)) {
|
|
208
222
|
const newIdx = parseInt(key.name, 10) - 1;
|
|
209
223
|
if (newIdx !== this.activeViewIndex && this.views[newIdx]) {
|
|
210
|
-
|
|
211
|
-
this.activeViewIndex = newIdx;
|
|
212
|
-
this.hoveredTabIndex = null;
|
|
213
|
-
const nextView = this.views[this.activeViewIndex];
|
|
214
|
-
if (nextView.onActivate) nextView.onActivate();
|
|
215
|
-
this.requestRender();
|
|
224
|
+
this.switchTab(newIdx);
|
|
216
225
|
return;
|
|
217
226
|
}
|
|
218
227
|
}
|
package/src/tui/index.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { TuiApp } from "./app.ts";
|
|
7
7
|
|
|
8
|
-
function parseInitialTab(): number {
|
|
8
|
+
function parseInitialTab(): number | undefined {
|
|
9
9
|
const args = process.argv.slice(2);
|
|
10
10
|
const tabArgIdx = args.findIndex((a) => a === "--tab" || a === "-t");
|
|
11
11
|
if (tabArgIdx !== -1 && args[tabArgIdx + 1]) {
|
|
@@ -21,7 +21,7 @@ function parseInitialTab(): number {
|
|
|
21
21
|
return parseInt(firstNumeric, 10);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
return
|
|
24
|
+
return undefined;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
async function main() {
|
package/src/tui/proxy-client.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { isPlaywrightInitialized } from "../services/playwright.ts";
|
|
14
14
|
import { getAccountConcurrencySnapshot } from "../core/account-concurrency.ts";
|
|
15
15
|
import { getRssUsageSnapshot } from "../core/memory-usage.ts";
|
|
16
|
+
import { metrics } from "../core/metrics.ts";
|
|
16
17
|
import type { ProxyStatusSnapshot } from "./types.ts";
|
|
17
18
|
|
|
18
19
|
export function maskAccountIdentifier(idOrEmail: string): string {
|
|
@@ -55,6 +56,7 @@ let lastOnlineState = false;
|
|
|
55
56
|
let lastOverallStatus = "offline";
|
|
56
57
|
let lastServerReadyAccounts: Set<string> | null = null;
|
|
57
58
|
let lastServerActiveAccounts: Set<string> | null = null;
|
|
59
|
+
let lastMetricsData: any = null;
|
|
58
60
|
export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
|
|
59
61
|
const port = config.server?.port || 7936;
|
|
60
62
|
const configuredHost = config.server?.host;
|
|
@@ -79,6 +81,9 @@ export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
|
|
|
79
81
|
if (Array.isArray(data.activeAccounts)) {
|
|
80
82
|
lastServerActiveAccounts = new Set(data.activeAccounts);
|
|
81
83
|
}
|
|
84
|
+
if (data.metrics) {
|
|
85
|
+
lastMetricsData = data.metrics;
|
|
86
|
+
}
|
|
82
87
|
} else {
|
|
83
88
|
lastOnlineState = false;
|
|
84
89
|
lastServerReadyAccounts = null;
|
|
@@ -106,8 +111,9 @@ export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
|
|
|
106
111
|
|
|
107
112
|
cachedAccounts = rawAccounts.map((acc) => {
|
|
108
113
|
const cooldownInfo = getAccountCooldownInfo(acc.id);
|
|
109
|
-
const onCooldown = Boolean(cooldownInfo?.onCooldown);
|
|
110
|
-
const remainingCooldownMs = cooldownInfo?.remainingMs || 0;
|
|
114
|
+
const onCooldown = Boolean(cooldownInfo?.onCooldown || (acc.cooldown_until && acc.cooldown_until > now));
|
|
115
|
+
const remainingCooldownMs = cooldownInfo?.remainingMs || (acc.cooldown_until && acc.cooldown_until > now ? acc.cooldown_until - now : 0);
|
|
116
|
+
const cooldownReason = cooldownInfo?.reason || acc.cooldown_reason || (onCooldown ? "RateLimited" : null);
|
|
111
117
|
const headersReady = lastServerReadyAccounts !== null
|
|
112
118
|
? lastServerReadyAccounts.has(acc.id)
|
|
113
119
|
: isAccountHeadersReady(acc.id);
|
|
@@ -121,26 +127,37 @@ export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
|
|
|
121
127
|
cooldownUntil: acc.cooldown_until || null,
|
|
122
128
|
onCooldown,
|
|
123
129
|
remainingCooldownMs,
|
|
130
|
+
cooldownReason,
|
|
124
131
|
headersReady,
|
|
125
132
|
isInitialized,
|
|
126
133
|
};
|
|
127
134
|
});
|
|
128
135
|
}
|
|
129
|
-
const accounts = cachedAccounts;
|
|
130
136
|
const online = lastOnlineState;
|
|
131
137
|
const overallStatus = lastOverallStatus;
|
|
132
138
|
|
|
133
139
|
// Concurrency stats
|
|
134
140
|
let activeStreams = 0;
|
|
135
141
|
let waitingStreams = 0;
|
|
142
|
+
const concurrencyMap = new Map<string, { active: number; waiting: number; limit: number }>();
|
|
136
143
|
try {
|
|
137
144
|
const snapshot = getAccountConcurrencySnapshot();
|
|
138
145
|
for (const item of snapshot) {
|
|
139
146
|
activeStreams += item.active;
|
|
140
147
|
waitingStreams += item.waiting;
|
|
148
|
+
concurrencyMap.set(item.accountId, item);
|
|
141
149
|
}
|
|
142
150
|
} catch {}
|
|
143
151
|
|
|
152
|
+
// Attach concurrency to accounts
|
|
153
|
+
const accounts = cachedAccounts.map((acc) => {
|
|
154
|
+
const concurrency = concurrencyMap.get(acc.id);
|
|
155
|
+
return {
|
|
156
|
+
...acc,
|
|
157
|
+
activeStreams: concurrency?.active ?? 0,
|
|
158
|
+
streamLimit: concurrency?.limit ?? config.concurrency.maxStreamsPerAccount,
|
|
159
|
+
};
|
|
160
|
+
});
|
|
144
161
|
// RAM usage
|
|
145
162
|
let rssMb = 0;
|
|
146
163
|
let systemMemoryPct = 0;
|
|
@@ -150,6 +167,28 @@ export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
|
|
|
150
167
|
systemMemoryPct = Math.round(rssSnap.usagePercent * 10) / 10;
|
|
151
168
|
} catch {}
|
|
152
169
|
|
|
170
|
+
const totalReqs = lastMetricsData?.requestsTotal ?? Number(metrics.get("requests.total")?.value ?? 0);
|
|
171
|
+
const totalErrs = lastMetricsData?.requestsErrors ?? Number(metrics.get("requests.errors")?.value ?? 0);
|
|
172
|
+
const successRate = totalReqs > 0 ? Number((((totalReqs - totalErrs) / totalReqs) * 100).toFixed(1)) : 100;
|
|
173
|
+
const latencyAvgMs = lastMetricsData?.latencyAvgMs ?? (() => {
|
|
174
|
+
const hist = metrics.get("latency.request")?.value;
|
|
175
|
+
if (hist && typeof hist === "object" && (hist as any).count > 0) {
|
|
176
|
+
return Math.round((hist as any).sum / (hist as any).count);
|
|
177
|
+
}
|
|
178
|
+
return 0;
|
|
179
|
+
})();
|
|
180
|
+
const deltasCount = lastMetricsData?.deltasCount ?? Number(metrics.get("requests.delta")?.value ?? 0);
|
|
181
|
+
const fullReplaysCount = lastMetricsData?.fullReplaysCount ?? Number(metrics.get("requests.full")?.value ?? 0);
|
|
182
|
+
const totalModes = deltasCount + fullReplaysCount;
|
|
183
|
+
const deltaRatio = totalModes > 0 ? Number(((deltasCount / totalModes) * 100).toFixed(1)) : (deltasCount > 0 ? 100 : 0);
|
|
184
|
+
const toolCallsCount = lastMetricsData?.toolCallsCount ?? Number(metrics.get("toolcalls.total")?.value ?? 0);
|
|
185
|
+
const toolCallsRecovered = lastMetricsData?.toolCallsRecovered ?? Number(metrics.get("toolcalls.recovered")?.value ?? 0);
|
|
186
|
+
const captchasDetected = lastMetricsData?.captchasDetected ?? Number(metrics.get("captcha.challenges.detected")?.value ?? 0);
|
|
187
|
+
const captchasSolved = lastMetricsData?.captchasSolved ?? Number(metrics.get("captcha.solves.succeeded")?.value ?? 0);
|
|
188
|
+
const chatsCleaned = lastMetricsData?.chatsCleaned ?? Number(metrics.get("chats.cleaned")?.value ?? 0);
|
|
189
|
+
const cacheHitRatio = lastMetricsData?.cache?.hitRatio;
|
|
190
|
+
const cacheBytesSaved = lastMetricsData?.cache?.bytesSaved;
|
|
191
|
+
|
|
153
192
|
return {
|
|
154
193
|
online,
|
|
155
194
|
port,
|
|
@@ -160,10 +199,25 @@ export async function fetchProxyStatus(): Promise<ProxyStatusSnapshot> {
|
|
|
160
199
|
systemMemoryPct,
|
|
161
200
|
activeStreams,
|
|
162
201
|
waitingStreams,
|
|
202
|
+
metrics: {
|
|
203
|
+
requestsTotal: totalReqs,
|
|
204
|
+
requestsErrors: totalErrs,
|
|
205
|
+
successRate,
|
|
206
|
+
latencyAvgMs,
|
|
207
|
+
deltasCount,
|
|
208
|
+
fullReplaysCount,
|
|
209
|
+
deltaRatio,
|
|
210
|
+
toolCallsCount,
|
|
211
|
+
toolCallsRecovered,
|
|
212
|
+
captchasDetected,
|
|
213
|
+
captchasSolved,
|
|
214
|
+
chatsCleaned,
|
|
215
|
+
cacheHitRatio,
|
|
216
|
+
cacheBytesSaved,
|
|
217
|
+
},
|
|
163
218
|
accounts,
|
|
164
219
|
};
|
|
165
220
|
}
|
|
166
|
-
|
|
167
221
|
export function resetAllCooldowns(): number {
|
|
168
222
|
return clearAllAccountCooldowns();
|
|
169
223
|
}
|
|
@@ -31,6 +31,7 @@ export class ServerManager {
|
|
|
31
31
|
private intercepted = false;
|
|
32
32
|
private isTuiRendering = false;
|
|
33
33
|
private startPromise: Promise<void> | null = null;
|
|
34
|
+
private remoteLogAbort: AbortController | null = null;
|
|
34
35
|
|
|
35
36
|
public static getInstance(): ServerManager {
|
|
36
37
|
if (!ServerManager.instance) {
|
|
@@ -234,6 +235,7 @@ export class ServerManager {
|
|
|
234
235
|
"INFO",
|
|
235
236
|
`✨ [Server] Conectado à instância em execução na porta ${port}`,
|
|
236
237
|
);
|
|
238
|
+
this.startRemoteLogStream(cleanHost, port);
|
|
237
239
|
return;
|
|
238
240
|
}
|
|
239
241
|
} catch {}
|
|
@@ -269,10 +271,53 @@ export class ServerManager {
|
|
|
269
271
|
}
|
|
270
272
|
|
|
271
273
|
public async stop(): Promise<void> {
|
|
274
|
+
if (this.remoteLogAbort) {
|
|
275
|
+
this.remoteLogAbort.abort();
|
|
276
|
+
this.remoteLogAbort = null;
|
|
277
|
+
}
|
|
272
278
|
this.restoreLogs();
|
|
273
279
|
try {
|
|
274
|
-
await stopServer();
|
|
275
280
|
this.state = "offline";
|
|
276
281
|
} catch {}
|
|
277
282
|
}
|
|
283
|
+
|
|
284
|
+
public startRemoteLogStream(host: string, port: number): void {
|
|
285
|
+
if (this.remoteLogAbort) {
|
|
286
|
+
this.remoteLogAbort.abort();
|
|
287
|
+
}
|
|
288
|
+
const abort = new AbortController();
|
|
289
|
+
this.remoteLogAbort = abort;
|
|
290
|
+
|
|
291
|
+
(async () => {
|
|
292
|
+
try {
|
|
293
|
+
const resp = await fetch(`http://${host}:${port}/logs/live`, {
|
|
294
|
+
signal: abort.signal,
|
|
295
|
+
headers: { Accept: "text/event-stream" },
|
|
296
|
+
});
|
|
297
|
+
if (!resp.ok || !resp.body) return;
|
|
298
|
+
const reader = resp.body.getReader();
|
|
299
|
+
const decoder = new TextDecoder();
|
|
300
|
+
let buf = "";
|
|
301
|
+
|
|
302
|
+
while (!abort.signal.aborted) {
|
|
303
|
+
const { done, value } = await reader.read();
|
|
304
|
+
if (done) break;
|
|
305
|
+
buf += decoder.decode(value, { stream: true });
|
|
306
|
+
const lines = buf.split("\n");
|
|
307
|
+
buf = lines.pop() ?? "";
|
|
308
|
+
|
|
309
|
+
for (const line of lines) {
|
|
310
|
+
if (line.startsWith("data: ")) {
|
|
311
|
+
try {
|
|
312
|
+
const data = JSON.parse(line.slice(6));
|
|
313
|
+
if (data && data.message) {
|
|
314
|
+
this.appendLog(data.level || "INFO", data.message);
|
|
315
|
+
}
|
|
316
|
+
} catch {}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
} catch {}
|
|
321
|
+
})();
|
|
322
|
+
}
|
|
278
323
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QwenProxy TUI - User Preferences & Settings Persistence
|
|
3
|
+
* Automatically saves and restores chosen model, reasoning effort, last active tab, and log filters.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { getTuiSettingsPath } from "../core/paths.ts";
|
|
9
|
+
|
|
10
|
+
export interface TuiSettings {
|
|
11
|
+
lastTab?: number;
|
|
12
|
+
chat?: {
|
|
13
|
+
model?: string;
|
|
14
|
+
effort?: "high" | "medium" | "low";
|
|
15
|
+
};
|
|
16
|
+
logs?: {
|
|
17
|
+
filter?: "all" | "warn" | "error";
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const defaultSettings: TuiSettings = {
|
|
22
|
+
lastTab: 1,
|
|
23
|
+
chat: {
|
|
24
|
+
model: "qwen3.8-max",
|
|
25
|
+
effort: "high",
|
|
26
|
+
},
|
|
27
|
+
logs: {
|
|
28
|
+
filter: "all",
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
let cachedSettings: TuiSettings | null = null;
|
|
33
|
+
|
|
34
|
+
export function resetTuiSettingsCacheForTests(): void {
|
|
35
|
+
cachedSettings = null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function loadTuiSettings(): TuiSettings {
|
|
39
|
+
if (cachedSettings) {
|
|
40
|
+
return {
|
|
41
|
+
...cachedSettings,
|
|
42
|
+
chat: { ...cachedSettings.chat },
|
|
43
|
+
logs: { ...cachedSettings.logs },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const filePath = getTuiSettingsPath();
|
|
48
|
+
try {
|
|
49
|
+
if (fs.existsSync(filePath)) {
|
|
50
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
51
|
+
const parsed = JSON.parse(raw);
|
|
52
|
+
const loaded: TuiSettings = {
|
|
53
|
+
...defaultSettings,
|
|
54
|
+
...parsed,
|
|
55
|
+
chat: { ...defaultSettings.chat, ...(parsed.chat || {}) },
|
|
56
|
+
logs: { ...defaultSettings.logs, ...(parsed.logs || {}) },
|
|
57
|
+
};
|
|
58
|
+
cachedSettings = loaded;
|
|
59
|
+
return {
|
|
60
|
+
...loaded,
|
|
61
|
+
chat: { ...loaded.chat },
|
|
62
|
+
logs: { ...loaded.logs },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
} catch {}
|
|
66
|
+
|
|
67
|
+
const finalSettings: TuiSettings = {
|
|
68
|
+
...defaultSettings,
|
|
69
|
+
chat: { ...defaultSettings.chat },
|
|
70
|
+
logs: { ...defaultSettings.logs },
|
|
71
|
+
};
|
|
72
|
+
cachedSettings = finalSettings;
|
|
73
|
+
return {
|
|
74
|
+
...finalSettings,
|
|
75
|
+
chat: { ...finalSettings.chat },
|
|
76
|
+
logs: { ...finalSettings.logs },
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function saveTuiSettings(updates: Partial<TuiSettings>): void {
|
|
81
|
+
try {
|
|
82
|
+
const current = loadTuiSettings();
|
|
83
|
+
const merged: TuiSettings = {
|
|
84
|
+
...current,
|
|
85
|
+
...updates,
|
|
86
|
+
chat: { ...current.chat, ...(updates.chat || {}) },
|
|
87
|
+
logs: { ...current.logs, ...(updates.logs || {}) },
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
cachedSettings = merged;
|
|
91
|
+
|
|
92
|
+
const filePath = getTuiSettingsPath();
|
|
93
|
+
const dir = path.dirname(filePath);
|
|
94
|
+
if (!fs.existsSync(dir)) {
|
|
95
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
fs.writeFileSync(filePath, JSON.stringify(merged, null, 2), "utf-8");
|
|
99
|
+
} catch {}
|
|
100
|
+
}
|