granclaw 0.0.1-beta.55 → 0.0.1-beta.56
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/dist/backend/agent/captcha-detect.js +33 -31
- package/dist/backend/agent/runner-pi.js +32 -69
- package/dist/backend/browser/stealth.js +0 -65
- package/dist/frontend/assets/{index-aOmMmTYZ.js → index-BFJ0hsuN.js} +1 -1
- package/dist/frontend/index.html +1 -1
- package/package.json +1 -1
- package/templates/AGENT.onboarding.md +2 -2
- package/templates/SYSTEM.md +1 -1
- package/dist/backend/assets/capmonster-extension/_locales/en/messages.json +0 -226
- package/dist/backend/assets/capmonster-extension/_locales/ru/messages.json +0 -226
- package/dist/backend/assets/capmonster-extension/_metadata/verified_contents.json +0 -1
- package/dist/backend/assets/capmonster-extension/background.js +0 -2
- package/dist/backend/assets/capmonster-extension/background.js.LICENSE.txt +0 -29
- package/dist/backend/assets/capmonster-extension/binanceInterceptor.js +0 -1
- package/dist/backend/assets/capmonster-extension/blsInterceptor.js +0 -2
- package/dist/backend/assets/capmonster-extension/blsInterceptor.js.LICENSE.txt +0 -29
- package/dist/backend/assets/capmonster-extension/content.js +0 -2
- package/dist/backend/assets/capmonster-extension/content.js.LICENSE.txt +0 -29
- package/dist/backend/assets/capmonster-extension/css/antd.variable.min.css +0 -10
- package/dist/backend/assets/capmonster-extension/css/content/solver.css +0 -152
- package/dist/backend/assets/capmonster-extension/css/popup/styles.css +0 -110
- package/dist/backend/assets/capmonster-extension/defaultSettings.json +0 -62
- package/dist/backend/assets/capmonster-extension/devtools/devtools.html +0 -1
- package/dist/backend/assets/capmonster-extension/devtools/devtools.js +0 -1
- package/dist/backend/assets/capmonster-extension/devtools/panel.html +0 -11
- package/dist/backend/assets/capmonster-extension/fonts/roboto/Roboto-Bold.ttf +0 -0
- package/dist/backend/assets/capmonster-extension/fonts/roboto/Roboto-Medium.ttf +0 -0
- package/dist/backend/assets/capmonster-extension/fonts/roboto/Roboto-Regular.ttf +0 -0
- package/dist/backend/assets/capmonster-extension/fonts/ubuntu/Ubuntu-B.ttf +0 -0
- package/dist/backend/assets/capmonster-extension/geetestInterceptor.js +0 -1
- package/dist/backend/assets/capmonster-extension/hcaptcha.js +0 -2
- package/dist/backend/assets/capmonster-extension/hcaptcha.js.LICENSE.txt +0 -29
- package/dist/backend/assets/capmonster-extension/hcaptchaInterceptor.js +0 -1
- package/dist/backend/assets/capmonster-extension/img/20x20_binance.svg +0 -14
- package/dist/backend/assets/capmonster-extension/img/20x20_bls.svg +0 -9
- package/dist/backend/assets/capmonster-extension/img/20x20_geetest.svg +0 -6
- package/dist/backend/assets/capmonster-extension/img/20x20_hcaptcha.svg +0 -28
- package/dist/backend/assets/capmonster-extension/img/20x20_recaptcha.svg +0 -5
- package/dist/backend/assets/capmonster-extension/img/20x20_text_captcha.svg +0 -5
- package/dist/backend/assets/capmonster-extension/img/20x20_turnstile.svg +0 -11
- package/dist/backend/assets/capmonster-extension/img/blue-cogs-animated.gif +0 -0
- package/dist/backend/assets/capmonster-extension/img/green-cogs.png +0 -0
- package/dist/backend/assets/capmonster-extension/img/icon.png +0 -0
- package/dist/backend/assets/capmonster-extension/img/logo.svg +0 -20
- package/dist/backend/assets/capmonster-extension/img/logo_icon.png +0 -0
- package/dist/backend/assets/capmonster-extension/img/red-cogs.png +0 -0
- package/dist/backend/assets/capmonster-extension/img/white-cogs.png +0 -0
- package/dist/backend/assets/capmonster-extension/manifest.json +0 -107
- package/dist/backend/assets/capmonster-extension/manifest_chrome.json +0 -105
- package/dist/backend/assets/capmonster-extension/manifest_firefox.json +0 -120
- package/dist/backend/assets/capmonster-extension/pageScript.js +0 -1
- package/dist/backend/assets/capmonster-extension/pageScriptHandler.js +0 -2
- package/dist/backend/assets/capmonster-extension/pageScriptHandler.js.LICENSE.txt +0 -14
- package/dist/backend/assets/capmonster-extension/panel.js +0 -16
- package/dist/backend/assets/capmonster-extension/panel.js.LICENSE.txt +0 -90
- package/dist/backend/assets/capmonster-extension/polyfills/browser-polyfill.js +0 -1
- package/dist/backend/assets/capmonster-extension/popup.html +0 -13
- package/dist/backend/assets/capmonster-extension/popup.js +0 -222
- package/dist/backend/assets/capmonster-extension/popup.js.LICENSE.txt +0 -90
- package/dist/backend/assets/capmonster-extension/recaptcha.js +0 -2
- package/dist/backend/assets/capmonster-extension/recaptcha.js.LICENSE.txt +0 -29
- package/dist/backend/assets/capmonster-extension/recaptcha2Interceptor.js +0 -1
- package/dist/backend/assets/capmonster-extension/recaptcha3Interceptor.js +0 -1
- package/dist/backend/assets/capmonster-extension/turnstileInterceptor.js +0 -2
- package/dist/backend/assets/capmonster-extension/turnstileInterceptor.js.LICENSE.txt +0 -14
|
@@ -4,22 +4,19 @@
|
|
|
4
4
|
*
|
|
5
5
|
* A stringified IIFE we ship to `agent-browser eval` to decide whether the
|
|
6
6
|
* currently-open page is blocked behind some kind of bot/captcha wall. The
|
|
7
|
-
* runner polls this snippet after every navigation command so
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* runner polls this snippet after every navigation command so the stealth
|
|
8
|
+
* extension has a chance to clear Cloudflare interstitials before the agent
|
|
9
|
+
* starts reading the page.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* - Inline widgets: .g-recaptcha, .h-captcha, [class*="captcha"].
|
|
19
|
-
* - PerimeterX / HUMAN bot check: #px-captcha.
|
|
11
|
+
* Returns one of three values:
|
|
12
|
+
* 'interstitial' — Cloudflare JS challenge ("Just a moment..."). The stealth
|
|
13
|
+
* extension can clear these; the runner should poll and wait.
|
|
14
|
+
* 'captcha' — An actual captcha widget (reCAPTCHA, hCaptcha, Turnstile,
|
|
15
|
+
* etc.) that requires human intervention. The runner should
|
|
16
|
+
* request a takeover immediately.
|
|
17
|
+
* 'clear' — No challenge detected.
|
|
20
18
|
*
|
|
21
|
-
* IMPORTANT: this runs as a string inside Chrome, NOT in Node.
|
|
22
|
-
* newer than the detector's tested Chrome baseline, no TypeScript. Keep it
|
|
19
|
+
* IMPORTANT: this runs as a string inside Chrome, NOT in Node. Keep it
|
|
23
20
|
* ES5-flavoured so it stays safe if the agent-browser build ever targets an
|
|
24
21
|
* older chromium.
|
|
25
22
|
*/
|
|
@@ -27,21 +24,32 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
27
24
|
exports.CAPTCHA_DETECT_JS = void 0;
|
|
28
25
|
exports.CAPTCHA_DETECT_JS = `(function() {
|
|
29
26
|
var title = (document.title || '').toLowerCase();
|
|
30
|
-
|
|
27
|
+
|
|
28
|
+
// Cloudflare JS interstitials — the stealth extension can clear these.
|
|
29
|
+
var cfTitlePatterns = [
|
|
31
30
|
'just a moment', // Cloudflare interstitial (EN)
|
|
32
31
|
'un momento', // CF Spanish
|
|
33
32
|
'un instant', // CF French
|
|
34
33
|
'einen moment', // CF German
|
|
35
34
|
'checking your browser', // CF older / generic anti-bot
|
|
36
|
-
'attention required', // CF WAF block page
|
|
37
35
|
'please wait', // generic "please wait while we check"
|
|
38
36
|
];
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
var cfSelectors = [
|
|
38
|
+
'#challenge-stage',
|
|
39
|
+
'#challenge-running',
|
|
40
|
+
'#cf-challenge-running',
|
|
41
|
+
'.cf-browser-verification',
|
|
42
|
+
'.cf-im-under-attack',
|
|
43
|
+
];
|
|
44
|
+
for (var i = 0; i < cfTitlePatterns.length; i++) {
|
|
45
|
+
if (title.indexOf(cfTitlePatterns[i]) !== -1) return 'interstitial';
|
|
46
|
+
}
|
|
47
|
+
for (var j = 0; j < cfSelectors.length; j++) {
|
|
48
|
+
if (document.querySelector(cfSelectors[j])) return 'interstitial';
|
|
41
49
|
}
|
|
42
50
|
|
|
43
|
-
|
|
44
|
-
|
|
51
|
+
// Actual captcha widgets — need human intervention.
|
|
52
|
+
var captchaSelectors = [
|
|
45
53
|
'iframe[src*="captcha-delivery"]', // DataDome
|
|
46
54
|
'iframe[src*="geo.captcha"]', // DataDome geo
|
|
47
55
|
'iframe[src*="recaptcha"]', // reCaptcha v2/v3
|
|
@@ -49,21 +57,15 @@ exports.CAPTCHA_DETECT_JS = `(function() {
|
|
|
49
57
|
'iframe[src*="challenges.cloudflare"]', // Cloudflare Turnstile
|
|
50
58
|
'iframe[src*="arkoselabs"]', // Arkose FunCaptcha
|
|
51
59
|
'iframe[src*="funcaptcha"]',
|
|
52
|
-
// Inline widgets
|
|
53
60
|
'.g-recaptcha',
|
|
54
61
|
'.h-captcha',
|
|
55
62
|
'[class*="captcha"]',
|
|
56
|
-
//
|
|
57
|
-
'#challenge-stage',
|
|
58
|
-
'#challenge-running',
|
|
59
|
-
'#cf-challenge-running',
|
|
60
|
-
'.cf-browser-verification',
|
|
61
|
-
'.cf-im-under-attack',
|
|
62
|
-
// PerimeterX / HUMAN
|
|
63
|
-
'#px-captcha',
|
|
63
|
+
'#px-captcha', // PerimeterX / HUMAN
|
|
64
64
|
];
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
// "Attention Required" is a CF WAF block — not a JS interstitial, needs human.
|
|
66
|
+
if (title.indexOf('attention required') !== -1) return 'captcha';
|
|
67
|
+
for (var k = 0; k < captchaSelectors.length; k++) {
|
|
68
|
+
if (document.querySelector(captchaSelectors[k])) return 'captcha';
|
|
67
69
|
}
|
|
68
70
|
return 'clear';
|
|
69
71
|
})()`;
|
|
@@ -29,17 +29,6 @@ const child_process_1 = require("child_process");
|
|
|
29
29
|
const util_1 = require("util");
|
|
30
30
|
const node_html_markdown_1 = require("node-html-markdown");
|
|
31
31
|
const undici_1 = require("undici");
|
|
32
|
-
// Cache ProxyAgent instances by URL so the same connection pool (and exit IP)
|
|
33
|
-
// is reused across all fetch_website calls for the same proxy.
|
|
34
|
-
const proxyAgentCache = new Map();
|
|
35
|
-
function getProxyAgent(proxyUrl) {
|
|
36
|
-
let agent = proxyAgentCache.get(proxyUrl);
|
|
37
|
-
if (!agent) {
|
|
38
|
-
agent = new undici_1.ProxyAgent(proxyUrl);
|
|
39
|
-
proxyAgentCache.set(proxyUrl, agent);
|
|
40
|
-
}
|
|
41
|
-
return agent;
|
|
42
|
-
}
|
|
43
32
|
const os_1 = require("os");
|
|
44
33
|
const crypto_1 = require("crypto");
|
|
45
34
|
const takeover_state_js_1 = require("../takeover-state.js");
|
|
@@ -337,37 +326,6 @@ function getLanIp() {
|
|
|
337
326
|
}
|
|
338
327
|
return 'localhost';
|
|
339
328
|
}
|
|
340
|
-
/**
|
|
341
|
-
* Stable hash of an agent ID string → non-negative integer.
|
|
342
|
-
* Used so proxy assignment is deterministic by agent ID, not config order.
|
|
343
|
-
*/
|
|
344
|
-
function hashAgentId(id) {
|
|
345
|
-
let h = 0;
|
|
346
|
-
for (const c of id)
|
|
347
|
-
h = (Math.imul(31, h) + c.charCodeAt(0)) | 0;
|
|
348
|
-
return Math.abs(h);
|
|
349
|
-
}
|
|
350
|
-
/**
|
|
351
|
-
* Resolve the proxy for an agent.
|
|
352
|
-
* Priority: agent.proxy config → GRANCLAW_PROXY_LIST (stable hash by agent ID) → undefined.
|
|
353
|
-
* Hash-based assignment means the same agent always gets the same proxy even if
|
|
354
|
-
* agents.config.json is reordered or new agents are added — so the Chrome daemon
|
|
355
|
-
* always restarts on the same IP.
|
|
356
|
-
* Only applies when GRANCLAW_PROXY_ENABLE=true or agent.proxy is explicitly set.
|
|
357
|
-
*/
|
|
358
|
-
function resolveAgentProxy(agentId, configProxy) {
|
|
359
|
-
if (configProxy)
|
|
360
|
-
return configProxy;
|
|
361
|
-
if (process.env.GRANCLAW_PROXY_ENABLE !== 'true')
|
|
362
|
-
return undefined;
|
|
363
|
-
const proxyList = process.env.GRANCLAW_PROXY_LIST;
|
|
364
|
-
if (!proxyList)
|
|
365
|
-
return undefined;
|
|
366
|
-
const proxies = proxyList.split(',').map(p => p.trim()).filter(Boolean);
|
|
367
|
-
if (proxies.length === 0)
|
|
368
|
-
return undefined;
|
|
369
|
-
return proxies[hashAgentId(agentId) % proxies.length];
|
|
370
|
-
}
|
|
371
329
|
async function runAgent(agent, message, onChunk, options) {
|
|
372
330
|
const workspaceDir = path_1.default.resolve(config_js_1.REPO_ROOT, agent.workspaceDir);
|
|
373
331
|
const channelId = options?.channelId ?? 'ui';
|
|
@@ -752,38 +710,43 @@ async function runAgent(agent, message, onChunk, options) {
|
|
|
752
710
|
// Privileged commands (record, close, tab close for session 0,
|
|
753
711
|
// session management) are rejected — the runtime owns those.
|
|
754
712
|
/**
|
|
755
|
-
* After a navigation command, check whether the page is blocked
|
|
756
|
-
* bot/captcha wall (widget captcha OR Cloudflare-style JS interstitial)
|
|
757
|
-
* and poll until CapMonster + the stealth extension have cleared it.
|
|
713
|
+
* After a navigation command, check whether the page is blocked.
|
|
758
714
|
*
|
|
759
|
-
*
|
|
760
|
-
*
|
|
761
|
-
*
|
|
762
|
-
* be reported as "unsolved" (regression verified on mariados 2026-04-15).
|
|
763
|
-
*
|
|
764
|
-
* Returns 'clear' (no captcha / solved), 'unsolved' (still blocked after timeout).
|
|
715
|
+
* 'interstitial' → Cloudflare JS challenge; stealth can clear it, poll up to 45s.
|
|
716
|
+
* 'captcha' → Actual captcha widget; no auto-solver, request takeover immediately.
|
|
717
|
+
* 'clear' → No challenge detected.
|
|
765
718
|
*/
|
|
766
719
|
async function waitForCaptchaResolution(bin, sessionId) {
|
|
767
720
|
const evalArgv = (id) => ['--session', id, 'eval', captcha_detect_js_1.CAPTCHA_DETECT_JS];
|
|
768
721
|
const check = async () => {
|
|
769
722
|
try {
|
|
770
723
|
const { stdout } = await execFileAsync(bin, evalArgv(sessionId), { timeout: 8_000 });
|
|
771
|
-
|
|
724
|
+
const out = stdout.trim();
|
|
725
|
+
if (out.includes('captcha'))
|
|
726
|
+
return 'captcha';
|
|
727
|
+
if (out.includes('interstitial'))
|
|
728
|
+
return 'interstitial';
|
|
729
|
+
return 'clear';
|
|
772
730
|
}
|
|
773
731
|
catch {
|
|
774
|
-
return
|
|
732
|
+
return 'clear';
|
|
775
733
|
}
|
|
776
734
|
};
|
|
777
|
-
// Initial check — short delay to let page settle
|
|
778
735
|
await new Promise(r => setTimeout(r, 1500));
|
|
779
|
-
|
|
736
|
+
const initial = await check();
|
|
737
|
+
if (initial === 'clear')
|
|
780
738
|
return 'clear';
|
|
781
|
-
|
|
739
|
+
if (initial === 'captcha')
|
|
740
|
+
return 'captcha';
|
|
741
|
+
// Cloudflare interstitial — poll every 2s until cleared or 45s elapses.
|
|
782
742
|
const deadline = Date.now() + 45_000;
|
|
783
743
|
while (Date.now() < deadline) {
|
|
784
744
|
await new Promise(r => setTimeout(r, 2_000));
|
|
785
|
-
|
|
745
|
+
const result = await check();
|
|
746
|
+
if (result === 'clear')
|
|
786
747
|
return 'clear';
|
|
748
|
+
if (result === 'captcha')
|
|
749
|
+
return 'captcha';
|
|
787
750
|
}
|
|
788
751
|
return 'unsolved';
|
|
789
752
|
}
|
|
@@ -817,7 +780,7 @@ async function runAgent(agent, message, onChunk, options) {
|
|
|
817
780
|
'Do not call `record start`, `record stop`, `close`, or `session` — the runtime manages those.',
|
|
818
781
|
'You do not need to screenshot for audit — the whole session is recorded as video automatically.',
|
|
819
782
|
'Saved logins persist automatically when the user has set up a profile via the dashboard Browser view.',
|
|
820
|
-
'CAPTCHA & Cloudflare handling: the browser ships
|
|
783
|
+
'CAPTCHA & Cloudflare handling: the browser ships a stealth extension that handles Cloudflare JS interstitials ("Just a moment…") automatically (waits up to ~45s). If an actual CAPTCHA widget is detected (reCAPTCHA, hCaptcha, Turnstile, etc.), the runtime returns immediately with a warning — call request_human_browser_takeover right away.',
|
|
821
784
|
'Examples: {"command":"open","args":["https://example.com"]}, {"command":"click","args":["--ref","e12"]}, {"command":"fill","args":["--ref","e5","Alice"]}',
|
|
822
785
|
],
|
|
823
786
|
parameters: {
|
|
@@ -870,11 +833,7 @@ async function runAgent(agent, message, onChunk, options) {
|
|
|
870
833
|
if (fs_1.default.existsSync(profileDir)) {
|
|
871
834
|
argv.push('--profile', profileDir);
|
|
872
835
|
}
|
|
873
|
-
|
|
874
|
-
proxy: resolveAgentProxy(agent.id, agent.proxy),
|
|
875
|
-
capmonsterKey: agent.capmonsterKey,
|
|
876
|
-
};
|
|
877
|
-
argv.push(...(0, stealth_js_1.stealthArgv)(stealthOpts));
|
|
836
|
+
argv.push(...(0, stealth_js_1.stealthArgv)());
|
|
878
837
|
argv.push(command, ...args);
|
|
879
838
|
try {
|
|
880
839
|
const { stdout, stderr } = await execFileAsync(agentBrowserBin, argv, {
|
|
@@ -884,15 +843,22 @@ async function runAgent(agent, message, onChunk, options) {
|
|
|
884
843
|
});
|
|
885
844
|
(0, session_manager_js_1.appendCommand)(browserState.handle, `${command} ${args.join(' ')}`.trim());
|
|
886
845
|
const out = stdout.trim() || stderr.trim() || 'ok';
|
|
887
|
-
// After navigation, check for CAPTCHA and wait up to 30s for the
|
|
888
|
-
// CapMonster extension to auto-solve it before returning to the agent.
|
|
889
846
|
if (command === 'open' || command === 'reload') {
|
|
890
847
|
const captchaResult = await waitForCaptchaResolution(agentBrowserBin, agent.id);
|
|
848
|
+
if (captchaResult === 'captcha') {
|
|
849
|
+
return {
|
|
850
|
+
content: [{
|
|
851
|
+
type: 'text',
|
|
852
|
+
text: out + '\n\n⚠️ CAPTCHA detected. ' +
|
|
853
|
+
'Use request_human_browser_takeover to let the user solve it, or try a different URL.',
|
|
854
|
+
}],
|
|
855
|
+
};
|
|
856
|
+
}
|
|
891
857
|
if (captchaResult === 'unsolved') {
|
|
892
858
|
return {
|
|
893
859
|
content: [{
|
|
894
860
|
type: 'text',
|
|
895
|
-
text: out + '\n\n⚠️
|
|
861
|
+
text: out + '\n\n⚠️ Cloudflare interstitial not cleared after 45s. ' +
|
|
896
862
|
'Use request_human_browser_takeover to let the user solve it, or try a different URL.',
|
|
897
863
|
}],
|
|
898
864
|
};
|
|
@@ -1140,9 +1106,7 @@ async function runAgent(agent, message, onChunk, options) {
|
|
|
1140
1106
|
return { content: [{ type: 'text', text: `fetch_website (unblocker) error: ${err instanceof Error ? err.message : String(err)}` }] };
|
|
1141
1107
|
}
|
|
1142
1108
|
}
|
|
1143
|
-
// Plain HTTP fetch — route through proxy if configured
|
|
1144
1109
|
try {
|
|
1145
|
-
const agentProxy = resolveAgentProxy(agent.id, agent.proxy);
|
|
1146
1110
|
const fetchOpts = {
|
|
1147
1111
|
headers: {
|
|
1148
1112
|
'User-Agent': 'Mozilla/5.0 (compatible; GranClaw/1.0; +https://granclaw.com)',
|
|
@@ -1150,7 +1114,6 @@ async function runAgent(agent, message, onChunk, options) {
|
|
|
1150
1114
|
},
|
|
1151
1115
|
signal: AbortSignal.timeout(60_000),
|
|
1152
1116
|
redirect: 'follow',
|
|
1153
|
-
...(agentProxy ? { dispatcher: getProxyAgent(agentProxy) } : {}),
|
|
1154
1117
|
};
|
|
1155
1118
|
const res = await (0, undici_1.fetch)(params.url, fetchOpts);
|
|
1156
1119
|
if (!res.ok) {
|
|
@@ -33,11 +33,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
33
33
|
};
|
|
34
34
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
35
|
exports.STEALTH_EXTENSION_DIR = void 0;
|
|
36
|
-
exports.capmonsterExtensionDir = capmonsterExtensionDir;
|
|
37
36
|
exports.stealthArgv = stealthArgv;
|
|
38
37
|
exports.__resetStealthCacheForTests = __resetStealthCacheForTests;
|
|
39
38
|
const fs_1 = __importDefault(require("fs"));
|
|
40
|
-
const os_1 = __importDefault(require("os"));
|
|
41
39
|
const path_1 = __importDefault(require("path"));
|
|
42
40
|
const child_process_1 = require("child_process");
|
|
43
41
|
// ── Extension dir ─────────────────────────────────────────────────────────────
|
|
@@ -59,55 +57,6 @@ function resolveExtensionDir() {
|
|
|
59
57
|
return null;
|
|
60
58
|
}
|
|
61
59
|
exports.STEALTH_EXTENSION_DIR = resolveExtensionDir();
|
|
62
|
-
// ── CapMonster extension ──────────────────────────────────────────────────────
|
|
63
|
-
/**
|
|
64
|
-
* Resolve the CapMonster extension directory (bundled in assets).
|
|
65
|
-
* Returns null if not found.
|
|
66
|
-
*/
|
|
67
|
-
function resolveCapmonsterExtensionDir() {
|
|
68
|
-
const candidates = [
|
|
69
|
-
path_1.default.resolve(__dirname, '../../assets/capmonster-extension'),
|
|
70
|
-
path_1.default.resolve(__dirname, '../assets/capmonster-extension'),
|
|
71
|
-
];
|
|
72
|
-
for (const candidate of candidates) {
|
|
73
|
-
if (fs_1.default.existsSync(path_1.default.join(candidate, 'manifest.json')))
|
|
74
|
-
return candidate;
|
|
75
|
-
}
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
const CAPMONSTER_BASE_DIR = resolveCapmonsterExtensionDir();
|
|
79
|
-
/**
|
|
80
|
-
* Return a CapMonster extension directory pre-configured with the given API key.
|
|
81
|
-
* Writes a patched copy to os.tmpdir() so the base assets stay clean.
|
|
82
|
-
* Result is cached per process — re-patched only if the key changes.
|
|
83
|
-
*/
|
|
84
|
-
let _patchedCapmonsterDir = null;
|
|
85
|
-
let _patchedCapmonsterKey = null;
|
|
86
|
-
function capmonsterExtensionDir(apiKey) {
|
|
87
|
-
if (!CAPMONSTER_BASE_DIR)
|
|
88
|
-
return null;
|
|
89
|
-
if (_patchedCapmonsterDir && _patchedCapmonsterKey === apiKey) {
|
|
90
|
-
return _patchedCapmonsterDir;
|
|
91
|
-
}
|
|
92
|
-
try {
|
|
93
|
-
const dest = path_1.default.join(os_1.default.tmpdir(), 'granclaw-capmonster-ext');
|
|
94
|
-
// Copy base extension
|
|
95
|
-
fs_1.default.cpSync(CAPMONSTER_BASE_DIR, dest, { recursive: true });
|
|
96
|
-
// Patch defaultSettings.json with the API key
|
|
97
|
-
const settingsPath = path_1.default.join(dest, 'defaultSettings.json');
|
|
98
|
-
if (fs_1.default.existsSync(settingsPath)) {
|
|
99
|
-
const settings = JSON.parse(fs_1.default.readFileSync(settingsPath, 'utf-8'));
|
|
100
|
-
settings.clientKey = apiKey;
|
|
101
|
-
fs_1.default.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
102
|
-
}
|
|
103
|
-
_patchedCapmonsterDir = dest;
|
|
104
|
-
_patchedCapmonsterKey = apiKey;
|
|
105
|
-
return dest;
|
|
106
|
-
}
|
|
107
|
-
catch {
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
60
|
// ── Chrome binary detection + UA derivation ───────────────────────────────────
|
|
112
61
|
let cachedChromePath;
|
|
113
62
|
function detectChromePath() {
|
|
@@ -234,24 +183,10 @@ function stealthArgv(options = {}) {
|
|
|
234
183
|
if (exports.STEALTH_EXTENSION_DIR) {
|
|
235
184
|
argv.push('--extension', exports.STEALTH_EXTENSION_DIR);
|
|
236
185
|
}
|
|
237
|
-
const capmonsterKey = options.capmonsterKey
|
|
238
|
-
|| process.env.CAPMONSTER_KEY
|
|
239
|
-
|| process.env.CAPMONSTER_API_KEY;
|
|
240
|
-
const capmonsterEnabled = process.env.CAPMONSTER_ENABLED !== 'false';
|
|
241
|
-
if (capmonsterKey && capmonsterEnabled) {
|
|
242
|
-
const capmonsterDir = capmonsterExtensionDir(capmonsterKey);
|
|
243
|
-
if (capmonsterDir)
|
|
244
|
-
argv.push('--extension', capmonsterDir);
|
|
245
|
-
}
|
|
246
186
|
const chrome = detectChromePath();
|
|
247
187
|
if (chrome) {
|
|
248
188
|
argv.push('--executable-path', chrome);
|
|
249
189
|
}
|
|
250
|
-
// Route browser traffic through the agent's proxy if configured
|
|
251
|
-
const proxy = options.proxy || process.env.GRANCLAW_PROXY;
|
|
252
|
-
if (proxy) {
|
|
253
|
-
argv.push('--proxy', proxy);
|
|
254
|
-
}
|
|
255
190
|
return argv;
|
|
256
191
|
}
|
|
257
192
|
// ── Test helpers ──────────────────────────────────────────────────────────────
|