@jsonstudio/rcc 0.89.2239 → 0.90.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/README.md +27 -0
  2. package/dist/build-info.js +2 -2
  3. package/dist/build-info.js.map +1 -1
  4. package/dist/cli/commands/claude.js +1 -0
  5. package/dist/cli/commands/claude.js.map +1 -1
  6. package/dist/cli/commands/codex.js +1 -0
  7. package/dist/cli/commands/codex.js.map +1 -1
  8. package/dist/cli/commands/guardian-daemon.d.ts +2 -0
  9. package/dist/cli/commands/guardian-daemon.js +299 -0
  10. package/dist/cli/commands/guardian-daemon.js.map +1 -0
  11. package/dist/cli/commands/init/camoufox.js +1 -1
  12. package/dist/cli/commands/init/camoufox.js.map +1 -1
  13. package/dist/cli/commands/launcher/types.d.ts +6 -0
  14. package/dist/cli/commands/launcher-kernel.js +456 -109
  15. package/dist/cli/commands/launcher-kernel.js.map +1 -1
  16. package/dist/cli/commands/port.js +28 -8
  17. package/dist/cli/commands/port.js.map +1 -1
  18. package/dist/cli/commands/restart.d.ts +4 -0
  19. package/dist/cli/commands/restart.js +91 -42
  20. package/dist/cli/commands/restart.js.map +1 -1
  21. package/dist/cli/commands/start-types.d.ts +4 -0
  22. package/dist/cli/commands/start.js +108 -65
  23. package/dist/cli/commands/start.js.map +1 -1
  24. package/dist/cli/commands/stop.d.ts +3 -0
  25. package/dist/cli/commands/stop.js +30 -63
  26. package/dist/cli/commands/stop.js.map +1 -1
  27. package/dist/cli/config/init-provider-catalog.js +8 -3
  28. package/dist/cli/config/init-provider-catalog.js.map +1 -1
  29. package/dist/cli/guardian/client.d.ts +38 -0
  30. package/dist/cli/guardian/client.js +237 -0
  31. package/dist/cli/guardian/client.js.map +1 -0
  32. package/dist/cli/guardian/paths.d.ts +7 -0
  33. package/dist/cli/guardian/paths.js +13 -0
  34. package/dist/cli/guardian/paths.js.map +1 -0
  35. package/dist/cli/guardian/types.d.ts +30 -0
  36. package/dist/cli/guardian/types.js +2 -0
  37. package/dist/cli/guardian/types.js.map +1 -0
  38. package/dist/cli/register/guardian-daemon-command.d.ts +2 -0
  39. package/dist/cli/register/guardian-daemon-command.js +5 -0
  40. package/dist/cli/register/guardian-daemon-command.js.map +1 -0
  41. package/dist/cli/server/port-utils.js +57 -1
  42. package/dist/cli/server/port-utils.js.map +1 -1
  43. package/dist/cli.js +48 -0
  44. package/dist/cli.js.map +1 -1
  45. package/dist/commands/oauth.js +6 -6
  46. package/dist/commands/oauth.js.map +1 -1
  47. package/dist/config/routecodex-config-loader.js +66 -1
  48. package/dist/config/routecodex-config-loader.js.map +1 -1
  49. package/dist/config/virtual-router-builder.js +18 -0
  50. package/dist/config/virtual-router-builder.js.map +1 -1
  51. package/dist/config/virtual-router-types.js +20 -5
  52. package/dist/config/virtual-router-types.js.map +1 -1
  53. package/dist/daemon-admin-ui/assets/index-C8vP_c5E.js +15 -0
  54. package/dist/daemon-admin-ui/assets/index-DjIoHmNv.css +1 -0
  55. package/dist/daemon-admin-ui/index.html +13 -0
  56. package/dist/docs/daemon-admin-ui.html +328 -57
  57. package/dist/index.d.ts +9 -0
  58. package/dist/index.js +268 -10
  59. package/dist/index.js.map +1 -1
  60. package/dist/manager/modules/quota/provider-quota-daemon.error-helpers.d.ts +1 -0
  61. package/dist/manager/modules/quota/provider-quota-daemon.error-helpers.js +36 -0
  62. package/dist/manager/modules/quota/provider-quota-daemon.error-helpers.js.map +1 -1
  63. package/dist/manager/modules/quota/provider-quota-daemon.events.js +50 -1
  64. package/dist/manager/modules/quota/provider-quota-daemon.events.js.map +1 -1
  65. package/dist/providers/auth/antigravity-user-agent.js +78 -31
  66. package/dist/providers/auth/antigravity-user-agent.js.map +1 -1
  67. package/dist/providers/auth/gemini-cli-userinfo-helper.js +94 -63
  68. package/dist/providers/auth/gemini-cli-userinfo-helper.js.map +1 -1
  69. package/dist/providers/auth/iflow-userinfo-helper.js +1 -1
  70. package/dist/providers/auth/iflow-userinfo-helper.js.map +1 -1
  71. package/dist/providers/auth/oauth-error-message.d.ts +1 -0
  72. package/dist/providers/auth/oauth-error-message.js +44 -0
  73. package/dist/providers/auth/oauth-error-message.js.map +1 -0
  74. package/dist/providers/auth/oauth-lifecycle/error-detection.js +42 -8
  75. package/dist/providers/auth/oauth-lifecycle/error-detection.js.map +1 -1
  76. package/dist/providers/auth/oauth-lifecycle/token-io.d.ts +1 -0
  77. package/dist/providers/auth/oauth-lifecycle/token-io.js +12 -0
  78. package/dist/providers/auth/oauth-lifecycle/token-io.js.map +1 -1
  79. package/dist/providers/auth/oauth-lifecycle.js +502 -87
  80. package/dist/providers/auth/oauth-lifecycle.js.map +1 -1
  81. package/dist/providers/auth/oauth-repair-cooldown.js +2 -7
  82. package/dist/providers/auth/oauth-repair-cooldown.js.map +1 -1
  83. package/dist/providers/auth/oauth-repair-env.js +3 -5
  84. package/dist/providers/auth/oauth-repair-env.js.map +1 -1
  85. package/dist/providers/auth/oauth-utils/error-extraction.js +42 -8
  86. package/dist/providers/auth/oauth-utils/error-extraction.js.map +1 -1
  87. package/dist/providers/core/config/camoufox-actions.d.ts +31 -0
  88. package/dist/providers/core/config/camoufox-actions.js +461 -0
  89. package/dist/providers/core/config/camoufox-actions.js.map +1 -0
  90. package/dist/providers/core/config/camoufox-launcher.d.ts +3 -0
  91. package/dist/providers/core/config/camoufox-launcher.js +518 -160
  92. package/dist/providers/core/config/camoufox-launcher.js.map +1 -1
  93. package/dist/providers/core/config/oauth-flows.js +6 -44
  94. package/dist/providers/core/config/oauth-flows.js.map +1 -1
  95. package/dist/providers/core/config/provider-oauth-configs.js +51 -7
  96. package/dist/providers/core/config/provider-oauth-configs.js.map +1 -1
  97. package/dist/providers/core/runtime/provider-error-classifier.js +32 -15
  98. package/dist/providers/core/runtime/provider-error-classifier.js.map +1 -1
  99. package/dist/providers/core/runtime/provider-family-profile-utils.js +1 -1
  100. package/dist/providers/core/runtime/provider-family-profile-utils.js.map +1 -1
  101. package/dist/providers/core/runtime/provider-response-postprocessor.js +61 -14
  102. package/dist/providers/core/runtime/provider-response-postprocessor.js.map +1 -1
  103. package/dist/providers/core/strategies/oauth-auth-code-flow.d.ts +1 -0
  104. package/dist/providers/core/strategies/oauth-auth-code-flow.js +124 -19
  105. package/dist/providers/core/strategies/oauth-auth-code-flow.js.map +1 -1
  106. package/dist/providers/core/strategies/oauth-device-flow.js +6 -3
  107. package/dist/providers/core/strategies/oauth-device-flow.js.map +1 -1
  108. package/dist/providers/profile/families/iflow-profile.js +83 -10
  109. package/dist/providers/profile/families/iflow-profile.js.map +1 -1
  110. package/dist/scripts/camoufox/launch-auth.mjs +112 -5
  111. package/dist/server/handlers/config-admin-handler.js +9 -2
  112. package/dist/server/handlers/config-admin-handler.js.map +1 -1
  113. package/dist/server/handlers/handler-utils.js +3 -12
  114. package/dist/server/handlers/handler-utils.js.map +1 -1
  115. package/dist/server/handlers/logging.js +3 -4
  116. package/dist/server/handlers/logging.js.map +1 -1
  117. package/dist/server/runtime/http-server/clock-client-reaper.js +3 -26
  118. package/dist/server/runtime/http-server/clock-client-reaper.js.map +1 -1
  119. package/dist/server/runtime/http-server/clock-client-registry-utils.d.ts +4 -0
  120. package/dist/server/runtime/http-server/clock-client-registry-utils.js +74 -16
  121. package/dist/server/runtime/http-server/clock-client-registry-utils.js.map +1 -1
  122. package/dist/server/runtime/http-server/clock-client-registry.d.ts +15 -0
  123. package/dist/server/runtime/http-server/clock-client-registry.js +300 -6
  124. package/dist/server/runtime/http-server/clock-client-registry.js.map +1 -1
  125. package/dist/server/runtime/http-server/clock-client-routes.js +49 -19
  126. package/dist/server/runtime/http-server/clock-client-routes.js.map +1 -1
  127. package/dist/server/runtime/http-server/clock-daemon-log-throttle.d.ts +16 -0
  128. package/dist/server/runtime/http-server/clock-daemon-log-throttle.js +49 -0
  129. package/dist/server/runtime/http-server/clock-daemon-log-throttle.js.map +1 -1
  130. package/dist/server/runtime/http-server/clock-scope-resolution.d.ts +14 -0
  131. package/dist/server/runtime/http-server/clock-scope-resolution.js +212 -0
  132. package/dist/server/runtime/http-server/clock-scope-resolution.js.map +1 -0
  133. package/dist/server/runtime/http-server/daemon-admin/auth-handler.js +5 -3
  134. package/dist/server/runtime/http-server/daemon-admin/auth-handler.js.map +1 -1
  135. package/dist/server/runtime/http-server/daemon-admin/control-handler.js +104 -15
  136. package/dist/server/runtime/http-server/daemon-admin/control-handler.js.map +1 -1
  137. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +2 -2
  138. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -1
  139. package/dist/server/runtime/http-server/daemon-admin/providers-handler-routing-utils.d.ts +24 -0
  140. package/dist/server/runtime/http-server/daemon-admin/providers-handler-routing-utils.js +316 -70
  141. package/dist/server/runtime/http-server/daemon-admin/providers-handler-routing-utils.js.map +1 -1
  142. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js +190 -1
  143. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js.map +1 -1
  144. package/dist/server/runtime/http-server/daemon-admin/routing-policy.js +18 -29
  145. package/dist/server/runtime/http-server/daemon-admin/routing-policy.js.map +1 -1
  146. package/dist/server/runtime/http-server/daemon-admin/stats-handler.js +2 -0
  147. package/dist/server/runtime/http-server/daemon-admin/stats-handler.js.map +1 -1
  148. package/dist/server/runtime/http-server/daemon-admin-routes.d.ts +8 -1
  149. package/dist/server/runtime/http-server/daemon-admin-routes.js +30 -0
  150. package/dist/server/runtime/http-server/daemon-admin-routes.js.map +1 -1
  151. package/dist/server/runtime/http-server/executor/client-injection-flow.d.ts +14 -0
  152. package/dist/server/runtime/http-server/executor/client-injection-flow.js +287 -0
  153. package/dist/server/runtime/http-server/executor/client-injection-flow.js.map +1 -0
  154. package/dist/server/runtime/http-server/executor/index.d.ts +1 -1
  155. package/dist/server/runtime/http-server/executor/index.js +1 -1
  156. package/dist/server/runtime/http-server/executor/index.js.map +1 -1
  157. package/dist/server/runtime/http-server/executor/provider-response-converter.js +236 -62
  158. package/dist/server/runtime/http-server/executor/provider-response-converter.js.map +1 -1
  159. package/dist/server/runtime/http-server/executor/request-executor-core-utils.d.ts +1 -0
  160. package/dist/server/runtime/http-server/executor/request-executor-core-utils.js +12 -0
  161. package/dist/server/runtime/http-server/executor/request-executor-core-utils.js.map +1 -1
  162. package/dist/server/runtime/http-server/executor/request-retry-helpers.js +16 -12
  163. package/dist/server/runtime/http-server/executor/request-retry-helpers.js.map +1 -1
  164. package/dist/server/runtime/http-server/executor/sse-error-handler.d.ts +1 -0
  165. package/dist/server/runtime/http-server/executor/sse-error-handler.js +13 -2
  166. package/dist/server/runtime/http-server/executor/sse-error-handler.js.map +1 -1
  167. package/dist/server/runtime/http-server/executor/usage-aggregator.d.ts +0 -12
  168. package/dist/server/runtime/http-server/executor/usage-aggregator.js +84 -88
  169. package/dist/server/runtime/http-server/executor/usage-aggregator.js.map +1 -1
  170. package/dist/server/runtime/http-server/executor-metadata.js +328 -7
  171. package/dist/server/runtime/http-server/executor-metadata.js.map +1 -1
  172. package/dist/server/runtime/http-server/executor-response.d.ts +1 -0
  173. package/dist/server/runtime/http-server/executor-response.js +52 -58
  174. package/dist/server/runtime/http-server/executor-response.js.map +1 -1
  175. package/dist/server/runtime/http-server/http-server-bootstrap.js +50 -6
  176. package/dist/server/runtime/http-server/http-server-bootstrap.js.map +1 -1
  177. package/dist/server/runtime/http-server/http-server-clock-daemon.d.ts +1 -0
  178. package/dist/server/runtime/http-server/http-server-clock-daemon.js +186 -44
  179. package/dist/server/runtime/http-server/http-server-clock-daemon.js.map +1 -1
  180. package/dist/server/runtime/http-server/http-server-lifecycle.js +4 -4
  181. package/dist/server/runtime/http-server/http-server-lifecycle.js.map +1 -1
  182. package/dist/server/runtime/http-server/hub-shadow-compare.js +1 -1
  183. package/dist/server/runtime/http-server/hub-shadow-compare.js.map +1 -1
  184. package/dist/server/runtime/http-server/index.d.ts +1 -0
  185. package/dist/server/runtime/http-server/index.js +1 -0
  186. package/dist/server/runtime/http-server/index.js.map +1 -1
  187. package/dist/server/runtime/http-server/middleware.js +82 -4
  188. package/dist/server/runtime/http-server/middleware.js.map +1 -1
  189. package/dist/server/runtime/http-server/request-executor.js +6 -5
  190. package/dist/server/runtime/http-server/request-executor.js.map +1 -1
  191. package/dist/server/runtime/http-server/routes.d.ts +2 -1
  192. package/dist/server/runtime/http-server/routes.js +4 -2
  193. package/dist/server/runtime/http-server/routes.js.map +1 -1
  194. package/dist/server/runtime/http-server/session-dir.js +12 -1
  195. package/dist/server/runtime/http-server/session-dir.js.map +1 -1
  196. package/dist/server/runtime/http-server/stats-manager.d.ts +35 -0
  197. package/dist/server/runtime/http-server/stats-manager.js +269 -21
  198. package/dist/server/runtime/http-server/stats-manager.js.map +1 -1
  199. package/dist/server/runtime/http-server/stopmessage-scope-rebind.d.ts +13 -0
  200. package/dist/server/runtime/http-server/stopmessage-scope-rebind.js +168 -0
  201. package/dist/server/runtime/http-server/stopmessage-scope-rebind.js.map +1 -0
  202. package/dist/server/runtime/http-server/tmux-session-probe.d.ts +10 -0
  203. package/dist/server/runtime/http-server/tmux-session-probe.js +97 -0
  204. package/dist/server/runtime/http-server/tmux-session-probe.js.map +1 -1
  205. package/dist/server-lifecycle/port-utils.d.ts +2 -1
  206. package/dist/server-lifecycle/port-utils.js +84 -4
  207. package/dist/server-lifecycle/port-utils.js.map +1 -1
  208. package/dist/token-daemon/index.d.ts +1 -0
  209. package/dist/token-daemon/index.js +17 -12
  210. package/dist/token-daemon/index.js.map +1 -1
  211. package/dist/utils/clock-client-token.d.ts +2 -1
  212. package/dist/utils/clock-client-token.js +52 -8
  213. package/dist/utils/clock-client-token.js.map +1 -1
  214. package/dist/utils/clock-scope-trace.d.ts +11 -0
  215. package/dist/utils/clock-scope-trace.js +41 -0
  216. package/dist/utils/clock-scope-trace.js.map +1 -0
  217. package/dist/utils/llms-engine-shadow.js +1 -1
  218. package/dist/utils/llms-engine-shadow.js.map +1 -1
  219. package/docs/DAEMON_CONTROL_PLANE.md +1 -0
  220. package/docs/ROUTING_POLICY_SCHEMA.md +4 -2
  221. package/docs/daemon-admin-ui.html +328 -57
  222. package/docs/design/servertool-stopmessage-lifecycle.md +109 -0
  223. package/docs/exec-command-guard-policy.example.v1.json +7 -1
  224. package/docs/providers/antigravity-gemini-provider-compat.md +2 -2
  225. package/package.json +21 -5
  226. package/scripts/build-core.mjs +12 -0
  227. package/scripts/camoufox/launch-auth.mjs +112 -5
  228. package/scripts/ci/repo-sanity.mjs +1 -0
  229. package/scripts/install-global.sh +6 -0
  230. package/scripts/install-verify.mjs +33 -16
  231. package/scripts/run-bg.sh +226 -43
  232. package/scripts/run-fg-gtimeout.sh +158 -14
  233. package/scripts/tests/blackbox-rcc-vs-routecodex-antigravity.mjs +3 -3
  234. package/scripts/tests/ci-jest.mjs +9 -1
  235. package/scripts/triage-errorsamples.mjs +216 -0
  236. package/scripts/verify-codex-error-samples.mjs +92 -15
  237. package/scripts/verify-install-e2e.mjs +57 -27
