@jsonstudio/rcc 0.89.1562 → 0.89.1803

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.
Files changed (223) hide show
  1. package/README.md +97 -13
  2. package/configsamples/config.json +8 -8
  3. package/configsamples/config.reference.json +1 -1
  4. package/configsamples/provider/crs/config.v1.json +1 -1
  5. package/configsamples/provider/glm/config.v1.json +1 -1
  6. package/configsamples/provider/glm-anthropic/config.v1.json +1 -1
  7. package/configsamples/provider/kimi/config.v1.json +1 -1
  8. package/configsamples/provider/lmstudio/config.v1.json +2 -1
  9. package/configsamples/provider/mimo/config.v1.json +1 -1
  10. package/configsamples/provider/modelscope/config.v1.json +1 -1
  11. package/configsamples/provider/qwen/config.v1.json +1 -1
  12. package/configsamples/provider/tab/config.v1.json +2 -1
  13. package/configsamples/provider/tabglm/config.v1.json +1 -1
  14. package/dist/build-info.js +3 -3
  15. package/dist/build-info.js.map +1 -1
  16. package/dist/cli/commands/config.js +8 -9
  17. package/dist/cli/commands/config.js.map +1 -1
  18. package/dist/cli/commands/restart.d.ts +4 -12
  19. package/dist/cli/commands/restart.js +226 -120
  20. package/dist/cli/commands/restart.js.map +1 -1
  21. package/dist/cli/commands/start.d.ts +1 -0
  22. package/dist/cli/commands/start.js +28 -1
  23. package/dist/cli/commands/start.js.map +1 -1
  24. package/dist/cli/commands/status.js +12 -6
  25. package/dist/cli/commands/status.js.map +1 -1
  26. package/dist/cli/config/init-provider-catalog.js +12 -11
  27. package/dist/cli/config/init-provider-catalog.js.map +1 -1
  28. package/dist/cli.js +3 -14
  29. package/dist/cli.js.map +1 -1
  30. package/dist/client/anthropic/anthropic-protocol-client.d.ts +1 -0
  31. package/dist/client/anthropic/anthropic-protocol-client.js +25 -0
  32. package/dist/client/anthropic/anthropic-protocol-client.js.map +1 -1
  33. package/dist/commands/oauth.js +185 -9
  34. package/dist/commands/oauth.js.map +1 -1
  35. package/dist/commands/token-daemon.js +12 -2
  36. package/dist/commands/token-daemon.js.map +1 -1
  37. package/dist/docs/daemon-admin-ui.html +1242 -234
  38. package/dist/index.js +119 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/manager/index.d.ts +2 -0
  41. package/dist/manager/index.js +39 -2
  42. package/dist/manager/index.js.map +1 -1
  43. package/dist/manager/modules/quota/antigravity-quota-manager.d.ts +29 -5
  44. package/dist/manager/modules/quota/antigravity-quota-manager.js +369 -113
  45. package/dist/manager/modules/quota/antigravity-quota-manager.js.map +1 -1
  46. package/dist/manager/modules/quota/provider-quota-daemon.cooldown.d.ts +7 -0
  47. package/dist/manager/modules/quota/provider-quota-daemon.cooldown.js +61 -0
  48. package/dist/manager/modules/quota/provider-quota-daemon.cooldown.js.map +1 -1
  49. package/dist/manager/modules/quota/provider-quota-daemon.d.ts +1 -0
  50. package/dist/manager/modules/quota/provider-quota-daemon.events.js +134 -5
  51. package/dist/manager/modules/quota/provider-quota-daemon.events.js.map +1 -1
  52. package/dist/manager/modules/quota/provider-quota-daemon.js +19 -13
  53. package/dist/manager/modules/quota/provider-quota-daemon.js.map +1 -1
  54. package/dist/manager/modules/quota/provider-quota-daemon.model-backoff.d.ts +1 -0
  55. package/dist/manager/modules/quota/provider-quota-daemon.model-backoff.js +8 -3
  56. package/dist/manager/modules/quota/provider-quota-daemon.model-backoff.js.map +1 -1
  57. package/dist/manager/modules/token/index.js +2 -2
  58. package/dist/manager/modules/token/index.js.map +1 -1
  59. package/dist/manager/quota/provider-quota-center.d.ts +9 -0
  60. package/dist/manager/quota/provider-quota-center.js +19 -2
  61. package/dist/manager/quota/provider-quota-center.js.map +1 -1
  62. package/dist/modules/llmswitch/bridge.d.ts +33 -1
  63. package/dist/modules/llmswitch/bridge.js +170 -2
  64. package/dist/modules/llmswitch/bridge.js.map +1 -1
  65. package/dist/modules/llmswitch/core-loader.js +64 -11
  66. package/dist/modules/llmswitch/core-loader.js.map +1 -1
  67. package/dist/modules/pipeline/utils/debug-logger.d.ts +1 -0
  68. package/dist/modules/pipeline/utils/debug-logger.js +50 -3
  69. package/dist/modules/pipeline/utils/debug-logger.js.map +1 -1
  70. package/dist/providers/auth/apikey-auth.js +15 -3
  71. package/dist/providers/auth/apikey-auth.js.map +1 -1
  72. package/dist/providers/auth/oauth-auth.js +26 -2
  73. package/dist/providers/auth/oauth-auth.js.map +1 -1
  74. package/dist/providers/auth/oauth-lifecycle.d.ts +13 -1
  75. package/dist/providers/auth/oauth-lifecycle.js +346 -45
  76. package/dist/providers/auth/oauth-lifecycle.js.map +1 -1
  77. package/dist/providers/auth/oauth-repair-cooldown.d.ts +21 -0
  78. package/dist/providers/auth/oauth-repair-cooldown.js +100 -0
  79. package/dist/providers/auth/oauth-repair-cooldown.js.map +1 -0
  80. package/dist/providers/auth/oauth-repair-env.d.ts +1 -0
  81. package/dist/providers/auth/oauth-repair-env.js +79 -0
  82. package/dist/providers/auth/oauth-repair-env.js.map +1 -0
  83. package/dist/providers/auth/qwen-userinfo-helper.d.ts +2 -0
  84. package/dist/providers/auth/qwen-userinfo-helper.js +72 -40
  85. package/dist/providers/auth/qwen-userinfo-helper.js.map +1 -1
  86. package/dist/providers/auth/tokenfile-auth.d.ts +2 -0
  87. package/dist/providers/auth/tokenfile-auth.js +163 -21
  88. package/dist/providers/auth/tokenfile-auth.js.map +1 -1
  89. package/dist/providers/core/api/provider-types.d.ts +10 -0
  90. package/dist/providers/core/config/camoufox-launcher.d.ts +3 -0
  91. package/dist/providers/core/config/camoufox-launcher.js +190 -3
  92. package/dist/providers/core/config/camoufox-launcher.js.map +1 -1
  93. package/dist/providers/core/config/oauth-flows.js +50 -19
  94. package/dist/providers/core/config/oauth-flows.js.map +1 -1
  95. package/dist/providers/core/config/provider-oauth-configs.js +1 -1
  96. package/dist/providers/core/config/provider-oauth-configs.js.map +1 -1
  97. package/dist/providers/core/runtime/gemini-cli-http-provider.d.ts +5 -0
  98. package/dist/providers/core/runtime/gemini-cli-http-provider.js +172 -15
  99. package/dist/providers/core/runtime/gemini-cli-http-provider.js.map +1 -1
  100. package/dist/providers/core/runtime/gemini-http-provider.d.ts +11 -0
  101. package/dist/providers/core/runtime/gemini-http-provider.js +281 -3
  102. package/dist/providers/core/runtime/gemini-http-provider.js.map +1 -1
  103. package/dist/providers/core/runtime/http-request-executor.js +55 -0
  104. package/dist/providers/core/runtime/http-request-executor.js.map +1 -1
  105. package/dist/providers/core/runtime/http-transport-provider.js +10 -14
  106. package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
  107. package/dist/providers/core/runtime/provider-factory.d.ts +1 -0
  108. package/dist/providers/core/runtime/provider-factory.js +40 -2
  109. package/dist/providers/core/runtime/provider-factory.js.map +1 -1
  110. package/dist/providers/core/strategies/oauth-auth-code-flow.js +45 -2
  111. package/dist/providers/core/strategies/oauth-auth-code-flow.js.map +1 -1
  112. package/dist/providers/core/strategies/oauth-device-flow.js +13 -2
  113. package/dist/providers/core/strategies/oauth-device-flow.js.map +1 -1
  114. package/dist/providers/core/strategies/oauth-refresh-errors.d.ts +1 -0
  115. package/dist/providers/core/strategies/oauth-refresh-errors.js +26 -0
  116. package/dist/providers/core/strategies/oauth-refresh-errors.js.map +1 -0
  117. package/dist/providers/core/utils/snapshot-writer.d.ts +4 -2
  118. package/dist/providers/core/utils/snapshot-writer.js +86 -23
  119. package/dist/providers/core/utils/snapshot-writer.js.map +1 -1
  120. package/dist/scripts/camoufox/launch-auth.mjs +545 -49
  121. package/dist/server/handlers/chat-handler.js +1 -1
  122. package/dist/server/handlers/chat-handler.js.map +1 -1
  123. package/dist/server/handlers/handler-utils.d.ts +1 -0
  124. package/dist/server/handlers/handler-utils.js +231 -3
  125. package/dist/server/handlers/handler-utils.js.map +1 -1
  126. package/dist/server/handlers/messages-handler.js +1 -1
  127. package/dist/server/handlers/messages-handler.js.map +1 -1
  128. package/dist/server/handlers/responses-handler.js +17 -5
  129. package/dist/server/handlers/responses-handler.js.map +1 -1
  130. package/dist/server/handlers/sse-dispatcher.js +10 -1
  131. package/dist/server/handlers/sse-dispatcher.js.map +1 -1
  132. package/dist/server/runtime/http-server/daemon-admin/control-handler.d.ts +3 -0
  133. package/dist/server/runtime/http-server/daemon-admin/control-handler.js +389 -0
  134. package/dist/server/runtime/http-server/daemon-admin/control-handler.js.map +1 -0
  135. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +190 -5
  136. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -1
  137. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js +2 -1
  138. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js.map +1 -1
  139. package/dist/server/runtime/http-server/daemon-admin/quota-handler.js +116 -14
  140. package/dist/server/runtime/http-server/daemon-admin/quota-handler.js.map +1 -1
  141. package/dist/server/runtime/http-server/daemon-admin/routing-policy.d.ts +30 -0
  142. package/dist/server/runtime/http-server/daemon-admin/routing-policy.js +133 -0
  143. package/dist/server/runtime/http-server/daemon-admin/routing-policy.js.map +1 -0
  144. package/dist/server/runtime/http-server/daemon-admin/status-handler.js +40 -1
  145. package/dist/server/runtime/http-server/daemon-admin/status-handler.js.map +1 -1
  146. package/dist/server/runtime/http-server/daemon-admin-routes.d.ts +5 -0
  147. package/dist/server/runtime/http-server/daemon-admin-routes.js +3 -0
  148. package/dist/server/runtime/http-server/daemon-admin-routes.js.map +1 -1
  149. package/dist/server/runtime/http-server/executor-pipeline.d.ts +10 -0
  150. package/dist/server/runtime/http-server/executor-pipeline.js +6 -0
  151. package/dist/server/runtime/http-server/executor-pipeline.js.map +1 -1
  152. package/dist/server/runtime/http-server/executor-response.js +26 -0
  153. package/dist/server/runtime/http-server/executor-response.js.map +1 -1
  154. package/dist/server/runtime/http-server/hub-shadow-compare.js +41 -3
  155. package/dist/server/runtime/http-server/hub-shadow-compare.js.map +1 -1
  156. package/dist/server/runtime/http-server/index.d.ts +9 -0
  157. package/dist/server/runtime/http-server/index.js +337 -91
  158. package/dist/server/runtime/http-server/index.js.map +1 -1
  159. package/dist/server/runtime/http-server/middleware.js +27 -1
  160. package/dist/server/runtime/http-server/middleware.js.map +1 -1
  161. package/dist/server/runtime/http-server/request-executor.js +159 -24
  162. package/dist/server/runtime/http-server/request-executor.js.map +1 -1
  163. package/dist/server/runtime/http-server/routes.d.ts +1 -0
  164. package/dist/server/runtime/http-server/routes.js +36 -3
  165. package/dist/server/runtime/http-server/routes.js.map +1 -1
  166. package/dist/server/runtime/http-server/server-id.d.ts +1 -0
  167. package/dist/server/runtime/http-server/server-id.js +18 -0
  168. package/dist/server/runtime/http-server/server-id.js.map +1 -0
  169. package/dist/server/runtime/http-server/stats-manager.d.ts +2 -0
  170. package/dist/server/runtime/http-server/stats-manager.js +63 -7
  171. package/dist/server/runtime/http-server/stats-manager.js.map +1 -1
  172. package/dist/server/runtime/http-server/types.d.ts +2 -0
  173. package/dist/server/utils/stage-logger.js +54 -9
  174. package/dist/server/utils/stage-logger.js.map +1 -1
  175. package/dist/token-daemon/history-store.d.ts +8 -3
  176. package/dist/token-daemon/history-store.js +41 -20
  177. package/dist/token-daemon/history-store.js.map +1 -1
  178. package/dist/token-daemon/index.d.ts +5 -1
  179. package/dist/token-daemon/index.js +191 -11
  180. package/dist/token-daemon/index.js.map +1 -1
  181. package/dist/token-daemon/quota-auth-issue.d.ts +7 -0
  182. package/dist/token-daemon/quota-auth-issue.js +231 -0
  183. package/dist/token-daemon/quota-auth-issue.js.map +1 -0
  184. package/dist/token-daemon/token-daemon.d.ts +2 -0
  185. package/dist/token-daemon/token-daemon.js +177 -14
  186. package/dist/token-daemon/token-daemon.js.map +1 -1
  187. package/dist/token-portal/local-token-portal.js +6 -0
  188. package/dist/token-portal/local-token-portal.js.map +1 -1
  189. package/docs/ANTIGRAVITY_IDE_FORWARD_PROXY.md +61 -0
  190. package/docs/ANTIGRAVITY_THOUGHT_SIGNATURE_BOOTSTRAP_429.md +80 -0
  191. package/docs/CLOCK.md +94 -0
  192. package/docs/DAEMON_CONTROL_PLANE.md +34 -0
  193. package/docs/OAUTH.md +172 -0
  194. package/docs/PROVIDERS_BUILTIN.md +5 -3
  195. package/docs/PROVIDER_TYPES.md +6 -4
  196. package/docs/QUOTA_MANAGER_V3.md +54 -0
  197. package/docs/ROUTING_POLICY_SCHEMA.md +47 -0
  198. package/docs/ROUTING_POLICY_UI.md +11 -0
  199. package/docs/SERVERTOOL_CLOCK_DESIGN.md +56 -25
  200. package/docs/antigravity-routing-contract.md +17 -11
  201. package/docs/config-secrets.md +49 -0
  202. package/docs/daemon-admin-ui.html +1242 -234
  203. package/docs/oauth-authentication-guide.md +4 -0
  204. package/docs/oauth-iflow-implementation.md +4 -0
  205. package/docs/provider-quota-design.md +11 -0
  206. package/docs/providers/antigravity-gemini-provider-compat.md +1 -0
  207. package/docs/providers/antigravity-thought-signature.md +127 -0
  208. package/docs/providers/tabglm-claude-code-compat.md +11 -3
  209. package/docs/refactoring/host-sharedmodule-safe-migration-plan.md +164 -0
  210. package/docs/token-daemon-preview.html +2 -2
  211. package/docs/token-refresh-daemon-plan.md +6 -6
  212. package/package.json +4 -3
  213. package/scripts/antigravity-ide-forward-proxy.mjs +362 -0
  214. package/scripts/backfill-apply-patch-exec-errorsamples.mjs +19 -0
  215. package/scripts/camoufox/launch-auth.mjs +545 -49
  216. package/scripts/ci/repo-sanity.mjs +2 -0
  217. package/scripts/install-global.sh +46 -0
  218. package/scripts/migrate-antigravity-session-signatures-alias.mjs +193 -0
  219. package/scripts/migrate-antigravity-session-signatures.mjs +165 -0
  220. package/scripts/tests/blackbox-rcc-vs-routecodex-antigravity.mjs +44 -9
  221. package/scripts/tests/ci-jest.mjs +3 -0
  222. package/scripts/verify-client-headers.mjs +33 -5
  223. package/scripts/virtual-router-dryrun.mjs +333 -0
