ZhaoShanGeng commited on
Commit
873f10f
·
1 Parent(s): 0fadeb3

feat: 多项修复与代码简化

Browse files

文件改动:

src/utils/utils.js:
- 修复非思考模型识别: isEnableThinking() 从 endsWith 改为 includes 匹配 -thinking
- 修复工具参数 Schema: EXCLUDED_KEYS 新增 anyOf/oneOf/allOf 等过滤
- convertOpenAIToolsToAntigravity() 确保顶层有 type 和 properties
- 新增 prepareImageRequest() 统一图片请求处理
- 移除 httpAgent/httpsAgent,DNS 优化移至 httpClient.js
- 简化 getDefaultIp() 遍历所有网卡

src/server/index.js:
- 修复工具调用流式响应: tool_calls 添加 index 字段符合 OpenAI schema
- 使用 prepareImageRequest() 简化图片模型处理
- 使用 registerMemoryPoolCleanup() 简化内存清理

src/utils/httpClient.js (新增):
- 统一 HTTP 请求和 DNS 解析优化 (IPv4 优先回退 IPv6)

public/app.js:
- 前端额度显示不取整: percentage 从 toFixed(0) 改为 toFixed(2)
- 重构 createTokenFormBody() 提取表单生成逻辑

src/api/client.js:
- 引入 httpClient 统一 HTTP 请求处理
- 简化代码结构

src/utils/memoryManager.js:
- 新增 registerMemoryPoolCleanup() 工具函数

src/config/config.js:
- 新增 getProxyConfig() 导出

src/utils/configReloader.js:
- 移除重复的 getProxyConfig(),改为从 config.js 导入
- 默认 thinking_budget 从 16000 改为 1024

src/constants/oauth.js:
- 新增 OAUTH_SCOPES 常量统一管理

src/routes/sd.js:
- 使用 prepareImageRequest() 简化图片请求处理

scripts/oauth-server.js, scripts/refresh-tokens.js:
- 简化代码减少冗余

API.md:
- thinking_budget 支持设为 0 关闭思考预算限制

README.md:
- 默认 thinkingBudget 从 16000 改为 1024

API.md CHANGED
@@ -162,7 +162,7 @@ curl http://localhost:8045/v1/chat/completions \
162
  | `top_p` | number | ❌ | Top P 参数,默认 1 |
163
  | `top_k` | number | ❌ | Top K 参数,默认 50 |
164
  | `max_tokens` | number | ❌ | 最大 token 数,默认 32000 |
165
- | `thinking_budget` | number | ❌ | 思考预算(仅对思考模型生效),范围 1024-32000,默认 16000 |
166
  | `reasoning_effort` | string | ❌ | 思维链强度(OpenAI 格式),可选值:`low`(1024)、`medium`(16000)、`high`(32000) |
167
  | `tools` | array | ❌ | 工具列表(Function Calling) |
168
 
 
162
  | `top_p` | number | ❌ | Top P 参数,默认 1 |
163
  | `top_k` | number | ❌ | Top K 参数,默认 50 |
164
  | `max_tokens` | number | ❌ | 最大 token 数,默认 32000 |
165
+ | `thinking_budget` | number | ❌ | 思考预算(仅对思考模型生效),可为 0 或 1024-32000,默认 16000(0 表示关闭思考预算限制) |
166
  | `reasoning_effort` | string | ❌ | 思维链强度(OpenAI 格式),可选值:`low`(1024)、`medium`(16000)、`high`(32000) |
167
  | `tools` | array | ❌ | 工具列表(Function Calling) |
168
 
README.md CHANGED
@@ -357,7 +357,7 @@ curl http://localhost:8045/v1/chat/completions \
357
  "topP": 1, // 默认 top_p
358
  "topK": 50, // 默认 top_k
359
  "maxTokens": 32000, // 默认最大 token 数
360
- "thinkingBudget": 16000 // 默认思考预算(仅对思考模型生效,范围 1024-32000)
361
  },
362
  "cache": {
363
  "modelListTTL": 3600000 // 模型列表缓存时间(毫秒),默认 1 小时
 
357
  "topP": 1, // 默认 top_p
358
  "topK": 50, // 默认 top_k
359
  "maxTokens": 32000, // 默认最大 token 数
360
+ "thinkingBudget": 1024 // 默认思考预算(仅对思考模型生效,范围 1024-32000)
361
  },
362
  "cache": {
363
  "modelListTTL": 3600000 // 模型列表缓存时间(毫秒),默认 1 小时
public/app.js CHANGED
@@ -221,17 +221,26 @@ function showOAuthModal() {
221
  modal.onclick = (e) => { if (e.target === modal) modal.remove(); };
222
  }
223
 
224
- function showManualModal() {
225
- const modal = document.createElement('div');
226
- modal.className = 'modal form-modal';
227
- modal.innerHTML = `
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  <div class="modal-content">
229
- <div class="modal-title">✏️ 手动填入Token</div>
230
- <div class="form-row">
231
- <input type="text" id="modalAccessToken" placeholder="Access Token (必填)">
232
- <input type="text" id="modalRefreshToken" placeholder="Refresh Token (必填)">
233
- <input type="number" id="modalExpiresIn" placeholder="过期时间(秒)" value="3599">
234
- </div>
235
  <p style="font-size: 0.8rem; color: var(--text-light); margin-bottom: 12px;">💡 过期时间默认3599秒(约1小时)</p>
236
  <div class="modal-actions">
237
  <button class="btn btn-secondary" onclick="this.closest('.modal').remove()">取消</button>
@@ -239,6 +248,12 @@ function showManualModal() {
239
  </div>
240
  </div>
241
  `;
 
 
 
 
 
 
242
  document.body.appendChild(modal);
243
  modal.onclick = (e) => { if (e.target === modal) modal.remove(); };
244
  }
@@ -554,7 +569,8 @@ function renderQuotaSummary(summaryEl, quotaData) {
554
  }
555
  });
556
 
557
- const percentage = (minQuota.remaining * 100).toFixed(0);
 
558
  const shortName = minModel.replace('models/', '').replace('publishers/google/', '').split('/').pop();
559
  const barColor = percentage > 50 ? '#10b981' : percentage > 20 ? '#f59e0b' : '#ef4444';
560
 
@@ -563,7 +579,7 @@ function renderQuotaSummary(summaryEl, quotaData) {
563
  <span class="quota-summary-icon">📊</span>
564
  <span class="quota-summary-model" title="${minModel}">${shortName}</span>
565
  <span class="quota-summary-bar"><span style="width:${percentage}%;background:${barColor}"></span></span>
566
- <span class="quota-summary-pct">${percentage}%</span>
567
  `;
568
  }
569
 
@@ -628,7 +644,8 @@ async function loadQuotaDetail(cardId, refreshToken) {
628
  if (items.length === 0) return '';
629
  let groupHtml = '';
630
  items.forEach(({ modelId, quota }) => {
631
- const percentage = (quota.remaining * 100).toFixed(0);
 
632
  const barColor = percentage > 50 ? '#10b981' : percentage > 20 ? '#f59e0b' : '#ef4444';
633
  const shortName = modelId.replace('models/', '').replace('publishers/google/', '').split('/').pop();
634
  // 紧凑的一行显示
@@ -637,7 +654,7 @@ async function loadQuotaDetail(cardId, refreshToken) {
637
  <span class="quota-detail-icon">${icon}</span>
638
  <span class="quota-detail-name">${shortName}</span>
639
  <span class="quota-detail-bar"><span style="width:${percentage}%;background:${barColor}"></span></span>
640
- <span class="quota-detail-pct">${percentage}%</span>
641
  </div>
642
  `;
643
  });