@@ -4,77 +4,28 @@ import fs from 'fs/promises';
4
4
  import fsSync from 'fs';
5
5
  import path from 'path';
6
6
  import os from 'os';
7
+ import { spawnSync } from 'node:child_process';
7
8
  import { fetchIFlowUserInfo, mergeIFlowTokenData } from './iflow-userinfo-helper.js';
8
9
  import { fetchQwenUserInfo, mergeQwenTokenData } from './qwen-userinfo-helper.js';
9
10
  import { fetchGeminiCLIUserInfo, fetchGeminiCLIProjects, mergeGeminiCLITokenData, getDefaultProjectId } from './gemini-cli-userinfo-helper.js';
10
11
  import { parseTokenSequenceFromPath } from './token-scanner/index.js';
11
12
  import { logOAuthDebug } from './oauth-logger.js';
13
+ import { formatOAuthErrorMessage } from './oauth-error-message.js';
12
14
  import { fetchAntigravityProjectId } from './antigravity-userinfo-helper.js';
13
15
  import { HTTP_PROTOCOLS, LOCAL_HOSTS } from '../../constants/index.js';
14
16
  import { withOAuthRepairEnv } from './oauth-repair-env.js';
15
17
  import { markInteractiveOAuthRepairAttempt, markInteractiveOAuthRepairSuccess, shouldSkipInteractiveOAuthRepair } from './oauth-repair-cooldown.js';
