qwenproxy-cli 1.0.30 → 1.0.32
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 +1 -1
- package/src/routes/chat/account.ts +12 -3
- package/src/services/playwright.ts +8 -0
- package/src/services/qwen.ts +22 -23
- package/src/sync/cline.ts +1 -1
- package/src/sync/index.ts +125 -81
- package/src/sync-clients.ts +2 -2
- package/src/tui/views/sync-view.ts +53 -47
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qwenproxy-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.32",
|
|
4
4
|
"description": "High-performance OpenAI & Anthropic compatible API gateway for Qwen with multi-account rotation, interactive TUI, and resilient tool calling.",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"bin": {
|
|
@@ -35,7 +35,11 @@ import {
|
|
|
35
35
|
type AccountLease,
|
|
36
36
|
} from "../../core/account-concurrency.ts";
|
|
37
37
|
import { isAuthMockEnabled } from "../../services/auth-playwright.ts";
|
|
38
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
isPlaywrightInitialized,
|
|
40
|
+
isAccountRecentlyActive,
|
|
41
|
+
refreshHeaders,
|
|
42
|
+
} from "../../services/playwright.ts";
|
|
39
43
|
import { enqueueOrphanChatDeletion } from "../../services/chat-cleanup.ts";
|
|
40
44
|
import {
|
|
41
45
|
clearAllSessionsForAccount,
|
|
@@ -78,14 +82,19 @@ const MAX_ANTI_BOT_ROTATIONS = 1;
|
|
|
78
82
|
* a stuck account page (closed context / WAF) can otherwise hold each browser
|
|
79
83
|
* op for 60s and keep the personalization mutex blocked for minutes.
|
|
80
84
|
*/
|
|
81
|
-
export const PERSONALIZATION_SYNC_DEADLINE_MS =
|
|
85
|
+
export const PERSONALIZATION_SYNC_DEADLINE_MS = 45_000;
|
|
82
86
|
export const COLD_ACCOUNT_PERSONALIZATION_SYNC_DEADLINE_MS = 60_000;
|
|
83
87
|
|
|
84
88
|
export function computePersonalizationDeadlineMs(
|
|
85
89
|
accountId: string | undefined,
|
|
86
90
|
navigationTimeoutMs = config.timeouts.navigation,
|
|
87
91
|
): number {
|
|
88
|
-
if (
|
|
92
|
+
if (
|
|
93
|
+
accountId &&
|
|
94
|
+
accountId !== "global" &&
|
|
95
|
+
isPlaywrightInitialized(accountId) &&
|
|
96
|
+
isAccountRecentlyActive(accountId, 5 * 60 * 1000)
|
|
97
|
+
) {
|
|
89
98
|
return PERSONALIZATION_SYNC_DEADLINE_MS;
|
|
90
99
|
}
|
|
91
100
|
return Math.max(COLD_ACCOUNT_PERSONALIZATION_SYNC_DEADLINE_MS, navigationTimeoutMs);
|
|
@@ -3342,6 +3342,14 @@ export function isPlaywrightInitialized(accountId: string): boolean {
|
|
|
3342
3342
|
return accountPages.has(accountId);
|
|
3343
3343
|
}
|
|
3344
3344
|
|
|
3345
|
+
export function isAccountRecentlyActive(
|
|
3346
|
+
accountId: string,
|
|
3347
|
+
maxIdleMs = 300_000,
|
|
3348
|
+
): boolean {
|
|
3349
|
+
const last = lastAccountActivity.get(accountId) ?? 0;
|
|
3350
|
+
return last > 0 && Date.now() - last < maxIdleMs;
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3345
3353
|
/**
|
|
3346
3354
|
* Register an account as if it had been initialized, with a chosen last
|
|
3347
3355
|
* activity timestamp. Lets the idle/keep-alive selection be exercised without
|
package/src/services/qwen.ts
CHANGED
|
@@ -2993,19 +2993,6 @@ async function createQwenStreamInternal(
|
|
|
2993
2993
|
const preview = await readResponsePreview(response);
|
|
2994
2994
|
const htmlBody = isHtmlResponseBody(preview);
|
|
2995
2995
|
const antiBotChallenge = isWafChallengeResponse(preview);
|
|
2996
|
-
logger.warn(
|
|
2997
|
-
htmlBody || isHtmlResponseContentType(responseContentType)
|
|
2998
|
-
? "[Qwen] Completion returned HTML instead of SSE"
|
|
2999
|
-
: "[Qwen] Completion returned a non-SSE body",
|
|
3000
|
-
{
|
|
3001
|
-
accountId: accountId ?? "global",
|
|
3002
|
-
chatId: chatSessionId ?? "new",
|
|
3003
|
-
status: response.status,
|
|
3004
|
-
contentType: responseContentType,
|
|
3005
|
-
antiBotChallenge,
|
|
3006
|
-
previewBytes: Buffer.byteLength(preview, "utf8"),
|
|
3007
|
-
},
|
|
3008
|
-
);
|
|
3009
2996
|
|
|
3010
2997
|
if (
|
|
3011
2998
|
antiBotChallenge &&
|
|
@@ -3026,6 +3013,19 @@ async function createQwenStreamInternal(
|
|
|
3026
3013
|
continue;
|
|
3027
3014
|
}
|
|
3028
3015
|
|
|
3016
|
+
logger.warn(
|
|
3017
|
+
htmlBody || isHtmlResponseContentType(responseContentType)
|
|
3018
|
+
? "[Qwen] Completion returned HTML instead of SSE"
|
|
3019
|
+
: "[Qwen] Completion returned a non-SSE body",
|
|
3020
|
+
{
|
|
3021
|
+
accountId: accountId ?? "global",
|
|
3022
|
+
chatId: chatSessionId ?? "new",
|
|
3023
|
+
status: response.status,
|
|
3024
|
+
contentType: responseContentType,
|
|
3025
|
+
antiBotChallenge,
|
|
3026
|
+
previewBytes: Buffer.byteLength(preview, "utf8"),
|
|
3027
|
+
},
|
|
3028
|
+
);
|
|
3029
3029
|
throw withCreatedChatMetadata(
|
|
3030
3030
|
new QwenUpstreamError(
|
|
3031
3031
|
antiBotChallenge
|
|
@@ -3070,16 +3070,6 @@ async function createQwenStreamInternal(
|
|
|
3070
3070
|
const htmlResponse = isHtmlResponseBody(errText);
|
|
3071
3071
|
const antiBotChallenge = isWafChallengeResponse(errText);
|
|
3072
3072
|
if (antiBotChallenge || htmlResponse) {
|
|
3073
|
-
logger.warn(
|
|
3074
|
-
"[Qwen] Completion returned an HTML or anti-bot challenge body instead of SSE.",
|
|
3075
|
-
{
|
|
3076
|
-
accountId: accountId ?? "global",
|
|
3077
|
-
chatId: chatSessionId ?? "new",
|
|
3078
|
-
antiBotChallenge,
|
|
3079
|
-
previewBytes: Buffer.byteLength(errText, "utf8"),
|
|
3080
|
-
},
|
|
3081
|
-
);
|
|
3082
|
-
|
|
3083
3073
|
if (
|
|
3084
3074
|
antiBotChallenge &&
|
|
3085
3075
|
(await retryAfterCaptchaRecovery(
|
|
@@ -3099,6 +3089,15 @@ async function createQwenStreamInternal(
|
|
|
3099
3089
|
continue;
|
|
3100
3090
|
}
|
|
3101
3091
|
|
|
3092
|
+
logger.warn(
|
|
3093
|
+
"[Qwen] Completion returned an HTML or anti-bot challenge body instead of SSE.",
|
|
3094
|
+
{
|
|
3095
|
+
accountId: accountId ?? "global",
|
|
3096
|
+
chatId: chatSessionId ?? "new",
|
|
3097
|
+
antiBotChallenge,
|
|
3098
|
+
previewBytes: Buffer.byteLength(errText, "utf8"),
|
|
3099
|
+
},
|
|
3100
|
+
);
|
|
3102
3101
|
throw withCreatedChatMetadata(
|
|
3103
3102
|
new QwenUpstreamError(
|
|
3104
3103
|
antiBotChallenge
|
package/src/sync/cline.ts
CHANGED
|
@@ -84,7 +84,7 @@ export function syncCline(options: SyncOptions): ClientSyncResult {
|
|
|
84
84
|
backupPath,
|
|
85
85
|
success: true,
|
|
86
86
|
action: backupPath ? "updated" : "created",
|
|
87
|
-
message: `Configured Cline
|
|
87
|
+
message: `Configured Cline in state.vscdb with model ${model} and reasoning effort ${reasoningEffort}`,
|
|
88
88
|
};
|
|
89
89
|
} catch (err: any) {
|
|
90
90
|
return {
|
package/src/sync/index.ts
CHANGED
|
@@ -151,6 +151,98 @@ export interface ClientDetectionStatus {
|
|
|
151
151
|
url?: string;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
export function isExecutableInPath(name: string): boolean {
|
|
155
|
+
const envPath = process.env.PATH || "";
|
|
156
|
+
const dirs = envPath.split(path.delimiter).filter(Boolean);
|
|
157
|
+
const extensions = process.platform === "win32"
|
|
158
|
+
? (process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.toLowerCase())
|
|
159
|
+
: [""];
|
|
160
|
+
|
|
161
|
+
for (const dir of dirs) {
|
|
162
|
+
for (const ext of extensions) {
|
|
163
|
+
const fullPath = path.join(dir, name + ext);
|
|
164
|
+
try {
|
|
165
|
+
if (fs.existsSync(fullPath) && !fs.statSync(fullPath).isDirectory()) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
} catch {}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function isVscodeExtensionInstalled(pattern: RegExp): boolean {
|
|
175
|
+
const home = os.homedir();
|
|
176
|
+
const candidateDirs = [
|
|
177
|
+
path.join(home, ".vscode", "extensions"),
|
|
178
|
+
path.join(home, ".vscode-insiders", "extensions"),
|
|
179
|
+
path.join(home, ".cursor", "extensions"),
|
|
180
|
+
path.join(home, ".windsurf", "extensions"),
|
|
181
|
+
];
|
|
182
|
+
for (const d of candidateDirs) {
|
|
183
|
+
if (fs.existsSync(d)) {
|
|
184
|
+
try {
|
|
185
|
+
const entries = fs.readdirSync(d);
|
|
186
|
+
if (entries.some((e) => pattern.test(e))) return true;
|
|
187
|
+
} catch {}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function isZedInstalled(): boolean {
|
|
194
|
+
if (isExecutableInPath("zed")) return true;
|
|
195
|
+
if (process.platform === "win32") {
|
|
196
|
+
const local = process.env.LOCALAPPDATA || "";
|
|
197
|
+
if (fs.existsSync(path.join(local, "Programs", "Zed"))) return true;
|
|
198
|
+
} else if (process.platform === "darwin") {
|
|
199
|
+
if (fs.existsSync("/Applications/Zed.app")) return true;
|
|
200
|
+
}
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function isMatchingLocalHost(text: string, port = 7936): boolean {
|
|
205
|
+
if (!text) return false;
|
|
206
|
+
const configuredPort = config.server?.port || 7936;
|
|
207
|
+
const ports = new Set([String(port), String(configuredPort), "7936", "3000"]);
|
|
208
|
+
const isLocal = text.includes("127.0.0.1") || text.includes("localhost") || text.includes("0.0.0.0");
|
|
209
|
+
return isLocal && Array.from(ports).some((p) => text.includes(`:${p}`));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function isClientToolInstalled(
|
|
213
|
+
id: SyncClientName,
|
|
214
|
+
targetPath: string,
|
|
215
|
+
isCustomTestPath = false,
|
|
216
|
+
): boolean {
|
|
217
|
+
if (isCustomTestPath) {
|
|
218
|
+
return fs.existsSync(targetPath);
|
|
219
|
+
}
|
|
220
|
+
switch (id) {
|
|
221
|
+
case "hermes":
|
|
222
|
+
return isExecutableInPath("hermes");
|
|
223
|
+
case "openclaw":
|
|
224
|
+
return isExecutableInPath("openclaw") || isExecutableInPath("clawdbot") || isExecutableInPath("moltbot");
|
|
225
|
+
case "aider":
|
|
226
|
+
return isExecutableInPath("aider");
|
|
227
|
+
case "kilo":
|
|
228
|
+
return isExecutableInPath("kilo") || isVscodeExtensionInstalled(/kilo/i);
|
|
229
|
+
case "cline":
|
|
230
|
+
return isExecutableInPath("cline") || isVscodeExtensionInstalled(/cline|zoo-code|roo-cline/i);
|
|
231
|
+
case "zed":
|
|
232
|
+
return isZedInstalled();
|
|
233
|
+
case "claude-code":
|
|
234
|
+
return isExecutableInPath("claude") || fs.existsSync(targetPath);
|
|
235
|
+
case "codex":
|
|
236
|
+
return isExecutableInPath("codex") || fs.existsSync(targetPath);
|
|
237
|
+
case "opencode":
|
|
238
|
+
return isExecutableInPath("opencode") || fs.existsSync(targetPath);
|
|
239
|
+
case "omp":
|
|
240
|
+
return isExecutableInPath("omp") || fs.existsSync(targetPath);
|
|
241
|
+
default:
|
|
242
|
+
return fs.existsSync(targetPath);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
154
246
|
/**
|
|
155
247
|
* Inspects a client configuration file to determine whether the client is installed
|
|
156
248
|
* and whether it is actively configured to route to QwenProxy.
|
|
@@ -161,37 +253,26 @@ export function inspectClientSyncStatus(
|
|
|
161
253
|
port = 7936,
|
|
162
254
|
): ClientDetectionStatus {
|
|
163
255
|
const defaultPaths = getDefaultPaths();
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
? defaultPaths.codex
|
|
170
|
-
: id === "opencode"
|
|
171
|
-
? defaultPaths.openCode
|
|
172
|
-
: id === "omp"
|
|
173
|
-
? defaultPaths.omp
|
|
174
|
-
: id === "hermes"
|
|
175
|
-
? defaultPaths.hermes
|
|
176
|
-
: id === "openclaw"
|
|
177
|
-
? defaultPaths.openClaw
|
|
178
|
-
: id === "kilo"
|
|
179
|
-
? defaultPaths.kilo
|
|
180
|
-
: id === "cline"
|
|
181
|
-
? defaultPaths.cline
|
|
182
|
-
: id === "zed"
|
|
183
|
-
? defaultPaths.zed
|
|
184
|
-
: defaultPaths.aider);
|
|
256
|
+
const defaultTarget = (defaultPaths as Record<string, string>)[id] || "";
|
|
257
|
+
const isCustomTestPath = Boolean(
|
|
258
|
+
filePath && defaultTarget && path.resolve(filePath) !== path.resolve(defaultTarget),
|
|
259
|
+
);
|
|
260
|
+
const targetPath = filePath || defaultTarget;
|
|
185
261
|
|
|
186
262
|
if (!fs.existsSync(targetPath)) {
|
|
187
263
|
return { id, installed: false, synced: false };
|
|
188
264
|
}
|
|
189
265
|
|
|
266
|
+
const toolInstalled = isClientToolInstalled(id, targetPath, isCustomTestPath);
|
|
267
|
+
if (!toolInstalled) {
|
|
268
|
+
return { id, installed: false, synced: false };
|
|
269
|
+
}
|
|
270
|
+
|
|
190
271
|
try {
|
|
191
272
|
if (id === "cline") {
|
|
192
|
-
// Inspect SQLite DB
|
|
193
273
|
let isSynced = false;
|
|
194
274
|
let model: string | undefined;
|
|
275
|
+
let rowExists = false;
|
|
195
276
|
try {
|
|
196
277
|
const db = new Database(targetPath, { readonly: true });
|
|
197
278
|
const row = db
|
|
@@ -201,18 +282,19 @@ export function inspectClientSyncStatus(
|
|
|
201
282
|
.get() as { value: string } | undefined;
|
|
202
283
|
db.close();
|
|
203
284
|
if (row && row.value) {
|
|
285
|
+
rowExists = true;
|
|
204
286
|
const parsed = JSON.parse(row.value);
|
|
205
287
|
const url = parsed.openAiBaseUrl || "";
|
|
206
288
|
model = parsed.openAiModelId;
|
|
207
|
-
isSynced =
|
|
208
|
-
url &&
|
|
209
|
-
(url.includes(String(port)) ||
|
|
210
|
-
url.includes(`127.0.0.1:${port}`) ||
|
|
211
|
-
url.includes(`localhost:${port}`)),
|
|
212
|
-
);
|
|
289
|
+
isSynced = isMatchingLocalHost(url, port);
|
|
213
290
|
}
|
|
214
291
|
} catch {}
|
|
215
|
-
|
|
292
|
+
|
|
293
|
+
const isInstalled = isCustomTestPath
|
|
294
|
+
? true
|
|
295
|
+
: rowExists || isExecutableInPath("cline") || isVscodeExtensionInstalled(/cline|zoo-code|roo-cline/i);
|
|
296
|
+
|
|
297
|
+
return { id, installed: isInstalled, synced: isInstalled && isSynced, model };
|
|
216
298
|
}
|
|
217
299
|
|
|
218
300
|
const raw = fs.readFileSync(targetPath, "utf-8");
|
|
@@ -221,12 +303,8 @@ export function inspectClientSyncStatus(
|
|
|
221
303
|
const data = JSON.parse(raw);
|
|
222
304
|
const url = data?.env?.ANTHROPIC_BASE_URL || "";
|
|
223
305
|
const model = data?.env?.ANTHROPIC_MODEL || data?.model || "";
|
|
224
|
-
const isLocalHost =
|
|
225
|
-
url.includes(String(port)) ||
|
|
226
|
-
url.includes(`127.0.0.1:${port}`) ||
|
|
227
|
-
url.includes(`localhost:${port}`);
|
|
228
306
|
const isSynced =
|
|
229
|
-
|
|
307
|
+
isMatchingLocalHost(url, port) &&
|
|
230
308
|
(model.toLowerCase().includes("qwen") || Boolean(data?.env?.ANTHROPIC_AUTH_TOKEN));
|
|
231
309
|
return {
|
|
232
310
|
id,
|
|
@@ -244,13 +322,11 @@ export function inspectClientSyncStatus(
|
|
|
244
322
|
const model = modelMatch ? modelMatch[1] : undefined;
|
|
245
323
|
const urlMatch = raw.match(/\[model_providers\.qwenproxy\][\s\S]*?base_url\s*=\s*["']([^"']+)["']/);
|
|
246
324
|
const url = urlMatch ? urlMatch[1] : "";
|
|
247
|
-
const
|
|
248
|
-
url && (url.includes(String(port)) || url.includes(`127.0.0.1:${port}`) || url.includes(`localhost:${port}`)),
|
|
249
|
-
);
|
|
325
|
+
const isSynced = hasProvider && isProviderActive && isMatchingLocalHost(url, port);
|
|
250
326
|
return {
|
|
251
327
|
id,
|
|
252
328
|
installed: true,
|
|
253
|
-
synced:
|
|
329
|
+
synced: isSynced,
|
|
254
330
|
model,
|
|
255
331
|
};
|
|
256
332
|
}
|
|
@@ -261,20 +337,13 @@ export function inspectClientSyncStatus(
|
|
|
261
337
|
const data = JSON.parse(raw);
|
|
262
338
|
const provider = data?.provider?.qwenproxy;
|
|
263
339
|
const url = provider?.options?.baseURL || "";
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
url.includes(`127.0.0.1:${port}`) ||
|
|
267
|
-
url.includes(`localhost:${port}`);
|
|
268
|
-
isSynced = Boolean(provider && isLocalHost);
|
|
340
|
+
const isSynced = Boolean(provider && isMatchingLocalHost(url, port));
|
|
341
|
+
return { id, installed: true, synced: isSynced };
|
|
269
342
|
} catch {
|
|
270
343
|
const qwenBlockMatch = raw.match(/"qwenproxy"\s*:\s*\{[\s\S]*?"baseURL"\s*:\s*"([^"]+)"/);
|
|
271
344
|
const url = qwenBlockMatch ? qwenBlockMatch[1] : "";
|
|
272
|
-
isSynced = Boolean(
|
|
273
|
-
|
|
274
|
-
(url.includes(String(port)) ||
|
|
275
|
-
url.includes(`127.0.0.1:${port}`) ||
|
|
276
|
-
url.includes(`localhost:${port}`)),
|
|
277
|
-
);
|
|
345
|
+
const isSynced = Boolean(url && isMatchingLocalHost(url, port));
|
|
346
|
+
return { id, installed: true, synced: isSynced };
|
|
278
347
|
}
|
|
279
348
|
return {
|
|
280
349
|
id,
|
|
@@ -290,12 +359,7 @@ export function inspectClientSyncStatus(
|
|
|
290
359
|
if (ompMatch) {
|
|
291
360
|
const urlMatch = ompMatch[1].match(/baseUrl:\s*(\S+)/);
|
|
292
361
|
url = urlMatch ? urlMatch[1].replace(/['"]/g, "") : undefined;
|
|
293
|
-
isSynced = Boolean(
|
|
294
|
-
url &&
|
|
295
|
-
(url.includes(String(port)) ||
|
|
296
|
-
url.includes(`127.0.0.1:${port}`) ||
|
|
297
|
-
url.includes(`localhost:${port}`)),
|
|
298
|
-
);
|
|
362
|
+
isSynced = Boolean(url && isMatchingLocalHost(url, port));
|
|
299
363
|
}
|
|
300
364
|
return {
|
|
301
365
|
id,
|
|
@@ -306,47 +370,27 @@ export function inspectClientSyncStatus(
|
|
|
306
370
|
}
|
|
307
371
|
|
|
308
372
|
if (id === "hermes") {
|
|
309
|
-
const
|
|
310
|
-
raw.includes(String(port)) ||
|
|
311
|
-
raw.includes(`127.0.0.1:${port}`) ||
|
|
312
|
-
raw.includes(`localhost:${port}`);
|
|
313
|
-
const isSynced = isLocalHost && (raw.includes("qwenproxy") || raw.includes("qwen"));
|
|
373
|
+
const isSynced = isMatchingLocalHost(raw, port) && (raw.includes("qwenproxy") || raw.includes("qwen"));
|
|
314
374
|
return { id, installed: true, synced: isSynced };
|
|
315
375
|
}
|
|
316
376
|
|
|
317
377
|
if (id === "openclaw") {
|
|
318
|
-
const
|
|
319
|
-
raw.includes(String(port)) ||
|
|
320
|
-
raw.includes(`127.0.0.1:${port}`) ||
|
|
321
|
-
raw.includes(`localhost:${port}`);
|
|
322
|
-
const isSynced = isLocalHost && raw.includes("qwenproxy");
|
|
378
|
+
const isSynced = isMatchingLocalHost(raw, port) && raw.includes("qwenproxy");
|
|
323
379
|
return { id, installed: true, synced: isSynced };
|
|
324
380
|
}
|
|
325
381
|
|
|
326
382
|
if (id === "kilo") {
|
|
327
|
-
const
|
|
328
|
-
raw.includes(String(port)) ||
|
|
329
|
-
raw.includes(`127.0.0.1:${port}`) ||
|
|
330
|
-
raw.includes(`localhost:${port}`);
|
|
331
|
-
const isSynced = isLocalHost && raw.includes("qwenproxy");
|
|
383
|
+
const isSynced = isMatchingLocalHost(raw, port) && raw.includes("qwenproxy");
|
|
332
384
|
return { id, installed: true, synced: isSynced };
|
|
333
385
|
}
|
|
334
386
|
|
|
335
387
|
if (id === "zed") {
|
|
336
|
-
const
|
|
337
|
-
raw.includes(String(port)) ||
|
|
338
|
-
raw.includes(`127.0.0.1:${port}`) ||
|
|
339
|
-
raw.includes(`localhost:${port}`);
|
|
340
|
-
const isSynced = isLocalHost && raw.includes("QwenProxy");
|
|
388
|
+
const isSynced = isMatchingLocalHost(raw, port) && raw.includes("QwenProxy");
|
|
341
389
|
return { id, installed: true, synced: isSynced };
|
|
342
390
|
}
|
|
343
391
|
|
|
344
392
|
if (id === "aider") {
|
|
345
|
-
const
|
|
346
|
-
raw.includes(String(port)) ||
|
|
347
|
-
raw.includes(`127.0.0.1:${port}`) ||
|
|
348
|
-
raw.includes(`localhost:${port}`);
|
|
349
|
-
const isSynced = isLocalHost && raw.includes("qwen");
|
|
393
|
+
const isSynced = isMatchingLocalHost(raw, port) && raw.includes("qwen");
|
|
350
394
|
return { id, installed: true, synced: isSynced };
|
|
351
395
|
}
|
|
352
396
|
} catch {
|
|
@@ -544,7 +588,7 @@ export function syncAllClients(options: SyncAllOptions = {}): SyncAllResult {
|
|
|
544
588
|
}
|
|
545
589
|
}
|
|
546
590
|
|
|
547
|
-
// 8. Cline
|
|
591
|
+
// 8. Cline
|
|
548
592
|
if (shouldSync("cline")) {
|
|
549
593
|
const clineExisted = fs.existsSync(paths.cline);
|
|
550
594
|
const clineRes = syncCline({
|
package/src/sync-clients.ts
CHANGED
|
@@ -67,7 +67,7 @@ Exemplos:
|
|
|
67
67
|
npm run sync claude # Sincroniza apenas o Claude Code
|
|
68
68
|
npm run sync openclaw # Sincroniza apenas o OpenClaw
|
|
69
69
|
npm run sync kilo # Sincroniza apenas o Kilo Code
|
|
70
|
-
npm run sync cline # Sincroniza
|
|
70
|
+
npm run sync cline # Sincroniza apenas o Cline
|
|
71
71
|
npm run sync omp # Sincroniza apenas o OMP (Oh My Pi)
|
|
72
72
|
npm run sync codex # Sincroniza apenas o Codex CLI
|
|
73
73
|
npm run sync zed # Sincroniza apenas o Zed Editor
|
|
@@ -108,7 +108,7 @@ async function main() {
|
|
|
108
108
|
{ id: "claude-code", name: "3. Claude Code", path: defaultPaths.claudeCode },
|
|
109
109
|
{ id: "openclaw", name: "4. OpenClaw", path: defaultPaths.openClaw },
|
|
110
110
|
{ id: "kilo", name: "5. Kilo Code", path: defaultPaths.kilo },
|
|
111
|
-
{ id: "cline", name: "6. Cline
|
|
111
|
+
{ id: "cline", name: "6. Cline", path: defaultPaths.cline },
|
|
112
112
|
{ id: "omp", name: "7. OMP (Oh My Pi)", path: defaultPaths.omp },
|
|
113
113
|
{ id: "codex", name: "8. Codex CLI", path: defaultPaths.codex },
|
|
114
114
|
{ id: "zed", name: "9. Zed Editor", path: defaultPaths.zed },
|
|
@@ -11,10 +11,11 @@ import {
|
|
|
11
11
|
getDefaultPaths,
|
|
12
12
|
inspectClientSyncStatus,
|
|
13
13
|
} from "../../sync/index.ts";
|
|
14
|
+
import type { SyncClientName } from "../../sync/types.ts";
|
|
14
15
|
import { fetchLiveModels } from "../proxy-client.ts";
|
|
15
16
|
|
|
16
17
|
interface ClientOption {
|
|
17
|
-
id:
|
|
18
|
+
id: SyncClientName;
|
|
18
19
|
name: string;
|
|
19
20
|
path: string;
|
|
20
21
|
selected: boolean;
|
|
@@ -27,7 +28,7 @@ export class SyncView implements TuiView {
|
|
|
27
28
|
public readonly title = "Sync";
|
|
28
29
|
public readonly tabNumber = 3;
|
|
29
30
|
private clients: ClientOption[] = [];
|
|
30
|
-
private selectedRowIndex = 0; // 0..
|
|
31
|
+
private selectedRowIndex = 0; // 0..9 for clients, 10 for model, 11 for scope, 12 for sync, 13 for restore
|
|
31
32
|
private hoveredActionRow: number | null = null;
|
|
32
33
|
private availableModels = [
|
|
33
34
|
"qwen3.8-max",
|
|
@@ -60,11 +61,17 @@ export class SyncView implements TuiView {
|
|
|
60
61
|
|
|
61
62
|
private detectClients(): void {
|
|
62
63
|
const paths = getDefaultPaths();
|
|
63
|
-
const defs: Array<{ id:
|
|
64
|
-
{ id: "
|
|
65
|
-
{ id: "codex", name: "OpenAI Codex", path: paths.codex },
|
|
64
|
+
const defs: Array<{ id: SyncClientName; name: string; path: string }> = [
|
|
65
|
+
{ id: "hermes", name: "Hermes Agent", path: paths.hermes },
|
|
66
66
|
{ id: "opencode", name: "OpenCode", path: paths.openCode },
|
|
67
|
+
{ id: "claude-code", name: "Claude Code", path: paths.claudeCode },
|
|
68
|
+
{ id: "openclaw", name: "OpenClaw", path: paths.openClaw },
|
|
69
|
+
{ id: "kilo", name: "Kilo Code", path: paths.kilo },
|
|
70
|
+
{ id: "cline", name: "Cline", path: paths.cline },
|
|
67
71
|
{ id: "omp", name: "OMP (Oh My Pi)", path: paths.omp },
|
|
72
|
+
{ id: "codex", name: "Codex CLI", path: paths.codex },
|
|
73
|
+
{ id: "zed", name: "Zed Editor", path: paths.zed },
|
|
74
|
+
{ id: "aider", name: "Aider", path: paths.aider },
|
|
68
75
|
];
|
|
69
76
|
|
|
70
77
|
this.clients = defs.map((d) => {
|
|
@@ -96,23 +103,23 @@ export class SyncView implements TuiView {
|
|
|
96
103
|
const { row, col } = key.mouse;
|
|
97
104
|
const leftW = this.lastLeftW || 46;
|
|
98
105
|
if (col >= 2 && col <= leftW - 1) {
|
|
99
|
-
if (row >= 8 && row <=
|
|
106
|
+
if (row >= 8 && row <= 17) {
|
|
100
107
|
const targetRow = row - 8;
|
|
101
108
|
if (this.selectedRowIndex !== targetRow) {
|
|
102
109
|
this.selectedRowIndex = targetRow;
|
|
103
110
|
return true;
|
|
104
111
|
}
|
|
105
|
-
} else if (row === 14) {
|
|
106
|
-
if (this.selectedRowIndex !==
|
|
107
|
-
this.selectedRowIndex =
|
|
112
|
+
} else if (row === 20 || row === 14) {
|
|
113
|
+
if (this.selectedRowIndex !== 10) {
|
|
114
|
+
this.selectedRowIndex = 10;
|
|
108
115
|
return true;
|
|
109
116
|
}
|
|
110
|
-
} else if (row === 15) {
|
|
111
|
-
if (this.selectedRowIndex !==
|
|
112
|
-
this.selectedRowIndex =
|
|
117
|
+
} else if (row === 21 || row === 15) {
|
|
118
|
+
if (this.selectedRowIndex !== 11) {
|
|
119
|
+
this.selectedRowIndex = 11;
|
|
113
120
|
return true;
|
|
114
121
|
}
|
|
115
|
-
} else if (row === 18 || row === 19) {
|
|
122
|
+
} else if (row === 24 || row === 18 || row === 25 || row === 19) {
|
|
116
123
|
if (this.hoveredActionRow !== row) {
|
|
117
124
|
this.hoveredActionRow = row;
|
|
118
125
|
return true;
|
|
@@ -132,8 +139,8 @@ export class SyncView implements TuiView {
|
|
|
132
139
|
const { row, col } = key.mouse;
|
|
133
140
|
const leftW = this.lastLeftW || 46;
|
|
134
141
|
if (col >= 2 && col <= leftW - 1) {
|
|
135
|
-
// Rows 8
|
|
136
|
-
if (row >= 8 && row <=
|
|
142
|
+
// Rows 8..17: Toggle client
|
|
143
|
+
if (row >= 8 && row <= 17) {
|
|
137
144
|
const client = this.clients[row - 8];
|
|
138
145
|
if (client) {
|
|
139
146
|
client.selected = !client.selected;
|
|
@@ -141,27 +148,27 @@ export class SyncView implements TuiView {
|
|
|
141
148
|
return true;
|
|
142
149
|
}
|
|
143
150
|
}
|
|
144
|
-
//
|
|
145
|
-
if (row === 14) {
|
|
151
|
+
// Model selector
|
|
152
|
+
if (row === 20 || row === 14) {
|
|
146
153
|
this.modelIndex = (this.modelIndex + 1) % this.availableModels.length;
|
|
147
|
-
this.selectedRowIndex =
|
|
154
|
+
this.selectedRowIndex = 10;
|
|
148
155
|
return true;
|
|
149
156
|
}
|
|
150
|
-
//
|
|
151
|
-
if (row === 15) {
|
|
157
|
+
// Scope selector
|
|
158
|
+
if (row === 21 || row === 15) {
|
|
152
159
|
this.syncAllModels = !this.syncAllModels;
|
|
153
|
-
this.selectedRowIndex =
|
|
160
|
+
this.selectedRowIndex = 11;
|
|
154
161
|
return true;
|
|
155
162
|
}
|
|
156
|
-
//
|
|
157
|
-
if (row === 18) {
|
|
158
|
-
this.selectedRowIndex =
|
|
163
|
+
// Sincronizar button
|
|
164
|
+
if (row === 24 || row === 18) {
|
|
165
|
+
this.selectedRowIndex = 12;
|
|
159
166
|
this.executeSync();
|
|
160
167
|
return true;
|
|
161
168
|
}
|
|
162
|
-
//
|
|
163
|
-
if (row === 19) {
|
|
164
|
-
this.selectedRowIndex =
|
|
169
|
+
// Restaurar button
|
|
170
|
+
if (row === 25 || row === 19) {
|
|
171
|
+
this.selectedRowIndex = 13;
|
|
165
172
|
this.executeRollback();
|
|
166
173
|
return true;
|
|
167
174
|
}
|
|
@@ -174,28 +181,28 @@ export class SyncView implements TuiView {
|
|
|
174
181
|
return true;
|
|
175
182
|
}
|
|
176
183
|
if (key.name === "down" || key.name === "wheeldown" || (key.name === "j" && !key.ctrl)) {
|
|
177
|
-
this.selectedRowIndex = Math.min(
|
|
184
|
+
this.selectedRowIndex = Math.min(13, this.selectedRowIndex + 1);
|
|
178
185
|
return true;
|
|
179
186
|
}
|
|
180
187
|
|
|
181
188
|
// Toggle client selection with Space
|
|
182
189
|
if (key.name === "space") {
|
|
183
|
-
if (this.selectedRowIndex <
|
|
190
|
+
if (this.selectedRowIndex < this.clients.length) {
|
|
184
191
|
const client = this.clients[this.selectedRowIndex];
|
|
185
192
|
if (client) {
|
|
186
193
|
client.selected = !client.selected;
|
|
187
194
|
}
|
|
188
|
-
} else if (this.selectedRowIndex ===
|
|
195
|
+
} else if (this.selectedRowIndex === 10) {
|
|
189
196
|
// Cycle model with space
|
|
190
197
|
this.modelIndex = (this.modelIndex + 1) % this.availableModels.length;
|
|
191
|
-
} else if (this.selectedRowIndex ===
|
|
198
|
+
} else if (this.selectedRowIndex === 11) {
|
|
192
199
|
this.syncAllModels = !this.syncAllModels;
|
|
193
200
|
}
|
|
194
201
|
return true;
|
|
195
202
|
}
|
|
196
203
|
|
|
197
|
-
// Cycle model left/right on row
|
|
198
|
-
if (this.selectedRowIndex ===
|
|
204
|
+
// Cycle model left/right on row 10
|
|
205
|
+
if (this.selectedRowIndex === 10 && (key.name === "left" || key.name === "right")) {
|
|
199
206
|
if (key.name === "left") {
|
|
200
207
|
this.modelIndex =
|
|
201
208
|
(this.modelIndex - 1 + this.availableModels.length) %
|
|
@@ -205,7 +212,6 @@ export class SyncView implements TuiView {
|
|
|
205
212
|
}
|
|
206
213
|
return true;
|
|
207
214
|
}
|
|
208
|
-
|
|
209
215
|
// Toggle all with 'a'
|
|
210
216
|
if (key.name === "a" && !key.ctrl) {
|
|
211
217
|
const allSelected = this.clients.every((c) => c.selected);
|
|
@@ -226,7 +232,7 @@ export class SyncView implements TuiView {
|
|
|
226
232
|
|
|
227
233
|
// Confirm action on Enter
|
|
228
234
|
if (key.name === "return") {
|
|
229
|
-
if (this.selectedRowIndex ===
|
|
235
|
+
if (this.selectedRowIndex === 13) {
|
|
230
236
|
this.executeRollback();
|
|
231
237
|
} else {
|
|
232
238
|
this.executeSync();
|
|
@@ -289,8 +295,8 @@ export class SyncView implements TuiView {
|
|
|
289
295
|
}
|
|
290
296
|
|
|
291
297
|
public render(width: number, height: number): string[] {
|
|
292
|
-
const contentH = Math.max(
|
|
293
|
-
const leftW = Math.max(
|
|
298
|
+
const contentH = Math.max(22, height);
|
|
299
|
+
const leftW = Math.max(48, Math.floor(width * 0.52));
|
|
294
300
|
this.lastLeftW = leftW;
|
|
295
301
|
const rightW = Math.max(30, width - leftW - 1);
|
|
296
302
|
|
|
@@ -323,8 +329,8 @@ export class SyncView implements TuiView {
|
|
|
323
329
|
leftContent.push("");
|
|
324
330
|
leftContent.push(` ${theme.bold("Modelo:")}`);
|
|
325
331
|
|
|
326
|
-
// Row index
|
|
327
|
-
const isModelFocused = this.selectedRowIndex ===
|
|
332
|
+
// Row index 10: Model Selector
|
|
333
|
+
const isModelFocused = this.selectedRowIndex === 10;
|
|
328
334
|
const modelPointer = isModelFocused ? theme.cyan(`${glyphs.pointer} `) : " ";
|
|
329
335
|
const currentModel = this.availableModels[this.modelIndex] || "qwen3.8-max";
|
|
330
336
|
const modelText = `${currentModel} (${this.modelIndex + 1}/${this.availableModels.length})`;
|
|
@@ -335,23 +341,23 @@ export class SyncView implements TuiView {
|
|
|
335
341
|
|
|
336
342
|
leftContent.push(isModelFocused ? theme.bgSelected(modelLine) : modelLine);
|
|
337
343
|
|
|
338
|
-
// Row index
|
|
339
|
-
const isScopeFocused = this.selectedRowIndex ===
|
|
344
|
+
// Row index 11: Scope Selector
|
|
345
|
+
const isScopeFocused = this.selectedRowIndex === 11;
|
|
340
346
|
const scopePointer = isScopeFocused ? theme.cyan(`${glyphs.pointer} `) : " ";
|
|
341
347
|
const scopeCheck = this.syncAllModels ? theme.green(glyphs.radioOn) : theme.muted(glyphs.radioOff);
|
|
342
348
|
const scopeLine = `${scopePointer}${scopeCheck} Registrar todos os modelos`;
|
|
343
349
|
leftContent.push(isScopeFocused ? theme.bgSelected(scopeLine) : scopeLine);
|
|
344
350
|
leftContent.push("");
|
|
345
351
|
leftContent.push(` ${theme.bold("Ações:")}`);
|
|
346
|
-
// Row
|
|
347
|
-
const isSyncFocused = this.selectedRowIndex ===
|
|
348
|
-
const isSyncHovered = this.hoveredActionRow === 18;
|
|
352
|
+
// Row index 12: Sincronizar
|
|
353
|
+
const isSyncFocused = this.selectedRowIndex === 12;
|
|
354
|
+
const isSyncHovered = this.hoveredActionRow === 24 || this.hoveredActionRow === 18;
|
|
349
355
|
const syncLine = ` ${isSyncHovered || isSyncFocused ? theme.bgHover(` ${theme.cyan("[ Enter ] Sincronizar")} `) : `${theme.cyan("[ Enter ]")} Sincronizar`}`;
|
|
350
356
|
leftContent.push(syncLine);
|
|
351
357
|
|
|
352
|
-
// Row
|
|
353
|
-
const isRestoreFocused = this.selectedRowIndex ===
|
|
354
|
-
const isRestoreHovered = this.hoveredActionRow === 19;
|
|
358
|
+
// Row index 13: Restaurar
|
|
359
|
+
const isRestoreFocused = this.selectedRowIndex === 13;
|
|
360
|
+
const isRestoreHovered = this.hoveredActionRow === 25 || this.hoveredActionRow === 19;
|
|
355
361
|
const restoreLine = ` ${isRestoreHovered || isRestoreFocused ? theme.bgHover(` ${theme.yellow("[ R ] Restaurar")} `) : `${theme.yellow("[ R ]")} Restaurar`}`;
|
|
356
362
|
leftContent.push(restoreLine);
|
|
357
363
|
|