qwenproxy-cli 1.3.5 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/qwenproxy.js +26 -8
- package/dist/clean-cache.js +5076 -0
- package/dist/delete-chats.js +9859 -0
- package/dist/index.js +33946 -0
- package/dist/login.js +6421 -0
- package/dist/reset-cooldowns.js +5462 -0
- package/dist/sync-clients.js +2824 -0
- package/package.json +7 -3
- package/src/api/server.ts +27 -10
- package/src/core/accounts.ts +2 -1
- package/src/core/config.ts +1 -1
- package/src/core/database.ts +139 -0
- package/src/core/paths.ts +0 -6
- package/src/core/version.ts +22 -0
- package/src/routes/chat/retry-policy.ts +4 -0
- package/src/routes/upload.ts +31 -10
- package/src/services/playwright.ts +403 -117
- package/src/services/qwen-chat-pool.ts +26 -0
- package/src/services/qwen-headers.ts +8 -0
- package/src/services/qwen.ts +105 -37
- package/src/tools/parser.ts +7 -1
- package/src/tui/app.ts +2 -12
- package/src/tui/server-manager.ts +3 -1
|
@@ -0,0 +1,2824 @@
|
|
|
1
|
+
// src/sync-clients.ts
|
|
2
|
+
import fs14 from "node:fs";
|
|
3
|
+
import dotenv from "dotenv";
|
|
4
|
+
|
|
5
|
+
// src/core/paths.ts
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
function isRunningUnderNodeTest() {
|
|
10
|
+
return process.argv.some(
|
|
11
|
+
(arg) => arg === "--test" || arg.includes("src/tests/") || arg.includes("src\\tests\\")
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
function getOsGlobalDataDir(options) {
|
|
15
|
+
const currentPlatform = options?.platform || process.platform;
|
|
16
|
+
const home = options?.homeDir || os.homedir();
|
|
17
|
+
if (currentPlatform === "win32") {
|
|
18
|
+
const winPath = path.win32 || path;
|
|
19
|
+
const appData = options?.appData || process.env.APPDATA || winPath.join(home, "AppData", "Roaming");
|
|
20
|
+
return winPath.join(appData, "qwenproxy");
|
|
21
|
+
}
|
|
22
|
+
if (currentPlatform === "darwin") {
|
|
23
|
+
return path.join(home, "Library", "Application Support", "qwenproxy");
|
|
24
|
+
}
|
|
25
|
+
const xdgDataHome = options?.xdgDataHome || process.env.XDG_DATA_HOME || path.join(home, ".local", "share");
|
|
26
|
+
return path.join(xdgDataHome, "qwenproxy");
|
|
27
|
+
}
|
|
28
|
+
function resolveDataDir(options) {
|
|
29
|
+
const envDir = options?.envDataDir ?? process.env.QWEN_DATA_DIR;
|
|
30
|
+
if (envDir && envDir.trim().length > 0) {
|
|
31
|
+
return path.resolve(envDir.trim());
|
|
32
|
+
}
|
|
33
|
+
const isTest = options?.isNodeTest ?? isRunningUnderNodeTest();
|
|
34
|
+
if (isTest) {
|
|
35
|
+
return path.resolve("data-test");
|
|
36
|
+
}
|
|
37
|
+
const localDir = path.resolve("data");
|
|
38
|
+
const localExists = options?.localDataExists ?? (fs.existsSync(path.join(localDir, "db", "qwenproxy.db")) || fs.existsSync(localDir));
|
|
39
|
+
if (localExists) {
|
|
40
|
+
return localDir;
|
|
41
|
+
}
|
|
42
|
+
return getOsGlobalDataDir({
|
|
43
|
+
platform: options?.platform,
|
|
44
|
+
appData: options?.appData,
|
|
45
|
+
homeDir: options?.homeDir,
|
|
46
|
+
xdgDataHome: options?.xdgDataHome
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function getDataDir() {
|
|
50
|
+
return resolveDataDir();
|
|
51
|
+
}
|
|
52
|
+
function getDbDir(customDataDir) {
|
|
53
|
+
return path.join(customDataDir || getDataDir(), "db");
|
|
54
|
+
}
|
|
55
|
+
function getProfilesDir(customDataDir) {
|
|
56
|
+
return path.join(customDataDir || getDataDir(), "qwen_profiles");
|
|
57
|
+
}
|
|
58
|
+
function getSyncStatePath(customDataDir) {
|
|
59
|
+
return path.join(customDataDir || getDataDir(), "sync-state.json");
|
|
60
|
+
}
|
|
61
|
+
function getEnvFilePath(customDataDir) {
|
|
62
|
+
if (isRunningUnderNodeTest()) {
|
|
63
|
+
return path.join(customDataDir || getDataDir(), ".env");
|
|
64
|
+
}
|
|
65
|
+
const localEnv = path.resolve(".env");
|
|
66
|
+
if (fs.existsSync(localEnv)) {
|
|
67
|
+
return localEnv;
|
|
68
|
+
}
|
|
69
|
+
return path.join(customDataDir || getDataDir(), ".env");
|
|
70
|
+
}
|
|
71
|
+
function getLogsDir(customDataDir) {
|
|
72
|
+
return path.join(customDataDir || getDataDir(), "logs");
|
|
73
|
+
}
|
|
74
|
+
function ensureDataDirs(targetDataDir) {
|
|
75
|
+
const root = targetDataDir || getDataDir();
|
|
76
|
+
const dbDir = getDbDir(root);
|
|
77
|
+
const profilesDir = getProfilesDir(root);
|
|
78
|
+
const logsDir = getLogsDir(root);
|
|
79
|
+
try {
|
|
80
|
+
if (!fs.existsSync(root)) fs.mkdirSync(root, { recursive: true });
|
|
81
|
+
if (!fs.existsSync(dbDir)) fs.mkdirSync(dbDir, { recursive: true });
|
|
82
|
+
if (!fs.existsSync(profilesDir)) fs.mkdirSync(profilesDir, { recursive: true });
|
|
83
|
+
if (!fs.existsSync(logsDir)) fs.mkdirSync(logsDir, { recursive: true });
|
|
84
|
+
} catch (err) {
|
|
85
|
+
console.error(`[Paths] Error ensuring data directories at ${root}:`, err?.message || String(err));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/sync/index.ts
|
|
90
|
+
import "dotenv/config";
|
|
91
|
+
import fs13 from "node:fs";
|
|
92
|
+
import path13 from "node:path";
|
|
93
|
+
import os3 from "node:os";
|
|
94
|
+
import Database2 from "better-sqlite3";
|
|
95
|
+
|
|
96
|
+
// src/core/config.ts
|
|
97
|
+
import { z } from "zod";
|
|
98
|
+
var envSchema = z.object({
|
|
99
|
+
PORT: z.string().regex(/^\d+$/, "PORT must be a number").refine((value) => {
|
|
100
|
+
const port = Number(value);
|
|
101
|
+
return port >= 1 && port <= 65535;
|
|
102
|
+
}, "PORT must be between 1 and 65535").default("7936"),
|
|
103
|
+
HOST: z.string().default("127.0.0.1"),
|
|
104
|
+
INTERNAL_HOST: z.string().default("127.0.0.1"),
|
|
105
|
+
USER_AGENT: z.string().default(
|
|
106
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36"
|
|
107
|
+
),
|
|
108
|
+
QWEN_BX_V: z.string().default("2.5.37"),
|
|
109
|
+
// Version header on Qwen API requests = the deployed web bundle version
|
|
110
|
+
// (fallback when dynamic DOM detection is not yet completed).
|
|
111
|
+
// Override via env when Qwen ships a new bundle.
|
|
112
|
+
QWEN_WEB_VERSION: z.string().default("0.2.91"),
|
|
113
|
+
// Controls bx-ua/bx-umidtoken injection on the GENERAL API paths
|
|
114
|
+
// (chats/new, settings): those work without them (live-probed). The
|
|
115
|
+
// completions path is the exception — the 0.2.86 HAR shows the real
|
|
116
|
+
// client POSTs completions WITH bx-ua, so it always includes the captured
|
|
117
|
+
// tokens regardless of this flag (buildCompletionHeaders). Set true
|
|
118
|
+
// to inject them everywhere (legacy behavior).
|
|
119
|
+
QWEN_SEND_BX_UA: z.string().default("false"),
|
|
120
|
+
// Conversation mode: "thread" (default) reuses the upstream Qwen chat via
|
|
121
|
+
// parent_id and sends the thread-native delta; "temp" creates a NEW Qwen
|
|
122
|
+
// temp chat (chat_mode:"local") for every request and sends the full
|
|
123
|
+
// history inline (OpenAI standard). Temp chats are ephemeral and never
|
|
124
|
+
// appear in the account's chat list (live-probed).
|
|
125
|
+
QWEN_CHAT_MODE: z.enum([
|
|
126
|
+
"thread",
|
|
127
|
+
"temp",
|
|
128
|
+
"temp-thread",
|
|
129
|
+
"thread-temp",
|
|
130
|
+
"stateless",
|
|
131
|
+
"stateles",
|
|
132
|
+
"stateless-temp",
|
|
133
|
+
"stateles-temp"
|
|
134
|
+
]).default("thread"),
|
|
135
|
+
PLAYWRIGHT_HEADLESS: z.string().default("true"),
|
|
136
|
+
PLAYWRIGHT_BROWSER: z.enum(["chromium", "chrome", "edge"]).default("chromium"),
|
|
137
|
+
PLAYWRIGHT_INIT_BATCH_SIZE: z.string().default("1"),
|
|
138
|
+
PLAYWRIGHT_CONTEXT_CLOSE_TIMEOUT_MS: z.string().default("10000"),
|
|
139
|
+
PLAYWRIGHT_IDLE_CONTEXT_TTL_MS: z.string().default("60000"),
|
|
140
|
+
PLAYWRIGHT_JS_HEAP_MB: z.string().default("256"),
|
|
141
|
+
PLAYWRIGHT_LOW_MEMORY_FLAGS: z.string().default("true"),
|
|
142
|
+
// Keep 2 warm contexts by default ({main + reserve} covers the common
|
|
143
|
+
// failover hop without opening one browser per account): after warmup two
|
|
144
|
+
// browsers stay open; any extra context (simultaneous use / failover) is
|
|
145
|
+
// closed once idle. The cap only evicts IDLE contexts — busy mutexes and
|
|
146
|
+
// active streams are never touched, so concurrent accounts each keep their
|
|
147
|
+
// own context while serving. Accounts in cooldown (rate-limited) sit idle,
|
|
148
|
+
// drop out of the warm set and get evicted.
|
|
149
|
+
PLAYWRIGHT_MAX_ACTIVE_CONTEXTS: z.string().default("2"),
|
|
150
|
+
PLAYWRIGHT_PREPARE_ALL_ON_STARTUP: z.string().default("false"),
|
|
151
|
+
CAPTCHA_SOLVER_ENABLED: z.string().default("true"),
|
|
152
|
+
CAPTCHA_SOLVER_MAX_ATTEMPTS: z.string().default("3"),
|
|
153
|
+
CAPTCHA_SOLVER_TIMEOUT_MS: z.string().default("15000"),
|
|
154
|
+
CAPTCHA_SOLVER_RETRY_DELAY_MS: z.string().default("1000"),
|
|
155
|
+
CAPTCHA_SOLVER_SETTLE_MS: z.string().default("2000"),
|
|
156
|
+
CAPTCHA_ACCOUNT_COOLDOWN_MS: z.string().default("120000"),
|
|
157
|
+
// Cap for the escalating quarantine applied when a challenge could NOT be
|
|
158
|
+
// solved (hard block). Each consecutive hard block doubles the window
|
|
159
|
+
// (base = CAPTCHA_ACCOUNT_COOLDOWN_MS) up to this cap.
|
|
160
|
+
CAPTCHA_HARD_BLOCK_MAX_COOLDOWN_MS: z.string().default("3600000"),
|
|
161
|
+
OSS_MULTIPART_THRESHOLD_MB: z.string().default("5"),
|
|
162
|
+
CHAT_REQUEST_LOG: z.string().default("false"),
|
|
163
|
+
HTTP_TIMEOUT: z.string().default("15000"),
|
|
164
|
+
CHAT_TIMEOUT: z.string().default("180000"),
|
|
165
|
+
NAVIGATION_TIMEOUT: z.string().default("60000"),
|
|
166
|
+
PAGE_TIMEOUT: z.string().default("60000"),
|
|
167
|
+
HEADERS_TIMEOUT: z.string().default("120000"),
|
|
168
|
+
TIME_TO_FIRST_BYTE: z.string().default("60000"),
|
|
169
|
+
IDLE_STREAM_TIMEOUT: z.string().default("60000"),
|
|
170
|
+
// Deadline for the FIRST upstream chunk on thinking models (the reasoning
|
|
171
|
+
// idle of 600s is for gaps AFTER data flows; a stream that produced
|
|
172
|
+
// nothing in this window is dead and should fail fast, retryable).
|
|
173
|
+
QWEN_FIRST_CHUNK_TIMEOUT: z.string().default("60000"),
|
|
174
|
+
TOTAL_REQUEST_TIMEOUT: z.string().default("600000"),
|
|
175
|
+
// Mid-stream silence window for thinking models: 3 min with ZERO upstream
|
|
176
|
+
// bytes is a dead stream (WAF swallow / dropped connection) — fail fast and
|
|
177
|
+
// let the retry policy rotate accounts. Flowing reasoning chunks RESET this
|
|
178
|
+
// timer, so legitimate slow thinking is never cut; only total silence is.
|
|
179
|
+
REASONING_MODEL_TIMEOUT: z.string().default("180000"),
|
|
180
|
+
CACHE_TTL: z.string().default("3600"),
|
|
181
|
+
RESPONSE_TTL: z.string().default("1800"),
|
|
182
|
+
CACHE_COMPRESSION_ENABLED: z.string().default("true"),
|
|
183
|
+
CACHE_COMPRESSION_THRESHOLD: z.string().default("1024"),
|
|
184
|
+
CACHE_COMPRESSION_LEVEL: z.string().default("6"),
|
|
185
|
+
METRICS_INTERVAL: z.string().default("10000"),
|
|
186
|
+
WATCHDOG_INTERVAL: z.string().default("5000"),
|
|
187
|
+
WATCHDOG_FAILURES: z.string().default("3"),
|
|
188
|
+
RAM_WARNING: z.string().default("80"),
|
|
189
|
+
RAM_CRITICAL: z.string().default("95"),
|
|
190
|
+
WS_WARNING: z.string().default("50"),
|
|
191
|
+
WS_CRITICAL: z.string().default("100"),
|
|
192
|
+
RETRY_BASE_DELAY_MS: z.string().default(process.env.TEST_MOCK_QWEN_AUTH === "true" ? "50" : "1000"),
|
|
193
|
+
RETRY_MAX_DELAY_MS: z.string().default(process.env.TEST_MOCK_QWEN_AUTH === "true" ? "200" : "10000"),
|
|
194
|
+
RETRY_MAX_ATTEMPTS: z.string().default("3"),
|
|
195
|
+
RETRY_MAX_ACCOUNT_SWITCHES: z.string().default("2"),
|
|
196
|
+
RETRY_ON_UNKNOWN_UPSTREAM: z.string().default("true"),
|
|
197
|
+
RETRY_AUTO_MALFORMED_TOOLS: z.string().default("true"),
|
|
198
|
+
RETRY_AUTO_MALFORMED_TOOLS_MAX: z.string().default("2"),
|
|
199
|
+
MAX_TOOL_CALLS_PER_TURN: z.string().default("6"),
|
|
200
|
+
QWEN_REPEATED_TOOL_CALL_WARN: z.string().default("2"),
|
|
201
|
+
ACCOUNT_MAX_CONCURRENT_STREAMS: z.string().default("2"),
|
|
202
|
+
ACCOUNT_BUSY_WAIT_MS: z.string().default("30000"),
|
|
203
|
+
// Cap for the "wait forever" account-lease queue (thread owner / last
|
|
204
|
+
// usable account): the queue previously had NO deadline, so a stuck lease
|
|
205
|
+
// holder made the next turn wait up to ~600s. A generous finite cap keeps
|
|
206
|
+
// the wait far above the normal 30s but still bounded.
|
|
207
|
+
ACCOUNT_QUEUE_WAIT_FOREVER_CAP_MS: z.string().default("120000"),
|
|
208
|
+
// Hard deadline for one stream-acquire attempt. A dead account (upstream
|
|
209
|
+
// swallows the completion fetch) otherwise chains metadata/header timeouts
|
|
210
|
+
// for minutes; this fails the attempt visibly so the retry loop switches.
|
|
211
|
+
ACQUIRE_DEADLINE_MS: z.string().default("120000"),
|
|
212
|
+
ACCOUNT_LEASE_MAX_DURATION_MS: z.string().default("600000"),
|
|
213
|
+
ACCOUNT_INIT_FAILURE_COOLDOWN_MS: z.string().default("300000"),
|
|
214
|
+
// Timeout before a request waiting on the CHAT lock gives up. The chat lock
|
|
215
|
+
// is held for the entire stream lifetime, so it must cover the longest
|
|
216
|
+
// legitimate generation (reasoning models with huge contexts can spend
|
|
217
|
+
// 2-3 min producing the first byte chain). A 60s hard cap turned a normal
|
|
218
|
+
// long turn into a 500 for every concurrent request on the same chat
|
|
219
|
+
// (2026-08-22 production log: lock held 130s -> acquire_deadline 120s).
|
|
220
|
+
CHAT_LOCK_TIMEOUT_MS: z.string().regex(/^\d+$/, "CHAT_LOCK_TIMEOUT_MS must be a number").default("180000"),
|
|
221
|
+
STREAM_DISCONNECT_GRACE_MS: z.string().regex(/^\d+$/, "STREAM_DISCONNECT_GRACE_MS must be a number").default("4000"),
|
|
222
|
+
CHAT_IN_PROGRESS_RETRY_DELAY_MS: z.string().default("2000"),
|
|
223
|
+
// Temporarily-busy window after a chat_in_progress: long enough to absorb
|
|
224
|
+
// the upstream chat settle (measured ~1-2s), short enough that the next
|
|
225
|
+
// turn of the sticky owner is not pushed to a cold account with a full
|
|
226
|
+
// context replay (8s caused a needless 13.3s hop in the 20:04 session).
|
|
227
|
+
CHAT_IN_PROGRESS_BUSY_MS: z.string().default("4000"),
|
|
228
|
+
// Same-chat retry budget for chat_in_progress. The upstream chat stays "in
|
|
229
|
+
// progress" for 2-16s after a completed turn (grows with turn size); each
|
|
230
|
+
// retry waits a jittered window (busyMs-based) and NO retry re-sends the
|
|
231
|
+
// full context — the escalation (new chat + full replay on a cold account)
|
|
232
|
+
// was the ~1MB re-upload that made tool loops feel like ~40 minutes. After
|
|
233
|
+
// this budget the request FAILS (thread binding kept) and the client's own
|
|
234
|
+
// retry lands on the settled chat with the delta intact.
|
|
235
|
+
CHAT_IN_PROGRESS_MAX_RETRIES: z.string().default("6"),
|
|
236
|
+
MID_STREAM_FAILOVER_THRESHOLD: z.string().default("2"),
|
|
237
|
+
MID_STREAM_FAILOVER_BUSY_MS: z.string().default("60000"),
|
|
238
|
+
QWEN_BASE_URL: z.string().default("https://chat.qwen.ai"),
|
|
239
|
+
QWEN_CHAT_POOL_SIZE: z.string().default("0"),
|
|
240
|
+
QWEN_CHAT_POOL_MODELS: z.string().default("qwen3.7-plus"),
|
|
241
|
+
QWEN_PERSONALIZATION_FROM_REQUEST: z.string().default("true"),
|
|
242
|
+
QWEN_PERSONALIZATION_VERIFY_GET: z.string().default("true"),
|
|
243
|
+
QWEN_BROWSER_ONLY_FETCH: z.string().default("true"),
|
|
244
|
+
QWEN_MAP_OPENAI_MODELS: z.string().default("true"),
|
|
245
|
+
QWEN_MAX_PROMPT_BYTES: z.string().default("0"),
|
|
246
|
+
QWEN_MAX_PERSONALIZATION_BYTES: z.string().default("500000"),
|
|
247
|
+
CONTEXT_METER_ENABLED: z.string().default("true"),
|
|
248
|
+
CONTEXT_METER_WINDOW_TOKENS: z.string().default("0"),
|
|
249
|
+
CONTEXT_METER_REPORT_USAGE: z.string().default("true"),
|
|
250
|
+
DELETE_ALL_CHATS_ON_SHUTDOWN: z.string().default("false"),
|
|
251
|
+
AUTO_CLEAN_CHATS_ON_STARTUP: z.string().default("true"),
|
|
252
|
+
AUTO_CLEAN_ORPHAN_CHATS: z.string().default("true"),
|
|
253
|
+
AUTO_CLEAN_CHAT_MAX_AGE_HOURS: z.string().default("168"),
|
|
254
|
+
// The Baxia WAF scores live page behavior (pointer/scroll events, open
|
|
255
|
+
// session) — an account whose page sits frozen for minutes returns a low
|
|
256
|
+
// trust score and gets TMD-challenged on the next request. On by default;
|
|
257
|
+
// the keeper skips accounts that are mid-stream or mutex-busy.
|
|
258
|
+
SESSION_KEEP_ALIVE_ENABLED: z.string().default("true"),
|
|
259
|
+
SESSION_KEEP_ALIVE_INTERVAL_MS: z.string().default("180000"),
|
|
260
|
+
SESSION_KEEP_ALIVE_IDLE_MS: z.string().default("120000"),
|
|
261
|
+
SESSION_KEEP_ALIVE_NAVIGATION_INTERVAL_MS: z.string().default("480000"),
|
|
262
|
+
API_KEY: z.string().default(""),
|
|
263
|
+
// Static x-ratelimit-* response headers (OpenAI-shaped, doc §5.2). The proxy
|
|
264
|
+
// does not enforce a token/request quota; these exist for SDK/tool parsing.
|
|
265
|
+
RATE_LIMIT_REQUESTS: z.string().default("5000"),
|
|
266
|
+
RATE_LIMIT_TOKENS: z.string().default("200000")
|
|
267
|
+
});
|
|
268
|
+
var env = envSchema.parse(process.env);
|
|
269
|
+
var config = {
|
|
270
|
+
server: {
|
|
271
|
+
port: parseInt(env.PORT),
|
|
272
|
+
host: env.HOST,
|
|
273
|
+
internalHost: env.INTERNAL_HOST,
|
|
274
|
+
rateLimit: {
|
|
275
|
+
requests: parseInt(env.RATE_LIMIT_REQUESTS),
|
|
276
|
+
tokens: parseInt(env.RATE_LIMIT_TOKENS)
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
logging: {
|
|
280
|
+
chatRequests: env.CHAT_REQUEST_LOG === "true"
|
|
281
|
+
},
|
|
282
|
+
auth: {
|
|
283
|
+
userAgent: env.USER_AGENT,
|
|
284
|
+
bxV: env.QWEN_BX_V
|
|
285
|
+
},
|
|
286
|
+
playwright: {
|
|
287
|
+
headless: env.PLAYWRIGHT_HEADLESS !== "false",
|
|
288
|
+
browser: env.PLAYWRIGHT_BROWSER,
|
|
289
|
+
initBatchSize: Math.max(1, parseInt(env.PLAYWRIGHT_INIT_BATCH_SIZE)),
|
|
290
|
+
contextCloseTimeoutMs: Math.max(
|
|
291
|
+
1e3,
|
|
292
|
+
parseInt(env.PLAYWRIGHT_CONTEXT_CLOSE_TIMEOUT_MS)
|
|
293
|
+
),
|
|
294
|
+
idleContextTtlMs: Math.max(0, parseInt(env.PLAYWRIGHT_IDLE_CONTEXT_TTL_MS)),
|
|
295
|
+
jsHeapMb: Math.max(64, parseInt(env.PLAYWRIGHT_JS_HEAP_MB)),
|
|
296
|
+
lowMemoryFlags: env.PLAYWRIGHT_LOW_MEMORY_FLAGS !== "false",
|
|
297
|
+
maxActiveContexts: Math.max(0, parseInt(env.PLAYWRIGHT_MAX_ACTIVE_CONTEXTS)),
|
|
298
|
+
prepareAllOnStartup: env.PLAYWRIGHT_PREPARE_ALL_ON_STARTUP !== "false"
|
|
299
|
+
},
|
|
300
|
+
captcha: {
|
|
301
|
+
enabled: env.CAPTCHA_SOLVER_ENABLED === "true",
|
|
302
|
+
maxAttempts: Math.max(1, Math.min(5, parseInt(env.CAPTCHA_SOLVER_MAX_ATTEMPTS))),
|
|
303
|
+
timeoutMs: Math.max(0, parseInt(env.CAPTCHA_SOLVER_TIMEOUT_MS)),
|
|
304
|
+
retryDelayMs: Math.max(0, parseInt(env.CAPTCHA_SOLVER_RETRY_DELAY_MS)),
|
|
305
|
+
settleMs: Math.max(0, parseInt(env.CAPTCHA_SOLVER_SETTLE_MS)),
|
|
306
|
+
/** Rest an account whose challenge could not be cleared before reusing it. */
|
|
307
|
+
accountCooldownMs: Math.max(0, parseInt(env.CAPTCHA_ACCOUNT_COOLDOWN_MS)),
|
|
308
|
+
/** Cap for the escalating hard-block quarantine (×2 per consecutive block). */
|
|
309
|
+
hardBlockMaxCooldownMs: Math.max(
|
|
310
|
+
0,
|
|
311
|
+
parseInt(env.CAPTCHA_HARD_BLOCK_MAX_COOLDOWN_MS)
|
|
312
|
+
)
|
|
313
|
+
},
|
|
314
|
+
oss: {
|
|
315
|
+
multipartThresholdBytes: Math.max(
|
|
316
|
+
1 * 1024 * 1024,
|
|
317
|
+
parseInt(env.OSS_MULTIPART_THRESHOLD_MB) * 1024 * 1024
|
|
318
|
+
)
|
|
319
|
+
},
|
|
320
|
+
timeouts: {
|
|
321
|
+
http: parseInt(env.HTTP_TIMEOUT),
|
|
322
|
+
chat: parseInt(env.CHAT_TIMEOUT),
|
|
323
|
+
navigation: parseInt(env.NAVIGATION_TIMEOUT),
|
|
324
|
+
page: parseInt(env.PAGE_TIMEOUT),
|
|
325
|
+
headers: parseInt(env.HEADERS_TIMEOUT),
|
|
326
|
+
timeToFirstByte: parseInt(env.TIME_TO_FIRST_BYTE),
|
|
327
|
+
idleStreamTimeout: parseInt(env.IDLE_STREAM_TIMEOUT),
|
|
328
|
+
totalRequestTimeout: parseInt(env.TOTAL_REQUEST_TIMEOUT),
|
|
329
|
+
reasoningModelTimeout: parseInt(env.REASONING_MODEL_TIMEOUT),
|
|
330
|
+
firstChunkTimeout: parseInt(env.QWEN_FIRST_CHUNK_TIMEOUT)
|
|
331
|
+
},
|
|
332
|
+
cache: {
|
|
333
|
+
defaultTTL: parseInt(env.CACHE_TTL),
|
|
334
|
+
responseTTL: parseInt(env.RESPONSE_TTL),
|
|
335
|
+
compression: {
|
|
336
|
+
enabled: env.CACHE_COMPRESSION_ENABLED !== "false",
|
|
337
|
+
threshold: parseInt(env.CACHE_COMPRESSION_THRESHOLD),
|
|
338
|
+
level: parseInt(env.CACHE_COMPRESSION_LEVEL)
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
metrics: {
|
|
342
|
+
interval: parseInt(env.METRICS_INTERVAL)
|
|
343
|
+
},
|
|
344
|
+
watchdog: {
|
|
345
|
+
checkInterval: parseInt(env.WATCHDOG_INTERVAL),
|
|
346
|
+
consecutiveFailuresThreshold: parseInt(env.WATCHDOG_FAILURES),
|
|
347
|
+
ram: {
|
|
348
|
+
warningThreshold: parseInt(env.RAM_WARNING),
|
|
349
|
+
criticalThreshold: parseInt(env.RAM_CRITICAL)
|
|
350
|
+
},
|
|
351
|
+
streams: {
|
|
352
|
+
warningThreshold: parseInt(env.WS_WARNING),
|
|
353
|
+
criticalThreshold: parseInt(env.WS_CRITICAL)
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
retry: {
|
|
357
|
+
baseDelayMs: parseInt(env.RETRY_BASE_DELAY_MS),
|
|
358
|
+
maxDelayMs: parseInt(env.RETRY_MAX_DELAY_MS),
|
|
359
|
+
maxAttempts: Math.max(1, parseInt(env.RETRY_MAX_ATTEMPTS)),
|
|
360
|
+
maxAccountSwitches: Math.max(0, parseInt(env.RETRY_MAX_ACCOUNT_SWITCHES)),
|
|
361
|
+
onUnknownUpstream: env.RETRY_ON_UNKNOWN_UPSTREAM !== "false",
|
|
362
|
+
chatInProgressDelayMs: Math.max(0, parseInt(env.CHAT_IN_PROGRESS_RETRY_DELAY_MS)),
|
|
363
|
+
chatInProgressBusyMs: Math.max(0, parseInt(env.CHAT_IN_PROGRESS_BUSY_MS)),
|
|
364
|
+
chatInProgressMaxAttempts: Math.max(1, parseInt(env.CHAT_IN_PROGRESS_MAX_RETRIES)),
|
|
365
|
+
midStreamFailoverThreshold: Math.max(
|
|
366
|
+
0,
|
|
367
|
+
parseInt(env.MID_STREAM_FAILOVER_THRESHOLD)
|
|
368
|
+
),
|
|
369
|
+
midStreamFailoverBusyMs: Math.max(
|
|
370
|
+
0,
|
|
371
|
+
parseInt(env.MID_STREAM_FAILOVER_BUSY_MS)
|
|
372
|
+
),
|
|
373
|
+
autoRetryMalformedTools: env.RETRY_AUTO_MALFORMED_TOOLS !== "false",
|
|
374
|
+
autoRetryMalformedToolsMax: Math.max(1, parseInt(env.RETRY_AUTO_MALFORMED_TOOLS_MAX)),
|
|
375
|
+
maxToolCallsPerTurn: Math.max(0, parseInt(env.MAX_TOOL_CALLS_PER_TURN)),
|
|
376
|
+
repeatedToolCallWarnThreshold: Math.max(
|
|
377
|
+
1,
|
|
378
|
+
parseInt(env.QWEN_REPEATED_TOOL_CALL_WARN)
|
|
379
|
+
)
|
|
380
|
+
},
|
|
381
|
+
concurrency: {
|
|
382
|
+
maxStreamsPerAccount: Math.max(1, parseInt(env.ACCOUNT_MAX_CONCURRENT_STREAMS)),
|
|
383
|
+
busyWaitMs: Math.max(
|
|
384
|
+
0,
|
|
385
|
+
Number.isFinite(parseInt(env.ACCOUNT_BUSY_WAIT_MS)) ? parseInt(env.ACCOUNT_BUSY_WAIT_MS) : 3e4
|
|
386
|
+
),
|
|
387
|
+
/** Bound for the "wait forever" lease queue (default 2 min). */
|
|
388
|
+
queueWaitForeverCapMs: Math.max(
|
|
389
|
+
0,
|
|
390
|
+
Number.isFinite(parseInt(env.ACCOUNT_QUEUE_WAIT_FOREVER_CAP_MS)) ? parseInt(env.ACCOUNT_QUEUE_WAIT_FOREVER_CAP_MS) : 12e4
|
|
391
|
+
),
|
|
392
|
+
/** Hard deadline for one stream-acquire attempt (default 2 min). */
|
|
393
|
+
acquireDeadlineMs: Math.max(
|
|
394
|
+
0,
|
|
395
|
+
Number.isFinite(parseInt(env.ACQUIRE_DEADLINE_MS)) ? parseInt(env.ACQUIRE_DEADLINE_MS) : 12e4
|
|
396
|
+
),
|
|
397
|
+
/** Safety net: force-release leases held longer than this (default 10 min). */
|
|
398
|
+
leaseMaxDurationMs: Math.max(
|
|
399
|
+
0,
|
|
400
|
+
parseInt(env.ACCOUNT_LEASE_MAX_DURATION_MS)
|
|
401
|
+
),
|
|
402
|
+
initFailureCooldownMs: Math.max(
|
|
403
|
+
3e4,
|
|
404
|
+
parseInt(env.ACCOUNT_INIT_FAILURE_COOLDOWN_MS)
|
|
405
|
+
),
|
|
406
|
+
/** Max time a request waits on the per-chat lock (default 3 min). */
|
|
407
|
+
chatLockTimeoutMs: Math.max(
|
|
408
|
+
0,
|
|
409
|
+
parseInt(env.CHAT_LOCK_TIMEOUT_MS)
|
|
410
|
+
)
|
|
411
|
+
},
|
|
412
|
+
stream: {
|
|
413
|
+
disconnectGraceMs: Math.max(
|
|
414
|
+
0,
|
|
415
|
+
parseInt(env.STREAM_DISCONNECT_GRACE_MS)
|
|
416
|
+
)
|
|
417
|
+
},
|
|
418
|
+
sessionKeeper: {
|
|
419
|
+
enabled: env.SESSION_KEEP_ALIVE_ENABLED !== "false",
|
|
420
|
+
intervalMs: parseInt(env.SESSION_KEEP_ALIVE_INTERVAL_MS),
|
|
421
|
+
idleMs: parseInt(env.SESSION_KEEP_ALIVE_IDLE_MS),
|
|
422
|
+
navigationIntervalMs: parseInt(
|
|
423
|
+
env.SESSION_KEEP_ALIVE_NAVIGATION_INTERVAL_MS
|
|
424
|
+
)
|
|
425
|
+
},
|
|
426
|
+
apiKey: env.API_KEY,
|
|
427
|
+
qwen: {
|
|
428
|
+
baseUrl: env.QWEN_BASE_URL,
|
|
429
|
+
chatPoolSize: Math.max(0, parseInt(env.QWEN_CHAT_POOL_SIZE)),
|
|
430
|
+
chatPoolModels: env.QWEN_CHAT_POOL_MODELS.split(",").map((model) => model.trim()).filter(Boolean),
|
|
431
|
+
personalizationFromRequest: env.QWEN_PERSONALIZATION_FROM_REQUEST === "true",
|
|
432
|
+
personalizationVerifyGet: env.QWEN_PERSONALIZATION_VERIFY_GET !== "false",
|
|
433
|
+
/** "thread" (reuse upstream chat) or "temp" (new ephemeral chat per request). */
|
|
434
|
+
/** When true, all requests (personalization, models, media, chat) route exclusively through the browser page (no direct Node fetch). */
|
|
435
|
+
browserOnlyFetch: env.QWEN_BROWSER_ONLY_FETCH !== "false",
|
|
436
|
+
mapOpenAiModels: env.QWEN_MAP_OPENAI_MODELS !== "false",
|
|
437
|
+
chatMode: normalizeChatMode(env.QWEN_CHAT_MODE),
|
|
438
|
+
maxPromptBytes: Math.max(0, parseInt(env.QWEN_MAX_PROMPT_BYTES)),
|
|
439
|
+
maxPersonalizationBytes: Math.max(
|
|
440
|
+
0,
|
|
441
|
+
parseInt(env.QWEN_MAX_PERSONALIZATION_BYTES)
|
|
442
|
+
),
|
|
443
|
+
deleteAllChatsOnShutdown: env.DELETE_ALL_CHATS_ON_SHUTDOWN === "true",
|
|
444
|
+
autoCleanChatsOnStartup: env.AUTO_CLEAN_CHATS_ON_STARTUP !== "false",
|
|
445
|
+
autoCleanOrphanChats: env.AUTO_CLEAN_ORPHAN_CHATS !== "false",
|
|
446
|
+
autoCleanChatMaxAgeHours: Math.max(
|
|
447
|
+
1,
|
|
448
|
+
parseInt(env.AUTO_CLEAN_CHAT_MAX_AGE_HOURS) || 168
|
|
449
|
+
),
|
|
450
|
+
sendBxUa: env.QWEN_SEND_BX_UA === "true",
|
|
451
|
+
/** Deployed web bundle version sent as the `version` API header. */
|
|
452
|
+
webVersion: env.QWEN_WEB_VERSION
|
|
453
|
+
},
|
|
454
|
+
contextMeter: {
|
|
455
|
+
enabled: env.CONTEXT_METER_ENABLED === "true",
|
|
456
|
+
windowTokens: Math.max(0, parseInt(env.CONTEXT_METER_WINDOW_TOKENS)),
|
|
457
|
+
reportUsage: env.CONTEXT_METER_REPORT_USAGE === "true"
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
function normalizeChatMode(mode) {
|
|
461
|
+
if (!mode) return "thread";
|
|
462
|
+
const m = mode.trim().toLowerCase().replace(/_/g, "-");
|
|
463
|
+
if (m === "stateless-temp" || m === "stateles-temp" || m === "temp") {
|
|
464
|
+
return "stateless-temp";
|
|
465
|
+
}
|
|
466
|
+
if (m === "stateless" || m === "stateles") {
|
|
467
|
+
return "stateless";
|
|
468
|
+
}
|
|
469
|
+
if (m === "thread-temp" || m === "temp-thread") {
|
|
470
|
+
return "thread-temp";
|
|
471
|
+
}
|
|
472
|
+
return "thread";
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// src/core/local-auth.ts
|
|
476
|
+
var PLACEHOLDER_API_KEY = "sk-qwenproxy-local";
|
|
477
|
+
function isPlaceholderApiKey(key) {
|
|
478
|
+
const value = (key || "").trim();
|
|
479
|
+
return value.length === 0 || value === PLACEHOLDER_API_KEY;
|
|
480
|
+
}
|
|
481
|
+
function isLoopbackHost(host) {
|
|
482
|
+
const normalized = host.trim().toLowerCase().replace(/^\[|\]$/g, "");
|
|
483
|
+
return normalized === "127.0.0.1" || normalized === "localhost" || normalized === "::1" || normalized === "0:0:0:0:0:0:0:1";
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// src/sync/claude-code.ts
|
|
487
|
+
import fs3 from "node:fs";
|
|
488
|
+
import path3 from "node:path";
|
|
489
|
+
|
|
490
|
+
// src/sync/utils.ts
|
|
491
|
+
import fs2 from "node:fs";
|
|
492
|
+
import path2 from "node:path";
|
|
493
|
+
function createTimestampBackup(filePath) {
|
|
494
|
+
const dir = path2.dirname(filePath);
|
|
495
|
+
const ext = path2.extname(filePath);
|
|
496
|
+
const base = path2.basename(filePath, ext);
|
|
497
|
+
const now = /* @__PURE__ */ new Date();
|
|
498
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
499
|
+
const timestamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}_${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
500
|
+
const backupPath = path2.join(dir, `${base}.qwenproxy.${timestamp}${ext}.bak`);
|
|
501
|
+
fs2.copyFileSync(filePath, backupPath);
|
|
502
|
+
return backupPath;
|
|
503
|
+
}
|
|
504
|
+
function findLatestBackup(filePath) {
|
|
505
|
+
const dir = path2.dirname(filePath);
|
|
506
|
+
const ext = path2.extname(filePath);
|
|
507
|
+
const base = path2.basename(filePath, ext);
|
|
508
|
+
if (!fs2.existsSync(dir)) return void 0;
|
|
509
|
+
try {
|
|
510
|
+
const files = fs2.readdirSync(dir);
|
|
511
|
+
const candidates = files.filter((f) => f.startsWith(base) && f.includes("qwenproxy") && f.endsWith(".bak")).map((f) => path2.join(dir, f)).sort((a, b) => {
|
|
512
|
+
try {
|
|
513
|
+
return fs2.statSync(b).mtimeMs - fs2.statSync(a).mtimeMs;
|
|
514
|
+
} catch {
|
|
515
|
+
return 0;
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
return candidates[0];
|
|
519
|
+
} catch {
|
|
520
|
+
return void 0;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
function restoreFromBackup(filePath, backupPath) {
|
|
524
|
+
const targetBackup = backupPath && fs2.existsSync(backupPath) ? backupPath : findLatestBackup(filePath);
|
|
525
|
+
if (!targetBackup || !fs2.existsSync(targetBackup)) {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
fs2.copyFileSync(targetBackup, filePath);
|
|
529
|
+
try {
|
|
530
|
+
fs2.unlinkSync(targetBackup);
|
|
531
|
+
} catch {
|
|
532
|
+
}
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
function formatModelDisplayName(model) {
|
|
536
|
+
if (model === "qwen3.8-max") return "Qwen 3.8 Max";
|
|
537
|
+
if (model === "qwen3.8-omni-flash") return "Qwen 3.8 Omni Flash";
|
|
538
|
+
if (model === "qwen3.7-plus") return "Qwen 3.7 Plus";
|
|
539
|
+
if (model === "qwen3.6-plus") return "Qwen 3.6 Plus";
|
|
540
|
+
return model.replace(/^qwen/i, "Qwen ").split(/[-_]/).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ").replace(/\s+/g, " ").trim();
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/sync/claude-code.ts
|
|
544
|
+
function syncClaudeCode(options) {
|
|
545
|
+
const { filePath, apiKey, baseUrl, model = "qwen3.8-max" } = options;
|
|
546
|
+
try {
|
|
547
|
+
fs3.mkdirSync(path3.dirname(filePath), { recursive: true });
|
|
548
|
+
let backupPath;
|
|
549
|
+
let existingSettings = {};
|
|
550
|
+
if (fs3.existsSync(filePath)) {
|
|
551
|
+
backupPath = createTimestampBackup(filePath);
|
|
552
|
+
try {
|
|
553
|
+
const content = fs3.readFileSync(filePath, "utf-8");
|
|
554
|
+
existingSettings = JSON.parse(content);
|
|
555
|
+
} catch {
|
|
556
|
+
existingSettings = {};
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const env2 = {
|
|
560
|
+
...existingSettings.env || {},
|
|
561
|
+
ANTHROPIC_BASE_URL: baseUrl,
|
|
562
|
+
ANTHROPIC_AUTH_TOKEN: apiKey,
|
|
563
|
+
ANTHROPIC_MODEL: model,
|
|
564
|
+
ANTHROPIC_CUSTOM_MODEL_OPTION: model,
|
|
565
|
+
ANTHROPIC_CUSTOM_MODEL_OPTION_NAME: formatModelDisplayName(model),
|
|
566
|
+
ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION: `QwenProxy ${model}`,
|
|
567
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: model,
|
|
568
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: "qwen3.7-plus",
|
|
569
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: model,
|
|
570
|
+
CLAUDE_CODE_MAX_CONTEXT_TOKENS: "1000000",
|
|
571
|
+
CLAUDE_CODE_DISABLE_ARTIFACT: "1"
|
|
572
|
+
};
|
|
573
|
+
const updatedSettings = {
|
|
574
|
+
...existingSettings,
|
|
575
|
+
env: env2,
|
|
576
|
+
model,
|
|
577
|
+
enableArtifact: false
|
|
578
|
+
};
|
|
579
|
+
fs3.writeFileSync(filePath, JSON.stringify(updatedSettings, null, 2) + "\n", "utf-8");
|
|
580
|
+
return {
|
|
581
|
+
client: "claude-code",
|
|
582
|
+
filePath,
|
|
583
|
+
backupPath,
|
|
584
|
+
success: true,
|
|
585
|
+
action: backupPath ? "updated" : "created",
|
|
586
|
+
message: `Configured Claude Code with model ${model} and baseUrl ${baseUrl}`
|
|
587
|
+
};
|
|
588
|
+
} catch (err) {
|
|
589
|
+
return {
|
|
590
|
+
client: "claude-code",
|
|
591
|
+
filePath,
|
|
592
|
+
success: false,
|
|
593
|
+
action: "failed",
|
|
594
|
+
error: err?.message || String(err)
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
function restoreClaudeCode(filePath, backupPath) {
|
|
599
|
+
const restoredFromBackup = restoreFromBackup(filePath, backupPath);
|
|
600
|
+
let manuallyCleaned = false;
|
|
601
|
+
if (fs3.existsSync(filePath)) {
|
|
602
|
+
try {
|
|
603
|
+
const data = JSON.parse(fs3.readFileSync(filePath, "utf-8"));
|
|
604
|
+
if (data.env && (data.env.ANTHROPIC_BASE_URL?.includes("7936") || data.env.ANTHROPIC_AUTH_TOKEN === "sk-qwenproxy-local" || data.env.ANTHROPIC_MODEL?.includes("qwen"))) {
|
|
605
|
+
delete data.env.ANTHROPIC_BASE_URL;
|
|
606
|
+
delete data.env.ANTHROPIC_AUTH_TOKEN;
|
|
607
|
+
delete data.env.ANTHROPIC_MODEL;
|
|
608
|
+
delete data.env.ANTHROPIC_CUSTOM_MODEL_OPTION;
|
|
609
|
+
delete data.env.ANTHROPIC_CUSTOM_MODEL_OPTION_NAME;
|
|
610
|
+
delete data.env.ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION;
|
|
611
|
+
delete data.env.ANTHROPIC_DEFAULT_SONNET_MODEL;
|
|
612
|
+
delete data.env.ANTHROPIC_DEFAULT_HAIKU_MODEL;
|
|
613
|
+
delete data.env.ANTHROPIC_DEFAULT_OPUS_MODEL;
|
|
614
|
+
delete data.env.CLAUDE_CODE_MAX_CONTEXT_TOKENS;
|
|
615
|
+
delete data.env.CLAUDE_CODE_DISABLE_ARTIFACT;
|
|
616
|
+
delete data.enableArtifact;
|
|
617
|
+
if (data.model && data.model.toLowerCase().includes("qwen")) {
|
|
618
|
+
delete data.model;
|
|
619
|
+
}
|
|
620
|
+
if (Object.keys(data.env).length === 0) {
|
|
621
|
+
delete data.env;
|
|
622
|
+
}
|
|
623
|
+
fs3.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
624
|
+
manuallyCleaned = true;
|
|
625
|
+
}
|
|
626
|
+
} catch {
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
const success = restoredFromBackup || manuallyCleaned;
|
|
630
|
+
return {
|
|
631
|
+
client: "claude-code",
|
|
632
|
+
filePath,
|
|
633
|
+
backupPath,
|
|
634
|
+
success,
|
|
635
|
+
action: success ? "restored" : "failed",
|
|
636
|
+
message: success ? restoredFromBackup ? "Restored Claude Code settings from backup" : "Removed QwenProxy configuration from Claude Code settings" : "Backup file not found"
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// src/sync/codex.ts
|
|
641
|
+
import fs4 from "node:fs";
|
|
642
|
+
import path4 from "node:path";
|
|
643
|
+
import os2 from "node:os";
|
|
644
|
+
function removeTomlSection(content, sectionHeader) {
|
|
645
|
+
const lines = content.split(/\r?\n/);
|
|
646
|
+
const result = [];
|
|
647
|
+
let inTargetSection = false;
|
|
648
|
+
const targetNorm = sectionHeader.replace(/\s+/g, "");
|
|
649
|
+
for (const line of lines) {
|
|
650
|
+
const trimmed = line.trim();
|
|
651
|
+
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
652
|
+
const headerInside = trimmed.slice(1, -1).trim().replace(/\s+/g, "");
|
|
653
|
+
if (headerInside === targetNorm) {
|
|
654
|
+
inTargetSection = true;
|
|
655
|
+
continue;
|
|
656
|
+
} else {
|
|
657
|
+
inTargetSection = false;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
if (!inTargetSection) {
|
|
661
|
+
result.push(line);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return result.join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
665
|
+
}
|
|
666
|
+
function updateTopLevelKey(content, key, value) {
|
|
667
|
+
const firstSectionIdx = content.search(/^\[/m);
|
|
668
|
+
const topPart = firstSectionIdx === -1 ? content : content.slice(0, firstSectionIdx);
|
|
669
|
+
const restPart = firstSectionIdx === -1 ? "" : content.slice(firstSectionIdx);
|
|
670
|
+
const formattedValue = typeof value === "string" ? `"${value}"` : String(value);
|
|
671
|
+
const keyRegex = new RegExp(`^${key}\\s*=.*$`, "m");
|
|
672
|
+
let newTopPart;
|
|
673
|
+
if (keyRegex.test(topPart)) {
|
|
674
|
+
newTopPart = topPart.replace(keyRegex, `${key} = ${formattedValue}`);
|
|
675
|
+
} else {
|
|
676
|
+
newTopPart = `${key} = ${formattedValue}
|
|
677
|
+
` + topPart.trimStart();
|
|
678
|
+
}
|
|
679
|
+
return newTopPart + restPart;
|
|
680
|
+
}
|
|
681
|
+
function syncCodexSingleFile(targetPath, options) {
|
|
682
|
+
fs4.mkdirSync(path4.dirname(targetPath), { recursive: true });
|
|
683
|
+
let backupPath;
|
|
684
|
+
let content = "";
|
|
685
|
+
if (fs4.existsSync(targetPath)) {
|
|
686
|
+
backupPath = createTimestampBackup(targetPath);
|
|
687
|
+
content = fs4.readFileSync(targetPath, "utf-8");
|
|
688
|
+
}
|
|
689
|
+
const { apiKey, baseUrl, model = "qwen3.8-max", setActive = true } = options;
|
|
690
|
+
const providerBlock = `[model_providers.qwenproxy]
|
|
691
|
+
name = "QwenProxy"
|
|
692
|
+
base_url = "${baseUrl}"
|
|
693
|
+
wire_api = "responses"
|
|
694
|
+
experimental_bearer_token = "${apiKey}"
|
|
695
|
+
`;
|
|
696
|
+
content = removeTomlSection(content, "model_providers.qwenproxy");
|
|
697
|
+
content = content.trimEnd() + (content.length > 0 ? "\n\n" : "") + providerBlock;
|
|
698
|
+
if (setActive) {
|
|
699
|
+
content = updateTopLevelKey(content, "model", model);
|
|
700
|
+
content = updateTopLevelKey(content, "model_provider", "qwenproxy");
|
|
701
|
+
content = updateTopLevelKey(content, "model_context_window", 1e6);
|
|
702
|
+
}
|
|
703
|
+
const catalogMatch = content.match(/^model_catalog_json\s*=\s*["']([^"']+)["']/m);
|
|
704
|
+
if (catalogMatch && catalogMatch[1]) {
|
|
705
|
+
try {
|
|
706
|
+
const rawCatalogPath = catalogMatch[1];
|
|
707
|
+
const catalogPath = path4.isAbsolute(rawCatalogPath) ? rawCatalogPath : path4.resolve(path4.dirname(targetPath), rawCatalogPath);
|
|
708
|
+
if (fs4.existsSync(catalogPath)) {
|
|
709
|
+
const catalog = JSON.parse(fs4.readFileSync(catalogPath, "utf-8"));
|
|
710
|
+
if (Array.isArray(catalog.models)) {
|
|
711
|
+
const hasModel = catalog.models.some((m) => m?.slug === model);
|
|
712
|
+
if (!hasModel) {
|
|
713
|
+
const template = catalog.models[0] || {};
|
|
714
|
+
catalog.models.unshift({
|
|
715
|
+
...template,
|
|
716
|
+
slug: model,
|
|
717
|
+
display_name: formatModelDisplayName(model),
|
|
718
|
+
description: `QwenProxy ${model}`,
|
|
719
|
+
context_window: 1e6,
|
|
720
|
+
max_context_window: 1e6
|
|
721
|
+
});
|
|
722
|
+
fs4.writeFileSync(catalogPath, JSON.stringify(catalog, null, 2), "utf-8");
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
} catch {
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
fs4.writeFileSync(targetPath, content.trimEnd() + "\n", "utf-8");
|
|
730
|
+
return { backupPath };
|
|
731
|
+
}
|
|
732
|
+
function syncCodex(options) {
|
|
733
|
+
const { filePath, baseUrl } = options;
|
|
734
|
+
try {
|
|
735
|
+
const { backupPath } = syncCodexSingleFile(filePath, options);
|
|
736
|
+
const home = os2.homedir();
|
|
737
|
+
const standardPath = path4.join(home, ".codex", "config.toml");
|
|
738
|
+
const codexHomePath = process.env.CODEX_HOME ? path4.join(process.env.CODEX_HOME, "config.toml") : void 0;
|
|
739
|
+
const isStandard = path4.resolve(filePath).toLowerCase() === path4.resolve(standardPath).toLowerCase();
|
|
740
|
+
const isCodexHome = Boolean(codexHomePath) && path4.resolve(filePath).toLowerCase() === path4.resolve(codexHomePath).toLowerCase();
|
|
741
|
+
if ((isStandard || isCodexHome) && !isRunningUnderNodeTest()) {
|
|
742
|
+
const alternatePath = isStandard ? codexHomePath : standardPath;
|
|
743
|
+
if (alternatePath && path4.resolve(alternatePath).toLowerCase() !== path4.resolve(filePath).toLowerCase()) {
|
|
744
|
+
try {
|
|
745
|
+
if (fs4.existsSync(alternatePath) || fs4.existsSync(path4.dirname(alternatePath))) {
|
|
746
|
+
syncCodexSingleFile(alternatePath, options);
|
|
747
|
+
}
|
|
748
|
+
} catch {
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return {
|
|
753
|
+
client: "codex",
|
|
754
|
+
filePath,
|
|
755
|
+
backupPath,
|
|
756
|
+
success: true,
|
|
757
|
+
action: backupPath ? "updated" : "created",
|
|
758
|
+
message: `Configured Codex with provider qwenproxy (${baseUrl})`
|
|
759
|
+
};
|
|
760
|
+
} catch (err) {
|
|
761
|
+
return {
|
|
762
|
+
client: "codex",
|
|
763
|
+
filePath,
|
|
764
|
+
success: false,
|
|
765
|
+
action: "failed",
|
|
766
|
+
error: err?.message || String(err)
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
function restoreCodex(filePath, backupPath) {
|
|
771
|
+
const restoredFromBackup = restoreFromBackup(filePath, backupPath);
|
|
772
|
+
let manuallyCleaned = false;
|
|
773
|
+
const cleanSingleFile = (p) => {
|
|
774
|
+
if (fs4.existsSync(p)) {
|
|
775
|
+
try {
|
|
776
|
+
let content = fs4.readFileSync(p, "utf-8");
|
|
777
|
+
if (content.includes("[model_providers.qwenproxy]") || /^model_provider\s*=\s*["']qwenproxy["']/m.test(content)) {
|
|
778
|
+
content = removeTomlSection(content, "model_providers.qwenproxy");
|
|
779
|
+
content = content.replace(/^model_provider\s*=\s*["']qwenproxy["']\r?\n?/m, "");
|
|
780
|
+
if (/^model\s*=\s*["']qwen/m.test(content)) {
|
|
781
|
+
content = content.replace(/^model\s*=\s*["']qwen[^"']*["']\r?\n?/m, "");
|
|
782
|
+
}
|
|
783
|
+
fs4.writeFileSync(p, content.trimEnd() + "\n", "utf-8");
|
|
784
|
+
manuallyCleaned = true;
|
|
785
|
+
}
|
|
786
|
+
} catch {
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
cleanSingleFile(filePath);
|
|
791
|
+
const home = os2.homedir();
|
|
792
|
+
const standardPath = path4.join(home, ".codex", "config.toml");
|
|
793
|
+
const codexHomePath = process.env.CODEX_HOME ? path4.join(process.env.CODEX_HOME, "config.toml") : void 0;
|
|
794
|
+
const isStandard = path4.resolve(filePath).toLowerCase() === path4.resolve(standardPath).toLowerCase();
|
|
795
|
+
const isCodexHome = Boolean(codexHomePath) && path4.resolve(filePath).toLowerCase() === path4.resolve(codexHomePath).toLowerCase();
|
|
796
|
+
if ((isStandard || isCodexHome) && !isRunningUnderNodeTest()) {
|
|
797
|
+
const alternatePath = isStandard ? codexHomePath : standardPath;
|
|
798
|
+
if (alternatePath && path4.resolve(alternatePath).toLowerCase() !== path4.resolve(filePath).toLowerCase()) {
|
|
799
|
+
cleanSingleFile(alternatePath);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
const success = restoredFromBackup || manuallyCleaned;
|
|
803
|
+
return {
|
|
804
|
+
client: "codex",
|
|
805
|
+
filePath,
|
|
806
|
+
backupPath,
|
|
807
|
+
success,
|
|
808
|
+
action: success ? "restored" : "failed",
|
|
809
|
+
message: success ? restoredFromBackup ? "Restored Codex config from backup" : "Removed QwenProxy configuration from Codex config" : "Backup file not found"
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// src/sync/opencode.ts
|
|
814
|
+
import fs5 from "node:fs";
|
|
815
|
+
import path5 from "node:path";
|
|
816
|
+
function buildOpenCodeProviderObject(baseUrl, apiKey, primaryModel = "qwen3.8-max", models) {
|
|
817
|
+
const modelsObj = {};
|
|
818
|
+
const modelList = Array.from(
|
|
819
|
+
new Set([primaryModel, ...models && models.length > 0 ? models : [primaryModel, "qwen3.7-plus"]].filter(Boolean))
|
|
820
|
+
);
|
|
821
|
+
for (const m of modelList) {
|
|
822
|
+
modelsObj[m] = {
|
|
823
|
+
name: formatModelDisplayName(m),
|
|
824
|
+
limit: { context: 1048576, output: 65536 },
|
|
825
|
+
modalities: { input: ["text", "image"], output: ["text"] },
|
|
826
|
+
reasoning: true,
|
|
827
|
+
variants: {
|
|
828
|
+
low: { effort: "low" },
|
|
829
|
+
medium: { effort: "medium" },
|
|
830
|
+
high: { effort: "high" }
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
return {
|
|
835
|
+
npm: "@ai-sdk/openai-compatible",
|
|
836
|
+
name: "QwenProxy",
|
|
837
|
+
options: {
|
|
838
|
+
baseURL: baseUrl,
|
|
839
|
+
apiKey
|
|
840
|
+
},
|
|
841
|
+
models: modelsObj
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
function findKeyObjectSpan(content, key) {
|
|
845
|
+
const regex = new RegExp(`"${key}"\\s*:\\s*\\{`);
|
|
846
|
+
const match = content.match(regex);
|
|
847
|
+
if (!match || match.index === void 0) return null;
|
|
848
|
+
const startIndex = match.index;
|
|
849
|
+
const braceIndex = content.indexOf("{", startIndex + match[0].length - 1);
|
|
850
|
+
if (braceIndex === -1) return null;
|
|
851
|
+
let depth = 0;
|
|
852
|
+
let inString = false;
|
|
853
|
+
let inLineComment = false;
|
|
854
|
+
let inBlockComment = false;
|
|
855
|
+
let escape = false;
|
|
856
|
+
for (let i = braceIndex; i < content.length; i++) {
|
|
857
|
+
const ch = content[i];
|
|
858
|
+
const nextCh = content[i + 1] || "";
|
|
859
|
+
if (inLineComment) {
|
|
860
|
+
if (ch === "\n") inLineComment = false;
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
if (inBlockComment) {
|
|
864
|
+
if (ch === "*" && nextCh === "/") {
|
|
865
|
+
inBlockComment = false;
|
|
866
|
+
i++;
|
|
867
|
+
}
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
if (inString) {
|
|
871
|
+
if (escape) {
|
|
872
|
+
escape = false;
|
|
873
|
+
} else if (ch === "\\") {
|
|
874
|
+
escape = true;
|
|
875
|
+
} else if (ch === '"') {
|
|
876
|
+
inString = false;
|
|
877
|
+
}
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
if (ch === "/" && nextCh === "/") {
|
|
881
|
+
inLineComment = true;
|
|
882
|
+
i++;
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
if (ch === "/" && nextCh === "*") {
|
|
886
|
+
inBlockComment = true;
|
|
887
|
+
i++;
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
if (ch === '"') {
|
|
891
|
+
inString = true;
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
if (ch === "{") {
|
|
895
|
+
depth++;
|
|
896
|
+
} else if (ch === "}") {
|
|
897
|
+
depth--;
|
|
898
|
+
if (depth === 0) {
|
|
899
|
+
let endIndex = i + 1;
|
|
900
|
+
let hasTrailingComma = false;
|
|
901
|
+
while (endIndex < content.length && /[\s,]/.test(content[endIndex])) {
|
|
902
|
+
if (content[endIndex] === ",") {
|
|
903
|
+
hasTrailingComma = true;
|
|
904
|
+
endIndex++;
|
|
905
|
+
break;
|
|
906
|
+
}
|
|
907
|
+
if (content[endIndex] === "\n") {
|
|
908
|
+
break;
|
|
909
|
+
}
|
|
910
|
+
endIndex++;
|
|
911
|
+
}
|
|
912
|
+
return { start: startIndex, end: endIndex, hasTrailingComma };
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return null;
|
|
917
|
+
}
|
|
918
|
+
function openCodeModelId(model) {
|
|
919
|
+
return `qwenproxy/${model}`;
|
|
920
|
+
}
|
|
921
|
+
function applyOpenCodeActiveModel(content, model, setActive) {
|
|
922
|
+
if (!setActive) return content;
|
|
923
|
+
const id = openCodeModelId(model);
|
|
924
|
+
if (/"model"\s*:/.test(content)) {
|
|
925
|
+
return content.replace(/"model"\s*:\s*("[^"]*"|'[^']*')/, `"model": "${id}"`);
|
|
926
|
+
}
|
|
927
|
+
return content.replace(/\{/, `{
|
|
928
|
+
"model": "${id}",`);
|
|
929
|
+
}
|
|
930
|
+
function syncOpenCode(options) {
|
|
931
|
+
const {
|
|
932
|
+
filePath,
|
|
933
|
+
apiKey,
|
|
934
|
+
baseUrl,
|
|
935
|
+
model = "qwen3.8-max",
|
|
936
|
+
models,
|
|
937
|
+
setActive = true
|
|
938
|
+
} = options;
|
|
939
|
+
try {
|
|
940
|
+
fs5.mkdirSync(path5.dirname(filePath), { recursive: true });
|
|
941
|
+
let backupPath;
|
|
942
|
+
let content = "";
|
|
943
|
+
if (fs5.existsSync(filePath)) {
|
|
944
|
+
backupPath = createTimestampBackup(filePath);
|
|
945
|
+
content = fs5.readFileSync(filePath, "utf-8");
|
|
946
|
+
}
|
|
947
|
+
const providerObj = buildOpenCodeProviderObject(baseUrl, apiKey, model, models);
|
|
948
|
+
const providerJson = JSON.stringify(providerObj, null, 6).split("\n").map((line, idx) => idx === 0 ? line : ` ${line}`).join("\n");
|
|
949
|
+
const qwenEntry = ` "qwenproxy": ${providerJson}`;
|
|
950
|
+
if (!content.trim()) {
|
|
951
|
+
const initial = {
|
|
952
|
+
$schema: "https://opencode.ai/config.json"
|
|
953
|
+
};
|
|
954
|
+
if (setActive) {
|
|
955
|
+
initial.model = openCodeModelId(model);
|
|
956
|
+
}
|
|
957
|
+
initial.provider = {
|
|
958
|
+
qwenproxy: providerObj
|
|
959
|
+
};
|
|
960
|
+
fs5.writeFileSync(filePath, JSON.stringify(initial, null, 2) + "\n", "utf-8");
|
|
961
|
+
} else {
|
|
962
|
+
const existingSpan = findKeyObjectSpan(content, "qwenproxy");
|
|
963
|
+
if (existingSpan) {
|
|
964
|
+
const comma = existingSpan.hasTrailingComma ? "," : "";
|
|
965
|
+
content = content.slice(0, existingSpan.start) + `"qwenproxy": ${providerJson}${comma}` + content.slice(existingSpan.end);
|
|
966
|
+
} else {
|
|
967
|
+
const providerMatch = content.match(/"provider"\s*:\s*\{/);
|
|
968
|
+
if (providerMatch && providerMatch.index !== void 0) {
|
|
969
|
+
const insertIdx = providerMatch.index + providerMatch[0].length;
|
|
970
|
+
content = content.slice(0, insertIdx) + "\n" + qwenEntry + "," + content.slice(insertIdx);
|
|
971
|
+
} else {
|
|
972
|
+
const lastBraceIdx = content.lastIndexOf("}");
|
|
973
|
+
if (lastBraceIdx !== -1) {
|
|
974
|
+
const comma = content.slice(0, lastBraceIdx).trimEnd().endsWith("{") ? "" : ",";
|
|
975
|
+
content = content.slice(0, lastBraceIdx).trimEnd() + `${comma}
|
|
976
|
+
"provider": {
|
|
977
|
+
${qwenEntry}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
`;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
fs5.writeFileSync(
|
|
985
|
+
filePath,
|
|
986
|
+
applyOpenCodeActiveModel(content, model, setActive),
|
|
987
|
+
"utf-8"
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
return {
|
|
991
|
+
client: "opencode",
|
|
992
|
+
filePath,
|
|
993
|
+
backupPath,
|
|
994
|
+
success: true,
|
|
995
|
+
action: backupPath ? "updated" : "created",
|
|
996
|
+
message: `Configured OpenCode with provider qwenproxy (${baseUrl})`
|
|
997
|
+
};
|
|
998
|
+
} catch (err) {
|
|
999
|
+
return {
|
|
1000
|
+
client: "opencode",
|
|
1001
|
+
filePath,
|
|
1002
|
+
success: false,
|
|
1003
|
+
action: "failed",
|
|
1004
|
+
error: err?.message || String(err)
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
function restoreOpenCode(filePath, backupPath) {
|
|
1009
|
+
const restoredFromBackup = restoreFromBackup(filePath, backupPath);
|
|
1010
|
+
let manuallyCleaned = false;
|
|
1011
|
+
if (fs5.existsSync(filePath)) {
|
|
1012
|
+
try {
|
|
1013
|
+
const data = JSON.parse(fs5.readFileSync(filePath, "utf-8"));
|
|
1014
|
+
if (data.provider && data.provider.qwenproxy) {
|
|
1015
|
+
delete data.provider.qwenproxy;
|
|
1016
|
+
if (data.model && data.model.includes("qwen")) {
|
|
1017
|
+
delete data.model;
|
|
1018
|
+
}
|
|
1019
|
+
if (Object.keys(data.provider).length === 0) {
|
|
1020
|
+
delete data.provider;
|
|
1021
|
+
}
|
|
1022
|
+
fs5.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
1023
|
+
manuallyCleaned = true;
|
|
1024
|
+
}
|
|
1025
|
+
} catch {
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
const success = restoredFromBackup || manuallyCleaned;
|
|
1029
|
+
return {
|
|
1030
|
+
client: "opencode",
|
|
1031
|
+
filePath,
|
|
1032
|
+
backupPath,
|
|
1033
|
+
success,
|
|
1034
|
+
action: success ? "restored" : "failed",
|
|
1035
|
+
message: success ? restoredFromBackup ? "Restored OpenCode config from backup" : "Removed QwenProxy configuration from OpenCode config" : "Backup file not found"
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// src/sync/omp.ts
|
|
1040
|
+
import fs6 from "node:fs";
|
|
1041
|
+
import path6 from "node:path";
|
|
1042
|
+
function buildOmpProviderYaml(baseUrl, apiKey, primaryModel = "qwen3.8-max", models) {
|
|
1043
|
+
const modelList = Array.from(
|
|
1044
|
+
new Set([primaryModel, ...models && models.length > 0 ? models : [primaryModel, "qwen3.7-plus"]].filter(Boolean))
|
|
1045
|
+
);
|
|
1046
|
+
const formattedModels = modelList.map(
|
|
1047
|
+
(m) => ` - id: ${m}
|
|
1048
|
+
name: ${formatModelDisplayName(m).replace(/\s+/g, "")}
|
|
1049
|
+
input: [text, image]
|
|
1050
|
+
contextWindow: 1000000
|
|
1051
|
+
maxTokens: 131072
|
|
1052
|
+
reasoning: true
|
|
1053
|
+
thinking:
|
|
1054
|
+
mode: effort
|
|
1055
|
+
efforts: [low, medium, high]`
|
|
1056
|
+
).join("\n");
|
|
1057
|
+
return ` qwenproxy:
|
|
1058
|
+
baseUrl: ${baseUrl}
|
|
1059
|
+
api: openai-completions
|
|
1060
|
+
apiKey: "${apiKey}"
|
|
1061
|
+
compat:
|
|
1062
|
+
supportsStore: true
|
|
1063
|
+
supportsReasoningEffort: true
|
|
1064
|
+
maxTokensField: max_completion_tokens
|
|
1065
|
+
models:
|
|
1066
|
+
${formattedModels}
|
|
1067
|
+
`;
|
|
1068
|
+
}
|
|
1069
|
+
function syncOmp(options) {
|
|
1070
|
+
const { filePath, apiKey, baseUrl, model = "qwen3.8-max", models } = options;
|
|
1071
|
+
try {
|
|
1072
|
+
fs6.mkdirSync(path6.dirname(filePath), { recursive: true });
|
|
1073
|
+
let backupPath;
|
|
1074
|
+
let content = "";
|
|
1075
|
+
if (fs6.existsSync(filePath)) {
|
|
1076
|
+
backupPath = createTimestampBackup(filePath);
|
|
1077
|
+
content = fs6.readFileSync(filePath, "utf-8");
|
|
1078
|
+
}
|
|
1079
|
+
const providerBlock = buildOmpProviderYaml(baseUrl, apiKey, model, models);
|
|
1080
|
+
if (!content.trim()) {
|
|
1081
|
+
content = `providers:
|
|
1082
|
+
${providerBlock}`;
|
|
1083
|
+
} else {
|
|
1084
|
+
const existingQwenRegex = /^ {2}qwenproxy:[\s\S]*?(?=(?:^ {2}[a-zA-Z0-9_-]+:|\Z))/m;
|
|
1085
|
+
if (existingQwenRegex.test(content)) {
|
|
1086
|
+
content = content.replace(existingQwenRegex, providerBlock);
|
|
1087
|
+
} else {
|
|
1088
|
+
const providersMatch = content.match(/^providers:\s*$/m);
|
|
1089
|
+
if (providersMatch && providersMatch.index !== void 0) {
|
|
1090
|
+
const insertIdx = providersMatch.index + providersMatch[0].length;
|
|
1091
|
+
content = content.slice(0, insertIdx) + "\n" + providerBlock + content.slice(insertIdx);
|
|
1092
|
+
} else {
|
|
1093
|
+
content = content.trimEnd() + "\n\nproviders:\n" + providerBlock;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
fs6.writeFileSync(filePath, content.trimEnd() + "\n", "utf-8");
|
|
1098
|
+
return {
|
|
1099
|
+
client: "omp",
|
|
1100
|
+
filePath,
|
|
1101
|
+
backupPath,
|
|
1102
|
+
success: true,
|
|
1103
|
+
action: backupPath ? "updated" : "created",
|
|
1104
|
+
message: `Configured OMP with provider qwenproxy (${baseUrl})`
|
|
1105
|
+
};
|
|
1106
|
+
} catch (err) {
|
|
1107
|
+
return {
|
|
1108
|
+
client: "omp",
|
|
1109
|
+
filePath,
|
|
1110
|
+
success: false,
|
|
1111
|
+
action: "failed",
|
|
1112
|
+
error: err?.message || String(err)
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
function restoreOmp(filePath, backupPath) {
|
|
1117
|
+
const restoredFromBackup = restoreFromBackup(filePath, backupPath);
|
|
1118
|
+
let manuallyCleaned = false;
|
|
1119
|
+
if (fs6.existsSync(filePath)) {
|
|
1120
|
+
try {
|
|
1121
|
+
let content = fs6.readFileSync(filePath, "utf-8");
|
|
1122
|
+
if (content.includes("qwenproxy:")) {
|
|
1123
|
+
const regex = /^\s*qwenproxy:\s*\r?\n(?:^[ \t].*\r?\n?)*/m;
|
|
1124
|
+
content = content.replace(regex, "");
|
|
1125
|
+
fs6.writeFileSync(filePath, content, "utf-8");
|
|
1126
|
+
manuallyCleaned = true;
|
|
1127
|
+
}
|
|
1128
|
+
} catch {
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
const success = restoredFromBackup || manuallyCleaned;
|
|
1132
|
+
return {
|
|
1133
|
+
client: "omp",
|
|
1134
|
+
filePath,
|
|
1135
|
+
backupPath,
|
|
1136
|
+
success,
|
|
1137
|
+
action: success ? "restored" : "failed",
|
|
1138
|
+
message: success ? restoredFromBackup ? "Restored OMP models config from backup" : "Removed QwenProxy configuration from OMP config" : "Backup file not found"
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// src/sync/hermes.ts
|
|
1143
|
+
import fs7 from "node:fs";
|
|
1144
|
+
import path7 from "node:path";
|
|
1145
|
+
function buildHermesModelBlock(baseUrl, apiKey, model = "qwen3.8-max", reasoningEffort = "high") {
|
|
1146
|
+
return `model:
|
|
1147
|
+
default: "${model}"
|
|
1148
|
+
provider: "custom"
|
|
1149
|
+
base_url: "${baseUrl}"
|
|
1150
|
+
api_key: "${apiKey}"
|
|
1151
|
+
reasoning_effort: "${reasoningEffort}"
|
|
1152
|
+
reasoning_overrides:
|
|
1153
|
+
"qwen3.8-max": "${reasoningEffort}"
|
|
1154
|
+
"qwen3.7-plus": "medium"
|
|
1155
|
+
`;
|
|
1156
|
+
}
|
|
1157
|
+
function buildHermesProviderBlock(baseUrl, apiKey) {
|
|
1158
|
+
return ` qwenproxy:
|
|
1159
|
+
base_url: "${baseUrl}"
|
|
1160
|
+
api_mode: chat_completions
|
|
1161
|
+
api_key: "${apiKey}"
|
|
1162
|
+
`;
|
|
1163
|
+
}
|
|
1164
|
+
function syncHermes(options) {
|
|
1165
|
+
const { filePath, apiKey, baseUrl, model = "qwen3.8-max", reasoningEffort = "high" } = options;
|
|
1166
|
+
try {
|
|
1167
|
+
fs7.mkdirSync(path7.dirname(filePath), { recursive: true });
|
|
1168
|
+
let backupPath;
|
|
1169
|
+
let content = "";
|
|
1170
|
+
if (fs7.existsSync(filePath)) {
|
|
1171
|
+
backupPath = createTimestampBackup(filePath);
|
|
1172
|
+
content = fs7.readFileSync(filePath, "utf-8");
|
|
1173
|
+
}
|
|
1174
|
+
const modelBlock = buildHermesModelBlock(baseUrl, apiKey, model, reasoningEffort);
|
|
1175
|
+
const providerBlock = buildHermesProviderBlock(baseUrl, apiKey);
|
|
1176
|
+
const modelRegex = /^model:[\s\S]*?(?=(?:^[a-zA-Z0-9_-]+:|\Z))/m;
|
|
1177
|
+
if (modelRegex.test(content)) {
|
|
1178
|
+
content = content.replace(modelRegex, modelBlock);
|
|
1179
|
+
} else {
|
|
1180
|
+
content = modelBlock + "\n" + content.trimStart();
|
|
1181
|
+
}
|
|
1182
|
+
if (!content.includes("providers:")) {
|
|
1183
|
+
content = content.trimEnd() + "\n\nproviders:\n" + providerBlock;
|
|
1184
|
+
} else {
|
|
1185
|
+
const existingQwenRegex = /^ {2}qwenproxy:[\s\S]*?(?=(?:^ {2}[a-zA-Z0-9_-]+:|^[a-zA-Z0-9_-]+:|\Z))/m;
|
|
1186
|
+
if (existingQwenRegex.test(content)) {
|
|
1187
|
+
content = content.replace(existingQwenRegex, providerBlock);
|
|
1188
|
+
} else {
|
|
1189
|
+
const providersMatch = content.match(/^providers:\s*$/m);
|
|
1190
|
+
if (providersMatch && providersMatch.index !== void 0) {
|
|
1191
|
+
const insertIdx = providersMatch.index + providersMatch[0].length;
|
|
1192
|
+
content = content.slice(0, insertIdx) + "\n" + providerBlock + content.slice(insertIdx);
|
|
1193
|
+
} else {
|
|
1194
|
+
content = content.trimEnd() + "\n\nproviders:\n" + providerBlock;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
fs7.writeFileSync(filePath, content.trimEnd() + "\n", "utf-8");
|
|
1199
|
+
return {
|
|
1200
|
+
client: "hermes",
|
|
1201
|
+
filePath,
|
|
1202
|
+
backupPath,
|
|
1203
|
+
success: true,
|
|
1204
|
+
action: backupPath ? "updated" : "created",
|
|
1205
|
+
message: `Configured Hermes Agent with model ${model} and reasoning effort ${reasoningEffort}`
|
|
1206
|
+
};
|
|
1207
|
+
} catch (err) {
|
|
1208
|
+
return {
|
|
1209
|
+
client: "hermes",
|
|
1210
|
+
filePath,
|
|
1211
|
+
success: false,
|
|
1212
|
+
action: "failed",
|
|
1213
|
+
error: err?.message || String(err)
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
function restoreHermes(filePath, backupPath) {
|
|
1218
|
+
const restored = restoreFromBackup(filePath, backupPath);
|
|
1219
|
+
return {
|
|
1220
|
+
client: "hermes",
|
|
1221
|
+
filePath,
|
|
1222
|
+
backupPath,
|
|
1223
|
+
success: restored,
|
|
1224
|
+
action: restored ? "restored" : "failed",
|
|
1225
|
+
message: restored ? "Restored Hermes config from backup" : "Backup file not found"
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// src/sync/openclaw.ts
|
|
1230
|
+
import fs8 from "node:fs";
|
|
1231
|
+
import path8 from "node:path";
|
|
1232
|
+
function findKeyObjectSpan2(content, key) {
|
|
1233
|
+
const regex = new RegExp(`"${key}"\\s*:\\s*\\{`);
|
|
1234
|
+
const match = content.match(regex);
|
|
1235
|
+
if (!match || match.index === void 0) return null;
|
|
1236
|
+
const startIndex = match.index;
|
|
1237
|
+
const braceIndex = content.indexOf("{", startIndex + match[0].length - 1);
|
|
1238
|
+
if (braceIndex === -1) return null;
|
|
1239
|
+
let depth = 0;
|
|
1240
|
+
let inString = false;
|
|
1241
|
+
let inLineComment = false;
|
|
1242
|
+
let inBlockComment = false;
|
|
1243
|
+
let escape = false;
|
|
1244
|
+
for (let i = braceIndex; i < content.length; i++) {
|
|
1245
|
+
const ch = content[i];
|
|
1246
|
+
const nextCh = content[i + 1] || "";
|
|
1247
|
+
if (inLineComment) {
|
|
1248
|
+
if (ch === "\n") inLineComment = false;
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
if (inBlockComment) {
|
|
1252
|
+
if (ch === "*" && nextCh === "/") {
|
|
1253
|
+
inBlockComment = false;
|
|
1254
|
+
i++;
|
|
1255
|
+
}
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
if (inString) {
|
|
1259
|
+
if (escape) {
|
|
1260
|
+
escape = false;
|
|
1261
|
+
} else if (ch === "\\") {
|
|
1262
|
+
escape = true;
|
|
1263
|
+
} else if (ch === '"') {
|
|
1264
|
+
inString = false;
|
|
1265
|
+
}
|
|
1266
|
+
continue;
|
|
1267
|
+
}
|
|
1268
|
+
if (ch === "/" && nextCh === "/") {
|
|
1269
|
+
inLineComment = true;
|
|
1270
|
+
i++;
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1273
|
+
if (ch === "/" && nextCh === "*") {
|
|
1274
|
+
inBlockComment = true;
|
|
1275
|
+
i++;
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
if (ch === '"') {
|
|
1279
|
+
inString = true;
|
|
1280
|
+
continue;
|
|
1281
|
+
}
|
|
1282
|
+
if (ch === "{") {
|
|
1283
|
+
depth++;
|
|
1284
|
+
} else if (ch === "}") {
|
|
1285
|
+
depth--;
|
|
1286
|
+
if (depth === 0) {
|
|
1287
|
+
let endIndex = i + 1;
|
|
1288
|
+
let hasTrailingComma = false;
|
|
1289
|
+
while (endIndex < content.length && /[\s,]/.test(content[endIndex])) {
|
|
1290
|
+
if (content[endIndex] === ",") {
|
|
1291
|
+
hasTrailingComma = true;
|
|
1292
|
+
endIndex++;
|
|
1293
|
+
break;
|
|
1294
|
+
}
|
|
1295
|
+
if (content[endIndex] === "\n") {
|
|
1296
|
+
break;
|
|
1297
|
+
}
|
|
1298
|
+
endIndex++;
|
|
1299
|
+
}
|
|
1300
|
+
return { start: startIndex, end: endIndex, hasTrailingComma };
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
return null;
|
|
1305
|
+
}
|
|
1306
|
+
function buildOpenClawProviderObject(baseUrl, apiKey, model = "qwen3.8-max", models) {
|
|
1307
|
+
const modelList = Array.from(
|
|
1308
|
+
new Set([model, ...models && models.length > 0 ? models : [model, "qwen3.7-plus"]].filter(Boolean))
|
|
1309
|
+
);
|
|
1310
|
+
const modelEntries = modelList.map((m) => ({
|
|
1311
|
+
id: m,
|
|
1312
|
+
name: formatModelDisplayName(m),
|
|
1313
|
+
reasoning: true,
|
|
1314
|
+
supportsReasoningEffort: true,
|
|
1315
|
+
supportedReasoningEfforts: ["low", "medium", "high"],
|
|
1316
|
+
contextWindow: 1e6,
|
|
1317
|
+
maxTokens: 65536
|
|
1318
|
+
}));
|
|
1319
|
+
return {
|
|
1320
|
+
baseUrl,
|
|
1321
|
+
apiKey,
|
|
1322
|
+
api: "openai-completions",
|
|
1323
|
+
models: modelEntries
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
function syncOpenClaw(options) {
|
|
1327
|
+
const { filePath, apiKey, baseUrl, model = "qwen3.8-max", models, reasoningEffort = "high" } = options;
|
|
1328
|
+
try {
|
|
1329
|
+
fs8.mkdirSync(path8.dirname(filePath), { recursive: true });
|
|
1330
|
+
let backupPath;
|
|
1331
|
+
let content = "";
|
|
1332
|
+
if (fs8.existsSync(filePath)) {
|
|
1333
|
+
backupPath = createTimestampBackup(filePath);
|
|
1334
|
+
content = fs8.readFileSync(filePath, "utf-8");
|
|
1335
|
+
}
|
|
1336
|
+
const providerObj = buildOpenClawProviderObject(baseUrl, apiKey, model, models);
|
|
1337
|
+
const providerJson = JSON.stringify(providerObj, null, 6).split("\n").map((line, idx) => idx === 0 ? line : ` ${line}`).join("\n");
|
|
1338
|
+
const qwenEntry = ` "qwenproxy": ${providerJson}`;
|
|
1339
|
+
if (!content.trim()) {
|
|
1340
|
+
const initial = {
|
|
1341
|
+
models: {
|
|
1342
|
+
mode: "merge",
|
|
1343
|
+
providers: {
|
|
1344
|
+
qwenproxy: providerObj
|
|
1345
|
+
}
|
|
1346
|
+
},
|
|
1347
|
+
agents: {
|
|
1348
|
+
defaults: {
|
|
1349
|
+
model: {
|
|
1350
|
+
primary: `qwenproxy/${model}`
|
|
1351
|
+
},
|
|
1352
|
+
thinking: {
|
|
1353
|
+
effort: reasoningEffort
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
};
|
|
1358
|
+
fs8.writeFileSync(filePath, JSON.stringify(initial, null, 2) + "\n", "utf-8");
|
|
1359
|
+
} else {
|
|
1360
|
+
const existingSpan = findKeyObjectSpan2(content, "qwenproxy");
|
|
1361
|
+
if (existingSpan) {
|
|
1362
|
+
const comma = existingSpan.hasTrailingComma ? "," : "";
|
|
1363
|
+
content = content.slice(0, existingSpan.start) + `"qwenproxy": ${providerJson}${comma}` + content.slice(existingSpan.end);
|
|
1364
|
+
} else {
|
|
1365
|
+
const providersMatch = content.match(/"providers"\s*:\s*\{/);
|
|
1366
|
+
if (providersMatch && providersMatch.index !== void 0) {
|
|
1367
|
+
const insertIdx = providersMatch.index + providersMatch[0].length;
|
|
1368
|
+
content = content.slice(0, insertIdx) + "\n" + qwenEntry + "," + content.slice(insertIdx);
|
|
1369
|
+
} else {
|
|
1370
|
+
const modelsMatch = content.match(/"models"\s*:\s*\{/);
|
|
1371
|
+
if (modelsMatch && modelsMatch.index !== void 0) {
|
|
1372
|
+
const insertIdx = modelsMatch.index + modelsMatch[0].length;
|
|
1373
|
+
const providersBlock = `
|
|
1374
|
+
"providers": {
|
|
1375
|
+
${qwenEntry}
|
|
1376
|
+
},`;
|
|
1377
|
+
content = content.slice(0, insertIdx) + providersBlock + content.slice(insertIdx);
|
|
1378
|
+
} else {
|
|
1379
|
+
const lastBraceIdx = content.lastIndexOf("}");
|
|
1380
|
+
if (lastBraceIdx !== -1) {
|
|
1381
|
+
const comma = content.slice(0, lastBraceIdx).trimEnd().endsWith("{") ? "" : ",";
|
|
1382
|
+
const modelsBlock = `${comma}
|
|
1383
|
+
"models": {
|
|
1384
|
+
"mode": "merge",
|
|
1385
|
+
"providers": {
|
|
1386
|
+
${qwenEntry}
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
`;
|
|
1390
|
+
content = content.slice(0, lastBraceIdx).trimEnd() + modelsBlock + "}\n";
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
fs8.writeFileSync(filePath, content, "utf-8");
|
|
1396
|
+
}
|
|
1397
|
+
return {
|
|
1398
|
+
client: "openclaw",
|
|
1399
|
+
filePath,
|
|
1400
|
+
backupPath,
|
|
1401
|
+
success: true,
|
|
1402
|
+
action: backupPath ? "updated" : "created",
|
|
1403
|
+
message: `Configured OpenClaw with provider qwenproxy (${baseUrl}) and reasoning effort ${reasoningEffort}`
|
|
1404
|
+
};
|
|
1405
|
+
} catch (err) {
|
|
1406
|
+
return {
|
|
1407
|
+
client: "openclaw",
|
|
1408
|
+
filePath,
|
|
1409
|
+
success: false,
|
|
1410
|
+
action: "failed",
|
|
1411
|
+
error: err?.message || String(err)
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
function restoreOpenClaw(filePath, backupPath) {
|
|
1416
|
+
const restored = restoreFromBackup(filePath, backupPath);
|
|
1417
|
+
return {
|
|
1418
|
+
client: "openclaw",
|
|
1419
|
+
filePath,
|
|
1420
|
+
backupPath,
|
|
1421
|
+
success: restored,
|
|
1422
|
+
action: restored ? "restored" : "failed",
|
|
1423
|
+
message: restored ? "Restored OpenClaw config from backup" : "Backup file not found"
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
// src/sync/kilo.ts
|
|
1428
|
+
import fs9 from "node:fs";
|
|
1429
|
+
import path9 from "node:path";
|
|
1430
|
+
function findKeyObjectSpan3(content, key) {
|
|
1431
|
+
const regex = new RegExp(`"${key}"\\s*:\\s*\\{`);
|
|
1432
|
+
const match = content.match(regex);
|
|
1433
|
+
if (!match || match.index === void 0) return null;
|
|
1434
|
+
const startIndex = match.index;
|
|
1435
|
+
const braceIndex = content.indexOf("{", startIndex + match[0].length - 1);
|
|
1436
|
+
if (braceIndex === -1) return null;
|
|
1437
|
+
let depth = 0;
|
|
1438
|
+
let inString = false;
|
|
1439
|
+
let inLineComment = false;
|
|
1440
|
+
let inBlockComment = false;
|
|
1441
|
+
let escape = false;
|
|
1442
|
+
for (let i = braceIndex; i < content.length; i++) {
|
|
1443
|
+
const ch = content[i];
|
|
1444
|
+
const nextCh = content[i + 1] || "";
|
|
1445
|
+
if (inLineComment) {
|
|
1446
|
+
if (ch === "\n") inLineComment = false;
|
|
1447
|
+
continue;
|
|
1448
|
+
}
|
|
1449
|
+
if (inBlockComment) {
|
|
1450
|
+
if (ch === "*" && nextCh === "/") {
|
|
1451
|
+
inBlockComment = false;
|
|
1452
|
+
i++;
|
|
1453
|
+
}
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
if (inString) {
|
|
1457
|
+
if (escape) {
|
|
1458
|
+
escape = false;
|
|
1459
|
+
} else if (ch === "\\") {
|
|
1460
|
+
escape = true;
|
|
1461
|
+
} else if (ch === '"') {
|
|
1462
|
+
inString = false;
|
|
1463
|
+
}
|
|
1464
|
+
continue;
|
|
1465
|
+
}
|
|
1466
|
+
if (ch === "/" && nextCh === "/") {
|
|
1467
|
+
inLineComment = true;
|
|
1468
|
+
i++;
|
|
1469
|
+
continue;
|
|
1470
|
+
}
|
|
1471
|
+
if (ch === "/" && nextCh === "*") {
|
|
1472
|
+
inBlockComment = true;
|
|
1473
|
+
i++;
|
|
1474
|
+
continue;
|
|
1475
|
+
}
|
|
1476
|
+
if (ch === '"') {
|
|
1477
|
+
inString = true;
|
|
1478
|
+
continue;
|
|
1479
|
+
}
|
|
1480
|
+
if (ch === "{") {
|
|
1481
|
+
depth++;
|
|
1482
|
+
} else if (ch === "}") {
|
|
1483
|
+
depth--;
|
|
1484
|
+
if (depth === 0) {
|
|
1485
|
+
let endIndex = i + 1;
|
|
1486
|
+
let hasTrailingComma = false;
|
|
1487
|
+
while (endIndex < content.length && /[\s,]/.test(content[endIndex])) {
|
|
1488
|
+
if (content[endIndex] === ",") {
|
|
1489
|
+
hasTrailingComma = true;
|
|
1490
|
+
endIndex++;
|
|
1491
|
+
break;
|
|
1492
|
+
}
|
|
1493
|
+
if (content[endIndex] === "\n") {
|
|
1494
|
+
break;
|
|
1495
|
+
}
|
|
1496
|
+
endIndex++;
|
|
1497
|
+
}
|
|
1498
|
+
return { start: startIndex, end: endIndex, hasTrailingComma };
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
return null;
|
|
1503
|
+
}
|
|
1504
|
+
function buildKiloProviderObject(baseUrl, apiKey, primaryModel = "qwen3.8-max", models) {
|
|
1505
|
+
const modelsObj = {};
|
|
1506
|
+
const modelList = Array.from(
|
|
1507
|
+
new Set([primaryModel, ...models && models.length > 0 ? models : [primaryModel, "qwen3.7-plus"]].filter(Boolean))
|
|
1508
|
+
);
|
|
1509
|
+
for (const m of modelList) {
|
|
1510
|
+
modelsObj[m] = {
|
|
1511
|
+
name: formatModelDisplayName(m),
|
|
1512
|
+
limit: { context: 1048576, output: 65536 },
|
|
1513
|
+
modalities: { input: ["text", "image"], output: ["text"] },
|
|
1514
|
+
reasoning: true,
|
|
1515
|
+
variants: {
|
|
1516
|
+
low: { effort: "low" },
|
|
1517
|
+
medium: { effort: "medium" },
|
|
1518
|
+
high: { effort: "high" }
|
|
1519
|
+
}
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
return {
|
|
1523
|
+
npm: "@ai-sdk/openai-compatible",
|
|
1524
|
+
name: "QwenProxy",
|
|
1525
|
+
options: {
|
|
1526
|
+
baseURL: baseUrl,
|
|
1527
|
+
apiKey
|
|
1528
|
+
},
|
|
1529
|
+
models: modelsObj
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
function syncKilo(options) {
|
|
1533
|
+
const { filePath, apiKey, baseUrl, model = "qwen3.8-max", models, setActive = true } = options;
|
|
1534
|
+
try {
|
|
1535
|
+
fs9.mkdirSync(path9.dirname(filePath), { recursive: true });
|
|
1536
|
+
let backupPath;
|
|
1537
|
+
let content = "";
|
|
1538
|
+
if (fs9.existsSync(filePath)) {
|
|
1539
|
+
backupPath = createTimestampBackup(filePath);
|
|
1540
|
+
content = fs9.readFileSync(filePath, "utf-8");
|
|
1541
|
+
}
|
|
1542
|
+
const providerObj = buildKiloProviderObject(baseUrl, apiKey, model, models);
|
|
1543
|
+
const providerJson = JSON.stringify(providerObj, null, 6).split("\n").map((line, idx) => idx === 0 ? line : ` ${line}`).join("\n");
|
|
1544
|
+
const qwenEntry = ` "qwenproxy": ${providerJson}`;
|
|
1545
|
+
if (!content.trim()) {
|
|
1546
|
+
const initial = {
|
|
1547
|
+
$schema: "https://kilo.ai/config.json",
|
|
1548
|
+
provider: {
|
|
1549
|
+
qwenproxy: providerObj
|
|
1550
|
+
}
|
|
1551
|
+
};
|
|
1552
|
+
if (setActive) {
|
|
1553
|
+
initial.model = `qwenproxy/${model}`;
|
|
1554
|
+
}
|
|
1555
|
+
fs9.writeFileSync(filePath, JSON.stringify(initial, null, 2) + "\n", "utf-8");
|
|
1556
|
+
} else {
|
|
1557
|
+
const existingSpan = findKeyObjectSpan3(content, "qwenproxy");
|
|
1558
|
+
if (existingSpan) {
|
|
1559
|
+
const comma = existingSpan.hasTrailingComma ? "," : "";
|
|
1560
|
+
content = content.slice(0, existingSpan.start) + `"qwenproxy": ${providerJson}${comma}` + content.slice(existingSpan.end);
|
|
1561
|
+
} else {
|
|
1562
|
+
const providerMatch = content.match(/"provider"\s*:\s*\{/);
|
|
1563
|
+
if (providerMatch && providerMatch.index !== void 0) {
|
|
1564
|
+
const insertIdx = providerMatch.index + providerMatch[0].length;
|
|
1565
|
+
content = content.slice(0, insertIdx) + "\n" + qwenEntry + "," + content.slice(insertIdx);
|
|
1566
|
+
} else {
|
|
1567
|
+
const lastBraceIdx = content.lastIndexOf("}");
|
|
1568
|
+
if (lastBraceIdx !== -1) {
|
|
1569
|
+
const comma = content.slice(0, lastBraceIdx).trimEnd().endsWith("{") ? "" : ",";
|
|
1570
|
+
content = content.slice(0, lastBraceIdx).trimEnd() + `${comma}
|
|
1571
|
+
"provider": {
|
|
1572
|
+
${qwenEntry}
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
`;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
if (setActive) {
|
|
1580
|
+
const modelRegex = /"model"\s*:\s*"[^"]*"/;
|
|
1581
|
+
if (modelRegex.test(content)) {
|
|
1582
|
+
content = content.replace(modelRegex, `"model": "qwenproxy/${model}"`);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
fs9.writeFileSync(filePath, content, "utf-8");
|
|
1586
|
+
}
|
|
1587
|
+
return {
|
|
1588
|
+
client: "kilo",
|
|
1589
|
+
filePath,
|
|
1590
|
+
backupPath,
|
|
1591
|
+
success: true,
|
|
1592
|
+
action: backupPath ? "updated" : "created",
|
|
1593
|
+
message: `Configured Kilo Code with provider qwenproxy (${baseUrl})`
|
|
1594
|
+
};
|
|
1595
|
+
} catch (err) {
|
|
1596
|
+
return {
|
|
1597
|
+
client: "kilo",
|
|
1598
|
+
filePath,
|
|
1599
|
+
success: false,
|
|
1600
|
+
action: "failed",
|
|
1601
|
+
error: err?.message || String(err)
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
function restoreKilo(filePath, backupPath) {
|
|
1606
|
+
const restored = restoreFromBackup(filePath, backupPath);
|
|
1607
|
+
return {
|
|
1608
|
+
client: "kilo",
|
|
1609
|
+
filePath,
|
|
1610
|
+
backupPath,
|
|
1611
|
+
success: restored,
|
|
1612
|
+
action: restored ? "restored" : "failed",
|
|
1613
|
+
message: restored ? "Restored Kilo Code config from backup" : "Backup file not found"
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// src/sync/cline.ts
|
|
1618
|
+
import fs10 from "node:fs";
|
|
1619
|
+
import path10 from "node:path";
|
|
1620
|
+
import Database from "better-sqlite3";
|
|
1621
|
+
var TARGET_KEYS = [
|
|
1622
|
+
"saoudrizwan.claude-dev",
|
|
1623
|
+
"ZooCodeOrganization.zoo-code",
|
|
1624
|
+
"RooVeterinaryInc.roo-cline"
|
|
1625
|
+
];
|
|
1626
|
+
function syncCline(options) {
|
|
1627
|
+
const { filePath, baseUrl, model = "qwen3.8-max", reasoningEffort = "high" } = options;
|
|
1628
|
+
try {
|
|
1629
|
+
fs10.mkdirSync(path10.dirname(filePath), { recursive: true });
|
|
1630
|
+
let backupPath;
|
|
1631
|
+
if (fs10.existsSync(filePath)) {
|
|
1632
|
+
backupPath = createTimestampBackup(filePath);
|
|
1633
|
+
}
|
|
1634
|
+
const db = new Database(filePath);
|
|
1635
|
+
db.exec(`CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value TEXT)`);
|
|
1636
|
+
const selectStmt = db.prepare(`SELECT value FROM ItemTable WHERE key = ?`);
|
|
1637
|
+
const insertStmt = db.prepare(`INSERT INTO ItemTable (key, value) VALUES (?, ?)`);
|
|
1638
|
+
const updateConfig = (existingVal) => {
|
|
1639
|
+
let parsed = {};
|
|
1640
|
+
if (existingVal) {
|
|
1641
|
+
try {
|
|
1642
|
+
parsed = JSON.parse(existingVal);
|
|
1643
|
+
} catch {
|
|
1644
|
+
parsed = {};
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
parsed.apiProvider = "openai";
|
|
1648
|
+
parsed.openAiBaseUrl = baseUrl;
|
|
1649
|
+
parsed.openAiModelId = model;
|
|
1650
|
+
parsed.enableReasoningEffort = true;
|
|
1651
|
+
parsed.reasoningEffort = reasoningEffort;
|
|
1652
|
+
parsed.openAiCustomModelInfo = {
|
|
1653
|
+
...parsed.openAiCustomModelInfo || {},
|
|
1654
|
+
contextWindow: 1e6,
|
|
1655
|
+
maxTokens: -1,
|
|
1656
|
+
supportsImages: true,
|
|
1657
|
+
supportsPromptCache: false,
|
|
1658
|
+
inputPrice: 0,
|
|
1659
|
+
outputPrice: 0
|
|
1660
|
+
};
|
|
1661
|
+
if (Array.isArray(parsed.listApiConfigMeta)) {
|
|
1662
|
+
if (!parsed.listApiConfigMeta.some((p) => p?.name === "QwenProxy")) {
|
|
1663
|
+
parsed.listApiConfigMeta.push({
|
|
1664
|
+
name: "QwenProxy",
|
|
1665
|
+
id: "qwenproxy-local",
|
|
1666
|
+
apiProvider: "openai",
|
|
1667
|
+
modelId: model
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
parsed.currentApiConfigName = "QwenProxy";
|
|
1671
|
+
}
|
|
1672
|
+
return JSON.stringify(parsed);
|
|
1673
|
+
};
|
|
1674
|
+
for (const key of TARGET_KEYS) {
|
|
1675
|
+
const row = selectStmt.get(key);
|
|
1676
|
+
if (row || key === "saoudrizwan.claude-dev" || key === "ZooCodeOrganization.zoo-code") {
|
|
1677
|
+
const newVal = updateConfig(row?.value);
|
|
1678
|
+
insertStmt.run(key, newVal);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
db.close();
|
|
1682
|
+
return {
|
|
1683
|
+
client: "cline",
|
|
1684
|
+
filePath,
|
|
1685
|
+
backupPath,
|
|
1686
|
+
success: true,
|
|
1687
|
+
action: backupPath ? "updated" : "created",
|
|
1688
|
+
message: `Configured Cline in state.vscdb with model ${model} and reasoning effort ${reasoningEffort}`
|
|
1689
|
+
};
|
|
1690
|
+
} catch (err) {
|
|
1691
|
+
return {
|
|
1692
|
+
client: "cline",
|
|
1693
|
+
filePath,
|
|
1694
|
+
success: false,
|
|
1695
|
+
action: "failed",
|
|
1696
|
+
error: err?.message || String(err)
|
|
1697
|
+
};
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
function restoreCline(filePath, backupPath) {
|
|
1701
|
+
const restoredFromBackup = restoreFromBackup(filePath, backupPath);
|
|
1702
|
+
let manuallyCleaned = false;
|
|
1703
|
+
if (fs10.existsSync(filePath)) {
|
|
1704
|
+
try {
|
|
1705
|
+
const db = new Database(filePath);
|
|
1706
|
+
const rows = db.prepare("SELECT key, value FROM ItemTable WHERE key = 'saoudrizwan.claude-dev' OR key = 'ZooCodeOrganization.zoo-code'").all();
|
|
1707
|
+
for (const row of rows) {
|
|
1708
|
+
try {
|
|
1709
|
+
const parsed = JSON.parse(row.value);
|
|
1710
|
+
if (parsed.openAiBaseUrl?.includes("7936") || parsed.openAiModelId?.includes("qwen")) {
|
|
1711
|
+
delete parsed.openAiBaseUrl;
|
|
1712
|
+
delete parsed.openAiApiKey;
|
|
1713
|
+
delete parsed.openAiModelId;
|
|
1714
|
+
db.prepare("UPDATE ItemTable SET value = ? WHERE key = ?").run(JSON.stringify(parsed), row.key);
|
|
1715
|
+
manuallyCleaned = true;
|
|
1716
|
+
}
|
|
1717
|
+
} catch {
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
db.close();
|
|
1721
|
+
} catch {
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
const success = restoredFromBackup || manuallyCleaned;
|
|
1725
|
+
return {
|
|
1726
|
+
client: "cline",
|
|
1727
|
+
filePath,
|
|
1728
|
+
backupPath,
|
|
1729
|
+
success,
|
|
1730
|
+
action: success ? "restored" : "failed",
|
|
1731
|
+
message: success ? restoredFromBackup ? "Restored Cline settings from backup" : "Removed QwenProxy configuration from Cline settings" : "Backup file not found"
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
// src/sync/zed.ts
|
|
1736
|
+
import fs11 from "node:fs";
|
|
1737
|
+
import path11 from "node:path";
|
|
1738
|
+
function parseJsonWithComments(raw) {
|
|
1739
|
+
let result = "";
|
|
1740
|
+
let inString = false;
|
|
1741
|
+
let inLineComment = false;
|
|
1742
|
+
let inBlockComment = false;
|
|
1743
|
+
let escape = false;
|
|
1744
|
+
for (let i = 0; i < raw.length; i++) {
|
|
1745
|
+
const ch = raw[i];
|
|
1746
|
+
const nextCh = raw[i + 1] || "";
|
|
1747
|
+
if (inLineComment) {
|
|
1748
|
+
if (ch === "\n") {
|
|
1749
|
+
inLineComment = false;
|
|
1750
|
+
result += ch;
|
|
1751
|
+
}
|
|
1752
|
+
continue;
|
|
1753
|
+
}
|
|
1754
|
+
if (inBlockComment) {
|
|
1755
|
+
if (ch === "*" && nextCh === "/") {
|
|
1756
|
+
inBlockComment = false;
|
|
1757
|
+
i++;
|
|
1758
|
+
}
|
|
1759
|
+
continue;
|
|
1760
|
+
}
|
|
1761
|
+
if (inString) {
|
|
1762
|
+
result += ch;
|
|
1763
|
+
if (escape) {
|
|
1764
|
+
escape = false;
|
|
1765
|
+
} else if (ch === "\\") {
|
|
1766
|
+
escape = true;
|
|
1767
|
+
} else if (ch === '"') {
|
|
1768
|
+
inString = false;
|
|
1769
|
+
}
|
|
1770
|
+
continue;
|
|
1771
|
+
}
|
|
1772
|
+
if (ch === "/" && nextCh === "/") {
|
|
1773
|
+
inLineComment = true;
|
|
1774
|
+
i++;
|
|
1775
|
+
continue;
|
|
1776
|
+
}
|
|
1777
|
+
if (ch === "/" && nextCh === "*") {
|
|
1778
|
+
inBlockComment = true;
|
|
1779
|
+
i++;
|
|
1780
|
+
continue;
|
|
1781
|
+
}
|
|
1782
|
+
if (ch === '"') {
|
|
1783
|
+
inString = true;
|
|
1784
|
+
result += ch;
|
|
1785
|
+
continue;
|
|
1786
|
+
}
|
|
1787
|
+
result += ch;
|
|
1788
|
+
}
|
|
1789
|
+
const cleaned = result.replace(/,\s*([\}\]])/g, "$1");
|
|
1790
|
+
return JSON.parse(cleaned);
|
|
1791
|
+
}
|
|
1792
|
+
function buildZedAvailableModels(primaryModel = "qwen3.8-max", models) {
|
|
1793
|
+
const modelList = Array.from(
|
|
1794
|
+
new Set([primaryModel, ...models && models.length > 0 ? models : [primaryModel, "qwen3.7-plus"]].filter(Boolean))
|
|
1795
|
+
);
|
|
1796
|
+
const zedModels = [];
|
|
1797
|
+
for (const m of modelList) {
|
|
1798
|
+
zedModels.push({
|
|
1799
|
+
name: m,
|
|
1800
|
+
max_tokens: 1e6,
|
|
1801
|
+
max_output_tokens: 131072,
|
|
1802
|
+
max_completion_tokens: 131072,
|
|
1803
|
+
capabilities: {
|
|
1804
|
+
tools: true,
|
|
1805
|
+
images: true,
|
|
1806
|
+
parallel_tool_calls: true,
|
|
1807
|
+
prompt_cache_key: true,
|
|
1808
|
+
chat_completions: true,
|
|
1809
|
+
interleaved_reasoning: true
|
|
1810
|
+
}
|
|
1811
|
+
});
|
|
1812
|
+
if (m === primaryModel) {
|
|
1813
|
+
zedModels.push(
|
|
1814
|
+
{
|
|
1815
|
+
name: `${m}-thinking`,
|
|
1816
|
+
max_tokens: 1e6,
|
|
1817
|
+
max_output_tokens: 131072,
|
|
1818
|
+
max_completion_tokens: 131072,
|
|
1819
|
+
capabilities: {
|
|
1820
|
+
tools: true,
|
|
1821
|
+
images: true,
|
|
1822
|
+
parallel_tool_calls: true,
|
|
1823
|
+
prompt_cache_key: true,
|
|
1824
|
+
chat_completions: true,
|
|
1825
|
+
interleaved_reasoning: true
|
|
1826
|
+
}
|
|
1827
|
+
},
|
|
1828
|
+
{
|
|
1829
|
+
name: `${m}-fast`,
|
|
1830
|
+
max_tokens: 1e6,
|
|
1831
|
+
max_output_tokens: 131072,
|
|
1832
|
+
max_completion_tokens: 131072,
|
|
1833
|
+
capabilities: {
|
|
1834
|
+
tools: true,
|
|
1835
|
+
images: true,
|
|
1836
|
+
parallel_tool_calls: true,
|
|
1837
|
+
prompt_cache_key: true,
|
|
1838
|
+
chat_completions: true,
|
|
1839
|
+
interleaved_reasoning: true
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
return zedModels;
|
|
1846
|
+
}
|
|
1847
|
+
function syncZed(options) {
|
|
1848
|
+
const { filePath, baseUrl, model = "qwen3.8-max", models, setActive = true } = options;
|
|
1849
|
+
try {
|
|
1850
|
+
fs11.mkdirSync(path11.dirname(filePath), { recursive: true });
|
|
1851
|
+
let backupPath;
|
|
1852
|
+
let existingSettings = {};
|
|
1853
|
+
if (fs11.existsSync(filePath)) {
|
|
1854
|
+
backupPath = createTimestampBackup(filePath);
|
|
1855
|
+
try {
|
|
1856
|
+
const content = fs11.readFileSync(filePath, "utf-8");
|
|
1857
|
+
existingSettings = parseJsonWithComments(content);
|
|
1858
|
+
} catch {
|
|
1859
|
+
existingSettings = {};
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
const languageModels = existingSettings.language_models || {};
|
|
1863
|
+
const openaiCompatible = languageModels.openai_compatible || {};
|
|
1864
|
+
openaiCompatible.QwenProxy = {
|
|
1865
|
+
api_url: baseUrl,
|
|
1866
|
+
available_models: buildZedAvailableModels(model, models)
|
|
1867
|
+
};
|
|
1868
|
+
const updatedSettings = {
|
|
1869
|
+
...existingSettings,
|
|
1870
|
+
language_models: {
|
|
1871
|
+
...languageModels,
|
|
1872
|
+
openai_compatible: openaiCompatible
|
|
1873
|
+
}
|
|
1874
|
+
};
|
|
1875
|
+
if (setActive) {
|
|
1876
|
+
const existingAgent = existingSettings.agent || {};
|
|
1877
|
+
updatedSettings.agent = {
|
|
1878
|
+
...existingAgent,
|
|
1879
|
+
default_model: {
|
|
1880
|
+
provider: "QwenProxy",
|
|
1881
|
+
model,
|
|
1882
|
+
enable_thinking: true
|
|
1883
|
+
}
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
fs11.writeFileSync(filePath, JSON.stringify(updatedSettings, null, 2) + "\n", "utf-8");
|
|
1887
|
+
return {
|
|
1888
|
+
client: "zed",
|
|
1889
|
+
filePath,
|
|
1890
|
+
backupPath,
|
|
1891
|
+
success: true,
|
|
1892
|
+
action: backupPath ? "updated" : "created",
|
|
1893
|
+
message: `Configured Zed Editor with QwenProxy (${baseUrl}) and model ${model}`
|
|
1894
|
+
};
|
|
1895
|
+
} catch (err) {
|
|
1896
|
+
return {
|
|
1897
|
+
client: "zed",
|
|
1898
|
+
filePath,
|
|
1899
|
+
success: false,
|
|
1900
|
+
action: "failed",
|
|
1901
|
+
error: err?.message || String(err)
|
|
1902
|
+
};
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
function restoreZed(filePath, backupPath) {
|
|
1906
|
+
const restoredFromBackup = restoreFromBackup(filePath, backupPath);
|
|
1907
|
+
let manuallyCleaned = false;
|
|
1908
|
+
if (fs11.existsSync(filePath)) {
|
|
1909
|
+
try {
|
|
1910
|
+
let content = fs11.readFileSync(filePath, "utf-8");
|
|
1911
|
+
if (content.includes("127.0.0.1:7936") || content.includes("qwen3.8-max")) {
|
|
1912
|
+
const data = parseJsonWithComments(content);
|
|
1913
|
+
if (data.language_models?.openai?.api_url?.includes("7936")) {
|
|
1914
|
+
delete data.language_models.openai;
|
|
1915
|
+
if (Object.keys(data.language_models).length === 0) {
|
|
1916
|
+
delete data.language_models;
|
|
1917
|
+
}
|
|
1918
|
+
fs11.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
1919
|
+
manuallyCleaned = true;
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
} catch {
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
const success = restoredFromBackup || manuallyCleaned;
|
|
1926
|
+
return {
|
|
1927
|
+
client: "zed",
|
|
1928
|
+
filePath,
|
|
1929
|
+
backupPath,
|
|
1930
|
+
success,
|
|
1931
|
+
action: success ? "restored" : "failed",
|
|
1932
|
+
message: success ? restoredFromBackup ? "Restored Zed settings from backup" : "Removed QwenProxy configuration from Zed settings" : "Backup file not found"
|
|
1933
|
+
};
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
// src/sync/aider.ts
|
|
1937
|
+
import fs12 from "node:fs";
|
|
1938
|
+
import path12 from "node:path";
|
|
1939
|
+
function updateYamlKeyValue(content, key, value) {
|
|
1940
|
+
const regex = new RegExp(`^${key}:.*$`, "m");
|
|
1941
|
+
const line = `${key}: ${value}`;
|
|
1942
|
+
if (regex.test(content)) {
|
|
1943
|
+
return content.replace(regex, line);
|
|
1944
|
+
}
|
|
1945
|
+
return content.trimEnd() + (content.length > 0 ? "\n" : "") + line + "\n";
|
|
1946
|
+
}
|
|
1947
|
+
function syncAider(options) {
|
|
1948
|
+
const {
|
|
1949
|
+
filePath,
|
|
1950
|
+
apiKey,
|
|
1951
|
+
baseUrl,
|
|
1952
|
+
model = "qwen3.8-max",
|
|
1953
|
+
reasoningEffort = "high",
|
|
1954
|
+
modelSettingsPath
|
|
1955
|
+
} = options;
|
|
1956
|
+
try {
|
|
1957
|
+
fs12.mkdirSync(path12.dirname(filePath), { recursive: true });
|
|
1958
|
+
let backupPath;
|
|
1959
|
+
let confContent = "";
|
|
1960
|
+
if (fs12.existsSync(filePath)) {
|
|
1961
|
+
backupPath = createTimestampBackup(filePath);
|
|
1962
|
+
confContent = fs12.readFileSync(filePath, "utf-8");
|
|
1963
|
+
}
|
|
1964
|
+
const fullModelName = model.startsWith("openai/") ? model : `openai/${model}`;
|
|
1965
|
+
const weakModelName = "openai/qwen3.7-plus";
|
|
1966
|
+
confContent = updateYamlKeyValue(confContent, "openai-api-base", baseUrl);
|
|
1967
|
+
confContent = updateYamlKeyValue(confContent, "openai-api-key", apiKey);
|
|
1968
|
+
confContent = updateYamlKeyValue(confContent, "model", fullModelName);
|
|
1969
|
+
confContent = updateYamlKeyValue(confContent, "editor-model", fullModelName);
|
|
1970
|
+
confContent = updateYamlKeyValue(confContent, "weak-model", weakModelName);
|
|
1971
|
+
fs12.writeFileSync(filePath, confContent.trimEnd() + "\n", "utf-8");
|
|
1972
|
+
let extraBackupPath;
|
|
1973
|
+
const resolvedSettingsPath = modelSettingsPath || path12.join(path12.dirname(filePath), ".aider.model.settings.yml");
|
|
1974
|
+
if (resolvedSettingsPath) {
|
|
1975
|
+
fs12.mkdirSync(path12.dirname(resolvedSettingsPath), { recursive: true });
|
|
1976
|
+
let settingsContent = "";
|
|
1977
|
+
if (fs12.existsSync(resolvedSettingsPath)) {
|
|
1978
|
+
extraBackupPath = createTimestampBackup(resolvedSettingsPath);
|
|
1979
|
+
settingsContent = fs12.readFileSync(resolvedSettingsPath, "utf-8");
|
|
1980
|
+
}
|
|
1981
|
+
const modelBlock = `- name: ${fullModelName}
|
|
1982
|
+
extra_params:
|
|
1983
|
+
extra_body:
|
|
1984
|
+
reasoning_effort: ${reasoningEffort}
|
|
1985
|
+
streaming: true
|
|
1986
|
+
`;
|
|
1987
|
+
const existingModelRegex = new RegExp(`- name: ${fullModelName}[\\s\\S]*?(?=(?:^-[ \\t]+name:|\\Z))`, "m");
|
|
1988
|
+
if (existingModelRegex.test(settingsContent)) {
|
|
1989
|
+
settingsContent = settingsContent.replace(existingModelRegex, modelBlock.trimEnd() + "\n");
|
|
1990
|
+
} else {
|
|
1991
|
+
settingsContent = settingsContent.trimEnd() + (settingsContent.length > 0 ? "\n\n" : "") + modelBlock;
|
|
1992
|
+
}
|
|
1993
|
+
fs12.writeFileSync(resolvedSettingsPath, settingsContent.trimEnd() + "\n", "utf-8");
|
|
1994
|
+
}
|
|
1995
|
+
return {
|
|
1996
|
+
client: "aider",
|
|
1997
|
+
filePath,
|
|
1998
|
+
backupPath,
|
|
1999
|
+
extraBackupPath,
|
|
2000
|
+
success: true,
|
|
2001
|
+
action: backupPath ? "updated" : "created",
|
|
2002
|
+
message: `Configured Aider with model ${fullModelName} and reasoning effort ${reasoningEffort}`
|
|
2003
|
+
};
|
|
2004
|
+
} catch (err) {
|
|
2005
|
+
return {
|
|
2006
|
+
client: "aider",
|
|
2007
|
+
filePath,
|
|
2008
|
+
success: false,
|
|
2009
|
+
action: "failed",
|
|
2010
|
+
error: err?.message || String(err)
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
function restoreAider(filePath, backupPath, modelSettingsPath, extraBackupPath) {
|
|
2015
|
+
const mainRestored = restoreFromBackup(filePath, backupPath);
|
|
2016
|
+
if (modelSettingsPath && extraBackupPath) {
|
|
2017
|
+
restoreFromBackup(modelSettingsPath, extraBackupPath);
|
|
2018
|
+
}
|
|
2019
|
+
return {
|
|
2020
|
+
client: "aider",
|
|
2021
|
+
filePath,
|
|
2022
|
+
backupPath,
|
|
2023
|
+
extraBackupPath,
|
|
2024
|
+
success: mainRestored,
|
|
2025
|
+
action: mainRestored ? "restored" : "failed",
|
|
2026
|
+
message: mainRestored ? "Restored Aider settings from backup" : "Backup file not found"
|
|
2027
|
+
};
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
// src/sync/index.ts
|
|
2031
|
+
function resolveApiKey(overrideKey, configKey, host = "127.0.0.1") {
|
|
2032
|
+
if (overrideKey && overrideKey.trim().length > 0) {
|
|
2033
|
+
if (isPlaceholderApiKey(overrideKey) && !isLoopbackHost(host)) {
|
|
2034
|
+
throw new Error(
|
|
2035
|
+
`Refusing to sync clients with placeholder API key ${PLACEHOLDER_API_KEY}. Set API_KEY first.`
|
|
2036
|
+
);
|
|
2037
|
+
}
|
|
2038
|
+
return overrideKey.trim();
|
|
2039
|
+
}
|
|
2040
|
+
const envKey = (!isPlaceholderApiKey(process.env.API_KEY) ? process.env.API_KEY : void 0) || (!isPlaceholderApiKey(process.env.ADMIN_PASSWORD) ? process.env.ADMIN_PASSWORD : void 0) || configKey;
|
|
2041
|
+
if (envKey && !isPlaceholderApiKey(envKey)) {
|
|
2042
|
+
return envKey.trim();
|
|
2043
|
+
}
|
|
2044
|
+
if (isLoopbackHost(host)) return PLACEHOLDER_API_KEY;
|
|
2045
|
+
throw new Error(
|
|
2046
|
+
`Refusing to sync clients with placeholder API key ${PLACEHOLDER_API_KEY}. Set API_KEY or start the proxy once to generate one.`
|
|
2047
|
+
);
|
|
2048
|
+
}
|
|
2049
|
+
function normalizeClientName(name) {
|
|
2050
|
+
const clean = name.trim().toLowerCase().replace(/[-_ ]/g, "");
|
|
2051
|
+
if (clean === "claude" || clean === "claudecode" || clean === "anthropic") return "claude-code";
|
|
2052
|
+
if (clean === "codex" || clean === "codexcli" || clean === "openai") return "codex";
|
|
2053
|
+
if (clean === "opencode") return "opencode";
|
|
2054
|
+
if (clean === "omp" || clean === "ohmypi" || clean === "pi") return "omp";
|
|
2055
|
+
if (clean === "hermes" || clean === "hermesagent" || clean === "nous" || clean === "nousresearch") return "hermes";
|
|
2056
|
+
if (clean === "openclaw" || clean === "claw" || clean === "clawdbot" || clean === "moltbot") return "openclaw";
|
|
2057
|
+
if (clean === "kilo" || clean === "kilocode") return "kilo";
|
|
2058
|
+
if (clean === "cline" || clean === "claudedev" || clean === "zoo" || clean === "zoocode" || clean === "roo" || clean === "roocode" || clean === "roocline") {
|
|
2059
|
+
return "cline";
|
|
2060
|
+
}
|
|
2061
|
+
if (clean === "zed" || clean === "zededitor") return "zed";
|
|
2062
|
+
if (clean === "aider" || clean === "aiderchat") return "aider";
|
|
2063
|
+
return null;
|
|
2064
|
+
}
|
|
2065
|
+
function resolveBaseUrls(port = 7936, host = "127.0.0.1") {
|
|
2066
|
+
const cleanHost = host === "0.0.0.0" ? "127.0.0.1" : host;
|
|
2067
|
+
return {
|
|
2068
|
+
anthropicBaseUrl: `http://${cleanHost}:${port}`,
|
|
2069
|
+
openaiBaseUrl: `http://${cleanHost}:${port}/v1`
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
function getDefaultPaths() {
|
|
2073
|
+
const home = os3.homedir();
|
|
2074
|
+
const isWindows = process.platform === "win32";
|
|
2075
|
+
const isMac = process.platform === "darwin";
|
|
2076
|
+
const appData = process.env.APPDATA || (isWindows ? path13.join(home, "AppData", "Roaming") : "");
|
|
2077
|
+
const openCodeCandidates = [
|
|
2078
|
+
path13.join(home, ".config", "opencode", "opencode.jsonc"),
|
|
2079
|
+
path13.join(home, ".config", "opencode", "opencode.json"),
|
|
2080
|
+
path13.join(home, ".opencode", "opencode.jsonc"),
|
|
2081
|
+
path13.join(home, ".opencode", "opencode.json")
|
|
2082
|
+
];
|
|
2083
|
+
const existingOpenCode = openCodeCandidates.find((p) => fs13.existsSync(p));
|
|
2084
|
+
const kiloCandidates = [
|
|
2085
|
+
path13.join(home, ".config", "kilo", "kilo.json"),
|
|
2086
|
+
path13.join(home, ".kilo", "kilo.json"),
|
|
2087
|
+
path13.join(home, ".config", "kilo", "config.json"),
|
|
2088
|
+
path13.join(home, ".kilo", "config.json")
|
|
2089
|
+
];
|
|
2090
|
+
const existingKilo = kiloCandidates.find((p) => fs13.existsSync(p));
|
|
2091
|
+
let clinePath;
|
|
2092
|
+
if (isWindows) {
|
|
2093
|
+
clinePath = path13.join(appData, "Code", "User", "globalStorage", "state.vscdb");
|
|
2094
|
+
} else if (isMac) {
|
|
2095
|
+
clinePath = path13.join(home, "Library", "Application Support", "Code", "User", "globalStorage", "state.vscdb");
|
|
2096
|
+
} else {
|
|
2097
|
+
clinePath = path13.join(home, ".config", "Code", "User", "globalStorage", "state.vscdb");
|
|
2098
|
+
}
|
|
2099
|
+
let zedPath;
|
|
2100
|
+
if (isWindows) {
|
|
2101
|
+
zedPath = path13.join(appData, "Zed", "settings.json");
|
|
2102
|
+
} else {
|
|
2103
|
+
zedPath = path13.join(home, ".config", "zed", "settings.json");
|
|
2104
|
+
}
|
|
2105
|
+
return {
|
|
2106
|
+
claudeCode: path13.join(home, ".claude", "settings.json"),
|
|
2107
|
+
codex: fs13.existsSync(path13.join(home, ".codex", "config.toml")) ? path13.join(home, ".codex", "config.toml") : process.env.CODEX_HOME ? path13.join(process.env.CODEX_HOME, "config.toml") : path13.join(home, ".codex", "config.toml"),
|
|
2108
|
+
openCode: existingOpenCode || openCodeCandidates[0],
|
|
2109
|
+
omp: path13.join(home, ".omp", "agent", "models.yml"),
|
|
2110
|
+
hermes: path13.join(home, ".hermes", "config.yaml"),
|
|
2111
|
+
openClaw: path13.join(home, ".openclaw", "openclaw.json"),
|
|
2112
|
+
kilo: existingKilo || kiloCandidates[0],
|
|
2113
|
+
cline: clinePath,
|
|
2114
|
+
zed: zedPath,
|
|
2115
|
+
aider: path13.join(home, ".aider.conf.yml")
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
function getDefaultStateFilePath() {
|
|
2119
|
+
return getSyncStatePath();
|
|
2120
|
+
}
|
|
2121
|
+
function isExecutableInPath(name) {
|
|
2122
|
+
const envPath2 = process.env.PATH || "";
|
|
2123
|
+
const dirs = envPath2.split(path13.delimiter).filter(Boolean);
|
|
2124
|
+
const extensions = process.platform === "win32" ? (process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.toLowerCase()) : [""];
|
|
2125
|
+
for (const dir of dirs) {
|
|
2126
|
+
for (const ext of extensions) {
|
|
2127
|
+
const fullPath = path13.join(dir, name + ext);
|
|
2128
|
+
try {
|
|
2129
|
+
if (fs13.existsSync(fullPath) && !fs13.statSync(fullPath).isDirectory()) {
|
|
2130
|
+
return true;
|
|
2131
|
+
}
|
|
2132
|
+
} catch {
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
return false;
|
|
2137
|
+
}
|
|
2138
|
+
function isVscodeExtensionInstalled(pattern) {
|
|
2139
|
+
const home = os3.homedir();
|
|
2140
|
+
const candidateDirs = [
|
|
2141
|
+
path13.join(home, ".vscode", "extensions"),
|
|
2142
|
+
path13.join(home, ".vscode-insiders", "extensions"),
|
|
2143
|
+
path13.join(home, ".cursor", "extensions"),
|
|
2144
|
+
path13.join(home, ".windsurf", "extensions")
|
|
2145
|
+
];
|
|
2146
|
+
for (const d of candidateDirs) {
|
|
2147
|
+
if (fs13.existsSync(d)) {
|
|
2148
|
+
try {
|
|
2149
|
+
const entries = fs13.readdirSync(d);
|
|
2150
|
+
if (entries.some((e) => pattern.test(e))) return true;
|
|
2151
|
+
} catch {
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
return false;
|
|
2156
|
+
}
|
|
2157
|
+
function isZedInstalled() {
|
|
2158
|
+
if (isExecutableInPath("zed")) return true;
|
|
2159
|
+
if (process.platform === "win32") {
|
|
2160
|
+
const local = process.env.LOCALAPPDATA || "";
|
|
2161
|
+
if (fs13.existsSync(path13.join(local, "Programs", "Zed"))) return true;
|
|
2162
|
+
} else if (process.platform === "darwin") {
|
|
2163
|
+
if (fs13.existsSync("/Applications/Zed.app")) return true;
|
|
2164
|
+
}
|
|
2165
|
+
return false;
|
|
2166
|
+
}
|
|
2167
|
+
function isMatchingLocalHost(text, port = 7936) {
|
|
2168
|
+
if (!text) return false;
|
|
2169
|
+
const configuredPort = config.server?.port || 7936;
|
|
2170
|
+
const ports = /* @__PURE__ */ new Set([String(port), String(configuredPort), "7936", "3000"]);
|
|
2171
|
+
const isLocal = text.includes("127.0.0.1") || text.includes("localhost") || text.includes("0.0.0.0");
|
|
2172
|
+
return isLocal && Array.from(ports).some((p) => text.includes(`:${p}`));
|
|
2173
|
+
}
|
|
2174
|
+
function isClientToolInstalled(id, targetPath, isCustomTestPath = false) {
|
|
2175
|
+
if (isCustomTestPath) {
|
|
2176
|
+
return fs13.existsSync(targetPath);
|
|
2177
|
+
}
|
|
2178
|
+
switch (id) {
|
|
2179
|
+
case "hermes":
|
|
2180
|
+
return isExecutableInPath("hermes");
|
|
2181
|
+
case "openclaw":
|
|
2182
|
+
return isExecutableInPath("openclaw") || isExecutableInPath("clawdbot") || isExecutableInPath("moltbot");
|
|
2183
|
+
case "aider":
|
|
2184
|
+
return isExecutableInPath("aider");
|
|
2185
|
+
case "kilo":
|
|
2186
|
+
return isExecutableInPath("kilo") || isVscodeExtensionInstalled(/kilo/i);
|
|
2187
|
+
case "cline":
|
|
2188
|
+
return isExecutableInPath("cline") || isVscodeExtensionInstalled(/cline|zoo-code|roo-cline/i);
|
|
2189
|
+
case "zed":
|
|
2190
|
+
return isZedInstalled();
|
|
2191
|
+
case "claude-code":
|
|
2192
|
+
return isExecutableInPath("claude") || fs13.existsSync(targetPath);
|
|
2193
|
+
case "codex":
|
|
2194
|
+
return isExecutableInPath("codex") || fs13.existsSync(targetPath);
|
|
2195
|
+
case "opencode":
|
|
2196
|
+
return isExecutableInPath("opencode") || fs13.existsSync(targetPath);
|
|
2197
|
+
case "omp":
|
|
2198
|
+
return isExecutableInPath("omp") || fs13.existsSync(targetPath);
|
|
2199
|
+
default:
|
|
2200
|
+
return fs13.existsSync(targetPath);
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
function inspectClientSyncStatus(id, filePath, port = 7936) {
|
|
2204
|
+
const defaultPaths = getDefaultPaths();
|
|
2205
|
+
const defaultTarget = defaultPaths[id] || "";
|
|
2206
|
+
const isCustomTestPath = Boolean(
|
|
2207
|
+
filePath && defaultTarget && path13.resolve(filePath) !== path13.resolve(defaultTarget)
|
|
2208
|
+
);
|
|
2209
|
+
const targetPath = filePath || defaultTarget;
|
|
2210
|
+
if (!fs13.existsSync(targetPath)) {
|
|
2211
|
+
return { id, installed: false, synced: false };
|
|
2212
|
+
}
|
|
2213
|
+
const toolInstalled = isClientToolInstalled(id, targetPath, isCustomTestPath);
|
|
2214
|
+
if (!toolInstalled) {
|
|
2215
|
+
return { id, installed: false, synced: false };
|
|
2216
|
+
}
|
|
2217
|
+
try {
|
|
2218
|
+
if (id === "cline") {
|
|
2219
|
+
let isSynced = false;
|
|
2220
|
+
let model;
|
|
2221
|
+
let rowExists = false;
|
|
2222
|
+
try {
|
|
2223
|
+
const db = new Database2(targetPath, { readonly: true });
|
|
2224
|
+
const row = db.prepare(
|
|
2225
|
+
`SELECT value FROM ItemTable WHERE key = 'saoudrizwan.claude-dev' OR key = 'ZooCodeOrganization.zoo-code' LIMIT 1`
|
|
2226
|
+
).get();
|
|
2227
|
+
db.close();
|
|
2228
|
+
if (row && row.value) {
|
|
2229
|
+
rowExists = true;
|
|
2230
|
+
const parsed = JSON.parse(row.value);
|
|
2231
|
+
const url = parsed.openAiBaseUrl || "";
|
|
2232
|
+
model = parsed.openAiModelId;
|
|
2233
|
+
isSynced = isMatchingLocalHost(url, port);
|
|
2234
|
+
}
|
|
2235
|
+
} catch {
|
|
2236
|
+
}
|
|
2237
|
+
const isInstalled = isCustomTestPath ? true : rowExists || isExecutableInPath("cline") || isVscodeExtensionInstalled(/cline|zoo-code|roo-cline/i);
|
|
2238
|
+
return { id, installed: isInstalled, synced: isInstalled && isSynced, model };
|
|
2239
|
+
}
|
|
2240
|
+
const raw = fs13.readFileSync(targetPath, "utf-8");
|
|
2241
|
+
if (id === "claude-code") {
|
|
2242
|
+
const data = JSON.parse(raw);
|
|
2243
|
+
const url = data?.env?.ANTHROPIC_BASE_URL || "";
|
|
2244
|
+
const model = data?.env?.ANTHROPIC_MODEL || data?.model || "";
|
|
2245
|
+
const isSynced = isMatchingLocalHost(url, port) && (model.toLowerCase().includes("qwen") || Boolean(data?.env?.ANTHROPIC_AUTH_TOKEN));
|
|
2246
|
+
return {
|
|
2247
|
+
id,
|
|
2248
|
+
installed: true,
|
|
2249
|
+
synced: isSynced,
|
|
2250
|
+
model: model || void 0,
|
|
2251
|
+
url: url || void 0
|
|
2252
|
+
};
|
|
2253
|
+
}
|
|
2254
|
+
if (id === "codex") {
|
|
2255
|
+
const hasProvider = raw.includes("[model_providers.qwenproxy]");
|
|
2256
|
+
const isProviderActive = /^model_provider\s*=\s*["']qwenproxy["']/m.test(raw);
|
|
2257
|
+
const modelMatch = raw.match(/^model\s*=\s*["']([^"']+)["']/m);
|
|
2258
|
+
const model = modelMatch ? modelMatch[1] : void 0;
|
|
2259
|
+
const urlMatch = raw.match(/\[model_providers\.qwenproxy\][\s\S]*?base_url\s*=\s*["']([^"']+)["']/);
|
|
2260
|
+
const url = urlMatch ? urlMatch[1] : "";
|
|
2261
|
+
const isSynced = hasProvider && isProviderActive && isMatchingLocalHost(url, port);
|
|
2262
|
+
return {
|
|
2263
|
+
id,
|
|
2264
|
+
installed: true,
|
|
2265
|
+
synced: isSynced,
|
|
2266
|
+
model
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
if (id === "opencode") {
|
|
2270
|
+
let isSynced = false;
|
|
2271
|
+
try {
|
|
2272
|
+
const data = JSON.parse(raw);
|
|
2273
|
+
const provider = data?.provider?.qwenproxy;
|
|
2274
|
+
const url = provider?.options?.baseURL || "";
|
|
2275
|
+
const isSynced2 = Boolean(provider && isMatchingLocalHost(url, port));
|
|
2276
|
+
return { id, installed: true, synced: isSynced2 };
|
|
2277
|
+
} catch {
|
|
2278
|
+
const qwenBlockMatch = raw.match(/"qwenproxy"\s*:\s*\{[\s\S]*?"baseURL"\s*:\s*"([^"]+)"/);
|
|
2279
|
+
const url = qwenBlockMatch ? qwenBlockMatch[1] : "";
|
|
2280
|
+
const isSynced2 = Boolean(url && isMatchingLocalHost(url, port));
|
|
2281
|
+
return { id, installed: true, synced: isSynced2 };
|
|
2282
|
+
}
|
|
2283
|
+
return {
|
|
2284
|
+
id,
|
|
2285
|
+
installed: true,
|
|
2286
|
+
synced: isSynced
|
|
2287
|
+
};
|
|
2288
|
+
}
|
|
2289
|
+
if (id === "omp") {
|
|
2290
|
+
const ompMatch = raw.match(/^[ \t]*qwenproxy:\s*\r?\n((?:[ \t]{4,}.*\r?\n?)*)/m);
|
|
2291
|
+
let isSynced = false;
|
|
2292
|
+
let url;
|
|
2293
|
+
if (ompMatch) {
|
|
2294
|
+
const urlMatch = ompMatch[1].match(/baseUrl:\s*(\S+)/);
|
|
2295
|
+
url = urlMatch ? urlMatch[1].replace(/['"]/g, "") : void 0;
|
|
2296
|
+
isSynced = Boolean(url && isMatchingLocalHost(url, port));
|
|
2297
|
+
}
|
|
2298
|
+
return {
|
|
2299
|
+
id,
|
|
2300
|
+
installed: true,
|
|
2301
|
+
synced: isSynced,
|
|
2302
|
+
url
|
|
2303
|
+
};
|
|
2304
|
+
}
|
|
2305
|
+
if (id === "hermes") {
|
|
2306
|
+
const isSynced = isMatchingLocalHost(raw, port) && (raw.includes("qwenproxy") || raw.includes("qwen"));
|
|
2307
|
+
return { id, installed: true, synced: isSynced };
|
|
2308
|
+
}
|
|
2309
|
+
if (id === "openclaw") {
|
|
2310
|
+
const isSynced = isMatchingLocalHost(raw, port) && raw.includes("qwenproxy");
|
|
2311
|
+
return { id, installed: true, synced: isSynced };
|
|
2312
|
+
}
|
|
2313
|
+
if (id === "kilo") {
|
|
2314
|
+
const isSynced = isMatchingLocalHost(raw, port) && raw.includes("qwenproxy");
|
|
2315
|
+
return { id, installed: true, synced: isSynced };
|
|
2316
|
+
}
|
|
2317
|
+
if (id === "zed") {
|
|
2318
|
+
const isSynced = isMatchingLocalHost(raw, port) && raw.includes("QwenProxy");
|
|
2319
|
+
return { id, installed: true, synced: isSynced };
|
|
2320
|
+
}
|
|
2321
|
+
if (id === "aider") {
|
|
2322
|
+
const isSynced = isMatchingLocalHost(raw, port) && raw.includes("qwen");
|
|
2323
|
+
return { id, installed: true, synced: isSynced };
|
|
2324
|
+
}
|
|
2325
|
+
} catch {
|
|
2326
|
+
return { id, installed: true, synced: false };
|
|
2327
|
+
}
|
|
2328
|
+
return { id, installed: true, synced: false };
|
|
2329
|
+
}
|
|
2330
|
+
function syncAllClients(options = {}) {
|
|
2331
|
+
const defaultPaths = getDefaultPaths();
|
|
2332
|
+
const paths = {
|
|
2333
|
+
claudeCode: options.customPaths?.claudeCode || defaultPaths.claudeCode,
|
|
2334
|
+
codex: options.customPaths?.codex || defaultPaths.codex,
|
|
2335
|
+
openCode: options.customPaths?.openCode || defaultPaths.openCode,
|
|
2336
|
+
omp: options.customPaths?.omp || defaultPaths.omp,
|
|
2337
|
+
hermes: options.customPaths?.hermes || defaultPaths.hermes,
|
|
2338
|
+
openClaw: options.customPaths?.openClaw || defaultPaths.openClaw,
|
|
2339
|
+
kilo: options.customPaths?.kilo || defaultPaths.kilo,
|
|
2340
|
+
cline: options.customPaths?.cline || defaultPaths.cline,
|
|
2341
|
+
zed: options.customPaths?.zed || defaultPaths.zed,
|
|
2342
|
+
aider: options.customPaths?.aider || defaultPaths.aider
|
|
2343
|
+
};
|
|
2344
|
+
const port = options.port ?? (config.server?.port || 7936);
|
|
2345
|
+
const configuredHost = config.server?.host;
|
|
2346
|
+
const host = options.host ?? (configuredHost && configuredHost !== "0.0.0.0" ? configuredHost : "127.0.0.1");
|
|
2347
|
+
const authHost = options.host ?? configuredHost ?? host;
|
|
2348
|
+
const apiKey = resolveApiKey(options.apiKey, config.apiKey, authHost);
|
|
2349
|
+
const { anthropicBaseUrl, openaiBaseUrl } = resolveBaseUrls(port, host);
|
|
2350
|
+
const stateFilePath = options.stateFilePath || getDefaultStateFilePath();
|
|
2351
|
+
const selectedModel = options.model || "qwen3.8-max";
|
|
2352
|
+
const allModels = options.models && options.models.length > 0 ? options.models : void 0;
|
|
2353
|
+
const syncModels = options.syncAllModels !== false && allModels ? allModels : [selectedModel];
|
|
2354
|
+
const results = {
|
|
2355
|
+
apiKey,
|
|
2356
|
+
port,
|
|
2357
|
+
host,
|
|
2358
|
+
clients: {}
|
|
2359
|
+
};
|
|
2360
|
+
const stateRecords = {};
|
|
2361
|
+
const shouldSync = (client) => {
|
|
2362
|
+
if (!options.targets || options.targets.length === 0) return true;
|
|
2363
|
+
return options.targets.includes(client);
|
|
2364
|
+
};
|
|
2365
|
+
if (shouldSync("claude-code")) {
|
|
2366
|
+
const claudeExisted = fs13.existsSync(paths.claudeCode);
|
|
2367
|
+
const claudeRes = syncClaudeCode({
|
|
2368
|
+
filePath: paths.claudeCode,
|
|
2369
|
+
apiKey,
|
|
2370
|
+
baseUrl: anthropicBaseUrl,
|
|
2371
|
+
model: selectedModel
|
|
2372
|
+
});
|
|
2373
|
+
results.clients.claudeCode = claudeRes;
|
|
2374
|
+
if (claudeRes.success && claudeRes.backupPath) {
|
|
2375
|
+
stateRecords.claudeCode = {
|
|
2376
|
+
filePath: paths.claudeCode,
|
|
2377
|
+
backupPath: claudeRes.backupPath,
|
|
2378
|
+
existedBefore: claudeExisted,
|
|
2379
|
+
syncedAt: Date.now()
|
|
2380
|
+
};
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
if (shouldSync("codex")) {
|
|
2384
|
+
const codexExisted = fs13.existsSync(paths.codex);
|
|
2385
|
+
const codexRes = syncCodex({
|
|
2386
|
+
filePath: paths.codex,
|
|
2387
|
+
apiKey,
|
|
2388
|
+
baseUrl: openaiBaseUrl,
|
|
2389
|
+
setActive: options.setActive ?? true,
|
|
2390
|
+
model: selectedModel
|
|
2391
|
+
});
|
|
2392
|
+
results.clients.codex = codexRes;
|
|
2393
|
+
if (codexRes.success && codexRes.backupPath) {
|
|
2394
|
+
stateRecords.codex = {
|
|
2395
|
+
filePath: paths.codex,
|
|
2396
|
+
backupPath: codexRes.backupPath,
|
|
2397
|
+
existedBefore: codexExisted,
|
|
2398
|
+
syncedAt: Date.now()
|
|
2399
|
+
};
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
if (shouldSync("opencode")) {
|
|
2403
|
+
const openCodeExisted = fs13.existsSync(paths.openCode);
|
|
2404
|
+
const openCodeRes = syncOpenCode({
|
|
2405
|
+
filePath: paths.openCode,
|
|
2406
|
+
apiKey,
|
|
2407
|
+
baseUrl: openaiBaseUrl,
|
|
2408
|
+
model: selectedModel,
|
|
2409
|
+
models: syncModels,
|
|
2410
|
+
setActive: options.setActive ?? true
|
|
2411
|
+
});
|
|
2412
|
+
results.clients.openCode = openCodeRes;
|
|
2413
|
+
if (openCodeRes.success && openCodeRes.backupPath) {
|
|
2414
|
+
stateRecords.openCode = {
|
|
2415
|
+
filePath: paths.openCode,
|
|
2416
|
+
backupPath: openCodeRes.backupPath,
|
|
2417
|
+
existedBefore: openCodeExisted,
|
|
2418
|
+
syncedAt: Date.now()
|
|
2419
|
+
};
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
if (shouldSync("omp")) {
|
|
2423
|
+
const ompExisted = fs13.existsSync(paths.omp);
|
|
2424
|
+
const ompRes = syncOmp({
|
|
2425
|
+
filePath: paths.omp,
|
|
2426
|
+
apiKey,
|
|
2427
|
+
baseUrl: openaiBaseUrl,
|
|
2428
|
+
model: selectedModel,
|
|
2429
|
+
models: syncModels
|
|
2430
|
+
});
|
|
2431
|
+
results.clients.omp = ompRes;
|
|
2432
|
+
if (ompRes.success && ompRes.backupPath) {
|
|
2433
|
+
stateRecords.omp = {
|
|
2434
|
+
filePath: paths.omp,
|
|
2435
|
+
backupPath: ompRes.backupPath,
|
|
2436
|
+
existedBefore: ompExisted,
|
|
2437
|
+
syncedAt: Date.now()
|
|
2438
|
+
};
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
if (shouldSync("hermes")) {
|
|
2442
|
+
const hermesExisted = fs13.existsSync(paths.hermes);
|
|
2443
|
+
const hermesRes = syncHermes({
|
|
2444
|
+
filePath: paths.hermes,
|
|
2445
|
+
apiKey,
|
|
2446
|
+
baseUrl: openaiBaseUrl,
|
|
2447
|
+
model: selectedModel
|
|
2448
|
+
});
|
|
2449
|
+
results.clients.hermes = hermesRes;
|
|
2450
|
+
if (hermesRes.success && hermesRes.backupPath) {
|
|
2451
|
+
stateRecords.hermes = {
|
|
2452
|
+
filePath: paths.hermes,
|
|
2453
|
+
backupPath: hermesRes.backupPath,
|
|
2454
|
+
existedBefore: hermesExisted,
|
|
2455
|
+
syncedAt: Date.now()
|
|
2456
|
+
};
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
if (shouldSync("openclaw")) {
|
|
2460
|
+
const openClawExisted = fs13.existsSync(paths.openClaw);
|
|
2461
|
+
const openClawRes = syncOpenClaw({
|
|
2462
|
+
filePath: paths.openClaw,
|
|
2463
|
+
apiKey,
|
|
2464
|
+
baseUrl: openaiBaseUrl,
|
|
2465
|
+
model: selectedModel,
|
|
2466
|
+
models: syncModels
|
|
2467
|
+
});
|
|
2468
|
+
results.clients.openClaw = openClawRes;
|
|
2469
|
+
if (openClawRes.success && openClawRes.backupPath) {
|
|
2470
|
+
stateRecords.openClaw = {
|
|
2471
|
+
filePath: paths.openClaw,
|
|
2472
|
+
backupPath: openClawRes.backupPath,
|
|
2473
|
+
existedBefore: openClawExisted,
|
|
2474
|
+
syncedAt: Date.now()
|
|
2475
|
+
};
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
if (shouldSync("kilo")) {
|
|
2479
|
+
const kiloExisted = fs13.existsSync(paths.kilo);
|
|
2480
|
+
const kiloRes = syncKilo({
|
|
2481
|
+
filePath: paths.kilo,
|
|
2482
|
+
apiKey,
|
|
2483
|
+
baseUrl: openaiBaseUrl,
|
|
2484
|
+
setActive: options.setActive ?? true,
|
|
2485
|
+
model: selectedModel,
|
|
2486
|
+
models: syncModels
|
|
2487
|
+
});
|
|
2488
|
+
results.clients.kilo = kiloRes;
|
|
2489
|
+
if (kiloRes.success && kiloRes.backupPath) {
|
|
2490
|
+
stateRecords.kilo = {
|
|
2491
|
+
filePath: paths.kilo,
|
|
2492
|
+
backupPath: kiloRes.backupPath,
|
|
2493
|
+
existedBefore: kiloExisted,
|
|
2494
|
+
syncedAt: Date.now()
|
|
2495
|
+
};
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
if (shouldSync("cline")) {
|
|
2499
|
+
const clineExisted = fs13.existsSync(paths.cline);
|
|
2500
|
+
const clineRes = syncCline({
|
|
2501
|
+
filePath: paths.cline,
|
|
2502
|
+
apiKey,
|
|
2503
|
+
baseUrl: openaiBaseUrl,
|
|
2504
|
+
model: selectedModel
|
|
2505
|
+
});
|
|
2506
|
+
results.clients.cline = clineRes;
|
|
2507
|
+
if (clineRes.success && clineRes.backupPath) {
|
|
2508
|
+
stateRecords.cline = {
|
|
2509
|
+
filePath: paths.cline,
|
|
2510
|
+
backupPath: clineRes.backupPath,
|
|
2511
|
+
existedBefore: clineExisted,
|
|
2512
|
+
syncedAt: Date.now()
|
|
2513
|
+
};
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
if (shouldSync("zed")) {
|
|
2517
|
+
const zedExisted = fs13.existsSync(paths.zed);
|
|
2518
|
+
const zedRes = syncZed({
|
|
2519
|
+
filePath: paths.zed,
|
|
2520
|
+
apiKey,
|
|
2521
|
+
baseUrl: openaiBaseUrl,
|
|
2522
|
+
setActive: options.setActive ?? true,
|
|
2523
|
+
model: selectedModel,
|
|
2524
|
+
models: syncModels
|
|
2525
|
+
});
|
|
2526
|
+
results.clients.zed = zedRes;
|
|
2527
|
+
if (zedRes.success && zedRes.backupPath) {
|
|
2528
|
+
stateRecords.zed = {
|
|
2529
|
+
filePath: paths.zed,
|
|
2530
|
+
backupPath: zedRes.backupPath,
|
|
2531
|
+
existedBefore: zedExisted,
|
|
2532
|
+
syncedAt: Date.now()
|
|
2533
|
+
};
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
if (shouldSync("aider")) {
|
|
2537
|
+
const aiderExisted = fs13.existsSync(paths.aider);
|
|
2538
|
+
const aiderRes = syncAider({
|
|
2539
|
+
filePath: paths.aider,
|
|
2540
|
+
apiKey,
|
|
2541
|
+
baseUrl: openaiBaseUrl,
|
|
2542
|
+
model: selectedModel
|
|
2543
|
+
});
|
|
2544
|
+
results.clients.aider = aiderRes;
|
|
2545
|
+
if (aiderRes.success && aiderRes.backupPath) {
|
|
2546
|
+
stateRecords.aider = {
|
|
2547
|
+
filePath: paths.aider,
|
|
2548
|
+
backupPath: aiderRes.backupPath,
|
|
2549
|
+
existedBefore: aiderExisted,
|
|
2550
|
+
syncedAt: Date.now()
|
|
2551
|
+
};
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
try {
|
|
2555
|
+
fs13.mkdirSync(path13.dirname(stateFilePath), { recursive: true });
|
|
2556
|
+
let existingClients = {};
|
|
2557
|
+
if (fs13.existsSync(stateFilePath)) {
|
|
2558
|
+
try {
|
|
2559
|
+
const raw = fs13.readFileSync(stateFilePath, "utf-8");
|
|
2560
|
+
const parsed = JSON.parse(raw);
|
|
2561
|
+
if (parsed?.clients && typeof parsed.clients === "object") {
|
|
2562
|
+
existingClients = parsed.clients;
|
|
2563
|
+
}
|
|
2564
|
+
} catch {
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
const stateContent = {
|
|
2568
|
+
version: 1,
|
|
2569
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2570
|
+
apiKey,
|
|
2571
|
+
port,
|
|
2572
|
+
host,
|
|
2573
|
+
clients: {
|
|
2574
|
+
...existingClients,
|
|
2575
|
+
...stateRecords
|
|
2576
|
+
}
|
|
2577
|
+
};
|
|
2578
|
+
fs13.writeFileSync(stateFilePath, JSON.stringify(stateContent, null, 2) + "\n", "utf-8");
|
|
2579
|
+
} catch (err) {
|
|
2580
|
+
console.error("Warning: could not write sync state file:", err);
|
|
2581
|
+
}
|
|
2582
|
+
return results;
|
|
2583
|
+
}
|
|
2584
|
+
function restoreAllClients(options = {}) {
|
|
2585
|
+
const defaultPaths = getDefaultPaths();
|
|
2586
|
+
const stateFilePath = options.stateFilePath || getDefaultStateFilePath();
|
|
2587
|
+
const details = [];
|
|
2588
|
+
let restoredCount = 0;
|
|
2589
|
+
let state = null;
|
|
2590
|
+
if (fs13.existsSync(stateFilePath)) {
|
|
2591
|
+
try {
|
|
2592
|
+
const raw = fs13.readFileSync(stateFilePath, "utf-8");
|
|
2593
|
+
state = JSON.parse(raw);
|
|
2594
|
+
} catch {
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
const shouldRestore = (client) => {
|
|
2598
|
+
if (!options.targets || options.targets.length === 0) return true;
|
|
2599
|
+
return options.targets.includes(client);
|
|
2600
|
+
};
|
|
2601
|
+
const restoreClientsList = [
|
|
2602
|
+
{ id: "claude-code", stateKey: "claudeCode", defaultPath: defaultPaths.claudeCode, stateRecord: state?.clients?.claudeCode, restoreFn: restoreClaudeCode },
|
|
2603
|
+
{ id: "codex", stateKey: "codex", defaultPath: defaultPaths.codex, stateRecord: state?.clients?.codex, restoreFn: restoreCodex },
|
|
2604
|
+
{ id: "opencode", stateKey: "openCode", defaultPath: defaultPaths.openCode, stateRecord: state?.clients?.openCode, restoreFn: restoreOpenCode },
|
|
2605
|
+
{ id: "omp", stateKey: "omp", defaultPath: defaultPaths.omp, stateRecord: state?.clients?.omp, restoreFn: restoreOmp },
|
|
2606
|
+
{ id: "hermes", stateKey: "hermes", defaultPath: defaultPaths.hermes, stateRecord: state?.clients?.hermes, restoreFn: restoreHermes },
|
|
2607
|
+
{ id: "openclaw", stateKey: "openClaw", defaultPath: defaultPaths.openClaw, stateRecord: state?.clients?.openClaw, restoreFn: restoreOpenClaw },
|
|
2608
|
+
{ id: "kilo", stateKey: "kilo", defaultPath: defaultPaths.kilo, stateRecord: state?.clients?.kilo, restoreFn: restoreKilo },
|
|
2609
|
+
{ id: "cline", stateKey: "cline", defaultPath: defaultPaths.cline, stateRecord: state?.clients?.cline, restoreFn: restoreCline },
|
|
2610
|
+
{ id: "zed", stateKey: "zed", defaultPath: defaultPaths.zed, stateRecord: state?.clients?.zed, restoreFn: restoreZed },
|
|
2611
|
+
{ id: "aider", stateKey: "aider", defaultPath: defaultPaths.aider, stateRecord: state?.clients?.aider, restoreFn: restoreAider }
|
|
2612
|
+
];
|
|
2613
|
+
for (const c of restoreClientsList) {
|
|
2614
|
+
if (!shouldRestore(c.id)) continue;
|
|
2615
|
+
const filePath = c.stateRecord?.filePath || c.defaultPath;
|
|
2616
|
+
const backupPath = c.stateRecord?.backupPath;
|
|
2617
|
+
const canAttempt = options.stateFilePath ? Boolean(c.stateRecord) : Boolean(c.stateRecord || fs13.existsSync(filePath));
|
|
2618
|
+
if (canAttempt) {
|
|
2619
|
+
const res = c.restoreFn(filePath, backupPath);
|
|
2620
|
+
details.push(res);
|
|
2621
|
+
if (res.success) {
|
|
2622
|
+
restoredCount++;
|
|
2623
|
+
if (state?.clients) {
|
|
2624
|
+
delete state.clients[c.stateKey];
|
|
2625
|
+
delete state.clients[c.id];
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
if (stateFilePath && fs13.existsSync(stateFilePath)) {
|
|
2631
|
+
try {
|
|
2632
|
+
if (state && state.clients && Object.keys(state.clients).length > 0) {
|
|
2633
|
+
fs13.writeFileSync(stateFilePath, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
2634
|
+
} else {
|
|
2635
|
+
fs13.unlinkSync(stateFilePath);
|
|
2636
|
+
}
|
|
2637
|
+
} catch {
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
return { restoredCount, details };
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
// src/sync-clients.ts
|
|
2644
|
+
process.env.DOTENV_CONFIG_QUIET = "true";
|
|
2645
|
+
ensureDataDirs();
|
|
2646
|
+
var envPath = getEnvFilePath();
|
|
2647
|
+
if (fs14.existsSync(envPath)) {
|
|
2648
|
+
dotenv.config({ path: envPath, quiet: true });
|
|
2649
|
+
} else {
|
|
2650
|
+
dotenv.config({ quiet: true });
|
|
2651
|
+
}
|
|
2652
|
+
function parseArgs() {
|
|
2653
|
+
const args = process.argv.slice(2);
|
|
2654
|
+
const options = {
|
|
2655
|
+
restore: false,
|
|
2656
|
+
list: false,
|
|
2657
|
+
help: false,
|
|
2658
|
+
setActive: true,
|
|
2659
|
+
targets: []
|
|
2660
|
+
};
|
|
2661
|
+
for (let i = 0; i < args.length; i++) {
|
|
2662
|
+
const arg = args[i];
|
|
2663
|
+
if (arg === "--restore" || arg === "restore") {
|
|
2664
|
+
options.restore = true;
|
|
2665
|
+
} else if (arg === "--list" || arg === "list") {
|
|
2666
|
+
options.list = true;
|
|
2667
|
+
} else if (arg === "--help" || arg === "-h" || arg === "help") {
|
|
2668
|
+
options.help = true;
|
|
2669
|
+
} else if (arg === "--api-key" && args[i + 1]) {
|
|
2670
|
+
options.apiKey = args[++i];
|
|
2671
|
+
} else if (arg === "--port" && args[i + 1]) {
|
|
2672
|
+
options.port = parseInt(args[++i], 10);
|
|
2673
|
+
} else if (arg === "--host" && args[i + 1]) {
|
|
2674
|
+
options.host = args[++i];
|
|
2675
|
+
} else if (arg === "--no-active") {
|
|
2676
|
+
options.setActive = false;
|
|
2677
|
+
} else if ((arg === "--model" || arg === "-m") && args[i + 1]) {
|
|
2678
|
+
options.model = args[++i];
|
|
2679
|
+
} else if (arg === "--client" && args[i + 1]) {
|
|
2680
|
+
const normalized = normalizeClientName(args[++i]);
|
|
2681
|
+
if (normalized) options.targets.push(normalized);
|
|
2682
|
+
} else if (!arg.startsWith("-")) {
|
|
2683
|
+
const normalized = normalizeClientName(arg);
|
|
2684
|
+
if (normalized) options.targets.push(normalized);
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
return options;
|
|
2688
|
+
}
|
|
2689
|
+
function printHelp() {
|
|
2690
|
+
console.log(`
|
|
2691
|
+
Uso:
|
|
2692
|
+
npm run sync [clientes...] [op\xE7\xF5es]
|
|
2693
|
+
|
|
2694
|
+
Exemplos:
|
|
2695
|
+
npm run sync # Sincroniza todos os 10 clientes detectados
|
|
2696
|
+
npm run sync hermes # Sincroniza apenas o Hermes Agent
|
|
2697
|
+
npm run sync opencode # Sincroniza apenas o OpenCode
|
|
2698
|
+
npm run sync claude # Sincroniza apenas o Claude Code
|
|
2699
|
+
npm run sync openclaw # Sincroniza apenas o OpenClaw
|
|
2700
|
+
npm run sync kilo # Sincroniza apenas o Kilo Code
|
|
2701
|
+
npm run sync cline # Sincroniza apenas o Cline
|
|
2702
|
+
npm run sync omp # Sincroniza apenas o OMP (Oh My Pi)
|
|
2703
|
+
npm run sync codex # Sincroniza apenas o Codex CLI
|
|
2704
|
+
npm run sync zed # Sincroniza apenas o Zed Editor
|
|
2705
|
+
npm run sync aider # Sincroniza apenas o Aider
|
|
2706
|
+
npm run sync claude codex # Sincroniza m\xFAltiplos clientes espec\xEDficos
|
|
2707
|
+
npm run sync --list # Lista status de detec\xE7\xE3o de todos os 10 clientes (ou qpx sync --list)
|
|
2708
|
+
npm run sync --restore # Restaura as configura\xE7\xF5es originais (ou qpx sync --restore)
|
|
2709
|
+
|
|
2710
|
+
Op\xE7\xF5es:
|
|
2711
|
+
--client <nome> Nome do cliente (hermes, opencode, claude, openclaw, kilo, cline, omp, codex, zed, aider)
|
|
2712
|
+
--model <modelo> Modelo padr\xE3o a configurar (padr\xE3o: qwen3.8-max)
|
|
2713
|
+
--api-key <chave> Sobrescrever chave de API (obrigat\xF3ria; n\xE3o usa placeholder)
|
|
2714
|
+
--port <porta> Sobrescrever porta do servidor (padr\xE3o: l\xEA do .env ou usa 7936)
|
|
2715
|
+
--host <host> Sobrescrever host do servidor (padr\xE3o: 127.0.0.1)
|
|
2716
|
+
--no-active N\xE3o definir o modelo ativo como padr\xE3o (apenas adiciona o provider)
|
|
2717
|
+
--restore Desfaz altera\xE7\xF5es restaurando backups
|
|
2718
|
+
--list Mostra status de detec\xE7\xE3o dos arquivos de configura\xE7\xE3o
|
|
2719
|
+
`);
|
|
2720
|
+
}
|
|
2721
|
+
async function main() {
|
|
2722
|
+
const options = parseArgs();
|
|
2723
|
+
console.log("==================================================");
|
|
2724
|
+
console.log(" \u{1F680} QwenProxy - Top 10 Client Configuration Sync");
|
|
2725
|
+
console.log("==================================================");
|
|
2726
|
+
if (options.help) {
|
|
2727
|
+
printHelp();
|
|
2728
|
+
return;
|
|
2729
|
+
}
|
|
2730
|
+
if (options.list) {
|
|
2731
|
+
console.log("\n\u{1F4C1} Status de detec\xE7\xE3o dos 10 clientes no seu computador:\n");
|
|
2732
|
+
const defaultPaths = getDefaultPaths();
|
|
2733
|
+
const clients = [
|
|
2734
|
+
{ id: "hermes", name: "1. Hermes Agent", path: defaultPaths.hermes },
|
|
2735
|
+
{ id: "opencode", name: "2. OpenCode", path: defaultPaths.openCode },
|
|
2736
|
+
{ id: "claude-code", name: "3. Claude Code", path: defaultPaths.claudeCode },
|
|
2737
|
+
{ id: "openclaw", name: "4. OpenClaw", path: defaultPaths.openClaw },
|
|
2738
|
+
{ id: "kilo", name: "5. Kilo Code", path: defaultPaths.kilo },
|
|
2739
|
+
{ id: "cline", name: "6. Cline", path: defaultPaths.cline },
|
|
2740
|
+
{ id: "omp", name: "7. OMP (Oh My Pi)", path: defaultPaths.omp },
|
|
2741
|
+
{ id: "codex", name: "8. Codex CLI", path: defaultPaths.codex },
|
|
2742
|
+
{ id: "zed", name: "9. Zed Editor", path: defaultPaths.zed },
|
|
2743
|
+
{ id: "aider", name: "10. Aider", path: defaultPaths.aider }
|
|
2744
|
+
];
|
|
2745
|
+
for (const c of clients) {
|
|
2746
|
+
const status = inspectClientSyncStatus(c.id, c.path);
|
|
2747
|
+
let badge = "";
|
|
2748
|
+
let icon = "\u26AA";
|
|
2749
|
+
if (status.synced) {
|
|
2750
|
+
icon = "\u2705";
|
|
2751
|
+
badge = status.model ? `[Sincronizado: ${status.model}]` : "[Sincronizado]";
|
|
2752
|
+
} else if (status.installed) {
|
|
2753
|
+
icon = "\u26A0\uFE0F ";
|
|
2754
|
+
badge = "[Detectado - Outro provedor]";
|
|
2755
|
+
} else {
|
|
2756
|
+
icon = "\u26AA";
|
|
2757
|
+
badge = "[N\xE3o instalado]";
|
|
2758
|
+
}
|
|
2759
|
+
console.log(` ${icon} ${c.name.padEnd(20)} ${badge}`);
|
|
2760
|
+
console.log(` ${c.path}`);
|
|
2761
|
+
}
|
|
2762
|
+
console.log("\nPara sincronizar um ou todos, execute:");
|
|
2763
|
+
console.log(" npm run sync");
|
|
2764
|
+
console.log(" npm run sync hermes");
|
|
2765
|
+
console.log(" npm run sync claude cline zed\n");
|
|
2766
|
+
return;
|
|
2767
|
+
}
|
|
2768
|
+
if (options.restore) {
|
|
2769
|
+
console.log("\n\u{1F504} Restaurando configura\xE7\xF5es originais a partir dos backups...\n");
|
|
2770
|
+
const result2 = restoreAllClients();
|
|
2771
|
+
if (result2.restoredCount === 0) {
|
|
2772
|
+
console.log("\u2139\uFE0F Nenhum backup ou arquivo de estado anterior encontrado para restaurar.");
|
|
2773
|
+
} else {
|
|
2774
|
+
for (const item of result2.details) {
|
|
2775
|
+
console.log(` \u2713 Restaurado [${item.client}]: ${item.filePath}`);
|
|
2776
|
+
}
|
|
2777
|
+
console.log(`
|
|
2778
|
+
\u2705 ${result2.restoredCount} configura\xE7\xE3o(\xF5es) restaurada(s) com sucesso!`);
|
|
2779
|
+
}
|
|
2780
|
+
return;
|
|
2781
|
+
}
|
|
2782
|
+
const targetNames = options.targets.length > 0 ? options.targets.join(", ") : "todos os detectados";
|
|
2783
|
+
console.log(`
|
|
2784
|
+
\u{1F4E1} Sincronizando clientes: [${targetNames}]...
|
|
2785
|
+
`);
|
|
2786
|
+
const result = syncAllClients({
|
|
2787
|
+
apiKey: options.apiKey,
|
|
2788
|
+
port: options.port,
|
|
2789
|
+
host: options.host,
|
|
2790
|
+
setActive: options.setActive,
|
|
2791
|
+
targets: options.targets.length > 0 ? options.targets : void 0,
|
|
2792
|
+
model: options.model
|
|
2793
|
+
});
|
|
2794
|
+
console.log(`\u{1F511} Chave API: ${result.apiKey}`);
|
|
2795
|
+
console.log(`\u{1F310} Porta: ${result.port}`);
|
|
2796
|
+
console.log(`\u{1F3E0} Host: ${result.host}
|
|
2797
|
+
`);
|
|
2798
|
+
let count = 0;
|
|
2799
|
+
for (const client of Object.values(result.clients)) {
|
|
2800
|
+
if (!client) continue;
|
|
2801
|
+
count++;
|
|
2802
|
+
const icon = client.success ? "\u2705" : "\u274C";
|
|
2803
|
+
console.log(`${icon} [${client.client}]`);
|
|
2804
|
+
console.log(` Arquivo: ${client.filePath}`);
|
|
2805
|
+
if (client.backupPath) {
|
|
2806
|
+
console.log(` Backup: ${client.backupPath}`);
|
|
2807
|
+
}
|
|
2808
|
+
if (client.message) {
|
|
2809
|
+
console.log(` Status: ${client.message}`);
|
|
2810
|
+
}
|
|
2811
|
+
if (client.error) {
|
|
2812
|
+
console.log(` Erro: ${client.error}`);
|
|
2813
|
+
}
|
|
2814
|
+
console.log("");
|
|
2815
|
+
}
|
|
2816
|
+
console.log("--------------------------------------------------");
|
|
2817
|
+
console.log(`\u2728 ${count} cliente(s) sincronizado(s) com zero perda de outras configs/provedores!`);
|
|
2818
|
+
console.log("\u{1F4A1} Para desfazer e restaurar a qualquer momento:");
|
|
2819
|
+
console.log(" qpx sync --restore (ou npm run sync --restore)");
|
|
2820
|
+
}
|
|
2821
|
+
main().catch((err) => {
|
|
2822
|
+
console.error("\u274C Erro fatal durante a sincroniza\xE7\xE3o:", err);
|
|
2823
|
+
process.exit(1);
|
|
2824
|
+
});
|