16
18
  import { openAuthInCamoufox } from '../core/config/camoufox-launcher.js';
17
- import { isGeminiCliFamily, resolveTokenFilePath, resolveCamoufoxAliasForAuth, resolveIflowCredentialCandidates } from './oauth-lifecycle/path-resolver.js';
18
- import { keyFor, shouldThrottle, updateThrottle, inFlight, interactiveTail } from './oauth-lifecycle/throttle.js';
19
+ import { isGeminiCliFamily, resolveTokenFilePath, resolveCamoufoxAliasForAuth } from './oauth-lifecycle/path-resolver.js';
20
+ import { keyFor, shouldThrottle, updateThrottle, lastRunAt, inFlight, interactiveTail } from './oauth-lifecycle/throttle.js';
19
21
  import { extractStatusCode, isGoogleAccountVerificationRequiredMessage, extractGoogleAccountVerificationUrl } from './oauth-lifecycle/error-detection.js';
20
22
  import { hasNonEmptyString, extractAccessToken, extractApiKey, hasApiKeyField, hasStableQwenApiKey, hasAccessToken, getExpiresAt, resolveProjectId, coerceExpiryTimestampSeconds, hasNoRefreshFlag, evaluateTokenState } from './oauth-lifecycle/token-helpers.js';
21
- import { normalizeGeminiCliAccountToken, sanitizeToken, readTokenFromFile, backupTokenFile, restoreTokenFileFromBackup, discardBackupFile, readRawTokenFile } from './oauth-lifecycle/token-io.js';
23
+ import { normalizeGeminiCliAccountToken, sanitizeToken, readTokenFromFile, backupTokenFile, restoreTokenFileFromBackup, discardBackupFile, clearTokenFile, readRawTokenFile } from './oauth-lifecycle/token-io.js';
24
+ const OAUTH_INTERACTIVE_LOCK_FILE = path.join(os.homedir(), '.routecodex', 'auth', '.oauth-interactive.lock.json');
25
+ const IFLOW_AUTO_FAILURE_FILE = path.join(os.homedir(), '.routecodex', 'auth', '.iflow-auto-failures.json');
26
+ const OAUTH_THROTTLE_WINDOW_MS = 60_000;
27
+ const IFLOW_REFRESH_FAILURE_BACKOFF_MS = 5 * 60_000;
22
28
  const TOKEN_REFRESH_SKEW_MS = 60_000;
