From c575dc219ea18301944a834e7a5f92e5c6b222fd Mon Sep 17 00:00:00 2001 From: Naagraz Date: Fri, 26 Jun 2026 16:02:42 +0530 Subject: [PATCH 1/4] fix(chat): stop dropping MCP tool calls via double-wrap extractLocalMcpToolCalls returns flat ParsedToolCall {id,name,arguments}. processToolCallsThroughGuard validates that flat shape and wraps into {function:{name,arguments}} itself. Pre-wrapping here produced {function:{name:tc.name, arguments:...}} with tc.name undefined at the guard's validation site -> validation failed -> tool call dropped. Pass the flat list straight through; let the guard do the wrapping. Co-Authored-By: Claude --- src/routes/chatNonStreaming.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/routes/chatNonStreaming.ts b/src/routes/chatNonStreaming.ts index 9b475c6..74cc12a 100644 --- a/src/routes/chatNonStreaming.ts +++ b/src/routes/chatNonStreaming.ts @@ -202,12 +202,11 @@ function parseQwenResponse(line: string, state: StreamProcessorState, ctx: NonSt // so we must extract them here to avoid losing tool calls. const localToolCalls = extractLocalMcpToolCalls(chunk); if (localToolCalls.length > 0) { - const parsed = localToolCalls.map((tc) => ({ - id: tc.id, - type: 'function' as const, - function: { name: tc.name, arguments: JSON.stringify(tc.arguments) }, - })); - processToolCallsThroughGuard(parsed, state.toolCallsOut, { + // extractLocalMcpToolCalls returns flat ParsedToolCall {id,name,arguments}. + // processToolCallsThroughGuard validates that flat shape and wraps into + // {function:{name,arguments}} itself — pre-wrapping here hid tc.name from + // the guard (tc.name undefined), failing validation and dropping the call. + processToolCallsThroughGuard(localToolCalls, state.toolCallsOut, { logId: ctx.logId, toolSpamGuard: state.toolSpamGuard, correctionPrompts: state.correctionPrompts, From 4b53fc805be06d182d90654a232c3d4cf9d4eff3 Mon Sep 17 00:00:00 2001 From: Naagraz Date: Fri, 26 Jun 2026 16:02:42 +0530 Subject: [PATCH 2/4] test: isolate .qwen data writes so tests can't clobber accounts.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit auth.test.ts saveCookies(testEmail, ...) wrote savecookies-test@example.com into the REAL .qwen/accounts.json via saveAccountsToFile, wiping the user's configured accounts on every `bun test` run. projectPath now honors a QWEN_DATA_DIR env override, but ONLY for the mutable `.qwen` data dir (accounts.json, master.key, monitor.json, browser-profiles) — source-tree reads like package.json still resolve under PROJECT_ROOT. A test preload (src/tests/setup.ts, wired via bunfig.toml) points QWEN_DATA_DIR at a fresh mkdtemp dir before any module loads, so ACCOUNTS_FILE (resolved at module load) lands in the temp dir. Verified: accounts.json md5 identical before/after a full `bun test` run. 154/154 tests pass. Typecheck clean. Co-Authored-By: Claude --- bunfig.toml | 4 ++++ src/tests/setup.ts | 14 ++++++++++++++ src/utils/paths.ts | 10 ++++++++++ 3 files changed, 28 insertions(+) create mode 100644 bunfig.toml create mode 100644 src/tests/setup.ts diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..20f01fa --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,4 @@ +[test] +# Redirect .qwen data writes to a temp dir before any test module loads, +# so the suite never clobbers the real .qwen/accounts.json. +preload = ["./src/tests/setup.ts"] diff --git a/src/tests/setup.ts b/src/tests/setup.ts new file mode 100644 index 0000000..261405c --- /dev/null +++ b/src/tests/setup.ts @@ -0,0 +1,14 @@ +/* + * Test preload — runs before any test module is imported (via bunfig.toml + * [test].preload). Redirects all .qwen data writes (accounts.json, master.key, + * monitor.json, browser-profiles) into a throwaway temp dir so the test suite + * can never clobber the real .qwen/accounts.json. Must set the env BEFORE + * accountManager.ts loads, because ACCOUNTS_FILE is resolved at module load. + */ +import { mkdtempSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +if (!process.env.QWEN_DATA_DIR) { + process.env.QWEN_DATA_DIR = mkdtempSync(join(tmpdir(), 'qwen-gate-test-')); +} diff --git a/src/utils/paths.ts b/src/utils/paths.ts index d4b064e..66a7ab1 100644 --- a/src/utils/paths.ts +++ b/src/utils/paths.ts @@ -12,7 +12,17 @@ export const PROJECT_ROOT = resolve(__dirname, '..', '..'); * Resolve a path relative to the project root. * Use this instead of process.cwd() to ensure paths work * regardless of where the CLI is invoked from. + * + * Honors the QWEN_DATA_DIR env override, but ONLY for the mutable `.qwen` data + * dir (accounts.json, master.key, monitor.json, browser-profiles). Source-tree + * reads (package.json, etc.) always resolve under PROJECT_ROOT. Tests point + * QWEN_DATA_DIR at a throwaway temp dir so writes never clobber real + * .qwen/accounts.json, while still reading package.json from the real root. */ export function projectPath(...segments: string[]): string { + if (process.env.QWEN_DATA_DIR && segments[0] === '.qwen') { + return resolve(process.env.QWEN_DATA_DIR, ...segments); + } return resolve(PROJECT_ROOT, ...segments); } + From 3e2d206aa38fef4e4fe4f1bde45998374df04cf8 Mon Sep 17 00:00:00 2001 From: Naagraz Date: Fri, 26 Jun 2026 16:02:43 +0530 Subject: [PATCH 3/4] fix(auth): warn once when Bun runtime hangs browser-profile launch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Confirmed live: cloakbrowser drives Chrome over --remote-debugging-pipe, which hangs under the Bun runtime on Windows — launchPersistentContext never resolves, hits the 30s timeout, and falls back to fetch-login with NO baxia/ WAF cookie harvest (cna, ssxmod_itna, tfstk, isg). The same launch under Node resolves in ~1.4s and harvests 11 cookies incl. baxia (verified: cna, xlly_s, atpsida, isg, token, sca). Net effect of the hang: accounts authenticate (fetch fallback works) but stay WAF-cold on steady-state traffic because profileCookies never populate. The real fix is to run the gateway under Node (`npm run start:node`) for full silent-refresh + WAF-warm chat. This commit surfaces that to the operator via a once-per-process warning on the profile-load path instead of failing silently. Typecheck clean (tsc --noEmit, exit 0). Co-Authored-By: Claude --- src/services/auth.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/services/auth.ts b/src/services/auth.ts index 142de91..676484d 100644 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -26,6 +26,10 @@ import { loginFresh } from './loginService.ts'; import { logStore } from './logStore.ts'; import { getActivePage, getBrowser } from './playwright.ts'; import { ensureAccountFresh, needsRefresh } from './tokenRefresh.ts'; +import { isBun } from '../utils/env.ts'; + +/** Log the bun-pipe warning at most once per process. */ +let warnedBunPipe = false; export { addAccount, @@ -235,6 +239,21 @@ export async function loadCookiesFromProfile(email: string): Promise 30s timeout -> + // fetch-login fallback with no baxia/WAF cookie harvest). Node drives the + // pipe correctly. Warn once so the operator can switch to `npm run start:node` + // when they want full WAF-cookie harvesting (silent-refresh, fewer captchas). + if (isBun && !warnedBunPipe) { + warnedBunPipe = true; + logStore.log( + 'warn', + 'auth', + 'Running under Bun: browser profile launch may hang (CDP pipe). ' + + 'For full baxia/WAF cookie harvesting run with `npm run start:node`. ' + + 'Auth still works via fetch-login fallback.', + ); + } const { BROWSER_DEFAULT_ARGS } = await import('./playwright.ts'); const { launchPersistentContext } = await import('cloakbrowser'); const PROFILE_LAUNCH_TIMEOUT_MS = 30_000; From 64b65aa3899d347c524671fd833f9ef2adb79ae5 Mon Sep 17 00:00:00 2001 From: Naagraz Date: Fri, 26 Jun 2026 16:02:43 +0530 Subject: [PATCH 4/4] fix(runtime): default to Node so browser-profile launch works MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The captcha fixes depend on harvesting baxia/WAF cookies (cna, ssxmod_itna, tfstk, isg) via a real browser profile launch. cloakbrowser drives Chrome over a CDP --remote-debugging-pipe that HANGS under the Bun runtime on Windows: the launch never resolves, hits the 30s timeout, and falls back to fetch-login with NO cookie harvest -> accounts stay WAF-cold -> captchas return. Under Node the same launch resolves in ~1.4s and harvests the full cookie set (verified live). Previously `npm start`, `qg start`, and `qg restart` all ran the server under Bun, so adding a new account harvested nothing. Now they default to Node: - package.json: `start`/`start:prod`/`dev`/`start:{firefox,chrome,edge}` -> `node --import tsx`. Bun kept as explicit `start:bun` / `dev:bun`. - cli.ts: server spawn runner 'bun' -> 'node --import tsx' (qg start/restart). Net effect: a freshly added account now goes through the browser-profile path under Node, harvests baxia cookies, persists them as profileCookies, and chat requests are WAF-warm from the first call — no manual runtime choice needed. Bun remains available via start:bun for anyone who wants it (accepts the WAF-cold tradeoff). Cluster mode stays Bun-only (Bun.serve/spawn/reusePort). Verified live: `npm start` boots under Node, 4/4 auth, chat returns, 0 captcha. Typecheck clean (tsc --noEmit, exit 0). Co-Authored-By: Claude --- package.json | 14 ++++++++------ src/cli.ts | 14 ++++++++++---- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index e9f1b36..3068b6a 100644 --- a/package.json +++ b/package.json @@ -11,13 +11,15 @@ "scripts": { "postinstall": "node scripts/setup.js || exit 0", "build": "tsc -p tsconfig.build.json", - "start": "bun src/index.tsx", - "start:prod": "bun dist/index.js", + "start": "node --import tsx src/index.tsx", + "start:bun": "bun src/index.tsx", + "start:prod": "node --import tsx dist/index.js", "start:node": "node --import tsx src/index.tsx", - "dev": "bun --watch src/index.tsx", - "start:firefox": "bun src/index.tsx --browser=firefox", - "start:chrome": "bun src/index.tsx --browser=chrome", - "start:edge": "bun src/index.tsx --browser=edge", + "dev": "node --import tsx --watch src/index.tsx", + "dev:bun": "bun --watch src/index.tsx", + "start:firefox": "node --import tsx src/index.tsx --browser=firefox", + "start:chrome": "node --import tsx src/index.tsx --browser=chrome", + "start:edge": "node --import tsx src/index.tsx --browser=edge", "cluster": "bun src/cluster.ts", "setup": "node scripts/setup.js", "test": "bun test", diff --git a/src/cli.ts b/src/cli.ts index 5d77d2f..8e8556a 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -63,12 +63,18 @@ async function startServer(args: string[]) { if (hostIdx !== -1 && args[hostIdx + 1]) extraArgs.push('--host', args[hostIdx + 1]); const entry = findEntry(); - const runner = 'bun'; - - out(`Starting server (bun ${entry})...`); + // Run the server under Node, not Bun. cloakbrowser drives Chrome over a CDP + // pipe that hangs under Bun on Windows (browser-profile launch never resolves + // -> 30s timeout -> fetch-login with no baxia/WAF cookie harvest -> captchas). + // Node drives the pipe correctly and harvests the WAF cookies. `tsx` lets Node + // execute the .tsx entry directly. + const runner = 'node'; + const runnerArgs = ['--import', 'tsx', entry, ...extraArgs]; + + out(`Starting server (node ${entry})...`); if (extraArgs.length) out(`Extra args: ${extraArgs.join(' ')}`); - const server = spawn(runner, [entry, ...extraArgs], { + const server = spawn(runner, runnerArgs, { stdio: 'inherit', shell: true, });