@jsonstudio/rcc 0.89.1552 → 0.89.1800

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 (236) 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 +10 -15
  14. package/dist/build-info.js +2 -2
  15. package/dist/cli/commands/camoufox.d.ts +34 -0
  16. package/dist/cli/commands/camoufox.js +107 -0
  17. package/dist/cli/commands/camoufox.js.map +1 -0
  18. package/dist/cli/commands/config.js +8 -9
  19. package/dist/cli/commands/config.js.map +1 -1
  20. package/dist/cli/commands/restart.d.ts +4 -12
  21. package/dist/cli/commands/restart.js +226 -120
  22. package/dist/cli/commands/restart.js.map +1 -1
  23. package/dist/cli/commands/start.d.ts +1 -0
  24. package/dist/cli/commands/start.js +34 -6
  25. package/dist/cli/commands/start.js.map +1 -1
  26. package/dist/cli/commands/status.js +12 -6
  27. package/dist/cli/commands/status.js.map +1 -1
  28. package/dist/cli/config/init-provider-catalog.js +12 -11
  29. package/dist/cli/config/init-provider-catalog.js.map +1 -1
  30. package/dist/cli/register/camoufox-command.d.ts +20 -0
  31. package/dist/cli/register/camoufox-command.js +22 -0
  32. package/dist/cli/register/camoufox-command.js.map +1 -0
  33. package/dist/cli.js +14 -14
  34. package/dist/cli.js.map +1 -1
  35. package/dist/client/anthropic/anthropic-protocol-client.d.ts +1 -0
  36. package/dist/client/anthropic/anthropic-protocol-client.js +25 -0
  37. package/dist/client/anthropic/anthropic-protocol-client.js.map +1 -1
  38. package/dist/commands/oauth.js +185 -9
  39. package/dist/commands/oauth.js.map +1 -1
  40. package/dist/commands/token-daemon.js +12 -2
  41. package/dist/commands/token-daemon.js.map +1 -1
  42. package/dist/commands/validate.js +1 -1
  43. package/dist/commands/validate.js.map +1 -1
  44. package/dist/docs/daemon-admin-ui.html +1355 -204
  45. package/dist/index.js +119 -0
  46. package/dist/index.js.map +1 -1
  47. package/dist/manager/index.d.ts +2 -0
  48. package/dist/manager/index.js +39 -2
  49. package/dist/manager/index.js.map +1 -1
  50. package/dist/manager/modules/quota/antigravity-quota-manager.d.ts +29 -5
  51. package/dist/manager/modules/quota/antigravity-quota-manager.js +369 -113
  52. package/dist/manager/modules/quota/antigravity-quota-manager.js.map +1 -1
  53. package/dist/manager/modules/quota/provider-quota-daemon.cooldown.d.ts +7 -0
  54. package/dist/manager/modules/quota/provider-quota-daemon.cooldown.js +61 -0
  55. package/dist/manager/modules/quota/provider-quota-daemon.cooldown.js.map +1 -1
  56. package/dist/manager/modules/quota/provider-quota-daemon.d.ts +1 -0
  57. package/dist/manager/modules/quota/provider-quota-daemon.events.js +245 -1
  58. package/dist/manager/modules/quota/provider-quota-daemon.events.js.map +1 -1
  59. package/dist/manager/modules/quota/provider-quota-daemon.js +20 -13
  60. package/dist/manager/modules/quota/provider-quota-daemon.js.map +1 -1
  61. package/dist/manager/modules/quota/provider-quota-daemon.model-backoff.d.ts +1 -0
  62. package/dist/manager/modules/quota/provider-quota-daemon.model-backoff.js +8 -3
  63. package/dist/manager/modules/quota/provider-quota-daemon.model-backoff.js.map +1 -1
  64. package/dist/manager/modules/token/index.js +2 -2
  65. package/dist/manager/modules/token/index.js.map +1 -1
  66. package/dist/manager/quota/provider-quota-center.d.ts +16 -1
  67. package/dist/manager/quota/provider-quota-center.js +24 -3
  68. package/dist/manager/quota/provider-quota-center.js.map +1 -1
  69. package/dist/modules/llmswitch/bridge.d.ts +33 -1
  70. package/dist/modules/llmswitch/bridge.js +170 -2
  71. package/dist/modules/llmswitch/bridge.js.map +1 -1
  72. package/dist/modules/llmswitch/core-loader.js +64 -11
  73. package/dist/modules/llmswitch/core-loader.js.map +1 -1
  74. package/dist/modules/pipeline/utils/debug-logger.d.ts +1 -0
  75. package/dist/modules/pipeline/utils/debug-logger.js +50 -3
  76. package/dist/modules/pipeline/utils/debug-logger.js.map +1 -1
  77. package/dist/providers/auth/apikey-auth.js +15 -3
  78. package/dist/providers/auth/apikey-auth.js.map +1 -1
  79. package/dist/providers/auth/oauth-lifecycle.d.ts +13 -1
  80. package/dist/providers/auth/oauth-lifecycle.js +346 -45
  81. package/dist/providers/auth/oauth-lifecycle.js.map +1 -1
  82. package/dist/providers/auth/oauth-repair-cooldown.d.ts +21 -0
  83. package/dist/providers/auth/oauth-repair-cooldown.js +100 -0
  84. package/dist/providers/auth/oauth-repair-cooldown.js.map +1 -0
  85. package/dist/providers/auth/oauth-repair-env.d.ts +1 -0
  86. package/dist/providers/auth/oauth-repair-env.js +79 -0
  87. package/dist/providers/auth/oauth-repair-env.js.map +1 -0
  88. package/dist/providers/auth/qwen-userinfo-helper.d.ts +2 -0
  89. package/dist/providers/auth/qwen-userinfo-helper.js +72 -40
  90. package/dist/providers/auth/qwen-userinfo-helper.js.map +1 -1
  91. package/dist/providers/auth/tokenfile-auth.js +148 -17
  92. package/dist/providers/auth/tokenfile-auth.js.map +1 -1
  93. package/dist/providers/core/api/provider-types.d.ts +10 -0
  94. package/dist/providers/core/config/camoufox-launcher.d.ts +3 -0
  95. package/dist/providers/core/config/camoufox-launcher.js +190 -3
  96. package/dist/providers/core/config/camoufox-launcher.js.map +1 -1
  97. package/dist/providers/core/config/oauth-flows.js +50 -19
  98. package/dist/providers/core/config/oauth-flows.js.map +1 -1
  99. package/dist/providers/core/config/provider-oauth-configs.js +1 -1
  100. package/dist/providers/core/config/provider-oauth-configs.js.map +1 -1
  101. package/dist/providers/core/runtime/gemini-cli-http-provider.d.ts +5 -0
  102. package/dist/providers/core/runtime/gemini-cli-http-provider.js +172 -15
  103. package/dist/providers/core/runtime/gemini-cli-http-provider.js.map +1 -1
  104. package/dist/providers/core/runtime/gemini-http-provider.d.ts +11 -0
  105. package/dist/providers/core/runtime/gemini-http-provider.js +281 -3
  106. package/dist/providers/core/runtime/gemini-http-provider.js.map +1 -1
  107. package/dist/providers/core/runtime/http-request-executor.js +55 -0
  108. package/dist/providers/core/runtime/http-request-executor.js.map +1 -1
  109. package/dist/providers/core/runtime/http-transport-provider.js +10 -14
  110. package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
  111. package/dist/providers/core/runtime/provider-factory.d.ts +1 -0
  112. package/dist/providers/core/runtime/provider-factory.js +40 -2
  113. package/dist/providers/core/runtime/provider-factory.js.map +1 -1
  114. package/dist/providers/core/strategies/oauth-auth-code-flow.js +45 -2
  115. package/dist/providers/core/strategies/oauth-auth-code-flow.js.map +1 -1
  116. package/dist/providers/core/strategies/oauth-device-flow.js +13 -2
  117. package/dist/providers/core/strategies/oauth-device-flow.js.map +1 -1
  118. package/dist/providers/core/strategies/oauth-refresh-errors.d.ts +1 -0
  119. package/dist/providers/core/strategies/oauth-refresh-errors.js +26 -0
  120. package/dist/providers/core/strategies/oauth-refresh-errors.js.map +1 -0
  121. package/dist/providers/core/utils/snapshot-writer.d.ts +4 -2
  122. package/dist/providers/core/utils/snapshot-writer.js +86 -23
  123. package/dist/providers/core/utils/snapshot-writer.js.map +1 -1
  124. package/dist/scripts/camoufox/launch-auth.mjs +545 -49
  125. package/dist/server/handlers/chat-handler.js +1 -1
  126. package/dist/server/handlers/chat-handler.js.map +1 -1
  127. package/dist/server/handlers/handler-utils.d.ts +1 -0
  128. package/dist/server/handlers/handler-utils.js +231 -3
  129. package/dist/server/handlers/handler-utils.js.map +1 -1
  130. package/dist/server/handlers/messages-handler.js +1 -1
  131. package/dist/server/handlers/messages-handler.js.map +1 -1
  132. package/dist/server/handlers/responses-handler.js +17 -5
  133. package/dist/server/handlers/responses-handler.js.map +1 -1
  134. package/dist/server/handlers/sse-dispatcher.js +10 -1
  135. package/dist/server/handlers/sse-dispatcher.js.map +1 -1
  136. package/dist/server/runtime/http-server/daemon-admin/control-handler.d.ts +3 -0
  137. package/dist/server/runtime/http-server/daemon-admin/control-handler.js +389 -0
  138. package/dist/server/runtime/http-server/daemon-admin/control-handler.js.map +1 -0
  139. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +190 -5
  140. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -1
  141. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js +2 -1
  142. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js.map +1 -1
  143. package/dist/server/runtime/http-server/daemon-admin/quota-handler.js +117 -14
  144. package/dist/server/runtime/http-server/daemon-admin/quota-handler.js.map +1 -1
  145. package/dist/server/runtime/http-server/daemon-admin/routing-policy.d.ts +30 -0
  146. package/dist/server/runtime/http-server/daemon-admin/routing-policy.js +133 -0
  147. package/dist/server/runtime/http-server/daemon-admin/routing-policy.js.map +1 -0
  148. package/dist/server/runtime/http-server/daemon-admin/status-handler.js +40 -1
  149. package/dist/server/runtime/http-server/daemon-admin/status-handler.js.map +1 -1
  150. package/dist/server/runtime/http-server/daemon-admin-routes.d.ts +5 -0
  151. package/dist/server/runtime/http-server/daemon-admin-routes.js +3 -0
  152. package/dist/server/runtime/http-server/daemon-admin-routes.js.map +1 -1
  153. package/dist/server/runtime/http-server/executor-pipeline.d.ts +10 -0
  154. package/dist/server/runtime/http-server/executor-pipeline.js +6 -0
  155. package/dist/server/runtime/http-server/executor-pipeline.js.map +1 -1
  156. package/dist/server/runtime/http-server/executor-response.js +26 -0
  157. package/dist/server/runtime/http-server/executor-response.js.map +1 -1
  158. package/dist/server/runtime/http-server/hub-shadow-compare.js +41 -3
  159. package/dist/server/runtime/http-server/hub-shadow-compare.js.map +1 -1
  160. package/dist/server/runtime/http-server/index.d.ts +9 -0
  161. package/dist/server/runtime/http-server/index.js +337 -91
  162. package/dist/server/runtime/http-server/index.js.map +1 -1
  163. package/dist/server/runtime/http-server/middleware.js +27 -1
  164. package/dist/server/runtime/http-server/middleware.js.map +1 -1
  165. package/dist/server/runtime/http-server/request-executor.js +199 -29
  166. package/dist/server/runtime/http-server/request-executor.js.map +1 -1
  167. package/dist/server/runtime/http-server/routes.d.ts +1 -0
  168. package/dist/server/runtime/http-server/routes.js +36 -3
  169. package/dist/server/runtime/http-server/routes.js.map +1 -1
  170. package/dist/server/runtime/http-server/server-id.d.ts +1 -0
  171. package/dist/server/runtime/http-server/server-id.js +18 -0
  172. package/dist/server/runtime/http-server/server-id.js.map +1 -0
  173. package/dist/server/runtime/http-server/stats-manager.d.ts +2 -0
  174. package/dist/server/runtime/http-server/stats-manager.js +63 -7
  175. package/dist/server/runtime/http-server/stats-manager.js.map +1 -1
  176. package/dist/server/runtime/http-server/types.d.ts +2 -0
  177. package/dist/server/utils/stage-logger.js +54 -9
  178. package/dist/server/utils/stage-logger.js.map +1 -1
  179. package/dist/token-daemon/history-store.d.ts +8 -3
  180. package/dist/token-daemon/history-store.js +41 -20
  181. package/dist/token-daemon/history-store.js.map +1 -1
  182. package/dist/token-daemon/index.d.ts +5 -1
  183. package/dist/token-daemon/index.js +191 -11
  184. package/dist/token-daemon/index.js.map +1 -1
  185. package/dist/token-daemon/quota-auth-issue.d.ts +7 -0
  186. package/dist/token-daemon/quota-auth-issue.js +231 -0
  187. package/dist/token-daemon/quota-auth-issue.js.map +1 -0
  188. package/dist/token-daemon/server-utils.js +1 -1
  189. package/dist/token-daemon/server-utils.js.map +1 -1
  190. package/dist/token-daemon/token-daemon.d.ts +2 -0
  191. package/dist/token-daemon/token-daemon.js +177 -14
  192. package/dist/token-daemon/token-daemon.js.map +1 -1
  193. package/dist/token-portal/local-token-portal.js +6 -0
  194. package/dist/token-portal/local-token-portal.js.map +1 -1
  195. package/dist/tools/provider-update/fetch-models.js +0 -1
  196. package/dist/tools/provider-update/fetch-models.js.map +1 -1
  197. package/dist/tools/provider-update/key-probe.js +0 -1
  198. package/dist/tools/provider-update/key-probe.js.map +1 -1
  199. package/docs/ANTIGRAVITY_IDE_FORWARD_PROXY.md +61 -0
  200. package/docs/ANTIGRAVITY_THOUGHT_SIGNATURE_BOOTSTRAP_429.md +80 -0
  201. package/docs/CLOCK.md +94 -0
  202. package/docs/DAEMON_CONTROL_PLANE.md +34 -0
  203. package/docs/OAUTH.md +172 -0
  204. package/docs/PROVIDERS_BUILTIN.md +8 -5
  205. package/docs/PROVIDER_TYPES.md +6 -4
  206. package/docs/QUOTA_MANAGER_V3.md +54 -0
  207. package/docs/ROUTING_POLICY_SCHEMA.md +47 -0
  208. package/docs/ROUTING_POLICY_UI.md +11 -0
  209. package/docs/SERVERTOOL_CLOCK_DESIGN.md +56 -25
  210. package/docs/antigravity-routing-contract.md +17 -11
  211. package/docs/config-secrets.md +49 -0
  212. package/docs/daemon-admin-ui.html +1355 -204
  213. package/docs/oauth-authentication-guide.md +4 -0
  214. package/docs/oauth-iflow-implementation.md +4 -0
  215. package/docs/provider-quota-design.md +11 -0
  216. package/docs/providers/antigravity-fingerprint-ua-warmup.md +25 -0
  217. package/docs/providers/antigravity-gemini-provider-compat.md +1 -0
  218. package/docs/providers/antigravity-thought-signature.md +127 -0
  219. package/docs/providers/tabglm-claude-code-compat.md +39 -0
  220. package/docs/refactoring/host-sharedmodule-safe-migration-plan.md +164 -0
  221. package/docs/stop-message-auto.md +1 -0
  222. package/docs/token-daemon-preview.html +2 -2
  223. package/docs/token-refresh-daemon-plan.md +6 -6
  224. package/package.json +5 -5
  225. package/scripts/antigravity-ide-forward-proxy.mjs +362 -0
  226. package/scripts/backfill-apply-patch-exec-errorsamples.mjs +19 -0
  227. package/scripts/camoufox/launch-auth.mjs +545 -49
  228. package/scripts/ci/repo-sanity.mjs +2 -0
  229. package/scripts/install-global.sh +46 -0
  230. package/scripts/migrate-antigravity-session-signatures-alias.mjs +193 -0
  231. package/scripts/migrate-antigravity-session-signatures.mjs +165 -0
  232. package/scripts/responses-compare-server.mjs +1 -1
  233. package/scripts/tests/blackbox-rcc-vs-routecodex-antigravity.mjs +44 -9
  234. package/scripts/tests/ci-jest.mjs +3 -0
  235. package/scripts/verify-client-headers.mjs +33 -5
  236. package/scripts/virtual-router-dryrun.mjs +333 -0