@@ -1120,7 +1137,8 @@ function renderQuotaModal(quotaContent, quotaData) {
1120
  if (items.length === 0) return '';
1121
  let groupHtml = `<div class="quota-group-title">${title}</div><div class="quota-grid">`;
1122
  items.forEach(({ modelId, quota }) => {
1123
- const percentage = (quota.remaining * 100).toFixed(0);
 
1124
  const barColor = percentage > 50 ? '#10b981' : percentage > 20 ? '#f59e0b' : '#ef4444';
1125
  const shortName = modelId.replace('models/', '').replace('publishers/google/', '');
1126
  groupHtml += `
@@ -1131,7 +1149,7 @@ function renderQuotaModal(quotaContent, quotaData) {
1131
  </div>
1132
  <div class="quota-info-row">
1133
  <span class="quota-reset">重置: ${quota.resetTime}</span>
1134
- <span class="quota-percentage">${percentage}%</span>
1135
  </div>
1136
  </div>
1137
  `;
@@ -1275,7 +1293,10 @@ document.getElementById('configForm').addEventListener('submit', async (e) => {
1275
  else if (key === 'DEFAULT_TOP_P') jsonConfig.defaults.topP = parseFloat(value) || undefined;
1276
  else if (key === 'DEFAULT_TOP_K') jsonConfig.defaults.topK = parseInt(value) || undefined;
1277
  else if (key === 'DEFAULT_MAX_TOKENS') jsonConfig.defaults.maxTokens = parseInt(value) || undefined;
1278
- else if (key === 'DEFAULT_THINKING_BUDGET') jsonConfig.defaults.thinkingBudget = parseInt(value) || undefined;
 
 
 
1279
  else if (key === 'TIMEOUT') jsonConfig.other.timeout = parseInt(value) || undefined;
1280
  else if (key === 'SKIP_PROJECT_ID_FETCH') jsonConfig.other.skipProjectIdFetch = value === 'true';
1281
  else if (key === 'ROTATION_STRATEGY') jsonConfig.rotation.strategy = value || undefined;
 
221
  modal.onclick = (e) => { if (e.target === modal) modal.remove(); };
222
  }
223
 
224
+ function createTokenFormBody({
225
+ title,
226
+ showAccess = true,
227
+ showRefresh = true,
228
+ showExpires = true
229
+ } = {}) {
230
+ const parts = [];
231
+ if (showAccess) {
232
+ parts.push('<input type="text" id="modalAccessToken" placeholder="Access Token (必填)">');
233
+ }
234
+ if (showRefresh) {
235
+ parts.push('<input type="text" id="modalRefreshToken" placeholder="Refresh Token (必填)">');
236
+ }
237
+ if (showExpires) {
238
+ parts.push('<input type="number" id="modalExpiresIn" placeholder="过期时间(秒)" value="3599">');
239
+ }
240
+ return `
241
  <div class="modal-content">
242
+ <div class="modal-title">${title}</div>
243
+ <div class="form-row">${parts.join('')}</div>
 
 
 
 
244
  <p style="font-size: 0.8rem; color: var(--text-light); margin-bottom: 12px;">💡 过期时间默认3599秒(约1小时)</p>
245
  <div class="modal-actions">
246
  <button class="btn btn-secondary" onclick="this.closest('.modal').remove()">取消</button>
 
248
  </div>
249
  </div>
250
  `;
251
+ }
252
+
253
+ function showManualModal() {
254
+ const modal = document.createElement('div');
255
+ modal.className = 'modal form-modal';
256
+ modal.innerHTML = createTokenFormBody({ title: '✏️ 手动填入Token' });
257
  document.body.appendChild(modal);
258
  modal.onclick = (e) => { if (e.target === modal) modal.remove(); };
259
  }
 
569
  }
570
  });
571
 
572
+ const percentage = minQuota.remaining * 100;
573
+ const percentageText = `${percentage.toFixed(2)}%`;
574
  const shortName = minModel.replace('models/', '').replace('publishers/google/', '').split('/').pop();
575
  const barColor = percentage > 50 ? '#10b981' : percentage > 20 ? '#f59e0b' : '#ef4444';
576
 
 
579
  <span class="quota-summary-icon">📊</span>
580
  <span class="quota-summary-model" title="${minModel}">${shortName}</span>
581
  <span class="quota-summary-bar"><span style="width:${percentage}%;background:${barColor}"></span></span>
582
+ <span class="quota-summary-pct">${percentageText}</span>
583
  `;
584
  }
585
 
 
644
  if (items.length === 0) return '';
645
  let groupHtml = '';
646
  items.forEach(({ modelId, quota }) => {
647
+ const percentage = quota.remaining * 100;
648
+ const percentageText = `${percentage.toFixed(2)}%`;
649
  const barColor = percentage > 50 ? '#10b981' : percentage > 20 ? '#f59e0b' : '#ef4444';
650
  const shortName = modelId.replace('models/', '').replace('publishers/google/', '').split('/').pop();
651
  // 紧凑的一行显示
 
654
  <span class="quota-detail-icon">${icon}</span>
655
  <span class="quota-detail-name">${shortName}</span>
656
  <span class="quota-detail-bar"><span style="width:${percentage}%;background:${barColor}"></span></span>
657
+ <span class="quota-detail-pct">${percentageText}</span>
658
  </div>
659
  `;
660
  });
 
1137
  if (items.length === 0) return '';
1138
  let groupHtml = `<div class="quota-group-title">${title}</div><div class="quota-grid">`;
