liuw15 commited on
Commit
89083cb
·
1 Parent(s): 76ce01e

解决普号错误projectId导致403的问题,同时为pro账号(进入pro组)提供随机的生成功能

Browse files
.env.example CHANGED
@@ -25,6 +25,7 @@ API_KEY=sk-text
25
  USE_NATIVE_AXIOS=false
26
  TIMEOUT=180000
27
  # PROXY=http://127.0.0.1:7897
 
28
 
29
  # 系统提示词
30
  SYSTEM_INSTRUCTION=你是聊天机器人,名字叫萌萌,如同名字这般,你的性格是软软糯糯萌萌哒的,专门为用户提供聊天和情绪价值,协助进行小说创作或者角色扮演
 
25
  USE_NATIVE_AXIOS=false
26
  TIMEOUT=180000
27
  # PROXY=http://127.0.0.1:7897
28
+ SKIP_PROJECT_ID_FETCH=false # 跳过从API获取projectId,直接随机生成(适用于Pro订阅账号)
29
 
30
  # 系统提示词
31
  SYSTEM_INSTRUCTION=你是聊天机器人,名字叫萌萌,如同名字这般,你的性格是软软糯糯萌萌哒的,专门为用户提供聊天和情绪价值,协助进行小说创作或者角色扮演
README.md CHANGED
@@ -12,6 +12,8 @@
12
  - ✅ API Key 认证
13
  - ✅ 思维链(Thinking)输出
14
  - ✅ 图片输入支持(Base64 编码)
 
 
15
 
16
  ## 环境要求
17
 
@@ -149,6 +151,21 @@ curl http://localhost:8045/v1/chat/completions \
149
  - GIF (`data:image/gif;base64,...`)
150
  - WebP (`data:image/webp;base64,...`)
151
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  ## 多账号管理
153
 
154
  `data/accounts.json` 支持多个账号,服务会自动轮换使用:
@@ -190,10 +207,11 @@ curl http://localhost:8045/v1/chat/completions \
190
  | `DEFAULT_TOP_P` | 默认 top_p | 0.85 |
191
  | `DEFAULT_TOP_K` | 默认 top_k | 50 |
192
  | `DEFAULT_MAX_TOKENS` | 默认最大 token 数 | 8096 |
193
- | `USE_NATIVE_FETCH` | 使用原生 axios | false |
194
  | `TIMEOUT` | 请求超时时间(毫秒) | 30000 |
195
  | `PROXY` | 代理地址 | - |
196
  | `SYSTEM_INSTRUCTION` | 系统提示词 | - |
 
197
 
198
  完整配置示例请参考 `.env.example` 文件。
199
 
@@ -245,6 +263,21 @@ npm run login
245
  └── package.json # 项目配置
246
  ```
247
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  ## 注意事项
249
 
250
  1. 首次使用需要复制 `.env.example` 为 `.env` 并配置
 
12
  - ✅ API Key 认证
13
  - ✅ 思维链(Thinking)输出
14
  - ✅ 图片输入支持(Base64 编码)
15
+ - ✅ 图片生成支持(大/小香蕉 模型)
16
+ - ✅ Pro 账号随机 ProjectId 支持
17
 
18
  ## 环境要求
19
 
 
151
  - GIF (`data:image/gif;base64,...`)
152
  - WebP (`data:image/webp;base64,...`)
153
 
154
+ ### 图片生成示例
155
+
156
+ 支持使用 大/小香蕉 模型生成图片,生成的图片会以 Markdown 格式返回:
157
+
158
+ ```bash
159
+ curl http://localhost:8045/v1/chat/completions \
160
+ -H "Content-Type: application/json" \
161
+ -H "Authorization: Bearer sk-text" \
162
+ -d '{
163
+ "model": "gemimi-3.0-pro-image",
164
+ "messages": [{"role": "user", "content": "画一只可爱的猫"}],
165
+ "stream": false
166
+ }'
167
+ ```
168
+
169
  ## 多账号管理
170
 
171
  `data/accounts.json` 支持多个账号,服务会自动轮换使用:
 
207
  | `DEFAULT_TOP_P` | 默认 top_p | 0.85 |
208
  | `DEFAULT_TOP_K` | 默认 top_k | 50 |
209
  | `DEFAULT_MAX_TOKENS` | 默认最大 token 数 | 8096 |
210
+ | `USE_NATIVE_AXIOS` | 使用原生 axios | false |
211
  | `TIMEOUT` | 请求超时时间(毫秒) | 30000 |
212
  | `PROXY` | 代理地址 | - |
213
  | `SYSTEM_INSTRUCTION` | 系统提示词 | - |
214
+ | `SKIP_PROJECT_ID_FETCH` | 跳过 API 获取 ProjectId,直接随机生成(Pro 账号可用) | false |
215
 
216
  完整配置示例请参考 `.env.example` 文件。
217
 
 
263
  └── package.json # 项目配置
264
  ```