@@ -5,12 +5,15 @@ import fsSync from 'fs';
5
5
  import path from 'path';
6
6
  import os from 'os';
7
7
  import { fetchIFlowUserInfo, mergeIFlowTokenData } from './iflow-userinfo-helper.js';
8
- import {} from './qwen-userinfo-helper.js';
8
+ import { fetchQwenUserInfo, mergeQwenTokenData } from './qwen-userinfo-helper.js';
9
9
  import { fetchGeminiCLIUserInfo, fetchGeminiCLIProjects, mergeGeminiCLITokenData, getDefaultProjectId } from './gemini-cli-userinfo-helper.js';
10
10
  import { parseTokenSequenceFromPath } from './token-scanner/index.js';
11
11
  import { logOAuthDebug } from './oauth-logger.js';
12
12
  import { fetchAntigravityProjectId } from './antigravity-userinfo-helper.js';
13
13
  import { HTTP_PROTOCOLS, LOCAL_HOSTS } from '../../constants/index.js';
14
+ import { withOAuthRepairEnv } from './oauth-repair-env.js';
15
+ import { markInteractiveOAuthRepairAttempt, shouldSkipInteractiveOAuthRepair } from './oauth-repair-cooldown.js';
16
+ import { openAuthInCamoufox } from '../core/config/camoufox-launcher.js';
14
17
  const TOKEN_REFRESH_SKEW_MS = 60_000;
