qwenproxy-cli 1.2.10 → 1.3.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/package.json +3 -3
- package/src/api/server.ts +14 -2
- package/src/core/accounts.ts +118 -17
- package/src/core/local-auth.ts +52 -0
- package/src/core/paths.ts +3 -0
- package/src/login.ts +55 -8
- package/src/routes/chat/errors.ts +8 -0
- package/src/routes/chat/retry-policy.ts +7 -0
- package/src/routes/chat/streaming.ts +42 -3
- package/src/routes/upload.ts +51 -12
- package/src/services/auth-playwright.ts +5 -28
- package/src/services/playwright.ts +338 -112
- package/src/services/qwen.ts +2 -2
- package/src/sync/codex.ts +154 -65
- package/src/sync/index.ts +7 -2
- package/src/tools/parser.ts +103 -0
- package/src/tools/toolcall-tags.ts +2 -2
- package/src/tui/server-manager.ts +6 -0
- package/src/tui/views/accounts-view.ts +54 -6
- package/src/tui/views/logs-view.ts +4 -20
- package/src/tui/views/status-view.ts +496 -3
- package/src/tui/views/sync-view.ts +22 -10
- package/src/utils/jwt.ts +40 -0
package/src/services/qwen.ts
CHANGED
|
@@ -1666,10 +1666,10 @@ export async function syncQwenRequestPersonalization(
|
|
|
1666
1666
|
|
|
1667
1667
|
if (isUnauthorized) {
|
|
1668
1668
|
console.warn(
|
|
1669
|
-
`[Qwen] Personalization 401 — refreshing session and retrying | account=${cacheKey}`,
|
|
1669
|
+
`[Qwen] Personalization 401 — refreshing session with re-auth and retrying | account=${cacheKey}`,
|
|
1670
1670
|
);
|
|
1671
1671
|
try {
|
|
1672
|
-
const { headers: freshHeaders } = await getQwenHeaders(true, accountId);
|
|
1672
|
+
const { headers: freshHeaders } = await getQwenHeaders(true, accountId, true);
|
|
1673
1673
|
requestHeaders = buildCapturedQwenHeaders(freshHeaders, {
|
|
1674
1674
|
referer: qwenUrl("/settings/personalization"),
|
|
1675
1675
|
});
|
package/src/sync/codex.ts
CHANGED
|
@@ -1,7 +1,34 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
3
4
|
import type { ClientSyncResult, SyncOptions } from "./types.ts";
|
|
4
5
|
import { createTimestampBackup, restoreFromBackup, formatModelDisplayName } from "./utils.ts";
|
|
6
|
+
import { isRunningUnderNodeTest } from "../core/paths.ts";
|
|
7
|
+
|
|
8
|
+
export function removeTomlSection(content: string, sectionHeader: string): string {
|
|
9
|
+
const lines = content.split(/\r?\n/);
|
|
10
|
+
const result: string[] = [];
|
|
11
|
+
let inTargetSection = false;
|
|
12
|
+
const targetNorm = sectionHeader.replace(/\s+/g, "");
|
|
13
|
+
|
|
14
|
+
for (const line of lines) {
|
|
15
|
+
const trimmed = line.trim();
|
|
16
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
17
|
+
const headerInside = trimmed.slice(1, -1).trim().replace(/\s+/g, "");
|
|
18
|
+
if (headerInside === targetNorm) {
|
|
19
|
+
inTargetSection = true;
|
|
20
|
+
continue;
|
|
21
|
+
} else {
|
|
22
|
+
inTargetSection = false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (!inTargetSection) {
|
|
26
|
+
result.push(line);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return result.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
31
|
+
}
|
|
5
32
|
|
|
6
33
|
function updateTopLevelKey(content: string, key: string, value: string | number): string {
|
|
7
34
|
// Find first section header [section]
|
|
@@ -22,74 +49,108 @@ function updateTopLevelKey(content: string, key: string, value: string | number)
|
|
|
22
49
|
return newTopPart + restPart;
|
|
23
50
|
}
|
|
24
51
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
52
|
+
function syncCodexSingleFile(
|
|
53
|
+
targetPath: string,
|
|
54
|
+
options: SyncOptions,
|
|
55
|
+
): { backupPath?: string } {
|
|
56
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
29
57
|
|
|
30
|
-
|
|
31
|
-
|
|
58
|
+
let backupPath: string | undefined;
|
|
59
|
+
let content = "";
|
|
32
60
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
61
|
+
if (fs.existsSync(targetPath)) {
|
|
62
|
+
backupPath = createTimestampBackup(targetPath);
|
|
63
|
+
content = fs.readFileSync(targetPath, "utf-8");
|
|
64
|
+
}
|
|
37
65
|
|
|
38
|
-
|
|
39
|
-
|
|
66
|
+
const { apiKey, baseUrl, model = "qwen3.8-max", setActive = true } = options;
|
|
67
|
+
|
|
68
|
+
// Build the qwenproxy model_provider block
|
|
69
|
+
const providerBlock = `[model_providers.qwenproxy]
|
|
40
70
|
name = "QwenProxy"
|
|
41
71
|
base_url = "${baseUrl}"
|
|
42
72
|
wire_api = "responses"
|
|
43
73
|
experimental_bearer_token = "${apiKey}"
|
|
44
74
|
`;
|
|
45
75
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
content = content.replace(providerRegex, providerBlock);
|
|
50
|
-
} else {
|
|
51
|
-
content = content.trimEnd() + (content.length > 0 ? "\n\n" : "") + providerBlock;
|
|
52
|
-
}
|
|
76
|
+
// Clean any previous or duplicate [model_providers.qwenproxy] blocks to guarantee zero duplicates
|
|
77
|
+
content = removeTomlSection(content, "model_providers.qwenproxy");
|
|
78
|
+
content = content.trimEnd() + (content.length > 0 ? "\n\n" : "") + providerBlock;
|
|
53
79
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
80
|
+
// Set active model if requested
|
|
81
|
+
if (setActive) {
|
|
82
|
+
content = updateTopLevelKey(content, "model", model);
|
|
83
|
+
content = updateTopLevelKey(content, "model_provider", "qwenproxy");
|
|
84
|
+
content = updateTopLevelKey(content, "model_context_window", 1000000);
|
|
85
|
+
}
|
|
60
86
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
87
|
+
// If a custom model_catalog_json is configured, ensure the target model is listed in it
|
|
88
|
+
const catalogMatch = content.match(/^model_catalog_json\s*=\s*["']([^"']+)["']/m);
|
|
89
|
+
if (catalogMatch && catalogMatch[1]) {
|
|
90
|
+
try {
|
|
91
|
+
const rawCatalogPath = catalogMatch[1];
|
|
92
|
+
const catalogPath = path.isAbsolute(rawCatalogPath)
|
|
93
|
+
? rawCatalogPath
|
|
94
|
+
: path.resolve(path.dirname(targetPath), rawCatalogPath);
|
|
95
|
+
if (fs.existsSync(catalogPath)) {
|
|
96
|
+
const catalog = JSON.parse(fs.readFileSync(catalogPath, "utf-8"));
|
|
97
|
+
if (Array.isArray(catalog.models)) {
|
|
98
|
+
const hasModel = catalog.models.some((m: any) => m?.slug === model);
|
|
99
|
+
if (!hasModel) {
|
|
100
|
+
const template = catalog.models[0] || {};
|
|
101
|
+
catalog.models.unshift({
|
|
102
|
+
...template,
|
|
103
|
+
slug: model,
|
|
104
|
+
display_name: formatModelDisplayName(model),
|
|
105
|
+
description: `QwenProxy ${model}`,
|
|
106
|
+
context_window: 1000000,
|
|
107
|
+
max_context_window: 1000000,
|
|
108
|
+
});
|
|
109
|
+
fs.writeFileSync(catalogPath, JSON.stringify(catalog, null, 2), "utf-8");
|
|
85
110
|
}
|
|
86
111
|
}
|
|
87
|
-
} catch {
|
|
88
|
-
// Non-fatal catalog enhancement
|
|
89
112
|
}
|
|
113
|
+
} catch {
|
|
114
|
+
// Non-fatal catalog enhancement
|
|
90
115
|
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
fs.writeFileSync(targetPath, content.trimEnd() + "\n", "utf-8");
|
|
119
|
+
return { backupPath };
|
|
120
|
+
}
|
|
91
121
|
|
|
92
|
-
|
|
122
|
+
export function syncCodex(options: SyncOptions): ClientSyncResult {
|
|
123
|
+
const { filePath, baseUrl } = options;
|
|
124
|
+
try {
|
|
125
|
+
const { backupPath } = syncCodexSingleFile(filePath, options);
|
|
126
|
+
|
|
127
|
+
// If an alternate Codex config exists (e.g. ~/.codex/config.toml vs CODEX_HOME/config.toml),
|
|
128
|
+
// sync it as well so both standard CLI and custom launcher environments are always synchronized.
|
|
129
|
+
const home = os.homedir();
|
|
130
|
+
const standardPath = path.join(home, ".codex", "config.toml");
|
|
131
|
+
const codexHomePath = process.env.CODEX_HOME
|
|
132
|
+
? path.join(process.env.CODEX_HOME, "config.toml")
|
|
133
|
+
: undefined;
|
|
134
|
+
|
|
135
|
+
const isStandard =
|
|
136
|
+
path.resolve(filePath).toLowerCase() === path.resolve(standardPath).toLowerCase();
|
|
137
|
+
const isCodexHome =
|
|
138
|
+
Boolean(codexHomePath) &&
|
|
139
|
+
path.resolve(filePath).toLowerCase() === path.resolve(codexHomePath!).toLowerCase();
|
|
140
|
+
|
|
141
|
+
if ((isStandard || isCodexHome) && !isRunningUnderNodeTest()) {
|
|
142
|
+
const alternatePath = isStandard ? codexHomePath : standardPath;
|
|
143
|
+
if (
|
|
144
|
+
alternatePath &&
|
|
145
|
+
path.resolve(alternatePath).toLowerCase() !== path.resolve(filePath).toLowerCase()
|
|
146
|
+
) {
|
|
147
|
+
try {
|
|
148
|
+
if (fs.existsSync(alternatePath) || fs.existsSync(path.dirname(alternatePath))) {
|
|
149
|
+
syncCodexSingleFile(alternatePath, options);
|
|
150
|
+
}
|
|
151
|
+
} catch {}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
93
154
|
|
|
94
155
|
return {
|
|
95
156
|
client: "codex",
|
|
@@ -116,20 +177,48 @@ export function restoreCodex(filePath: string, backupPath?: string): ClientSyncR
|
|
|
116
177
|
// If backup was restored but still had qwenproxy (or if no backup was found),
|
|
117
178
|
// strip the QwenProxy provider block cleanly so the file is guaranteed un-synced.
|
|
118
179
|
let manuallyCleaned = false;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
content = content
|
|
180
|
+
const cleanSingleFile = (p: string) => {
|
|
181
|
+
if (fs.existsSync(p)) {
|
|
182
|
+
try {
|
|
183
|
+
let content = fs.readFileSync(p, "utf-8");
|
|
184
|
+
if (
|
|
185
|
+
content.includes("[model_providers.qwenproxy]") ||
|
|
186
|
+
/^model_provider\s*=\s*["']qwenproxy["']/m.test(content)
|
|
187
|
+
) {
|
|
188
|
+
content = removeTomlSection(content, "model_providers.qwenproxy");
|
|
189
|
+
content = content.replace(/^model_provider\s*=\s*["']qwenproxy["']\r?\n?/m, "");
|
|
190
|
+
if (/^model\s*=\s*["']qwen/m.test(content)) {
|
|
191
|
+
content = content.replace(/^model\s*=\s*["']qwen[^"']*["']\r?\n?/m, "");
|
|
192
|
+
}
|
|
193
|
+
fs.writeFileSync(p, content.trimEnd() + "\n", "utf-8");
|
|
194
|
+
manuallyCleaned = true;
|
|
128
195
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
196
|
+
} catch {}
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
cleanSingleFile(filePath);
|
|
201
|
+
|
|
202
|
+
const home = os.homedir();
|
|
203
|
+
const standardPath = path.join(home, ".codex", "config.toml");
|
|
204
|
+
const codexHomePath = process.env.CODEX_HOME
|
|
205
|
+
? path.join(process.env.CODEX_HOME, "config.toml")
|
|
206
|
+
: undefined;
|
|
207
|
+
|
|
208
|
+
const isStandard =
|
|
209
|
+
path.resolve(filePath).toLowerCase() === path.resolve(standardPath).toLowerCase();
|
|
210
|
+
const isCodexHome =
|
|
211
|
+
Boolean(codexHomePath) &&
|
|
212
|
+
path.resolve(filePath).toLowerCase() === path.resolve(codexHomePath!).toLowerCase();
|
|
213
|
+
|
|
214
|
+
if ((isStandard || isCodexHome) && !isRunningUnderNodeTest()) {
|
|
215
|
+
const alternatePath = isStandard ? codexHomePath : standardPath;
|
|
216
|
+
if (
|
|
217
|
+
alternatePath &&
|
|
218
|
+
path.resolve(alternatePath).toLowerCase() !== path.resolve(filePath).toLowerCase()
|
|
219
|
+
) {
|
|
220
|
+
cleanSingleFile(alternatePath);
|
|
221
|
+
}
|
|
133
222
|
}
|
|
134
223
|
|
|
135
224
|
const success = restoredFromBackup || manuallyCleaned;
|
package/src/sync/index.ts
CHANGED
|
@@ -64,7 +64,10 @@ export function resolveApiKey(
|
|
|
64
64
|
}
|
|
65
65
|
return overrideKey.trim();
|
|
66
66
|
}
|
|
67
|
-
const envKey =
|
|
67
|
+
const envKey =
|
|
68
|
+
(!isPlaceholderApiKey(process.env.API_KEY) ? process.env.API_KEY : undefined) ||
|
|
69
|
+
(!isPlaceholderApiKey(process.env.ADMIN_PASSWORD) ? process.env.ADMIN_PASSWORD : undefined) ||
|
|
70
|
+
configKey;
|
|
68
71
|
if (envKey && !isPlaceholderApiKey(envKey)) {
|
|
69
72
|
return envKey.trim();
|
|
70
73
|
}
|
|
@@ -165,7 +168,9 @@ export function getDefaultPaths(): {
|
|
|
165
168
|
|
|
166
169
|
return {
|
|
167
170
|
claudeCode: path.join(home, ".claude", "settings.json"),
|
|
168
|
-
codex:
|
|
171
|
+
codex: fs.existsSync(path.join(home, ".codex", "config.toml"))
|
|
172
|
+
? path.join(home, ".codex", "config.toml")
|
|
173
|
+
: process.env.CODEX_HOME
|
|
169
174
|
? path.join(process.env.CODEX_HOME, "config.toml")
|
|
170
175
|
: path.join(home, ".codex", "config.toml"),
|
|
171
176
|
openCode: existingOpenCode || openCodeCandidates[0],
|
package/src/tools/parser.ts
CHANGED
|
@@ -629,6 +629,9 @@ function coerceParameterValue(rawValue: string): unknown {
|
|
|
629
629
|
*/
|
|
630
630
|
function extractToolName(openTag: string, block: string): string {
|
|
631
631
|
const combined = `${openTag}\n${block}`;
|
|
632
|
+
const fnMatch = combined.match(/<function=([^\s>]+)>/i);
|
|
633
|
+
if (fnMatch) return fnMatch[1].trim();
|
|
634
|
+
|
|
632
635
|
const attrMatch = combined.match(
|
|
633
636
|
/<tool_call(?:s)?\b[^>]*\bname\s*=\s*["']([^"']+)["']/i,
|
|
634
637
|
);
|
|
@@ -640,6 +643,48 @@ function extractToolName(openTag: string, block: string): string {
|
|
|
640
643
|
return "";
|
|
641
644
|
}
|
|
642
645
|
|
|
646
|
+
/**
|
|
647
|
+
* Parse function shorthand XML <function=name><parameter=key>value</parameter></function> format.
|
|
648
|
+
*/
|
|
649
|
+
function parseFunctionShorthandToolCall(
|
|
650
|
+
block: string,
|
|
651
|
+
openTag: string,
|
|
652
|
+
tools: ToolDefinitionLike[],
|
|
653
|
+
): { name: string; arguments: Record<string, unknown> } | null {
|
|
654
|
+
const combined = `${openTag}\n${block}`;
|
|
655
|
+
const fnMatch = combined.match(/<function=([^\s>]+)>/i);
|
|
656
|
+
let toolName = fnMatch ? fnMatch[1].trim() : "";
|
|
657
|
+
|
|
658
|
+
const args: Record<string, unknown> = {};
|
|
659
|
+
const parameterRe = /<parameter=([^\s>]+)>([\s\S]*?)<\/parameter>/gi;
|
|
660
|
+
let match: RegExpExecArray | null = parameterRe.exec(block);
|
|
661
|
+
while (match !== null) {
|
|
662
|
+
args[match[1]] = coerceParameterValue(match[2]);
|
|
663
|
+
match = parameterRe.exec(block);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Also support <parameter name="..."> inside <function=...>
|
|
667
|
+
if (Object.keys(args).length === 0) {
|
|
668
|
+
const namedParamRe =
|
|
669
|
+
/<parameter\b[^>]*\bname\s*=\s*["']([^"']+)["'][^>]*>([\s\S]*?)<\/parameter>/gi;
|
|
670
|
+
let namedMatch: RegExpExecArray | null = namedParamRe.exec(block);
|
|
671
|
+
while (namedMatch !== null) {
|
|
672
|
+
args[namedMatch[1]] = coerceParameterValue(namedMatch[2]);
|
|
673
|
+
namedMatch = namedParamRe.exec(block);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (Object.keys(args).length === 0) return null;
|
|
678
|
+
|
|
679
|
+
if (!toolName) {
|
|
680
|
+
toolName =
|
|
681
|
+
extractToolName(openTag, block) || inferToolNameFromParameters(args, tools);
|
|
682
|
+
}
|
|
683
|
+
if (!toolName) return null;
|
|
684
|
+
|
|
685
|
+
return { name: toolName, arguments: args };
|
|
686
|
+
}
|
|
687
|
+
|
|
643
688
|
/**
|
|
644
689
|
* Infer tool name by matching parameter keys against tool definitions.
|
|
645
690
|
* Only returns a name if exactly one tool matches all argument keys.
|
|
@@ -2193,6 +2238,47 @@ export class StreamingToolParser {
|
|
|
2193
2238
|
});
|
|
2194
2239
|
}
|
|
2195
2240
|
|
|
2241
|
+
// 0) Try function shorthand format (<function=name><parameter=k>v</parameter></function>)
|
|
2242
|
+
const fnShorthand = parseFunctionShorthandToolCall(
|
|
2243
|
+
t,
|
|
2244
|
+
this.currentOpenTag,
|
|
2245
|
+
this.tools,
|
|
2246
|
+
);
|
|
2247
|
+
if (fnShorthand) {
|
|
2248
|
+
const resolvedName = this.resolveDeclaredToolName(fnShorthand.name);
|
|
2249
|
+
if (!resolvedName) {
|
|
2250
|
+
this.recordMalformedToolCall(content, {
|
|
2251
|
+
undeclaredNames: [fnShorthand.name],
|
|
2252
|
+
category: "undeclared",
|
|
2253
|
+
});
|
|
2254
|
+
this.preserveLiteralToolCall(
|
|
2255
|
+
content,
|
|
2256
|
+
result,
|
|
2257
|
+
`undeclared tool name: ${fnShorthand.name}`,
|
|
2258
|
+
);
|
|
2259
|
+
return;
|
|
2260
|
+
}
|
|
2261
|
+
fnShorthand.name = resolvedName;
|
|
2262
|
+
if (isToolcallDebugEnabled()) {
|
|
2263
|
+
logger.debug(
|
|
2264
|
+
"[parser] processToolContent: function shorthand parsed successfully",
|
|
2265
|
+
{
|
|
2266
|
+
name: fnShorthand.name,
|
|
2267
|
+
arguments: fnShorthand.arguments,
|
|
2268
|
+
},
|
|
2269
|
+
);
|
|
2270
|
+
}
|
|
2271
|
+
this.finalizeSuccessfulToolCall(
|
|
2272
|
+
{
|
|
2273
|
+
id: `call_${crypto.randomUUID()}`,
|
|
2274
|
+
name: fnShorthand.name,
|
|
2275
|
+
arguments: fnShorthand.arguments,
|
|
2276
|
+
},
|
|
2277
|
+
result,
|
|
2278
|
+
);
|
|
2279
|
+
return;
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2196
2282
|
// 1) Try Hermes-style XML <parameter> format first
|
|
2197
2283
|
const xmlParsed = parseXmlParameterToolCall(
|
|
2198
2284
|
t,
|
|
@@ -2488,6 +2574,23 @@ export class StreamingToolParser {
|
|
|
2488
2574
|
});
|
|
2489
2575
|
}
|
|
2490
2576
|
|
|
2577
|
+
// Try function shorthand format first
|
|
2578
|
+
const fnShorthand = parseFunctionShorthandToolCall(
|
|
2579
|
+
block,
|
|
2580
|
+
this.currentOpenTag,
|
|
2581
|
+
this.tools,
|
|
2582
|
+
);
|
|
2583
|
+
if (fnShorthand) {
|
|
2584
|
+
const resolvedName = this.resolveDeclaredToolName(fnShorthand.name);
|
|
2585
|
+
if (resolvedName) {
|
|
2586
|
+
return {
|
|
2587
|
+
id: `call_${crypto.randomUUID()}`,
|
|
2588
|
+
name: resolvedName,
|
|
2589
|
+
arguments: fnShorthand.arguments,
|
|
2590
|
+
};
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2491
2594
|
// Try full parse first
|
|
2492
2595
|
const xmlParsed = parseXmlParameterToolCall(
|
|
2493
2596
|
block,
|
|
@@ -32,12 +32,12 @@ function tagName(tag: string): string {
|
|
|
32
32
|
|
|
33
33
|
/** All accepted open tag names (custom + legacy). */
|
|
34
34
|
export function getOpenNames(): string[] {
|
|
35
|
-
return Array.from(new Set([tagName(TOOL_CALL_OPEN), "tool_call", "tool_calls"]));
|
|
35
|
+
return Array.from(new Set([tagName(TOOL_CALL_OPEN), "tool_call", "tool_calls", "function"]));
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
/** All accepted close tag names (custom + legacy). */
|
|
39
39
|
export function getCloseNames(): string[] {
|
|
40
|
-
return Array.from(new Set([tagName(TOOL_CALL_CLOSE), "tool_call", "tool_calls", "tool"]));
|
|
40
|
+
return Array.from(new Set([tagName(TOOL_CALL_CLOSE), "tool_call", "tool_calls", "tool", "function"]));
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/** Wrap a JSON payload in the canonical open/close tags. */
|
|
@@ -282,10 +282,16 @@ export class ServerManager {
|
|
|
282
282
|
}
|
|
283
283
|
this.restoreLogs();
|
|
284
284
|
try {
|
|
285
|
+
await stopServer();
|
|
285
286
|
this.state = "offline";
|
|
286
287
|
} catch {}
|
|
287
288
|
}
|
|
288
289
|
|
|
290
|
+
public async restart(): Promise<void> {
|
|
291
|
+
await this.stop();
|
|
292
|
+
await this.ensureStarted();
|
|
293
|
+
}
|
|
294
|
+
|
|
289
295
|
public startRemoteLogStream(host: string, port: number): void {
|
|
290
296
|
if (this.remoteLogAbort) {
|
|
291
297
|
this.remoteLogAbort.abort();
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* QwenProxy TUI - Accounts & Cooldowns Management View (Tab 5)
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import crypto from "crypto";
|
|
5
6
|
import type { TuiView, ProxyStatusSnapshot } from "../types.ts";
|
|
6
7
|
import type { KeyEvent } from "../screen.ts";
|
|
7
8
|
import { theme, glyphs, drawBox, pad, truncate, getClipboardText } from "../theme.ts";
|
|
@@ -56,6 +57,7 @@ export class AccountsView implements TuiView {
|
|
|
56
57
|
private statusMessageTimer: NodeJS.Timeout | null = null;
|
|
57
58
|
private isAddModalOpen = false;
|
|
58
59
|
private isBatchModalOpen = false;
|
|
60
|
+
private isValidatingAccount = false;
|
|
59
61
|
private addEmailInput = "";
|
|
60
62
|
private addPasswordInput = "";
|
|
61
63
|
private addEmailCursor = 0;
|
|
@@ -147,6 +149,7 @@ export class AccountsView implements TuiView {
|
|
|
147
149
|
}
|
|
148
150
|
|
|
149
151
|
private async saveModalAccount(): Promise<void> {
|
|
152
|
+
if (this.isValidatingAccount) return;
|
|
150
153
|
const email = this.addEmailInput.trim();
|
|
151
154
|
const password = this.addPasswordInput.trim();
|
|
152
155
|
if (!email || !password) {
|
|
@@ -154,17 +157,54 @@ export class AccountsView implements TuiView {
|
|
|
154
157
|
return;
|
|
155
158
|
}
|
|
156
159
|
|
|
160
|
+
if (process.env.NODE_TEST_CONTEXT) {
|
|
161
|
+
try {
|
|
162
|
+
const newAcc = addAccount(email, password);
|
|
163
|
+
this.isAddModalOpen = false;
|
|
164
|
+
this.addEmailInput = "";
|
|
165
|
+
this.addPasswordInput = "";
|
|
166
|
+
this.addEmailCursor = 0;
|
|
167
|
+
this.addPasswordCursor = 0;
|
|
168
|
+
await this.refresh();
|
|
169
|
+
this.setStatusMessage(theme.green(`✓ Conta ${email} salva! Conectando...`));
|
|
170
|
+
} catch (err: any) {
|
|
171
|
+
this.setStatusMessage(theme.red(`✗ Erro ao salvar: ${err?.message || String(err)}`));
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
this.isValidatingAccount = true;
|
|
177
|
+
this.setStatusMessage(theme.yellow(`[...] Validando credenciais para ${email}...`));
|
|
178
|
+
|
|
179
|
+
const tempId = crypto.randomUUID();
|
|
157
180
|
try {
|
|
158
|
-
const
|
|
181
|
+
const { validateAccountLogin } = await import("../../services/playwright.ts");
|
|
182
|
+
const { wipeAccountSessionFiles } = await import("../../core/accounts.ts");
|
|
183
|
+
const { clearAccountCooldown } = await import("../../core/account-manager.ts");
|
|
184
|
+
|
|
185
|
+
const ok = await validateAccountLogin(
|
|
186
|
+
{ id: tempId, email, password },
|
|
187
|
+
config.playwright.headless,
|
|
188
|
+
config.playwright.browser,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
if (!ok) {
|
|
192
|
+
wipeAccountSessionFiles(tempId);
|
|
193
|
+
clearAccountCooldown(tempId);
|
|
194
|
+
this.setStatusMessage(theme.red(`✗ Falha no login: credenciais inválidas ou bloqueio no Qwen`));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const newAcc = addAccount(email, password, tempId);
|
|
159
199
|
this.isAddModalOpen = false;
|
|
160
200
|
this.addEmailInput = "";
|
|
161
201
|
this.addPasswordInput = "";
|
|
162
202
|
this.addEmailCursor = 0;
|
|
163
203
|
this.addPasswordCursor = 0;
|
|
164
204
|
await this.refresh();
|
|
165
|
-
this.setStatusMessage(theme.green(`✓ Conta ${email} salva! Conectando...`));
|
|
205
|
+
this.setStatusMessage(theme.green(`✓ Conta ${email} validada e salva! Conectando...`));
|
|
166
206
|
|
|
167
|
-
if (process.stdout.isTTY
|
|
207
|
+
if (process.stdout.isTTY) {
|
|
168
208
|
const sManager = ServerManager.getInstance();
|
|
169
209
|
const sState = sManager.getState();
|
|
170
210
|
if (sState !== "online" && sState !== "warming") {
|
|
@@ -189,7 +229,13 @@ export class AccountsView implements TuiView {
|
|
|
189
229
|
}
|
|
190
230
|
}
|
|
191
231
|
} catch (err: any) {
|
|
192
|
-
|
|
232
|
+
const { wipeAccountSessionFiles } = await import("../../core/accounts.ts");
|
|
233
|
+
const { clearAccountCooldown } = await import("../../core/account-manager.ts");
|
|
234
|
+
wipeAccountSessionFiles(tempId);
|
|
235
|
+
clearAccountCooldown(tempId);
|
|
236
|
+
this.setStatusMessage(theme.red(`✗ Erro ao validar: ${err?.message || String(err)}`));
|
|
237
|
+
} finally {
|
|
238
|
+
this.isValidatingAccount = false;
|
|
193
239
|
}
|
|
194
240
|
}
|
|
195
241
|
private async saveBatchAccounts(): Promise<void> {
|
|
@@ -445,6 +491,7 @@ export class AccountsView implements TuiView {
|
|
|
445
491
|
|
|
446
492
|
// 1. Add Account Modal Active
|
|
447
493
|
if (this.isAddModalOpen) {
|
|
494
|
+
if (this.isValidatingAccount) return true;
|
|
448
495
|
if (key.name === "escape") {
|
|
449
496
|
this.isAddModalOpen = false;
|
|
450
497
|
this.addEmailInput = "";
|
|
@@ -1161,8 +1208,9 @@ export class AccountsView implements TuiView {
|
|
|
1161
1208
|
? (passHover ? theme.bgHover(` ${maskedPass} `) : theme.cyan(` ${maskedPass} `))
|
|
1162
1209
|
: (passHover ? theme.bgHover(" (digite a senha) ") : theme.muted(" (digite a senha) "));
|
|
1163
1210
|
}
|
|
1164
|
-
const saveBtn =
|
|
1165
|
-
|
|
1211
|
+
const saveBtn = this.isValidatingAccount
|
|
1212
|
+
? theme.yellow(" [ Validando... ] ")
|
|
1213
|
+
: this.modalHoveredField === "save"
|
|
1166
1214
|
? theme.bgHover(theme.green(" [ Enter ] Salvar "))
|
|
1167
1215
|
: theme.green("[ Enter ] Salvar");
|
|
1168
1216
|
|
|
@@ -469,26 +469,10 @@ export class LogsView implements TuiView {
|
|
|
469
469
|
if (singleLineOnly && this.selectedLogIndex !== null && rawEntries[this.selectedLogIndex]) {
|
|
470
470
|
const entry = rawEntries[this.selectedLogIndex];
|
|
471
471
|
text = `[${entry.time}] [${entry.level}] ${entry.message}`;
|
|
472
|
-
} else {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
const logPath = getServerLogFilePath();
|
|
477
|
-
if (fs.existsSync(logPath)) {
|
|
478
|
-
const diskContent = fs.readFileSync(logPath, "utf-8");
|
|
479
|
-
if (diskContent && diskContent.trim().length > 0) {
|
|
480
|
-
text = diskContent.trim();
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
} catch {}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// Fallback to memory buffer if disk log is empty or on filtered view
|
|
487
|
-
if (!text && rawEntries.length > 0) {
|
|
488
|
-
text = rawEntries
|
|
489
|
-
.map((entry) => `[${entry.time}] [${entry.level}] ${entry.message}`)
|
|
490
|
-
.join("\n");
|
|
491
|
-
}
|
|
472
|
+
} else if (rawEntries.length > 0) {
|
|
473
|
+
text = rawEntries
|
|
474
|
+
.map((entry) => `[${entry.time}] [${entry.level}] ${entry.message}`)
|
|
475
|
+
.join("\n");
|
|
492
476
|
}
|
|
493
477
|
|
|
494
478
|
if (!text) return;
|