265
 
266
+ ## Pro 账号随机 ProjectId
267
+
268
+ 对于 Pro 订阅账号,可以跳过 API 验证直接使用随机生成的 ProjectId:
269
+
270
+ 1. 在 `.env` 文件中设置:
271
+ ```env
272
+ SKIP_PROJECT_ID_FETCH=true
273
+ ```
274
+
275
+ 2. 运行 `npm run login` 登录时会自动使用随机生成的 ProjectId
276
+
277
+ 3. 已有账号也会在使用时自动生成随机 ProjectId
278
+
279
+ 注意:此功能仅适用于 Pro 订阅账号。官方已修复免费账号使用随机 ProjectId 的漏洞。
280
+
281
  ## 注意事项
282
 
283
  1. 首次使用需要复制 `.env.example` 为 `.env` 并配置
scripts/oauth-server.js CHANGED
@@ -1,11 +1,13 @@
1
  import http from 'http';
2
- import https from 'https';
3
  import { URL } from 'url';
4
  import crypto from 'crypto';
5
  import fs from 'fs';
6
  import path from 'path';
7
  import { fileURLToPath } from 'url';
8
  import log from '../src/utils/logger.js';
 
 
 
9
 
10
  const __filename = fileURLToPath(import.meta.url);
11
  const __dirname = path.dirname(__filename);
@@ -36,47 +38,54 @@ function generateAuthUrl(port) {
36
  return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
37
  }
38
 
39
- function exchangeCodeForToken(code, port) {
40
- return new Promise((resolve, reject) => {
41
- const postData = new URLSearchParams({
42
- code: code,
43
- client_id: CLIENT_ID,
44
- redirect_uri: `http://localhost:${port}/oauth-callback`,
45
- grant_type: 'authorization_code'
46
- });
47
-
48
- if (CLIENT_SECRET) {
49
- postData.append('client_secret', CLIENT_SECRET);
50
- }
51
-
52
- const data = postData.toString();
53
-
54
- const options = {
55
- hostname: 'oauth2.googleapis.com',
56
- path: '/token',
57
- method: 'POST',
58
- headers: {
59
- 'Content-Type': 'application/x-www-form-urlencoded',
60
- 'Content-Length': Buffer.byteLength(data)
61
- }
62
  };
63
-
64
- const req = https.request(options, (res) => {
65
- let body = '';
66
- res.on('data', chunk => body += chunk);
67
- res.on('end', () => {
68
- if (res.statusCode === 200) {
69
- resolve(JSON.parse(body));
70
- } else {
71
- reject(new Error(`HTTP ${res.statusCode}: ${body}`));
72
- }
73
- });
74
- });
75
-
76
- req.on('error', reject);
77
- req.write(data);
78
- req.end();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  });
 
80
  }
81
 