@@ -53,7 +53,121 @@ function stripAnsi(input) {
53
53
  return input.replace(/\u001b\[[0-9;]*m/g, '');
54
54
  }
55
55
 
56
+ function resolveGoogleLanguageHint() {
57
+ const raw = String(
58
+ process.env.ROUTECODEX_OAUTH_GOOGLE_HL ||
59
+ process.env.RCC_OAUTH_GOOGLE_HL ||
60
+ 'en'
61
+ ).trim();
62
+ if (!raw) {
63
+ return 'en';
64
+ }
65
+ const lowered = raw.toLowerCase();
66
+ if (lowered === 'auto' || lowered === 'off' || lowered === 'none' || lowered === '0' || lowered === 'false') {
67
+ return 'en';
68
+ }
69
+ return raw.replace(/_/g, '-');
70
+ }
71
+
72
+ function buildLocaleEnv() {
73
+ const localeTag = resolveGoogleLanguageHint();
74
+ const parts = String(localeTag || 'en').split('-').filter(Boolean);
75
+ const language = (parts[0] || 'en').toLowerCase();
76
+ const region = (parts[1] || (language === 'zh' ? 'CN' : language === 'ja' ? 'JP' : language === 'ko' ? 'KR' : 'US')).toUpperCase();
77
+ const normalizedTag = `${language}-${region}`;
78
+ const posixLocale = `${language}_${region}.UTF-8`;
79
+ return {
80
+ LANG: posixLocale,
81
+ LC_ALL: posixLocale,
82
+ LANGUAGE: normalizedTag
83
+ };
84
+ }
85
+
86
+ function buildFirefoxUserPrefs() {
87
+ const lang = resolveGoogleLanguageHint();
88
+ const osFonts = process.platform === 'darwin'
89
+ ? {
90
+ sansCn: 'PingFang SC, Hiragino Sans GB, Heiti SC',
91
+ serifCn: 'Songti SC, STSong',
92
+ sansJa: 'Hiragino Sans, Yu Gothic, Osaka',
93
+ sansKo: 'Apple SD Gothic Neo, Nanum Gothic'
94
+ }
95
+ : {
96
+ sansCn: 'Noto Sans CJK SC, Microsoft YaHei, SimHei',
97
+ serifCn: 'Noto Serif CJK SC, SimSun',
98
+ sansJa: 'Noto Sans CJK JP, Yu Gothic',
99
+ sansKo: 'Noto Sans CJK KR, Malgun Gothic'
100
+ };
101
+
102
+ return {
103
+ 'intl.accept_languages': lang,
104
+ 'javascript.use_us_english_locale': true,
105
+ 'intl.charset.fallback.override': 'UTF-8',
106
+ 'gfx.downloadable_fonts.enabled': true,
107
+ 'font.default.x-western': 'sans-serif',
108
+ 'font.name.sans-serif.x-western': 'Arial',
109
+ 'font.name.serif.x-western': 'Times New Roman',
110
+ 'font.name.sans-serif.zh-CN': osFonts.sansCn,
111
+ 'font.name.serif.zh-CN': osFonts.serifCn,
112
+ 'font.name.sans-serif.ja': osFonts.sansJa,
113
+ 'font.name.sans-serif.ko': osFonts.sansKo
114
+ };
115
+ }
116
+
117
+ function buildCamoufoxLaunchEnv() {
118
+ return {
119
+ ...process.env,
120
+ ...buildLocaleEnv()
121
+ };
122
+ }
123
+
124
+ function quoteUserPrefValue(value) {
125
+ if (typeof value === 'string') {
126
+ return JSON.stringify(value);
127
+ }
128
+ if (typeof value === 'number' || typeof value === 'boolean') {
129
+ return String(value);
130
+ }
131
+ return JSON.stringify(String(value));
132
+ }
133
+
134
+ function buildManagedUserPrefBlock() {
135
+ const start = '// ROUTECODEX_CAMOUFOX_PREFS_BEGIN';
136
+ const end = '// ROUTECODEX_CAMOUFOX_PREFS_END';
137
+ const prefs = buildFirefoxUserPrefs();
138
+ const lines = [start];
139
+ for (const [key, value] of Object.entries(prefs)) {
140
+ lines.push('user_pref(' + JSON.stringify(key) + ', ' + quoteUserPrefValue(value) + ');');
141
+ }
142
+ lines.push(end);
143
+ return lines.join('\n') + '\n';
144
+ }
145
+
146
+ function ensureManagedProfilePrefs(profileDir) {
147
+ const userJsPath = path.join(profileDir, 'user.js');
148
+ const start = '// ROUTECODEX_CAMOUFOX_PREFS_BEGIN';
149
+ const end = '// ROUTECODEX_CAMOUFOX_PREFS_END';
150
+ const managedBlock = buildManagedUserPrefBlock();
151
+ let existing = '';
152
+ try {
153
+ existing = fs.readFileSync(userJsPath, 'utf8');
154
+ } catch {
155
+ existing = '';
156
+ }
157
+
158
+ let nextContent = managedBlock;
159
+ if (existing && existing.includes(start) && existing.includes(end)) {
160
+ const pattern = new RegExp(start + '[\\s\\S]*?' + end + '\n?', 'm');
161
+ nextContent = existing.replace(pattern, managedBlock);
162
+ } else if (existing.trim().length > 0) {
163
+ nextContent = existing.trimEnd() + '\n\n' + managedBlock;
164
+ }
165
+
166
+ fs.writeFileSync(userJsPath, nextContent, 'utf8');
167
+ }
168
+
56
169
  async function getCamoufoxCacheRoot() {
170
+ const timeoutMs = Number(process.env.ROUTECODEX_CAMOUFOX_PATH_TIMEOUT_MS || 8000);
57
171
  return new Promise((resolve) => {
58
172
  const child = spawn('python3', ['-m', 'camoufox', 'path'], {
59
173
  stdio: ['ignore', 'pipe', 'pipe']
@@ -62,8 +176,26 @@ async function getCamoufoxCacheRoot() {
62
176
  child.stdout.on('data', (chunk) => {
63
177
  out += String(chunk);
64
178
  });
65
- child.on('error', () => resolve(null));
179
+ const timer = setTimeout(() => {
180
+ try {
181
+ child.kill('SIGKILL');
182
+ } catch {
183
+ // ignore
184
+ }
185
+ console.warn(
186
+ `[camoufox-launch-auth] camoufox path resolution timed out after ${timeoutMs}ms; falling back to PATH/override.`
187
+ );
188
+ resolve(null);
189
+ }, timeoutMs);
190
+ if (typeof timer.unref === 'function') {
191
+ timer.unref();
192
+ }
193
+ child.on('error', () => {
194
+ clearTimeout(timer);
195
+ resolve(null);
196
+ });
66
197
  child.on('close', () => {
198
+ clearTimeout(timer);
67
199
  const cleaned = stripAnsi(out).trim();
68
200
  const line = cleaned.split(/\r?\n/).filter((l) => l.trim()).pop() || '';
69
201
  resolve(line || null);
@@ -149,8 +281,29 @@ async function main() {
149
281
 
150
282
  const profileId = profile || 'default';
151
283
  const profileDir = await ensureProfileDir(profileId);
284
+ try {
285
+ ensureManagedProfilePrefs(profileDir);
286
+ } catch (error) {
287
+ console.warn(
288
+ '[camoufox-launch-auth] Failed to persist managed profile prefs:',
289
+ error instanceof Error ? error.message : String(error)
290
+ );
291
+ }
292
+
293
+ const urlPreview = String(url).length > 160 ? `${String(url).slice(0, 160)}…` : String(url);
294
+ console.log(
295
+ `[camoufox-launch-auth] start profileId=${profileId} devMode=${devMode ? '1' : '0'} autoMode=${autoMode || '-'}`
296
+ );
297
+ console.log(`[camoufox-launch-auth] url=${urlPreview}`);
298
+
299
+ const binaryOverride = (process.env.ROUTECODEX_CAMOUFOX_BINARY || '').trim();
300
+ if (binaryOverride) {
301
+ console.log('[camoufox-launch-auth] ROUTECODEX_CAMOUFOX_BINARY is set; skipping python3 camoufox path lookup.');
302
+ } else {
303
+ console.log('[camoufox-launch-auth] Resolving Camoufox path via python3 -m camoufox path ...');
304
+ }
152
305
 
153
- const cacheRoot = await getCamoufoxCacheRoot();
306
+ const cacheRoot = binaryOverride ? null : await getCamoufoxCacheRoot();
154
307
  if (!cacheRoot) {
155
308
  console.warn(
156
309
  '[camoufox-launch-auth] Failed to resolve Camoufox cache root via "python3 -m camoufox path"; falling back to PATH/override.'
@@ -158,6 +311,18 @@ async function main() {
158
311
  }
159
312
 
160
313
  const camoufoxBinary = resolveCamoufoxBinary(cacheRoot);
314
+ console.log(`[camoufox-launch-auth] binary=${camoufoxBinary}`);
315
+ console.log(`[camoufox-launch-auth] profileDir=${profileDir}`);
316
+
317
+ const openOnly = isTruthy(
318
+ process.env.ROUTECODEX_CAMOUFOX_OPEN_ONLY || process.env.RCC_CAMOUFOX_OPEN_ONLY
319
+ );
320
+ if (openOnly) {
321
+ console.log('[camoufox-launch-auth] open-only mode enabled; launching Camoufox and exiting (no automation/wait).');
322
+ await launchCamoufoxDetached({ camoufoxBinary, profileDir, url });
323
+ process.exit(0);
324
+ return;
325
+ }
161
326
 
162
327
  if (autoMode && autoMode.trim().toLowerCase() === 'iflow') {
163
328
  try {
@@ -201,6 +366,40 @@ async function main() {
201
366
  return;
202
367
  }
203
368
 
369
+ if (autoMode && autoMode.trim().toLowerCase() === 'qwen') {
370
+ try {
371
+ await runAutoFlowWithFallback('qwen', { url, profileDir, profileId, camoufoxBinary, devMode });
372
+ process.exit(0);
373
+ } catch (error) {
374
+ console.error(
375
+ '[camoufox-launch-auth] Auto qwen auth failed:',
376
+ error instanceof Error ? error.message : String(error)
377
+ );
378
+ process.exit(1);
379
+ }
380
+ return;
381
+ }
382
+
383
+ // Interactive CLI flows set devMode=true by default. In that case, prefer Playwright headed
384
+ // "manual assist" mode so users always see a window and clear progress logs.
385
+ if (devMode) {
386
+ try {
387
+ await runHeadedManualAssistFlow({
388
+ url,
389
+ profileDir,
390
+ camoufoxBinary,
391
+ timeoutMs: Number(process.env.ROUTECODEX_OAUTH_TIMEOUT_MS || 10 * 60_000),
392
+ label: 'manual'
393
+ });
394
+ process.exit(0);
395
+ } catch (error) {
396
+ console.warn(
397
+ '[camoufox-launch-auth] manual: headed assist failed, falling back to direct Camoufox launch:',
398
+ error instanceof Error ? error.message : String(error)
399
+ );
400
+ }
401
+ }
402
+
204
403
  await launchManualCamoufox({ camoufoxBinary, profileDir, url });
205
404
  }
206
405
 
@@ -210,6 +409,10 @@ main().catch((err) => {
210
409
  });
211
410
 
212
411
  async function launchManualCamoufox({ camoufoxBinary, profileDir, url }) {
412
+ console.log('[camoufox-launch-auth] Launching Camoufox (direct binary) for manual completion...');
413
+ console.log(`[camoufox-launch-auth] binary=${camoufoxBinary}`);
414
+ console.log(`[camoufox-launch-auth] profileDir=${profileDir}`);
415
+ console.log(`[camoufox-launch-auth] url=${url}`);
213
416
  let browserExitCode = 0;
214
417
  let browser = null;
215
418
  const shutdownBrowser = (signal = 'SIGTERM') => {
@@ -229,7 +432,8 @@ async function launchManualCamoufox({ camoufoxBinary, profileDir, url }) {
229
432
  try {
230
433
  browser = spawn(camoufoxBinary, ['-profile', profileDir, url], {
231
434
  detached: false,
232
- stdio: 'ignore'
435
+ stdio: 'ignore',
436
+ env: buildCamoufoxLaunchEnv()
233
437
  });
234
438
 
235
439
  browserExitCode = await new Promise((resolve) => {
@@ -247,6 +451,24 @@ async function launchManualCamoufox({ camoufoxBinary, profileDir, url }) {
247
451
  process.exit(browserExitCode);
248
452
  }
249
453
 
454
+ async function launchCamoufoxDetached({ camoufoxBinary, profileDir, url }) {
455
+ console.log('[camoufox-launch-auth] Launching Camoufox (detached) ...');
456
+ try {
457
+ const child = spawn(camoufoxBinary, ['-profile', profileDir, url], {
458
+ detached: true,
459
+ stdio: 'ignore',
460
+ env: buildCamoufoxLaunchEnv()
461
+ });
462
+ child.unref();
463
+ } catch (error) {
464
+ console.error(
465
+ '[camoufox-launch-auth] Detached launch failed:',
466
+ error instanceof Error ? error.message : String(error)
467
+ );
468
+ process.exit(1);
469
+ }
470
+ }
471
+
250
472
  function isSelectorOrTimeoutError(error) {
251
473
  const message = error instanceof Error ? error.message : String(error || '');
252
474
  return (
@@ -275,7 +497,8 @@ async function runHeadedManualAssistFlow({ url, profileDir, camoufoxBinary, time
275
497
  const context = await firefox.launchPersistentContext(profileDir, {
276
498
  executablePath: camoufoxBinary,
277
499
  headless: false,
278
- acceptDownloads: false
500
+ acceptDownloads: false,
501
+ firefoxUserPrefs: buildFirefoxUserPrefs()
279
502
  });
280
503
 
281
504
  let closed = false;
@@ -320,9 +543,22 @@ async function runAutoFlowWithFallback(kind, options) {
320
543
  await runAntigravityAutoFlow(options);
321
544
  return;
322
545
  }
546
+ if (mode === 'qwen') {
547
+ await runQwenAutoFlow(options);
548
+ return;
549
+ }
323
550
  throw new Error(`Unknown auto mode: ${mode}`);
324
551
  } catch (error) {
325
552
  if (!options.devMode && isSelectorOrTimeoutError(error)) {
553
+ if (mode === 'qwen') {
554
+ await runHeadedQwenManualAssistFlow({
555
+ url: options.url,
556
+ profileDir: options.profileDir,
557
+ camoufoxBinary: options.camoufoxBinary,
558
+ timeoutMs: Number(process.env.ROUTECODEX_CAMOUFOX_QWEN_TIMEOUT_MS || 10 * 60_000)
559
+ });
560
+ return;
561
+ }
326
562
  await runHeadedManualAssistFlow({
327
563
  url: options.url,
328
564
  profileDir: options.profileDir,
@@ -336,6 +572,59 @@ async function runAutoFlowWithFallback(kind, options) {
336
572
  }
337
573
  }
338
574
 
575
+ async function runHeadedQwenManualAssistFlow({ url, profileDir, camoufoxBinary, timeoutMs }) {
576
+ let firefox;
577
+ try {
578
+ ({ firefox } = await import('playwright-core'));
579
+ } catch (error) {
580
+ throw new Error(
581
+ `playwright-core is required for headed qwen manual assist (${error instanceof Error ? error.message : String(error)})`
582
+ );
583
+ }
584
+
585
+ console.warn(
586
+ '[camoufox-launch-auth] qwen: falling back to headed mode for manual completion (no selector match).'
587
+ );
588
+ cleanupExistingCamoufox(profileDir);
589
+ const context = await firefox.launchPersistentContext(profileDir, {
590
+ executablePath: camoufoxBinary,
591
+ headless: false,
592
+ acceptDownloads: false,
593
+ firefoxUserPrefs: buildFirefoxUserPrefs()
594
+ });
595
+
596
+ let closed = false;
597
+ const shutdown = async () => {
598
+ if (closed) return;
599
+ closed = true;
600
+ await context.close().catch(() => {});
601
+ };
602
+ ['SIGTERM', 'SIGINT', 'SIGHUP'].forEach((signal) => {
603
+ process.on(signal, () => {
604
+ void shutdown().finally(() => process.exit(0));
605
+ });
606
+ });
607
+
608
+ try {
609
+ const page = context.pages()[0] || (await context.newPage());
610
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 }).catch(() => {});
611
+ console.log('[camoufox-launch-auth] Headed Qwen browser opened. Please complete Qwen authorization manually...');
612
+
613
+ const started = Date.now();
614
+ while (Date.now() - started < timeoutMs) {
615
+ const pages = context.pages();
616
+ if (pages.length === 0) {
617
+ console.log('[camoufox-launch-auth] Browser closed by user, exiting.');
618
+ return;
619
+ }
620
+ await new Promise((resolve) => setTimeout(resolve, 1000));
621
+ }
622
+ console.warn('[camoufox-launch-auth] Qwen manual assist timed out; exiting.');
623
+ } finally {
624
+ await shutdown();
625
+ }
626
+ }
627
+
339
628
  async function runIflowAutoFlow({ url, profileDir, profileId, camoufoxBinary, devMode }) {
340
629
  let firefox;
341
630
  try {
@@ -352,7 +641,8 @@ async function runIflowAutoFlow({ url, profileDir, profileId, camoufoxBinary, de
352
641
  const context = await firefox.launchPersistentContext(profileDir, {
353
642
  executablePath: camoufoxBinary,
354
643
  headless,
355
- acceptDownloads: false
644
+ acceptDownloads: false,
645
+ firefoxUserPrefs: buildFirefoxUserPrefs()
356
646
  });
357
647
  let closing = false;
358
648
  const shutdown = async () => {
@@ -454,9 +744,9 @@ async function runIflowAutoFlow({ url, profileDir, profileId, camoufoxBinary, de
454
744
  }
455
745
  console.log('[camoufox-launch-auth] Account clicked, waiting for callback...');
456
746
 
457
- const callbackPage = await waitForCallback(context, iflowPage);
747
+ const callbackPage = await waitForCallback(context, iflowPage, timeoutMs);
458
748
  callbackObserved = true;
459
- await callbackPage.waitForLoadState('load', { timeout: 120000 }).catch(() => {});
749
+ await callbackPage.waitForLoadState('load', { timeout: timeoutMs }).catch(() => {});
460
750
  console.log('[camoufox-launch-auth] OAuth callback detected, automation complete.');
461
751
  } catch (error) {
462
752
  if (callbackObserved && isBrowserClosedError(error)) {
@@ -479,13 +769,14 @@ async function runGeminiAutoFlow({ url, profileDir, camoufoxBinary, devMode }) {
479
769
  );
480
770
  }
481
771
 
482
- const timeoutMs = Number(process.env.ROUTECODEX_CAMOUFOX_GEMINI_TIMEOUT_MS || 120_000);
772
+ const timeoutMs = Number(process.env.ROUTECODEX_CAMOUFOX_GEMINI_TIMEOUT_MS || 300_000);
483
773
  const accountPreference = (process.env.ROUTECODEX_CAMOUFOX_ACCOUNT_TEXT || '').trim();
484
774
  cleanupExistingCamoufox(profileDir);
485
775
  const context = await firefox.launchPersistentContext(profileDir, {
486
776
  executablePath: camoufoxBinary,
487
777
  headless: !devMode,
488
- acceptDownloads: false
778
+ acceptDownloads: false,
779
+ firefoxUserPrefs: buildFirefoxUserPrefs()
489
780
  });
490
781
  let closing = false;
491
782
  const shutdown = async () => {
@@ -559,8 +850,8 @@ async function runGeminiAutoFlow({ url, profileDir, camoufoxBinary, devMode }) {
559
850
  console.log('[camoufox-launch-auth] No confirmation button detected within 120s, continuing...');
560
851
  }
561
852
 
562
- const activePage = confirmResult?.page || authPage;
563
- const callbackPage = await waitForCallback(context, activePage);
853
+ const activePage = confirmResult?.page || page;
854
+ const callbackPage = await waitForCallback(context, activePage, timeoutMs);
564
855
  await callbackPage.waitForLoadState('load', { timeout: timeoutMs }).catch(() => {});
565
856
  console.log('[camoufox-launch-auth] OAuth callback detected, automation complete.');
566
857
  } finally {
@@ -578,13 +869,17 @@ async function runAntigravityAutoFlow({ url, profileDir, camoufoxBinary, devMode
578
869
  );
579
870
  }
580
871
 
581
- const timeoutMs = Number(process.env.ROUTECODEX_CAMOUFOX_GEMINI_TIMEOUT_MS || 120_000);
872
+ const timeoutMs = Number(process.env.ROUTECODEX_CAMOUFOX_GEMINI_TIMEOUT_MS || 300_000);
873
+ const portalButtonTimeoutMs = Number(process.env.ROUTECODEX_CAMOUFOX_PORTAL_BUTTON_TIMEOUT_MS || 300_000);
874
+ const portalPopupTimeoutMs = Number(process.env.ROUTECODEX_CAMOUFOX_PORTAL_POPUP_TIMEOUT_MS || 300_000);
875
+ const pageLoadTimeoutMs = Number(process.env.ROUTECODEX_CAMOUFOX_PAGE_LOAD_TIMEOUT_MS || 300_000);
582
876
  const accountPreference = (process.env.ROUTECODEX_CAMOUFOX_ACCOUNT_TEXT || '').trim();
583
877
  cleanupExistingCamoufox(profileDir);
584
878
  const context = await firefox.launchPersistentContext(profileDir, {
585
879
  executablePath: camoufoxBinary,
586
880
  headless: !devMode,
587
- acceptDownloads: false
881
+ acceptDownloads: false,
882
+ firefoxUserPrefs: buildFirefoxUserPrefs()
588
883
  });
589
884
  let closing = false;
590
885
  const shutdown = async () => {
@@ -607,12 +902,33 @@ async function runAntigravityAutoFlow({ url, profileDir, camoufoxBinary, devMode
607
902
  if (page.url().includes('token-auth')) {
608
903
  console.log('[camoufox-launch-auth] Portal detected, auto-clicking continue button...');
609
904
  const button = page.locator('#continue-btn');
610
- await button.waitFor({ timeout: 20000 });
611
- const popupPromise = context.waitForEvent('page', { timeout: 10000 }).catch(() => null);
612
- await button.click();
905
+ await button.waitFor({ timeout: portalButtonTimeoutMs });
906
+ const popupPromise = context.waitForEvent('page', { timeout: portalPopupTimeoutMs }).catch(() => null);
907
+ const navPromise = page
908
+ .waitForURL((current) => typeof current === 'string' && !String(current).includes('token-auth'), {
909
+ timeout: portalPopupTimeoutMs
910
+ })
911
+ .catch(() => null);
912
+ try {
913
+ await button.click({ timeout: portalButtonTimeoutMs });
914
+ } catch {
915
+ await page.evaluate(() => {
916
+ const el = document.querySelector('#continue-btn');
917
+ if (!el) return;
918
+ const events = ['mouseenter', 'mouseover', 'mousemove', 'mousedown', 'mouseup', 'click'];
919
+ for (const type of events) {
920
+ el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, view: window }));
921
+ }
922
+ });
923
+ }
613
924
  const popup = await popupPromise;
614
- authPage = popup ?? page;
615
- await authPage.waitForLoadState('domcontentloaded', { timeout: 30000 }).catch(() => {});
925
+ if (popup) {
926
+ authPage = popup;
927
+ } else {
928
+ await navPromise;
929
+ authPage = page;
930
+ }
931
+ await authPage.waitForLoadState('domcontentloaded', { timeout: pageLoadTimeoutMs }).catch(() => {});
616
932
  }
617
933
 
618
934
  console.log('[camoufox-launch-auth] Antigravity OAuth page loaded, waiting for account selector (<=120s)...');
@@ -652,23 +968,37 @@ async function runAntigravityAutoFlow({ url, profileDir, camoufoxBinary, devMode
652
968
  }
653
969
  }, handle);
654
970
 
655
- const confirmSelector = 'span.VfPpkd-vQzf8d[jsname="V67aGc"]';
656
- const confirmResult = await waitForElementInPages(context, confirmSelector, timeoutMs);
971
+ // Google confirmation screens vary by locale/font/text; click by container/role instead of innerText.
972
+ const confirmSelectors = [
973
+ // Common primary action container on Google OAuth screens.
974
+ 'div.VfPpkd-RLmnJb',
975
+ // Common button class.
976
+ 'button.VfPpkd-LgbsSe',
977
+ // Alternate shape (sometimes rendered as div role=button).
978
+ 'div[role="button"].VfPpkd-LgbsSe'
979
+ ];
980
+ const confirmResult = await waitForAnyElementInPages(context, confirmSelectors, timeoutMs);
657
981
  if (confirmResult) {
658
- const signIn = confirmResult.locator.filter({ hasText: 'Sign in' });
659
- if ((await signIn.count().catch(() => 0)) > 0) {
660
- console.log('[camoufox-launch-auth] Sign-in confirmation span located (jsname=V67aGc), clicking...');
661
- await signIn.first().click({ timeout: timeoutMs }).catch(() => {});
662
- console.log('[camoufox-launch-auth] Antigravity confirmation acknowledged, waiting for callback...');
663
- } else {
664
- console.warn('[camoufox-launch-auth] Confirmation element present but text mismatch; skipping auto-click.');
982
+ console.log(`[camoufox-launch-auth] Confirmation element detected (${confirmResult.selector}), clicking...`);
983
+ try {
984
+ await confirmResult.locator.first().click({ timeout: timeoutMs });
985
+ } catch {
986
+ await confirmResult.page.evaluate((sel) => {
987
+ const el = document.querySelector(sel);
988
+ if (!el) return;
989
+ const events = ['mouseenter', 'mouseover', 'mousemove', 'mousedown', 'mouseup', 'click'];
990
+ for (const type of events) {
991
+ el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, view: window }));
992
+ }
993
+ }, confirmResult.selector);
665
994
  }
995
+ console.log('[camoufox-launch-auth] Antigravity confirmation acknowledged, waiting for callback...');
666
996
  } else {
667
997
  console.log('[camoufox-launch-auth] No Antigravity confirmation button detected within 120s, continuing...');
668
998
  }
669
999
 
670
1000
  const activePage = confirmResult?.page || authPage;
671
- const callbackPage = await waitForCallback(context, activePage);
1001
+ const callbackPage = await waitForCallback(context, activePage, timeoutMs);
672
1002
  callbackObserved = true;
673
1003
  await callbackPage.waitForLoadState('load', { timeout: timeoutMs }).catch(() => {});
674
1004
  console.log('[camoufox-launch-auth] OAuth callback detected, automation complete.');
@@ -683,6 +1013,118 @@ async function runAntigravityAutoFlow({ url, profileDir, camoufoxBinary, devMode
683
1013
  }
684
1014
  }
685
1015
 
1016
+ async function runQwenAutoFlow({ url, profileDir, camoufoxBinary, devMode }) {
1017
+ let firefox;
1018
+ try {
1019
+ ({ firefox } = await import('playwright-core'));
1020
+ } catch (error) {
1021
+ throw new Error(
1022
+ `playwright-core is required for auto qwen auth (${error instanceof Error ? error.message : String(error)})`
1023
+ );
1024
+ }
1025
+
1026
+ const timeoutMs = Number(process.env.ROUTECODEX_CAMOUFOX_QWEN_TIMEOUT_MS || 120_000);
1027
+ cleanupExistingCamoufox(profileDir);
1028
+ const context = await firefox.launchPersistentContext(profileDir, {
1029
+ executablePath: camoufoxBinary,
1030
+ headless: !devMode,
1031
+ acceptDownloads: false,
1032
+ firefoxUserPrefs: buildFirefoxUserPrefs()
1033
+ });
1034
+ let closing = false;
1035
+ const shutdown = async () => {
1036
+ if (closing) return;
1037
+ closing = true;
1038
+ await context.close().catch(() => {});
1039
+ };
1040
+ ['SIGTERM', 'SIGINT', 'SIGHUP'].forEach((signal) => {
1041
+ process.on(signal, () => {
1042
+ void shutdown().finally(() => process.exit(0));
1043
+ });
1044
+ });
1045
+
1046
+ try {
1047
+ const page = context.pages()[0] || (await context.newPage());
1048
+ await page.goto(url, { waitUntil: 'domcontentloaded', timeout: timeoutMs });
1049
+
1050
+ let authPage = page;
1051
+ if (page.url().includes('token-auth')) {
1052
+ console.log('[camoufox-launch-auth] Portal detected, auto-clicking continue button...');
1053
+ const button = page.locator('#continue-btn');
1054
+ const portalButtonTimeoutMs = Number(process.env.ROUTECODEX_CAMOUFOX_PORTAL_BUTTON_TIMEOUT_MS || 300_000);
1055
+ const portalPopupTimeoutMs = Number(process.env.ROUTECODEX_CAMOUFOX_PORTAL_POPUP_TIMEOUT_MS || 300_000);
1056
+ const pageLoadTimeoutMs = Number(process.env.ROUTECODEX_CAMOUFOX_PAGE_LOAD_TIMEOUT_MS || 300_000);
1057
+ await button.waitFor({ timeout: portalButtonTimeoutMs });
1058
+ const popupPromise = context.waitForEvent('page', { timeout: portalPopupTimeoutMs }).catch(() => null);
1059
+ const navPromise = page
1060
+ .waitForURL((current) => typeof current === 'string' && !String(current).includes('token-auth'), {
1061
+ timeout: portalPopupTimeoutMs
1062
+ })
1063
+ .catch(() => null);
1064
+ try {
1065
+ await button.click({ timeout: portalButtonTimeoutMs });
1066
+ } catch {
1067
+ await page.evaluate(() => {
1068
+ const el = document.querySelector('#continue-btn');
1069
+ if (!el) return;
1070
+ const events = ['mouseenter', 'mouseover', 'mousemove', 'mousedown', 'mouseup', 'click'];
1071
+ for (const type of events) {
1072
+ el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, view: window }));
1073
+ }
1074
+ });
1075
+ }
1076
+ const popup = await popupPromise;
1077
+ if (popup) {
1078
+ authPage = popup;
1079
+ } else {
1080
+ await navPromise;
1081
+ authPage = page;
1082
+ }
1083
+ await authPage.waitForLoadState('domcontentloaded', { timeout: pageLoadTimeoutMs }).catch(() => {});
1084
+ }
1085
+
1086
+ console.log('[camoufox-launch-auth] Qwen authorize page loaded, waiting for confirm button...');
1087
+ const confirmSelector = 'button.qwen-confirm-btn';
1088
+ const confirmResult = await waitForElementInPages(context, confirmSelector, timeoutMs);
1089
+ if (!confirmResult) {
1090
+ throw new Error('未能定位 Qwen Confirm 按钮');
1091
+ }
1092
+ console.log('[camoufox-launch-auth] Qwen confirm button detected, clicking...');
1093
+ try {
1094
+ await confirmResult.locator.first().click({ timeout: timeoutMs });
1095
+ } catch {
1096
+ await confirmResult.page.evaluate((sel) => {
1097
+ const el = document.querySelector(sel);
1098
+ if (!el) return;
1099
+ const events = ['mouseenter', 'mouseover', 'mousemove', 'mousedown', 'mouseup', 'click'];
1100
+ for (const type of events) {
1101
+ el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, view: window }));
1102
+ }
1103
+ }, confirmSelector);
1104
+ }
1105
+ console.log('[camoufox-launch-auth] Qwen confirm clicked. Waiting for authorization to settle...');
1106
+ // Heuristics (device-code flow): we don't get a localhost callback, so wait for either:
1107
+ // - confirm button disappears, or
1108
+ // - URL leaves /authorize, or
1109
+ // - a short settle window elapses.
1110
+ await Promise.race([
1111
+ confirmResult.page
1112
+ .locator(confirmSelector)
1113
+ .first()
1114
+ .waitFor({ state: 'detached', timeout: 30_000 })
1115
+ .catch(() => {}),
1116
+ confirmResult.page
1117
+ .waitForURL((current) => typeof current === 'string' && !String(current).includes('/authorize'), {
1118
+ timeout: 30_000
1119
+ })
1120
+ .catch(() => {}),
1121
+ new Promise((resolve) => setTimeout(resolve, 5000))
1122
+ ]);
1123
+ } finally {
1124
+ await shutdown();
1125
+ }
1126
+ }
1127
+
686
1128
  function isBrowserClosedError(error) {
687
1129
  if (!error) {
688
1130
  return false;
@@ -719,34 +1161,88 @@ async function waitForElementInPages(context, selector, timeoutMs) {
719
1161
  return null;
720
1162
  }
721
1163
 
722
- async function waitForCallback(context, fallbackPage, timeoutMs = 120000) {
1164
+ async function waitForAnyElementInPages(context, selectors, timeoutMs) {
1165
+ const list = Array.isArray(selectors) ? selectors.filter(Boolean) : [];
1166
+ if (list.length === 0) {
1167
+ return null;
1168
+ }
1169
+ const start = Date.now();
1170
+ while (Date.now() - start < timeoutMs) {
1171
+ for (const candidate of context.pages()) {
1172
+ for (const selector of list) {
1173
+ try {
1174
+ const locator = candidate.locator(selector);
1175
+ if ((await locator.count()) > 0) {
1176
+ return { page: candidate, locator, selector };
1177
+ }
1178
+ } catch {
1179
+ // ignore closed pages
1180
+ }
1181
+ }
1182
+ }
1183
+ const elapsed = Date.now() - start;
1184
+ const remaining = timeoutMs - elapsed;
1185
+ const waitSlice = Math.min(1000, remaining);
1186
+ if (waitSlice <= 0) {
1187
+ break;
1188
+ }
1189
+ try {
1190
+ await context.waitForEvent('page', { timeout: waitSlice });
1191
+ } catch {
1192
+ await new Promise((resolve) => setTimeout(resolve, waitSlice));
1193
+ }
1194
+ }
1195
+ return null;
1196
+ }
1197
+
1198
+ async function waitForCallback(context, _fallbackPage, timeoutMs = 120000) {
723
1199
  const isCallbackUrl = (current) => {
724
- if (typeof current !== 'string') {
1200
+ if (typeof current !== 'string' || !current) {
1201
+ return false;
1202
+ }
1203
+ try {
1204
+ const parsed = new URL(current);
1205
+ const host = parsed.hostname.toLowerCase();
1206
+ if (host !== '127.0.0.1' && host !== 'localhost') {
1207
+ return false;
1208
+ }
1209
+ const pathname = parsed.pathname.toLowerCase();
1210
+ const isOAuthCallback = pathname === '/oauth2callback' || /oauth.*callback/.test(pathname);
1211
+ if (!isOAuthCallback) {
1212
+ return false;
1213
+ }
1214
+ return parsed.searchParams.has('code') || parsed.searchParams.has('error');
1215
+ } catch {
725
1216
  return false;
726
1217
  }
727
- const lower = current.toLowerCase();
728
- return (
729
- lower.startsWith('http://127.0.0.1') ||
730
- lower.startsWith('http://localhost') ||
731
- lower.startsWith('https://127.0.0.1')
732
- );
733
1218
  };
734
1219
 
735
- const currentPages = context.pages();
736
- for (const page of currentPages) {
737
- if (isCallbackUrl(page.url())) {
738
- return page;
1220
+ const startedAt = Date.now();
1221
+ while (Date.now() - startedAt < timeoutMs) {
1222
+ for (const page of context.pages()) {
1223
+ try {
1224
+ if (isCallbackUrl(page.url())) {
1225
+ await page.waitForLoadState('domcontentloaded', { timeout: 60000 }).catch(() => {});
1226
+ return page;
1227
+ }
1228
+ } catch {
1229
+ // ignore closed page races
1230
+ }
1231
+ }
1232
+
1233
+ const elapsed = Date.now() - startedAt;
1234
+ const remaining = timeoutMs - elapsed;
1235
+ const waitSlice = Math.min(1000, remaining);
1236
+ if (waitSlice <= 0) {
1237
+ break;
739
1238
  }
740
- }
741
1239
 
742
- try {
743
- await fallbackPage.waitForURL((current) => isCallbackUrl(current), { timeout: timeoutMs });
744
- return fallbackPage;
745
- } catch {
746
- // ignore and wait for popup
1240
+ try {
1241
+ await context.waitForEvent('page', { timeout: waitSlice });
1242
+ } catch {
1243
+ await new Promise((resolve) => setTimeout(resolve, waitSlice));
1244
+ }
747
1245
  }
748
1246
 
749
- const callback = await context.waitForEvent('page', { timeout: timeoutMs });
750
- await callback.waitForLoadState('domcontentloaded', { timeout: 60000 }).catch(() => {});
751
- return callback;
1247
+ throw new Error('Timed out waiting for OAuth callback URL (code/error not observed)');
752
1248
  }