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.
@@ -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
- console.log(chalk.gray(`指纹配置: ${fingerprint.viewport.width}x${fingerprint.viewport.height}, ${fingerprint.locale}, ${fingerprint.timezoneId}`));
34
- this.browser = await Camoufox({
35
- headless: this.options.headless,
36
- // 禁用 Cross-Origin-Opener-Policy,允许操作跨域 iframe 中的元素
37
- disable_coop: true,
38
- // 允许在主世界执行脚本,用于 iframe 内的 evaluate
39
- main_world_eval: true,
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
- console.log(chalk.gray('开始查找连接按钮...'));
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('✅ 点击了 "Continue to the app" 按钮'));
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('未找到 "Continue to the app" 按钮,继续...'));
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(`发现模态框: ${selector},尝试关闭...`));
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.gray('查找 Build App iframe...'));
299
+ console.log(chalk.cyan(`${timestamp()} 查找 Build App iframe...`));
183
300
  const frames = this.page.frames();
184
- console.log(chalk.gray(`检查 ${frames.length} 个 frame...`));
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.gray(`找到 Build App iframe: ${frameUrl.substring(0, 60)}...`));
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.gray(`evaluate 点击失败: ${e}`));
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('✅ 点击了连接按钮 (click())'));
354
+ console.log(chalk.green(`${timestamp()} ✅ 点击了连接按钮 (click())`));
237
355
  }
238
356
  }
239
357
  catch (e) {
240
- console.log(chalk.gray(`click() 失败: ${e}`));
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.gray(`iframe 访问失败: ${e}`));
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
- // 第四步:等待并验证连接成功(按钮文字变成 "Disconnect WS")
254
- console.log(chalk.gray('等待 WebSocket 连接...'));
371
+ // 第四步:等待并验证连接成功(通过控制台日志检测)
372
+ console.log(chalk.cyan(`${timestamp()} 等待 WebSocket 连接...`));
255
373
  let connected = false;
256
- for (let i = 0; i < 15; i++) { // 最多等待 15
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 buttonText = await buildAppFrame.evaluate(() => {
261
- const btn = document.querySelector('button[title="Connect WebSocket Proxy"], button[title="Disconnect WebSocket Proxy"]');
262
- return btn?.textContent?.trim() || '';
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
- console.log(chalk.gray(` 检测按钮文字: "${buttonText}"`));
265
- if (buttonText.includes('Disconnect')) {
266
- connected = true;
267
- console.log(chalk.green('✅ WebSocket 连接成功!'));
268
- break;
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 (e) {
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 {
@@ -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
- headless = true;
78
- console.log(chalk.green('📂 检测到已保存的登录状态'));
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('启动 Camoufox 浏览器...').start();
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: true,
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.17",
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",