82
  const server = http.createServer((req, res) => {
@@ -89,7 +98,7 @@ const server = http.createServer((req, res) => {
89
 
90
  if (code) {
91
  log.info('收到授权码,正在交换 Token...');
92
- exchangeCodeForToken(code, port).then(tokenData => {
93
  const account = {
94
  access_token: tokenData.access_token,
95
  refresh_token: tokenData.refresh_token,
@@ -97,6 +106,33 @@ const server = http.createServer((req, res) => {
97
  timestamp: Date.now()
98
  };
99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  let accounts = [];
101
  try {
102
  if (fs.existsSync(ACCOUNTS_FILE)) {
@@ -116,7 +152,6 @@ const server = http.createServer((req, res) => {
116
  fs.writeFileSync(ACCOUNTS_FILE, JSON.stringify(accounts, null, 2));
117
 
118
  log.info(`Token 已保存到 ${ACCOUNTS_FILE}`);
119
- //log.info(`过期时间: ${account.expires_in}秒`);
120
 
121
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
122
  res.end('<h1>授权成功!</h1><p>Token 已保存,可以关闭此页面。</p>');
 
1
  import http from 'http';
 
2
  import { URL } from 'url';
3
  import crypto from 'crypto';
4
  import fs from 'fs';
5
  import path from 'path';
6
  import { fileURLToPath } from 'url';
7
  import log from '../src/utils/logger.js';
8
+ import axios from 'axios';
9
+ import config from '../src/config/config.js';
10
+ import { generateProjectId } from '../src/utils/idGenerator.js';
11
 
12
  const __filename = fileURLToPath(import.meta.url);
13
  const __dirname = path.dirname(__filename);
 
38
  return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
39
  }
40
 
41
+ function getAxiosConfig() {
42
+ const axiosConfig = { timeout: config.timeout };
43
+ if (config.proxy) {
44
+ const proxyUrl = new URL(config.proxy);
45
+ axiosConfig.proxy = {
46
+ protocol: proxyUrl.protocol.replace(':', ''),
47
+ host: proxyUrl.hostname,
48
+ port: parseInt(proxyUrl.port)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  };
50
+ }
51
+ return axiosConfig;
52
+ }
53
+
54
+ async function exchangeCodeForToken(code, port) {
55
+ const postData = new URLSearchParams({
56
+ code,
57
+ client_id: CLIENT_ID,
58
+ client_secret: CLIENT_SECRET,
59
+ redirect_uri: `http://localhost:${port}/oauth-callback`,
60
+ grant_type: 'authorization_code'
61
+ });
62
+
63
+ const response = await axios({
64
+ method: 'POST',
65
+ url: 'https://oauth2.googleapis.com/token',
66
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
67
+ data: postData.toString(),
68
+ ...getAxiosConfig()
69
+ });
70
+
71
+ return response.data;
72
+ }
73
+
74
+ async function fetchProjectId(accessToken) {
75
+ const response = await axios({
76
+ method: 'POST',
77
+ url: 'https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:loadCodeAssist',
78
+ headers: {
79
+ 'Host': 'daily-cloudcode-pa.sandbox.googleapis.com',
80
+ 'User-Agent': 'antigravity/1.11.9 windows/amd64',
81
+ 'Authorization': `Bearer ${accessToken}`,
82
+ 'Content-Type': 'application/json',
83
+ 'Accept-Encoding': 'gzip'
84
+ },
85
+ data: JSON.stringify({ metadata: { ideType: 'ANTIGRAVITY' } }),
86
+ ...getAxiosConfig()
87
  });
88
+ return response.data?.cloudaicompanionProject;
89
  }
90
 
91
  const server = http.createServer((req, res) => {
 
98
 
99
  if (code) {
100
  log.info('收到授权码,正在交换 Token...');
101
+ exchangeCodeForToken(code, port).then(async (tokenData) => {
102
  const account = {
103
  access_token: tokenData.access_token,
104
  refresh_token: tokenData.refresh_token,
 
106
  timestamp: Date.now()
107
  };
108
 
109
+ if (config.skipProjectIdFetch) {
110
+ account.projectId = generateProjectId();
111
+ account.enable = true;
112
+ log.info('已跳过API验证,使用随机生成的projectId: ' + account.projectId);
113
+ } else {
114
+ log.info('正在验证账号资格...');
115
+ try {
116
+ const projectId = await fetchProjectId(account.access_token);
117
+ if (projectId === undefined) {
118
+ log.warn('该账号无资格使用(无法获取projectId),已跳过保存');
119
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
120
+ res.end('<h1>账号无资格</h1><p>该账号无法获取projectId,未保存。</p>');
121
+ setTimeout(() => server.close(), 1000);
122
+ return;
123
+ }
124
+ account.projectId = projectId;
125
+ account.enable = true;
126
+ log.info('账号验证通过');
127
+ } catch (err) {
128
+ log.error('验证账号资格失败:', err.message);
129
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
130
+ res.end('<h1>验证失败</h1><p>无法验证账号资格,请查看控制台。</p>');
131
+ setTimeout(() => server.close(), 1000);
132
+ return;
133
+ }
134
+ }
135
+
136
  let accounts = [];
137
  try {
138
  if (fs.existsSync(ACCOUNTS_FILE)) {
 
152
  fs.writeFileSync(ACCOUNTS_FILE, JSON.stringify(accounts, null, 2));
153
 
154
  log.info(`Token 已保存到 ${ACCOUNTS_FILE}`);
 
155
 
156
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
157
  res.end('<h1>授权成功!</h1><p>Token 已保存,可以关闭此页面。</p>');
src/auth/token_manager.js CHANGED
@@ -3,7 +3,7 @@ import path from 'path';
3
  import { fileURLToPath } from 'url';
4
  import axios from 'axios';
5
  import { log } from '../utils/logger.js';
6
- import { generateProjectId, generateSessionId } from '../utils/idGenerator.js';
7
  import config from '../config/config.js';
8
 
9
  const __filename = fileURLToPath(import.meta.url);
@@ -20,29 +20,17 @@ class TokenManager {
20
  this.initialize();
21
  }
22
 
23
- initialize() {
24
  try {
25
  log.info('正在初始化token管理器...');
26
  const data = fs.readFileSync(this.filePath, 'utf8');
27
  let tokenArray = JSON.parse(data);
28
- let needSave = false;
29
-
30
- tokenArray = tokenArray.map(token => {
31
- if (!token.projectId) {
32
- token.projectId = generateProjectId();
33
- needSave = true;
34
- }
35
- return token;
36
- });
37
-
38
- if (needSave) {
39
- fs.writeFileSync(this.filePath, JSON.stringify(tokenArray, null, 2), 'utf8');
40
- }
41
 
42
  this.tokens = tokenArray.filter(token => token.enable !== false).map(token => ({
43
  ...token,
44
  sessionId: generateSessionId()
45
  }));
 
46
  this.currentIndex = 0;
47
  log.info(`成功加载 ${this.tokens.length} 个可用token`);
48
  } catch (error) {
@@ -51,6 +39,27 @@ class TokenManager {
51
  }
52
  }
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  isExpired(token) {
55
  if (!token.timestamp || !token.expires_in) return true;
56
  const expiresAt = token.timestamp + (token.expires_in * 1000);
@@ -124,7 +133,7 @@ class TokenManager {
124
  async getToken() {
125
  if (this.tokens.length === 0) return null;
126
 
127
- const startIndex = this.currentIndex;
128
  const totalTokens = this.tokens.length;
129
 
130
  for (let i = 0; i < totalTokens; i++) {
@@ -134,16 +143,38 @@ class TokenManager {
134
  if (this.isExpired(token)) {
135
  await this.refreshToken(token);
136
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  this.currentIndex = (this.currentIndex + 1) % this.tokens.length;
138
  return token;
139
  } catch (error) {
140
  if (error.statusCode === 403 || error.statusCode === 400) {
141
- const accountNum = this.currentIndex + 1;
142
- log.warn(`账号 ${accountNum}: Token 已失效或错误,已自动禁用该账号`);
143
  this.disableToken(token);
144
  if (this.tokens.length === 0) return null;
145
  } else {
146
- log.error(`Token ${this.currentIndex + 1} 刷新失败:`, error.message);
147
  this.currentIndex = (this.currentIndex + 1) % this.tokens.length;
148
  }
149
  }
 
3
  import { fileURLToPath } from 'url';
4
  import axios from 'axios';
5
  import { log } from '../utils/logger.js';
6
+ import { generateSessionId, generateProjectId } from '../utils/idGenerator.js';
7
  import config from '../config/config.js';
8
 
9
  const __filename = fileURLToPath(import.meta.url);
 
20
  this.initialize();
21
  }
22
 
23
+ async initialize() {
24
  try {
25
  log.info('正在初始化token管理器...');
26
  const data = fs.readFileSync(this.filePath, 'utf8');
27
  let tokenArray = JSON.parse(data);
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
  this.tokens = tokenArray.filter(token => token.enable !== false).map(token => ({
30
  ...token,
31
  sessionId: generateSessionId()
32
  }));
33
+
34
  this.currentIndex = 0;
35
  log.info(`成功加载 ${this.tokens.length} 个可用token`);
36
  } catch (error) {
 
39
  }
40
  }
41
 
42
+ async fetchProjectId(token) {
43
+ const response = await axios({
44
+ method: 'POST',
45
+ url: 'https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:loadCodeAssist',
46
+ headers: {
47
+ 'Host': 'daily-cloudcode-pa.sandbox.googleapis.com',
48
+ 'User-Agent': 'antigravity/1.11.9 windows/amd64',
49
+ 'Authorization': `Bearer ${token.access_token}`,
50
+ 'Content-Type': 'application/json',
51
+ 'Accept-Encoding': 'gzip'
52
+ },
53
+ data: JSON.stringify({ metadata: { ideType: 'ANTIGRAVITY' } }),
54
+ timeout: config.timeout,
55
+ proxy: config.proxy ? (() => {
56
+ const proxyUrl = new URL(config.proxy);
57
+ return { protocol: proxyUrl.protocol.replace(':', ''), host: proxyUrl.hostname, port: parseInt(proxyUrl.port) };
58
+ })() : false
59
+ });
60
+ return response.data?.cloudaicompanionProject;
61
+ }
62
+
63
  isExpired(token) {
64
  if (!token.timestamp || !token.expires_in) return true;
65
  const expiresAt = token.timestamp + (token.expires_in * 1000);
 
133
  async getToken() {
134
  if (this.tokens.length === 0) return null;
135
 
136
+ //const startIndex = this.currentIndex;
137
  const totalTokens = this.tokens.length;
138
 
139
  for (let i = 0; i < totalTokens; i++) {
 
143
  if (this.isExpired(token)) {
144
  await this.refreshToken(token);
145
  }
146
+ if (!token.projectId) {
147
+ if (config.skipProjectIdFetch) {
148
+ token.projectId = generateProjectId();
149
+ this.saveToFile();
150
+ log.info(`...${token.access_token.slice(-8)}: 使用随机生成的projectId: ${token.projectId}`);
151
+ } else {
152
+ try {
153
+ const projectId = await this.fetchProjectId(token);
154
+ if (projectId === undefined) {
155
+ log.warn(`...${token.access_token.slice(-8)}: 无资格获取projectId,跳过保存`);
156
+ this.disableToken(token);
157
+ if (this.tokens.length === 0) return null;
158
+ continue;
159
+ }
160
+ token.projectId = projectId;
161
+ this.saveToFile();
162
+ } catch (error) {
163
+ log.error(`...${token.access_token.slice(-8)}: 获取projectId失败:`, error.message);
164
+ this.currentIndex = (this.currentIndex + 1) % this.tokens.length;
165
+ continue;
166
+ }
167
+ }
168
+ }
169
  this.currentIndex = (this.currentIndex + 1) % this.tokens.length;
170
  return token;
171
  } catch (error) {
172
  if (error.statusCode === 403 || error.statusCode === 400) {
173
+ log.warn(`...${token.access_token.slice(-8)}: Token 已失效或错误,已自动禁用该账号`);
 
174
  this.disableToken(token);
175
  if (this.tokens.length === 0) return null;
176
  } else {
177
+ log.error(`...${token.access_token.slice(-8)} 刷新失败:`, error.message);
178
  this.currentIndex = (this.currentIndex + 1) % this.tokens.length;
179
  }
180
  }
src/config/config.js CHANGED
@@ -70,7 +70,8 @@ const config = {
70
  useNativeAxios: process.env.USE_NATIVE_AXIOS !== 'false',
71
  timeout: parseInt(process.env.TIMEOUT) || 30000,
72
  proxy: process.env.PROXY || null,
73
- systemInstruction: process.env.SYSTEM_INSTRUCTION || '你是聊天机器人,名字叫萌萌,如同名字这般,你的性格是软软糯糯萌萌哒的,专门为用户提供聊天和情绪价值,协助进行小说创作或者角色扮演'
 
74
  };
75
 
76
  log.info('✓ 配置加载成功');
 
70
  useNativeAxios: process.env.USE_NATIVE_AXIOS !== 'false',
71
  timeout: parseInt(process.env.TIMEOUT) || 30000,
72
  proxy: process.env.PROXY || null,
73
+ systemInstruction: process.env.SYSTEM_INSTRUCTION || '你是聊天机器人,名字叫萌萌,如同名字这般,你的性格是软软糯糯萌萌哒的,专门为用户提供聊天和情绪价值,协助进行小说创作或者角色扮演',
74
+ skipProjectIdFetch: process.env.SKIP_PROJECT_ID_FETCH === 'true'
75
  };
76
 
77
  log.info('✓ 配置加载成功');