gemini-proxy-client 1.0.18 → 1.0.19
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/browser/manager.js +275 -40
- package/dist/cli.js +1 -0
- package/dist/commands/start.js +27 -4
- package/package.json +2 -1
package/dist/browser/manager.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Camoufox } from 'camoufox-js';
|
|
2
|
+
import { firefox } from 'playwright-core';
|
|
2
3
|
import fs from 'fs';
|
|
3
4
|
import path from 'path';
|
|
4
5
|
import chalk from 'chalk';
|
|
@@ -11,6 +12,9 @@ const GOOGLE_LOGGED_IN_INDICATOR = 'aistudio.google.com';
|
|
|
11
12
|
const LOGIN_CHECK_INTERVAL = 2 * 60 * 60 * 1000;
|
|
12
13
|
// 连接状态检查间隔: 30 秒
|
|
13
14
|
const CONNECTION_CHECK_INTERVAL = 30 * 1000;
|
|
15
|
+
// 用户活动模拟间隔: 2-5 秒随机
|
|
16
|
+
const ACTIVITY_INTERVAL_MIN = 2000;
|
|
17
|
+
const ACTIVITY_INTERVAL_MAX = 5000;
|
|
14
18
|
export class BrowserManager {
|
|
15
19
|
browser = null;
|
|
16
20
|
context = null;
|
|
@@ -21,6 +25,8 @@ export class BrowserManager {
|
|
|
21
25
|
isRunning = false;
|
|
22
26
|
loginCheckTimer = null;
|
|
23
27
|
connectionCheckTimer = null;
|
|
28
|
+
activityTimer = null;
|
|
29
|
+
wsConnected = false; // 通过控制台日志跟踪 WebSocket 连接状态
|
|
24
30
|
constructor(options) {
|
|
25
31
|
this.options = options;
|
|
26
32
|
}
|
|
@@ -30,16 +36,41 @@ export class BrowserManager {
|
|
|
30
36
|
async launch() {
|
|
31
37
|
// 获取随机指纹配置(同一 dataDir 会保持一致)
|
|
32
38
|
const fingerprint = getRandomFingerprint(this.options.dataDir);
|
|
33
|
-
|
|
34
|
-
this.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
const modeText = this.options.headless === 'virtual' ? '虚拟显示' : (this.options.headless ? '无头' : '有界面');
|
|
40
|
+
const browserType = this.options.usePlaywright ? 'Playwright Firefox' : 'Camoufox';
|
|
41
|
+
console.log(chalk.cyan(`[${new Date().toLocaleTimeString()}] 启动 ${browserType} (${modeText}模式)`));
|
|
42
|
+
console.log(chalk.gray(`[${new Date().toLocaleTimeString()}] 指纹配置: ${fingerprint.viewport.width}x${fingerprint.viewport.height}, ${fingerprint.locale}, ${fingerprint.timezoneId}`));
|
|
43
|
+
console.log(chalk.gray(`[${new Date().toLocaleTimeString()}] 数据目录: ${this.options.dataDir}`));
|
|
44
|
+
if (this.options.usePlaywright) {
|
|
45
|
+
// 使用原生 Playwright Firefox
|
|
46
|
+
// 对于 Playwright,virtual 模式也使用 headless(Playwright 的 headless 模式比较稳定)
|
|
47
|
+
const useHeadless = this.options.headless === true || this.options.headless === 'virtual';
|
|
48
|
+
this.browser = await firefox.launch({
|
|
49
|
+
headless: useHeadless,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// 使用 Camoufox
|
|
54
|
+
// 注意: headless 模式下可能有 TLS/JA3 指纹问题导致 WebSocket 连接失败
|
|
55
|
+
// 如果遇到问题,可以尝试 --playwright 选项使用原生 Firefox
|
|
56
|
+
this.browser = await Camoufox({
|
|
57
|
+
headless: this.options.headless,
|
|
58
|
+
// 禁用 Cross-Origin-Opener-Policy,允许操作跨域 iframe 中的元素
|
|
59
|
+
disable_coop: true,
|
|
60
|
+
// 允许在主世界执行脚本,用于 iframe 内的 evaluate
|
|
61
|
+
main_world_eval: true,
|
|
62
|
+
// 忽略 disable_coop 警告
|
|
63
|
+
i_know_what_im_doing: true,
|
|
64
|
+
// Firefox 偏好设置
|
|
65
|
+
firefox_user_prefs: {
|
|
66
|
+
// WebSocket 相关设置
|
|
67
|
+
'network.websocket.allowInsecureFromHTTPS': true,
|
|
68
|
+
'network.websocket.max-connections': 200,
|
|
69
|
+
'network.websocket.timeout.open': 60,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
console.log(chalk.green(`[${new Date().toLocaleTimeString()}] 浏览器启动成功`));
|
|
43
74
|
this.context = await this.browser.newContext({
|
|
44
75
|
viewport: fingerprint.viewport,
|
|
45
76
|
locale: fingerprint.locale,
|
|
@@ -48,6 +79,89 @@ export class BrowserManager {
|
|
|
48
79
|
// 加载已保存的 cookies
|
|
49
80
|
await this.loadCookies();
|
|
50
81
|
this.page = await this.context.newPage();
|
|
82
|
+
// 在 headless 模式下,强制页面和所有 iframe 保持 "visible" 状态
|
|
83
|
+
if (this.options.headless) {
|
|
84
|
+
const visibilityScript = `
|
|
85
|
+
// 覆盖 Page Visibility API
|
|
86
|
+
Object.defineProperty(document, 'visibilityState', {
|
|
87
|
+
get: () => 'visible',
|
|
88
|
+
configurable: true,
|
|
89
|
+
});
|
|
90
|
+
Object.defineProperty(document, 'hidden', {
|
|
91
|
+
get: () => false,
|
|
92
|
+
configurable: true,
|
|
93
|
+
});
|
|
94
|
+
// 阻止 visibilitychange 事件
|
|
95
|
+
document.addEventListener('visibilitychange', (e) => {
|
|
96
|
+
e.stopImmediatePropagation();
|
|
97
|
+
}, true);
|
|
98
|
+
// 模拟 focus 状态
|
|
99
|
+
Object.defineProperty(document, 'hasFocus', {
|
|
100
|
+
value: () => true,
|
|
101
|
+
configurable: true,
|
|
102
|
+
});
|
|
103
|
+
`;
|
|
104
|
+
// 主页面注入
|
|
105
|
+
await this.page.addInitScript(visibilityScript);
|
|
106
|
+
// 监听所有 frame 并注入
|
|
107
|
+
this.page.on('frameattached', async (frame) => {
|
|
108
|
+
try {
|
|
109
|
+
await frame.evaluate(visibilityScript);
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
// 忽略跨域 frame 错误
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
// 监听网络请求,用于调试
|
|
117
|
+
this.page.on('request', (request) => {
|
|
118
|
+
const url = request.url();
|
|
119
|
+
// 只显示 API 相关请求
|
|
120
|
+
if (url.includes('ProxyStreamedCall') || url.includes('generativelanguage')) {
|
|
121
|
+
console.log(chalk.cyan(`[${new Date().toLocaleTimeString()}] 📤 请求: ${request.method()} ${url.substring(0, 80)}...`));
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
this.page.on('response', (response) => {
|
|
125
|
+
const url = response.url();
|
|
126
|
+
// 只显示 API 相关响应
|
|
127
|
+
if (url.includes('ProxyStreamedCall') || url.includes('generativelanguage')) {
|
|
128
|
+
const status = response.status();
|
|
129
|
+
const statusColor = status >= 400 ? chalk.red : chalk.green;
|
|
130
|
+
console.log(statusColor(`[${new Date().toLocaleTimeString()}] 📥 响应: ${status} ${url.substring(0, 80)}...`));
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
this.page.on('requestfailed', (request) => {
|
|
134
|
+
const url = request.url();
|
|
135
|
+
if (url.includes('ProxyStreamedCall') || url.includes('generativelanguage')) {
|
|
136
|
+
console.log(chalk.red(`[${new Date().toLocaleTimeString()}] ❌ 请求失败: ${url.substring(0, 80)}... - ${request.failure()?.errorText}`));
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
// 监听控制台消息
|
|
140
|
+
this.page.on('console', (msg) => {
|
|
141
|
+
const text = msg.text();
|
|
142
|
+
const type = msg.type();
|
|
143
|
+
// 过滤掉不重要的警告
|
|
144
|
+
if (type === 'warning') {
|
|
145
|
+
if (text.includes('Content-Security-Policy') ||
|
|
146
|
+
text.includes('cookie') ||
|
|
147
|
+
text.includes('expires') ||
|
|
148
|
+
text.includes('No ID or name')) {
|
|
149
|
+
return; // 忽略这些警告
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// 跟踪 WebSocket 连接状态
|
|
153
|
+
if (text.includes('WebSocket Proxy: CONNECTED') || text.includes('WebSocket Proxy Status: CONNECTED')) {
|
|
154
|
+
this.wsConnected = true;
|
|
155
|
+
}
|
|
156
|
+
else if (text.includes('DISCONNECTED')) {
|
|
157
|
+
this.wsConnected = false;
|
|
158
|
+
}
|
|
159
|
+
// 只显示重要的日志
|
|
160
|
+
if (type === 'error' || text.includes('WebSocket') || text.includes('Proxy') || text.includes('request') || text.includes('Request')) {
|
|
161
|
+
const color = type === 'error' ? chalk.red : (type === 'warning' ? chalk.yellow : chalk.gray);
|
|
162
|
+
console.log(color(`[${new Date().toLocaleTimeString()}] 🖥️ 控制台[${type}]: ${text.substring(0, 150)}`));
|
|
163
|
+
}
|
|
164
|
+
});
|
|
51
165
|
this.startTime = Date.now();
|
|
52
166
|
this.isRunning = true;
|
|
53
167
|
}
|
|
@@ -138,7 +252,8 @@ export class BrowserManager {
|
|
|
138
252
|
async clickConnectButton() {
|
|
139
253
|
if (!this.page)
|
|
140
254
|
throw new Error('Browser not launched');
|
|
141
|
-
|
|
255
|
+
const timestamp = () => `[${new Date().toLocaleTimeString()}]`;
|
|
256
|
+
console.log(chalk.cyan(`${timestamp()} 开始查找连接按钮...`));
|
|
142
257
|
// 等待页面完全加载
|
|
143
258
|
await sleep(3000);
|
|
144
259
|
// 第一步:点击 "Continue to the app" 按钮(如果存在)
|
|
@@ -146,12 +261,12 @@ export class BrowserManager {
|
|
|
146
261
|
const continueButton = await this.page.waitForSelector('button:has-text("Continue to the app")', { timeout: 5000 });
|
|
147
262
|
if (continueButton) {
|
|
148
263
|
await continueButton.click();
|
|
149
|
-
console.log(chalk.green(
|
|
264
|
+
console.log(chalk.green(`${timestamp()} ✅ 点击了 "Continue to the app" 按钮`));
|
|
150
265
|
await sleep(5000); // 等待页面切换,给更多时间
|
|
151
266
|
}
|
|
152
267
|
}
|
|
153
268
|
catch (e) {
|
|
154
|
-
console.log(chalk.gray(
|
|
269
|
+
console.log(chalk.gray(`${timestamp()} 未找到 "Continue to the app" 按钮,继续...`));
|
|
155
270
|
}
|
|
156
271
|
let clicked = false;
|
|
157
272
|
let buildAppFrame = null;
|
|
@@ -166,7 +281,7 @@ export class BrowserManager {
|
|
|
166
281
|
try {
|
|
167
282
|
const modal = await this.page.$(selector);
|
|
168
283
|
if (modal) {
|
|
169
|
-
console.log(chalk.gray(
|
|
284
|
+
console.log(chalk.gray(`${timestamp()} 发现模态框: ${selector},尝试关闭...`));
|
|
170
285
|
await this.page.keyboard.press('Escape');
|
|
171
286
|
await sleep(500);
|
|
172
287
|
}
|
|
@@ -181,23 +296,24 @@ export class BrowserManager {
|
|
|
181
296
|
}
|
|
182
297
|
// 第三步:使用 frames() API 访问 Build App iframe
|
|
183
298
|
try {
|
|
184
|
-
console.log(chalk.
|
|
299
|
+
console.log(chalk.cyan(`${timestamp()} 查找 Build App iframe...`));
|
|
185
300
|
const frames = this.page.frames();
|
|
186
|
-
console.log(chalk.gray(
|
|
301
|
+
console.log(chalk.gray(`${timestamp()} 检查 ${frames.length} 个 frame...`));
|
|
187
302
|
for (const frame of frames) {
|
|
188
303
|
if (clicked)
|
|
189
304
|
break;
|
|
190
305
|
const frameUrl = frame.url();
|
|
191
306
|
// 查找 Build App iframe (可能是 blob: URL 或 scf.usercontent.goog)
|
|
192
307
|
if (frameUrl.includes('scf.usercontent.goog') || frameUrl.startsWith('blob:')) {
|
|
193
|
-
console.log(chalk.
|
|
308
|
+
console.log(chalk.green(`${timestamp()} 找到 Build App iframe: ${frameUrl.substring(0, 60)}...`));
|
|
194
309
|
buildAppFrame = frame;
|
|
195
310
|
// 等待 iframe 内容加载
|
|
196
311
|
try {
|
|
197
312
|
await frame.waitForLoadState('domcontentloaded');
|
|
313
|
+
console.log(chalk.gray(`${timestamp()} iframe 内容加载完成`));
|
|
198
314
|
}
|
|
199
315
|
catch (e) {
|
|
200
|
-
|
|
316
|
+
console.log(chalk.yellow(`${timestamp()} iframe 加载等待超时,继续...`));
|
|
201
317
|
}
|
|
202
318
|
// 直接使用 evaluate 在 iframe 中执行点击
|
|
203
319
|
// 这样可以绑过任何遮挡问题
|
|
@@ -217,11 +333,11 @@ export class BrowserManager {
|
|
|
217
333
|
return false;
|
|
218
334
|
});
|
|
219
335
|
if (clicked) {
|
|
220
|
-
console.log(chalk.green(
|
|
336
|
+
console.log(chalk.green(`${timestamp()} ✅ 点击了连接按钮 (dispatchEvent)`));
|
|
221
337
|
}
|
|
222
338
|
}
|
|
223
339
|
catch (e) {
|
|
224
|
-
console.log(chalk.
|
|
340
|
+
console.log(chalk.yellow(`${timestamp()} evaluate 点击失败: ${e}`));
|
|
225
341
|
}
|
|
226
342
|
// 如果 dispatchEvent 失败,尝试直接调用 onclick
|
|
227
343
|
if (!clicked) {
|
|
@@ -235,49 +351,82 @@ export class BrowserManager {
|
|
|
235
351
|
return false;
|
|
236
352
|
});
|
|
237
353
|
if (clicked) {
|
|
238
|
-
console.log(chalk.green(
|
|
354
|
+
console.log(chalk.green(`${timestamp()} ✅ 点击了连接按钮 (click())`));
|
|
239
355
|
}
|
|
240
356
|
}
|
|
241
357
|
catch (e) {
|
|
242
|
-
console.log(chalk.
|
|
358
|
+
console.log(chalk.yellow(`${timestamp()} click() 失败: ${e}`));
|
|
243
359
|
}
|
|
244
360
|
}
|
|
245
361
|
}
|
|
246
362
|
}
|
|
247
363
|
}
|
|
248
364
|
catch (e) {
|
|
249
|
-
console.log(chalk.
|
|
365
|
+
console.log(chalk.red(`${timestamp()} iframe 访问失败: ${e}`));
|
|
250
366
|
}
|
|
251
367
|
if (!clicked) {
|
|
252
|
-
console.log(chalk.yellow(
|
|
368
|
+
console.log(chalk.yellow(`${timestamp()} ⚠️ 未找到连接按钮,请手动点击连接`));
|
|
253
369
|
return;
|
|
254
370
|
}
|
|
255
|
-
//
|
|
256
|
-
console.log(chalk.
|
|
371
|
+
// 第四步:等待并验证连接成功(通过控制台日志检测)
|
|
372
|
+
console.log(chalk.cyan(`${timestamp()} 等待 WebSocket 连接...`));
|
|
257
373
|
let connected = false;
|
|
258
|
-
for (let i = 0; i <
|
|
374
|
+
for (let i = 0; i < 30; i++) { // 最多等待 30 秒
|
|
259
375
|
await sleep(1000);
|
|
376
|
+
// 通过控制台日志跟踪的状态判断
|
|
377
|
+
if (this.wsConnected) {
|
|
378
|
+
connected = true;
|
|
379
|
+
console.log(chalk.green(`${timestamp()} ✅ WebSocket 连接成功!`));
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
console.log(chalk.gray(`${timestamp()} 等待连接... (${i + 1}/30)`));
|
|
383
|
+
}
|
|
384
|
+
if (!connected) {
|
|
385
|
+
console.log(chalk.yellow(`${timestamp()} ⚠️ WebSocket 连接超时或失败`));
|
|
386
|
+
}
|
|
387
|
+
// 第五步:点击 "Launch!" 按钮(如果存在蒙层)
|
|
388
|
+
try {
|
|
389
|
+
console.log(chalk.cyan(`${timestamp()} 检查是否有 Launch 蒙层...`));
|
|
390
|
+
// 先在主页面查找
|
|
391
|
+
const launchButton = await this.page.waitForSelector('button:has-text("Launch")', { timeout: 3000 });
|
|
392
|
+
if (launchButton) {
|
|
393
|
+
await launchButton.click();
|
|
394
|
+
console.log(chalk.green(`${timestamp()} ✅ 点击了 "Launch!" 按钮`));
|
|
395
|
+
await sleep(1000);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
catch (e) {
|
|
399
|
+
// 在 iframe 中查找
|
|
260
400
|
if (buildAppFrame) {
|
|
261
401
|
try {
|
|
262
|
-
const
|
|
263
|
-
const btn = document.querySelector('button
|
|
264
|
-
|
|
402
|
+
const clicked = await buildAppFrame.evaluate(() => {
|
|
403
|
+
const btn = document.querySelector('button');
|
|
404
|
+
if (btn && btn.textContent?.includes('Launch')) {
|
|
405
|
+
btn.click();
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
// 查找所有按钮
|
|
409
|
+
const buttons = document.querySelectorAll('button');
|
|
410
|
+
for (const b of buttons) {
|
|
411
|
+
if (b.textContent?.includes('Launch')) {
|
|
412
|
+
b.click();
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return false;
|
|
265
417
|
});
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
418
|
+
if (clicked) {
|
|
419
|
+
console.log(chalk.green(`${timestamp()} ✅ 点击了 iframe 中的 "Launch!" 按钮`));
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
console.log(chalk.gray(`${timestamp()} 未找到 Launch 蒙层`));
|
|
271
423
|
}
|
|
272
424
|
}
|
|
273
|
-
catch (
|
|
274
|
-
|
|
425
|
+
catch (e2) {
|
|
426
|
+
console.log(chalk.gray(`${timestamp()} 未找到 Launch 蒙层`));
|
|
275
427
|
}
|
|
276
428
|
}
|
|
277
429
|
}
|
|
278
|
-
if (!connected) {
|
|
279
|
-
console.log(chalk.yellow('⚠️ WebSocket 连接可能未成功,请检查'));
|
|
280
|
-
}
|
|
281
430
|
}
|
|
282
431
|
/**
|
|
283
432
|
* 设置服务器地址
|
|
@@ -344,9 +493,91 @@ export class BrowserManager {
|
|
|
344
493
|
this.connectionCheckTimer = setInterval(async () => {
|
|
345
494
|
await this.checkConnection();
|
|
346
495
|
}, CONNECTION_CHECK_INTERVAL);
|
|
496
|
+
// 启动用户活动模拟(仅在 headless 或 virtual 模式下)
|
|
497
|
+
if (this.options.headless) {
|
|
498
|
+
this.startHumanActivity();
|
|
499
|
+
}
|
|
347
500
|
// 定期更新状态显示
|
|
348
501
|
this.startStatusDisplay();
|
|
349
502
|
}
|
|
503
|
+
/**
|
|
504
|
+
* 启动人类活动模拟
|
|
505
|
+
* 定期模拟鼠标移动和其他用户交互,防止页面因检测不到用户而暂停
|
|
506
|
+
*/
|
|
507
|
+
startHumanActivity() {
|
|
508
|
+
const scheduleNext = () => {
|
|
509
|
+
// 随机间隔 5-15 秒
|
|
510
|
+
const interval = ACTIVITY_INTERVAL_MIN + Math.random() * (ACTIVITY_INTERVAL_MAX - ACTIVITY_INTERVAL_MIN);
|
|
511
|
+
this.activityTimer = setTimeout(async () => {
|
|
512
|
+
await this.simulateHumanActivity();
|
|
513
|
+
if (this.isRunning) {
|
|
514
|
+
scheduleNext();
|
|
515
|
+
}
|
|
516
|
+
}, interval);
|
|
517
|
+
};
|
|
518
|
+
console.log(chalk.cyan(`[${new Date().toLocaleTimeString()}] 🖱️ 启动用户活动模拟...`));
|
|
519
|
+
scheduleNext();
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* 模拟人类活动
|
|
523
|
+
*/
|
|
524
|
+
async simulateHumanActivity() {
|
|
525
|
+
if (!this.page || !this.isRunning)
|
|
526
|
+
return;
|
|
527
|
+
try {
|
|
528
|
+
// 获取视口大小
|
|
529
|
+
const viewport = this.page.viewportSize();
|
|
530
|
+
if (!viewport)
|
|
531
|
+
return;
|
|
532
|
+
// 生成随机坐标(避开边缘区域)
|
|
533
|
+
const margin = 50;
|
|
534
|
+
const x = margin + Math.random() * (viewport.width - 2 * margin);
|
|
535
|
+
const y = margin + Math.random() * (viewport.height - 2 * margin);
|
|
536
|
+
// 随机选择活动类型
|
|
537
|
+
const activityType = Math.random();
|
|
538
|
+
if (activityType < 0.7) {
|
|
539
|
+
// 70% 概率:鼠标移动
|
|
540
|
+
await this.page.mouse.move(x, y, {
|
|
541
|
+
steps: 5 + Math.floor(Math.random() * 10), // 随机步数使移动更自然
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
else if (activityType < 0.9) {
|
|
545
|
+
// 20% 概率:鼠标移动 + 小幅度滚动
|
|
546
|
+
await this.page.mouse.move(x, y, { steps: 5 });
|
|
547
|
+
const scrollAmount = (Math.random() - 0.5) * 100; // -50 到 50
|
|
548
|
+
await this.page.mouse.wheel(0, scrollAmount);
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
// 10% 概率:在 Build App iframe 内模拟鼠标移动
|
|
552
|
+
const frames = this.page.frames();
|
|
553
|
+
for (const frame of frames) {
|
|
554
|
+
const frameUrl = frame.url();
|
|
555
|
+
if (frameUrl.includes('scf.usercontent.goog') || frameUrl.startsWith('blob:')) {
|
|
556
|
+
try {
|
|
557
|
+
// 在 iframe 内触发 mousemove 事件
|
|
558
|
+
await frame.evaluate(() => {
|
|
559
|
+
const event = new MouseEvent('mousemove', {
|
|
560
|
+
bubbles: true,
|
|
561
|
+
cancelable: true,
|
|
562
|
+
clientX: Math.random() * window.innerWidth,
|
|
563
|
+
clientY: Math.random() * window.innerHeight,
|
|
564
|
+
view: window
|
|
565
|
+
});
|
|
566
|
+
document.dispatchEvent(event);
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
catch (e) {
|
|
570
|
+
// iframe 可能已经不可访问,忽略
|
|
571
|
+
}
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
catch (error) {
|
|
578
|
+
// 忽略模拟活动时的错误,不影响主流程
|
|
579
|
+
}
|
|
580
|
+
}
|
|
350
581
|
/**
|
|
351
582
|
* 检查并刷新登录状态
|
|
352
583
|
*/
|
|
@@ -445,6 +676,10 @@ export class BrowserManager {
|
|
|
445
676
|
clearInterval(this.connectionCheckTimer);
|
|
446
677
|
this.connectionCheckTimer = null;
|
|
447
678
|
}
|
|
679
|
+
if (this.activityTimer) {
|
|
680
|
+
clearTimeout(this.activityTimer);
|
|
681
|
+
this.activityTimer = null;
|
|
682
|
+
}
|
|
448
683
|
if (this.browser) {
|
|
449
684
|
await this.browser.close();
|
|
450
685
|
this.browser = null;
|
package/dist/cli.js
CHANGED
|
@@ -27,6 +27,7 @@ program
|
|
|
27
27
|
.option('-d, --daemon', '后台运行模式')
|
|
28
28
|
.option('--headless', '强制无头模式 (可能有兼容性问题)')
|
|
29
29
|
.option('--virtual', '虚拟显示模式 (需要安装 xvfb,推荐)')
|
|
30
|
+
.option('--playwright', '使用原生 Playwright Firefox (解决 Camoufox WebSocket 问题)')
|
|
30
31
|
.option('--data-dir <path>', '数据目录路径', DEFAULT_DATA_DIR)
|
|
31
32
|
.action(async (options) => {
|
|
32
33
|
try {
|
package/dist/commands/start.js
CHANGED
|
@@ -5,6 +5,12 @@ import path from 'path';
|
|
|
5
5
|
import { execSync } from 'child_process';
|
|
6
6
|
import { BrowserManager } from '../browser/manager.js';
|
|
7
7
|
import { generateToken, ensureDataDir } from '../utils/helpers.js';
|
|
8
|
+
/**
|
|
9
|
+
* 检查是否是 macOS
|
|
10
|
+
*/
|
|
11
|
+
function isMacOS() {
|
|
12
|
+
return process.platform === 'darwin';
|
|
13
|
+
}
|
|
8
14
|
/**
|
|
9
15
|
* 检查 Camoufox 是否已安装
|
|
10
16
|
*/
|
|
@@ -67,28 +73,45 @@ export async function startClient(options) {
|
|
|
67
73
|
const hasCookies = fs.existsSync(cookiesPath);
|
|
68
74
|
// 决定使用哪种模式: false (有界面) / true (无头) / 'virtual' (虚拟显示)
|
|
69
75
|
let headless = options.headless ?? false;
|
|
76
|
+
let usePlaywright = options.playwright ?? false;
|
|
70
77
|
// 如果指定了 --virtual,使用虚拟显示模式
|
|
71
78
|
if (options.virtual) {
|
|
72
79
|
headless = 'virtual';
|
|
73
80
|
}
|
|
81
|
+
// macOS 上自动使用 Playwright(Camoufox headless 在 macOS 上有问题)
|
|
82
|
+
if (isMacOS() && !options.playwright && (options.headless || options.virtual)) {
|
|
83
|
+
console.log(chalk.yellow('⚠️ macOS 检测到,自动切换到 Playwright Firefox'));
|
|
84
|
+
usePlaywright = true;
|
|
85
|
+
headless = true; // macOS 上使用纯 headless
|
|
86
|
+
}
|
|
74
87
|
if (!hasCookies) {
|
|
75
88
|
console.log(chalk.yellow('⚠️ 未检测到 Google 登录状态'));
|
|
76
89
|
console.log(chalk.white(' 将打开浏览器,请登录您的 Google 账号...'));
|
|
77
90
|
headless = false;
|
|
91
|
+
usePlaywright = false; // 登录时使用 Camoufox 有界面模式
|
|
78
92
|
}
|
|
79
93
|
else if (!options.headless && !options.virtual) {
|
|
80
94
|
// 有 cookies,默认使用虚拟显示模式(如果可用)或无头模式
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
95
|
+
if (isMacOS()) {
|
|
96
|
+
headless = true;
|
|
97
|
+
usePlaywright = true;
|
|
98
|
+
console.log(chalk.green('📂 检测到已保存的登录状态'));
|
|
99
|
+
console.log(chalk.gray(' macOS: 使用 Playwright Firefox 无头模式'));
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
headless = 'virtual';
|
|
103
|
+
console.log(chalk.green('📂 检测到已保存的登录状态'));
|
|
104
|
+
console.log(chalk.gray(' 尝试使用虚拟显示模式 (需要 xvfb)'));
|
|
105
|
+
}
|
|
84
106
|
}
|
|
85
|
-
const spinner = ora('
|
|
107
|
+
const spinner = ora('启动浏览器...').start();
|
|
86
108
|
try {
|
|
87
109
|
let browserManager = new BrowserManager({
|
|
88
110
|
headless,
|
|
89
111
|
dataDir,
|
|
90
112
|
serverUrl: server,
|
|
91
113
|
token,
|
|
114
|
+
usePlaywright,
|
|
92
115
|
});
|
|
93
116
|
// 设置信号处理
|
|
94
117
|
setupSignalHandlers(browserManager);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gemini-proxy-client",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.19",
|
|
4
4
|
"description": "Gemini Proxy Build App 客户端 - 使用 Camoufox 自动保持连接",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"camoufox-js": "^0.8.5",
|
|
29
|
+
"playwright-core": "^1.40.0",
|
|
29
30
|
"commander": "^12.0.0",
|
|
30
31
|
"chalk": "^5.3.0",
|
|
31
32
|
"ora": "^8.0.1",
|