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.
@@ -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
- 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
- // 忽略 disable_coop 警告
41
- i_know_what_im_doing: true,
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
- console.log(chalk.gray('开始查找连接按钮...'));
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('✅ 点击了 "Continue to the app" 按钮'));
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('未找到 "Continue to the app" 按钮,继续...'));
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(`发现模态框: ${selector},尝试关闭...`));
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.gray('查找 Build App iframe...'));
299
+ console.log(chalk.cyan(`${timestamp()} 查找 Build App iframe...`));
185
300
  const frames = this.page.frames();
186
- console.log(chalk.gray(`检查 ${frames.length} 个 frame...`));
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.gray(`找到 Build App iframe: ${frameUrl.substring(0, 60)}...`));
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.gray(`evaluate 点击失败: ${e}`));
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('✅ 点击了连接按钮 (click())'));
354
+ console.log(chalk.green(`${timestamp()} ✅ 点击了连接按钮 (click())`));
239
355
  }
240
356
  }
241
357
  catch (e) {
242
- console.log(chalk.gray(`click() 失败: ${e}`));
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.gray(`iframe 访问失败: ${e}`));
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
- // 第四步:等待并验证连接成功(按钮文字变成 "Disconnect WS")
256
- console.log(chalk.gray('等待 WebSocket 连接...'));
371
+ // 第四步:等待并验证连接成功(通过控制台日志检测)
372
+ console.log(chalk.cyan(`${timestamp()} 等待 WebSocket 连接...`));
257
373
  let connected = false;
258
- for (let i = 0; i < 15; i++) { // 最多等待 15
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 buttonText = await buildAppFrame.evaluate(() => {
263
- const btn = document.querySelector('button[title="Connect WebSocket Proxy"], button[title="Disconnect WebSocket Proxy"]');
264
- 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;
265
417
  });
266
- console.log(chalk.gray(` 检测按钮文字: "${buttonText}"`));
267
- if (buttonText.includes('Disconnect')) {
268
- connected = true;
269
- console.log(chalk.green('✅ WebSocket 连接成功!'));
270
- break;
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 (e) {
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 {
@@ -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
- headless = 'virtual';
82
- console.log(chalk.green('📂 检测到已保存的登录状态'));
83
- console.log(chalk.gray(' 尝试使用虚拟显示模式 (需要 xvfb)'));
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('启动 Camoufox 浏览器...').start();
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.18",
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",