gemini-proxy-client 1.0.17 → 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 -38
- package/dist/cli.js +3 -1
- package/dist/commands/start.js +41 -12
- 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,14 +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
|
-
|
|
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()}] 浏览器启动成功`));
|
|
41
74
|
this.context = await this.browser.newContext({
|
|
42
75
|
viewport: fingerprint.viewport,
|
|
43
76
|
locale: fingerprint.locale,
|
|
@@ -46,6 +79,89 @@ export class BrowserManager {
|
|
|
46
79
|
// 加载已保存的 cookies
|
|
47
80
|
await this.loadCookies();
|
|
48
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
|
+
});
|
|
49
165
|
this.startTime = Date.now();
|
|
50
166
|
this.isRunning = true;
|
|
51
167
|
}
|
|
@@ -136,7 +252,8 @@ export class BrowserManager {
|
|
|
136
252
|
async clickConnectButton() {
|
|
137
253
|
if (!this.page)
|
|
138
254
|
throw new Error('Browser not launched');
|
|
139
|
-
|
|
255
|
+
const timestamp = () => `[${new Date().toLocaleTimeString()}]`;
|
|
256
|
+
console.log(chalk.cyan(`${timestamp()} 开始查找连接按钮...`));
|
|
140
257
|
// 等待页面完全加载
|
|
141
258
|
await sleep(3000);
|
|
142
259
|
// 第一步:点击 "Continue to the app" 按钮(如果存在)
|
|
@@ -144,12 +261,12 @@ export class BrowserManager {
|
|
|
144
261
|
const continueButton = await this.page.waitForSelector('button:has-text("Continue to the app")', { timeout: 5000 });
|
|
145
262
|
if (continueButton) {
|
|
146
263
|
await continueButton.click();
|
|
147
|
-
console.log(chalk.green(
|
|
264
|
+
console.log(chalk.green(`${timestamp()} ✅ 点击了 "Continue to the app" 按钮`));
|
|
148
265
|
await sleep(5000); // 等待页面切换,给更多时间
|
|
149
266
|
}
|
|
150
267
|
}
|
|
151
268
|
catch (e) {
|
|
152
|
-
console.log(chalk.gray(
|
|
269
|
+
console.log(chalk.gray(`${timestamp()} 未找到 "Continue to the app" 按钮,继续...`));
|
|
153
270
|
}
|
|
154
271
|
let clicked = false;
|
|
155
272
|
let buildAppFrame = null;
|
|
@@ -164,7 +281,7 @@ export class BrowserManager {
|
|
|
164
281
|
try {
|
|
165
282
|
const modal = await this.page.$(selector);
|
|
166
283
|
if (modal) {
|
|
167
|
-
console.log(chalk.gray(
|
|
284
|
+
console.log(chalk.gray(`${timestamp()} 发现模态框: ${selector},尝试关闭...`));
|
|
168
285
|
await this.page.keyboard.press('Escape');
|
|
169
286
|
await sleep(500);
|
|
170
287
|
}
|
|
@@ -179,23 +296,24 @@ export class BrowserManager {
|
|
|
179
296
|
}
|
|
180
297
|
// 第三步:使用 frames() API 访问 Build App iframe
|
|
181
298
|
try {
|
|
182
|
-
console.log(chalk.
|
|
299
|
+
console.log(chalk.cyan(`${timestamp()} 查找 Build App iframe...`));
|
|
183
300
|
const frames = this.page.frames();
|
|
184
|
-
console.log(chalk.gray(
|
|
301
|
+
console.log(chalk.gray(`${timestamp()} 检查 ${frames.length} 个 frame...`));
|
|
185
302
|
for (const frame of frames) {
|
|
186
303
|
if (clicked)
|
|
187
304
|
break;
|
|
188
305
|
const frameUrl = frame.url();
|
|
189
306
|
// 查找 Build App iframe (可能是 blob: URL 或 scf.usercontent.goog)
|
|
190
307
|
if (frameUrl.includes('scf.usercontent.goog') || frameUrl.startsWith('blob:')) {
|
|
191
|
-
console.log(chalk.
|
|
308
|
+
console.log(chalk.green(`${timestamp()} 找到 Build App iframe: ${frameUrl.substring(0, 60)}...`));
|
|
192
309
|
buildAppFrame = frame;
|
|
193
310
|
// 等待 iframe 内容加载
|
|
194
311
|
try {
|
|
195
312
|
await frame.waitForLoadState('domcontentloaded');
|
|
313
|
+
console.log(chalk.gray(`${timestamp()} iframe 内容加载完成`));
|
|
196
314
|
}
|
|
197
315
|
catch (e) {
|
|
198
|
-
|
|
316
|
+
console.log(chalk.yellow(`${timestamp()} iframe 加载等待超时,继续...`));
|
|
199
317
|
}
|
|
200
318
|
// 直接使用 evaluate 在 iframe 中执行点击
|
|
201
319
|
// 这样可以绑过任何遮挡问题
|
|
@@ -215,11 +333,11 @@ export class BrowserManager {
|
|
|
215
333
|
return false;
|
|
216
334
|
});
|
|
217
335
|
if (clicked) {
|
|
218
|
-
console.log(chalk.green(
|
|
336
|
+
console.log(chalk.green(`${timestamp()} ✅ 点击了连接按钮 (dispatchEvent)`));
|
|
219
337
|
}
|
|
220
338
|
}
|
|
221
339
|
catch (e) {
|
|
222
|
-
console.log(chalk.
|
|
340
|
+
console.log(chalk.yellow(`${timestamp()} evaluate 点击失败: ${e}`));
|
|
223
341
|
}
|
|
224
342
|
// 如果 dispatchEvent 失败,尝试直接调用 onclick
|
|
225
343
|
if (!clicked) {
|
|
@@ -233,49 +351,82 @@ export class BrowserManager {
|
|
|
233
351
|
return false;
|
|
234
352
|
});
|
|
235
353
|
if (clicked) {
|
|
236
|
-
console.log(chalk.green(
|
|
354
|
+
console.log(chalk.green(`${timestamp()} ✅ 点击了连接按钮 (click())`));
|
|
237
355
|
}
|
|
238
356
|
}
|
|
239
357
|
catch (e) {
|
|
240
|
-
console.log(chalk.
|
|
358
|
+
console.log(chalk.yellow(`${timestamp()} click() 失败: ${e}`));
|
|
241
359
|
}
|
|
242
360
|
}
|
|
243
361
|
}
|
|
244
362
|
}
|
|
245
363
|
}
|
|
246
364
|
catch (e) {
|
|
247
|
-
console.log(chalk.
|
|
365
|
+
console.log(chalk.red(`${timestamp()} iframe 访问失败: ${e}`));
|
|
248
366
|
}
|
|
249
367
|
if (!clicked) {
|
|
250
|
-
console.log(chalk.yellow(
|
|
368
|
+
console.log(chalk.yellow(`${timestamp()} ⚠️ 未找到连接按钮,请手动点击连接`));
|
|
251
369
|
return;
|
|
252
370
|
}
|
|
253
|
-
//
|
|
254
|
-
console.log(chalk.
|
|
371
|
+
// 第四步:等待并验证连接成功(通过控制台日志检测)
|
|
372
|
+
console.log(chalk.cyan(`${timestamp()} 等待 WebSocket 连接...`));
|
|
255
373
|
let connected = false;
|
|
256
|
-
for (let i = 0; i <
|
|
374
|
+
for (let i = 0; i < 30; i++) { // 最多等待 30 秒
|
|
257
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 中查找
|
|
258
400
|
if (buildAppFrame) {
|
|
259
401
|
try {
|
|
260
|
-
const
|
|
261
|
-
const btn = document.querySelector('button
|
|
262
|
-
|
|
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;
|
|
263
417
|
});
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
418
|
+
if (clicked) {
|
|
419
|
+
console.log(chalk.green(`${timestamp()} ✅ 点击了 iframe 中的 "Launch!" 按钮`));
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
console.log(chalk.gray(`${timestamp()} 未找到 Launch 蒙层`));
|
|
269
423
|
}
|
|
270
424
|
}
|
|
271
|
-
catch (
|
|
272
|
-
|
|
425
|
+
catch (e2) {
|
|
426
|
+
console.log(chalk.gray(`${timestamp()} 未找到 Launch 蒙层`));
|
|
273
427
|
}
|
|
274
428
|
}
|
|
275
429
|
}
|
|
276
|
-
if (!connected) {
|
|
277
|
-
console.log(chalk.yellow('⚠️ WebSocket 连接可能未成功,请检查'));
|
|
278
|
-
}
|
|
279
430
|
}
|
|
280
431
|
/**
|
|
281
432
|
* 设置服务器地址
|
|
@@ -342,9 +493,91 @@ export class BrowserManager {
|
|
|
342
493
|
this.connectionCheckTimer = setInterval(async () => {
|
|
343
494
|
await this.checkConnection();
|
|
344
495
|
}, CONNECTION_CHECK_INTERVAL);
|
|
496
|
+
// 启动用户活动模拟(仅在 headless 或 virtual 模式下)
|
|
497
|
+
if (this.options.headless) {
|
|
498
|
+
this.startHumanActivity();
|
|
499
|
+
}
|
|
345
500
|
// 定期更新状态显示
|
|
346
501
|
this.startStatusDisplay();
|
|
347
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
|
+
}
|
|
348
581
|
/**
|
|
349
582
|
* 检查并刷新登录状态
|
|
350
583
|
*/
|
|
@@ -443,6 +676,10 @@ export class BrowserManager {
|
|
|
443
676
|
clearInterval(this.connectionCheckTimer);
|
|
444
677
|
this.connectionCheckTimer = null;
|
|
445
678
|
}
|
|
679
|
+
if (this.activityTimer) {
|
|
680
|
+
clearTimeout(this.activityTimer);
|
|
681
|
+
this.activityTimer = null;
|
|
682
|
+
}
|
|
446
683
|
if (this.browser) {
|
|
447
684
|
await this.browser.close();
|
|
448
685
|
this.browser = null;
|
package/dist/cli.js
CHANGED
|
@@ -25,7 +25,9 @@ program
|
|
|
25
25
|
.option('-s, --server <url>', '代理服务器 WebSocket 地址', 'ws://localhost:5345/v1/ws')
|
|
26
26
|
.option('-t, --token <token>', '客户端认证令牌 (不指定则自动生成)')
|
|
27
27
|
.option('-d, --daemon', '后台运行模式')
|
|
28
|
-
.option('--headless', '强制无头模式 (
|
|
28
|
+
.option('--headless', '强制无头模式 (可能有兼容性问题)')
|
|
29
|
+
.option('--virtual', '虚拟显示模式 (需要安装 xvfb,推荐)')
|
|
30
|
+
.option('--playwright', '使用原生 Playwright Firefox (解决 Camoufox WebSocket 问题)')
|
|
29
31
|
.option('--data-dir <path>', '数据目录路径', DEFAULT_DATA_DIR)
|
|
30
32
|
.action(async (options) => {
|
|
31
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
|
*/
|
|
@@ -65,25 +71,47 @@ export async function startClient(options) {
|
|
|
65
71
|
console.log(chalk.gray('─'.repeat(50)));
|
|
66
72
|
const cookiesPath = path.join(dataDir, 'cookies.json');
|
|
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;
|
|
77
|
+
// 如果指定了 --virtual,使用虚拟显示模式
|
|
78
|
+
if (options.virtual) {
|
|
79
|
+
headless = 'virtual';
|
|
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
|
+
}
|
|
70
87
|
if (!hasCookies) {
|
|
71
88
|
console.log(chalk.yellow('⚠️ 未检测到 Google 登录状态'));
|
|
72
89
|
console.log(chalk.white(' 将打开浏览器,请登录您的 Google 账号...'));
|
|
73
90
|
headless = false;
|
|
91
|
+
usePlaywright = false; // 登录时使用 Camoufox 有界面模式
|
|
74
92
|
}
|
|
75
|
-
else if (!options.headless) {
|
|
76
|
-
// 有 cookies
|
|
77
|
-
|
|
78
|
-
|
|
93
|
+
else if (!options.headless && !options.virtual) {
|
|
94
|
+
// 有 cookies,默认使用虚拟显示模式(如果可用)或无头模式
|
|
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
|
+
}
|
|
79
106
|
}
|
|
80
|
-
const spinner = ora('
|
|
107
|
+
const spinner = ora('启动浏览器...').start();
|
|
81
108
|
try {
|
|
82
109
|
let browserManager = new BrowserManager({
|
|
83
110
|
headless,
|
|
84
111
|
dataDir,
|
|
85
112
|
serverUrl: server,
|
|
86
113
|
token,
|
|
114
|
+
usePlaywright,
|
|
87
115
|
});
|
|
88
116
|
// 设置信号处理
|
|
89
117
|
setupSignalHandlers(browserManager);
|
|
@@ -103,19 +131,19 @@ export async function startClient(options) {
|
|
|
103
131
|
// 保存 cookies
|
|
104
132
|
await browserManager.saveCookies();
|
|
105
133
|
console.log(chalk.green('✅ Google 登录成功!已保存登录状态'));
|
|
106
|
-
//
|
|
107
|
-
console.log(chalk.gray('🔄
|
|
134
|
+
// 关闭有界面的浏览器,以虚拟显示模式重新启动
|
|
135
|
+
console.log(chalk.gray('🔄 切换到虚拟显示模式...'));
|
|
108
136
|
await browserManager.close();
|
|
109
|
-
//
|
|
137
|
+
// 创建新的虚拟显示模式浏览器实例
|
|
110
138
|
browserManager = new BrowserManager({
|
|
111
|
-
headless:
|
|
139
|
+
headless: 'virtual',
|
|
112
140
|
dataDir,
|
|
113
141
|
serverUrl: server,
|
|
114
142
|
token,
|
|
115
143
|
});
|
|
116
144
|
// 重新设置信号处理
|
|
117
145
|
setupSignalHandlers(browserManager);
|
|
118
|
-
spinner.start('
|
|
146
|
+
spinner.start('以虚拟显示模式重新启动...');
|
|
119
147
|
await browserManager.launch();
|
|
120
148
|
spinner.text = '打开 Build App 页面...';
|
|
121
149
|
await browserManager.openBuildApp();
|
|
@@ -124,8 +152,9 @@ export async function startClient(options) {
|
|
|
124
152
|
spinner.text = '点击 ws connect 按钮...';
|
|
125
153
|
await browserManager.clickConnectButton();
|
|
126
154
|
spinner.succeed(chalk.green('已连接到代理服务器!'));
|
|
155
|
+
const modeText = headless === 'virtual' ? '虚拟显示模式' : (headless ? '无头模式' : '有界面模式');
|
|
127
156
|
console.log(chalk.gray('─'.repeat(50)));
|
|
128
|
-
console.log(chalk.green(`✅ 客户端已启动 (
|
|
157
|
+
console.log(chalk.green(`✅ 客户端已启动 (${modeText})`));
|
|
129
158
|
console.log(chalk.white(` Token: ${token}`));
|
|
130
159
|
console.log(chalk.gray(' 按 Ctrl+C 退出'));
|
|
131
160
|
console.log(chalk.gray('─'.repeat(50)));
|
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",
|