15
18
  const inFlight = new Map();
16
19
  const lastRunAt = new Map();
@@ -31,7 +34,9 @@ function defaultTokenFile(providerType) {
31
34
  return path.join(home, '.iflow', 'oauth_creds.json');
32
35
  }
33
36
  if (providerType === 'qwen') {
34
- return path.join(home, '.routecodex', 'auth', 'qwen-oauth.json');
37
+ // Align with TokenFileAuthProvider + token-daemon defaults:
38
+ // keep a stable, well-known Qwen token file for alias="default".
39
+ return path.join(home, '.routecodex', 'auth', 'qwen-oauth-1-default.json');
35
40
  }
36
41
  if (isGeminiCliFamily(providerType)) {
37
42
  const file = providerType.toLowerCase() === 'antigravity'
@@ -60,7 +65,22 @@ function resolveTokenFilePath(auth, providerType) {
60
65
  const homeDir = process.env.HOME || os.homedir();
61
66
  const authDir = path.join(homeDir, '.routecodex', 'auth');
62
67
  const pattern = new RegExp(`^${providerType}-oauth-(\\d+)(?:-(.+))?\\.json$`, 'i');
68
+ const pt = providerType.toLowerCase();
69
+ // Qwen: keep a stable "default" file name whenever possible.
70
+ if (pt === 'qwen' && alias === 'default') {
71
+ const pinned = path.join(authDir, 'qwen-oauth-1-default.json');
72
+ try {
73
+ if (fsSync.existsSync(pinned)) {
74
+ auth.tokenFile = pinned;
75
+ return pinned;
76
+ }
77
+ }
78
+ catch {
79
+ // ignore and fall back to scanning
80
+ }
81
+ }
63
82
  let existingPath = null;
83
+ let bestSeqForAlias = 0;
64
84
  let maxSeq = 0;
65
85
  try {
66
86
  const entries = fsSync.readdirSync(authDir);
@@ -74,7 +94,8 @@ function resolveTokenFilePath(auth, providerType) {
74
94
  continue;
75
95
  }
76
96
  const entryAlias = (match[2] || 'default');
77
- if (entryAlias === alias && !existingPath) {
97
+ if (entryAlias === alias && seq >= bestSeqForAlias) {
98
+ bestSeqForAlias = seq;
78
99
  existingPath = path.join(authDir, entry);
79
100
  }
80
101
  if (seq > maxSeq) {
@@ -89,7 +110,10 @@ function resolveTokenFilePath(auth, providerType) {
89
110
  auth.tokenFile = existingPath;
90
111
  return existingPath;
91
112
  }
92
- const nextSeq = maxSeq + 1;
113
+ // When we don't have any existing token for this alias:
114
+ // - Qwen default alias should always map to seq=1 for stability.
115
+ // - Otherwise, allocate next seq to avoid collisions.
116
+ const nextSeq = (pt === 'qwen' && alias === 'default') ? 1 : (maxSeq + 1);
93
117
  const fileName = `${providerType}-oauth-${nextSeq}-${alias}.json`;
94
118
  const fullPath = path.join(authDir, fileName);
95
119
  auth.tokenFile = fullPath;
@@ -102,6 +126,139 @@ function shouldThrottle(k, ms = 60_000) {
102
126
  function updateThrottle(k) {
103
127
  lastRunAt.set(k, Date.now());
104
128
  }
129
+ function extractStatusCode(upstreamError) {
130
+ if (!upstreamError || typeof upstreamError !== 'object') {
131
+ return undefined;
132
+ }
133
+ const anyErr = upstreamError;
134
+ const direct = anyErr.statusCode;
135
+ if (typeof direct === 'number' && Number.isFinite(direct)) {
136
+ return direct;
137
+ }
138
+ const status = anyErr.status;
139
+ if (typeof status === 'number' && Number.isFinite(status)) {
140
+ return status;
141
+ }
142
+ const response = anyErr.response;
143
+ if (response && typeof response === 'object') {
144
+ const respStatus = response.status;
145
+ if (typeof respStatus === 'number' && Number.isFinite(respStatus)) {
146
+ return respStatus;
147
+ }
148
+ const respStatusCode = response.statusCode;
149
+ if (typeof respStatusCode === 'number' && Number.isFinite(respStatusCode)) {
150
+ return respStatusCode;
151
+ }
152
+ }
153
+ return undefined;
154
+ }
155
+ function isGoogleAccountVerificationRequiredMessage(lower) {
156
+ if (!lower) {
157
+ return false;
158
+ }
159
+ return (lower.includes('verify your account') ||
160
+ lower.includes('validation_required') ||
161
+ lower.includes('validation required') ||
162
+ lower.includes('validation_url') ||
163
+ lower.includes('validation url') ||
164
+ lower.includes('accounts.google.com/signin/continue') ||
165
+ lower.includes('support.google.com/accounts?p=al_alert'));
166
+ }
167
+ function extractGoogleAccountVerificationUrl(message) {
168
+ const msg = typeof message === 'string' ? message : '';
169
+ if (!msg) {
170
+ return null;
171
+ }
172
+ const normalized = msg
173
+ .replace(/\\\//g, '/')
174
+ .replace(/\\u0026/gi, '&')
175
+ .replace(/\\u003d/gi, '=')
176
+ .replace(/\\x26/gi, '&')
177
+ .replace(/\\x3d/gi, '=');
178
+ const patterns = [
179
+ /https:\/\/accounts\.google\.com\/signin\/continue[^\s"'\\<>)]*/i,
180
+ /https:\/\/accounts\.google\.com\/[^\s"'\\<>)]*/i,
181
+ /https:\/\/support\.google\.com\/accounts\?p=al_alert[^\s"'\\<>)]*/i
182
+ ];
183
+ for (const re of patterns) {
184
+ const m = normalized.match(re);
185
+ if (m && m[0]) {
186
+ const url = String(m[0]).trim().replace(/[\\"']+$/g, '').replace(/[),.]+$/g, '');
187
+ if (url) {
188
+ return url;
189
+ }
190
+ }
191
+ }
192
+ return null;
193
+ }
194
+ function resolveCamoufoxAliasForAuth(providerType, auth) {
195
+ const raw = typeof auth.tokenFile === 'string' ? auth.tokenFile.trim() : '';
196
+ if (raw && !raw.includes('/') && !raw.includes('\\') && !raw.endsWith('.json')) {
197
+ return raw;
198
+ }
199
+ const base = raw ? path.basename(raw) : '';
200
+ const pt = String(providerType || '').trim().toLowerCase();
201
+ if (base && pt) {
202
+ const re = new RegExp(`^${pt}-oauth-\\d+(?:-(.+))?\\.json$`, 'i');
203
+ const m = base.match(re);
204
+ const alias = m && m[1] ? String(m[1]).trim() : '';
205
+ if (alias) {
206
+ return alias;
207
+ }
208
+ }
209
+ return 'default';
210
+ }
211
+ async function openGoogleAccountVerificationInCamoufox(args) {
212
+ const providerType = args.providerType;
213
+ const url = args.url;
214
+ if (!url) {
215
+ return;
216
+ }
217
+ const alias = resolveCamoufoxAliasForAuth(providerType, args.auth);
218
+ const prevBrowser = process.env.ROUTECODEX_OAUTH_BROWSER;
219
+ const prevAutoMode = process.env.ROUTECODEX_CAMOUFOX_AUTO_MODE;
220
+ const prevDevMode = process.env.ROUTECODEX_CAMOUFOX_DEV_MODE;
221
+ const prevOpenOnly = process.env.ROUTECODEX_CAMOUFOX_OPEN_ONLY;
222
+ process.env.ROUTECODEX_OAUTH_BROWSER = 'camoufox';
223
+ delete process.env.ROUTECODEX_CAMOUFOX_AUTO_MODE;
224
+ process.env.ROUTECODEX_CAMOUFOX_DEV_MODE = '1';
225
+ process.env.ROUTECODEX_CAMOUFOX_OPEN_ONLY = '1';
226
+ try {
227
+ const ok = await openAuthInCamoufox({ url, provider: providerType, alias });
228
+ if (ok) {
229
+ console.warn(`[OAuth] Google account verification opened in Camoufox (provider=${providerType} alias=${alias}).`);
230
+ }
231
+ }
232
+ catch {
233
+ // best-effort; never block requests
234
+ }
235
+ finally {
236
+ if (prevBrowser === undefined) {
237
+ delete process.env.ROUTECODEX_OAUTH_BROWSER;
238
+ }
239
+ else {
240
+ process.env.ROUTECODEX_OAUTH_BROWSER = prevBrowser;
241
+ }
242
+ if (prevAutoMode === undefined) {
243
+ delete process.env.ROUTECODEX_CAMOUFOX_AUTO_MODE;
244
+ }
245
+ else {
246
+ process.env.ROUTECODEX_CAMOUFOX_AUTO_MODE = prevAutoMode;
247
+ }
248
+ if (prevDevMode === undefined) {
249
+ delete process.env.ROUTECODEX_CAMOUFOX_DEV_MODE;
250
+ }
251
+ else {
252
+ process.env.ROUTECODEX_CAMOUFOX_DEV_MODE = prevDevMode;
253
+ }
254
+ if (prevOpenOnly === undefined) {
255
+ delete process.env.ROUTECODEX_CAMOUFOX_OPEN_ONLY;
256
+ }
257
+ else {
258
+ process.env.ROUTECODEX_CAMOUFOX_OPEN_ONLY = prevOpenOnly;
259
+ }
260
+ }
261
+ }
105
262
  function isOAuthConfig(auth) {
106
263
  return Boolean(auth && typeof auth.type === 'string' && auth.type.toLowerCase().includes('oauth'));
107
264
  }
@@ -249,12 +406,30 @@ function extractAccessToken(token) {
249
406
  }
250
407
  return undefined;
251
408
  }
409
+ function extractApiKey(token) {
410
+ if (!token) {
411
+ return undefined;
412
+ }
413
+ const candidate = token.apiKey ?? token.api_key;
414
+ return hasNonEmptyString(candidate) ? String(candidate) : undefined;
415
+ }
252
416
  function hasApiKeyField(token) {
253
417
  if (!token) {
254
418
  return false;
255
419
  }
256
- const candidate = token.apiKey ?? token.api_key;
257
- return hasNonEmptyString(candidate);
420
+ return hasNonEmptyString(token.apiKey ?? token.api_key);
421
+ }
422
+ /**
423
+ * Qwen: api_key 可能被降级为 access_token(userInfo 404 时的兼容写法),这种情况不应被视为“稳定 API Key”。
424
+ * 只有当 api_key 存在且与 access_token 不同(或缺失 access_token)时,才认为可以长期复用并跳过刷新。
425
+ */
426
+ function hasStableQwenApiKey(token) {
427
+ const apiKey = extractApiKey(token);
428
+ if (!apiKey) {
429
+ return false;
430
+ }
431
+ const access = extractAccessToken(token);
432
+ return !access || apiKey !== access;
258
433
  }
259
434
  function hasAccessToken(token) {
260
435
  return hasNonEmptyString(token?.access_token) || hasNonEmptyString(token?.AccessToken);
@@ -448,7 +623,8 @@ function evaluateTokenState(token, providerType) {
448
623
  validAccess = hasApiKey || (!isExpiredOrNear && hasAccess);
449
624
  }
450
625
  else if (pt === 'qwen') {
451
- validAccess = (hasApiKey || hasAccess) && !isExpiredOrNear;
626
+ // Qwen: 当获取到稳定 api_key 后,可跳过 refresh/reauth;否则仍依赖 access_token 的有效期。
627
+ validAccess = hasStableQwenApiKey(token) || (!isExpiredOrNear && (hasAccess || hasApiKey));
452
628
  }
453
629
  else {
454
630
  validAccess = (hasApiKey || hasAccess) && !isExpiredOrNear;
@@ -669,6 +845,37 @@ async function finalizeTokenWrite(providerType, strategy, tokenFilePath, tokenDa
669
845
  logOAuthDebug(`[OAuth] Token ${reason} saved: ${tokenFilePath}`);
670
846
  }
671
847
  async function maybeEnrichToken(providerType, tokenData, tokenFilePath) {
848
+ if (providerType === 'qwen') {
849
+ const sanitized = sanitizeToken(tokenData) ?? tokenData;
850
+ if (hasStableQwenApiKey(sanitized)) {
851
+ return tokenData;
852
+ }
853
+ const accessToken = extractAccessToken(sanitized);
854
+ if (!accessToken) {
855
+ logOAuthDebug('[OAuth] Qwen: no access_token found in auth result, skipping API Key fetch');
856
+ return tokenData;
857
+ }
858
+ try {
859
+ const userInfo = await fetchQwenUserInfo(accessToken);
860
+ if (userInfo.apiKey) {
861
+ logOAuthDebug(`[OAuth] Qwen: successfully fetched API Key${userInfo.email ? ` for ${userInfo.email}` : ''}`);
862
+ }
863
+ else {
864
+ logOAuthDebug('[OAuth] Qwen: user info fetched but apiKey missing; continuing with access_token only');
865
+ }
866
+ return mergeQwenTokenData(tokenData, userInfo);
867
+ }
868
+ catch (error) {
869
+ const msg = error instanceof Error ? error.message : String(error);
870
+ // If userInfo endpoint is unavailable (404), treat access_token as api_key to avoid repeated lookups.
871
+ if (/\bHTTP\s+404\b/i.test(msg) || /\bnot\s+found\b/i.test(msg)) {
872
+ logOAuthDebug('[OAuth] Qwen: userInfo endpoint unavailable (404); using access_token as api_key fallback');
873
+ return mergeQwenTokenData(tokenData, { apiKey: accessToken });
874
+ }
875
+ logOAuthDebug(`[OAuth] Qwen: failed to fetch user info - ${msg}`);
876
+ return tokenData;
877
+ }
878
+ }
672
879
  if (providerType === 'iflow') {
673
880
  const accessToken = extractAccessToken(sanitizeToken(tokenData) ?? null);
674
881
  if (!accessToken) {
@@ -847,9 +1054,14 @@ export async function ensureValidOAuthToken(providerType, auth, opts = {}) {
847
1054
  await inFlight.get(cacheKey);
848
1055
  return;
849
1056
  }
1057
+ // Only treat "open browser" as explicit user intent when caller passed it explicitly.
1058
+ // This prevents background flows (daemon/provider init) from bypassing noRefresh due to env defaults.
1059
+ const openBrowserRequested = opts.openBrowser === true;
850
1060
  // 当 opts.forceReauthorize 显式为 true 时,跳过节流检查,
851
1061
  // 确保来自上游 401/406 等认证错误的修复请求不会被初始化阶段的调用吞掉。
852
- if (!opts.forceReauthorize && shouldThrottle(cacheKey)) {
1062
+ // Explicit user-triggered OAuth (openBrowser=true) must also bypass throttle,
1063
+ // otherwise repeated "Authorize" clicks in WebUI can become a silent no-op.
1064
+ if (!opts.forceReauthorize && !openBrowserRequested && shouldThrottle(cacheKey)) {
853
1065
  return;
854
1066
  }
855
1067
  const aliasInfo = parseTokenSequenceFromPath(tokenFilePath);
@@ -869,6 +1081,22 @@ export async function ensureValidOAuthToken(providerType, auth, opts = {}) {
869
1081
  const strategy = createStrategy(providerType, overrides, tokenFilePath);
870
1082
  let token = await readTokenFromFile(tokenFilePath);
871
1083
  const hadExistingTokenFile = token !== null;
1084
+ // Qwen: ensure api_key is present even when access_token is still valid.
1085
+ // Qwen OpenAI-compatible endpoints may require api_key (not access_token) for business requests.
1086
+ if (providerType === 'qwen' && token && !hasStableQwenApiKey(token)) {
1087
+ try {
1088
+ const enriched = await maybeEnrichToken(providerType, token);
1089
+ if (enriched && typeof strategy.saveToken === 'function') {
1090
+ const prepared = await prepareTokenForStorage(providerType, tokenFilePath, enriched);
1091
+ await strategy.saveToken(prepared);
1092
+ token = sanitizeToken(enriched) ?? enriched;
1093
+ }
1094
+ }
1095
+ catch (error) {
1096
+ const msg = error instanceof Error ? error.message : String(error);
1097
+ console.error(`[OAuth] Qwen: failed to enrich existing token with api_key - ${msg}`);
1098
+ }
1099
+ }
872
1100
  // Gemini CLI family: if existing token lacks project metadata, try to enrich it without
873
1101
  // forcing a full OAuth flow. Use current access_token to fetch userinfo/projects and write back.
874
1102
  if (isGeminiCliFamily(providerType) && token) {
@@ -900,7 +1128,7 @@ export async function ensureValidOAuthToken(providerType, auth, opts = {}) {
900
1128
  logTokenSnapshot(providerType, token, endpoints);
901
1129
  const tokenState = evaluateTokenState(token, providerType);
902
1130
  const noRefresh = hasNoRefreshFlag(token);
903
- if (noRefresh) {
1131
+ if (noRefresh && !forceReauth && !openBrowserRequested) {
904
1132
  logOAuthDebug(`[OAuth] norefresh flag set for provider=${providerType} tokenFile=${tokenFilePath} - skip auto-refresh and re-authorization.`);
905
1133
  updateThrottle(cacheKey);
906
1134
  return;
@@ -953,48 +1181,80 @@ export async function ensureValidOAuthToken(providerType, auth, opts = {}) {
953
1181
  inFlight.delete(cacheKey);
954
1182
  }
955
1183
  }
956
- export async function handleUpstreamInvalidOAuthToken(providerType, auth, upstreamError) {
1184
+ export async function handleUpstreamInvalidOAuthToken(providerType, auth, upstreamError, options) {
957
1185
  const pt = providerType.toLowerCase();
1186
+ const allowBlocking = options?.allowBlocking !== false;
1187
+ const ensureValid = options?.ensureValidOAuthToken ?? ensureValidOAuthToken;
958
1188
  try {
959
- const msg = upstreamError instanceof Error ? upstreamError.message : String(upstreamError || '');
1189
+ if (!shouldTriggerInteractiveOAuthRepair(providerType, upstreamError)) {
1190
+ return false;
1191
+ }
1192
+ const msg = upstreamError instanceof Error
1193
+ ? upstreamError.message
1194
+ : upstreamError && typeof upstreamError === 'object' && typeof upstreamError.message === 'string'
1195
+ ? String(upstreamError.message)
1196
+ : String(upstreamError || '');
960
1197
  const lower = msg.toLowerCase();
961
- let statusCode;
962
- try {
963
- const anyErr = upstreamError;
964
- if (anyErr) {
965
- if (typeof anyErr.statusCode === 'number') {
966
- statusCode = anyErr.statusCode;
967
- }
968
- else if (typeof anyErr.status === 'number') {
969
- statusCode = anyErr.status;
970
- }
971
- }
1198
+ const statusCode = extractStatusCode(upstreamError);
1199
+ const tokenFilePath = resolveTokenFilePath(auth, providerType);
1200
+ const cooldownReason = statusCode === 403 && isGoogleAccountVerificationRequiredMessage(lower) ? 'google_verify' : 'generic';
1201
+ const gate = await shouldSkipInteractiveOAuthRepair({
1202
+ providerType,
1203
+ tokenFile: tokenFilePath,
1204
+ reason: cooldownReason
1205
+ });
1206
+ if (gate.skip) {
1207
+ const msLeft = typeof gate.msLeft === 'number' ? gate.msLeft : 0;
1208
+ console.warn(`[OAuth] interactive repair skipped due to cooldown (provider=${providerType} status=${statusCode ?? 'unknown'} reason=${cooldownReason} msLeft=${msLeft} tokenFile=${tokenFilePath})`);
1209
+ return false;
972
1210
  }
973
- catch {
974
- // best-effort statusCode extraction
975
- }
976
- // 基本令牌失效判定:只看典型 OAuth 文案
977
- let looksInvalid = /invalid[_-]?token|invalid[_-]?grant|unauthenticated|unauthorized|token has expired|access token expired/.test(lower);
978
- // 对于 iflow / qwen,保留基于 401/403 的宽松判定,避免破坏既有行为。
979
- if (!looksInvalid && (pt === 'iflow' || pt === 'qwen')) {
980
- if (statusCode === 401 ||
981
- statusCode === 403 ||
982
- /\b401\b|\b403\b|40308/.test(msg)) {
983
- looksInvalid = true;
1211
+ // Mark immediately so repeated auth failures don't cause infinite auth loops within a short window.
1212
+ await markInteractiveOAuthRepairAttempt({
1213
+ providerType,
1214
+ tokenFile: tokenFilePath,
1215
+ reason: cooldownReason
1216
+ });
1217
+ // Non-blocking server semantics:
1218
+ // - Try silent refresh first (fast path).
1219
+ // - If refresh fails or interactive is required (e.g. 403 verify), kick off interactive flow in background.
1220
+ // - Return false so Virtual Router can failover immediately.
1221
+ if (!allowBlocking) {
1222
+ if (statusCode === 403 && cooldownReason === 'google_verify') {
1223
+ const url = extractGoogleAccountVerificationUrl(msg);
1224
+ if (url) {
1225
+ void openGoogleAccountVerificationInCamoufox({
1226
+ providerType,
1227
+ auth: auth,
1228
+ url
1229
+ }).catch(() => { });
1230
+ }
1231
+ return false;
984
1232
  }
985
- }
986
- // 对于 gemini / gemini-cli / antigravity,排除纯服务开关类错误,
987
- // 但如果明确提示缺少 project_id 或需要重新 OAuth,则视为令牌失效。
988
- if (pt === 'gemini' || pt === 'gemini-cli' || pt === 'antigravity') {
989
- if (/service_disabled/.test(lower) || lower.includes('has not been used in project')) {
990
- looksInvalid = false;
1233
+ try {
1234
+ await withOAuthRepairEnv(providerType, async () => {
1235
+ await ensureValid(providerType, auth, {
1236
+ forceReacquireIfRefreshFails: false,
1237
+ openBrowser: false,
1238
+ forceReauthorize: false
1239
+ });
1240
+ });
1241
+ return true;
991
1242
  }
992
- if (lower.includes('project_id not found in token') ||
993
- lower.includes('please authenticate with google oauth first')) {
994
- looksInvalid = true;
1243
+ catch {
1244
+ // ignore silent refresh errors; fall through to background interactive flow
995
1245
  }
996
- }
997
- if (!looksInvalid) {
1246
+ const interactiveOpts = {
1247
+ forceReacquireIfRefreshFails: true,
1248
+ openBrowser: true,
1249
+ // 上游已经明确返回“认证失效”(包括 iflow 的 406/439),
1250
+ // 此时强制跳过节流并允许走完整 OAuth 流程。
1251
+ forceReauthorize: pt === 'gemini' || pt === 'gemini-cli' || pt === 'antigravity' || pt === 'iflow' || pt === 'qwen'
1252
+ };
1253
+ void withOAuthRepairEnv(providerType, async () => {
1254
+ await ensureValid(providerType, auth, interactiveOpts);
1255
+ }).catch(() => {
1256
+ // background repair failure must never block requests
1257
+ });
998
1258
  return false;
999
1259
  }
1000
1260
  const opts = {
@@ -1004,13 +1264,54 @@ export async function handleUpstreamInvalidOAuthToken(providerType, auth, upstre
1004
1264
  // 此时强制跳过节流并允许走完整 OAuth 流程。
1005
1265
  forceReauthorize: pt === 'gemini' || pt === 'gemini-cli' || pt === 'antigravity' || pt === 'iflow' || pt === 'qwen'
1006
1266
  };
1007
- await ensureValidOAuthToken(providerType, auth, opts);
1267
+ await withOAuthRepairEnv(providerType, async () => {
1268
+ await ensureValid(providerType, auth, opts);
1269
+ });
1008
1270
  return true;
1009
1271
  }
1010
1272
  catch {
1011
1273
  return false;
1012
1274
  }
1013
1275
  }
1276
+ export function shouldTriggerInteractiveOAuthRepair(providerType, upstreamError) {
1277
+ const pt = providerType.toLowerCase();
1278
+ const msg = upstreamError instanceof Error
1279
+ ? upstreamError.message
1280
+ : upstreamError && typeof upstreamError === 'object' && typeof upstreamError.message === 'string'
1281
+ ? String(upstreamError.message)
1282
+ : String(upstreamError || '');
1283
+ const lower = msg.toLowerCase();
1284
+ const statusCode = extractStatusCode(upstreamError);
1285
+ // 基本令牌失效判定:只看典型 OAuth 文案
1286
+ let looksInvalid = /invalid[_-]?token|invalid[_-]?grant|unauthenticated|unauthorized|token has expired|access token expired/.test(lower);
1287
+ // 对于 iflow / qwen,保留基于 401/403 的宽松判定,避免破坏既有行为。
1288
+ if (!looksInvalid && (pt === 'iflow' || pt === 'qwen')) {
1289
+ if (statusCode === 401 ||
1290
+ statusCode === 403 ||
1291
+ /\b401\b|\b403\b|40308/.test(msg)) {
1292
+ looksInvalid = true;
1293
+ }
1294
+ }
1295
+ // 对于 gemini / gemini-cli / antigravity,排除纯服务开关类错误,
1296
+ // 但如果明确提示缺少 project_id 或需要重新 OAuth,则视为令牌失效。
1297
+ if (pt === 'gemini' || pt === 'gemini-cli' || pt === 'antigravity') {
1298
+ if (/service_disabled/.test(lower) || lower.includes('has not been used in project')) {
1299
+ looksInvalid = false;
1300
+ }
1301
+ if (lower.includes('project_id not found in token') ||
1302
+ lower.includes('please authenticate with google oauth first')) {
1303
+ looksInvalid = true;
1304
+ }
1305
+ // Antigravity/Gemini may return 403 "verify your account" / validation_required.
1306
+ // This is not a token-expired case, but it still requires an interactive OAuth/browser flow
1307
+ // to unblock the account. Treat it as "needs interactive reauth".
1308
+ if (statusCode === 403 &&
1309
+ isGoogleAccountVerificationRequiredMessage(lower)) {
1310
+ looksInvalid = true;
1311
+ }
1312
+ }
1313
+ return looksInvalid;
1314
+ }
1014
1315
  async function inferIflowClientCredsFromLog() {
1015
1316
  try {
1016
1317
  const home = process.env.HOME || '';