23
- async function selectBestIflowTokenCandidate(targetTokenFilePath) {
24
- const targetResolved = path.resolve(targetTokenFilePath);
25
- const candidates = resolveIflowCredentialCandidates();
26
- let best = null;
27
- for (const candidatePath of candidates) {
28
- if (!candidatePath) {
29
- continue;
30
- }
31
- const resolved = path.resolve(candidatePath);
32
- if (resolved === targetResolved) {
33
- continue;
34
- }
35
- const token = await readTokenFromFile(candidatePath);
36
- if (!token) {
37
- continue;
38
- }
39
- const state = evaluateTokenState(token, 'iflow');
40
- if (!state.validAccess) {
41
- continue;
42
- }
43
- const expiresAt = state.expiresAt ?? null;
44
- if (!best) {
45
- best = { token, sourcePath: candidatePath, expiresAt };
46
- continue;
47
- }
48
- const bestExpiry = best.expiresAt ?? -1;
49
- const currentExpiry = expiresAt ?? -1;
50
- if (currentExpiry > bestExpiry) {
51
- best = { token, sourcePath: candidatePath, expiresAt };
52
- }
53
- }
54
- return best;
55
- }
56
- async function maybeAdoptIflowExternalToken(strategy, tokenFilePath, existingToken) {
57
- const currentState = evaluateTokenState(existingToken, 'iflow');
58
- if (currentState.validAccess) {
59
- return existingToken;
60
- }
61
- const bestCandidate = await selectBestIflowTokenCandidate(tokenFilePath);
62
- if (!bestCandidate) {
63
- return existingToken;
64
- }
65
- // Keep per-alias token files in RouteCodex, but adopt fresh credentials from iFlow-native stores when available.
66
- const prepared = await prepareTokenForStorage('iflow', tokenFilePath, bestCandidate.token);
67
- if (typeof strategy.saveToken === 'function') {
68
- await strategy.saveToken(prepared);
69
- }
70
- else {
71
- await fs.mkdir(path.dirname(tokenFilePath), { recursive: true });
72
- await fs.writeFile(tokenFilePath, `${JSON.stringify(prepared, null, 2)}\n`, 'utf8');
73
- }
74
- const normalized = sanitizeToken(prepared) ?? bestCandidate.token;
75
- logOAuthDebug(`[OAuth] iflow token adopted from ${bestCandidate.sourcePath} -> ${tokenFilePath}`);
76
- return normalized;
77
- }
78
29
  async function openGoogleAccountVerificationInCamoufox(args) {
79
30
  const providerType = args.providerType;
80
31
  const url = args.url;
@@ -126,6 +77,129 @@ async function openGoogleAccountVerificationInCamoufox(args) {
126
77
  }
127
78
  }
128
79
  }
80
+ function isIflowRefreshEndpointRejectionMessage(message) {
81
+ const normalized = (message || '').toLowerCase();
82
+ if (!normalized) {
83
+ return false;
84
+ }
85
+ return (normalized.includes('oauth token endpoint rejected request') ||
86
+ (normalized.includes('token refresh failed') && normalized.includes('iflow.cn/oauth/token')));
87
+ }
88
+ function isIflowAkBlockedMessage(message) {
89
+ const normalized = (message || '').toLowerCase();
90
+ if (!normalized) {
91
+ return false;
92
+ }
93
+ return normalized.includes('access to the current ak has been blocked due to unauthorized requests');
94
+ }
95
+ function shouldClearIflowTokenOnRefreshFailure(message) {
96
+ const normalized = (message || '').toLowerCase();
97
+ if (!normalized) {
98
+ return false;
99
+ }
100
+ if (isIflowAkBlockedMessage(normalized)) {
101
+ return false;
102
+ }
103
+ if (normalized.includes('oauth error: invalid_grant')) {
104
+ return true;
105
+ }
106
+ if (normalized.includes('oauth error: invalid_client')) {
107
+ return true;
108
+ }
109
+ if (normalized.includes('oauth error: unauthorized_client')) {
110
+ return true;
111
+ }
112
+ if (normalized.includes('oauth error: invalid_request') &&
113
+ (normalized.includes('refresh token') ||
114
+ normalized.includes('refresh_token') ||
115
+ normalized.includes('client_id'))) {
116
+ return true;
117
+ }
118
+ return false;
119
+ }
120
+ function applyRefreshFailureBackoff(cacheKey, providerType, message) {
121
+ if (providerType !== 'iflow' || !isIflowRefreshEndpointRejectionMessage(message)) {
122
+ return;
123
+ }
124
+ // For iFlow refresh endpoint 500/generic failures, avoid hammering token endpoint
125
+ // from preflight/retry loops. Keep a longer cooldown before next refresh attempt.
126
+ lastRunAt.set(cacheKey, Date.now() + IFLOW_REFRESH_FAILURE_BACKOFF_MS - OAUTH_THROTTLE_WINDOW_MS);
127
+ }
128
+ function isElementMissingAutomationFailure(message) {
129
+ const normalized = String(message || '').toLowerCase();
130
+ if (!normalized) {
131
+ return false;
132
+ }
133
+ return (normalized.includes('element not found') ||
134
+ normalized.includes('element_not_found') ||
135
+ normalized.includes('required but not matched'));
136
+ }
137
+ async function runInteractiveRepairWithAutoFallback(args) {
138
+ const { providerType, auth, ensureValid, opts } = args;
139
+ const autoModeAtStart = String(process.env.ROUTECODEX_CAMOUFOX_AUTO_MODE || '').trim();
140
+ try {
141
+ await ensureValid(providerType, auth, opts);
142
+ return;
143
+ }
144
+ catch (error) {
145
+ if (!autoModeAtStart) {
146
+ throw error;
147
+ }
148
+ const msg = error instanceof Error ? error.message : String(error || '');
149
+ const selectorFailure = isElementMissingAutomationFailure(msg);
150
+ let tokenFilePath = '';
151
+ try {
152
+ tokenFilePath = resolveTokenFilePath(auth, providerType);
153
+ }
154
+ catch {
155
+ tokenFilePath = '';
156
+ }
157
+ if (tokenFilePath) {
158
+ closeOAuthAuthResources(providerType, tokenFilePath);
159
+ }
160
+ console.warn(`[OAuth] Camoufox auto OAuth failed (${providerType}, autoMode=${autoModeAtStart}): ${msg}. Falling back to headful manual mode once.`);
161
+ if (selectorFailure) {
162
+ console.warn(`[OAuth] Camoufox auto selector step failed; switched to headful manual mode (provider=${providerType}${tokenFilePath ? ` tokenFile=${tokenFilePath}` : ''}).`);
163
+ }
164
+ }
165
+ const prevAutoMode = process.env.ROUTECODEX_CAMOUFOX_AUTO_MODE;
166
+ const prevAutoConfirm = process.env.ROUTECODEX_OAUTH_AUTO_CONFIRM;
167
+ const prevDevMode = process.env.ROUTECODEX_CAMOUFOX_DEV_MODE;
168
+ const prevOpenOnly = process.env.ROUTECODEX_CAMOUFOX_OPEN_ONLY;
169
+ try {
170
+ delete process.env.ROUTECODEX_CAMOUFOX_AUTO_MODE;
171
+ delete process.env.ROUTECODEX_OAUTH_AUTO_CONFIRM;
172
+ process.env.ROUTECODEX_CAMOUFOX_DEV_MODE = '1';
173
+ process.env.ROUTECODEX_CAMOUFOX_OPEN_ONLY = '1';
174
+ await ensureValid(providerType, auth, opts);
175
+ }
176
+ finally {
177
+ if (prevAutoMode === undefined) {
178
+ delete process.env.ROUTECODEX_CAMOUFOX_AUTO_MODE;
179
+ }
180
+ else {
181
+ process.env.ROUTECODEX_CAMOUFOX_AUTO_MODE = prevAutoMode;
182
+ }
183
+ if (prevAutoConfirm === undefined) {
184
+ delete process.env.ROUTECODEX_OAUTH_AUTO_CONFIRM;
185
+ }
186
+ else {
187
+ process.env.ROUTECODEX_OAUTH_AUTO_CONFIRM = prevAutoConfirm;
188
+ }
189
+ if (prevDevMode === undefined) {
190
+ delete process.env.ROUTECODEX_CAMOUFOX_DEV_MODE;
191
+ }
192
+ else {
193
+ process.env.ROUTECODEX_CAMOUFOX_DEV_MODE = prevDevMode;
194
+ }
195
+ if (prevOpenOnly === undefined) {
196
+ delete process.env.ROUTECODEX_CAMOUFOX_OPEN_ONLY;
197
+ }
198
+ else {
199
+ process.env.ROUTECODEX_CAMOUFOX_OPEN_ONLY = prevOpenOnly;
200
+ }
201
+ }
202
+ }
129
203
  function isOAuthConfig(auth) {
130
204
  return Boolean(auth && typeof auth.type === 'string' && auth.type.toLowerCase().includes('oauth'));
131
205
  }
