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.
@@ -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
- export function syncCodex(options: SyncOptions): ClientSyncResult {
26
- const { filePath, apiKey, baseUrl, model = "qwen3.8-max", setActive = true } = options;
27
- try {
28
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
52
+ function syncCodexSingleFile(
53
+ targetPath: string,
54
+ options: SyncOptions,
55
+ ): { backupPath?: string } {
56
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
29
57
 
30
- let backupPath: string | undefined;
31
- let content = "";
58
+ let backupPath: string | undefined;
59
+ let content = "";
32
60
 
33
- if (fs.existsSync(filePath)) {
34
- backupPath = createTimestampBackup(filePath);
35
- content = fs.readFileSync(filePath, "utf-8");
36
- }
61
+ if (fs.existsSync(targetPath)) {
62
+ backupPath = createTimestampBackup(targetPath);
63
+ content = fs.readFileSync(targetPath, "utf-8");
64
+ }
37
65
 
38
- // Build the qwenproxy model_provider block
39
- const providerBlock = `[model_providers.qwenproxy]
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
- // Replace or append [model_providers.qwenproxy]
47
- const providerRegex = /\[model_providers\.qwenproxy\][\s\S]*?(?=(?:^\[|\Z))/m;
48
- if (providerRegex.test(content)) {
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
- // Set active model if requested
55
- if (setActive) {
56
- content = updateTopLevelKey(content, "model", model);
57
- content = updateTopLevelKey(content, "model_provider", "qwenproxy");
58
- content = updateTopLevelKey(content, "model_context_window", 1000000);
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
- // If a custom model_catalog_json is configured, ensure the target model is listed in it
62
- const catalogMatch = content.match(/^model_catalog_json\s*=\s*["']([^"']+)["']/m);
63
- if (catalogMatch && catalogMatch[1]) {
64
- try {
65
- const rawCatalogPath = catalogMatch[1];
66
- const catalogPath = path.isAbsolute(rawCatalogPath)
67
- ? rawCatalogPath
68
- : path.resolve(path.dirname(filePath), rawCatalogPath);
69
- if (fs.existsSync(catalogPath)) {
70
- const catalog = JSON.parse(fs.readFileSync(catalogPath, "utf-8"));
71
- if (Array.isArray(catalog.models)) {
72
- const hasModel = catalog.models.some((m: any) => m?.slug === model);
73
- if (!hasModel) {
74
- const template = catalog.models[0] || {};
75
- catalog.models.unshift({
76
- ...template,
77
- slug: model,
78
- display_name: formatModelDisplayName(model),
79
- description: `QwenProxy ${model}`,
80
- context_window: 1000000,
81
- max_context_window: 1000000,
82
- });
83
- fs.writeFileSync(catalogPath, JSON.stringify(catalog, null, 2), "utf-8");
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
- fs.writeFileSync(filePath, content.trimEnd() + "\n", "utf-8");
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
- 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, "");
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
- fs.writeFileSync(filePath, content.trimEnd() + "\n", "utf-8");
130
- manuallyCleaned = true;
131
- }
132
- } catch {}
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 = process.env.API_KEY || process.env.ADMIN_PASSWORD || configKey;
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: process.env.CODEX_HOME
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],
@@ -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 newAcc = addAccount(email, password);
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 && !process.env.NODE_TEST_CONTEXT) {
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
- this.setStatusMessage(theme.red(`✗ Erro ao salvar: ${err?.message || String(err)}`));
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
- this.modalHoveredField === "save"
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
- // For full copy of "all" filter, prefer the complete persistent file from disk if available
474
- if (this.filter === "all" && !isRunningUnderNodeTest()) {
475
- try {
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;