@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.
- package/README.md +97 -13
- package/configsamples/config.json +8 -8
- package/configsamples/config.reference.json +1 -1
- package/configsamples/provider/crs/config.v1.json +1 -1
- package/configsamples/provider/glm/config.v1.json +1 -1
- package/configsamples/provider/glm-anthropic/config.v1.json +1 -1
- package/configsamples/provider/kimi/config.v1.json +1 -1
- package/configsamples/provider/lmstudio/config.v1.json +2 -1
- package/configsamples/provider/mimo/config.v1.json +1 -1
- package/configsamples/provider/modelscope/config.v1.json +1 -1
- package/configsamples/provider/qwen/config.v1.json +1 -1
- package/configsamples/provider/tab/config.v1.json +2 -1
- package/configsamples/provider/tabglm/config.v1.json +1 -1
- package/dist/build-info.js +3 -3
- package/dist/build-info.js.map +1 -1
- package/dist/cli/commands/config.js +8 -9
- package/dist/cli/commands/config.js.map +1 -1
- package/dist/cli/commands/restart.d.ts +4 -12
- package/dist/cli/commands/restart.js +226 -120
- package/dist/cli/commands/restart.js.map +1 -1
- package/dist/cli/commands/start.d.ts +1 -0
- package/dist/cli/commands/start.js +28 -1
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/commands/status.js +12 -6
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/config/init-provider-catalog.js +12 -11
- package/dist/cli/config/init-provider-catalog.js.map +1 -1
- package/dist/cli.js +3 -14
- package/dist/cli.js.map +1 -1
- package/dist/client/anthropic/anthropic-protocol-client.d.ts +1 -0
- package/dist/client/anthropic/anthropic-protocol-client.js +25 -0
- package/dist/client/anthropic/anthropic-protocol-client.js.map +1 -1
- package/dist/commands/oauth.js +185 -9
- package/dist/commands/oauth.js.map +1 -1
- package/dist/commands/token-daemon.js +12 -2
- package/dist/commands/token-daemon.js.map +1 -1
- package/dist/docs/daemon-admin-ui.html +1242 -234
- package/dist/index.js +119 -0
- package/dist/index.js.map +1 -1
- package/dist/manager/index.d.ts +2 -0
- package/dist/manager/index.js +39 -2
- package/dist/manager/index.js.map +1 -1
- package/dist/manager/modules/quota/antigravity-quota-manager.d.ts +29 -5
- package/dist/manager/modules/quota/antigravity-quota-manager.js +369 -113
- package/dist/manager/modules/quota/antigravity-quota-manager.js.map +1 -1
- package/dist/manager/modules/quota/provider-quota-daemon.cooldown.d.ts +7 -0
- package/dist/manager/modules/quota/provider-quota-daemon.cooldown.js +61 -0
- package/dist/manager/modules/quota/provider-quota-daemon.cooldown.js.map +1 -1
- package/dist/manager/modules/quota/provider-quota-daemon.d.ts +1 -0
- package/dist/manager/modules/quota/provider-quota-daemon.events.js +134 -5
- package/dist/manager/modules/quota/provider-quota-daemon.events.js.map +1 -1
- package/dist/manager/modules/quota/provider-quota-daemon.js +19 -13
- package/dist/manager/modules/quota/provider-quota-daemon.js.map +1 -1
- package/dist/manager/modules/quota/provider-quota-daemon.model-backoff.d.ts +1 -0
- package/dist/manager/modules/quota/provider-quota-daemon.model-backoff.js +8 -3
- package/dist/manager/modules/quota/provider-quota-daemon.model-backoff.js.map +1 -1
- package/dist/manager/modules/token/index.js +2 -2
- package/dist/manager/modules/token/index.js.map +1 -1
- package/dist/manager/quota/provider-quota-center.d.ts +9 -0
- package/dist/manager/quota/provider-quota-center.js +19 -2
- package/dist/manager/quota/provider-quota-center.js.map +1 -1
- package/dist/modules/llmswitch/bridge.d.ts +33 -1
- package/dist/modules/llmswitch/bridge.js +170 -2
- package/dist/modules/llmswitch/bridge.js.map +1 -1
- package/dist/modules/llmswitch/core-loader.js +64 -11
- package/dist/modules/llmswitch/core-loader.js.map +1 -1
- package/dist/modules/pipeline/utils/debug-logger.d.ts +1 -0
- package/dist/modules/pipeline/utils/debug-logger.js +50 -3
- package/dist/modules/pipeline/utils/debug-logger.js.map +1 -1
- package/dist/providers/auth/apikey-auth.js +15 -3
- package/dist/providers/auth/apikey-auth.js.map +1 -1
- package/dist/providers/auth/oauth-auth.js +26 -2
- package/dist/providers/auth/oauth-auth.js.map +1 -1
- package/dist/providers/auth/oauth-lifecycle.d.ts +13 -1
- package/dist/providers/auth/oauth-lifecycle.js +346 -45
- package/dist/providers/auth/oauth-lifecycle.js.map +1 -1
- package/dist/providers/auth/oauth-repair-cooldown.d.ts +21 -0
- package/dist/providers/auth/oauth-repair-cooldown.js +100 -0
- package/dist/providers/auth/oauth-repair-cooldown.js.map +1 -0
- package/dist/providers/auth/oauth-repair-env.d.ts +1 -0
- package/dist/providers/auth/oauth-repair-env.js +79 -0
- package/dist/providers/auth/oauth-repair-env.js.map +1 -0
- package/dist/providers/auth/qwen-userinfo-helper.d.ts +2 -0
- package/dist/providers/auth/qwen-userinfo-helper.js +72 -40
- package/dist/providers/auth/qwen-userinfo-helper.js.map +1 -1
- package/dist/providers/auth/tokenfile-auth.d.ts +2 -0
- package/dist/providers/auth/tokenfile-auth.js +163 -21
- package/dist/providers/auth/tokenfile-auth.js.map +1 -1
- package/dist/providers/core/api/provider-types.d.ts +10 -0
- package/dist/providers/core/config/camoufox-launcher.d.ts +3 -0
- package/dist/providers/core/config/camoufox-launcher.js +190 -3
- package/dist/providers/core/config/camoufox-launcher.js.map +1 -1
- package/dist/providers/core/config/oauth-flows.js +50 -19
- package/dist/providers/core/config/oauth-flows.js.map +1 -1
- package/dist/providers/core/config/provider-oauth-configs.js +1 -1
- package/dist/providers/core/config/provider-oauth-configs.js.map +1 -1
- package/dist/providers/core/runtime/gemini-cli-http-provider.d.ts +5 -0
- package/dist/providers/core/runtime/gemini-cli-http-provider.js +172 -15
- package/dist/providers/core/runtime/gemini-cli-http-provider.js.map +1 -1
- package/dist/providers/core/runtime/gemini-http-provider.d.ts +11 -0
- package/dist/providers/core/runtime/gemini-http-provider.js +281 -3
- package/dist/providers/core/runtime/gemini-http-provider.js.map +1 -1
- package/dist/providers/core/runtime/http-request-executor.js +55 -0
- package/dist/providers/core/runtime/http-request-executor.js.map +1 -1
- package/dist/providers/core/runtime/http-transport-provider.js +10 -14
- package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
- package/dist/providers/core/runtime/provider-factory.d.ts +1 -0
- package/dist/providers/core/runtime/provider-factory.js +40 -2
- package/dist/providers/core/runtime/provider-factory.js.map +1 -1
- package/dist/providers/core/strategies/oauth-auth-code-flow.js +45 -2
- package/dist/providers/core/strategies/oauth-auth-code-flow.js.map +1 -1
- package/dist/providers/core/strategies/oauth-device-flow.js +13 -2
- package/dist/providers/core/strategies/oauth-device-flow.js.map +1 -1
- package/dist/providers/core/strategies/oauth-refresh-errors.d.ts +1 -0
- package/dist/providers/core/strategies/oauth-refresh-errors.js +26 -0
- package/dist/providers/core/strategies/oauth-refresh-errors.js.map +1 -0
- package/dist/providers/core/utils/snapshot-writer.d.ts +4 -2
- package/dist/providers/core/utils/snapshot-writer.js +86 -23
- package/dist/providers/core/utils/snapshot-writer.js.map +1 -1
- package/dist/scripts/camoufox/launch-auth.mjs +545 -49
- package/dist/server/handlers/chat-handler.js +1 -1
- package/dist/server/handlers/chat-handler.js.map +1 -1
- package/dist/server/handlers/handler-utils.d.ts +1 -0
- package/dist/server/handlers/handler-utils.js +231 -3
- package/dist/server/handlers/handler-utils.js.map +1 -1
- package/dist/server/handlers/messages-handler.js +1 -1
- package/dist/server/handlers/messages-handler.js.map +1 -1
- package/dist/server/handlers/responses-handler.js +17 -5
- package/dist/server/handlers/responses-handler.js.map +1 -1
- package/dist/server/handlers/sse-dispatcher.js +10 -1
- package/dist/server/handlers/sse-dispatcher.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/control-handler.d.ts +3 -0
- package/dist/server/runtime/http-server/daemon-admin/control-handler.js +389 -0
- package/dist/server/runtime/http-server/daemon-admin/control-handler.js.map +1 -0
- package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +190 -5
- package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/providers-handler.js +2 -1
- package/dist/server/runtime/http-server/daemon-admin/providers-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/quota-handler.js +116 -14
- package/dist/server/runtime/http-server/daemon-admin/quota-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin/routing-policy.d.ts +30 -0
- package/dist/server/runtime/http-server/daemon-admin/routing-policy.js +133 -0
- package/dist/server/runtime/http-server/daemon-admin/routing-policy.js.map +1 -0
- package/dist/server/runtime/http-server/daemon-admin/status-handler.js +40 -1
- package/dist/server/runtime/http-server/daemon-admin/status-handler.js.map +1 -1
- package/dist/server/runtime/http-server/daemon-admin-routes.d.ts +5 -0
- package/dist/server/runtime/http-server/daemon-admin-routes.js +3 -0
- package/dist/server/runtime/http-server/daemon-admin-routes.js.map +1 -1
- package/dist/server/runtime/http-server/executor-pipeline.d.ts +10 -0
- package/dist/server/runtime/http-server/executor-pipeline.js +6 -0
- package/dist/server/runtime/http-server/executor-pipeline.js.map +1 -1
- package/dist/server/runtime/http-server/executor-response.js +26 -0
- package/dist/server/runtime/http-server/executor-response.js.map +1 -1
- package/dist/server/runtime/http-server/hub-shadow-compare.js +41 -3
- package/dist/server/runtime/http-server/hub-shadow-compare.js.map +1 -1
- package/dist/server/runtime/http-server/index.d.ts +9 -0
- package/dist/server/runtime/http-server/index.js +337 -91
- package/dist/server/runtime/http-server/index.js.map +1 -1
- package/dist/server/runtime/http-server/middleware.js +27 -1
- package/dist/server/runtime/http-server/middleware.js.map +1 -1
- package/dist/server/runtime/http-server/request-executor.js +159 -24
- package/dist/server/runtime/http-server/request-executor.js.map +1 -1
- package/dist/server/runtime/http-server/routes.d.ts +1 -0
- package/dist/server/runtime/http-server/routes.js +36 -3
- package/dist/server/runtime/http-server/routes.js.map +1 -1
- package/dist/server/runtime/http-server/server-id.d.ts +1 -0
- package/dist/server/runtime/http-server/server-id.js +18 -0
- package/dist/server/runtime/http-server/server-id.js.map +1 -0
- package/dist/server/runtime/http-server/stats-manager.d.ts +2 -0
- package/dist/server/runtime/http-server/stats-manager.js +63 -7
- package/dist/server/runtime/http-server/stats-manager.js.map +1 -1
- package/dist/server/runtime/http-server/types.d.ts +2 -0
- package/dist/server/utils/stage-logger.js +54 -9
- package/dist/server/utils/stage-logger.js.map +1 -1
- package/dist/token-daemon/history-store.d.ts +8 -3
- package/dist/token-daemon/history-store.js +41 -20
- package/dist/token-daemon/history-store.js.map +1 -1
- package/dist/token-daemon/index.d.ts +5 -1
- package/dist/token-daemon/index.js +191 -11
- package/dist/token-daemon/index.js.map +1 -1
- package/dist/token-daemon/quota-auth-issue.d.ts +7 -0
- package/dist/token-daemon/quota-auth-issue.js +231 -0
- package/dist/token-daemon/quota-auth-issue.js.map +1 -0
- package/dist/token-daemon/token-daemon.d.ts +2 -0
- package/dist/token-daemon/token-daemon.js +177 -14
- package/dist/token-daemon/token-daemon.js.map +1 -1
- package/dist/token-portal/local-token-portal.js +6 -0
- package/dist/token-portal/local-token-portal.js.map +1 -1
- package/docs/ANTIGRAVITY_IDE_FORWARD_PROXY.md +61 -0
- package/docs/ANTIGRAVITY_THOUGHT_SIGNATURE_BOOTSTRAP_429.md +80 -0
- package/docs/CLOCK.md +94 -0
- package/docs/DAEMON_CONTROL_PLANE.md +34 -0
- package/docs/OAUTH.md +172 -0
- package/docs/PROVIDERS_BUILTIN.md +5 -3
- package/docs/PROVIDER_TYPES.md +6 -4
- package/docs/QUOTA_MANAGER_V3.md +54 -0
- package/docs/ROUTING_POLICY_SCHEMA.md +47 -0
- package/docs/ROUTING_POLICY_UI.md +11 -0
- package/docs/SERVERTOOL_CLOCK_DESIGN.md +56 -25
- package/docs/antigravity-routing-contract.md +17 -11
- package/docs/config-secrets.md +49 -0
- package/docs/daemon-admin-ui.html +1242 -234
- package/docs/oauth-authentication-guide.md +4 -0
- package/docs/oauth-iflow-implementation.md +4 -0
- package/docs/provider-quota-design.md +11 -0
- package/docs/providers/antigravity-gemini-provider-compat.md +1 -0
- package/docs/providers/antigravity-thought-signature.md +127 -0
- package/docs/providers/tabglm-claude-code-compat.md +11 -3
- package/docs/refactoring/host-sharedmodule-safe-migration-plan.md +164 -0
- package/docs/token-daemon-preview.html +2 -2
- package/docs/token-refresh-daemon-plan.md +6 -6
- package/package.json +4 -3
- package/scripts/antigravity-ide-forward-proxy.mjs +362 -0
- package/scripts/backfill-apply-patch-exec-errorsamples.mjs +19 -0
- package/scripts/camoufox/launch-auth.mjs +545 -49
- package/scripts/ci/repo-sanity.mjs +2 -0
- package/scripts/install-global.sh +46 -0
- package/scripts/migrate-antigravity-session-signatures-alias.mjs +193 -0
- package/scripts/migrate-antigravity-session-signatures.mjs +165 -0
- package/scripts/tests/blackbox-rcc-vs-routecodex-antigravity.mjs +44 -9
- package/scripts/tests/ci-jest.mjs +3 -0
- package/scripts/verify-client-headers.mjs +33 -5
- 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
|
-
|
|
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:
|
|
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 ||
|
|
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 ||
|
|
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 ||
|
|
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:
|
|
611
|
-
const popupPromise = context.waitForEvent('page', { timeout:
|
|
612
|
-
|
|
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
|
-
|
|
615
|
-
|
|
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
|
-
|
|
656
|
-
const
|
|
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
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
|
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
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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
|
-
|
|
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
|
}
|