@@ -396,7 +470,7 @@ function buildHeaderOverrides(defaults, providerType) {
396
470
  if (providerType === 'iflow') {
397
471
  return {
398
472
  ...baseHeaders,
399
- 'User-Agent': 'iflow-cli/2.0',
473
+ 'User-Agent': 'iFlow-Cli',
400
474
  'X-Requested-With': 'XMLHttpRequest',
401
475
  'Origin': 'https://iflow.cn',
402
476
  'Referer': 'https://iflow.cn/oauth',
@@ -489,7 +563,7 @@ async function maybeEnrichToken(providerType, tokenData, tokenFilePath) {
489
563
  return mergeQwenTokenData(tokenData, userInfo);
490
564
  }
491
565
  catch (error) {
492
- const msg = error instanceof Error ? error.message : String(error);
566
+ const msg = formatOAuthErrorMessage(error);
493
567
  // If userInfo endpoint is unavailable (404), treat access_token as api_key to avoid repeated lookups.
494
568
  if (/\bHTTP\s+404\b/i.test(msg) || /\bnot\s+found\b/i.test(msg)) {
495
569
  logOAuthDebug('[OAuth] Qwen: userInfo endpoint unavailable (404); using access_token as api_key fallback');
@@ -511,7 +585,7 @@ async function maybeEnrichToken(providerType, tokenData, tokenFilePath) {
511
585
  return mergeIFlowTokenData(tokenData, userInfo);
512
586
  }
513
587
  catch (error) {
514
- console.error(`[OAuth] iFlow: failed to fetch API Key - ${error instanceof Error ? error.message : String(error)}`);
588
+ console.error(`[OAuth] iFlow: failed to fetch API Key - ${formatOAuthErrorMessage(error)}`);
515
589
  return tokenData;
516
590
  }
517
591
  }
@@ -532,7 +606,7 @@ async function maybeEnrichToken(providerType, tokenData, tokenFilePath) {
532
606
  return merged;
533
607
  }
534
608
  catch (error) {
535
- const msg = error instanceof Error ? error.message : String(error);
609
+ const msg = formatOAuthErrorMessage(error);
536
610
  console.error(`[OAuth] Antigravity: failed to fetch metadata - ${msg}`);
537
611
  const normalized = msg.toLowerCase();
538
612
  const isAuthError = normalized.includes('401') ||
@@ -560,7 +634,7 @@ async function maybeEnrichToken(providerType, tokenData, tokenFilePath) {
560
634
  projects = await fetchGeminiCLIProjects(accessToken);
561
635
  }
562
636
  catch (projectsError) {
563
- const msg = projectsError instanceof Error ? projectsError.message : String(projectsError);
637
+ const msg = formatOAuthErrorMessage(projectsError);
564
638
  console.error(`[OAuth] ${label}: failed to fetch Projects - ${msg}`);
565
639
  projects = [];
566
640
  }
@@ -580,7 +654,7 @@ async function maybeEnrichToken(providerType, tokenData, tokenFilePath) {
580
654
  return merged;
581
655
  }
582
656
  catch (error) {
583
- const msg = error instanceof Error ? error.message : String(error);
657
+ const msg = formatOAuthErrorMessage(error);
584
658
  console.error(`[OAuth] ${label}: failed to fetch UserInfo - ${msg}`);
585
659
  // 将明确的 401/invalid token 视为凭证失效,由调用方决定是否触发重新授权。
586
660
  const normalized = msg.toLowerCase();
@@ -615,6 +689,255 @@ function logOAuthSetup(providerType, defaults, overrides, endpoints, client, tok
615
689
  function createStrategy(providerType, overrides, tokenFilePath) {
616
690
  return createProviderOAuthStrategy(providerType, overrides, tokenFilePath);
617
691
  }
692
+ function resolveCamoCommand() {
693
+ const configured = String(process.env.ROUTECODEX_CAMO_CLI_PATH || process.env.RCC_CAMO_CLI_PATH || '').trim();
694
+ return configured || 'camo';
695
+ }
696
+ function isTruthyFlag(value) {
697
+ const raw = String(value || '').trim().toLowerCase();
698
+ return raw === '1' || raw === 'true' || raw === 'yes' || raw === 'on';
699
+ }
700
+ function resolveOAuthProfileId(providerType, tokenFilePath) {
701
+ const parsed = parseTokenSequenceFromPath(tokenFilePath);
702
+ const alias = String(parsed?.alias || 'default').trim().toLowerCase();
703
+ const normalizedAlias = alias.replace(/[^a-z0-9._-]+/gi, '-');
704
+ const normalizedProvider = String(providerType || '').trim().toLowerCase();
705
+ const providerFamily = normalizedProvider === 'gemini-cli' || normalizedProvider === 'antigravity'
706
+ ? 'gemini'
707
+ : normalizedProvider;
708
+ const base = providerFamily ? `${providerFamily}.${normalizedAlias || 'default'}` : (normalizedAlias || 'default');
709
+ const profile = `rc-${base}`;
710
+ return profile.length > 64 ? profile.slice(0, 64) : profile;
711
+ }
712
+ function closeOAuthAuthResources(providerType, tokenFilePath) {
713
+ const profileId = resolveOAuthProfileId(providerType, tokenFilePath);
714
+ try {
715
+ const result = spawnSync(resolveCamoCommand(), ['stop', profileId], {
716
+ stdio: 'ignore',
717
+ env: process.env
718
+ });
719
+ if (result.status === 0) {
720
+ logOAuthDebug(`[OAuth] auth cleanup: stopped camo profile=${profileId}`);
721
+ }
722
+ else {
723
+ logOAuthDebug(`[OAuth] auth cleanup: camo stop profile=${profileId} status=${result.status ?? 'n/a'}`);
724
+ }
725
+ }
726
+ catch (error) {
727
+ logOAuthDebug(`[OAuth] auth cleanup failed profile=${profileId}: ${error instanceof Error ? error.message : String(error)}`);
728
+ }
729
+ }
730
+ function shouldAutoCloseOAuthBrowserSession() {
731
+ const raw = String(process.env.ROUTECODEX_OAUTH_AUTO_CLOSE_BROWSER ??
732
+ process.env.RCC_OAUTH_AUTO_CLOSE_BROWSER ??
733
+ '').trim().toLowerCase();
734
+ if (!raw) {
735
+ // Default: keep browser session alive and rely on camo idle-timeout cleanup.
736
+ return false;
737
+ }
738
+ return raw === '1' || raw === 'true' || raw === 'yes' || raw === 'on';
739
+ }
740
+ function readInteractiveOAuthLock() {
741
+ try {
742
+ if (!fsSync.existsSync(OAUTH_INTERACTIVE_LOCK_FILE)) {
743
+ return null;
744
+ }
745
+ const raw = fsSync.readFileSync(OAUTH_INTERACTIVE_LOCK_FILE, 'utf8');
746
+ const parsed = JSON.parse(raw);
747
+ if (!parsed || typeof parsed !== 'object') {
748
+ return null;
749
+ }
750
+ const node = parsed;
751
+ if (typeof node.pid !== 'number' || typeof node.tokenFile !== 'string' || typeof node.providerType !== 'string') {
752
+ return null;
753
+ }
754
+ return {
755
+ pid: node.pid,
756
+ tokenFile: node.tokenFile,
757
+ providerType: node.providerType,
758
+ startedAt: typeof node.startedAt === 'number' ? node.startedAt : Date.now(),
759
+ callbackPort: typeof node.callbackPort === 'number' ? node.callbackPort : undefined
760
+ };
761
+ }
762
+ catch {
763
+ return null;
764
+ }
765
+ }
766
+ function isSameInteractiveOAuthLock(left, right) {
767
+ return (left.pid === right.pid &&
768
+ left.providerType === right.providerType &&
769
+ path.resolve(left.tokenFile) === path.resolve(right.tokenFile));
770
+ }
771
+ function isProcessAlive(pid) {
772
+ if (!Number.isFinite(pid) || pid <= 0) {
773
+ return false;
774
+ }
775
+ try {
776
+ process.kill(pid, 0);
777
+ return true;
778
+ }
779
+ catch {
780
+ return false;
781
+ }
782
+ }
783
+ async function forceReclaimInteractiveOAuthLock(lock) {
784
+ try {
785
+ const existing = readInteractiveOAuthLock();
786
+ if (!existing || !isSameInteractiveOAuthLock(existing, lock)) {
787
+ return false;
788
+ }
789
+ await fs.unlink(OAUTH_INTERACTIVE_LOCK_FILE);
790
+ logOAuthDebug(`[OAuth] interactive lock reclaimed pid=${lock.pid} token=${lock.tokenFile} provider=${lock.providerType}`);
791
+ return true;
792
+ }
793
+ catch {
794
+ return false;
795
+ }
796
+ }
797
+ async function notifyOAuthLockCancel(lock) {
798
+ if (!lock.callbackPort || !Number.isFinite(lock.callbackPort) || lock.callbackPort <= 0) {
799
+ return;
800
+ }
801
+ const url = `http://127.0.0.1:${lock.callbackPort}/oauth2callback?error=cancelled_by_new_auth`;
802
+ try {
803
+ await fetch(url, { method: 'GET' });
804
+ logOAuthDebug(`[OAuth] interactive lock cancel signal sent port=${lock.callbackPort}`);
805
+ }
806
+ catch (error) {
807
+ logOAuthDebug(`[OAuth] interactive lock cancel signal failed port=${lock.callbackPort}: ${error instanceof Error ? error.message : String(error)}`);
808
+ }
809
+ }
810
+ async function acquireInteractiveOAuthLock(providerType, tokenFilePath) {
811
+ await fs.mkdir(path.dirname(OAUTH_INTERACTIVE_LOCK_FILE), { recursive: true });
812
+ const current = {
813
+ pid: process.pid,
814
+ providerType,
815
+ tokenFile: path.resolve(tokenFilePath),
816
+ startedAt: Date.now()
817
+ };
818
+ const maxAttempts = 20;
819
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
820
+ try {
821
+ await fs.writeFile(OAUTH_INTERACTIVE_LOCK_FILE, `${JSON.stringify(current, null, 2)}\n`, { flag: 'wx' });
822
+ process.env.ROUTECODEX_OAUTH_INTERACTIVE_LOCK_FILE = OAUTH_INTERACTIVE_LOCK_FILE;
823
+ return () => {
824
+ try {
825
+ const lock = readInteractiveOAuthLock();
826
+ if (lock && lock.pid === process.pid && path.resolve(lock.tokenFile) === current.tokenFile) {
827
+ fsSync.unlinkSync(OAUTH_INTERACTIVE_LOCK_FILE);
828
+ }
829
+ }
830
+ catch {
831
+ // ignore release errors
832
+ }
833
+ finally {
834
+ if (process.env.ROUTECODEX_OAUTH_INTERACTIVE_LOCK_FILE === OAUTH_INTERACTIVE_LOCK_FILE) {
835
+ delete process.env.ROUTECODEX_OAUTH_INTERACTIVE_LOCK_FILE;
836
+ }
837
+ }
838
+ };
839
+ }
840
+ catch (error) {
841
+ const code = error?.code || '';
842
+ if (code !== 'EEXIST') {
843
+ throw error;
844
+ }
845
+ const existing = readInteractiveOAuthLock();
846
+ if (!existing) {
847
+ try {
848
+ await fs.unlink(OAUTH_INTERACTIVE_LOCK_FILE);
849
+ }
850
+ catch {
851
+ // ignore
852
+ }
853
+ continue;
854
+ }
855
+ if (!isProcessAlive(existing.pid)) {
856
+ try {
857
+ await fs.unlink(OAUTH_INTERACTIVE_LOCK_FILE);
858
+ }
859
+ catch {
860
+ // ignore
861
+ }
862
+ continue;
863
+ }
864
+ const sameToken = path.resolve(existing.tokenFile) === current.tokenFile;
865
+ if (sameToken) {
866
+ await notifyOAuthLockCancel(existing);
867
+ await new Promise((resolve) => setTimeout(resolve, 300));
868
+ const afterCancel = readInteractiveOAuthLock();
869
+ const stuckOnSameLock = !!afterCancel && isSameInteractiveOAuthLock(afterCancel, existing);
870
+ if (stuckOnSameLock) {
871
+ const lockAgeMs = Math.max(0, Date.now() - (afterCancel.startedAt || Date.now()));
872
+ const shouldForceReclaim = attempt >= 3 || lockAgeMs >= 15_000;
873
+ if (shouldForceReclaim) {
874
+ await forceReclaimInteractiveOAuthLock(afterCancel);
875
+ }
876
+ }
877
+ continue;
878
+ }
879
+ throw new Error(`Interactive OAuth is already running for token=${existing.tokenFile}. Concurrent auth is disabled.`);
880
+ }
881
+ }
882
+ throw new Error('Failed to acquire interactive OAuth lock after multiple attempts');
883
+ }
884
+ function readIflowAutoFailureState() {
885
+ try {
886
+ if (!fsSync.existsSync(IFLOW_AUTO_FAILURE_FILE)) {
887
+ return {};
888
+ }
889
+ const raw = fsSync.readFileSync(IFLOW_AUTO_FAILURE_FILE, 'utf8');
890
+ const parsed = JSON.parse(raw);
891
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
892
+ return {};
893
+ }
894
+ return parsed;
895
+ }
896
+ catch {
897
+ return {};
898
+ }
899
+ }
900
+ function writeIflowAutoFailureState(state) {
901
+ try {
902
+ fsSync.mkdirSync(path.dirname(IFLOW_AUTO_FAILURE_FILE), { recursive: true });
903
+ fsSync.writeFileSync(IFLOW_AUTO_FAILURE_FILE, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
904
+ }
905
+ catch {
906
+ // ignore persistence failures
907
+ }
908
+ }
909
+ function resolveIflowFailureKey(tokenFilePath) {
910
+ return path.resolve(tokenFilePath);
911
+ }
912
+ function clearIflowAutoFailureState(tokenFilePath) {
913
+ const state = readIflowAutoFailureState();
914
+ const key = resolveIflowFailureKey(tokenFilePath);
915
+ if (!state[key]) {
916
+ return;
917
+ }
918
+ delete state[key];
919
+ writeIflowAutoFailureState(state);
920
+ }
921
+ function markIflowAutoFailureState(tokenFilePath, maxAttempts, errorText) {
922
+ const state = readIflowAutoFailureState();
923
+ const key = resolveIflowFailureKey(tokenFilePath);
924
+ const previous = state[key];
925
+ const nextCount = (previous?.count || 0) + 1;
926
+ const record = {
927
+ count: nextCount,
928
+ manualRequired: nextCount >= maxAttempts,
929
+ updatedAt: Date.now(),
930
+ lastError: errorText
931
+ };
932
+ state[key] = record;
933
+ writeIflowAutoFailureState(state);
934
+ return record;
935
+ }
936
+ function getIflowAutoFailureState(tokenFilePath) {
937
+ const state = readIflowAutoFailureState();
938
+ const key = resolveIflowFailureKey(tokenFilePath);
939
+ return state[key] || null;
940
+ }
618
941
  async function runInteractiveAuthorizationFlow(providerType, overrides, tokenFilePath, openBrowser, forceTokenReset, forceReauth) {
619
942
  const execute = async () => {
620
943
  let backupFile = null;
@@ -633,6 +956,10 @@ async function runInteractiveAuthorizationFlow(providerType, overrides, tokenFil
633
956
  await finalizeTokenWrite(providerType, strategy, tokenFilePath, authed, 'acquired');
634
957
  }
635
958
  await discardBackupFile(backupFile);
959
+ if (openBrowser && shouldAutoCloseOAuthBrowserSession()) {
960
+ // Optional: close only after token is fully written; never close browser on failed auth.
961
+ closeOAuthAuthResources(providerType, tokenFilePath);
962
+ }
636
963
  }
637
964
  catch (error) {
638
965
  await restoreTokenFileFromBackup(backupFile, tokenFilePath);
@@ -650,10 +977,12 @@ async function runInteractiveAuthorizationFlow(providerType, overrides, tokenFil
650
977
  })
651
978
  .then(async () => {
652
979
  logOAuthDebug(`[OAuth] interactive queue enter ${label}`);
980
+ const releaseLock = await acquireInteractiveOAuthLock(providerType, tokenFilePath);
653
981
  try {
654
982
  await execute();
655
983
  }
656
984
  finally {
985
+ releaseLock();
657
986
  logOAuthDebug(`[OAuth] interactive queue leave ${label}`);
658
987
  }
659
988
  });
@@ -662,6 +991,12 @@ async function runInteractiveAuthorizationFlow(providerType, overrides, tokenFil
662
991
  }
663
992
  async function runIflowAuthorizationSequence(providerType, overrides, tokenFilePath, forceReauth) {
664
993
  const authCodeOverrides = { ...overrides, flowType: OAuthFlowType.AUTHORIZATION_CODE };
994
+ const autoMode = String(process.env.ROUTECODEX_CAMOUFOX_AUTO_MODE || '').trim().toLowerCase();
995
+ if (autoMode === 'iflow') {
996
+ // Auto mode should stay single-path to keep retry lifecycle deterministic.
997
+ await executeAuthFlow(providerType, authCodeOverrides, tokenFilePath, forceReauth);
998
+ return;
999
+ }
665
1000
  try {
666
1001
  await executeAuthFlow(providerType, authCodeOverrides, tokenFilePath, forceReauth);
667
1002
  return;
@@ -673,9 +1008,58 @@ async function runIflowAuthorizationSequence(providerType, overrides, tokenFileP
673
1008
  await executeAuthFlow(providerType, deviceOverrides, tokenFilePath, forceReauth);
674
1009
  }
675
1010
  async function executeAuthFlow(providerType, overrides, tokenFilePath, forceReauth) {
676
- const strategy = createStrategy(providerType, overrides, tokenFilePath);
677
- const authed = await strategy.authenticate?.({ openBrowser: true, forceReauthorize: forceReauth });
678
- await finalizeTokenWrite(providerType, strategy, tokenFilePath, authed, overrides.flowType ? `acquired (${String(overrides.flowType)})` : 'acquired');
1011
+ const runOnce = async () => {
1012
+ const strategy = createStrategy(providerType, overrides, tokenFilePath);
1013
+ const authed = await strategy.authenticate?.({ openBrowser: true, forceReauthorize: forceReauth });
1014
+ await finalizeTokenWrite(providerType, strategy, tokenFilePath, authed, overrides.flowType ? `acquired (${String(overrides.flowType)})` : 'acquired');
1015
+ };
1016
+ const autoMode = String(process.env.ROUTECODEX_CAMOUFOX_AUTO_MODE || '').trim().toLowerCase();
1017
+ const iflowAutoEnabled = providerType === 'iflow' && autoMode === 'iflow';
1018
+ if (!iflowAutoEnabled) {
1019
+ await runOnce();
1020
+ if (providerType === 'iflow') {
1021
+ clearIflowAutoFailureState(tokenFilePath);
1022
+ }
1023
+ return;
1024
+ }
1025
+ const headfulMode = isTruthyFlag(process.env.ROUTECODEX_CAMOUFOX_DEV_MODE);
1026
+ const maxAutoAttemptsRaw = Number.parseInt(String(process.env.ROUTECODEX_IFLOW_AUTO_MAX_ATTEMPTS || '').trim(), 10);
1027
+ const maxAutoAttempts = Number.isFinite(maxAutoAttemptsRaw) && maxAutoAttemptsRaw > 0 ? maxAutoAttemptsRaw : 3;
1028
+ const retryDelayRaw = Number.parseInt(String(process.env.ROUTECODEX_IFLOW_AUTO_RETRY_DELAY_MS || '').trim(), 10);
1029
+ const retryDelayMs = Number.isFinite(retryDelayRaw) && retryDelayRaw >= 0 ? retryDelayRaw : 1000;
1030
+ // Headful run is considered manual trigger; successful manual run clears auto failure gate.
1031
+ if (headfulMode) {
1032
+ await runOnce();
1033
+ clearIflowAutoFailureState(tokenFilePath);
1034
+ return;
1035
+ }
1036
+ const existingFailure = getIflowAutoFailureState(tokenFilePath);
1037
+ if (existingFailure?.manualRequired) {
1038
+ throw new Error(`[OAuth] iflow auto auth is disabled for token=${tokenFilePath} after ${existingFailure.count} failures. Manual trigger required.`);
1039
+ }
1040
+ let lastError = null;
1041
+ for (let attempt = 1; attempt <= maxAutoAttempts; attempt += 1) {
1042
+ try {
1043
+ await runOnce();
1044
+ clearIflowAutoFailureState(tokenFilePath);
1045
+ return;
1046
+ }
1047
+ catch (error) {
1048
+ lastError = error;
1049
+ const msg = error instanceof Error ? error.message : String(error || '');
1050
+ const record = markIflowAutoFailureState(tokenFilePath, maxAutoAttempts, msg);
1051
+ logOAuthDebug(`[OAuth] iflow auto auth attempt ${attempt}/${maxAutoAttempts} failed: ${msg} ` +
1052
+ `(failureCount=${record.count} manualRequired=${record.manualRequired ? '1' : '0'})`);
1053
+ if (attempt < maxAutoAttempts) {
1054
+ await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
1055
+ }
1056
+ }
1057
+ }
1058
+ const finalRecord = getIflowAutoFailureState(tokenFilePath);
1059
+ if (finalRecord?.manualRequired) {
1060
+ throw new Error(`[OAuth] iflow auto auth failed ${finalRecord.count} times; manual trigger is required and auto retries are suspended.`);
1061
+ }
1062
+ throw (lastError instanceof Error ? lastError : new Error(String(lastError || 'iflow auto auth failed')));
679
1063
  }
680
1064
  export async function ensureValidOAuthToken(providerType, auth, opts = {}) {
681
1065
  if (!isOAuthConfig(auth)) {
@@ -714,9 +1098,6 @@ export async function ensureValidOAuthToken(providerType, auth, opts = {}) {
714
1098
  logOAuthSetup(providerType, defaults, overrides, endpoints, client, tokenFilePath, openBrowser, forceReauth);
715
1099
  const strategy = createStrategy(providerType, overrides, tokenFilePath);
716
1100
  let token = await readTokenFromFile(tokenFilePath);
717
- if (providerType === 'iflow') {
718
- token = await maybeAdoptIflowExternalToken(strategy, tokenFilePath, token);
719
- }
720
1101
  const hadExistingTokenFile = token !== null;
721
1102
  // Qwen: ensure api_key is present even when access_token is still valid.
722
1103
  // Qwen OpenAI-compatible endpoints may require api_key (not access_token) for business requests.
@@ -787,13 +1168,27 @@ export async function ensureValidOAuthToken(providerType, auth, opts = {}) {
787
1168
  return;
788
1169
  }
789
1170
  catch (error) {
1171
+ const message = error instanceof Error ? error.message : String(error || '');
1172
+ applyRefreshFailureBackoff(cacheKey, providerType, message);
1173
+ if (providerType === 'iflow' && shouldClearIflowTokenOnRefreshFailure(message)) {
1174
+ // Only clear token file for permanent refresh-credential failures.
1175
+ await clearTokenFile(tokenFilePath);
1176
+ }
790
1177
  if (!opts.forceReacquireIfRefreshFails) {
791
1178
  throw error;
792
1179
  }
1180
+ logOAuthDebug(`[OAuth] refresh failed (${providerType}): ${message}`);
793
1181
  logOAuthDebug('[OAuth] refresh failed, attempting interactive authorization...');
794
1182
  }
795
1183
  }
796
1184
  try {
1185
+ const flowTypeRaw = String(overrides.flowType || defaults.flowType || '').trim().toLowerCase();
1186
+ const authorizationCodeFlow = flowTypeRaw === String(OAuthFlowType.AUTHORIZATION_CODE).trim().toLowerCase();
1187
+ if (!openBrowser && authorizationCodeFlow) {
1188
+ // Non-interactive contexts must never enter auth-code callback/manual prompts.
1189
+ // Let callers decide whether to retry in explicit interactive mode.
1190
+ throw new Error(`[OAuth] interactive authorization requires openBrowser=true for ${providerType} (flow=${flowTypeRaw || 'authorization_code'})`);
1191
+ }
797
1192
  await runInteractiveAuthorizationFlow(providerType, overrides, tokenFilePath, openBrowser, forceReauth || hadExistingTokenFile, forceReauth);
798
1193
  updateThrottle(cacheKey);
799
1194
  }
@@ -868,22 +1263,25 @@ export async function handleUpstreamInvalidOAuthToken(providerType, auth, upstre
868
1263
  }
869
1264
  return false;
870
1265
  }
871
- try {
872
- await withOAuthRepairEnv(providerType, async () => {
873
- await ensureValid(providerType, auth, {
874
- forceReacquireIfRefreshFails: false,
875
- openBrowser: false,
876
- forceReauthorize: false
1266
+ const refreshRejectedForIflow = pt === 'iflow' && isIflowRefreshEndpointRejectionMessage(lower);
1267
+ if (!refreshRejectedForIflow) {
1268
+ try {
1269
+ await withOAuthRepairEnv(providerType, async () => {
1270
+ await ensureValid(providerType, auth, {
1271
+ forceReacquireIfRefreshFails: false,
1272
+ openBrowser: false,
1273
+ forceReauthorize: false
1274
+ });
877
1275
  });
878
- });
879
- await markInteractiveOAuthRepairSuccess({
880
- providerType,
881
- tokenFile: tokenFilePath
882
- });
883
- return true;
884
- }
885
- catch {
886
- // ignore silent refresh errors; fall through to background interactive flow
1276
+ await markInteractiveOAuthRepairSuccess({
1277
+ providerType,
1278
+ tokenFile: tokenFilePath
1279
+ });
1280
+ return true;
1281
+ }
1282
+ catch {
1283
+ // ignore silent refresh errors; fall through to background interactive flow
1284
+ }
887
1285
  }
888
1286
  const interactiveOpts = {
889
1287
  forceReacquireIfRefreshFails: true,
@@ -893,7 +1291,12 @@ export async function handleUpstreamInvalidOAuthToken(providerType, auth, upstre
893
1291
  forceReauthorize: pt === 'gemini' || pt === 'gemini-cli' || pt === 'antigravity' || pt === 'iflow' || pt === 'qwen'
894
1292
  };
895
1293
  void withOAuthRepairEnv(providerType, async () => {
896
- await ensureValid(providerType, auth, interactiveOpts);
1294
+ await runInteractiveRepairWithAutoFallback({
1295
+ providerType,
1296
+ auth,
1297
+ ensureValid,
1298
+ opts: interactiveOpts
1299
+ });
897
1300
  }).catch(() => {
898
1301
  // background repair failure must never block requests
899
1302
  });
@@ -907,7 +1310,12 @@ export async function handleUpstreamInvalidOAuthToken(providerType, auth, upstre
907
1310
  forceReauthorize: pt === 'gemini' || pt === 'gemini-cli' || pt === 'antigravity' || pt === 'iflow' || pt === 'qwen'
908
1311
  };
909
1312
  await withOAuthRepairEnv(providerType, async () => {
910
- await ensureValid(providerType, auth, opts);
1313
+ await runInteractiveRepairWithAutoFallback({
1314
+ providerType,
1315
+ auth,
1316
+ ensureValid,
1317
+ opts
1318
+ });
911
1319
  });
912
1320
  await markInteractiveOAuthRepairSuccess({
913
1321
  providerType,
@@ -928,6 +1336,10 @@ export function shouldTriggerInteractiveOAuthRepair(providerType, upstreamError)
928
1336
  : String(upstreamError || '');
929
1337
  const lower = msg.toLowerCase();
930
1338
  const statusCode = extractStatusCode(upstreamError);
1339
+ if (pt === 'iflow' && (statusCode === 434 || isIflowAkBlockedMessage(lower))) {
1340
+ // iFlow 434 是账号级封禁,必须人工恢复,不走自动修复。
1341
+ return false;
1342
+ }
931
1343
  // 基本令牌失效判定:只看典型 OAuth 文案
932
1344
  let looksInvalid = /invalid[_-]?token|invalid[_-]?grant|unauthenticated|unauthorized|token has expired|access token expired/.test(lower);
933
1345
  // 对于 iflow / qwen,保留基于 401/403 的宽松判定,避免破坏既有行为。
@@ -938,6 +1350,9 @@ export function shouldTriggerInteractiveOAuthRepair(providerType, upstreamError)
938
1350
  looksInvalid = true;
939
1351
  }
940
1352
  }
1353
+ if (!looksInvalid && pt === 'iflow' && isIflowRefreshEndpointRejectionMessage(lower)) {
1354
+ looksInvalid = true;
1355
+ }
941
1356
  // 对于 gemini / gemini-cli / antigravity,排除纯服务开关类错误,
942
1357
  // 但如果明确提示缺少 project_id 或需要重新 OAuth,则视为令牌失效。
943
1358
  if (pt === 'gemini' || pt === 'gemini-cli' || pt === 'antigravity') {