1139
  items.forEach(({ modelId, quota }) => {
1140
+ const percentage = quota.remaining * 100;
1141
+ const percentageText = `${percentage.toFixed(2)}%`;
1142
  const barColor = percentage > 50 ? '#10b981' : percentage > 20 ? '#f59e0b' : '#ef4444';
1143
  const shortName = modelId.replace('models/', '').replace('publishers/google/', '');
1144
  groupHtml += `
 
1149
  </div>
1150
  <div class="quota-info-row">
1151
  <span class="quota-reset">重置: ${quota.resetTime}</span>
1152
+ <span class="quota-percentage">${percentageText}</span>
1153
  </div>
1154
  </div>
1155
  `;
 
1293
  else if (key === 'DEFAULT_TOP_P') jsonConfig.defaults.topP = parseFloat(value) || undefined;
1294
  else if (key === 'DEFAULT_TOP_K') jsonConfig.defaults.topK = parseInt(value) || undefined;
1295
  else if (key === 'DEFAULT_MAX_TOKENS') jsonConfig.defaults.maxTokens = parseInt(value) || undefined;
1296
+ else if (key === 'DEFAULT_THINKING_BUDGET') {
1297
+ const num = parseInt(value);
1298
+ jsonConfig.defaults.thinkingBudget = Number.isNaN(num) ? undefined : num;
1299
+ }
1300
  else if (key === 'TIMEOUT') jsonConfig.other.timeout = parseInt(value) || undefined;
1301
  else if (key === 'SKIP_PROJECT_ID_FETCH') jsonConfig.other.skipProjectIdFetch = value === 'true';
1302
  else if (key === 'ROTATION_STRATEGY') jsonConfig.rotation.strategy = value || undefined;
scripts/oauth-server.js CHANGED
@@ -4,75 +4,57 @@ 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);
 
14
  const ACCOUNTS_FILE = path.join(__dirname, '..', 'data', 'accounts.json');
15
-
16
- const CLIENT_ID = '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com';
17
- const CLIENT_SECRET = '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';
18
  const STATE = crypto.randomUUID();
19
 
20
- const SCOPES = [
21
- 'https://www.googleapis.com/auth/cloud-platform',
22
- 'https://www.googleapis.com/auth/userinfo.email',
23
- 'https://www.googleapis.com/auth/userinfo.profile',
24
- 'https://www.googleapis.com/auth/cclog',
25
- 'https://www.googleapis.com/auth/experimentsandconfigs'
26
- ];
27
 
28
  function generateAuthUrl(port) {
29
  const params = new URLSearchParams({
30
  access_type: 'offline',
31
- client_id: CLIENT_ID,
32
  prompt: 'consent',
33
  redirect_uri: `http://localhost:${port}/oauth-callback`,
34
  response_type: 'code',
35
  scope: SCOPES.join(' '),
36
  state: STATE
37
  });
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 fetchUserEmail(accessToken) {
75
- const response = await axios({
76
  method: 'GET',
77
  url: 'https://www.googleapis.com/oauth2/v2/userinfo',
78
  headers: {
@@ -81,28 +63,11 @@ async function fetchUserEmail(accessToken) {
81
  'Authorization': `Bearer ${accessToken}`,
82
  'Accept-Encoding': 'gzip'
83
  },
84
- ...getAxiosConfig()
85
- });
86
  return response.data?.email;
87
  }
88
 
89
- async function fetchProjectId(accessToken) {
90
- const response = await axios({
91
- method: 'POST',
92
- url: 'https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:loadCodeAssist',
93
- headers: {
94
- 'Host': 'daily-cloudcode-pa.sandbox.googleapis.com',
95
- 'User-Agent': 'antigravity/1.11.9 windows/amd64',
96
- 'Authorization': `Bearer ${accessToken}`,
97
- 'Content-Type': 'application/json',
98
- 'Accept-Encoding': 'gzip'
99
- },
100
- data: JSON.stringify({ metadata: { ideType: 'ANTIGRAVITY' } }),
101
- ...getAxiosConfig()
102
- });
103
- return response.data?.cloudaicompanionProject;
104
- }
105
-
106
  const server = http.createServer((req, res) => {
107
  const port = server.address().port;
108
  const url = new URL(req.url, `http://localhost:${port}`);
@@ -138,7 +103,7 @@ const server = http.createServer((req, res) => {
138
  } else {
139
  log.info('正在验证账号资格...');
140
  try {
141
- const projectId = await fetchProjectId(account.access_token);
142
  if (projectId === undefined) {
143
  log.warn('该账号无资格使用(无法获取projectId),已跳过保存');
144
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
@@ -158,26 +123,13 @@ const server = http.createServer((req, res) => {
158
  }
159
  }
160
 
161
- let accounts = [];
162
- try {
163
- if (fs.existsSync(ACCOUNTS_FILE)) {
164
- accounts = JSON.parse(fs.readFileSync(ACCOUNTS_FILE, 'utf-8'));
165
- }
166
- } catch (err) {
167
- log.warn('读取 accounts.json 失败,将创建新文件');
168
- }
169
-
170
- accounts.push(account);
171
-
172
- const dir = path.dirname(ACCOUNTS_FILE);
173
- if (!fs.existsSync(dir)) {
174
- fs.mkdirSync(dir, { recursive: true });
175
  }
176
 
177
- fs.writeFileSync(ACCOUNTS_FILE, JSON.stringify(accounts, null, 2));
178
-
179
- log.info(`Token 已保存到 ${ACCOUNTS_FILE}`);
180
-
181
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
182
  res.end('<h1>授权成功!</h1><p>Token 已保存,可以关闭此页面。</p>');
183
 
 
4
  import fs from 'fs';
5
  import path from 'path';
6
  import { fileURLToPath } from 'url';
 
7
  import axios from 'axios';
8
+ import log from '../src/utils/logger.js';
9
  import config from '../src/config/config.js';
10
  import { generateProjectId } from '../src/utils/idGenerator.js';
11
+ import tokenManager from '../src/auth/token_manager.js';
12
+ import { OAUTH_CONFIG, OAUTH_SCOPES } from '../src/constants/oauth.js';
13
+ import { buildAxiosRequestConfig } from '../src/utils/httpClient.js';
14
 
15
  const __filename = fileURLToPath(import.meta.url);
16
  const __dirname = path.dirname(__filename);
17
+ // 账号文件路径保持不变,仅用于日志展示,具体读写交给 TokenManager 处理
18
  const ACCOUNTS_FILE = path.join(__dirname, '..', 'data', 'accounts.json');
 
 
 
19
  const STATE = crypto.randomUUID();
20
 
21
+ const SCOPES = OAUTH_SCOPES;
 
 
 
 
 
 
22
 
23
  function generateAuthUrl(port) {
24
  const params = new URLSearchParams({
25
  access_type: 'offline',
26
+ client_id: OAUTH_CONFIG.CLIENT_ID,
27
  prompt: 'consent',
28
  redirect_uri: `http://localhost:${port}/oauth-callback`,
29
  response_type: 'code',
30
  scope: SCOPES.join(' '),
31
  state: STATE
32
  });
33
+ return `${OAUTH_CONFIG.AUTH_URL}?${params.toString()}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  }
35
 
36
  async function exchangeCodeForToken(code, port) {
37
  const postData = new URLSearchParams({
38
  code,
39
+ client_id: OAUTH_CONFIG.CLIENT_ID,
40
+ client_secret: OAUTH_CONFIG.CLIENT_SECRET,
41
  redirect_uri: `http://localhost:${port}/oauth-callback`,
42
  grant_type: 'authorization_code'
43
  });
44
 
45
+ const response = await axios(buildAxiosRequestConfig({
46
  method: 'POST',
47
+ url: OAUTH_CONFIG.TOKEN_URL,
48
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
49
  data: postData.toString(),
50
+ timeout: config.timeout
51
+ }));
52
 
53
  return response.data;
54
  }
55
 
56
  async function fetchUserEmail(accessToken) {
57
+ const response = await axios(buildAxiosRequestConfig({
58
  method: 'GET',
59
  url: 'https://www.googleapis.com/oauth2/v2/userinfo',
60
  headers: {
 
63
  'Authorization': `Bearer ${accessToken}`,
64
  'Accept-Encoding': 'gzip'
65
  },
66
+ timeout: config.timeout
67
+ }));
68
  return response.data?.email;
69
  }
70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  const server = http.createServer((req, res) => {
72
  const port = server.address().port;
73
  const url = new URL(req.url, `http://localhost:${port}`);
 
103
  } else {
104
  log.info('正在验证账号资格...');
105
  try {
106
+ const projectId = await tokenManager.fetchProjectId({ access_token: account.access_token });
107
  if (projectId === undefined) {
108
  log.warn('该账号无资格使用(无法获取projectId),已跳过保存');
109
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
 
123
  }
124
  }
125
 
126
+ const result = tokenManager.addToken(account);
127
+ if (result.success) {
128
+ log.info(`Token 已保存到 ${ACCOUNTS_FILE}`);
129
+ } else {
130
+ log.error('保存 Token 失败:', result.message);
 
 
 
 
 
 
 
 
 
131
  }
132
 
 
 
 
 
133
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
134
  res.end('<h1>授权成功!</h1><p>Token 已保存,可以关闭此页面。</p>');
135
 
scripts/refresh-tokens.js CHANGED
@@ -1,84 +1,52 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
  import { fileURLToPath } from 'url';
 
4
  import log from '../src/utils/logger.js';
 
5
 
6
  const __filename = fileURLToPath(import.meta.url);
7
  const __dirname = path.dirname(__filename);
8
  const ACCOUNTS_FILE = path.join(__dirname, '..', 'data', 'accounts.json');
9
 
10
- const CLIENT_ID = '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com';
11
- const CLIENT_SECRET = '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';
12
-
13
- async function refreshToken(refreshToken) {
14
- const body = new URLSearchParams({
15
- client_id: CLIENT_ID,
16
- client_secret: CLIENT_SECRET,
17
- grant_type: 'refresh_token',
18
- refresh_token: refreshToken
19
- });
20
-
21
- const response = await fetch('https://oauth2.googleapis.com/token', {
22
- method: 'POST',
23
- headers: {
24
- 'Host': 'oauth2.googleapis.com',
25
- 'User-Agent': 'Go-http-client/1.1',
26
- 'Content-Length': body.toString().length.toString(),
27
- 'Content-Type': 'application/x-www-form-urlencoded',
28
- 'Accept-Encoding': 'gzip'
29
- },
30
- body: body.toString()
31
- });
32
-
33
- if (!response.ok) {
34
- throw new Error(`HTTP ${response.status}: ${await response.text()}`);
35
- }
36
-
37
- return await response.json();
38
- }
39
-
40
  async function refreshAllTokens() {
41
- if (!fs.existsSync(ACCOUNTS_FILE)) {
42
- log.error(`文件不存在: ${ACCOUNTS_FILE}`);
43
- process.exit(1);
 
44
  }
45
 
46
- const accounts = JSON.parse(fs.readFileSync(ACCOUNTS_FILE, 'utf-8'));
47
- log.info(`找到 ${accounts.length} 个账号`);
48
 
49
  let successCount = 0;
50
  let failCount = 0;
51
 
52
- for (let i = 0; i < accounts.length; i++) {
53
- const account = accounts[i];
54
-
55
- if (account.enable === false) {
56
  log.warn(`账号 ${i + 1}: 已禁用,跳过`);
57
  continue;
58
  }
59
 
60
  try {
61
  log.info(`刷新账号 ${i + 1}...`);
62
- const tokenData = await refreshToken(account.refresh_token);
63
- account.access_token = tokenData.access_token;
64
- account.expires_in = tokenData.expires_in;
65
- account.timestamp = Date.now();
66
-
67
  successCount++;
68
  log.info(`账号 ${i + 1}: 刷新成功`);
69
  } catch (error) {
70
  failCount++;
 
71
  log.error(`账号 ${i + 1}: 刷新失败 - ${error.message}`);
72
-
73
- if (error.message.includes('invalid_grant') || error.message.includes('400')) {
74
- account.enable = false;
 
75
  log.warn(`账号 ${i + 1}: Token 已失效或错误,已自动禁用该账号`);
76
  }
77
  }
78
  }
79
 
80
- fs.writeFileSync(ACCOUNTS_FILE, JSON.stringify(accounts, null, 2));
81
  log.info(`刷新完成: 成功 ${successCount} 个, 失败 ${failCount} 个`);
 
82
  }
83
 
84
  refreshAllTokens().catch(err => {
 
 
 
1
  import { fileURLToPath } from 'url';
2
+ import path from 'path';
3
  import log from '../src/utils/logger.js';
4
+ import tokenManager from '../src/auth/token_manager.js';
5
 
6
  const __filename = fileURLToPath(import.meta.url);
7
  const __dirname = path.dirname(__filename);
8
  const ACCOUNTS_FILE = path.join(__dirname, '..', 'data', 'accounts.json');
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  async function refreshAllTokens() {
11
+ const tokens = tokenManager.getTokenList();
12
+ if (!tokens || tokens.length === 0) {
13
+ log.warn('未找到可刷新账号');
14
+ return;
15
  }
16
 
17
+ log.info(`找到 ${tokens.length} 个账号`);
 
18
 
19
  let successCount = 0;
20
  let failCount = 0;
21
 
22
+ for (let i = 0; i < tokens.length; i++) {
23
+ const token = tokens[i];
24
+
25
+ if (token.enable === false) {
26
  log.warn(`账号 ${i + 1}: 已禁用,跳过`);
27
  continue;
28
  }
29
 
30
  try {
31
  log.info(`刷新账号 ${i + 1}...`);
32
+ await tokenManager.refreshToken(token);
 
 
 
 
33
  successCount++;
34
  log.info(`账号 ${i + 1}: 刷新成功`);
35
  } catch (error) {
36
  failCount++;
37
+ const statusCode = error.statusCode;
38
  log.error(`账号 ${i + 1}: 刷新失败 - ${error.message}`);
39
+
40
+ // 对于 400/403 之类错误,统一禁用该账号,行为与运行时一致
41
+ if (statusCode === 400 || statusCode === 403) {
42
+ tokenManager.disableToken(token);
43
  log.warn(`账号 ${i + 1}: Token 已失效或错误,已自动禁用该账号`);
44
  }
45
  }
46
  }
47
 
 
48
  log.info(`刷新完成: 成功 ${successCount} 个, 失败 ${failCount} 个`);
49
+ log.info(`账号文件路径: ${ACCOUNTS_FILE}`);
50
  }
51
 
52
  refreshAllTokens().catch(err => {
src/api/client.js CHANGED
@@ -5,8 +5,8 @@ import { generateToolCallId } from '../utils/idGenerator.js';
5
  import AntigravityRequester from '../AntigravityRequester.js';
6
  import { saveBase64Image } from '../utils/imageStorage.js';
7
  import logger from '../utils/logger.js';
8
- import { httpAgent, httpsAgent } from '../utils/utils.js';
9
- import memoryManager, { MemoryPressure } from '../utils/memoryManager.js';
10
 
11
  // 请求客户端:优先使用 AntigravityRequester,失败则降级到 axios
12
  let requester = null;
@@ -136,17 +136,11 @@ const releaseToolCallObject = (obj) => {
136
 
137
  // 注册内存清理回调
138
  function registerMemoryCleanup() {
 
 
 
 
139
  memoryManager.registerCleanup((pressure) => {
140
- const poolSizes = memoryManager.getPoolSizes();
141
-
142
- // 根据压力缩减对象池
143
- while (toolCallPool.length > poolSizes.toolCall) {
144
- toolCallPool.pop();
145
- }
146
- while (lineBufferPool.length > poolSizes.lineBuffer) {
147
- lineBufferPool.pop();
148
- }
149
-
150
  // 高压力或紧急时清理模型缓存
151
  if (pressure === MemoryPressure.HIGH || pressure === MemoryPressure.CRITICAL) {
152
  const ttl = getModelCacheTTL();
@@ -183,21 +177,12 @@ function buildHeaders(token) {
183
  }
184
 
185
  function buildAxiosConfig(url, headers, body = null) {
186
- const axiosConfig = {
187
  method: 'POST',
188
  url,
189
  headers,
190
- timeout: config.timeout,
191
- // 使用自定义 DNS 解析的 Agent(优先 IPv4,失败则 IPv6)
192
- httpAgent,
193
- httpsAgent,
194
- proxy: config.proxy ? (() => {
195
- const proxyUrl = new URL(config.proxy);
196
- return { protocol: proxyUrl.protocol.replace(':', ''), host: proxyUrl.hostname, port: parseInt(proxyUrl.port) };
197
- })() : false
198
- };
199
- if (body !== null) axiosConfig.data = body;
200
- return axiosConfig;
201
  }
202
 
203
  function buildRequesterConfig(headers, body = null) {
@@ -251,10 +236,10 @@ function convertToToolCall(functionCall) {
251
  // 解析并发送流式响应片段(会修改 state 并触发 callback)
252
  // 支持 DeepSeek 格式:思维链内容通过 reasoning_content 字段输出
253
  function parseAndEmitStreamChunk(line, state, callback) {
254
- if (!line.startsWith('data: ')) return;
255
 
256
  try {
257
- const data = JSON.parse(line.slice(6));
258
  //console.log(JSON.stringify(data));
259
  const parts = data.response?.candidates?.[0]?.content?.parts;
260
 
@@ -361,12 +346,29 @@ export async function generateAssistantResponse(requestBody, token, callback) {
361
  }
362
  }
363
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
  export async function getAvailableModels() {
365
  // 检查缓存是否有效(动态 TTL)
366
  const now = Date.now();
367
  const ttl = getModelCacheTTL();
368
  if (modelListCache && (now - modelListCacheTime) < ttl) {
369
- logger.info(`使用缓存的模型列表 (剩余有效期: ${Math.round((ttl - (now - modelListCacheTime)) / 1000)}秒)`);
370
  return modelListCache;
371
  }
372
 
@@ -378,63 +380,45 @@ export async function getAvailableModels() {
378
  }
379
 
380
  const headers = buildHeaders(token);
 
 
 
 
 
 
 
 
 
 
 
 
 
381
 
382
- try {
383
- let data;
384
- if (useAxios) {
385
- data = (await axios(buildAxiosConfig(config.api.modelsUrl, headers, {}))).data;
386
- } else {
387
- const response = await requester.antigravity_fetch(config.api.modelsUrl, buildRequesterConfig(headers, {}));
388
- if (response.status !== 200) {
389
- const errorBody = await response.text();
390
- throw { status: response.status, message: errorBody };
391
- }
392
- data = await response.json();
393
- }
394
- //console.log(JSON.stringify(data,null,2));
395
- const created = Math.floor(Date.now() / 1000);
396
- const modelList = Object.keys(data.models || {}).map(id => ({
397
- id,
398
  object: 'model',
399
  created,
400
  owned_by: 'google'
401
- }));
402
-
403
- // 添加默认模型(如果 API 返回的列表中没有)
404
- const existingIds = new Set(modelList.map(m => m.id));
405
- for (const defaultModel of DEFAULT_MODELS) {
406
- if (!existingIds.has(defaultModel)) {
407
- modelList.push({
408
- id: defaultModel,
409
- object: 'model',
410
- created,
411
- owned_by: 'google'
412
- });
413
- }
414
- }
415
-
416
- const result = {
417
- object: 'list',
418
- data: modelList
419
- };
420
-
421
- // 更新缓存
422
- modelListCache = result;
423
- modelListCacheTime = now;
424
- const currentTTL = getModelCacheTTL();
425
- logger.info(`模型列表已缓存 (有效期: ${currentTTL / 1000}秒, 模型数量: ${modelList.length})`);
426
-
427
- return result;
428
- } catch (error) {
429
- // 如果请求失败但有缓存,返回过期的缓存
430
- if (modelListCache) {
431
- logger.warn(`获取模型列表失败,使用过期缓存: ${error.message}`);
432
- return modelListCache;
433
  }
434
- // 没有缓存时返回默认模型列表
435
- logger.warn(`获取模型列表失败,返回默认模型列表: ${error.message}`);
436
- return getDefaultModelList();
437
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
438
  }
439
 
440
  // 清除模型列表缓存(可用于手动刷新)
@@ -446,34 +430,20 @@ export function clearModelListCache() {
446
 
447
  export async function getModelsWithQuotas(token) {
448
  const headers = buildHeaders(token);
449
-
450
- try {
451
- let data;
452
- if (useAxios) {
453
- data = (await axios(buildAxiosConfig(config.api.modelsUrl, headers, {}))).data;
454
- } else {
455
- const response = await requester.antigravity_fetch(config.api.modelsUrl, buildRequesterConfig(headers, {}));
456
- if (response.status !== 200) {
457
- const errorBody = await response.text();
458
- throw { status: response.status, message: errorBody };
459
- }
460
- data = await response.json();
461
  }
462
-
463
- const quotas = {};
464
- Object.entries(data.models || {}).forEach(([modelId, modelData]) => {
465
- if (modelData.quotaInfo) {
466
- quotas[modelId] = {
467
- r: modelData.quotaInfo.remainingFraction,
468
- t: modelData.quotaInfo.resetTime
469
- };
470
- }
471
- });
472
-
473
- return quotas;
474
- } catch (error) {
475
- await handleApiError(error, token);
476
- }
477
  }
478
 
479
  export async function generateAssistantResponseNoStream(requestBody, token) {
 
5
  import AntigravityRequester from '../AntigravityRequester.js';
6
  import { saveBase64Image } from '../utils/imageStorage.js';
7
  import logger from '../utils/logger.js';
8
+ import memoryManager, { MemoryPressure, registerMemoryPoolCleanup } from '../utils/memoryManager.js';
9
+ import { buildAxiosRequestConfig } from '../utils/httpClient.js';
10
 
11
  // 请求客户端:优先使用 AntigravityRequester,失败则降级到 axios
12
  let requester = null;
 
136
 
137
  // 注册内存清理回调
138
  function registerMemoryCleanup() {
139
+ // 使用通用池清理工具,避免重复 while-pop 逻辑
140
+ registerMemoryPoolCleanup(toolCallPool, () => memoryManager.getPoolSizes().toolCall);
141
+ registerMemoryPoolCleanup(lineBufferPool, () => memoryManager.getPoolSizes().lineBuffer);
142
+
143
  memoryManager.registerCleanup((pressure) => {
 
 
 
 
 
 
 
 
 
 
144
  // 高压力或紧急时清理模型缓存
145
  if (pressure === MemoryPressure.HIGH || pressure === MemoryPressure.CRITICAL) {
146
  const ttl = getModelCacheTTL();
 
177
  }
178
 
179
  function buildAxiosConfig(url, headers, body = null) {
180
+ return buildAxiosRequestConfig({
181
  method: 'POST',
182
  url,
183
  headers,
184
+ data: body
185
+ });
 
 
 
 
 
 
 
 
 
186
  }
187
 
188
  function buildRequesterConfig(headers, body = null) {
 
236
  // 解析并发送流式响应片段(会修改 state 并触发 callback)
237
  // 支持 DeepSeek 格式:思维链内容通过 reasoning_content 字段输出
238
  function parseAndEmitStreamChunk(line, state, callback) {
239
+ if (!line.startsWith(DATA_PREFIX)) return;
240
 
241
  try {
242
+ const data = JSON.parse(line.slice(DATA_PREFIX_LEN));
243
  //console.log(JSON.stringify(data));
244
  const parts = data.response?.candidates?.[0]?.content?.parts;
245
 
 
346
  }
347
  }
348
 
349
+ // 内部工具:从远端拉取完整模型原始数据
350
+ async function fetchRawModels(headers, token) {
351
+ try {
352
+ if (useAxios) {
353
+ const response = await axios(buildAxiosConfig(config.api.modelsUrl, headers, {}));
354
+ return response.data;
355
+ }
356
+ const response = await requester.antigravity_fetch(config.api.modelsUrl, buildRequesterConfig(headers, {}));
357
+ if (response.status !== 200) {
358
+ const errorBody = await response.text();
359
+ throw { status: response.status, message: errorBody };
360
+ }
361
+ return await response.json();
362
+ } catch (error) {
363
+ await handleApiError(error, token);
364
+ }
365
+ }
366
+
367
  export async function getAvailableModels() {
368
  // 检查缓存是否有效(动态 TTL)
369
  const now = Date.now();
370
  const ttl = getModelCacheTTL();
371
  if (modelListCache && (now - modelListCacheTime) < ttl) {
 
372
  return modelListCache;
373
  }
374
 
 
380
  }
381
 
382
  const headers = buildHeaders(token);
383
+ const data = await fetchRawModels(headers, token);
384
+ if (!data) {
385
+ // fetchRawModels 里已经做了统一错误处理,这里兜底为默认列表
386
+ return getDefaultModelList();
387
+ }
388
+
389
+ const created = Math.floor(Date.now() / 1000);
390
+ const modelList = Object.keys(data.models || {}).map(id => ({
391
+ id,
392
+ object: 'model',
393
+ created,
394
+ owned_by: 'google'
395
+ }));
396
 
397
+ // 添加默认模型(如果 API 返回的列表中没有)
398
+ const existingIds = new Set(modelList.map(m => m.id));
399
+ for (const defaultModel of DEFAULT_MODELS) {
400
+ if (!existingIds.has(defaultModel)) {
401
+ modelList.push({
402
+ id: defaultModel,
 
 
 
 
 
 
 
 
 
 
403
  object: 'model',
404
  created,
405
  owned_by: 'google'
406
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
  }
 
 
 
408
  }
409
+
410
+ const result = {
411
+ object: 'list',
412
+ data: modelList
413
+ };
414
+
415
+ // 更新缓存
416
+ modelListCache = result;
417
+ modelListCacheTime = now;
418
+ const currentTTL = getModelCacheTTL();
419
+ logger.info(`模型列表已缓存 (有效期: ${currentTTL / 1000}秒, 模型数量: ${modelList.length})`);
420
+
421
+ return result;
422
  }
423
 
424
  // 清除模型列表缓存(可用于手动刷新)
 
430
 
431
  export async function getModelsWithQuotas(token) {
432
  const headers = buildHeaders(token);
433
+ const data = await fetchRawModels(headers, token);
434
+ if (!data) return {};
435
+
436
+ const quotas = {};
437
+ Object.entries(data.models || {}).forEach(([modelId, modelData]) => {
438
+ if (modelData.quotaInfo) {
439
+ quotas[modelId] = {
440
+ r: modelData.quotaInfo.remainingFraction,
441
+ t: modelData.quotaInfo.resetTime
442
+ };
 
 
443
  }
444
+ });
445
+
446
+ return quotas;
 
 
 
 
 
 
 
 
 
 
 
 
447
  }
448
 
449
  export async function generateAssistantResponseNoStream(requestBody, token) {
src/auth/token_manager.js CHANGED
@@ -6,6 +6,7 @@ import { log } from '../utils/logger.js';
6
  import { generateSessionId, generateProjectId } from '../utils/idGenerator.js';
7
  import config, { getConfigJson } from '../config/config.js';
8
  import { OAUTH_CONFIG } from '../constants/oauth.js';
 
9
 
10
  const __filename = fileURLToPath(import.meta.url);
11
  const __dirname = path.dirname(__filename);
@@ -109,7 +110,7 @@ class TokenManager {
109
  }
110
 
111
  async fetchProjectId(token) {
112
- const response = await axios({
113
  method: 'POST',
114
  url: 'https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:loadCodeAssist',
115
  headers: {
@@ -119,13 +120,8 @@ class TokenManager {
119
  'Content-Type': 'application/json',
120
  'Accept-Encoding': 'gzip'
121
  },
122
- data: JSON.stringify({ metadata: { ideType: 'ANTIGRAVITY' } }),
123
- timeout: config.timeout,
124
- proxy: config.proxy ? (() => {
125
- const proxyUrl = new URL(config.proxy);
126
- return { protocol: proxyUrl.protocol.replace(':', ''), host: proxyUrl.hostname, port: parseInt(proxyUrl.port) };
127
- })() : false
128
- });
129
  return response.data?.cloudaicompanionProject;
130
  }
131
 
@@ -145,7 +141,7 @@ class TokenManager {
145
  });
146
 
147
  try {
148
- const response = await axios({
149
  method: 'POST',
150
  url: OAUTH_CONFIG.TOKEN_URL,
151
  headers: {
@@ -154,13 +150,8 @@ class TokenManager {
154
  'Content-Type': 'application/x-www-form-urlencoded',
155
  'Accept-Encoding': 'gzip'
156
  },
157
- data: body.toString(),
158
- timeout: config.timeout,
159
- proxy: config.proxy ? (() => {
160
- const proxyUrl = new URL(config.proxy);
161
- return { protocol: proxyUrl.protocol.replace(':', ''), host: proxyUrl.hostname, port: parseInt(proxyUrl.port) };
162
- })() : false
163
- });
164
 
165
  token.access_token = response.data.access_token;
166
  token.expires_in = response.data.expires_in;
 
6
  import { generateSessionId, generateProjectId } from '../utils/idGenerator.js';
7
  import config, { getConfigJson } from '../config/config.js';
8
  import { OAUTH_CONFIG } from '../constants/oauth.js';
9
+ import { buildAxiosRequestConfig } from '../utils/httpClient.js';
10
 
11
  const __filename = fileURLToPath(import.meta.url);
12
  const __dirname = path.dirname(__filename);
 
110
  }
111
 
112
  async fetchProjectId(token) {
113
+ const response = await axios(buildAxiosRequestConfig({
114
  method: 'POST',
115
  url: 'https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:loadCodeAssist',
116
  headers: {
 
120
  'Content-Type': 'application/json',
121
  'Accept-Encoding': 'gzip'
122
  },
123
+ data: JSON.stringify({ metadata: { ideType: 'ANTIGRAVITY' } })
124
+ }));
 
 
 
 
 
125
  return response.data?.cloudaicompanionProject;
126
  }
127
 
 
141
  });
142
 
143
  try {
144
+ const response = await axios(buildAxiosRequestConfig({
145
  method: 'POST',
146
  url: OAUTH_CONFIG.TOKEN_URL,
147
  headers: {
 
150
  'Content-Type': 'application/x-www-form-urlencoded',
151
  'Accept-Encoding': 'gzip'
152
  },
153
+ data: body.toString()
154
+ }));
 
 
 
 
 
155
 
156
  token.access_token = response.data.access_token;
157
  token.expires_in = response.data.expires_in;
src/config/config.js CHANGED
@@ -28,7 +28,7 @@ if (fs.existsSync(configJsonPath)) {
28
  dotenv.config();
29
 
30
  // 获取代理配置:优先使用 PROXY,其次使用系统代理环境变量
31
- function getProxyConfig() {
32
  // 优先使用显式配置的 PROXY
33
  if (process.env.PROXY) {
34
  return process.env.PROXY;
@@ -77,7 +77,7 @@ const config = {
77
  top_p: jsonConfig.defaults?.topP || 0.85,
78
  top_k: jsonConfig.defaults?.topK || 50,
79
  max_tokens: jsonConfig.defaults?.maxTokens || 32000,
80
- thinking_budget: jsonConfig.defaults?.thinkingBudget || 16000
81
  },
82
  security: {
83
  maxRequestSize: jsonConfig.server?.maxRequestSize || '50mb',
 
28
  dotenv.config();
29
 
30
  // 获取代理配置:优先使用 PROXY,其次使用系统代理环境变量
31
+ export function getProxyConfig() {
32
  // 优先使用显式配置的 PROXY
33
  if (process.env.PROXY) {
34
  return process.env.PROXY;
 
77
  top_p: jsonConfig.defaults?.topP || 0.85,
78
  top_k: jsonConfig.defaults?.topK || 50,
79
  max_tokens: jsonConfig.defaults?.maxTokens || 32000,
80
+ thinking_budget: jsonConfig.defaults?.thinkingBudget ?? 1024
81
  },
82
  security: {
83
  maxRequestSize: jsonConfig.server?.maxRequestSize || '50mb',
src/constants/oauth.js CHANGED
@@ -1,9 +1,19 @@
1
  /**
2
- * Google OAuth 凭证
3
- * 统一管理,避免在多个文件中重复定义
4
  */
5
  export const OAUTH_CONFIG = {
6
  CLIENT_ID: '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com',
7
  CLIENT_SECRET: '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',
8
- TOKEN_URL: 'https://oauth2.googleapis.com/token'
 
9
  };
 
 
 
 
 
 
 
 
 
 
1
  /**
2
+ * Google OAuth 配置
3
+ * 统一管理,避免在多个文件中重复定义和硬编码
4
  */
5
  export const OAUTH_CONFIG = {
6
  CLIENT_ID: '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com',
7
  CLIENT_SECRET: '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',
8
+ TOKEN_URL: 'https://oauth2.googleapis.com/token',
9
+ AUTH_URL: 'https://accounts.google.com/o/oauth2/v2/auth'
10
  };
11
+
12
+ // 服务器端使用的默认 OAuth Scope 列表
13
+ export const OAUTH_SCOPES = [
14
+ 'https://www.googleapis.com/auth/cloud-platform',
15
+ 'https://www.googleapis.com/auth/userinfo.email',
16
+ 'https://www.googleapis.com/auth/userinfo.profile',
17
+ 'https://www.googleapis.com/auth/cclog',
18
+ 'https://www.googleapis.com/auth/experimentsandconfigs'
19
+ ];
src/routes/sd.js CHANGED
@@ -1,6 +1,6 @@
1
  import express from 'express';
2
  import { getAvailableModels, generateImageForSD } from '../api/client.js';
3
- import { generateRequestBody } from '../utils/utils.js';
4
  import tokenManager from '../auth/token_manager.js';
5
  import logger from '../utils/logger.js';
6
 
@@ -72,12 +72,7 @@ const SD_MOCK_DATA = {
72
  function buildImageRequestBody(prompt, token) {
73
  const messages = [{ role: 'user', content: prompt }];
74
  const requestBody = generateRequestBody(messages, 'gemini-3-pro-image', {}, null, token);
75
- requestBody.request.generationConfig = { candidateCount: 1 };
76
- requestBody.requestType = 'image_gen';
77
- delete requestBody.request.systemInstruction;
78
- delete requestBody.request.tools;
79
- delete requestBody.request.toolConfig;
80
- return requestBody;
81
  }
82
 
83
  // GET 路由
@@ -141,12 +136,9 @@ router.post('/img2img', async (req, res) => {
141
  }
142
 
143
  const messages = [{ role: 'user', content }];
144
- const requestBody = generateRequestBody(messages, 'gemini-3-pro-image', {}, null, token);
145
- requestBody.request.generationConfig = { candidateCount: 1 };
146
- requestBody.requestType = 'image_gen';
147
- delete requestBody.request.systemInstruction;
148
- delete requestBody.request.tools;
149
- delete requestBody.request.toolConfig;
150
 
151
  const images = await generateImageForSD(requestBody, token);
152
 
 
1
  import express from 'express';
2
  import { getAvailableModels, generateImageForSD } from '../api/client.js';
3
+ import { generateRequestBody, prepareImageRequest } from '../utils/utils.js';
4
  import tokenManager from '../auth/token_manager.js';
5
  import logger from '../utils/logger.js';
6
 
 
72
  function buildImageRequestBody(prompt, token) {
73
  const messages = [{ role: 'user', content: prompt }];
74
  const requestBody = generateRequestBody(messages, 'gemini-3-pro-image', {}, null, token);
75
+ return prepareImageRequest(requestBody);
 
 
 
 
 
76
  }
77
 
78
  // GET 路由
 
136
  }
137
 
138
  const messages = [{ role: 'user', content }];
139
+ const requestBody = prepareImageRequest(
140
+ generateRequestBody(messages, 'gemini-3-pro-image', {}, null, token)
141
+ );
 
 
 
142
 
143
  const images = await generateImageForSD(requestBody, token);
144
 
src/server/index.js CHANGED
@@ -3,13 +3,13 @@ import cors from 'cors';
3
  import path from 'path';
4
  import { fileURLToPath } from 'url';
5
  import { generateAssistantResponse, generateAssistantResponseNoStream, getAvailableModels, generateImageForSD, closeRequester } from '../api/client.js';
6
- import { generateRequestBody } from '../utils/utils.js';
7
  import logger from '../utils/logger.js';
8
  import config from '../config/config.js';
9
  import tokenManager from '../auth/token_manager.js';
10
  import adminRouter from '../routes/admin.js';
11
  import sdRouter from '../routes/sd.js';
12
- import memoryManager, { MemoryPressure } from '../utils/memoryManager.js';
13
 
14
  const __filename = fileURLToPath(import.meta.url);
15
  const __dirname = path.dirname(__filename);
@@ -65,14 +65,8 @@ const releaseChunkObject = (obj) => {
65
  if (chunkPool.length < maxSize) chunkPool.push(obj);
66
  };
67
 
68
- // 注册内存清理回调
69
- memoryManager.registerCleanup((pressure) => {
70
- const poolSizes = memoryManager.getPoolSizes();
71
- // 根据压力缩减对象池
72
- while (chunkPool.length > poolSizes.chunk) {
73
- chunkPool.pop();
74
- }
75
- });
76
 
77
  // 启动内存管理器
78
  memoryManager.start(30000);
@@ -193,17 +187,7 @@ app.post('/v1/chat/completions', async (req, res) => {
193
  const isImageModel = model.includes('-image');
194
  const requestBody = generateRequestBody(messages, model, params, tools, token);
195
  if (isImageModel) {
196
- requestBody.request.generationConfig={
197
- candidateCount: 1,
198
- // imageConfig:{
199
- // aspectRatio: "1:1"
200
- // }
201
- }
202
- requestBody.requestType="image_gen";
203
- //requestBody.request.systemInstruction.parts[0].text += "现在你作为绘画模型聚焦于帮助用户生成图片";
204
- delete requestBody.request.systemInstruction;
205
- delete requestBody.request.tools;
206
- delete requestBody.request.toolConfig;
207
  }
208
  //console.log(JSON.stringify(requestBody,null,2))
209
 
@@ -233,7 +217,12 @@ app.post('/v1/chat/completions', async (req, res) => {
233
  writeStreamData(res, createStreamChunk(id, created, model, delta));
234
  } else if (data.type === 'tool_calls') {
235
  hasToolCall = true;
236
- const delta = { tool_calls: data.tool_calls };
 
 
 
 
 
237
  writeStreamData(res, createStreamChunk(id, created, model, delta));
238
  } else {
239
  const delta = { content: data.content };
 
3
  import path from 'path';
4
  import { fileURLToPath } from 'url';
5
  import { generateAssistantResponse, generateAssistantResponseNoStream, getAvailableModels, generateImageForSD, closeRequester } from '../api/client.js';
6
+ import { generateRequestBody, prepareImageRequest } from '../utils/utils.js';
7
  import logger from '../utils/logger.js';
8
  import config from '../config/config.js';
9
  import tokenManager from '../auth/token_manager.js';
10
  import adminRouter from '../routes/admin.js';
11
  import sdRouter from '../routes/sd.js';
12
+ import memoryManager, { MemoryPressure, registerMemoryPoolCleanup } from '../utils/memoryManager.js';
13
 
14
  const __filename = fileURLToPath(import.meta.url);
15
  const __dirname = path.dirname(__filename);
 
65
  if (chunkPool.length < maxSize) chunkPool.push(obj);
66
  };
67
 
68
+ // 注册内存清理回调(使用统一工具收缩对象池)
69
+ registerMemoryPoolCleanup(chunkPool, () => memoryManager.getPoolSizes().chunk);
 
 
 
 
 
 
70
 
71
  // 启动内存管理器
72
  memoryManager.start(30000);
 
187
  const isImageModel = model.includes('-image');
188
  const requestBody = generateRequestBody(messages, model, params, tools, token);
189
  if (isImageModel) {
190
+ prepareImageRequest(requestBody);
 
 
 
 
 
 
 
 
 
 
191
  }
192
  //console.log(JSON.stringify(requestBody,null,2))
193
 
 
217
  writeStreamData(res, createStreamChunk(id, created, model, delta));
218
  } else if (data.type === 'tool_calls') {
219
  hasToolCall = true;
220
+ // OpenAI 流式 schema 要求每个 tool_call 带有 index 字段
221
+ const toolCallsWithIndex = data.tool_calls.map((toolCall, index) => ({
222
+ index,
223
+ ...toolCall
224
+ }));
225
+ const delta = { tool_calls: toolCallsWithIndex };
226
  writeStreamData(res, createStreamChunk(id, created, model, delta));
227
  } else {
228
  const delta = { content: data.content };
src/utils/configReloader.js CHANGED
@@ -1,4 +1,4 @@
1
- import config, { getConfigJson } from '../config/config.js';
2
 
3
  /**
4
  * 配置字段映射表:config对象路径 -> config.json路径 / 环境变量
@@ -10,7 +10,7 @@ const CONFIG_MAPPING = [
10
  { target: 'defaults.top_p', source: 'defaults.topP', default: 0.85 },
11
  { target: 'defaults.top_k', source: 'defaults.topK', default: 50 },
12
  { target: 'defaults.max_tokens', source: 'defaults.maxTokens', default: 32000 },
13
- { target: 'defaults.thinking_budget', source: 'defaults.thinkingBudget', default: 16000 },
14
  { target: 'timeout', source: 'other.timeout', default: 300000 },
15
  { target: 'skipProjectIdFetch', source: 'other.skipProjectIdFetch', default: false, transform: v => v === true },
16
  { target: 'maxImages', source: 'other.maxImages', default: 10 },
@@ -22,25 +22,6 @@ const CONFIG_MAPPING = [
22
  { target: 'api.userAgent', source: 'api.userAgent', default: 'antigravity/1.11.3 windows/amd64' }
23
  ];
24
 
25
- /**
26
- * 获取代理配置:优先使用 PROXY,其次使用系统代理环境变量
27
- */
28
- function getProxyConfig() {
29
- // 优先使用显式配置的 PROXY
30
- if (process.env.PROXY) {
31
- return process.env.PROXY;
32
- }
33
-
34
- // 检查系统代理环境变量(按优先级)
35
- return process.env.HTTPS_PROXY ||
36
- process.env.https_proxy ||
37
- process.env.HTTP_PROXY ||
38
- process.env.http_proxy ||
39
- process.env.ALL_PROXY ||
40
- process.env.all_proxy ||
41
- null;
42
- }
43
-
44
  const ENV_MAPPING = [
45
  { target: 'security.apiKey', env: 'API_KEY', default: null },
46
  { target: 'systemInstruction', env: 'SYSTEM_INSTRUCTION', default: '' }
 
1
+ import config, { getConfigJson, getProxyConfig } from '../config/config.js';
2
 
3
  /**
4
  * 配置字段映射表:config对象路径 -> config.json路径 / 环境变量
 
10
  { target: 'defaults.top_p', source: 'defaults.topP', default: 0.85 },
11
  { target: 'defaults.top_k', source: 'defaults.topK', default: 50 },
12
  { target: 'defaults.max_tokens', source: 'defaults.maxTokens', default: 32000 },
13
+ { target: 'defaults.thinking_budget', source: 'defaults.thinkingBudget', default: 1024 },
14
  { target: 'timeout', source: 'other.timeout', default: 300000 },
15
  { target: 'skipProjectIdFetch', source: 'other.skipProjectIdFetch', default: false, transform: v => v === true },
16
  { target: 'maxImages', source: 'other.maxImages', default: 10 },
 
22
  { target: 'api.userAgent', source: 'api.userAgent', default: 'antigravity/1.11.3 windows/amd64' }
23
  ];
24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  const ENV_MAPPING = [
26
  { target: 'security.apiKey', env: 'API_KEY', default: null },
27
  { target: 'systemInstruction', env: 'SYSTEM_INSTRUCTION', default: '' }
src/utils/httpClient.js ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import axios from 'axios';
2
+ import dns from 'dns';
3
+ import http from 'http';
4
+ import https from 'https';
5
+ import config from '../config/config.js';
6
+
7
+ // ==================== DNS & 代理统一配置 ====================
8
+
9
+ // 自定义 DNS 解析:优先 IPv4,失败则回退 IPv6
10
+ function customLookup(hostname, options, callback) {
11
+ dns.lookup(hostname, { ...options, family: 4 }, (err4, address4, family4) => {
12
+ if (!err4 && address4) {
13
+ return callback(null, address4, family4);
14
+ }
15
+ dns.lookup(hostname, { ...options, family: 6 }, (err6, address6, family6) => {
16
+ if (!err6 && address6) {
17
+ return callback(null, address6, family6);
18
+ }
19
+ callback(err4 || err6);
20
+ });
21
+ });
22
+ }
23
+
24
+ // 使用自定义 DNS 解析的 Agent(优先 IPv4,失败则 IPv6)
25
+ const httpAgent = new http.Agent({
26
+ lookup: customLookup,
27
+ keepAlive: true
28
+ });
29
+
30
+ const httpsAgent = new https.Agent({
31
+ lookup: customLookup,
32
+ keepAlive: true
33
+ });
34
+
35
+ // 统一构建代理配置
36
+ function buildProxyConfig() {
37
+ if (!config.proxy) return false;
38
+ try {
39
+ const proxyUrl = new URL(config.proxy);
40
+ return {
41
+ protocol: proxyUrl.protocol.replace(':', ''),
42
+ host: proxyUrl.hostname,
43
+ port: parseInt(proxyUrl.port, 10)
44
+ };
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+
50
+ // 为 axios 构建统一请求配置
51
+ export function buildAxiosRequestConfig({ method = 'POST', url, headers, data = null, timeout = config.timeout }) {
52
+ const axiosConfig = {
53
+ method,
54
+ url,
55
+ headers,
56
+ timeout,
57
+ httpAgent,
58
+ httpsAgent,
59
+ proxy: buildProxyConfig()
60
+ };
61
+
62
+ if (data !== null) axiosConfig.data = data;
63
+ return axiosConfig;
64
+ }
65
+
66
+ // 简单封装 axios 调用,方便后续统一扩展(重试、打点等)
67
+ export async function httpRequest(configOverrides) {
68
+ const axiosConfig = buildAxiosRequestConfig(configOverrides);
69
+ return axios(axiosConfig);
70
+ }
src/utils/memoryManager.js CHANGED
@@ -265,4 +265,14 @@ class MemoryManager {
265
  // 单例导出
266
  const memoryManager = new MemoryManager();
267
  export default memoryManager;
 
 
 
 
 
 
 
 
 
 
268
  export { MemoryPressure, THRESHOLDS };
 
265
  // 单例导出
266
  const memoryManager = new MemoryManager();
267
  export default memoryManager;
268
+
269
+ // 统一封装注册清理回调,方便在各模块中保持一致风格
270
+ export function registerMemoryPoolCleanup(pool, getMaxSize) {
271
+ memoryManager.registerCleanup(() => {
272
+ const maxSize = getMaxSize();
273
+ while (pool.length > maxSize) {
274
+ pool.pop();
275
+ }
276
+ });
277
+ }
278
  export { MemoryPressure, THRESHOLDS };
src/utils/utils.js CHANGED
@@ -2,43 +2,6 @@ import config from '../config/config.js';
2
  import tokenManager from '../auth/token_manager.js';
3
  import { generateRequestId } from './idGenerator.js';
4
  import os from 'os';
5
- import dns from 'dns';
6
- import http from 'http';
7
- import https from 'https';
8
- import logger from './logger.js';
9
-
10
- // ==================== DNS 解析优化 ====================
11
- // 自定义 DNS 解析:优先 IPv4,失败则回退 IPv6
12
- function customLookup(hostname, options, callback) {
13
- // 先尝试 IPv4
14
- dns.lookup(hostname, { ...options, family: 4 }, (err4, address4, family4) => {
15
- if (!err4 && address4) {
16
- // IPv4 成功
17
- return callback(null, address4, family4);
18
- }
19
- // IPv4 失败,尝试 IPv6
20
- dns.lookup(hostname, { ...options, family: 6 }, (err6, address6, family6) => {
21
- if (!err6 && address6) {
22
- // IPv6 成功
23
- logger.debug(`DNS: ${hostname} IPv4 失败,使用 IPv6: ${address6}`);
24
- return callback(null, address6, family6);
25
- }
26
- // 都失败,返回 IPv4 的错误
27
- callback(err4 || err6);
28
- });
29
- });
30
- }
31
-
32
- // 创建使用自定义 DNS 解析的 HTTP/HTTPS Agent
33
- const httpAgent = new http.Agent({
34
- lookup: customLookup,
35
- keepAlive: true
36
- });
37
-
38
- const httpsAgent = new https.Agent({
39
- lookup: customLookup,
40
- keepAlive: true
41
- });
42
 
43
  function extractImagesFromContent(content) {
44
  const result = { text: '', images: [] };
@@ -224,7 +187,7 @@ function generateGenerationConfig(parameters, enableThinking, actualModelName){
224
  // 1. 优先使用 thinking_budget(直接数值)
225
  // 2. 其次使用 reasoning_effort(OpenAI 格式:low/medium/high)
226
  // 3. 最后使用配置默认值或硬编码默认值
227
- const defaultThinkingBudget = config.defaults.thinking_budget ?? 16000;
228
 
229
  let thinkingBudget = 0;
230
  if (enableThinking) {
@@ -260,7 +223,30 @@ function generateGenerationConfig(parameters, enableThinking, actualModelName){
260
  }
261
  return generationConfig
262
  }
263
- const EXCLUDED_KEYS = new Set(['$schema', 'additionalProperties', 'minLength', 'maxLength', 'minItems', 'maxItems', 'uniqueItems']);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
 
265
  function cleanParameters(obj) {
266
  if (!obj || typeof obj !== 'object') return obj;
@@ -269,7 +255,8 @@ function cleanParameters(obj) {
269
 
270
  for (const [key, value] of Object.entries(obj)) {
271
  if (EXCLUDED_KEYS.has(key)) continue;
272
- cleaned[key] = (value && typeof value === 'object') ? cleanParameters(value) : value;
 
273
  }
274
 
275
  return cleaned;
@@ -278,12 +265,26 @@ function cleanParameters(obj) {
278
  function convertOpenAIToolsToAntigravity(openaiTools){
279
  if (!openaiTools || openaiTools.length === 0) return [];
280
  return openaiTools.map((tool)=>{
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
  return {
282
  functionDeclarations: [
283
  {
284
  name: tool.function.name,
285
  description: tool.function.description,
286
- parameters: cleanParameters(tool.function.parameters)
287
  }
288
  ]
289
  }
@@ -302,11 +303,12 @@ function modelMapping(modelName){
302
  }
303
 
304
  function isEnableThinking(modelName){
305
- return modelName.endsWith('-thinking') ||
 
306
  modelName === 'gemini-2.5-pro' ||
307
  modelName.startsWith('gemini-3-pro-') ||
308
  modelName === "rev19-uic3-1p" ||
309
- modelName === "gpt-oss-120b-medium"
310
  }
311
 
312
  function generateRequestBody(openaiMessages,modelName,parameters,openaiTools,token){
@@ -356,17 +358,29 @@ function generateRequestBody(openaiMessages,modelName,parameters,openaiTools,tok
356
 
357
  return requestBody;
358
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
  function getDefaultIp(){
360
  const interfaces = os.networkInterfaces();
361
- if (interfaces.WLAN){
362
- for (const inter of interfaces.WLAN){
363
  if (inter.family === 'IPv4' && !inter.internal){
364
- return inter.address;
365
- }
366
- }
367
- } else if (interfaces.wlan2) {
368
- for (const inter of interfaces.wlan2) {
369
- if (inter.family === 'IPv4' && !inter.internal) {
370
  return inter.address;
371
  }
372
  }
@@ -376,7 +390,6 @@ function getDefaultIp(){
376
  export{
377
  generateRequestId,
378
  generateRequestBody,
379
- getDefaultIp,
380
- httpAgent,
381
- httpsAgent
382
  }
 
2
  import tokenManager from '../auth/token_manager.js';
3
  import { generateRequestId } from './idGenerator.js';
4
  import os from 'os';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  function extractImagesFromContent(content) {
7
  const result = { text: '', images: [] };
 
187
  // 1. 优先使用 thinking_budget(直接数值)
188
  // 2. 其次使用 reasoning_effort(OpenAI 格式:low/medium/high)
189
  // 3. 最后使用配置默认值或硬编码默认值
190
+ const defaultThinkingBudget = config.defaults.thinking_budget ?? 1024;
191
 
192
  let thinkingBudget = 0;
193
  if (enableThinking) {
 
223
  }
224
  return generationConfig
225
  }
226
+ // 不被 Google 工具参数 Schema 支持的字段,在这里统一过滤掉
227
+ // 包括:
228
+ // - JSON Schema 的元信息字段:$schema, additionalProperties
229
+ // - 长度/数量约束:minLength, maxLength, minItems, maxItems, uniqueItems(不必传给后端)
230
+ // - 严格上下界 / 常量:exclusiveMaximum, exclusiveMinimum, const(Google Schema 不支持)
231
+ // - 组合约束:anyOf/oneOf/allOf 以及其非标准写法 any_of/one_of/all_of(为避免上游实现差异,这里一律去掉)
232
+ const EXCLUDED_KEYS = new Set([
233
+ '$schema',
234
+ 'additionalProperties',
235
+ 'minLength',
236
+ 'maxLength',
237
+ 'minItems',
238
+ 'maxItems',
239
+ 'uniqueItems',
240
+ 'exclusiveMaximum',
241
+ 'exclusiveMinimum',
242
+ 'const',
243
+ 'anyOf',
244
+ 'oneOf',
245
+ 'allOf',
246
+ 'any_of',
247
+ 'one_of',
248
+ 'all_of'
249
+ ]);
250
 
251
  function cleanParameters(obj) {
252
  if (!obj || typeof obj !== 'object') return obj;
 
255
 
256
  for (const [key, value] of Object.entries(obj)) {
257
  if (EXCLUDED_KEYS.has(key)) continue;
258
+ const cleanedValue = (value && typeof value === 'object') ? cleanParameters(value) : value;
259
+ cleaned[key] = cleanedValue;
260
  }
261
 
262
  return cleaned;
 
265
  function convertOpenAIToolsToAntigravity(openaiTools){
266
  if (!openaiTools || openaiTools.length === 0) return [];
267
  return openaiTools.map((tool)=>{
268
+ // 先清洗一遍参数,过滤/规范化不兼容字段
269
+ const rawParams = tool.function?.parameters || {};
270
+ const cleanedParams = cleanParameters(rawParams) || {};
271
+
272
+ // 确保顶层是一个合法的 JSON Schema 对象
273
+ // 如果用户没显式指定 type,则默认按 OpenAI 习惯设为 object
274
+ if (cleanedParams.type === undefined) {
275
+ cleanedParams.type = 'object';
276
+ }
277
+ // 对于 object 类型,至少保证有 properties 字段
278
+ if (cleanedParams.type === 'object' && cleanedParams.properties === undefined) {
279
+ cleanedParams.properties = {};
280
+ }
281
+
282
  return {
283
  functionDeclarations: [
284
  {
285
  name: tool.function.name,
286
  description: tool.function.description,
287
+ parameters: cleanedParams
288
  }
289
  ]
290
  }
 
303
  }
304
 
305
  function isEnableThinking(modelName){
306
+ // 只要模型名里包含 -thinking(例如 gemini-2.0-flash-thinking-exp),就认为支持思考配置
307
+ return modelName.includes('-thinking') ||
308
  modelName === 'gemini-2.5-pro' ||
309
  modelName.startsWith('gemini-3-pro-') ||
310
  modelName === "rev19-uic3-1p" ||
311
+ modelName === "gpt-oss-120b-medium";
312
  }
313
 
314
  function generateRequestBody(openaiMessages,modelName,parameters,openaiTools,token){
 
358
 
359
  return requestBody;
360
  }
361
+ /**
362
+ * 将通用文本对话请求体转换为图片生成请求体
363
+ * 统一配置 image_gen 所需字段,避免在各处手动删除/覆盖字段
364
+ */
365
+ function prepareImageRequest(requestBody) {
366
+ if (!requestBody || !requestBody.request) return requestBody;
367
+
368
+ requestBody.request.generationConfig = { candidateCount: 1 };
369
+ requestBody.requestType = 'image_gen';
370
+
371
+ // image_gen 模式下不需要这些字段
372
+ delete requestBody.request.systemInstruction;
373
+ delete requestBody.request.tools;
374
+ delete requestBody.request.toolConfig;
375
+
376
+ return requestBody;
377
+ }
378
+
379
  function getDefaultIp(){
380
  const interfaces = os.networkInterfaces();
381
+ for (const iface of Object.values(interfaces)){
382
+ for (const inter of iface){
383
  if (inter.family === 'IPv4' && !inter.internal){
 
 
 
 
 
 
384
  return inter.address;
385
  }
386
  }
 
390
  export{
391
  generateRequestId,
392
  generateRequestBody,
393
+ prepareImageRequest,
394
+ getDefaultIp
 
395
  }