gemini-proxy-client 1.0.0
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/README.md +172 -0
- package/dist/browser/manager.js +346 -0
- package/dist/cli.js +73 -0
- package/dist/commands/login.js +40 -0
- package/dist/commands/start.js +96 -0
- package/dist/commands/status.js +36 -0
- package/dist/utils/helpers.js +57 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Gemini Proxy Client
|
|
2
|
+
|
|
3
|
+
基于 Camoufox 的 Gemini Proxy 客户端工具,用于自动化连接和保活 Build App。
|
|
4
|
+
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- **反指纹检测**: 使用 Camoufox (基于 Firefox) 防止 Google 检测多账号
|
|
8
|
+
- **自动登录管理**: 首次需要手动登录,之后自动保持登录状态
|
|
9
|
+
- **自动保活**: 每 2 小时检查登录状态,断线自动重连
|
|
10
|
+
- **无头运行**: 登录后可在后台无头运行,节省资源
|
|
11
|
+
|
|
12
|
+
## 安装
|
|
13
|
+
|
|
14
|
+
### 方式 1: 从 npm 安装(推荐)
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install -g gemini-proxy-client
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
安装时会自动下载 Camoufox 浏览器(约 200MB)。如果下载失败,手动执行:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx camoufox-js fetch
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### 方式 2: 从源码安装
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
git clone https://github.com/user/gemini-proxy-panel.git
|
|
30
|
+
cd gemini-proxy-panel/client
|
|
31
|
+
npm install
|
|
32
|
+
npm run build
|
|
33
|
+
npm link # 可选,全局安装
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 使用方法
|
|
37
|
+
|
|
38
|
+
### 1. 启动客户端
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# 连接到代理服务器
|
|
42
|
+
gemini-client start --server wss://your-server.com/v1/ws
|
|
43
|
+
|
|
44
|
+
# 或使用简写
|
|
45
|
+
gemini-client start -s wss://your-server.com/v1/ws
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**首次运行**会打开浏览器窗口,请登录您的 Google 账号。登录成功后,cookies 会自动保存,之后运行将使用无头模式。
|
|
49
|
+
|
|
50
|
+
### 2. 指定 Token
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# 不指定 token 会自动生成 gp_client_YYYY_MM_DD_HH_mm_ss 格式
|
|
54
|
+
gemini-client start -s wss://your-server.com/v1/ws
|
|
55
|
+
|
|
56
|
+
# 指定自定义 token
|
|
57
|
+
gemini-client start -s wss://your-server.com/v1/ws -t my-custom-token
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 3. 其他命令
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# 单独登录 Google 账号
|
|
64
|
+
gemini-client login
|
|
65
|
+
|
|
66
|
+
# 查看状态
|
|
67
|
+
gemini-client status
|
|
68
|
+
|
|
69
|
+
# 清除登录状态
|
|
70
|
+
gemini-client logout
|
|
71
|
+
|
|
72
|
+
# 查看帮助
|
|
73
|
+
gemini-client --help
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## 命令参考
|
|
77
|
+
|
|
78
|
+
### start - 启动客户端
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
gemini-client start [options]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
| 选项 | 说明 | 默认值 |
|
|
85
|
+
|------|------|--------|
|
|
86
|
+
| `-s, --server <url>` | 代理服务器 WebSocket 地址 | `ws://localhost:5345/v1/ws` |
|
|
87
|
+
| `-t, --token <token>` | 客户端认证令牌 | 自动生成 |
|
|
88
|
+
| `-d, --daemon` | 后台运行模式 | false |
|
|
89
|
+
| `--headless` | 强制无头模式 | 自动判断 |
|
|
90
|
+
| `--data-dir <path>` | 数据目录路径 | `~/.gemini-client` |
|
|
91
|
+
|
|
92
|
+
### login - 登录 Google
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
gemini-client login [options]
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
打开浏览器让用户登录 Google 账号,保存 cookies 供后续使用。
|
|
99
|
+
|
|
100
|
+
### status - 查看状态
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
gemini-client status [options]
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
显示当前登录状态和配置信息。
|
|
107
|
+
|
|
108
|
+
### logout - 清除登录
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
gemini-client logout [options]
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
清除保存的 Google 登录 cookies。
|
|
115
|
+
|
|
116
|
+
## 数据存储
|
|
117
|
+
|
|
118
|
+
默认数据目录: `~/.gemini-client/`
|
|
119
|
+
|
|
120
|
+
| 文件 | 说明 |
|
|
121
|
+
|------|------|
|
|
122
|
+
| `cookies.json` | Google 登录 cookies |
|
|
123
|
+
| `config.json` | 配置信息 |
|
|
124
|
+
|
|
125
|
+
## 保活机制
|
|
126
|
+
|
|
127
|
+
- **登录状态检查**: 每 2 小时检查一次 Google 登录状态
|
|
128
|
+
- **连接状态检查**: 每 30 秒检查一次 WebSocket 连接
|
|
129
|
+
- **自动重连**: 连接断开时自动尝试重新连接
|
|
130
|
+
- **登录过期处理**: 登录过期时会弹出浏览器让用户重新登录
|
|
131
|
+
|
|
132
|
+
## 系统要求
|
|
133
|
+
|
|
134
|
+
- Node.js >= 18.0.0
|
|
135
|
+
- 能够访问 GitHub(下载 Camoufox 浏览器)
|
|
136
|
+
- 能够访问 Google AI Studio(非中国大陆 IP)
|
|
137
|
+
|
|
138
|
+
## 常见问题
|
|
139
|
+
|
|
140
|
+
### Q: Camoufox 下载失败怎么办?
|
|
141
|
+
|
|
142
|
+
A: 可能是网络问题,尝试使用代理或手动下载:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# 设置代理
|
|
146
|
+
export https_proxy=http://127.0.0.1:7890
|
|
147
|
+
|
|
148
|
+
# 重新下载
|
|
149
|
+
npx camoufox-js fetch
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Q: Google 登录状态能保持多久?
|
|
153
|
+
|
|
154
|
+
A: 通常 1-2 周,但可能因以下原因提前失效:
|
|
155
|
+
- IP 地址变化
|
|
156
|
+
- 长时间无活动
|
|
157
|
+
- Google 安全检测
|
|
158
|
+
|
|
159
|
+
客户端会每 2 小时检查一次,失效时会提示重新登录。
|
|
160
|
+
|
|
161
|
+
### Q: 可以同时运行多个客户端吗?
|
|
162
|
+
|
|
163
|
+
A: 可以,使用不同的 `--data-dir` 参数:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
gemini-client start -s wss://server.com/v1/ws --data-dir ~/.gemini-client-1
|
|
167
|
+
gemini-client start -s wss://server.com/v1/ws --data-dir ~/.gemini-client-2
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { Camoufox } from 'camoufox-js';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { sleep, formatUptime } from '../utils/helpers.js';
|
|
6
|
+
// Build App URL
|
|
7
|
+
const BUILD_APP_URL = 'https://aistudio.google.com/apps/drive/1JqM2cAKs9japdW99aJwkg2k6GZSeCSHR?showPreview=true&pli=1&showAssistant=true';
|
|
8
|
+
const GOOGLE_LOGIN_URL = 'https://accounts.google.com/';
|
|
9
|
+
const GOOGLE_LOGGED_IN_INDICATOR = 'aistudio.google.com';
|
|
10
|
+
// 登录检查间隔: 2 小时
|
|
11
|
+
const LOGIN_CHECK_INTERVAL = 2 * 60 * 60 * 1000;
|
|
12
|
+
// 连接状态检查间隔: 30 秒
|
|
13
|
+
const CONNECTION_CHECK_INTERVAL = 30 * 1000;
|
|
14
|
+
export class BrowserManager {
|
|
15
|
+
browser = null;
|
|
16
|
+
context = null;
|
|
17
|
+
page = null;
|
|
18
|
+
options;
|
|
19
|
+
startTime = 0;
|
|
20
|
+
requestCount = 0;
|
|
21
|
+
isRunning = false;
|
|
22
|
+
loginCheckTimer = null;
|
|
23
|
+
connectionCheckTimer = null;
|
|
24
|
+
constructor(options) {
|
|
25
|
+
this.options = options;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 启动浏览器
|
|
29
|
+
*/
|
|
30
|
+
async launch() {
|
|
31
|
+
this.browser = await Camoufox({
|
|
32
|
+
headless: this.options.headless,
|
|
33
|
+
});
|
|
34
|
+
this.context = await this.browser.newContext({
|
|
35
|
+
viewport: { width: 1280, height: 800 },
|
|
36
|
+
locale: 'en-US',
|
|
37
|
+
});
|
|
38
|
+
// 加载已保存的 cookies
|
|
39
|
+
await this.loadCookies();
|
|
40
|
+
this.page = await this.context.newPage();
|
|
41
|
+
this.startTime = Date.now();
|
|
42
|
+
this.isRunning = true;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 打开 Build App 页面
|
|
46
|
+
*/
|
|
47
|
+
async openBuildApp() {
|
|
48
|
+
if (!this.page)
|
|
49
|
+
throw new Error('Browser not launched');
|
|
50
|
+
// 构建带 token 的 URL (如果 Build App 支持)
|
|
51
|
+
// 注意: 实际的 ws connect 按钮会在页面内处理连接
|
|
52
|
+
await this.page.goto(BUILD_APP_URL, {
|
|
53
|
+
waitUntil: 'networkidle',
|
|
54
|
+
timeout: 60000,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 打开 Google 登录页面
|
|
59
|
+
*/
|
|
60
|
+
async openGoogleLogin() {
|
|
61
|
+
if (!this.page)
|
|
62
|
+
throw new Error('Browser not launched');
|
|
63
|
+
await this.page.goto(GOOGLE_LOGIN_URL, {
|
|
64
|
+
waitUntil: 'networkidle',
|
|
65
|
+
timeout: 60000,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 检查是否已登录 Google
|
|
70
|
+
*/
|
|
71
|
+
async checkGoogleLogin() {
|
|
72
|
+
if (!this.page)
|
|
73
|
+
return false;
|
|
74
|
+
try {
|
|
75
|
+
// 检查页面 URL 是否在 AI Studio 域名下且没有登录重定向
|
|
76
|
+
const url = this.page.url();
|
|
77
|
+
// 如果在 accounts.google.com,说明需要登录
|
|
78
|
+
if (url.includes('accounts.google.com')) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
// 如果在 AI Studio,检查是否有登录相关的元素
|
|
82
|
+
if (url.includes(GOOGLE_LOGGED_IN_INDICATOR)) {
|
|
83
|
+
// 检查是否有用户头像或登录状态指示器
|
|
84
|
+
const isLoggedIn = await this.page.evaluate(() => {
|
|
85
|
+
// 检查是否有登录提示或错误
|
|
86
|
+
const loginPrompt = document.querySelector('[data-login-required]');
|
|
87
|
+
if (loginPrompt)
|
|
88
|
+
return false;
|
|
89
|
+
// 检查是否有用户相关元素
|
|
90
|
+
const userAvatar = document.querySelector('img[alt*="Account"], img[alt*="Profile"]');
|
|
91
|
+
return !!userAvatar || !document.body.textContent?.includes('Sign in');
|
|
92
|
+
});
|
|
93
|
+
return isLoggedIn;
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
console.error('检查登录状态失败:', error);
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* 等待用户完成 Google 登录
|
|
104
|
+
*/
|
|
105
|
+
async waitForGoogleLogin(timeout = 300000) {
|
|
106
|
+
if (!this.page)
|
|
107
|
+
throw new Error('Browser not launched');
|
|
108
|
+
const startTime = Date.now();
|
|
109
|
+
while (Date.now() - startTime < timeout) {
|
|
110
|
+
// 检查是否已经在 AI Studio 页面
|
|
111
|
+
const url = this.page.url();
|
|
112
|
+
if (url.includes(GOOGLE_LOGGED_IN_INDICATOR) && !url.includes('accounts.google.com')) {
|
|
113
|
+
// 额外等待页面加载完成
|
|
114
|
+
await sleep(2000);
|
|
115
|
+
// 再次确认登录状态
|
|
116
|
+
const isLoggedIn = await this.checkGoogleLogin();
|
|
117
|
+
if (isLoggedIn) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
await sleep(1000);
|
|
122
|
+
}
|
|
123
|
+
throw new Error('登录超时');
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* 点击 ws connect 按钮
|
|
127
|
+
*/
|
|
128
|
+
async clickConnectButton() {
|
|
129
|
+
if (!this.page)
|
|
130
|
+
throw new Error('Browser not launched');
|
|
131
|
+
// 等待页面完全加载
|
|
132
|
+
await sleep(2000);
|
|
133
|
+
// 查找并修改 WebSocket 连接地址 (如果需要)
|
|
134
|
+
// 这取决于 Build App 的实现方式
|
|
135
|
+
// 尝试多种方式找到连接按钮
|
|
136
|
+
const buttonSelectors = [
|
|
137
|
+
'button:has-text("ws connect")',
|
|
138
|
+
'button:has-text("connect")',
|
|
139
|
+
'[data-action="connect"]',
|
|
140
|
+
'.connect-button',
|
|
141
|
+
];
|
|
142
|
+
let clicked = false;
|
|
143
|
+
for (const selector of buttonSelectors) {
|
|
144
|
+
try {
|
|
145
|
+
const button = await this.page.$(selector);
|
|
146
|
+
if (button) {
|
|
147
|
+
// 先尝试设置服务器地址 (如果有输入框)
|
|
148
|
+
await this.setServerAddress();
|
|
149
|
+
await button.click();
|
|
150
|
+
clicked = true;
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
// 继续尝试下一个选择器
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (!clicked) {
|
|
159
|
+
// 如果找不到按钮,可能需要通过其他方式触发连接
|
|
160
|
+
// 尝试在页面中执行 JavaScript
|
|
161
|
+
await this.page.evaluate(({ serverUrl, token }) => {
|
|
162
|
+
// 尝试找到全局的连接函数或 WebSocket 实例
|
|
163
|
+
const wsUrl = `${serverUrl}?auth_token=${token}`;
|
|
164
|
+
console.log('Attempting to connect to:', wsUrl);
|
|
165
|
+
// 这里的实现取决于 Build App 的具体代码结构
|
|
166
|
+
// 可能需要根据实际情况调整
|
|
167
|
+
}, { serverUrl: this.options.serverUrl, token: this.options.token });
|
|
168
|
+
console.log(chalk.yellow('⚠️ 未找到连接按钮,请手动点击连接'));
|
|
169
|
+
}
|
|
170
|
+
// 等待连接建立
|
|
171
|
+
await sleep(3000);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* 设置服务器地址
|
|
175
|
+
*/
|
|
176
|
+
async setServerAddress() {
|
|
177
|
+
if (!this.page)
|
|
178
|
+
return;
|
|
179
|
+
try {
|
|
180
|
+
// 查找服务器地址输入框
|
|
181
|
+
const inputSelectors = [
|
|
182
|
+
'input[placeholder*="server"]',
|
|
183
|
+
'input[placeholder*="ws://"]',
|
|
184
|
+
'input[name="server"]',
|
|
185
|
+
'#server-url',
|
|
186
|
+
];
|
|
187
|
+
for (const selector of inputSelectors) {
|
|
188
|
+
const input = await this.page.$(selector);
|
|
189
|
+
if (input) {
|
|
190
|
+
await input.fill(`${this.options.serverUrl}?auth_token=${this.options.token}`);
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
// 忽略错误
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* 保存 Cookies
|
|
201
|
+
*/
|
|
202
|
+
async saveCookies() {
|
|
203
|
+
if (!this.context)
|
|
204
|
+
return;
|
|
205
|
+
const cookies = await this.context.cookies();
|
|
206
|
+
const cookiesPath = path.join(this.options.dataDir, 'cookies.json');
|
|
207
|
+
fs.writeFileSync(cookiesPath, JSON.stringify(cookies, null, 2));
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* 加载 Cookies
|
|
211
|
+
*/
|
|
212
|
+
async loadCookies() {
|
|
213
|
+
if (!this.context)
|
|
214
|
+
return;
|
|
215
|
+
const cookiesPath = path.join(this.options.dataDir, 'cookies.json');
|
|
216
|
+
if (fs.existsSync(cookiesPath)) {
|
|
217
|
+
try {
|
|
218
|
+
const cookies = JSON.parse(fs.readFileSync(cookiesPath, 'utf-8'));
|
|
219
|
+
await this.context.addCookies(cookies);
|
|
220
|
+
}
|
|
221
|
+
catch (e) {
|
|
222
|
+
console.error('加载 Cookies 失败:', e);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* 启动保活机制
|
|
228
|
+
*/
|
|
229
|
+
async startKeepAlive() {
|
|
230
|
+
// 登录状态检查 (每 2 小时)
|
|
231
|
+
this.loginCheckTimer = setInterval(async () => {
|
|
232
|
+
await this.checkAndRefreshLogin();
|
|
233
|
+
}, LOGIN_CHECK_INTERVAL);
|
|
234
|
+
// 连接状态检查 (每 30 秒)
|
|
235
|
+
this.connectionCheckTimer = setInterval(async () => {
|
|
236
|
+
await this.checkConnection();
|
|
237
|
+
}, CONNECTION_CHECK_INTERVAL);
|
|
238
|
+
// 定期更新状态显示
|
|
239
|
+
this.startStatusDisplay();
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* 检查并刷新登录状态
|
|
243
|
+
*/
|
|
244
|
+
async checkAndRefreshLogin() {
|
|
245
|
+
if (!this.page || !this.isRunning)
|
|
246
|
+
return;
|
|
247
|
+
console.log(chalk.gray(`\n[${new Date().toLocaleTimeString()}] 检查 Google 登录状态...`));
|
|
248
|
+
const isLoggedIn = await this.checkGoogleLogin();
|
|
249
|
+
if (!isLoggedIn) {
|
|
250
|
+
console.log(chalk.red('⚠️ Google 登录已失效!'));
|
|
251
|
+
console.log(chalk.yellow(' 需要重新登录,正在打开浏览器...'));
|
|
252
|
+
// 切换到有界面模式重新登录
|
|
253
|
+
// 这里需要重启浏览器
|
|
254
|
+
await this.handleLoginExpired();
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
console.log(chalk.green('✅ Google 登录状态正常'));
|
|
258
|
+
// 刷新 cookies
|
|
259
|
+
await this.saveCookies();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* 处理登录过期
|
|
264
|
+
*/
|
|
265
|
+
async handleLoginExpired() {
|
|
266
|
+
// 关闭当前浏览器
|
|
267
|
+
await this.close();
|
|
268
|
+
// 以有界面模式重新启动
|
|
269
|
+
this.options.headless = false;
|
|
270
|
+
await this.launch();
|
|
271
|
+
await this.openBuildApp();
|
|
272
|
+
console.log(chalk.yellow('\n🔐 请在浏览器中重新登录 Google 账号...'));
|
|
273
|
+
await this.waitForGoogleLogin();
|
|
274
|
+
await this.saveCookies();
|
|
275
|
+
console.log(chalk.green('✅ 重新登录成功!'));
|
|
276
|
+
// 重新连接
|
|
277
|
+
await this.clickConnectButton();
|
|
278
|
+
// 切回无头模式 (下次启动时)
|
|
279
|
+
this.options.headless = true;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* 检查 WebSocket 连接状态
|
|
283
|
+
*/
|
|
284
|
+
async checkConnection() {
|
|
285
|
+
if (!this.page || !this.isRunning)
|
|
286
|
+
return;
|
|
287
|
+
try {
|
|
288
|
+
// 检查页面是否还在运行
|
|
289
|
+
const url = this.page.url();
|
|
290
|
+
if (!url.includes(GOOGLE_LOGGED_IN_INDICATOR)) {
|
|
291
|
+
console.log(chalk.yellow('⚠️ 页面异常,尝试重新打开...'));
|
|
292
|
+
await this.openBuildApp();
|
|
293
|
+
await this.clickConnectButton();
|
|
294
|
+
}
|
|
295
|
+
// 检查连接状态 (通过页面元素或 JavaScript)
|
|
296
|
+
const connectionStatus = await this.page.evaluate(() => {
|
|
297
|
+
// 这里需要根据 Build App 的实际实现来检查连接状态
|
|
298
|
+
// 例如检查某个状态指示器元素
|
|
299
|
+
const statusEl = document.querySelector('[data-connection-status]');
|
|
300
|
+
if (statusEl) {
|
|
301
|
+
return statusEl.textContent?.includes('connected');
|
|
302
|
+
}
|
|
303
|
+
return true; // 默认假设连接正常
|
|
304
|
+
});
|
|
305
|
+
if (!connectionStatus) {
|
|
306
|
+
console.log(chalk.yellow('⚠️ 连接断开,尝试重新连接...'));
|
|
307
|
+
await this.clickConnectButton();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
console.error('检查连接状态失败:', error);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* 启动状态显示
|
|
316
|
+
*/
|
|
317
|
+
startStatusDisplay() {
|
|
318
|
+
// 每分钟更新一次状态
|
|
319
|
+
setInterval(() => {
|
|
320
|
+
if (this.isRunning) {
|
|
321
|
+
const uptime = formatUptime(this.startTime);
|
|
322
|
+
process.stdout.write(`\r${chalk.gray(`运行时间: ${uptime} | 状态: 在线`)}`);
|
|
323
|
+
}
|
|
324
|
+
}, 60000);
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* 关闭浏览器
|
|
328
|
+
*/
|
|
329
|
+
async close() {
|
|
330
|
+
this.isRunning = false;
|
|
331
|
+
if (this.loginCheckTimer) {
|
|
332
|
+
clearInterval(this.loginCheckTimer);
|
|
333
|
+
this.loginCheckTimer = null;
|
|
334
|
+
}
|
|
335
|
+
if (this.connectionCheckTimer) {
|
|
336
|
+
clearInterval(this.connectionCheckTimer);
|
|
337
|
+
this.connectionCheckTimer = null;
|
|
338
|
+
}
|
|
339
|
+
if (this.browser) {
|
|
340
|
+
await this.browser.close();
|
|
341
|
+
this.browser = null;
|
|
342
|
+
this.context = null;
|
|
343
|
+
this.page = null;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import { startClient } from './commands/start.js';
|
|
8
|
+
import { loginCommand } from './commands/login.js';
|
|
9
|
+
import { statusCommand } from './commands/status.js';
|
|
10
|
+
const DEFAULT_DATA_DIR = path.join(os.homedir(), '.gemini-client');
|
|
11
|
+
const program = new Command();
|
|
12
|
+
program
|
|
13
|
+
.name('gemini-client')
|
|
14
|
+
.description('Gemini Proxy Build App 客户端 - 使用 Camoufox 自动保持连接')
|
|
15
|
+
.version('1.0.0');
|
|
16
|
+
program
|
|
17
|
+
.command('start')
|
|
18
|
+
.description('启动客户端,连接到代理服务器')
|
|
19
|
+
.option('-s, --server <url>', '代理服务器 WebSocket 地址', 'ws://localhost:5345/v1/ws')
|
|
20
|
+
.option('-t, --token <token>', '客户端认证令牌 (不指定则自动生成)')
|
|
21
|
+
.option('-d, --daemon', '后台运行模式')
|
|
22
|
+
.option('--headless', '强制无头模式 (需要已有登录状态)')
|
|
23
|
+
.option('--data-dir <path>', '数据目录路径', DEFAULT_DATA_DIR)
|
|
24
|
+
.action(async (options) => {
|
|
25
|
+
try {
|
|
26
|
+
await startClient(options);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
console.error(chalk.red('启动失败:'), error);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
program
|
|
34
|
+
.command('login')
|
|
35
|
+
.description('登录 Google 账号 (会打开浏览器)')
|
|
36
|
+
.option('--data-dir <path>', '数据目录路径', DEFAULT_DATA_DIR)
|
|
37
|
+
.action(async (options) => {
|
|
38
|
+
try {
|
|
39
|
+
await loginCommand(options);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
console.error(chalk.red('登录失败:'), error);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
program
|
|
47
|
+
.command('status')
|
|
48
|
+
.description('查看当前状态')
|
|
49
|
+
.option('--data-dir <path>', '数据目录路径', DEFAULT_DATA_DIR)
|
|
50
|
+
.action(async (options) => {
|
|
51
|
+
try {
|
|
52
|
+
await statusCommand(options);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
console.error(chalk.red('查询失败:'), error);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
program
|
|
60
|
+
.command('logout')
|
|
61
|
+
.description('清除保存的登录状态')
|
|
62
|
+
.option('--data-dir <path>', '数据目录路径', DEFAULT_DATA_DIR)
|
|
63
|
+
.action(async (options) => {
|
|
64
|
+
const cookiesPath = path.join(options.dataDir, 'cookies.json');
|
|
65
|
+
if (fs.existsSync(cookiesPath)) {
|
|
66
|
+
fs.unlinkSync(cookiesPath);
|
|
67
|
+
console.log(chalk.green('✅ 已清除登录状态'));
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
console.log(chalk.yellow('⚠️ 没有保存的登录状态'));
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
program.parse();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { BrowserManager } from '../browser/manager.js';
|
|
5
|
+
import { ensureDataDir } from '../utils/helpers.js';
|
|
6
|
+
export async function loginCommand(options) {
|
|
7
|
+
const { dataDir } = options;
|
|
8
|
+
ensureDataDir(dataDir);
|
|
9
|
+
console.log(chalk.cyan('🔐 Google 账号登录'));
|
|
10
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
11
|
+
const spinner = ora('启动浏览器...').start();
|
|
12
|
+
const browserManager = new BrowserManager({
|
|
13
|
+
headless: false, // 登录必须有界面
|
|
14
|
+
dataDir,
|
|
15
|
+
serverUrl: '',
|
|
16
|
+
token: '',
|
|
17
|
+
});
|
|
18
|
+
try {
|
|
19
|
+
await browserManager.launch();
|
|
20
|
+
spinner.text = '打开 Google 登录页面...';
|
|
21
|
+
await browserManager.openGoogleLogin();
|
|
22
|
+
spinner.stop();
|
|
23
|
+
console.log(chalk.yellow('\n🔐 请在浏览器中登录 Google 账号...'));
|
|
24
|
+
console.log(chalk.gray(' 登录成功后将自动保存'));
|
|
25
|
+
// 等待用户登录
|
|
26
|
+
await browserManager.waitForGoogleLogin();
|
|
27
|
+
// 保存 cookies
|
|
28
|
+
await browserManager.saveCookies();
|
|
29
|
+
console.log(chalk.green('\n✅ 登录成功!已保存登录状态'));
|
|
30
|
+
console.log(chalk.gray(` Cookies 保存在: ${path.join(dataDir, 'cookies.json')}`));
|
|
31
|
+
console.log(chalk.cyan('\n现在可以运行 gemini-client start 启动客户端'));
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
spinner.fail('登录失败');
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
await browserManager.close();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { BrowserManager } from '../browser/manager.js';
|
|
6
|
+
import { generateToken, ensureDataDir } from '../utils/helpers.js';
|
|
7
|
+
export async function startClient(options) {
|
|
8
|
+
const { server, daemon, dataDir } = options;
|
|
9
|
+
// 确保数据目录存在
|
|
10
|
+
ensureDataDir(dataDir);
|
|
11
|
+
// 生成或使用指定的 token
|
|
12
|
+
const token = options.token || generateToken();
|
|
13
|
+
console.log(chalk.cyan('🦊 Gemini Proxy Client'));
|
|
14
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
15
|
+
console.log(chalk.white(`服务器: ${server}`));
|
|
16
|
+
console.log(chalk.white(`Token: ${token}`));
|
|
17
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
18
|
+
const cookiesPath = path.join(dataDir, 'cookies.json');
|
|
19
|
+
const hasCookies = fs.existsSync(cookiesPath);
|
|
20
|
+
// 决定是否使用无头模式
|
|
21
|
+
let headless = options.headless ?? false;
|
|
22
|
+
if (!hasCookies) {
|
|
23
|
+
console.log(chalk.yellow('⚠️ 未检测到 Google 登录状态'));
|
|
24
|
+
console.log(chalk.white(' 将打开浏览器,请登录您的 Google 账号...'));
|
|
25
|
+
headless = false;
|
|
26
|
+
}
|
|
27
|
+
else if (!options.headless) {
|
|
28
|
+
// 有 cookies,默认使用无头模式
|
|
29
|
+
headless = true;
|
|
30
|
+
console.log(chalk.green('📂 检测到已保存的登录状态'));
|
|
31
|
+
}
|
|
32
|
+
const spinner = ora('启动 Camoufox 浏览器...').start();
|
|
33
|
+
try {
|
|
34
|
+
const browserManager = new BrowserManager({
|
|
35
|
+
headless,
|
|
36
|
+
dataDir,
|
|
37
|
+
serverUrl: server,
|
|
38
|
+
token,
|
|
39
|
+
});
|
|
40
|
+
// 设置信号处理
|
|
41
|
+
setupSignalHandlers(browserManager);
|
|
42
|
+
spinner.text = '初始化浏览器...';
|
|
43
|
+
await browserManager.launch();
|
|
44
|
+
spinner.text = '打开 Build App 页面...';
|
|
45
|
+
await browserManager.openBuildApp();
|
|
46
|
+
// 检查登录状态
|
|
47
|
+
spinner.text = '检查 Google 登录状态...';
|
|
48
|
+
const isLoggedIn = await browserManager.checkGoogleLogin();
|
|
49
|
+
if (!isLoggedIn) {
|
|
50
|
+
spinner.stop();
|
|
51
|
+
console.log(chalk.yellow('\n🔐 请在浏览器中登录 Google 账号...'));
|
|
52
|
+
console.log(chalk.gray(' 登录成功后将自动继续'));
|
|
53
|
+
// 等待用户登录
|
|
54
|
+
await browserManager.waitForGoogleLogin();
|
|
55
|
+
// 保存 cookies
|
|
56
|
+
await browserManager.saveCookies();
|
|
57
|
+
console.log(chalk.green('✅ Google 登录成功!已保存登录状态'));
|
|
58
|
+
spinner.start('继续连接...');
|
|
59
|
+
}
|
|
60
|
+
// 点击连接按钮
|
|
61
|
+
spinner.text = '点击 ws connect 按钮...';
|
|
62
|
+
await browserManager.clickConnectButton();
|
|
63
|
+
spinner.succeed(chalk.green('已连接到代理服务器!'));
|
|
64
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
65
|
+
console.log(chalk.green(`✅ 客户端已启动`));
|
|
66
|
+
console.log(chalk.white(` Token: ${token}`));
|
|
67
|
+
console.log(chalk.gray(' 按 Ctrl+C 退出'));
|
|
68
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
69
|
+
// 启动保活和状态监控
|
|
70
|
+
await browserManager.startKeepAlive();
|
|
71
|
+
// 如果是 daemon 模式,保持运行
|
|
72
|
+
if (daemon) {
|
|
73
|
+
console.log(chalk.cyan('🔄 后台运行模式'));
|
|
74
|
+
}
|
|
75
|
+
// 保持进程运行
|
|
76
|
+
await new Promise(() => { });
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
spinner.fail(chalk.red('启动失败'));
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function setupSignalHandlers(browserManager) {
|
|
84
|
+
const cleanup = async () => {
|
|
85
|
+
console.log(chalk.yellow('\n\n🛑 正在关闭...'));
|
|
86
|
+
try {
|
|
87
|
+
await browserManager.close();
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
// ignore
|
|
91
|
+
}
|
|
92
|
+
process.exit(0);
|
|
93
|
+
};
|
|
94
|
+
process.on('SIGINT', cleanup);
|
|
95
|
+
process.on('SIGTERM', cleanup);
|
|
96
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { ensureDataDir } from '../utils/helpers.js';
|
|
5
|
+
export async function statusCommand(options) {
|
|
6
|
+
const { dataDir } = options;
|
|
7
|
+
ensureDataDir(dataDir);
|
|
8
|
+
console.log(chalk.cyan('📊 Gemini Client 状态'));
|
|
9
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
10
|
+
// 检查 cookies
|
|
11
|
+
const cookiesPath = path.join(dataDir, 'cookies.json');
|
|
12
|
+
if (fs.existsSync(cookiesPath)) {
|
|
13
|
+
const stats = fs.statSync(cookiesPath);
|
|
14
|
+
const lastModified = stats.mtime;
|
|
15
|
+
const ageHours = (Date.now() - lastModified.getTime()) / (1000 * 60 * 60);
|
|
16
|
+
console.log(chalk.green('✅ Google 登录状态: 已保存'));
|
|
17
|
+
console.log(chalk.gray(` 保存时间: ${lastModified.toLocaleString()}`));
|
|
18
|
+
console.log(chalk.gray(` 已过去: ${ageHours.toFixed(1)} 小时`));
|
|
19
|
+
if (ageHours > 24 * 7) {
|
|
20
|
+
console.log(chalk.yellow(' ⚠️ 登录状态可能已过期,建议重新登录'));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
console.log(chalk.yellow('⚠️ Google 登录状态: 未登录'));
|
|
25
|
+
console.log(chalk.gray(' 运行 gemini-client login 进行登录'));
|
|
26
|
+
}
|
|
27
|
+
// 检查配置
|
|
28
|
+
const configPath = path.join(dataDir, 'config.json');
|
|
29
|
+
if (fs.existsSync(configPath)) {
|
|
30
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
31
|
+
console.log(chalk.gray('\n配置:'));
|
|
32
|
+
console.log(chalk.gray(` 服务器: ${config.server || '未设置'}`));
|
|
33
|
+
console.log(chalk.gray(` Token: ${config.token || '未设置'}`));
|
|
34
|
+
}
|
|
35
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
36
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* 生成客户端 Token
|
|
5
|
+
* 格式: gp_client_YYYY_MM_DD_HH_mm_ss
|
|
6
|
+
*/
|
|
7
|
+
export function generateToken() {
|
|
8
|
+
const now = new Date();
|
|
9
|
+
const year = now.getFullYear();
|
|
10
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
11
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
12
|
+
const hour = String(now.getHours()).padStart(2, '0');
|
|
13
|
+
const minute = String(now.getMinutes()).padStart(2, '0');
|
|
14
|
+
const second = String(now.getSeconds()).padStart(2, '0');
|
|
15
|
+
return `gp_client_${year}_${month}_${day}_${hour}_${minute}_${second}`;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* 确保数据目录存在
|
|
19
|
+
*/
|
|
20
|
+
export function ensureDataDir(dataDir) {
|
|
21
|
+
if (!fs.existsSync(dataDir)) {
|
|
22
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 保存配置
|
|
27
|
+
*/
|
|
28
|
+
export function saveConfig(dataDir, config) {
|
|
29
|
+
const configPath = path.join(dataDir, 'config.json');
|
|
30
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 加载配置
|
|
34
|
+
*/
|
|
35
|
+
export function loadConfig(dataDir) {
|
|
36
|
+
const configPath = path.join(dataDir, 'config.json');
|
|
37
|
+
if (fs.existsSync(configPath)) {
|
|
38
|
+
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 格式化运行时间
|
|
44
|
+
*/
|
|
45
|
+
export function formatUptime(startTime) {
|
|
46
|
+
const elapsed = Date.now() - startTime;
|
|
47
|
+
const seconds = Math.floor(elapsed / 1000) % 60;
|
|
48
|
+
const minutes = Math.floor(elapsed / (1000 * 60)) % 60;
|
|
49
|
+
const hours = Math.floor(elapsed / (1000 * 60 * 60));
|
|
50
|
+
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 延迟函数
|
|
54
|
+
*/
|
|
55
|
+
export function sleep(ms) {
|
|
56
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
57
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gemini-proxy-client",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Gemini Proxy Build App 客户端 - 使用 Camoufox 自动保持连接",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"gemini-client": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "node dist/cli.js",
|
|
13
|
+
"dev": "tsx src/cli.ts",
|
|
14
|
+
"postinstall": "camoufox-js fetch || echo 'Camoufox download failed. Run: npx camoufox-js fetch'",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
16
|
+
},
|
|
17
|
+
"keywords": ["gemini", "proxy", "build-app", "camoufox", "automation", "google-ai-studio"],
|
|
18
|
+
"author": "",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18.0.0"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"camoufox-js": "^0.8.5",
|
|
29
|
+
"commander": "^12.0.0",
|
|
30
|
+
"chalk": "^5.3.0",
|
|
31
|
+
"ora": "^8.0.1",
|
|
32
|
+
"dotenv": "^16.4.1"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^20.11.0",
|
|
36
|
+
"tsx": "^4.7.0",
|
|
37
|
+
"typescript": "^5.3.3"
|
|
38
|
+
}
|
|
39
|
+
}
|