ZhaoShanGeng commited on
Commit
4bf0fdd
·
1 Parent(s): 6cdc5e2

refactor: 代码模块化重构与前端优化

Browse files

## 代码重构
- 将 utils.js (500+行) 拆分为6个模块:
- openai_mapping.js: 请求体构建
- openai_messages.js: 消息格式转换
- openai_tools.js: 工具格式转换
- openai_generation.js: 生成配置
- openai_system.js: 系统指令提取
- openai_signatures.js: 签名常量
- 将 client.js 的流式解析抽取为 stream_parser.js

## 路径与常量统一
- 新增 paths.js 统一处理 pkg 打包和开发环境的路径
- 新增 constants/index.js 集中管理所有魔法数字和配置默认值

## 错误处理
- 新增 errors.js 统一错误类层次结构
- 新增 Express 错误处理中间件

## Token管理优化
- 新增 TokenStore 类处理文件读写
- 文件操作改为异步 (async/await)
- API方法 (addToken/updateToken/deleteToken/getTokenList) 改为异步
- 额度耗尽策略新增高性能可用列表维护

## 前端优化
- 防闪烁: 页面加载前根据登录状态和Tab状态设置初始显示
- 字体异步加载: 使用 media=print onload 技术避免阻塞渲染
- 本地背景图: 替代远程 Unsplash 图片加快加载
- Tab状态持久化: 保存到 localStorage

## 新功能
- 新增 passSignatureToClient 配置控制签名透传
- 轮询策略默认改为 request_count

config.json CHANGED
@@ -7,7 +7,7 @@
7
  "memoryThreshold": 25
8
  },
9
  "rotation": {
10
- "strategy": "round_robin",
11
  "requestCount": 50
12
  },
13
  "api": {
@@ -32,6 +32,7 @@
32
  "retryTimes": 3,
33
  "skipProjectIdFetch": false,
34
  "useNativeAxios": false,
35
- "useContextSystemPrompt": false
 
36
  }
37
  }
 
7
  "memoryThreshold": 25
8
  },
9
  "rotation": {
10
+ "strategy": "request_count",
11
  "requestCount": 50
12
  },
13
  "api": {
 
32
  "retryTimes": 3,
33
  "skipProjectIdFetch": false,
34
  "useNativeAxios": false,
35
+ "useContextSystemPrompt": false,
36
+ "passSignatureToClient": false
37
  }
38
  }
public/assets/bg.jpg ADDED

Git LFS Details

  • SHA256: 776c94ca48626f719199620438ac8564945a6073cf71f1ca7850d56fa02c9c42
  • Pointer size: 131 Bytes
  • Size of remote file: 116 kB
public/index.html CHANGED
@@ -4,21 +4,54 @@
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
  <title>Token 管理</title>
7
- <!-- 引入 MiSans 字体 -->
8
- <link rel="stylesheet" href="https://font.sec.miui.com/font/css?family=MiSans:100,200,300,400,450,500,600,650,700,900:Chinese_Simplify,Latin&display=swap">
9
- <!-- 引入 Ubuntu Mono 等宽字体 -->
10
- <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Ubuntu+Mono:wght@400;700&display=swap">
11
- <link rel="stylesheet" href="style.css">
12
  <script>
13
- // 页面加载前检查登录状态,避免闪现
14
- if (localStorage.getItem('authToken')) {
15
- document.documentElement.classList.add('logged-in');
16
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  </script>
18
  <style>
19
- .logged-in #loginForm { display: none !important; }
20
- .logged-in #mainContent { display: block !important; }
 
 
 
 
 
 
 
 
 
21
  </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  </head>
23
  <body>
24
  <div class="container">
@@ -42,11 +75,10 @@
42
  <div id="mainContent" class="main-content hidden">
43
  <div class="header">
44
  <div class="tabs">
45
- <button class="tab active" onclick="switchTab('tokens')">🎯 Token</button>
46
- <button class="tab" onclick="switchTab('settings')">⚙️ 设置</button>
47
  </div>
48
  <div class="header-right">
49
- <span class="server-info" id="serverInfo"></span>
50
  <button onclick="logout()">🚪 退出</button>
51
  </div>
52
  </div>
@@ -175,6 +207,13 @@
175
  <span class="slider"></span>
176
  </label>
177
  </div>
 
 
 
 
 
 
 
178
  </div>
179
  <div class="form-group compact">
180
  <label>系统提示词</label>
@@ -244,12 +283,12 @@
244
  </div>
245
 
246
  <!-- 按依赖顺序加载模块 -->
247
- <script src="js/utils.js"></script>
248
- <script src="js/ui.js"></script>
249
- <script src="js/auth.js"></script>
250
- <script src="js/quota.js"></script>
251
- <script src="js/tokens.js"></script>
252
- <script src="js/config.js"></script>
253
- <script src="js/main.js"></script>
254
  </body>
255
  </html>
 
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
  <title>Token 管理</title>
7
+ <!-- 防止页面闪烁:在渲染前根据登录状态和Tab状态设置初始显示 -->
 
 
 
 
8
  <script>
9
+ (function() {
10
+ var isLoggedIn = localStorage.getItem('authToken');
11
+ var currentTab = localStorage.getItem('currentTab') || 'tokens';
12
+ var classes = ['auth-ready'];
13
+ if (isLoggedIn) classes.push('logged-in');
14
+ if (currentTab === 'settings') classes.push('tab-settings');
15
+ document.documentElement.className = classes.join(' ');
16
+ // 检测字体加载
17
+ if ('fonts' in document) {
18
+ document.fonts.ready.then(function() {
19
+ document.documentElement.classList.add('fonts-loaded');
20
+ });
21
+ } else {
22
+ // 后备方案:延迟添加
23
+ setTimeout(function() {
24
+ document.documentElement.classList.add('fonts-loaded');
25
+ }, 1000);
26
+ }
27
+ })();
28
  </script>
29
  <style>
30
+ /* 防止闪烁的关键样式 */
31
+ html:not(.auth-ready) #loginForm,
32
+ html:not(.auth-ready) #mainContent { visibility: hidden; }
33
+ html.logged-in #loginForm { display: none !important; }
34
+ html.logged-in #mainContent { display: flex !important; }
35
+ html:not(.logged-in) #mainContent { display: none !important; }
36
+ /* Tab状态防闪烁 */
37
+ html.tab-settings #tokensPage { display: none !important; }
38
+ html.tab-settings #settingsPage { display: block !important; }
39
+ html.tab-settings .tab[data-tab="tokens"] { background: transparent !important; color: var(--text-light, #888) !important; }
40
+ html.tab-settings .tab[data-tab="settings"] { background: var(--primary, #4f46e5) !important; color: white !important; }
41
  </style>
42
+ <!-- 主样式表 - 优先加载 -->
43
+ <link rel="stylesheet" href="style.css">
44
+ <!-- 预连接字体服务器 -->
45
+ <link rel="preconnect" href="https://font.sec.miui.com" crossorigin>
46
+ <link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
47
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
48
+ <!-- 字体异步加载 - 不阻塞渲染 -->
49
+ <link rel="stylesheet" href="https://font.sec.miui.com/font/css?family=MiSans:400,500,600,700:Chinese_Simplify,Latin&display=swap" media="print" onload="this.media='all'">
50
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Ubuntu+Mono:wght@400;700&display=swap" media="print" onload="this.media='all'">
51
+ <noscript>
52
+ <link rel="stylesheet" href="https://font.sec.miui.com/font/css?family=MiSans:400,500,600,700:Chinese_Simplify,Latin&display=swap">
53
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Ubuntu+Mono:wght@400;700&display=swap">
54
+ </noscript>
55
  </head>
56
  <body>
57
  <div class="container">
 
75
  <div id="mainContent" class="main-content hidden">
76
  <div class="header">
77
  <div class="tabs">
78
+ <button class="tab active" data-tab="tokens" onclick="switchTab('tokens')">🎯 Token</button>
79
+ <button class="tab" data-tab="settings" onclick="switchTab('settings')">⚙️ 设置</button>
80
  </div>
81
  <div class="header-right">
 
82
  <button onclick="logout()">🚪 退出</button>
83
  </div>
84
  </div>
 
207
  <span class="slider"></span>
208
  </label>
209
  </div>
210
+ <div class="form-group compact switch-group">
211
+ <label>透传签名 <span class="help-tip" data-tooltip="将响应中的thoughtSignature透传到客户端">?</span></label>
212
+ <label class="switch">
213
+ <input type="checkbox" name="PASS_SIGNATURE_TO_CLIENT">
214
+ <span class="slider"></span>
215
+ </label>
216
+ </div>
217
  </div>
218
  <div class="form-group compact">
219
  <label>系统提示词</label>
 
283
  </div>
284
 
285
  <!-- 按依赖顺序加载模块 -->
286
+ <script src="js/utils.js" defer></script>
287
+ <script src="js/ui.js" defer></script>
288
+ <script src="js/auth.js" defer></script>
289
+ <script src="js/quota.js" defer></script>
290
+ <script src="js/tokens.js" defer></script>
291
+ <script src="js/config.js" defer></script>
292
+ <script src="js/main.js" defer></script>
293
  </body>
294
  </html>
public/js/config.js CHANGED
@@ -46,11 +46,6 @@ async function loadConfig() {
46
  const form = document.getElementById('configForm');
47
  const { env, json } = data.data;
48
 
49
- const serverInfo = document.getElementById('serverInfo');
50
- if (serverInfo && json.server) {
51
- serverInfo.textContent = `${json.server.host || '0.0.0.0'}:${json.server.port || 8045}`;
52
- }
53
-
54
  Object.entries(env).forEach(([key, value]) => {
55
  const input = form.elements[key];
56
  if (input) input.value = value || '';
@@ -76,6 +71,7 @@ async function loadConfig() {
76
  if (form.elements['SKIP_PROJECT_ID_FETCH']) form.elements['SKIP_PROJECT_ID_FETCH'].checked = json.other.skipProjectIdFetch || false;
77
  if (form.elements['USE_NATIVE_AXIOS']) form.elements['USE_NATIVE_AXIOS'].checked = json.other.useNativeAxios !== false;
78
  if (form.elements['USE_CONTEXT_SYSTEM_PROMPT']) form.elements['USE_CONTEXT_SYSTEM_PROMPT'].checked = json.other.useContextSystemPrompt || false;
 
79
  }
80
  if (json.rotation) {
81
  if (form.elements['ROTATION_STRATEGY']) {
@@ -114,6 +110,7 @@ async function saveConfig(e) {
114
  jsonConfig.other.skipProjectIdFetch = form.elements['SKIP_PROJECT_ID_FETCH']?.checked || false;
115
  jsonConfig.other.useNativeAxios = form.elements['USE_NATIVE_AXIOS']?.checked || false;
116
  jsonConfig.other.useContextSystemPrompt = form.elements['USE_CONTEXT_SYSTEM_PROMPT']?.checked || false;
 
117
 
118
  Object.entries(allConfig).forEach(([key, value]) => {
119
  if (sensitiveKeys.includes(key)) {
@@ -137,7 +134,7 @@ async function saveConfig(e) {
137
  const num = parseInt(value);
138
  jsonConfig.other.retryTimes = Number.isNaN(num) ? undefined : num;
139
  }
140
- else if (key === 'SKIP_PROJECT_ID_FETCH' || key === 'USE_NATIVE_AXIOS' || key === 'USE_CONTEXT_SYSTEM_PROMPT') {
141
  // 跳过,已在上面处理
142
  }
143
  else if (key === 'ROTATION_STRATEGY') jsonConfig.rotation.strategy = value || undefined;
 
46
  const form = document.getElementById('configForm');
47
  const { env, json } = data.data;
48
 
 
 
 
 
 
49
  Object.entries(env).forEach(([key, value]) => {
50
  const input = form.elements[key];
51
  if (input) input.value = value || '';
 
71
  if (form.elements['SKIP_PROJECT_ID_FETCH']) form.elements['SKIP_PROJECT_ID_FETCH'].checked = json.other.skipProjectIdFetch || false;
72
  if (form.elements['USE_NATIVE_AXIOS']) form.elements['USE_NATIVE_AXIOS'].checked = json.other.useNativeAxios !== false;
73
  if (form.elements['USE_CONTEXT_SYSTEM_PROMPT']) form.elements['USE_CONTEXT_SYSTEM_PROMPT'].checked = json.other.useContextSystemPrompt || false;
74
+ if (form.elements['PASS_SIGNATURE_TO_CLIENT']) form.elements['PASS_SIGNATURE_TO_CLIENT'].checked = json.other.passSignatureToClient || false;
75
  }
76
  if (json.rotation) {
77
  if (form.elements['ROTATION_STRATEGY']) {
 
110
  jsonConfig.other.skipProjectIdFetch = form.elements['SKIP_PROJECT_ID_FETCH']?.checked || false;
111
  jsonConfig.other.useNativeAxios = form.elements['USE_NATIVE_AXIOS']?.checked || false;
112
  jsonConfig.other.useContextSystemPrompt = form.elements['USE_CONTEXT_SYSTEM_PROMPT']?.checked || false;
113
+ jsonConfig.other.passSignatureToClient = form.elements['PASS_SIGNATURE_TO_CLIENT']?.checked || false;
114
 
115
  Object.entries(allConfig).forEach(([key, value]) => {
116
  if (sensitiveKeys.includes(key)) {
 
134
  const num = parseInt(value);
135
  jsonConfig.other.retryTimes = Number.isNaN(num) ? undefined : num;
136
  }
137
+ else if (key === 'SKIP_PROJECT_ID_FETCH' || key === 'USE_NATIVE_AXIOS' || key === 'USE_CONTEXT_SYSTEM_PROMPT' || key === 'PASS_SIGNATURE_TO_CLIENT') {
138
  // 跳过,已在上面处理
139
  }
140
  else if (key === 'ROTATION_STRATEGY') jsonConfig.rotation.strategy = value || undefined;
public/js/main.js CHANGED
@@ -7,8 +7,12 @@ initSensitiveInfo();
7
  // 如果已登录,显示主内容
8
  if (authToken) {
9
  showMainContent();
 
10
  loadTokens();
11
- loadConfig();
 
 
 
12
  }
13
 
14
  // 登录表单提交
 
7
  // 如果已登录,显示主内容
8
  if (authToken) {
9
  showMainContent();
10
+ restoreTabState(); // 恢复Tab状态
11
  loadTokens();
12
+ // 只有在设置页面时才加载配置
13
+ if (localStorage.getItem('currentTab') === 'settings') {
14
+ loadConfig();
15
+ }
16
  }
17
 
18
  // 登录表单提交
public/js/ui.js CHANGED
@@ -52,17 +52,45 @@ function hideLoading() {
52
  if (overlay) overlay.remove();
53
  }
54
 
55
- function switchTab(tab) {
 
 
 
 
 
 
 
 
56
  document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
57
- event.target.classList.add('active');
58
 
 
 
 
 
 
 
 
59
  document.getElementById('tokensPage').classList.add('hidden');
60
  document.getElementById('settingsPage').classList.add('hidden');
61
 
 
62
  if (tab === 'tokens') {
63
  document.getElementById('tokensPage').classList.remove('hidden');
64
  } else if (tab === 'settings') {
65
  document.getElementById('settingsPage').classList.remove('hidden');
66
  loadConfig();
67
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  }
 
52
  if (overlay) overlay.remove();
53
  }
54
 
55
+ function switchTab(tab, saveState = true) {
56
+ // 更新html元素的class以防止闪烁
57
+ if (tab === 'settings') {
58
+ document.documentElement.classList.add('tab-settings');
59
+ } else {
60
+ document.documentElement.classList.remove('tab-settings');
61
+ }
62
+
63
+ // 移除所有tab的active状态
64
  document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
 
65
 
66
+ // 找到对应的tab按钮并激活
67
+ const targetTab = document.querySelector(`.tab[data-tab="${tab}"]`);
68
+ if (targetTab) {
69
+ targetTab.classList.add('active');
70
+ }
71
+
72
+ // 隐藏所有页面
73
  document.getElementById('tokensPage').classList.add('hidden');
74
  document.getElementById('settingsPage').classList.add('hidden');
75
 
76
+ // 显示对应页面
77
  if (tab === 'tokens') {
78
  document.getElementById('tokensPage').classList.remove('hidden');
79
  } else if (tab === 'settings') {
80
  document.getElementById('settingsPage').classList.remove('hidden');
81
  loadConfig();
82
  }
83
+
84
+ // 保存当前Tab状态到localStorage
85
+ if (saveState) {
86
+ localStorage.setItem('currentTab', tab);
87
+ }
88
+ }
89
+
90
+ // 恢复Tab状态
91
+ function restoreTabState() {
92
+ const savedTab = localStorage.getItem('currentTab');
93
+ if (savedTab && (savedTab === 'tokens' || savedTab === 'settings')) {
94
+ switchTab(savedTab, false);
95
+ }
96
  }
public/style.css CHANGED
@@ -26,7 +26,7 @@
26
 
27
  * { margin: 0; padding: 0; box-sizing: border-box; }
28
 
29
- /* 固定背景图片 - 使用静态风景图(清晰无模糊) */
30
  body::before {
31
  content: '';
32
  position: fixed;
@@ -34,7 +34,8 @@ body::before {
34
  left: 0;
35
  right: 0;
36
  bottom: 0;
37
- background-image: url('https://images.unsplash.com/photo-1506905925346-21bda4d32df4?w=1920&q=80');
 
38
  background-size: cover;
39
  background-position: center;
40
  background-repeat: no-repeat;
@@ -88,7 +89,7 @@ html {
88
  font-size: var(--font-size-base);
89
  }
90
  body {
91
- font-family: 'Ubuntu Mono', 'MiSans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
92
  background: var(--bg);
93
  color: var(--text);
94
  line-height: 1.5;
@@ -101,6 +102,11 @@ body {
101
  font-weight: 400;
102
  }
103
 
 
 
 
 
 
104
  /* 确保所有元素继承字体 */
105
  *, *::before, *::after {
106
  font-family: inherit;
@@ -158,16 +164,16 @@ label {
158
  color: var(--text);
159
  font-size: 0.875rem;
160
  }
161
- input, select, textarea {
162
- width: 100%;
163
- min-height: 40px;
164
- padding: 0.5rem 0.75rem 0.5rem 0.5rem !important;
165
- border: 1.5px solid var(--border);
166
- border-radius: 0.5rem;
167
- font-size: 0.875rem;
168
- background: var(--card);
169
- color: var(--text);
170
- transition: all 0.2s;
171
  }
172
  input:focus, select:focus, textarea:focus {
173
  outline: none;
@@ -357,7 +363,6 @@ button.loading::after {
357
  display: grid;
358
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
359
  gap: 0.75rem;
360
- align-items: start;
361
  }
362
  .token-card {
363
  background: rgba(255, 255, 255, 0.6);
@@ -465,7 +470,7 @@ button.loading::after {
465
  .inline-edit-input {
466
  flex: 1;
467
  min-height: 24px;
468
- padding: 0.125rem 0.375rem 0.125rem 0.5rem;
469
  font-size: 0.75rem;
470
  border: 1px solid var(--primary);
471
  border-radius: 0.25rem;
@@ -579,13 +584,13 @@ button.loading::after {
579
  .form-group.compact input,
580
  .form-group.compact select {
581
  min-height: 36px;
582
- padding: 0.375rem 0.5rem 0.375rem 0.75rem;
583
  font-size: 0.8rem;
584
  }
585
  .form-group.compact textarea {
586
  min-height: 60px;
587
  max-height: 300px;
588
- padding: 0.375rem 0.5rem 0.375rem 0.75rem;
589
  font-size: 0.8rem;
590
  resize: vertical;
591
  height: auto;
 
26
 
27
  * { margin: 0; padding: 0; box-sizing: border-box; }
28
 
29
+ /* 固定背景图片 - 使用本地图片(快速加载) */
30
  body::before {
31
  content: '';
32
  position: fixed;
 
34
  left: 0;
35
  right: 0;
36
  bottom: 0;
37
+ background-color: var(--bg);
38
+ background-image: url('assets/bg.jpg');
39
  background-size: cover;
40
  background-position: center;
41
  background-repeat: no-repeat;
 
89
  font-size: var(--font-size-base);
90
  }
91
  body {
92
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Ubuntu Mono', 'MiSans';
93
  background: var(--bg);
94
  color: var(--text);
95
  line-height: 1.5;
 
102
  font-weight: 400;
103
  }
104
 
105
+ /* 字体加载完成后应用 */
106
+ .fonts-loaded body {
107
+ font-family: 'Ubuntu Mono', 'MiSans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
108
+ }
109
+
110
  /* 确保所有元素继承字体 */
111
  *, *::before, *::after {
112
  font-family: inherit;
 
164
  color: var(--text);
165
  font-size: 0.875rem;
166
  }
167
+ input, select, textarea {
168
+ width: 100%;
169
+ min-height: 40px;
170
+ padding: 0.5rem 0.75rem;
171
+ border: 1.5px solid var(--border);
172
+ border-radius: 0.5rem;
173
+ font-size: 0.875rem;
174
+ background: var(--card);
175
+ color: var(--text);
176
+ transition: all 0.2s;
177
  }
178
  input:focus, select:focus, textarea:focus {
179
  outline: none;
 
363
  display: grid;
364
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
365
  gap: 0.75rem;
 
366
  }
367
  .token-card {
368
  background: rgba(255, 255, 255, 0.6);
 
470
  .inline-edit-input {
471
  flex: 1;
472
  min-height: 24px;
473
+ padding: 0.125rem 0.375rem;
474
  font-size: 0.75rem;
475
  border: 1px solid var(--primary);
476
  border-radius: 0.25rem;
 
584
  .form-group.compact input,
585
  .form-group.compact select {
586
  min-height: 36px;
587
+ padding: 0.375rem 0.5rem;
588
  font-size: 0.8rem;
589
  }
590
  .form-group.compact textarea {
591
  min-height: 60px;
592
  max-height: 300px;
593
+ padding: 0.375rem 0.5rem;
594
  font-size: 0.8rem;
595
  resize: vertical;
596
  height: auto;
src/api/client.js CHANGED
@@ -1,14 +1,20 @@
1
  import axios from 'axios';
2
  import tokenManager from '../auth/token_manager.js';
3
  import config from '../config/config.js';
4
- 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 memoryManager, { MemoryPressure, registerMemoryPoolCleanup } from '../utils/memoryManager.js';
9
  import { buildAxiosRequestConfig } from '../utils/httpClient.js';
10
- import { setReasoningSignature, setToolSignature } from '../utils/thoughtSignatureCache.js';
11
- import { getOriginalToolName } from '../utils/toolNameCache.js';
 
 
 
 
 
 
 
12
 
13
  // 请求客户端:优先使用 AntigravityRequester,失败则降级到 axios
14
  let requester = null;
@@ -17,7 +23,7 @@ let useAxios = false;
17
  // ==================== 模型列表缓存(智能管理) ====================
18
  // 缓存过期时间根据内存压力动态调整
19
  const getModelCacheTTL = () => {
20
- const baseTTL = config.cache?.modelListTTL || 60 * 60 * 1000;
21
  const pressure = memoryManager.currentPressure;
22
  // 高压力时缩短缓存时间
23
  if (pressure === MemoryPressure.CRITICAL) return Math.min(baseTTL, 5 * 60 * 1000);
@@ -72,75 +78,10 @@ if (config.useNativeAxios === true) {
72
  }
73
  }
74
 
75
- // ==================== 零拷贝优化 ====================
76
-
77
- // 预编译的常量(避免重复创建字符串)
78
- const DATA_PREFIX = 'data: ';
79
- const DATA_PREFIX_LEN = DATA_PREFIX.length;
80
-
81
- // 高效的行分割器(零拷贝,避免 split 创建新数组)
82
- // 使用对象池复用 LineBuffer 实例
83
- class LineBuffer {
84
- constructor() {
85
- this.buffer = '';
86
- this.lines = [];
87
- }
88
-
89
- // 追加数据并返回完整的行
90
- append(chunk) {
91
- this.buffer += chunk;
92
- this.lines.length = 0; // 重用数组
93
-
94
- let start = 0;
95
- let end;
96
- while ((end = this.buffer.indexOf('\n', start)) !== -1) {
97
- this.lines.push(this.buffer.slice(start, end));
98
- start = end + 1;
99
- }
100
-
101
- // 保留未完成的部分
102
- this.buffer = start < this.buffer.length ? this.buffer.slice(start) : '';
103
- return this.lines;
104
- }
105
-
106
- // 清空缓冲区(用于归还到池之前)
107
- clear() {
108
- this.buffer = '';
109
- this.lines.length = 0;
110
- }
111
- }
112
-
113
- // LineBuffer 对象池
114
- const lineBufferPool = [];
115
- const getLineBuffer = () => {
116
- const buffer = lineBufferPool.pop();
117
- if (buffer) {
118
- buffer.clear();
119
- return buffer;
120
- }
121
- return new LineBuffer();
122
- };
123
- const releaseLineBuffer = (buffer) => {
124
- const maxSize = memoryManager.getPoolSizes().lineBuffer;
125
- if (lineBufferPool.length < maxSize) {
126
- buffer.clear();
127
- lineBufferPool.push(buffer);
128
- }
129
- };
130
-
131
- // 对象池:复用 toolCall 对象
132
- const toolCallPool = [];
133
- const getToolCallObject = () => toolCallPool.pop() || { id: '', type: 'function', function: { name: '', arguments: '' } };
134
- const releaseToolCallObject = (obj) => {
135
- const maxSize = memoryManager.getPoolSizes().toolCall;
136
- if (toolCallPool.length < maxSize) toolCallPool.push(obj);
137
- };
138
-
139
- // 注册内存清理回调
140
  function registerMemoryCleanup() {
141
- // 使用通用池清理工具,避免重复 while-pop 逻辑
142
- registerMemoryPoolCleanup(toolCallPool, () => memoryManager.getPoolSizes().toolCall);
143
- registerMemoryPoolCleanup(lineBufferPool, () => memoryManager.getPoolSizes().lineBuffer);
144
 
145
  memoryManager.registerCleanup((pressure) => {
146
  // 高压力或紧急时清理模型缓存
@@ -154,7 +95,6 @@ function registerMemoryCleanup() {
154
  }
155
  }
156
 
157
- // 紧急时强制清理模型缓存
158
  if (pressure === MemoryPressure.CRITICAL && modelListCache) {
159
  modelListCache = null;
160
  modelListCacheTime = 0;
@@ -198,14 +138,6 @@ function buildRequesterConfig(headers, body = null) {
198
  return reqConfig;
199
  }
200
 
201
- // 统一构造上游 API 错误对象,方便服务器层识别并透传
202
- function createApiError(message, status, rawBody) {
203
- const err = new Error(message);
204
- err.status = status;
205
- err.rawBody = rawBody;
206
- err.isUpstreamApiError = true;
207
- return err;
208
- }
209
 
210
  // 统一错误处理
211
  async function handleApiError(error, token) {
@@ -235,88 +167,6 @@ async function handleApiError(error, token) {
235
  throw createApiError(`API请求失败 (${status}): ${errorBody}`, status, errorBody);
236
  }
237
 
238
- // 转换 functionCall 为 OpenAI 格式(使用对象池)
239
- // 会尝试将安全工具名还原为原始工具名
240
- function convertToToolCall(functionCall, sessionId, model) {
241
- const toolCall = getToolCallObject();
242
- toolCall.id = functionCall.id || generateToolCallId();
243
- let name = functionCall.name;
244
- if (sessionId && model) {
245
- const original = getOriginalToolName(sessionId, model, functionCall.name);
246
- if (original) name = original;
247
- }
248
- toolCall.function.name = name;
249
- toolCall.function.arguments = JSON.stringify(functionCall.args);
250
- return toolCall;
251
- }
252
-
253
- // 解析并发送流式响应片段(会修改 state 并触发 callback)
254
- // 支持 DeepSeek 格式:思维链内容通过 reasoning_content 字段输出
255
- // 同时透传 thoughtSignature,方便客户端后续复用
256
- function parseAndEmitStreamChunk(line, state, callback) {
257
- if (!line.startsWith(DATA_PREFIX)) return;
258
-
259
- try {
260
- const data = JSON.parse(line.slice(DATA_PREFIX_LEN));
261
- //console.log(JSON.stringify(data));
262
- const parts = data.response?.candidates?.[0]?.content?.parts;
263
-
264
- if (parts) {
265
- for (const part of parts) {
266
- if (part.thought === true) {
267
- // 思维链内容 - 使用 DeepSeek 格式的 reasoning_content
268
- // 缓存最新的签名,方便后续片段缺省时复用,并写入全局缓存
269
- if (part.thoughtSignature) {
270
- state.reasoningSignature = part.thoughtSignature;
271
- if (state.sessionId && state.model) {
272
- setReasoningSignature(state.sessionId, state.model, part.thoughtSignature);
273
- }
274
- }
275
- callback({
276
- type: 'reasoning',
277
- reasoning_content: part.text || '',
278
- thoughtSignature: part.thoughtSignature || state.reasoningSignature || null
279
- });
280
- } else if (part.text !== undefined) {
281
- // 普通文本内容
282
- callback({ type: 'text', content: part.text });
283
- } else if (part.functionCall) {
284
- // 工具调用,透传工具签名,并写入全局缓存
285
- const toolCall = convertToToolCall(part.functionCall, state.sessionId, state.model);
286
- if (part.thoughtSignature) {
287
- toolCall.thoughtSignature = part.thoughtSignature;
288
- if (state.sessionId && state.model) {
289
- setToolSignature(state.sessionId, state.model, part.thoughtSignature);
290
- }
291
- }
292
- state.toolCalls.push(toolCall);
293
- }
294
- }
295
- }
296
-
297
- // 响应结束时发送工具调用和使用统计
298
- if (data.response?.candidates?.[0]?.finishReason) {
299
- if (state.toolCalls.length > 0) {
300
- callback({ type: 'tool_calls', tool_calls: state.toolCalls });
301
- state.toolCalls = [];
302
- }
303
- // 提取 token 使用统计
304
- const usage = data.response?.usageMetadata;
305
- if (usage) {
306
- callback({
307
- type: 'usage',
308
- usage: {
309
- prompt_tokens: usage.promptTokenCount || 0,
310
- completion_tokens: usage.candidatesTokenCount || 0,
311
- total_tokens: usage.totalTokenCount || 0
312
- }
313
- });
314
- }
315
- }
316
- } catch (e) {
317
- // 忽略 JSON 解析错误
318
- }
319
- }
320
 
321
  // ==================== 导出函数 ====================
322
 
 
1
  import axios from 'axios';
2
  import tokenManager from '../auth/token_manager.js';
3
  import config from '../config/config.js';
 
4
  import AntigravityRequester from '../AntigravityRequester.js';
5
  import { saveBase64Image } from '../utils/imageStorage.js';
6
  import logger from '../utils/logger.js';
7
+ import memoryManager, { MemoryPressure } from '../utils/memoryManager.js';
8
  import { buildAxiosRequestConfig } from '../utils/httpClient.js';
9
+ import { MODEL_LIST_CACHE_TTL } from '../constants/index.js';
10
+ import { createApiError, UpstreamApiError } from '../utils/errors.js';
11
+ import {
12
+ getLineBuffer,
13
+ releaseLineBuffer,
14
+ parseAndEmitStreamChunk,
15
+ convertToToolCall,
16
+ registerStreamMemoryCleanup
17
+ } from './stream_parser.js';
18
 
19
  // 请求客户端:优先使用 AntigravityRequester,失败则降级到 axios
20
  let requester = null;
 
23
  // ==================== 模型列表缓存(智能管理) ====================
24
  // 缓存过期时间根据内存压力动态调整
25
  const getModelCacheTTL = () => {
26
+ const baseTTL = config.cache?.modelListTTL || MODEL_LIST_CACHE_TTL;
27
  const pressure = memoryManager.currentPressure;
28
  // 高压力时缩短缓存时间
29
  if (pressure === MemoryPressure.CRITICAL) return Math.min(baseTTL, 5 * 60 * 1000);
 
78
  }
79
  }
80
 
81
+ // 注册对象池与模型缓存的内存清理回调
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  function registerMemoryCleanup() {
83
+ // 由流式解析模块管理自身对象池大小
84
+ registerStreamMemoryCleanup();
 
85
 
86
  memoryManager.registerCleanup((pressure) => {
87
  // 高压力或紧急时清理模型缓存
 
95
  }
96
  }
97
 
 
98
  if (pressure === MemoryPressure.CRITICAL && modelListCache) {
99
  modelListCache = null;
100
  modelListCacheTime = 0;
 
138
  return reqConfig;
139
  }
140
 
 
 
 
 
 
 
 
 
141
 
142
  // 统一错误处理
143
  async function handleApiError(error, token) {
 
167
  throw createApiError(`API请求失败 (${status}): ${errorBody}`, status, errorBody);
168
  }
169
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
 
171
  // ==================== 导出函数 ====================
172
 
src/api/stream_parser.js ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import memoryManager, { registerMemoryPoolCleanup } from '../utils/memoryManager.js';
2
+ import { generateToolCallId } from '../utils/idGenerator.js';
3
+ import { setReasoningSignature, setToolSignature } from '../utils/thoughtSignatureCache.js';
4
+ import { getOriginalToolName } from '../utils/toolNameCache.js';
5
+
6
+ // 预编译的常量(避免重复创建字符串)
7
+ const DATA_PREFIX = 'data: ';
8
+ const DATA_PREFIX_LEN = DATA_PREFIX.length;
9
+
10
+ // 高效的行分割器(零拷贝,避免 split 创建新数组)
11
+ // 使用对象池复用 LineBuffer 实例
12
+ class LineBuffer {
13
+ constructor() {
14
+ this.buffer = '';
15
+ this.lines = [];
16
+ }
17
+
18
+ // 追加数据并返回完整的行
19
+ append(chunk) {
20
+ this.buffer += chunk;
21
+ this.lines.length = 0; // 重用数组
22
+
23
+ let start = 0;
24
+ let end;
25
+ while ((end = this.buffer.indexOf('\n', start)) !== -1) {
26
+ this.lines.push(this.buffer.slice(start, end));
27
+ start = end + 1;
28
+ }
29
+
30
+ // 保留未完成的部分
31
+ this.buffer = start < this.buffer.length ? this.buffer.slice(start) : '';
32
+ return this.lines;
33
+ }
34
+
35
+ clear() {
36
+ this.buffer = '';
37
+ this.lines.length = 0;
38
+ }
39
+ }
40
+
41
+ // LineBuffer 对象池
42
+ const lineBufferPool = [];
43
+ const getLineBuffer = () => {
44
+ const buffer = lineBufferPool.pop();
45
+ if (buffer) {
46
+ buffer.clear();
47
+ return buffer;
48
+ }
49
+ return new LineBuffer();
50
+ };
51
+ const releaseLineBuffer = (buffer) => {
52
+ const maxSize = memoryManager.getPoolSizes().lineBuffer;
53
+ if (lineBufferPool.length < maxSize) {
54
+ buffer.clear();
55
+ lineBufferPool.push(buffer);
56
+ }
57
+ };
58
+
59
+ // toolCall 对象池
60
+ const toolCallPool = [];
61
+ const getToolCallObject = () => toolCallPool.pop() || { id: '', type: 'function', function: { name: '', arguments: '' } };
62
+ const releaseToolCallObject = (obj) => {
63
+ const maxSize = memoryManager.getPoolSizes().toolCall;
64
+ if (toolCallPool.length < maxSize) toolCallPool.push(obj);
65
+ };
66
+
67
+ // 注册内存清理回调(供外部统一调用)
68
+ function registerStreamMemoryCleanup() {
69
+ registerMemoryPoolCleanup(toolCallPool, () => memoryManager.getPoolSizes().toolCall);
70
+ registerMemoryPoolCleanup(lineBufferPool, () => memoryManager.getPoolSizes().lineBuffer);
71
+ }
72
+
73
+ // 转换 functionCall 为 OpenAI 格式(使用对象池)
74
+ // 会尝试将安全工具名还原为原始工具名
75
+ function convertToToolCall(functionCall, sessionId, model) {
76
+ const toolCall = getToolCallObject();
77
+ toolCall.id = functionCall.id || generateToolCallId();
78
+ let name = functionCall.name;
79
+ if (sessionId && model) {
80
+ const original = getOriginalToolName(sessionId, model, functionCall.name);
81
+ if (original) name = original;
82
+ }
83
+ toolCall.function.name = name;
84
+ toolCall.function.arguments = JSON.stringify(functionCall.args);
85
+ return toolCall;
86
+ }
87
+
88
+ // 解析并发送流式响应片段(会修改 state 并触发 callback)
89
+ // 支持 DeepSeek 格式:思维链内容通过 reasoning_content 字段输出
90
+ // 同时透传 thoughtSignature,方便客户端后续复用
91
+ function parseAndEmitStreamChunk(line, state, callback) {
92
+ if (!line.startsWith(DATA_PREFIX)) return;
93
+
94
+ try {
95
+ const data = JSON.parse(line.slice(DATA_PREFIX_LEN));
96
+ const parts = data.response?.candidates?.[0]?.content?.parts;
97
+
98
+ if (parts) {
99
+ for (const part of parts) {
100
+ if (part.thought === true) {
101
+ if (part.thoughtSignature) {
102
+ state.reasoningSignature = part.thoughtSignature;
103
+ if (state.sessionId && state.model) {
104
+ setReasoningSignature(state.sessionId, state.model, part.thoughtSignature);
105
+ }
106
+ }
107
+ callback({
108
+ type: 'reasoning',
109
+ reasoning_content: part.text || '',
110
+ thoughtSignature: part.thoughtSignature || state.reasoningSignature || null
111
+ });
112
+ } else if (part.text !== undefined) {
113
+ callback({ type: 'text', content: part.text });
114
+ } else if (part.functionCall) {
115
+ const toolCall = convertToToolCall(part.functionCall, state.sessionId, state.model);
116
+ if (part.thoughtSignature) {
117
+ toolCall.thoughtSignature = part.thoughtSignature;
118
+ if (state.sessionId && state.model) {
119
+ setToolSignature(state.sessionId, state.model, part.thoughtSignature);
120
+ }
121
+ }
122
+ state.toolCalls.push(toolCall);
123
+ }
124
+ }
125
+ }
126
+
127
+ if (data.response?.candidates?.[0]?.finishReason) {
128
+ if (state.toolCalls.length > 0) {
129
+ callback({ type: 'tool_calls', tool_calls: state.toolCalls });
130
+ state.toolCalls = [];
131
+ }
132
+ const usage = data.response?.usageMetadata;
133
+ if (usage) {
134
+ callback({
135
+ type: 'usage',
136
+ usage: {
137
+ prompt_tokens: usage.promptTokenCount || 0,
138
+ completion_tokens: usage.candidatesTokenCount || 0,
139
+ total_tokens: usage.totalTokenCount || 0
140
+ }
141
+ });
142
+ }
143
+ }
144
+ } catch {
145
+ // 忽略 JSON 解析错误
146
+ }
147
+ }
148
+
149
+ export {
150
+ getLineBuffer,
151
+ releaseLineBuffer,
152
+ parseAndEmitStreamChunk,
153
+ convertToToolCall,
154
+ registerStreamMemoryCleanup,
155
+ releaseToolCallObject
156
+ };
src/auth/quota_manager.js CHANGED
@@ -1,30 +1,20 @@
1
  import fs from 'fs';
2
  import path from 'path';
3
- import { fileURLToPath } from 'url';
4
  import { log } from '../utils/logger.js';
5
  import memoryManager, { MemoryPressure } from '../utils/memoryManager.js';
6
-
7
- const __filename = fileURLToPath(import.meta.url);
8
- const __dirname = path.dirname(__filename);
9
-
10
- // 获取数据目录(支持 pkg 打包环境)
11
- function getDataDir() {
12
- // 检测是否在 pkg 打包环境中运行
13
- if (process.pkg) {
14
- // pkg 环境:使用可执行文件所在目录的 data 子目录
15
- const execDir = path.dirname(process.execPath);
16
- return path.join(execDir, 'data');
17
- }
18
- // 普通环境:使用项目根目录的 data 子目录
19
- return path.join(__dirname, '..', '..', 'data');
20
- }
21
 
22
  class QuotaManager {
 
 
 
23
  constructor(filePath = path.join(getDataDir(), 'quotas.json')) {
24
  this.filePath = filePath;
 
25
  this.cache = new Map();
26
- this.CACHE_TTL = 5 * 60 * 1000; // 5分钟缓存
27
- this.CLEANUP_INTERVAL = 60 * 60 * 1000; // 1小时清理一次
28
  this.cleanupTimer = null;
29
  this.ensureFileExists();
30
  this.loadFromFile();
 
1
  import fs from 'fs';
2
  import path from 'path';
 
3
  import { log } from '../utils/logger.js';
4
  import memoryManager, { MemoryPressure } from '../utils/memoryManager.js';
5
+ import { getDataDir } from '../utils/paths.js';
6
+ import { QUOTA_CACHE_TTL, QUOTA_CLEANUP_INTERVAL } from '../constants/index.js';
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
  class QuotaManager {
9
+ /**
10
+ * @param {string} filePath - 额度数据文件路径
11
+ */
12
  constructor(filePath = path.join(getDataDir(), 'quotas.json')) {
13
  this.filePath = filePath;
14
+ /** @type {Map<string, {lastUpdated: number, models: Object}>} */
15
  this.cache = new Map();
16
+ this.CACHE_TTL = QUOTA_CACHE_TTL;
17
+ this.CLEANUP_INTERVAL = QUOTA_CLEANUP_INTERVAL;
18
  this.cleanupTimer = null;
19
  this.ensureFileExists();
20
  this.loadFromFile();
src/auth/token_manager.js CHANGED
@@ -1,53 +1,15 @@
1
- import fs from 'fs';
2
- import path from 'path';
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, { 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);
13
-
14
- // 检测是否在 pkg 打包环境中运行
15
- const isPkg = typeof process.pkg !== 'undefined';
16
-
17
- // 获取数据目录路径
18
- // pkg 环境下使用可执行文件所在目录或当前工作目录
19
- function getDataDir() {
20
- if (isPkg) {
21
- // pkg 环境:优先使用可执行文件旁边的 data 目录
22
- const exeDir = path.dirname(process.execPath);
23
- const exeDataDir = path.join(exeDir, 'data');
24
- // 检查是否可以在该目录创建文件
25
- try {
26
- if (!fs.existsSync(exeDataDir)) {
27
- fs.mkdirSync(exeDataDir, { recursive: true });
28
- }
29
- return exeDataDir;
30
- } catch (e) {
31
- // 如果无法创建,尝试当前工作目录
32
- const cwdDataDir = path.join(process.cwd(), 'data');
33
- try {
34
- if (!fs.existsSync(cwdDataDir)) {
35
- fs.mkdirSync(cwdDataDir, { recursive: true });
36
- }
37
- return cwdDataDir;
38
- } catch (e2) {
39
- // 最后使用用户主目录
40
- const homeDataDir = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.antigravity', 'data');
41
- if (!fs.existsSync(homeDataDir)) {
42
- fs.mkdirSync(homeDataDir, { recursive: true });
43
- }
44
- return homeDataDir;
45
- }
46
- }
47
- }
48
- // 开发环境
49
- return path.join(__dirname, '..', '..', 'data');
50
- }
51
 
52
  // 轮询策略枚举
53
  const RotationStrategy = {
@@ -56,37 +18,43 @@ const RotationStrategy = {
56
  REQUEST_COUNT: 'request_count' // 自定义次数后切换
57
  };
58
 
 
 
 
 
59
  class TokenManager {
60
- constructor(filePath = path.join(getDataDir(), 'accounts.json')) {
61
- this.filePath = filePath;
 
 
 
 
62
  this.tokens = [];
 
63
  this.currentIndex = 0;
64
 
65
  // 轮询策略相关 - 使用原子操作避免锁
 
66
  this.rotationStrategy = RotationStrategy.ROUND_ROBIN;
67
- this.requestCountPerToken = 50; // request_count 策略下每个token请求次数后切换
68
- this.tokenRequestCounts = new Map(); // 记录每个token的请求次数
 
 
69
 
70
- this.ensureFileExists();
71
- this.initialize();
72
- }
73
-
74
- ensureFileExists() {
75
- const dir = path.dirname(this.filePath);
76
- if (!fs.existsSync(dir)) {
77
- fs.mkdirSync(dir, { recursive: true });
78
- }
79
- if (!fs.existsSync(this.filePath)) {
80
- fs.writeFileSync(this.filePath, '[]', 'utf8');
81
- log.info('✓ 已创建账号配置文件');
82
- }
83
  }
84
 
85
- async initialize() {
86
  try {
87
  log.info('正在初始化token管理器...');
88
- const data = fs.readFileSync(this.filePath, 'utf8');
89
- let tokenArray = JSON.parse(data);
90
 
91
  this.tokens = tokenArray.filter(token => token.enable !== false).map(token => ({
92
  ...token,
@@ -95,6 +63,7 @@ class TokenManager {
95
 
96
  this.currentIndex = 0;
97
  this.tokenRequestCounts.clear();
 
98
 
99
  // 加载轮询策略配置
100
  this.loadRotationConfig();
@@ -110,6 +79,9 @@ class TokenManager {
110
  } else {
111
  log.info(`轮询策略: ${this.rotationStrategy}`);
112
  }
 
 
 
113
  }
114
  } catch (error) {
115
  log.error('初始化token失败:', error.message);
@@ -117,6 +89,77 @@ class TokenManager {
117
  }
118
  }
119
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  // 加载轮询策略配置
121
  loadRotationConfig() {
122
  try {
@@ -147,6 +190,33 @@ class TokenManager {
147
  }
148
  }
149
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  async fetchProjectId(token) {
151
  const response = await axios(buildAxiosRequestConfig({
152
  method: 'POST',
@@ -163,10 +233,15 @@ class TokenManager {
163
  return response.data?.cloudaicompanionProject;
164
  }
165
 
 
 
 
 
 
166
  isExpired(token) {
167
  if (!token.timestamp || !token.expires_in) return true;
168
  const expiresAt = token.timestamp + (token.expires_in * 1000);
169
- return Date.now() >= expiresAt - 300000;
170
  }
171
 
172
  async refreshToken(token) {
@@ -197,37 +272,19 @@ class TokenManager {
197
  this.saveToFile(token);
198
  return token;
199
  } catch (error) {
200
- throw { statusCode: error.response?.status, message: error.response?.data || error.message };
 
 
 
 
201
  }
202
  }
203
 
204
  saveToFile(tokenToUpdate = null) {
205
- try {
206
- const data = fs.readFileSync(this.filePath, 'utf8');
207
- const allTokens = JSON.parse(data);
208
-
209
- // 如果指定了要更新的token,直接更新它
210
- if (tokenToUpdate) {
211
- const index = allTokens.findIndex(t => t.refresh_token === tokenToUpdate.refresh_token);
212
- if (index !== -1) {
213
- const { sessionId, ...tokenToSave } = tokenToUpdate;
214
- allTokens[index] = tokenToSave;
215
- }
216
- } else {
217
- // 否则更新内存中的所有token
218
- this.tokens.forEach(memToken => {
219
- const index = allTokens.findIndex(t => t.refresh_token === memToken.refresh_token);
220
- if (index !== -1) {
221
- const { sessionId, ...tokenToSave } = memToken;
222
- allTokens[index] = tokenToSave;
223
- }
224
- });
225
- }
226
-
227
- fs.writeFileSync(this.filePath, JSON.stringify(allTokens, null, 2), 'utf8');
228
- } catch (error) {
229
- log.error('保存文件失败:', error.message);
230
- }
231
  }
232
 
233
  disableToken(token) {
@@ -236,6 +293,8 @@ class TokenManager {
236
  this.saveToFile();
237
  this.tokens = this.tokens.filter(t => t.refresh_token !== token.refresh_token);
238
  this.currentIndex = this.currentIndex % Math.max(this.tokens.length, 1);
 
 
239
  }
240
 
241
  // 原子操作:获取并递增请求计数
@@ -284,8 +343,11 @@ class TokenManager {
284
  this.saveToFile(token);
285
  log.warn(`...${token.access_token.slice(-8)}: 额度已耗尽,标记为无额度`);
286
 
287
- // 如果是额度耗尽策略,立即切换到下一个token
288
  if (this.rotationStrategy === RotationStrategy.QUOTA_EXHAUSTED) {
 
 
 
 
289
  this.currentIndex = (this.currentIndex + 1) % Math.max(this.tokens.length, 1);
290
  }
291
  }
@@ -297,79 +359,173 @@ class TokenManager {
297
  log.info(`...${token.access_token.slice(-8)}: 额度已恢复`);
298
  }
299
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
  async getToken() {
 
301
  if (this.tokens.length === 0) return null;
302
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  const totalTokens = this.tokens.length;
304
  const startIndex = this.currentIndex;
305
 
306
  for (let i = 0; i < totalTokens; i++) {
307
  const index = (startIndex + i) % totalTokens;
308
  const token = this.tokens[index];
309
-
310
- // 额度耗尽策略:跳过无额度的token
311
- if (this.rotationStrategy === RotationStrategy.QUOTA_EXHAUSTED && token.hasQuota === false) {
312
- continue;
313
- }
314
-
315
  try {
316
- if (this.isExpired(token)) {
317
- await this.refreshToken(token);
318
- }
319
- if (!token.projectId) {
320
- if (config.skipProjectIdFetch) {
321
- token.projectId = generateProjectId();
322
- this.saveToFile(token);
323
- log.info(`...${token.access_token.slice(-8)}: 使用随机生成的projectId: ${token.projectId}`);
324
- } else {
325
- try {
326
- const projectId = await this.fetchProjectId(token);
327
- if (projectId === undefined) {
328
- log.warn(`...${token.access_token.slice(-8)}: 无资格获取projectId,跳过保存`);
329
- this.disableToken(token);
330
- if (this.tokens.length === 0) return null;
331
- continue;
332
- }
333
- token.projectId = projectId;
334
- this.saveToFile(token);
335
- } catch (error) {
336
- log.error(`...${token.access_token.slice(-8)}: 获取projectId失败:`, error.message);
337
- continue;
338
- }
339
- }
340
  }
341
-
342
  // 更新当前索引
343
  this.currentIndex = index;
344
-
345
  // 根据策略决定是否切换
346
  if (this.shouldRotate(token)) {
347
- this.currentIndex = (this.currentIndex + 1) % totalTokens;
348
  }
349
-
350
  return token;
351
  } catch (error) {
352
- if (error.statusCode === 403 || error.statusCode === 400) {
353
- log.warn(`...${token.access_token.slice(-8)}: Token 已失效或错误,已自动禁用该账号`);
354
  this.disableToken(token);
355
  if (this.tokens.length === 0) return null;
356
- } else {
357
- log.error(`...${token.access_token.slice(-8)} 刷新失败:`, error.message);
358
  }
 
359
  }
360
  }
361
 
362
- // 如果所有token都无额度,重置所有token的额度状态并重试
363
- if (this.rotationStrategy === RotationStrategy.QUOTA_EXHAUSTED) {
364
- log.warn('所有token额度已耗尽,重置额度状态');
365
- this.tokens.forEach(t => {
366
- t.hasQuota = true;
367
- });
368
- this.saveToFile();
369
- // 返回第一个可用token
370
- return this.tokens[0] || null;
371
- }
372
-
373
  return null;
374
  }
375
 
@@ -382,15 +538,14 @@ class TokenManager {
382
 
383
  // API管理方法
384
  async reload() {
385
- await this.initialize();
 
386
  log.info('Token已热重载');
387
  }
388
 
389
- addToken(tokenData) {
390
  try {
391
- this.ensureFileExists();
392
- const data = fs.readFileSync(this.filePath, 'utf8');
393
- const allTokens = JSON.parse(data);
394
 
395
  const newToken = {
396
  access_token: tokenData.access_token,
@@ -411,9 +566,9 @@ class TokenManager {
411
  }
412
 
413
  allTokens.push(newToken);
414
- fs.writeFileSync(this.filePath, JSON.stringify(allTokens, null, 2), 'utf8');
415
 
416
- this.reload();
417
  return { success: true, message: 'Token添加成功' };
418
  } catch (error) {
419
  log.error('添加Token失败:', error.message);
@@ -421,11 +576,9 @@ class TokenManager {
421
  }
422
  }
423
 
424
- updateToken(refreshToken, updates) {
425
  try {
426
- this.ensureFileExists();
427
- const data = fs.readFileSync(this.filePath, 'utf8');
428
- const allTokens = JSON.parse(data);
429
 
430
  const index = allTokens.findIndex(t => t.refresh_token === refreshToken);
431
  if (index === -1) {
@@ -433,9 +586,9 @@ class TokenManager {
433
  }
434
 
435
  allTokens[index] = { ...allTokens[index], ...updates };
436
- fs.writeFileSync(this.filePath, JSON.stringify(allTokens, null, 2), 'utf8');
437
 
438
- this.reload();
439
  return { success: true, message: 'Token更新成功' };
440
  } catch (error) {
441
  log.error('更新Token失败:', error.message);
@@ -443,20 +596,18 @@ class TokenManager {
443
  }
444
  }
445
 
446
- deleteToken(refreshToken) {
447
  try {
448
- this.ensureFileExists();
449
- const data = fs.readFileSync(this.filePath, 'utf8');
450
- const allTokens = JSON.parse(data);
451
 
452
  const filteredTokens = allTokens.filter(t => t.refresh_token !== refreshToken);
453
  if (filteredTokens.length === allTokens.length) {
454
  return { success: false, message: 'Token不存在' };
455
  }
456
 
457
- fs.writeFileSync(this.filePath, JSON.stringify(filteredTokens, null, 2), 'utf8');
458
 
459
- this.reload();
460
  return { success: true, message: 'Token删除成功' };
461
  } catch (error) {
462
  log.error('删除Token失败:', error.message);
@@ -464,11 +615,9 @@ class TokenManager {
464
  }
465
  }
466
 
467
- getTokenList() {
468
  try {
469
- this.ensureFileExists();
470
- const data = fs.readFileSync(this.filePath, 'utf8');
471
- const allTokens = JSON.parse(data);
472
 
473
  return allTokens.map(token => ({
474
  refresh_token: token.refresh_token,
 
 
 
 
1
  import axios from 'axios';
2
  import { log } from '../utils/logger.js';
3
  import { generateSessionId, generateProjectId } from '../utils/idGenerator.js';
4
  import config, { getConfigJson } from '../config/config.js';
5
  import { OAUTH_CONFIG } from '../constants/oauth.js';
6
  import { buildAxiosRequestConfig } from '../utils/httpClient.js';
7
+ import {
8
+ DEFAULT_REQUEST_COUNT_PER_TOKEN,
9
+ TOKEN_REFRESH_BUFFER
10
+ } from '../constants/index.js';
11
+ import TokenStore from './token_store.js';
12
+ import { TokenError } from '../utils/errors.js';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  // 轮询策略枚举
15
  const RotationStrategy = {
 
18
  REQUEST_COUNT: 'request_count' // 自定义次数后切换
19
  };
20
 
21
+ /**
22
+ * Token 管理器
23
+ * 负责 Token 的存储、轮询、刷新等功能
24
+ */
25
  class TokenManager {
26
+ /**
27
+ * @param {string} filePath - Token 数据文件路径
28
+ */
29
+ constructor(filePath) {
30
+ this.store = new TokenStore(filePath);
31
+ /** @type {Array<Object>} */
32
  this.tokens = [];
33
+ /** @type {number} */
34
  this.currentIndex = 0;
35
 
36
  // 轮询策略相关 - 使用原子操作避免锁
37
+ /** @type {string} */
38
  this.rotationStrategy = RotationStrategy.ROUND_ROBIN;
39
+ /** @type {number} */
40
+ this.requestCountPerToken = DEFAULT_REQUEST_COUNT_PER_TOKEN;
41
+ /** @type {Map<string, number>} */
42
+ this.tokenRequestCounts = new Map();
43
 
44
+ // 针对额度耗尽策略的可用 token 索引缓存(优化大规模账号场景)
45
+ /** @type {number[]} */
46
+ this.availableQuotaTokenIndices = [];
47
+ /** @type {number} */
48
+ this.currentQuotaIndex = 0;
49
+
50
+ /** @type {Promise<void>|null} */
51
+ this._initPromise = null;
 
 
 
 
 
52
  }
53
 
54
+ async _initialize() {
55
  try {
56
  log.info('正在初始化token管理器...');
57
+ const tokenArray = await this.store.readAll();
 
58
 
59
  this.tokens = tokenArray.filter(token => token.enable !== false).map(token => ({
60
  ...token,
 
63
 
64
  this.currentIndex = 0;
65
  this.tokenRequestCounts.clear();
66
+ this._rebuildAvailableQuotaTokens();
67
 
68
  // 加载轮询策略配置
69
  this.loadRotationConfig();
 
79
  } else {
80
  log.info(`轮询策略: ${this.rotationStrategy}`);
81
  }
82
+
83
+ // 并发刷新所有过期的 token
84
+ await this._refreshExpiredTokensConcurrently();
85
  }
86
  } catch (error) {
87
  log.error('初始化token失败:', error.message);
 
89
  }
90
  }
91
 
92
+ /**
93
+ * 并发刷新所有过期的 token
94
+ * @private
95
+ */
96
+ async _refreshExpiredTokensConcurrently() {
97
+ const expiredTokens = this.tokens.filter(token => this.isExpired(token));
98
+ if (expiredTokens.length === 0) {
99
+ return;
100
+ }
101
+
102
+ log.info(`发现 ${expiredTokens.length} 个过期token,开始并发刷新...`);
103
+ const startTime = Date.now();
104
+
105
+ const results = await Promise.allSettled(
106
+ expiredTokens.map(token => this._refreshTokenSafe(token))
107
+ );
108
+
109
+ let successCount = 0;
110
+ let failCount = 0;
111
+ const tokensToDisable = [];
112
+
113
+ results.forEach((result, index) => {
114
+ const token = expiredTokens[index];
115
+ if (result.status === 'fulfilled') {
116
+ if (result.value === 'success') {
117
+ successCount++;
118
+ } else if (result.value === 'disable') {
119
+ tokensToDisable.push(token);
120
+ failCount++;
121
+ }
122
+ } else {
123
+ failCount++;
124
+ log.error(`...${token.access_token?.slice(-8) || 'unknown'} 刷新失败:`, result.reason?.message || result.reason);
125
+ }
126
+ });
127
+
128
+ // 批量禁用失效的 token
129
+ for (const token of tokensToDisable) {
130
+ this.disableToken(token);
131
+ }
132
+
133
+ const elapsed = Date.now() - startTime;
134
+ log.info(`并发刷新完成: 成功 ${successCount}, 失败 ${failCount}, 耗时 ${elapsed}ms`);
135
+ }
136
+
137
+ /**
138
+ * 安全刷新单个 token(不抛出异常)
139
+ * @param {Object} token - Token 对象
140
+ * @returns {Promise<'success'|'disable'|'skip'>} 刷新结果
141
+ * @private
142
+ */
143
+ async _refreshTokenSafe(token) {
144
+ try {
145
+ await this.refreshToken(token);
146
+ return 'success';
147
+ } catch (error) {
148
+ if (error.statusCode === 403 || error.statusCode === 400) {
149
+ log.warn(`...${token.access_token?.slice(-8) || 'unknown'}: Token 已失效,将被禁用`);
150
+ return 'disable';
151
+ }
152
+ throw error;
153
+ }
154
+ }
155
+
156
+ async _ensureInitialized() {
157
+ if (!this._initPromise) {
158
+ this._initPromise = this._initialize();
159
+ }
160
+ return this._initPromise;
161
+ }
162
+
163
  // 加载轮询策略配置
164
  loadRotationConfig() {
165
  try {
 
190
  }
191
  }
192
 
193
+ // 重建额度耗尽策略下的可用 token 列表
194
+ _rebuildAvailableQuotaTokens() {
195
+ this.availableQuotaTokenIndices = [];
196
+ this.tokens.forEach((token, index) => {
197
+ if (token.enable !== false && token.hasQuota !== false) {
198
+ this.availableQuotaTokenIndices.push(index);
199
+ }
200
+ });
201
+
202
+ if (this.availableQuotaTokenIndices.length === 0) {
203
+ this.currentQuotaIndex = 0;
204
+ } else {
205
+ this.currentQuotaIndex = this.currentQuotaIndex % this.availableQuotaTokenIndices.length;
206
+ }
207
+ }
208
+
209
+ // 从额度耗尽策略的可用列表中移除指定下标
210
+ _removeQuotaIndex(tokenIndex) {
211
+ const pos = this.availableQuotaTokenIndices.indexOf(tokenIndex);
212
+ if (pos !== -1) {
213
+ this.availableQuotaTokenIndices.splice(pos, 1);
214
+ if (this.currentQuotaIndex >= this.availableQuotaTokenIndices.length) {
215
+ this.currentQuotaIndex = 0;
216
+ }
217
+ }
218
+ }
219
+
220
  async fetchProjectId(token) {
221
  const response = await axios(buildAxiosRequestConfig({
222
  method: 'POST',
 
233
  return response.data?.cloudaicompanionProject;
234
  }
235
 
236
+ /**
237
+ * 检查 Token 是否过期
238
+ * @param {Object} token - Token 对象
239
+ * @returns {boolean} 是否过期
240
+ */
241
  isExpired(token) {
242
  if (!token.timestamp || !token.expires_in) return true;
243
  const expiresAt = token.timestamp + (token.expires_in * 1000);
244
+ return Date.now() >= expiresAt - TOKEN_REFRESH_BUFFER;
245
  }
246
 
247
  async refreshToken(token) {
 
272
  this.saveToFile(token);
273
  return token;
274
  } catch (error) {
275
+ const statusCode = error.response?.status;
276
+ const rawBody = error.response?.data;
277
+ const suffix = token.access_token ? token.access_token.slice(-8) : null;
278
+ const message = typeof rawBody === 'string' ? rawBody : (rawBody?.error?.message || error.message || '刷新 token 失败');
279
+ throw new TokenError(message, suffix, statusCode || 500);
280
  }
281
  }
282
 
283
  saveToFile(tokenToUpdate = null) {
284
+ // 保持与旧接口同步调用方式一致,内部使用异步写入
285
+ this.store.mergeActiveTokens(this.tokens, tokenToUpdate).catch((error) => {
286
+ log.error('保存账号配置文件失败:', error.message);
287
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  }
289
 
290
  disableToken(token) {
 
293
  this.saveToFile();
294
  this.tokens = this.tokens.filter(t => t.refresh_token !== token.refresh_token);
295
  this.currentIndex = this.currentIndex % Math.max(this.tokens.length, 1);
296
+ // tokens 结构发生变化时,重建额度耗尽策略下的可用列表
297
+ this._rebuildAvailableQuotaTokens();
298
  }
299
 
300
  // 原子操作:获取并递增请求计数
 
343
  this.saveToFile(token);
344
  log.warn(`...${token.access_token.slice(-8)}: 额度已耗尽,标记为无额度`);
345
 
 
346
  if (this.rotationStrategy === RotationStrategy.QUOTA_EXHAUSTED) {
347
+ const tokenIndex = this.tokens.findIndex(t => t.refresh_token === token.refresh_token);
348
+ if (tokenIndex !== -1) {
349
+ this._removeQuotaIndex(tokenIndex);
350
+ }
351
  this.currentIndex = (this.currentIndex + 1) % Math.max(this.tokens.length, 1);
352
  }
353
  }
 
359
  log.info(`...${token.access_token.slice(-8)}: 额度已恢复`);
360
  }
361
 
362
+ /**
363
+ * 准备单个 token(刷新 + 获取 projectId)
364
+ * @param {Object} token - Token 对象
365
+ * @returns {Promise<'ready'|'skip'|'disable'>} 处理结果
366
+ * @private
367
+ */
368
+ async _prepareToken(token) {
369
+ // 刷新过期 token
370
+ if (this.isExpired(token)) {
371
+ await this.refreshToken(token);
372
+ }
373
+
374
+ // 获取 projectId
375
+ if (!token.projectId) {
376
+ if (config.skipProjectIdFetch) {
377
+ token.projectId = generateProjectId();
378
+ this.saveToFile(token);
379
+ log.info(`...${token.access_token.slice(-8)}: 使用随机生成的projectId: ${token.projectId}`);
380
+ } else {
381
+ const projectId = await this.fetchProjectId(token);
382
+ if (projectId === undefined) {
383
+ log.warn(`...${token.access_token.slice(-8)}: 无资格获取projectId,禁用账号`);
384
+ return 'disable';
385
+ }
386
+ token.projectId = projectId;
387
+ this.saveToFile(token);
388
+ }
389
+ }
390
+
391
+ return 'ready';
392
+ }
393
+
394
+ /**
395
+ * 处理 token 准备过程中的错误
396
+ * @param {Error} error - 错误对象
397
+ * @param {Object} token - Token 对象
398
+ * @returns {'disable'|'skip'} 处理结果
399
+ * @private
400
+ */
401
+ _handleTokenError(error, token) {
402
+ const suffix = token.access_token?.slice(-8) || 'unknown';
403
+ if (error.statusCode === 403 || error.statusCode === 400) {
404
+ log.warn(`...${suffix}: Token 已失效或错误,已自动禁用该账号`);
405
+ return 'disable';
406
+ }
407
+ log.error(`...${suffix} 操作失败:`, error.message);
408
+ return 'skip';
409
+ }
410
+
411
+ /**
412
+ * 重置所有 token 的额度状态
413
+ * @private
414
+ */
415
+ _resetAllQuotas() {
416
+ log.warn('所有token额度已耗尽,重置额度状态');
417
+ this.tokens.forEach(t => {
418
+ t.hasQuota = true;
419
+ });
420
+ this.saveToFile();
421
+ this._rebuildAvailableQuotaTokens();
422
+ }
423
+
424
  async getToken() {
425
+ await this._ensureInitialized();
426
  if (this.tokens.length === 0) return null;
427
 
428
+ // 针对额度耗尽策略做单独的高性能处理
429
+ if (this.rotationStrategy === RotationStrategy.QUOTA_EXHAUSTED) {
430
+ return this._getTokenForQuotaExhaustedStrategy();
431
+ }
432
+
433
+ return this._getTokenForDefaultStrategy();
434
+ }
435
+
436
+ /**
437
+ * 额度耗尽策略的 token 获取
438
+ * @private
439
+ */
440
+ async _getTokenForQuotaExhaustedStrategy() {
441
+ // 如果当前没有可用 token,尝试重置额度
442
+ if (this.availableQuotaTokenIndices.length === 0) {
443
+ this._resetAllQuotas();
444
+ }
445
+
446
+ const totalAvailable = this.availableQuotaTokenIndices.length;
447
+ if (totalAvailable === 0) {
448
+ return null;
449
+ }
450
+
451
+ const startIndex = this.currentQuotaIndex % totalAvailable;
452
+
453
+ for (let i = 0; i < totalAvailable; i++) {
454
+ const listIndex = (startIndex + i) % totalAvailable;
455
+ const tokenIndex = this.availableQuotaTokenIndices[listIndex];
456
+ const token = this.tokens[tokenIndex];
457
+
458
+ try {
459
+ const result = await this._prepareToken(token);
460
+ if (result === 'disable') {
461
+ this.disableToken(token);
462
+ this._rebuildAvailableQuotaTokens();
463
+ if (this.tokens.length === 0 || this.availableQuotaTokenIndices.length === 0) {
464
+ return null;
465
+ }
466
+ continue;
467
+ }
468
+
469
+ this.currentIndex = tokenIndex;
470
+ this.currentQuotaIndex = listIndex;
471
+ return token;
472
+ } catch (error) {
473
+ const action = this._handleTokenError(error, token);
474
+ if (action === 'disable') {
475
+ this.disableToken(token);
476
+ this._rebuildAvailableQuotaTokens();
477
+ if (this.tokens.length === 0 || this.availableQuotaTokenIndices.length === 0) {
478
+ return null;
479
+ }
480
+ }
481
+ // skip: 继续尝试下一个 token
482
+ }
483
+ }
484
+
485
+ // 所有可用 token 都不可用,重置额度状态
486
+ this._resetAllQuotas();
487
+ return this.tokens[0] || null;
488
+ }
489
+
490
+ /**
491
+ * 默认策略(round_robin / request_count)的 token 获取
492
+ * @private
493
+ */
494
+ async _getTokenForDefaultStrategy() {
495
  const totalTokens = this.tokens.length;
496
  const startIndex = this.currentIndex;
497
 
498
  for (let i = 0; i < totalTokens; i++) {
499
  const index = (startIndex + i) % totalTokens;
500
  const token = this.tokens[index];
501
+
 
 
 
 
 
502
  try {
503
+ const result = await this._prepareToken(token);
504
+ if (result === 'disable') {
505
+ this.disableToken(token);
506
+ if (this.tokens.length === 0) return null;
507
+ continue;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
508
  }
509
+
510
  // 更新当前索引
511
  this.currentIndex = index;
512
+
513
  // 根据策略决定是否切换
514
  if (this.shouldRotate(token)) {
515
+ this.currentIndex = (this.currentIndex + 1) % this.tokens.length;
516
  }
517
+
518
  return token;
519
  } catch (error) {
520
+ const action = this._handleTokenError(error, token);
521
+ if (action === 'disable') {
522
  this.disableToken(token);
523
  if (this.tokens.length === 0) return null;
 
 
524
  }
525
+ // skip: 继续尝试下一个 token
526
  }
527
  }
528
 
 
 
 
 
 
 
 
 
 
 
 
529
  return null;
530
  }
531
 
 
538
 
539
  // API管理方法
540
  async reload() {
541
+ this._initPromise = this._initialize();
542
+ await this._initPromise;
543
  log.info('Token已热重载');
544
  }
545
 
546
+ async addToken(tokenData) {
547
  try {
548
+ const allTokens = await this.store.readAll();
 
 
549
 
550
  const newToken = {
551
  access_token: tokenData.access_token,
 
566
  }
567
 
568
  allTokens.push(newToken);
569
+ await this.store.writeAll(allTokens);
570
 
571
+ await this.reload();
572
  return { success: true, message: 'Token添加成功' };
573
  } catch (error) {
574
  log.error('添加Token失败:', error.message);
 
576
  }
577
  }
578
 
579
+ async updateToken(refreshToken, updates) {
580
  try {
581
+ const allTokens = await this.store.readAll();
 
 
582
 
583
  const index = allTokens.findIndex(t => t.refresh_token === refreshToken);
584
  if (index === -1) {
 
586
  }
587
 
588
  allTokens[index] = { ...allTokens[index], ...updates };
589
+ await this.store.writeAll(allTokens);
590
 
591
+ await this.reload();
592
  return { success: true, message: 'Token更新成功' };
593
  } catch (error) {
594
  log.error('更新Token失败:', error.message);
 
596
  }
597
  }
598
 
599
+ async deleteToken(refreshToken) {
600
  try {
601
+ const allTokens = await this.store.readAll();
 
 
602
 
603
  const filteredTokens = allTokens.filter(t => t.refresh_token !== refreshToken);
604
  if (filteredTokens.length === allTokens.length) {
605
  return { success: false, message: 'Token不存在' };
606
  }
607
 
608
+ await this.store.writeAll(filteredTokens);
609
 
610
+ await this.reload();
611
  return { success: true, message: 'Token删除成功' };
612
  } catch (error) {
613
  log.error('删除Token失败:', error.message);
 
615
  }
616
  }
617
 
618
+ async getTokenList() {
619
  try {
620
+ const allTokens = await this.store.readAll();
 
 
621
 
622
  return allTokens.map(token => ({
623
  refresh_token: token.refresh_token,
src/auth/token_store.js ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import { getDataDir } from '../utils/paths.js';
4
+ import { FILE_CACHE_TTL } from '../constants/index.js';
5
+ import { log } from '../utils/logger.js';
6
+
7
+ /**
8
+ * 负责 token 文件的读写与简单缓存
9
+ * 不关心业务字段,只处理 JSON 数组的加载和保存
10
+ */
11
+ class TokenStore {
12
+ constructor(filePath = path.join(getDataDir(), 'accounts.json')) {
13
+ this.filePath = filePath;
14
+ this._cache = null;
15
+ this._cacheTime = 0;
16
+ this._cacheTTL = FILE_CACHE_TTL;
17
+ }
18
+
19
+ async _ensureFileExists() {
20
+ const dir = path.dirname(this.filePath);
21
+ try {
22
+ await fs.mkdir(dir, { recursive: true });
23
+ } catch (e) {
24
+ // 目录已存在等情况忽略
25
+ }
26
+
27
+ try {
28
+ await fs.access(this.filePath);
29
+ } catch (e) {
30
+ // 文件不存在时创建空数组
31
+ await fs.writeFile(this.filePath, '[]', 'utf8');
32
+ log.info('✓ 已创建账号配置文件');
33
+ }
34
+ }
35
+
36
+ _isCacheValid() {
37
+ if (!this._cache) return false;
38
+ const now = Date.now();
39
+ return (now - this._cacheTime) < this._cacheTTL;
40
+ }
41
+
42
+ /**
43
+ * 读取全部 token(包含禁用的),带简单内存缓存
44
+ * @returns {Promise<Array<object>>}
45
+ */
46
+ async readAll() {
47
+ if (this._isCacheValid()) {
48
+ return this._cache;
49
+ }
50
+
51
+ await this._ensureFileExists();
52
+ try {
53
+ const data = await fs.readFile(this.filePath, 'utf8');
54
+ const parsed = JSON.parse(data || '[]');
55
+ if (!Array.isArray(parsed)) {
56
+ log.warn('账号配置文件格式异常,已重置为空数组');
57
+ this._cache = [];
58
+ } else {
59
+ this._cache = parsed;
60
+ }
61
+ } catch (error) {
62
+ log.error('读取账号配置文件失败:', error.message);
63
+ this._cache = [];
64
+ }
65
+ this._cacheTime = Date.now();
66
+ return this._cache;
67
+ }
68
+
69
+ /**
70
+ * 覆盖写入全部 token,更新缓存
71
+ * @param {Array<object>} tokens
72
+ */
73
+ async writeAll(tokens) {
74
+ await this._ensureFileExists();
75
+ const normalized = Array.isArray(tokens) ? tokens : [];
76
+ try {
77
+ await fs.writeFile(this.filePath, JSON.stringify(normalized, null, 2), 'utf8');
78
+ this._cache = normalized;
79
+ this._cacheTime = Date.now();
80
+ } catch (error) {
81
+ log.error('保存账号配置文件失败:', error.message);
82
+ throw error;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * 根据内存中的启用 token 列表,将对应记录合并回文件
88
+ * - 仅按 refresh_token 匹配并更新已有记录
89
+ * - 未出现在 activeTokens 中的记录(例如已禁用账号)保持不变
90
+ * @param {Array<object>} activeTokens - 内存中的启用 token 列表(可能包含 sessionId)
91
+ * @param {object|null} tokenToUpdate - 如果只需要单个更新,可传入该 token 以减少遍历
92
+ */
93
+ async mergeActiveTokens(activeTokens, tokenToUpdate = null) {
94
+ const allTokens = [...await this.readAll()];
95
+
96
+ const applyUpdate = (targetToken) => {
97
+ if (!targetToken) return;
98
+ const index = allTokens.findIndex(t => t.refresh_token === targetToken.refresh_token);
99
+ if (index !== -1) {
100
+ const { sessionId, ...plain } = targetToken;
101
+ allTokens[index] = { ...allTokens[index], ...plain };
102
+ }
103
+ };
104
+
105
+ if (tokenToUpdate) {
106
+ applyUpdate(tokenToUpdate);
107
+ } else if (Array.isArray(activeTokens) && activeTokens.length > 0) {
108
+ for (const memToken of activeTokens) {
109
+ applyUpdate(memToken);
110
+ }
111
+ }
112
+
113
+ await this.writeAll(allTokens);
114
+ }
115
+ }
116
+
117
+ export default TokenStore;
src/config/config.js CHANGED
@@ -1,65 +1,26 @@
1
  import dotenv from 'dotenv';
2
  import fs from 'fs';
3
- import path from 'path';
4
- import { fileURLToPath } from 'url';
5
  import log from '../utils/logger.js';
6
  import { deepMerge } from '../utils/deepMerge.js';
7
-
8
- const __filename = fileURLToPath(import.meta.url);
9
- const __dirname = path.dirname(__filename);
10
-
11
- // 检测是否在 pkg 打包环境中运行
12
- const isPkg = typeof process.pkg !== 'undefined';
13
-
14
- // 获取配置文件路径
15
- // pkg 环境下使用可执行文件所在目录或当前工作目录
16
- function getConfigPaths() {
17
- if (isPkg) {
18
- // pkg 环境:优先使用可执行文件旁边的配置文件
19
- const exeDir = path.dirname(process.execPath);
20
- const cwdDir = process.cwd();
21
-
22
- // 查找 .env 文件
23
- let envPath = path.join(exeDir, '.env');
24
- if (!fs.existsSync(envPath)) {
25
- const cwdEnvPath = path.join(cwdDir, '.env');
26
- if (fs.existsSync(cwdEnvPath)) {
27
- envPath = cwdEnvPath;
28
- }
29
- }
30
-
31
- // 查找 config.json 文件
32
- let configJsonPath = path.join(exeDir, 'config.json');
33
- if (!fs.existsSync(configJsonPath)) {
34
- const cwdConfigPath = path.join(cwdDir, 'config.json');
35
- if (fs.existsSync(cwdConfigPath)) {
36
- configJsonPath = cwdConfigPath;
37
- }
38
- }
39
-
40
- // 查找 .env.example 文件
41
- let examplePath = path.join(exeDir, '.env.example');
42
- if (!fs.existsSync(examplePath)) {
43
- const cwdExamplePath = path.join(cwdDir, '.env.example');
44
- if (fs.existsSync(cwdExamplePath)) {
45
- examplePath = cwdExamplePath;
46
- }
47
- }
48
-
49
- return { envPath, configJsonPath, examplePath };
50
- }
51
-
52
- // 开发环境
53
- return {
54
- envPath: path.join(__dirname, '../../.env'),
55
- configJsonPath: path.join(__dirname, '../../config.json'),
56
- examplePath: path.join(__dirname, '../../.env.example')
57
- };
58
- }
59
 
60
  const { envPath, configJsonPath, examplePath } = getConfigPaths();
61
 
62
- // 确保 .env 存在
63
  if (!fs.existsSync(envPath)) {
64
  if (fs.existsSync(examplePath)) {
65
  fs.copyFileSync(examplePath, envPath);
@@ -100,24 +61,26 @@ export function getProxyConfig() {
100
 
101
  /**
102
  * 从 JSON 和环境变量构建配置对象
 
 
103
  */
104
  export function buildConfig(jsonConfig) {
105
  return {
106
  server: {
107
- port: jsonConfig.server?.port || 8045,
108
- host: jsonConfig.server?.host || '0.0.0.0',
109
- heartbeatInterval: jsonConfig.server?.heartbeatInterval || 15000,
110
  memoryThreshold: jsonConfig.server?.memoryThreshold || 500
111
  },
112
  cache: {
113
- modelListTTL: jsonConfig.cache?.modelListTTL || 60 * 60 * 1000
114
  },
115
  rotation: {
116
  strategy: jsonConfig.rotation?.strategy || 'round_robin',
117
  requestCount: jsonConfig.rotation?.requestCount || 10
118
  },
119
  imageBaseUrl: process.env.IMAGE_BASE_URL || null,
120
- maxImages: jsonConfig.other?.maxImages || 10,
121
  api: {
122
  url: jsonConfig.api?.url || 'https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:streamGenerateContent?alt=sse',
123
  modelsUrl: jsonConfig.api?.modelsUrl || 'https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:fetchAvailableModels',
@@ -126,28 +89,29 @@ export function buildConfig(jsonConfig) {
126
  userAgent: jsonConfig.api?.userAgent || 'antigravity/1.11.3 windows/amd64'
127
  },
128
  defaults: {
129
- temperature: jsonConfig.defaults?.temperature || 1,
130
- top_p: jsonConfig.defaults?.topP || 0.85,
131
- top_k: jsonConfig.defaults?.topK || 50,
132
- max_tokens: jsonConfig.defaults?.maxTokens || 32000,
133
- thinking_budget: jsonConfig.defaults?.thinkingBudget ?? 1024
134
  },
135
  security: {
136
- maxRequestSize: jsonConfig.server?.maxRequestSize || '50mb',
137
  apiKey: process.env.API_KEY || null
138
  },
139
  admin: {
140
- username: process.env.ADMIN_USERNAME || 'admin',
141
- password: process.env.ADMIN_PASSWORD || 'admin123',
142
- jwtSecret: process.env.JWT_SECRET || 'your-jwt-secret-key-change-this-in-production'
143
  },
144
  useNativeAxios: jsonConfig.other?.useNativeAxios !== false,
145
- timeout: jsonConfig.other?.timeout || 300000,
146
- retryTimes: Number.isFinite(jsonConfig.other?.retryTimes) ? jsonConfig.other.retryTimes : 3,
147
  proxy: getProxyConfig(),
148
  systemInstruction: process.env.SYSTEM_INSTRUCTION || '',
149
  skipProjectIdFetch: jsonConfig.other?.skipProjectIdFetch === true,
150
- useContextSystemPrompt: jsonConfig.other?.useContextSystemPrompt === true
 
151
  };
152
  }
153
 
 
1
  import dotenv from 'dotenv';
2
  import fs from 'fs';
 
 
3
  import log from '../utils/logger.js';
4
  import { deepMerge } from '../utils/deepMerge.js';
5
+ import { getConfigPaths } from '../utils/paths.js';
6
+ import {
7
+ DEFAULT_SERVER_PORT,
8
+ DEFAULT_SERVER_HOST,
9
+ DEFAULT_HEARTBEAT_INTERVAL,
10
+ DEFAULT_TIMEOUT,
11
+ DEFAULT_RETRY_TIMES,
12
+ DEFAULT_MAX_REQUEST_SIZE,
13
+ DEFAULT_MAX_IMAGES,
14
+ MODEL_LIST_CACHE_TTL,
15
+ DEFAULT_GENERATION_PARAMS,
16
+ DEFAULT_ADMIN_USERNAME,
17
+ DEFAULT_ADMIN_PASSWORD,
18
+ DEFAULT_JWT_SECRET
19
+ } from '../constants/index.js';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  const { envPath, configJsonPath, examplePath } = getConfigPaths();
22
 
23
+ // 确保 .env 存在(如果缺失则从 .env.example 复制一份)
24
  if (!fs.existsSync(envPath)) {
25
  if (fs.existsSync(examplePath)) {
26
  fs.copyFileSync(examplePath, envPath);
 
61
 
62
  /**
63
  * 从 JSON 和环境变量构建配置对象
64
+ * @param {Object} jsonConfig - JSON 配置对象
65
+ * @returns {Object} 完整配置对象
66
  */
67
  export function buildConfig(jsonConfig) {
68
  return {
69
  server: {
70
+ port: jsonConfig.server?.port || DEFAULT_SERVER_PORT,
71
+ host: jsonConfig.server?.host || DEFAULT_SERVER_HOST,
72
+ heartbeatInterval: jsonConfig.server?.heartbeatInterval || DEFAULT_HEARTBEAT_INTERVAL,
73
  memoryThreshold: jsonConfig.server?.memoryThreshold || 500
74
  },
75
  cache: {
76
+ modelListTTL: jsonConfig.cache?.modelListTTL || MODEL_LIST_CACHE_TTL
77
  },
78
  rotation: {
79
  strategy: jsonConfig.rotation?.strategy || 'round_robin',
80
  requestCount: jsonConfig.rotation?.requestCount || 10
81
  },
82
  imageBaseUrl: process.env.IMAGE_BASE_URL || null,
83
+ maxImages: jsonConfig.other?.maxImages || DEFAULT_MAX_IMAGES,
84
  api: {
85
  url: jsonConfig.api?.url || 'https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:streamGenerateContent?alt=sse',
86
  modelsUrl: jsonConfig.api?.modelsUrl || 'https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:fetchAvailableModels',
 
89
  userAgent: jsonConfig.api?.userAgent || 'antigravity/1.11.3 windows/amd64'
90
  },
91
  defaults: {
92
+ temperature: jsonConfig.defaults?.temperature ?? DEFAULT_GENERATION_PARAMS.temperature,
93
+ top_p: jsonConfig.defaults?.topP ?? DEFAULT_GENERATION_PARAMS.top_p,
94
+ top_k: jsonConfig.defaults?.topK ?? DEFAULT_GENERATION_PARAMS.top_k,
95
+ max_tokens: jsonConfig.defaults?.maxTokens ?? DEFAULT_GENERATION_PARAMS.max_tokens,
96
+ thinking_budget: jsonConfig.defaults?.thinkingBudget ?? DEFAULT_GENERATION_PARAMS.thinking_budget
97
  },
98
  security: {
99
+ maxRequestSize: jsonConfig.server?.maxRequestSize || DEFAULT_MAX_REQUEST_SIZE,
100
  apiKey: process.env.API_KEY || null
101
  },
102
  admin: {
103
+ username: process.env.ADMIN_USERNAME || DEFAULT_ADMIN_USERNAME,
104
+ password: process.env.ADMIN_PASSWORD || DEFAULT_ADMIN_PASSWORD,
105
+ jwtSecret: process.env.JWT_SECRET || DEFAULT_JWT_SECRET
106
  },
107
  useNativeAxios: jsonConfig.other?.useNativeAxios !== false,
108
+ timeout: jsonConfig.other?.timeout || DEFAULT_TIMEOUT,
109
+ retryTimes: Number.isFinite(jsonConfig.other?.retryTimes) ? jsonConfig.other.retryTimes : DEFAULT_RETRY_TIMES,
110
  proxy: getProxyConfig(),
111
  systemInstruction: process.env.SYSTEM_INSTRUCTION || '',
112
  skipProjectIdFetch: jsonConfig.other?.skipProjectIdFetch === true,
113
+ useContextSystemPrompt: jsonConfig.other?.useContextSystemPrompt === true,
114
+ passSignatureToClient: jsonConfig.other?.passSignatureToClient === true
115
  };
116
  }
117
 
src/constants/index.js ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * 应用常量定义
3
+ * @module constants
4
+ */
5
+
6
+ // ==================== 缓存相关常量 ====================
7
+
8
+ /**
9
+ * 文件缓存有效期(毫秒)
10
+ * @type {number}
11
+ */
12
+ export const FILE_CACHE_TTL = 5000;
13
+
14
+ /**
15
+ * 文件保存延迟(毫秒)- 用于 debounce
16
+ * @type {number}
17
+ */
18
+ export const FILE_SAVE_DELAY = 1000;
19
+
20
+ /**
21
+ * 额度缓存有效期(毫秒)- 5分钟
22
+ * @type {number}
23
+ */
24
+ export const QUOTA_CACHE_TTL = 5 * 60 * 1000;
25
+
26
+ /**
27
+ * 额度清理间隔(毫秒)- 1小时
28
+ * @type {number}
29
+ */
30
+ export const QUOTA_CLEANUP_INTERVAL = 60 * 60 * 1000;
31
+
32
+ /**
33
+ * 模型列表缓存默认有效期(毫秒)- 1小时
34
+ * @type {number}
35
+ */
36
+ export const MODEL_LIST_CACHE_TTL = 60 * 60 * 1000;
37
+
38
+ // ==================== 内存管理常量 ====================
39
+
40
+ /**
41
+ * 内存压力阈值(字节)
42
+ */
43
+ export const MEMORY_THRESHOLDS = {
44
+ /** 低压力阈值 - 15MB */
45
+ LOW: 15 * 1024 * 1024,
46
+ /** 中等压力阈值 - 25MB */
47
+ MEDIUM: 25 * 1024 * 1024,
48
+ /** 高压力阈值 - 35MB */
49
+ HIGH: 35 * 1024 * 1024,
50
+ /** 目标内存 - 20MB */
51
+ TARGET: 20 * 1024 * 1024
52
+ };
53
+
54
+ /**
55
+ * GC 冷却时间(毫秒)
56
+ * @type {number}
57
+ */
58
+ export const GC_COOLDOWN = 10000;
59
+
60
+ /**
61
+ * 默认内存检查间隔(毫秒)
62
+ * @type {number}
63
+ */
64
+ export const MEMORY_CHECK_INTERVAL = 30000;
65
+
66
+ // ==================== 服务器相关常量 ====================
67
+
68
+ /**
69
+ * 默认心跳间隔(毫秒)
70
+ * @type {number}
71
+ */
72
+ export const DEFAULT_HEARTBEAT_INTERVAL = 15000;
73
+
74
+ /**
75
+ * 默认服务器端口
76
+ * @type {number}
77
+ */
78
+ export const DEFAULT_SERVER_PORT = 8045;
79
+
80
+ /**
81
+ * 默认服务器主机
82
+ * @type {string}
83
+ */
84
+ export const DEFAULT_SERVER_HOST = '0.0.0.0';
85
+
86
+ /**
87
+ * 默认请求超时(毫秒)
88
+ * @type {number}
89
+ */
90
+ export const DEFAULT_TIMEOUT = 300000;
91
+
92
+ /**
93
+ * 默认重试次数
94
+ * @type {number}
95
+ */
96
+ export const DEFAULT_RETRY_TIMES = 3;
97
+
98
+ /**
99
+ * 默认最大请求体大小
100
+ * @type {string}
101
+ */
102
+ export const DEFAULT_MAX_REQUEST_SIZE = '50mb';
103
+
104
+ // ==================== Token 轮询相关常量 ====================
105
+
106
+ /**
107
+ * 默认每个 Token 请求次数后切换
108
+ * @type {number}
109
+ */
110
+ export const DEFAULT_REQUEST_COUNT_PER_TOKEN = 50;
111
+
112
+ /**
113
+ * Token 过期提前刷新时间(毫秒)- 5分钟
114
+ * @type {number}
115
+ */
116
+ export const TOKEN_REFRESH_BUFFER = 300000;
117
+
118
+ // ==================== 生成参数默认值 ====================
119
+
120
+ /**
121
+ * 默认生成参数
122
+ */
123
+ export const DEFAULT_GENERATION_PARAMS = {
124
+ temperature: 1,
125
+ top_p: 0.85,
126
+ top_k: 50,
127
+ max_tokens: 32000,
128
+ thinking_budget: 1024
129
+ };
130
+
131
+ /**
132
+ * reasoning_effort 到 thinkingBudget 的映射
133
+ */
134
+ export const REASONING_EFFORT_MAP = {
135
+ low: 1024,
136
+ medium: 16000,
137
+ high: 32000
138
+ };
139
+
140
+ // ==================== 图片相关常量 ====================
141
+
142
+ /**
143
+ * 默认最大保留图片数量
144
+ * @type {number}
145
+ */
146
+ export const DEFAULT_MAX_IMAGES = 10;
147
+
148
+ /**
149
+ * MIME 类型到文件扩展名映射
150
+ */
151
+ export const MIME_TO_EXT = {
152
+ 'image/jpeg': 'jpg',
153
+ 'image/png': 'png',
154
+ 'image/gif': 'gif',
155
+ 'image/webp': 'webp'
156
+ };
157
+
158
+ // ==================== 停止序列 ====================
159
+
160
+ /**
161
+ * 默认停止序列
162
+ * @type {string[]}
163
+ */
164
+ export const DEFAULT_STOP_SEQUENCES = [
165
+ '<|user|>',
166
+ '<|bot|>',
167
+ '<|context_request|>',
168
+ '<|endoftext|>',
169
+ '<|end_of_turn|>'
170
+ ];
171
+
172
+ // ==================== 管理员默认配置 ====================
173
+
174
+ /**
175
+ * 默认管理员用户名
176
+ * @type {string}
177
+ */
178
+ export const DEFAULT_ADMIN_USERNAME = 'admin';
179
+
180
+ /**
181
+ * 默认管理员密码
182
+ * @type {string}
183
+ */
184
+ export const DEFAULT_ADMIN_PASSWORD = 'admin123';
185
+
186
+ /**
187
+ * 默认 JWT 密钥(生产环境应更改)
188
+ * @type {string}
189
+ */
190
+ export const DEFAULT_JWT_SECRET = 'your-jwt-secret-key-change-this-in-production';
src/routes/admin.js CHANGED
@@ -1,5 +1,4 @@
1
  import express from 'express';
2
- import fs from 'fs';
3
  import { generateToken, authMiddleware } from '../auth/jwt.js';
4
  import tokenManager from '../auth/token_manager.js';
5
  import quotaManager from '../auth/quota_manager.js';
@@ -10,38 +9,9 @@ import { parseEnvFile, updateEnvFile } from '../utils/envParser.js';
10
  import { reloadConfig } from '../utils/configReloader.js';
11
  import { deepMerge } from '../utils/deepMerge.js';
12
  import { getModelsWithQuotas } from '../api/client.js';
13
- import path from 'path';
14
- import { fileURLToPath } from 'url';
15
  import dotenv from 'dotenv';
16
 
17
- const __filename = fileURLToPath(import.meta.url);
18
- const __dirname = path.dirname(__filename);
19
-
20
- // 检测是否在 pkg 打包环境中运行
21
- const isPkg = typeof process.pkg !== 'undefined';
22
-
23
- // 获取 .env 文件路径
24
- // pkg 环境下使用可执行文件所在目录或当前工作目录
25
- function getEnvPath() {
26
- if (isPkg) {
27
- // pkg 环境:优先使用可执行文件旁边的 .env
28
- const exeDir = path.dirname(process.execPath);
29
- const exeEnvPath = path.join(exeDir, '.env');
30
- if (fs.existsSync(exeEnvPath)) {
31
- return exeEnvPath;
32
- }
33
- // 其次使用当前工作目录的 .env
34
- const cwdEnvPath = path.join(process.cwd(), '.env');
35
- if (fs.existsSync(cwdEnvPath)) {
36
- return cwdEnvPath;
37
- }
38
- // 返回可执行文件目录的路径(即使不存在)
39
- return exeEnvPath;
40
- }
41
- // 开发环境
42
- return path.join(__dirname, '../../.env');
43
- }
44
-
45
  const envPath = getEnvPath();
46
 
47
  const router = express.Router();
@@ -59,12 +29,17 @@ router.post('/login', (req, res) => {
59
  });
60
 
61
  // Token管理API - 需要JWT认证
62
- router.get('/tokens', authMiddleware, (req, res) => {
63
- const tokens = tokenManager.getTokenList();
64
- res.json({ success: true, data: tokens });
 
 
 
 
 
65
  });
66
 
67
- router.post('/tokens', authMiddleware, (req, res) => {
68
  const { access_token, refresh_token, expires_in, timestamp, enable, projectId, email } = req.body;
69
  if (!access_token || !refresh_token) {
70
  return res.status(400).json({ success: false, message: 'access_token和refresh_token必填' });
@@ -75,21 +50,36 @@ router.post('/tokens', authMiddleware, (req, res) => {
75
  if (projectId) tokenData.projectId = projectId;
76
  if (email) tokenData.email = email;
77
 
78
- const result = tokenManager.addToken(tokenData);
79
- res.json(result);
 
 
 
 
 
80
  });
81
 
82
- router.put('/tokens/:refreshToken', authMiddleware, (req, res) => {
83
  const { refreshToken } = req.params;
84
  const updates = req.body;
85
- const result = tokenManager.updateToken(refreshToken, updates);
86
- res.json(result);
 
 
 
 
 
87
  });
88
 
89
- router.delete('/tokens/:refreshToken', authMiddleware, (req, res) => {
90
  const { refreshToken } = req.params;
91
- const result = tokenManager.deleteToken(refreshToken);
92
- res.json(result);
 
 
 
 
 
93
  });
94
 
95
  router.post('/tokens/reload', authMiddleware, async (req, res) => {
@@ -202,7 +192,7 @@ router.get('/tokens/:refreshToken/quotas', authMiddleware, async (req, res) => {
202
  try {
203
  const { refreshToken } = req.params;
204
  const forceRefresh = req.query.refresh === 'true';
205
- const tokens = tokenManager.getTokenList();
206
  let tokenData = tokens.find(t => t.refresh_token === refreshToken);
207
 
208
  if (!tokenData) {
 
1
  import express from 'express';
 
2
  import { generateToken, authMiddleware } from '../auth/jwt.js';
3
  import tokenManager from '../auth/token_manager.js';
4
  import quotaManager from '../auth/quota_manager.js';
 
9
  import { reloadConfig } from '../utils/configReloader.js';
10
  import { deepMerge } from '../utils/deepMerge.js';
11
  import { getModelsWithQuotas } from '../api/client.js';
12
+ import { getEnvPath } from '../utils/paths.js';
 
13
  import dotenv from 'dotenv';
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  const envPath = getEnvPath();
16
 
17
  const router = express.Router();
 
29
  });
30
 
31
  // Token管理API - 需要JWT认证
32
+ router.get('/tokens', authMiddleware, async (req, res) => {
33
+ try {
34
+ const tokens = await tokenManager.getTokenList();
35
+ res.json({ success: true, data: tokens });
36
+ } catch (error) {
37
+ logger.error('获取Token列表失败:', error.message);
38
+ res.status(500).json({ success: false, message: error.message });
39
+ }
40
  });
41
 
42
+ router.post('/tokens', authMiddleware, async (req, res) => {
43
  const { access_token, refresh_token, expires_in, timestamp, enable, projectId, email } = req.body;
44
  if (!access_token || !refresh_token) {
45
  return res.status(400).json({ success: false, message: 'access_token和refresh_token必填' });
 
50
  if (projectId) tokenData.projectId = projectId;
51
  if (email) tokenData.email = email;
52
 
53
+ try {
54
+ const result = await tokenManager.addToken(tokenData);
55
+ res.json(result);
56
+ } catch (error) {
57
+ logger.error('添加Token失败:', error.message);
58
+ res.status(500).json({ success: false, message: error.message });
59
+ }
60
  });
61
 
62
+ router.put('/tokens/:refreshToken', authMiddleware, async (req, res) => {
63
  const { refreshToken } = req.params;
64
  const updates = req.body;
65
+ try {
66
+ const result = await tokenManager.updateToken(refreshToken, updates);
67
+ res.json(result);
68
+ } catch (error) {
69
+ logger.error('更新Token失败:', error.message);
70
+ res.status(500).json({ success: false, message: error.message });
71
+ }
72
  });
73
 
74
+ router.delete('/tokens/:refreshToken', authMiddleware, async (req, res) => {
75
  const { refreshToken } = req.params;
76
+ try {
77
+ const result = await tokenManager.deleteToken(refreshToken);
78
+ res.json(result);
79
+ } catch (error) {
80
+ logger.error('删除Token失败:', error.message);
81
+ res.status(500).json({ success: false, message: error.message });
82
+ }
83
  });
84
 
85
  router.post('/tokens/reload', authMiddleware, async (req, res) => {
 
192
  try {
193
  const { refreshToken } = req.params;
194
  const forceRefresh = req.query.refresh === 'true';
195
+ const tokens = await tokenManager.getTokenList();
196
  let tokenData = tokens.find(t => t.refresh_token === refreshToken);
197
 
198
  if (!tokenData) {
src/server/index.js CHANGED
@@ -1,8 +1,6 @@
1
  import express from 'express';
2
  import cors from 'cors';
3
  import path from 'path';
4
- import fs from 'fs';
5
- import { fileURLToPath } from 'url';
6
  import { generateAssistantResponse, generateAssistantResponseNoStream, getAvailableModels, generateImageForSD, closeRequester } from '../api/client.js';
7
  import { generateRequestBody, prepareImageRequest } from '../utils/utils.js';
8
  import logger from '../utils/logger.js';
@@ -10,54 +8,13 @@ import config from '../config/config.js';
10
  import tokenManager from '../auth/token_manager.js';
11
  import adminRouter from '../routes/admin.js';
12
  import sdRouter from '../routes/sd.js';
13
- import memoryManager, { MemoryPressure, registerMemoryPoolCleanup } from '../utils/memoryManager.js';
14
-
15
- const __filename = fileURLToPath(import.meta.url);
16
- const __dirname = path.dirname(__filename);
17
-
18
- // 检测是否在 pkg 打包环境中运行
19
- const isPkg = typeof process.pkg !== 'undefined';
20
-
21
- // 获取静态文件目录
22
- // pkg 环境下使用可执行文件所在目录的 public 文件夹
23
- // 开发环境下使用项目根目录的 public 文件夹
24
- function getPublicDir() {
25
- if (isPkg) {
26
- // pkg 环境:优先使用可执行文件旁边的 public 目录
27
- const exeDir = path.dirname(process.execPath);
28
- const exePublicDir = path.join(exeDir, 'public');
29
- if (fs.existsSync(exePublicDir)) {
30
- return exePublicDir;
31
- }
32
- // 其次使用当前工作目录的 public 目录
33
- const cwdPublicDir = path.join(process.cwd(), 'public');
34
- if (fs.existsSync(cwdPublicDir)) {
35
- return cwdPublicDir;
36
- }
37
- // 最后使用打包内的 public 目录(通过 snapshot)
38
- return path.join(__dirname, '../../public');
39
- }
40
- // 开发环境
41
- return path.join(__dirname, '../../public');
42
- }
43
 
44
  const publicDir = getPublicDir();
45
 
46
- // 计算相对路径用于日志显示
47
- function getRelativePath(absolutePath) {
48
- if (isPkg) {
49
- const exeDir = path.dirname(process.execPath);
50
- if (absolutePath.startsWith(exeDir)) {
51
- return '.' + absolutePath.slice(exeDir.length).replace(/\\/g, '/');
52
- }
53
- const cwdDir = process.cwd();
54
- if (absolutePath.startsWith(cwdDir)) {
55
- return '.' + absolutePath.slice(cwdDir.length).replace(/\\/g, '/');
56
- }
57
- }
58
- return absolutePath;
59
- }
60
-
61
  logger.info(`静态文件目录: ${getRelativePath(publicDir)}`);
62
 
63
  const app = express();
@@ -84,7 +41,7 @@ const with429Retry = async (fn, maxRetries, loggerPrefix = '') => {
84
  };
85
 
86
  // ==================== 心跳机制(防止 CF 超时) ====================
87
- const HEARTBEAT_INTERVAL = config.server.heartbeatInterval || 15000; // 从配置读取心跳间隔
88
  const SSE_HEARTBEAT = Buffer.from(': heartbeat\n\n');
89
 
90
  // 创建心跳定时器
@@ -136,7 +93,7 @@ const releaseChunkObject = (obj) => {
136
  registerMemoryPoolCleanup(chunkPool, () => memoryManager.getPoolSizes().chunk);
137
 
138
  // 启动内存管理器
139
- memoryManager.start(30000);
140
 
141
  const createStreamChunk = (id, created, model, delta, finish_reason = null) => {
142
  const chunk = getChunkObject();
@@ -152,11 +109,6 @@ const createStreamChunk = (id, created, model, delta, finish_reason = null) => {
152
  // 工具函数:零拷贝写入流式数据
153
  const writeStreamData = (res, data) => {
154
  const json = JSON.stringify(data);
155
- // 释放对象回池
156
- const delta = { reasoning_content: data.reasoning_content };
157
- if (data.thoughtSignature) {
158
- delta.thoughtSignature = data.thoughtSignature;
159
- }
160
  res.write(SSE_PREFIX);
161
  res.write(json);
162
  res.write(SSE_SUFFIX);
@@ -169,38 +121,6 @@ const endStream = (res) => {
169
  res.end();
170
  };
171
 
172
- // OpenAI 兼容错误响应构造
173
- const buildOpenAIErrorPayload = (error, statusCode) => {
174
- if (error.isUpstreamApiError && error.rawBody) {
175
- try {
176
- const raw = typeof error.rawBody === 'string' ? JSON.parse(error.rawBody) : error.rawBody;
177
- const inner = raw.error || raw;
178
- return {
179
- error: {
180
- message: inner.message || error.message || 'Upstream API error',
181
- type: inner.type || 'upstream_api_error',
182
- code: inner.code ?? statusCode
183
- }
184
- };
185
- } catch {
186
- return {
187
- error: {
188
- message: error.rawBody || error.message || 'Upstream API error',
189
- type: 'upstream_api_error',
190
- code: statusCode
191
- }
192
- };
193
- }
194
- }
195
-
196
- return {
197
- error: {
198
- message: error.message || 'Internal server error',
199
- type: 'server_error',
200
- code: statusCode
201
- }
202
- };
203
- };
204
 
205
  app.use(cors());
206
  app.use(express.json({ limit: config.security.maxRequestSize }));
@@ -212,12 +132,8 @@ app.use(express.static(publicDir));
212
  // 管理路由
213
  app.use('/admin', adminRouter);
214
 
215
- app.use((err, req, res, next) => {
216
- if (err.type === 'entity.too.large') {
217
- return res.status(413).json({ error: `请求体过大,最大支持 ${config.security.maxRequestSize}` });
218
- }
219
- next(err);
220
- });
221
 
222
  app.use((req, res, next) => {
223
  const ignorePaths = ['/images', '/favicon.ico', '/.well-known', '/sdapi/v1/options', '/sdapi/v1/samplers', '/sdapi/v1/schedulers', '/sdapi/v1/upscalers', '/sdapi/v1/latent-upscale-modes', '/sdapi/v1/sd-vae', '/sdapi/v1/sd-modules'];
@@ -324,13 +240,21 @@ app.post('/v1/chat/completions', async (req, res) => {
324
  usageData = data.usage;
325
  } else if (data.type === 'reasoning') {
326
  const delta = { reasoning_content: data.reasoning_content };
327
- if (data.thoughtSignature) {
328
  delta.thoughtSignature = data.thoughtSignature;
329
  }
330
  writeStreamData(res, createStreamChunk(id, created, model, delta));
331
  } else if (data.type === 'tool_calls') {
332
  hasToolCall = true;
333
- const toolCallsWithIndex = data.tool_calls.map((toolCall, index) => ({ index, ...toolCall }));
 
 
 
 
 
 
 
 
334
  const delta = { tool_calls: toolCallsWithIndex };
335
  writeStreamData(res, createStreamChunk(id, created, model, delta));
336
  } else {
@@ -364,9 +288,16 @@ app.post('/v1/chat/completions', async (req, res) => {
364
  // DeepSeek 格式:reasoning_content 在 content 之前
365
  const message = { role: 'assistant' };
366
  if (reasoningContent) message.reasoning_content = reasoningContent;
367
- if (reasoningSignature) message.thoughtSignature = reasoningSignature;
368
  message.content = content;
369
- if (toolCalls.length > 0) message.tool_calls = toolCalls;
 
 
 
 
 
 
 
370
 
371
  // 使用预构建的响应对象,减少内存分配
372
  const response = {
 
1
  import express from 'express';
2
  import cors from 'cors';
3
  import path from 'path';
 
 
4
  import { generateAssistantResponse, generateAssistantResponseNoStream, getAvailableModels, generateImageForSD, closeRequester } from '../api/client.js';
5
  import { generateRequestBody, prepareImageRequest } from '../utils/utils.js';
6
  import logger from '../utils/logger.js';
 
8
  import tokenManager from '../auth/token_manager.js';
9
  import adminRouter from '../routes/admin.js';
10
  import sdRouter from '../routes/sd.js';
11
+ import memoryManager, { registerMemoryPoolCleanup } from '../utils/memoryManager.js';
12
+ import { getPublicDir, getRelativePath } from '../utils/paths.js';
13
+ import { DEFAULT_HEARTBEAT_INTERVAL, MEMORY_CHECK_INTERVAL } from '../constants/index.js';
14
+ import { buildOpenAIErrorPayload, errorHandler, ValidationError } from '../utils/errors.js';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  const publicDir = getPublicDir();
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  logger.info(`静态文件目录: ${getRelativePath(publicDir)}`);
19
 
20
  const app = express();
 
41
  };
42
 
43
  // ==================== 心跳机制(防止 CF 超时) ====================
44
+ const HEARTBEAT_INTERVAL = config.server.heartbeatInterval || DEFAULT_HEARTBEAT_INTERVAL;
45
  const SSE_HEARTBEAT = Buffer.from(': heartbeat\n\n');
46
 
47
  // 创建心跳定时器
 
93
  registerMemoryPoolCleanup(chunkPool, () => memoryManager.getPoolSizes().chunk);
94
 
95
  // 启动内存管理器
96
+ memoryManager.start(MEMORY_CHECK_INTERVAL);
97
 
98
  const createStreamChunk = (id, created, model, delta, finish_reason = null) => {
99
  const chunk = getChunkObject();
 
109
  // 工具函数:零拷贝写入流式数据
110
  const writeStreamData = (res, data) => {
111
  const json = JSON.stringify(data);
 
 
 
 
 
112
  res.write(SSE_PREFIX);
113
  res.write(json);
114
  res.write(SSE_SUFFIX);
 
121
  res.end();
122
  };
123
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
 
125
  app.use(cors());
126
  app.use(express.json({ limit: config.security.maxRequestSize }));
 
132
  // 管理路由
133
  app.use('/admin', adminRouter);
134
 
135
+ // 使用统一错误处理中间件
136
+ app.use(errorHandler);
 
 
 
 
137
 
138
  app.use((req, res, next) => {
139
  const ignorePaths = ['/images', '/favicon.ico', '/.well-known', '/sdapi/v1/options', '/sdapi/v1/samplers', '/sdapi/v1/schedulers', '/sdapi/v1/upscalers', '/sdapi/v1/latent-upscale-modes', '/sdapi/v1/sd-vae', '/sdapi/v1/sd-modules'];
 
240
  usageData = data.usage;
241
  } else if (data.type === 'reasoning') {
242
  const delta = { reasoning_content: data.reasoning_content };
243
+ if (data.thoughtSignature && config.passSignatureToClient) {
244
  delta.thoughtSignature = data.thoughtSignature;
245
  }
246
  writeStreamData(res, createStreamChunk(id, created, model, delta));
247
  } else if (data.type === 'tool_calls') {
248
  hasToolCall = true;
249
+ // 根据配置决定是否透传工具调用中的签名
250
+ const toolCallsWithIndex = data.tool_calls.map((toolCall, index) => {
251
+ if (config.passSignatureToClient) {
252
+ return { index, ...toolCall };
253
+ } else {
254
+ const { thoughtSignature, ...rest } = toolCall;
255
+ return { index, ...rest };
256
+ }
257
+ });
258
  const delta = { tool_calls: toolCallsWithIndex };
259
  writeStreamData(res, createStreamChunk(id, created, model, delta));
260
  } else {
 
288
  // DeepSeek 格式:reasoning_content 在 content 之前
289
  const message = { role: 'assistant' };
290
  if (reasoningContent) message.reasoning_content = reasoningContent;
291
+ if (reasoningSignature && config.passSignatureToClient) message.thoughtSignature = reasoningSignature;
292
  message.content = content;
293
+ if (toolCalls.length > 0) {
294
+ // 根据配置决定是否透传工具调用中的签名
295
+ if (config.passSignatureToClient) {
296
+ message.tool_calls = toolCalls;
297
+ } else {
298
+ message.tool_calls = toolCalls.map(({ thoughtSignature, ...rest }) => rest);
299
+ }
300
+ }
301
 
302
  // 使用预构建的响应对象,减少内存分配
303
  const response = {
src/utils/errors.js ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * 统一错误处理模块
3
+ * @module utils/errors
4
+ */
5
+
6
+ /**
7
+ * 应用错误基类
8
+ */
9
+ export class AppError extends Error {
10
+ /**
11
+ * @param {string} message - 错误消息
12
+ * @param {number} statusCode - HTTP 状态码
13
+ * @param {string} type - 错误类型
14
+ */
15
+ constructor(message, statusCode = 500, type = 'server_error') {
16
+ super(message);
17
+ this.name = 'AppError';
18
+ this.statusCode = statusCode;
19
+ this.type = type;
20
+ this.isOperational = true;
21
+ Error.captureStackTrace(this, this.constructor);
22
+ }
23
+ }
24
+
25
+ /**
26
+ * 上游 API 错误
27
+ */
28
+ export class UpstreamApiError extends AppError {
29
+ /**
30
+ * @param {string} message - 错误消息
31
+ * @param {number} statusCode - HTTP 状态码
32
+ * @param {string|Object} rawBody - 原始响应体
33
+ */
34
+ constructor(message, statusCode, rawBody = null) {
35
+ super(message, statusCode, 'upstream_api_error');
36
+ this.name = 'UpstreamApiError';
37
+ this.rawBody = rawBody;
38
+ this.isUpstreamApiError = true;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * 认证错误
44
+ */
45
+ export class AuthenticationError extends AppError {
46
+ /**
47
+ * @param {string} message - 错误消息
48
+ */
49
+ constructor(message = '认证失败') {
50
+ super(message, 401, 'authentication_error');
51
+ this.name = 'AuthenticationError';
52
+ }
53
+ }
54
+
55
+ /**
56
+ * 授权错误
57
+ */
58
+ export class AuthorizationError extends AppError {
59
+ /**
60
+ * @param {string} message - 错误消息
61
+ */
62
+ constructor(message = '无权限访问') {
63
+ super(message, 403, 'authorization_error');
64
+ this.name = 'AuthorizationError';
65
+ }
66
+ }
67
+
68
+ /**
69
+ * 验证错误
70
+ */
71
+ export class ValidationError extends AppError {
72
+ /**
73
+ * @param {string} message - 错误消息
74
+ * @param {Object} details - 验证详情
75
+ */
76
+ constructor(message = '请求参数无效', details = null) {
77
+ super(message, 400, 'validation_error');
78
+ this.name = 'ValidationError';
79
+ this.details = details;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * 资源未找到错误
85
+ */
86
+ export class NotFoundError extends AppError {
87
+ /**
88
+ * @param {string} message - 错误消息
89
+ */
90
+ constructor(message = '资源未找到') {
91
+ super(message, 404, 'not_found');
92
+ this.name = 'NotFoundError';
93
+ }
94
+ }
95
+
96
+ /**
97
+ * 速率限制错误
98
+ */
99
+ export class RateLimitError extends AppError {
100
+ /**
101
+ * @param {string} message - 错误消息
102
+ * @param {number} retryAfter - 重试等待时间(秒)
103
+ */
104
+ constructor(message = '请求过于频繁', retryAfter = null) {
105
+ super(message, 429, 'rate_limit_error');
106
+ this.name = 'RateLimitError';
107
+ this.retryAfter = retryAfter;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Token 相关错误
113
+ */
114
+ export class TokenError extends AppError {
115
+ /**
116
+ * @param {string} message - 错误消息
117
+ * @param {string} tokenSuffix - Token 后缀(用于日志)
118
+ * @param {number} statusCode - HTTP 状态码
119
+ */
120
+ constructor(message, tokenSuffix = null, statusCode = 500) {
121
+ super(message, statusCode, 'token_error');
122
+ this.name = 'TokenError';
123
+ this.tokenSuffix = tokenSuffix;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * 创建上游 API 错误(工厂函数)
129
+ * @param {string} message - 错误消息
130
+ * @param {number} status - HTTP 状态码
131
+ * @param {string|Object} rawBody - 原始响应体
132
+ * @returns {UpstreamApiError}
133
+ */
134
+ export function createApiError(message, status, rawBody) {
135
+ return new UpstreamApiError(message, status, rawBody);
136
+ }
137
+
138
+ /**
139
+ * 构建 OpenAI 兼容的错误响应
140
+ * @param {Error} error - 错误对象
141
+ * @param {number} statusCode - HTTP 状态码
142
+ * @returns {{error: {message: string, type: string, code: number}}}
143
+ */
144
+ export function buildOpenAIErrorPayload(error, statusCode) {
145
+ // 处理上游 API 错误
146
+ if (error.isUpstreamApiError && error.rawBody) {
147
+ try {
148
+ const raw = typeof error.rawBody === 'string' ? JSON.parse(error.rawBody) : error.rawBody;
149
+ const inner = raw.error || raw;
150
+ return {
151
+ error: {
152
+ message: inner.message || error.message || 'Upstream API error',
153
+ type: inner.type || 'upstream_api_error',
154
+ code: inner.code ?? statusCode
155
+ }
156
+ };
157
+ } catch {
158
+ return {
159
+ error: {
160
+ message: error.rawBody || error.message || 'Upstream API error',
161
+ type: 'upstream_api_error',
162
+ code: statusCode
163
+ }
164
+ };
165
+ }
166
+ }
167
+
168
+ // 处理应用错误
169
+ if (error instanceof AppError) {
170
+ return {
171
+ error: {
172
+ message: error.message,
173
+ type: error.type,
174
+ code: error.statusCode
175
+ }
176
+ };
177
+ }
178
+
179
+ // 处理通用错误
180
+ return {
181
+ error: {
182
+ message: error.message || 'Internal server error',
183
+ type: 'server_error',
184
+ code: statusCode
185
+ }
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Express 错误处理中间件
191
+ * @param {Error} err - 错误对象
192
+ * @param {import('express').Request} req - 请求对象
193
+ * @param {import('express').Response} res - 响应对象
194
+ * @param {import('express').NextFunction} next - 下一个中间件
195
+ */
196
+ export function errorHandler(err, req, res, next) {
197
+ // 如果响应��发送,交给默认处理
198
+ if (res.headersSent) {
199
+ return next(err);
200
+ }
201
+
202
+ // 处理请求体过大错误
203
+ if (err.type === 'entity.too.large') {
204
+ return res.status(413).json({
205
+ error: {
206
+ message: '请求体过大',
207
+ type: 'payload_too_large',
208
+ code: 413
209
+ }
210
+ });
211
+ }
212
+
213
+ // 确定状态码
214
+ const statusCode = err.statusCode || err.status || 500;
215
+
216
+ // 构建错误响应
217
+ const errorPayload = buildOpenAIErrorPayload(err, statusCode);
218
+
219
+ return res.status(statusCode).json(errorPayload);
220
+ }
221
+
222
+ /**
223
+ * 异步路由包装器(自动捕获异步错误)
224
+ * @param {Function} fn - 异步路由处理函数
225
+ * @returns {Function} 包装后的路由处理函数
226
+ */
227
+ export function asyncHandler(fn) {
228
+ return (req, res, next) => {
229
+ Promise.resolve(fn(req, res, next)).catch(next);
230
+ };
231
+ }
src/utils/imageStorage.js CHANGED
@@ -1,48 +1,9 @@
1
  import fs from 'fs';
2
  import path from 'path';
3
- import { fileURLToPath } from 'url';
4
  import config from '../config/config.js';
5
  import { getDefaultIp } from './utils.js';
6
-
7
- const __filename = fileURLToPath(import.meta.url);
8
- const __dirname = path.dirname(__filename);
9
-
10
- // 检测是否在 pkg 打包环境中运行
11
- const isPkg = typeof process.pkg !== 'undefined';
12
-
13
- // 获取图片存储目录
14
- // pkg 环境下使用可执行文件所在目录或当前工作目录
15
- function getImageDir() {
16
- if (isPkg) {
17
- // pkg 环境:优先使用可执行文件旁边的 public/images 目录
18
- const exeDir = path.dirname(process.execPath);
19
- const exeImageDir = path.join(exeDir, 'public', 'images');
20
- try {
21
- if (!fs.existsSync(exeImageDir)) {
22
- fs.mkdirSync(exeImageDir, { recursive: true });
23
- }
24
- return exeImageDir;
25
- } catch (e) {
26
- // 如果无法创建,尝试当前工作目录
27
- const cwdImageDir = path.join(process.cwd(), 'public', 'images');
28
- try {
29
- if (!fs.existsSync(cwdImageDir)) {
30
- fs.mkdirSync(cwdImageDir, { recursive: true });
31
- }
32
- return cwdImageDir;
33
- } catch (e2) {
34
- // 最后使用用户主目录
35
- const homeImageDir = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.antigravity', 'images');
36
- if (!fs.existsSync(homeImageDir)) {
37
- fs.mkdirSync(homeImageDir, { recursive: true });
38
- }
39
- return homeImageDir;
40
- }
41
- }
42
- }
43
- // 开发环境
44
- return path.join(__dirname, '../../public/images');
45
- }
46
 
47
  const IMAGE_DIR = getImageDir();
48
 
@@ -51,14 +12,6 @@ if (!isPkg && !fs.existsSync(IMAGE_DIR)) {
51
  fs.mkdirSync(IMAGE_DIR, { recursive: true });
52
  }
53
 
54
- // MIME 类型到文件扩展名映射
55
- const MIME_TO_EXT = {
56
- 'image/jpeg': 'jpg',
57
- 'image/png': 'png',
58
- 'image/gif': 'gif',
59
- 'image/webp': 'webp'
60
- };
61
-
62
  /**
63
  * 清理超过限制数量的旧图片
64
  * @param {number} maxCount - 最大保留图片数量
 
1
  import fs from 'fs';
2
  import path from 'path';
 
3
  import config from '../config/config.js';
4
  import { getDefaultIp } from './utils.js';
5
+ import { getImageDir, isPkg } from './paths.js';
6
+ import { MIME_TO_EXT } from '../constants/index.js';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
  const IMAGE_DIR = getImageDir();
9
 
 
12
  fs.mkdirSync(IMAGE_DIR, { recursive: true });
13
  }
14
 
 
 
 
 
 
 
 
 
15
  /**
16
  * 清理超过限制数量的旧图片
17
  * @param {number} maxCount - 最大保留图片数量
src/utils/memoryManager.js CHANGED
@@ -2,11 +2,16 @@
2
  * 智能内存管理器
3
  * 采用分级策略,根据内存压力动态调整缓存和对象池
4
  * 目标:在保证性能的前提下,将内存稳定在 20MB 左右
 
5
  */
6
 
7
  import logger from './logger.js';
 
8
 
9
- // 内存压力级别
 
 
 
10
  const MemoryPressure = {
11
  LOW: 'low', // < 15MB - 正常运行
12
  MEDIUM: 'medium', // 15-25MB - 轻度清理
@@ -14,13 +19,8 @@ const MemoryPressure = {
14
  CRITICAL: 'critical' // > 35MB - 紧急清理
15
  };
16
 
17
- // 阈值配置(字节)
18
- const THRESHOLDS = {
19
- LOW: 15 * 1024 * 1024, // 15MB
20
- MEDIUM: 25 * 1024 * 1024, // 25MB
21
- HIGH: 35 * 1024 * 1024, // 35MB
22
- TARGET: 20 * 1024 * 1024 // 20MB 目标
23
- };
24
 
25
  // 对象池最大大小配置(根据压力调整)
26
  const POOL_SIZES = {
@@ -30,12 +30,19 @@ const POOL_SIZES = {
30
  [MemoryPressure.CRITICAL]: { chunk: 5, toolCall: 3, lineBuffer: 1 }
31
  };
32
 
 
 
 
33
  class MemoryManager {
34
  constructor() {
 
35
  this.currentPressure = MemoryPressure.LOW;
 
36
  this.cleanupCallbacks = new Set();
 
37
  this.lastGCTime = 0;
38
- this.gcCooldown = 10000; // GC 冷却时间 10秒
 
39
  this.checkInterval = null;
40
  this.isShuttingDown = false;
41
 
 
2
  * 智能内存管理器
3
  * 采用分级策略,根据内存压力动态调整缓存和对象池
4
  * 目标:在保证性能的前提下,将内存稳定在 20MB 左右
5
+ * @module utils/memoryManager
6
  */
7
 
8
  import logger from './logger.js';
9
+ import { MEMORY_THRESHOLDS, GC_COOLDOWN } from '../constants/index.js';
10
 
11
+ /**
12
+ * 内存压力级别枚举
13
+ * @enum {string}
14
+ */
15
  const MemoryPressure = {
16
  LOW: 'low', // < 15MB - 正常运行
17
  MEDIUM: 'medium', // 15-25MB - 轻度清理
 
19
  CRITICAL: 'critical' // > 35MB - 紧急清理
20
  };
21
 
22
+ // 使用导入的常量
23
+ const THRESHOLDS = MEMORY_THRESHOLDS;
 
 
 
 
 
24
 
25
  // 对象池最大大小配置(根据压力调整)
26
  const POOL_SIZES = {
 
30
  [MemoryPressure.CRITICAL]: { chunk: 5, toolCall: 3, lineBuffer: 1 }
31
  };
32
 
33
+ /**
34
+ * 内存管理器类
35
+ */
36
  class MemoryManager {
37
  constructor() {
38
+ /** @type {string} */
39
  this.currentPressure = MemoryPressure.LOW;
40
+ /** @type {Set<Function>} */
41
  this.cleanupCallbacks = new Set();
42
+ /** @type {number} */
43
  this.lastGCTime = 0;
44
+ /** @type {number} */
45
+ this.gcCooldown = GC_COOLDOWN;
46
  this.checkInterval = null;
47
  this.isShuttingDown = false;
48
 
src/utils/openai_generation.js ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import config from '../config/config.js';
2
+ import { REASONING_EFFORT_MAP, DEFAULT_STOP_SEQUENCES } from '../constants/index.js';
3
+
4
+ function modelMapping(modelName) {
5
+ if (modelName === 'claude-sonnet-4-5-thinking') {
6
+ return 'claude-sonnet-4-5';
7
+ } else if (modelName === 'claude-opus-4-5') {
8
+ return 'claude-opus-4-5-thinking';
9
+ } else if (modelName === 'gemini-2.5-flash-thinking') {
10
+ return 'gemini-2.5-flash';
11
+ }
12
+ return modelName;
13
+ }
14
+
15
+ function isEnableThinking(modelName) {
16
+ return modelName.includes('-thinking') ||
17
+ modelName === 'gemini-2.5-pro' ||
18
+ modelName.startsWith('gemini-3-pro-') ||
19
+ modelName === 'rev19-uic3-1p' ||
20
+ modelName === 'gpt-oss-120b-medium';
21
+ }
22
+
23
+ function generateGenerationConfig(parameters, enableThinking, actualModelName) {
24
+ const defaultThinkingBudget = config.defaults.thinking_budget ?? 1024;
25
+ let thinkingBudget = 0;
26
+ if (enableThinking) {
27
+ if (parameters.thinking_budget !== undefined) {
28
+ thinkingBudget = parameters.thinking_budget;
29
+ } else if (parameters.reasoning_effort !== undefined) {
30
+ thinkingBudget = REASONING_EFFORT_MAP[parameters.reasoning_effort] ?? defaultThinkingBudget;
31
+ } else {
32
+ thinkingBudget = defaultThinkingBudget;
33
+ }
34
+ }
35
+
36
+ const generationConfig = {
37
+ topP: parameters.top_p ?? config.defaults.top_p,
38
+ topK: parameters.top_k ?? config.defaults.top_k,
39
+ temperature: parameters.temperature ?? config.defaults.temperature,
40
+ candidateCount: 1,
41
+ maxOutputTokens: parameters.max_tokens ?? config.defaults.max_tokens,
42
+ stopSequences: DEFAULT_STOP_SEQUENCES,
43
+ thinkingConfig: {
44
+ includeThoughts: enableThinking,
45
+ thinkingBudget: thinkingBudget
46
+ }
47
+ };
48
+ if (enableThinking && actualModelName.includes('claude')) {
49
+ delete generationConfig.topP;
50
+ }
51
+ return generationConfig;
52
+ }
53
+
54
+ export {
55
+ modelMapping,
56
+ isEnableThinking,
57
+ generateGenerationConfig
58
+ };
src/utils/openai_mapping.js ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import config from '../config/config.js';
2
+ import { generateRequestId } from './idGenerator.js';
3
+ import { openaiMessageToAntigravity } from './openai_messages.js';
4
+ import { extractSystemInstruction } from './openai_system.js';
5
+ import { convertOpenAIToolsToAntigravity } from './openai_tools.js';
6
+ import { modelMapping, isEnableThinking, generateGenerationConfig } from './openai_generation.js';
7
+ import os from 'os';
8
+
9
+ function generateRequestBody(openaiMessages, modelName, parameters, openaiTools, token) {
10
+ const enableThinking = isEnableThinking(modelName);
11
+ const actualModelName = modelMapping(modelName);
12
+ const mergedSystemInstruction = extractSystemInstruction(openaiMessages);
13
+
14
+ let startIndex = 0;
15
+ if (config.useContextSystemPrompt) {
16
+ for (let i = 0; i < openaiMessages.length; i++) {
17
+ if (openaiMessages[i].role === 'system') {
18
+ startIndex = i + 1;
19
+ } else {
20
+ break;
21
+ }
22
+ }
23
+ }
24
+ const filteredMessages = openaiMessages.slice(startIndex);
25
+
26
+ const requestBody = {
27
+ project: token.projectId,
28
+ requestId: generateRequestId(),
29
+ request: {
30
+ contents: openaiMessageToAntigravity(filteredMessages, enableThinking, actualModelName, token.sessionId),
31
+ tools: convertOpenAIToolsToAntigravity(openaiTools, token.sessionId, actualModelName),
32
+ toolConfig: {
33
+ functionCallingConfig: {
34
+ mode: 'VALIDATED'
35
+ }
36
+ },
37
+ generationConfig: generateGenerationConfig(parameters, enableThinking, actualModelName),
38
+ sessionId: token.sessionId
39
+ },
40
+ model: actualModelName,
41
+ userAgent: 'antigravity'
42
+ };
43
+
44
+ if (mergedSystemInstruction) {
45
+ requestBody.request.systemInstruction = {
46
+ role: 'user',
47
+ parts: [{ text: mergedSystemInstruction }]
48
+ };
49
+ }
50
+
51
+ return requestBody;
52
+ }
53
+
54
+ function prepareImageRequest(requestBody) {
55
+ if (!requestBody || !requestBody.request) return requestBody;
56
+ requestBody.request.generationConfig = { candidateCount: 1 };
57
+ requestBody.requestType = 'image_gen';
58
+ delete requestBody.request.systemInstruction;
59
+ delete requestBody.request.tools;
60
+ delete requestBody.request.toolConfig;
61
+ return requestBody;
62
+ }
63
+
64
+ function getDefaultIp() {
65
+ const interfaces = os.networkInterfaces();
66
+ for (const iface of Object.values(interfaces)) {
67
+ for (const inter of iface) {
68
+ if (inter.family === 'IPv4' && !inter.internal) {
69
+ return inter.address;
70
+ }
71
+ }
72
+ }
73
+ return '127.0.0.1';
74
+ }
75
+
76
+ export {
77
+ generateRequestId,
78
+ generateRequestBody,
79
+ prepareImageRequest,
80
+ getDefaultIp
81
+ };
src/utils/openai_messages.js ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { getReasoningSignature, getToolSignature } from './thoughtSignatureCache.js';
2
+ import { setToolNameMapping } from './toolNameCache.js';
3
+ import { getThoughtSignatureForModel, getToolSignatureForModel } from './openai_signatures.js';
4
+
5
+ function extractImagesFromContent(content) {
6
+ const result = { text: '', images: [] };
7
+ if (typeof content === 'string') {
8
+ result.text = content;
9
+ return result;
10
+ }
11
+ if (Array.isArray(content)) {
12
+ for (const item of content) {
13
+ if (item.type === 'text') {
14
+ result.text += item.text;
15
+ } else if (item.type === 'image_url') {
16
+ const imageUrl = item.image_url?.url || '';
17
+ const match = imageUrl.match(/^data:image\/(\w+);base64,(.+)$/);
18
+ if (match) {
19
+ const format = match[1];
20
+ const base64Data = match[2];
21
+ result.images.push({
22
+ inlineData: {
23
+ mimeType: `image/${format}`,
24
+ data: base64Data
25
+ }
26
+ });
27
+ }
28
+ }
29
+ }
30
+ }
31
+ return result;
32
+ }
33
+
34
+ function handleUserMessage(extracted, antigravityMessages) {
35
+ antigravityMessages.push({
36
+ role: 'user',
37
+ parts: [
38
+ { text: extracted.text },
39
+ ...extracted.images
40
+ ]
41
+ });
42
+ }
43
+
44
+ function sanitizeToolName(name) {
45
+ if (!name || typeof name !== 'string') {
46
+ return 'tool';
47
+ }
48
+ let cleaned = name.replace(/[^a-zA-Z0-9_-]/g, '_');
49
+ cleaned = cleaned.replace(/^_+|_+$/g, '');
50
+ if (!cleaned) {
51
+ cleaned = 'tool';
52
+ }
53
+ if (cleaned.length > 128) {
54
+ cleaned = cleaned.slice(0, 128);
55
+ }
56
+ return cleaned;
57
+ }
58
+
59
+ function handleAssistantMessage(message, antigravityMessages, enableThinking, actualModelName, sessionId) {
60
+ const lastMessage = antigravityMessages[antigravityMessages.length - 1];
61
+ const hasToolCalls = message.tool_calls && message.tool_calls.length > 0;
62
+ const hasContent = message.content && message.content.trim() !== '';
63
+
64
+ const antigravityTools = hasToolCalls
65
+ ? message.tool_calls.map(toolCall => {
66
+ const originalName = toolCall.function.name;
67
+ const safeName = sanitizeToolName(originalName);
68
+
69
+ const part = {
70
+ functionCall: {
71
+ id: toolCall.id,
72
+ name: safeName,
73
+ args: {
74
+ query: toolCall.function.arguments
75
+ }
76
+ }
77
+ };
78
+
79
+ if (sessionId && actualModelName && safeName !== originalName) {
80
+ setToolNameMapping(sessionId, actualModelName, safeName, originalName);
81
+ }
82
+
83
+ if (enableThinking) {
84
+ const cachedToolSig = getToolSignature(sessionId, actualModelName);
85
+ part.thoughtSignature = toolCall.thoughtSignature || cachedToolSig || getToolSignatureForModel(actualModelName);
86
+ }
87
+
88
+ return part;
89
+ })
90
+ : [];
91
+
92
+ if (lastMessage?.role === 'model' && hasToolCalls && !hasContent) {
93
+ lastMessage.parts.push(...antigravityTools);
94
+ } else {
95
+ const parts = [];
96
+
97
+ if (enableThinking) {
98
+ const cachedSig = getReasoningSignature(sessionId, actualModelName);
99
+ const thoughtSignature = message.thoughtSignature || cachedSig || getThoughtSignatureForModel(actualModelName);
100
+ let reasoningText = '';
101
+ if (typeof message.reasoning_content === 'string' && message.reasoning_content.length > 0) {
102
+ reasoningText = message.reasoning_content;
103
+ } else {
104
+ reasoningText = ' ';
105
+ }
106
+ parts.push({ text: reasoningText, thought: true });
107
+ parts.push({ text: ' ', thoughtSignature });
108
+ }
109
+
110
+ if (hasContent) parts.push({ text: message.content.trimEnd() });
111
+ parts.push(...antigravityTools);
112
+
113
+ antigravityMessages.push({
114
+ role: 'model',
115
+ parts
116
+ });
117
+ }
118
+ }
119
+
120
+ function handleToolCall(message, antigravityMessages) {
121
+ let functionName = '';
122
+ for (let i = antigravityMessages.length - 1; i >= 0; i--) {
123
+ if (antigravityMessages[i].role === 'model') {
124
+ const parts = antigravityMessages[i].parts;
125
+ for (const part of parts) {
126
+ if (part.functionCall && part.functionCall.id === message.tool_call_id) {
127
+ functionName = part.functionCall.name;
128
+ break;
129
+ }
130
+ }
131
+ if (functionName) break;
132
+ }
133
+ }
134
+
135
+ const lastMessage = antigravityMessages[antigravityMessages.length - 1];
136
+ const functionResponse = {
137
+ functionResponse: {
138
+ id: message.tool_call_id,
139
+ name: functionName,
140
+ response: {
141
+ output: message.content
142
+ }
143
+ }
144
+ };
145
+
146
+ if (lastMessage?.role === 'user' && lastMessage.parts.some(p => p.functionResponse)) {
147
+ lastMessage.parts.push(functionResponse);
148
+ } else {
149
+ antigravityMessages.push({
150
+ role: 'user',
151
+ parts: [functionResponse]
152
+ });
153
+ }
154
+ }
155
+
156
+ function openaiMessageToAntigravity(openaiMessages, enableThinking, actualModelName, sessionId) {
157
+ const antigravityMessages = [];
158
+ for (const message of openaiMessages) {
159
+ if (message.role === 'user' || message.role === 'system') {
160
+ const extracted = extractImagesFromContent(message.content);
161
+ handleUserMessage(extracted, antigravityMessages);
162
+ } else if (message.role === 'assistant') {
163
+ handleAssistantMessage(message, antigravityMessages, enableThinking, actualModelName, sessionId);
164
+ } else if (message.role === 'tool') {
165
+ handleToolCall(message, antigravityMessages);
166
+ }
167
+ }
168
+ return antigravityMessages;
169
+ }
170
+
171
+ export {
172
+ extractImagesFromContent,
173
+ handleUserMessage,
174
+ sanitizeToolName,
175
+ handleAssistantMessage,
176
+ handleToolCall,
177
+ openaiMessageToAntigravity
178
+ };
src/utils/openai_signatures.js ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const CLAUDE_THOUGHT_SIGNATURE = 'RXFRRENrZ0lDaEFDR0FJcVFKV1Bvcy9GV20wSmtMV2FmWkFEbGF1ZTZzQTdRcFlTc1NvbklmemtSNFo4c1dqeitIRHBOYW9hS2NYTE1TeTF3bjh2T1RHdE1KVjVuYUNQclZ5cm9DMFNETHk4M0hOSWsrTG1aRUhNZ3hvTTl0ZEpXUDl6UUMzOExxc2ZJakI0UkkxWE1mdWJ1VDQrZnY0Znp0VEoyTlhtMjZKL2daYi9HL1gwcmR4b2x0VE54empLemtLcEp0ZXRia2plb3NBcWlRSWlXUHloMGhVVTk1dHNha1dyNDVWNUo3MTJjZDNxdHQ5Z0dkbjdFaFk4dUllUC9CcThVY2VZZC9YbFpYbDc2bHpEbmdzL2lDZXlNY3NuZXdQMjZBTDRaQzJReXdibVQzbXlSZmpld3ZSaUxxOWR1TVNidHIxYXRtYTJ0U1JIRjI0Z0JwUnpadE1RTmoyMjR4bTZVNUdRNXlOSWVzUXNFNmJzRGNSV0RTMGFVOEZERExybmhVQWZQT2JYMG5lTGR1QnU1VGZOWW9NZglRbTgyUHVqVE1xaTlmN0t2QmJEUUdCeXdyVXR2eUNnTEFHNHNqeWluZDRCOEg3N2ZJamt5blI3Q3ZpQzlIOTVxSENVTCt3K3JzMmsvV0sxNlVsbGlTK0pET3UxWXpPMWRPOUp3V3hEMHd5ZVU0a0Y5MjIxaUE5Z2lUd2djZXhSU2c4TWJVMm1NSjJlaGdlY3g0YjJ3QloxR0FFPQ==';
2
+ const GEMINI_THOUGHT_SIGNATURE = 'EqAHCp0HAXLI2nygRbdzD4Vgzxxi7tbM87zIRkNgPLqTj+Jxv9mY8Q0G87DzbTtvsIFhWB0RZMoEK6ntm5GmUe6ADtxHk4zgHUs/FKqTu8tzUdPRDrKn3KCAtFW4LJqijZoFxNKMyQRmlgPUX4tGYE7pllD77UK6SjCwKhKZoSVZLMiPXP9YFktbida1Q5upXMrzG1t8abPmpFo983T/rgWlNqJp+Fb+bsoH0zuSpmU4cPKO3LIGsxBhvRhM/xydahZD+VpEX7TEJAN58z1RomFyx9u0IR7ukwZr2UyoNA+uj8OChUDFupQsVwbm3XE1UAt22BGvfYIyyZ42fxgOgsFFY+AZ72AOufcmZb/8vIw3uEUgxHczdl+NGLuS4Hsy/AAntdcH9sojSMF3qTf+ZK1FMav23SPxUBtU5T9HCEkKqQWRnMsVGYV1pupFisWo85hRLDTUipxVy9ug1hN8JBYBNmGLf8KtWLhVp7Z11PIAZj3C6HzoVyiVeuiorwNrn0ZaaXNe+y5LHuDF0DNZhrIfnXByq6grLLSAv4fTLeCJvfGzTWWyZDMbVXNx1HgumKq8calP9wv33t0hfEaOlcmfGIyh1J/N+rOGR0WXcuZZP5/VsFR44S2ncpwTPT+MmR0PsjocDenRY5m/X4EXbGGkZ+cfPnWoA64bn3eLeJTwxl9W1ZbmYS6kjpRGUMxExgRNOzWoGISddHCLcQvN7o50K8SF5k97rxiS5q4rqDmqgRPXzQTQnZyoL3dCxScX9cvLSjNCZDcotonDBAWHfkXZ0/EmFiONQcLJdANtAjwoA44Mbn50gubrTsNd7d0Rm/hbNEh/ZceUalV5MMcl6tJtahCJoybQMsnjWuBXl7cXiKmqAvxTDxIaBgQBYAo4FrbV4zQv35zlol+O3YiyjJn/U0oBeO5pEcH1d0vnLgYP71jZVY2FjWRKnDR9aw4JhiuqAa+i0tupkBy+H4/SVwHADFQq6wcsL8qvXlwktJL9MIAoaXDkIssw6gKE9EuGd7bSO9f+sA8CZ0I8LfJ3jcHUsE/3qd4pFrn5RaET56+1p8ZHZDDUQ0p1okApUCCYsC2WuL6O9P4fcg3yitAA/AfUUNjHKANE+ANneQ0efMG7fx9bvI+iLbXgPupApoov24JRkmhHsrJiu9bp+G/pImd2PNv7ArunJ6upl0VAUWtRyLWyGfdl6etGuY8vVJ7JdWEQ8aWzRK3g6e+8YmDtP5DAfw==';
3
+ const CLAUDE_TOOL_SIGNATURE = 'RXVNQkNrZ0lDaEFDR0FJcVFLZGsvMnlyR0VTbmNKMXEyTFIrcWwyY2ozeHhoZHRPb0VOYWJ2VjZMSnE2MlBhcEQrUWdIM3ZWeHBBUG9rbGN1aXhEbXprZTcvcGlkbWRDQWs5MWcrTVNERnRhbWJFOU1vZWZGc1pWSGhvTUxsMXVLUzRoT3BIaWwyeXBJakNYa05EVElMWS9talprdUxvRjFtMmw5dnkrbENhSDNNM3BYNTM0K1lRZ0NaWTQvSUNmOXo4SkhZVzU2Sm1WcTZBcVNRUURBRGVMV1BQRXk1Q0JsS0dCZXlNdHp2NGRJQVlGbDFSMDBXNGhqNHNiSWNKeGY0UGZVQTBIeE1mZjJEYU5BRXdrWUJ4MmNzRFMrZGM1N1hnUlVNblpkZ0hTVHVNaGdod1lBUT09';
4
+ const GEMINI_TOOL_SIGNATURE = 'EqoNCqcNAXLI2nwkidsFconk7xHt7x0zIOX7n/JR7DTKiPa/03uqJ9OmZaujaw0xNQxZ0wNCx8NguJ+sAfaIpek62+aBnciUTQd5UEmwM/V5o6EA2wPvv4IpkXyl6Eyvr8G+jD/U4c2Tu4M4WzVhcImt9Lf/ZH6zydhxgU9ZgBtMwck292wuThVNqCZh9akqy12+BPHs9zW8IrPGv3h3u64Q2Ye9Mzx+EtpV2Tiz8mcq4whdUu72N6LQVQ+xLLdzZ+CQ7WgEjkqOWQs2C09DlAsdu5vjLeF5ZgpL9seZIag9Dmhuk589l/I20jGgg7EnCgojzarBPHNOCHrxTbcp325tTLPa6Y7U4PgofJEkv0MX4O22mu/On6TxAlqYkVa6twdEHYb+zMFWQl7SVFwQTY9ub7zeSaW+p/yJ+5H43LzC95aEcrfTaX0P2cDWGrQ1IVtoaEWPi7JVOtDSqchVC1YLRbIUHaWGyAysx7BRoSBIr46aVbGNy2Xvt35Vqt0tDJRyBdRuKXTmf1px6mbDpsjldxE/YLzCkCtAp1Ji1X9XPFhZbj7HTNIjCRfIeHA/6IyOB0WgBiCw5e2p50frlixd+iWD3raPeS/VvCBvn/DPCsnH8lzgpDQqaYeN/y0K5UWeMwFUg+00YFoN9D34q6q3PV9yuj1OGT2l/DzCw8eR5D460S6nQtYOaEsostvCgJGipamf/dnUzHomoiqZegJzfW7uzIQl1HJXQJTnpTmk07LarQwxIPtId9JP+dXKLZMw5OAYWITfSXF5snb7F1jdN0NydJOVkeanMsxnbIyU7/iKLDWJAmcRru/GavbJGgB0vJgY52SkPi9+uhfF8u60gLqFpbhsal3oxSPJSzeg+TN/qktBGST2YvLHxilPKmLBhggTUZhDSzSjxPfseE41FHYniyn6O+b3tujCdvexnrIjmmX+KTQC3ovjfk/ArwImI/cGihFYOc+wDnri5iHofdLbFymE/xb1Q4Sn06gVq1sgmeeS/li0F6C0v9GqOQ4olqQrTT2PPDVMbDrXgjZMfHk9ciqQ5OB6r19uyIqb6lFplKsE/ZSacAGtw1K0HENMq9q576m0beUTtNRJMktXem/OJIDbpRE0cXfBt1J9VxYHBe6aEiIZmRzJnXtJmUCjqfLPg9n0FKUIjnnln7as+aiRpItb5ZfJjrMEu154ePgUa1JYv2MA8oj5rvzpxRSxycD2p8HTxshitnLFI8Q6Kl2gUqBI27uzYSPyBtrvWZaVtrXYMiyjOFBdjUFunBIW2UvoPSKYEaNrUO3tTSYO4GjgLsfCRQ2CMfclq/TbCALjvzjMaYLrn6OKQnSDI/Tt1J6V6pDXfSyLdCIDg77NTvdqTH2Cv3yT3fE3nOOW5mUPZtXAIxPkFGo9eL+YksEgLIeZor0pdb+BHs1kQ4z7EplCYVhpTbo6fMcarW35Qew9HPMTFQ03rQaDhlNnUUI3tacnDMQvKsfo4OPTQYG2zP4lHXSsf4IpGRJyTBuMGK6siiKBiL/u73HwKTDEu2RU/4ZmM6dQJkoh+6sXCCmoZuweYOeF2cAx2AJAHD72qmEPzLihm6bWeSRXDxJGm2RO85NgK5khNfV2Mm1etmQdDdbTLJV5FTvJQJ5zVDnYQkk7SKDio9rQMBucw5M6MyvFFDFdzJQlVKZm/GZ5T21GsmNHMJNd9G2qYAKwUV3Mb64Ipk681x8TFG+1AwkfzSWCHnbXMG2bOX+JUt/4rldyRypArvxhyNimEDc7HoqSHwTVfpd6XA0u8emcQR1t+xAR2BiT/elQHecAvhRtJt+ts44elcDIzTCBiJG4DEoV8X0pHb1oTLJFcD8aF29BWczl4kYDPtR9Dtlyuvmaljt0OEeLz9zS0MGvpflvMtUmFdGq7ZP+GztIdWup4kZZ59pzTuSR9itskMAnqYj+V9YBCSUUmsxW6Zj4Uvzw0nLYsjIgTjP3SU9WvwUhvJWzu5wZkdu3e03YoGxUjLWDXMKeSZ/g2Th5iNn3xlJwp5Z2p0jsU1rH4K/iMsYiLBJkGnsYuBqqFt2UIPYziqxOKV41oSKdEU+n4mD3WarU/kR4krTkmmEj2aebWgvHpsZSW0ULaeK3QxNBdx7waBUUkZ7nnDIRDi31T/sBYl+UADEFvm2INIsFuXPUyXbAthNWn5vIQNlKNLCwpGYqhuzO4hno8vyqbxKsrMtayk1U+0TQsBbQY1VuFF2bDBNFcPQOv/7KPJDL8hal0U6J0E6DVZVcH4Gel7pgsBeC+48=';
5
+
6
+ const DEFAULT_THOUGHT_SIGNATURE = CLAUDE_THOUGHT_SIGNATURE;
7
+ const DEFAULT_TOOL_SIGNATURE = CLAUDE_TOOL_SIGNATURE;
8
+
9
+ function getThoughtSignatureForModel(actualModelName) {
10
+ if (!actualModelName) return DEFAULT_THOUGHT_SIGNATURE;
11
+ const lower = actualModelName.toLowerCase();
12
+ if (lower.includes('claude')) return CLAUDE_THOUGHT_SIGNATURE;
13
+ if (lower.includes('gemini')) return GEMINI_THOUGHT_SIGNATURE;
14
+ return DEFAULT_THOUGHT_SIGNATURE;
15
+ }
16
+
17
+ function getToolSignatureForModel(actualModelName) {
18
+ if (!actualModelName) return DEFAULT_TOOL_SIGNATURE;
19
+ const lower = actualModelName.toLowerCase();
20
+ if (lower.includes('claude')) return CLAUDE_TOOL_SIGNATURE;
21
+ if (lower.includes('gemini')) return GEMINI_TOOL_SIGNATURE;
22
+ return DEFAULT_TOOL_SIGNATURE;
23
+ }
24
+
25
+ export {
26
+ getThoughtSignatureForModel,
27
+ getToolSignatureForModel
28
+ };
src/utils/openai_system.js ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import config from '../config/config.js';
2
+
3
+ function extractSystemInstruction(openaiMessages) {
4
+ const baseSystem = config.systemInstruction || '';
5
+ if (!config.useContextSystemPrompt) {
6
+ return baseSystem;
7
+ }
8
+ const systemTexts = [];
9
+ for (const message of openaiMessages) {
10
+ if (message.role === 'system') {
11
+ const content = typeof message.content === 'string'
12
+ ? message.content
13
+ : (Array.isArray(message.content)
14
+ ? message.content.filter(item => item.type === 'text').map(item => item.text).join('')
15
+ : '');
16
+ if (content.trim()) {
17
+ systemTexts.push(content.trim());
18
+ }
19
+ } else {
20
+ break;
21
+ }
22
+ }
23
+ const parts = [];
24
+ if (baseSystem.trim()) {
25
+ parts.push(baseSystem.trim());
26
+ }
27
+ if (systemTexts.length > 0) {
28
+ parts.push(systemTexts.join('\n\n'));
29
+ }
30
+ return parts.join('\n\n');
31
+ }
32
+
33
+ export { extractSystemInstruction };
src/utils/openai_tools.js ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { setToolNameMapping } from './toolNameCache.js';
2
+
3
+ const EXCLUDED_KEYS = new Set([
4
+ '$schema',
5
+ 'additionalProperties',
6
+ 'minLength',
7
+ 'maxLength',
8
+ 'minItems',
9
+ 'maxItems',
10
+ 'uniqueItems',
11
+ 'exclusiveMaximum',
12
+ 'exclusiveMinimum',
13
+ 'const',
14
+ 'anyOf',
15
+ 'oneOf',
16
+ 'allOf',
17
+ 'any_of',
18
+ 'one_of',
19
+ 'all_of'
20
+ ]);
21
+
22
+ function cleanParameters(obj) {
23
+ if (!obj || typeof obj !== 'object') return obj;
24
+ const cleaned = Array.isArray(obj) ? [] : {};
25
+ for (const [key, value] of Object.entries(obj)) {
26
+ if (EXCLUDED_KEYS.has(key)) continue;
27
+ const cleanedValue = (value && typeof value === 'object') ? cleanParameters(value) : value;
28
+ cleaned[key] = cleanedValue;
29
+ }
30
+ return cleaned;
31
+ }
32
+
33
+ function sanitizeToolName(name) {
34
+ if (!name || typeof name !== 'string') {
35
+ return 'tool';
36
+ }
37
+ let cleaned = name.replace(/[^a-zA-Z0-9_-]/g, '_');
38
+ cleaned = cleaned.replace(/^_+|_+$/g, '');
39
+ if (!cleaned) {
40
+ cleaned = 'tool';
41
+ }
42
+ if (cleaned.length > 128) {
43
+ cleaned = cleaned.slice(0, 128);
44
+ }
45
+ return cleaned;
46
+ }
47
+
48
+ function convertOpenAIToolsToAntigravity(openaiTools, sessionId, actualModelName) {
49
+ if (!openaiTools || openaiTools.length === 0) return [];
50
+ return openaiTools.map((tool) => {
51
+ const rawParams = tool.function?.parameters || {};
52
+ const cleanedParams = cleanParameters(rawParams) || {};
53
+ if (cleanedParams.type === undefined) {
54
+ cleanedParams.type = 'object';
55
+ }
56
+ if (cleanedParams.type === 'object' && cleanedParams.properties === undefined) {
57
+ cleanedParams.properties = {};
58
+ }
59
+
60
+ const originalName = tool.function?.name;
61
+ const safeName = sanitizeToolName(originalName);
62
+
63
+ if (sessionId && actualModelName && safeName !== originalName) {
64
+ setToolNameMapping(sessionId, actualModelName, safeName, originalName);
65
+ }
66
+
67
+ return {
68
+ functionDeclarations: [
69
+ {
70
+ name: safeName,
71
+ description: tool.function.description,
72
+ parameters: cleanedParams
73
+ }
74
+ ]
75
+ };
76
+ });
77
+ }
78
+
79
+ export {
80
+ cleanParameters,
81
+ sanitizeToolName,
82
+ convertOpenAIToolsToAntigravity
83
+ };
src/utils/paths.js ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * 路径工具模块
3
+ * 统一处理 pkg 打包环境和开发环境下的路径获取
4
+ * @module utils/paths
5
+ */
6
+
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import { fileURLToPath } from 'url';
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+
14
+ /**
15
+ * 检测是否在 pkg 打包环境中运行
16
+ * @type {boolean}
17
+ */
18
+ export const isPkg = typeof process.pkg !== 'undefined';
19
+
20
+ /**
21
+ * 获取项目根目录
22
+ * @returns {string} 项目根目录路径
23
+ */
24
+ export function getProjectRoot() {
25
+ if (isPkg) {
26
+ return path.dirname(process.execPath);
27
+ }
28
+ return path.join(__dirname, '../..');
29
+ }
30
+
31
+ /**
32
+ * 获取数据目录路径
33
+ * pkg 环境下使用可执行文件所在目录或当前工作目录
34
+ * @returns {string} 数据目录路径
35
+ */
36
+ export function getDataDir() {
37
+ if (isPkg) {
38
+ // pkg 环境:优先使用可执行文件旁边的 data 目录
39
+ const exeDir = path.dirname(process.execPath);
40
+ const exeDataDir = path.join(exeDir, 'data');
41
+ // 检查是否可以在该目录创建文件
42
+ try {
43
+ if (!fs.existsSync(exeDataDir)) {
44
+ fs.mkdirSync(exeDataDir, { recursive: true });
45
+ }
46
+ return exeDataDir;
47
+ } catch (e) {
48
+ // 如果无法创建,尝试当前工作目录
49
+ const cwdDataDir = path.join(process.cwd(), 'data');
50
+ try {
51
+ if (!fs.existsSync(cwdDataDir)) {
52
+ fs.mkdirSync(cwdDataDir, { recursive: true });
53
+ }
54
+ return cwdDataDir;
55
+ } catch (e2) {
56
+ // 最后使用用户主目录
57
+ const homeDataDir = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.antigravity', 'data');
58
+ if (!fs.existsSync(homeDataDir)) {
59
+ fs.mkdirSync(homeDataDir, { recursive: true });
60
+ }
61
+ return homeDataDir;
62
+ }
63
+ }
64
+ }
65
+ // 开发环境
66
+ return path.join(__dirname, '..', '..', 'data');
67
+ }
68
+
69
+ /**
70
+ * 获取公共静态文件目录
71
+ * @returns {string} public 目录路径
72
+ */
73
+ export function getPublicDir() {
74
+ if (isPkg) {
75
+ // pkg 环境:优先使用可执行文件旁边的 public 目录
76
+ const exeDir = path.dirname(process.execPath);
77
+ const exePublicDir = path.join(exeDir, 'public');
78
+ if (fs.existsSync(exePublicDir)) {
79
+ return exePublicDir;
80
+ }
81
+ // 其次使用当前工作目录的 public 目录
82
+ const cwdPublicDir = path.join(process.cwd(), 'public');
83
+ if (fs.existsSync(cwdPublicDir)) {
84
+ return cwdPublicDir;
85
+ }
86
+ // 最后使用打包内的 public 目录(通过 snapshot)
87
+ return path.join(__dirname, '../../public');
88
+ }
89
+ // 开发环境
90
+ return path.join(__dirname, '../../public');
91
+ }
92
+
93
+ /**
94
+ * 获取图片存储目录
95
+ * @returns {string} 图片目录路径
96
+ */
97
+ export function getImageDir() {
98
+ if (isPkg) {
99
+ // pkg 环境:优先使用可执行文件旁边的 public/images 目录
100
+ const exeDir = path.dirname(process.execPath);
101
+ const exeImageDir = path.join(exeDir, 'public', 'images');
102
+ try {
103
+ if (!fs.existsSync(exeImageDir)) {
104
+ fs.mkdirSync(exeImageDir, { recursive: true });
105
+ }
106
+ return exeImageDir;
107
+ } catch (e) {
108
+ // 如果无法创建,尝试当前工作目录
109
+ const cwdImageDir = path.join(process.cwd(), 'public', 'images');
110
+ try {
111
+ if (!fs.existsSync(cwdImageDir)) {
112
+ fs.mkdirSync(cwdImageDir, { recursive: true });
113
+ }
114
+ return cwdImageDir;
115
+ } catch (e2) {
116
+ // 最后使用用户主目录
117
+ const homeImageDir = path.join(process.env.HOME || process.env.USERPROFILE || '.', '.antigravity', 'images');
118
+ if (!fs.existsSync(homeImageDir)) {
119
+ fs.mkdirSync(homeImageDir, { recursive: true });
120
+ }
121
+ return homeImageDir;
122
+ }
123
+ }
124
+ }
125
+ // 开发环境
126
+ return path.join(__dirname, '../../public/images');
127
+ }
128
+
129
+ /**
130
+ * 获取 .env 文件路径
131
+ * @returns {string} .env 文件路径
132
+ */
133
+ export function getEnvPath() {
134
+ if (isPkg) {
135
+ // pkg 环境:优先使用可执行文件旁边的 .env
136
+ const exeDir = path.dirname(process.execPath);
137
+ const exeEnvPath = path.join(exeDir, '.env');
138
+ if (fs.existsSync(exeEnvPath)) {
139
+ return exeEnvPath;
140
+ }
141
+ // 其次使用当前工作目录的 .env
142
+ const cwdEnvPath = path.join(process.cwd(), '.env');
143
+ if (fs.existsSync(cwdEnvPath)) {
144
+ return cwdEnvPath;
145
+ }
146
+ // 返回可执行文件目录的路径(即使不存在)
147
+ return exeEnvPath;
148
+ }
149
+ // 开发环境
150
+ return path.join(__dirname, '../../.env');
151
+ }
152
+
153
+ /**
154
+ * 获取配置文件路径集合
155
+ * @returns {{envPath: string, configJsonPath: string, examplePath: string}} 配置文件路径
156
+ */
157
+ export function getConfigPaths() {
158
+ if (isPkg) {
159
+ // pkg 环境:优先使用可执行文件旁边的配置文件
160
+ const exeDir = path.dirname(process.execPath);
161
+ const cwdDir = process.cwd();
162
+
163
+ // 查找 .env 文件
164
+ let envPath = path.join(exeDir, '.env');
165
+ if (!fs.existsSync(envPath)) {
166
+ const cwdEnvPath = path.join(cwdDir, '.env');
167
+ if (fs.existsSync(cwdEnvPath)) {
168
+ envPath = cwdEnvPath;
169
+ }
170
+ }
171
+
172
+ // 查找 config.json 文件
173
+ let configJsonPath = path.join(exeDir, 'config.json');
174
+ if (!fs.existsSync(configJsonPath)) {
175
+ const cwdConfigPath = path.join(cwdDir, 'config.json');
176
+ if (fs.existsSync(cwdConfigPath)) {
177
+ configJsonPath = cwdConfigPath;
178
+ }
179
+ }
180
+
181
+ // 查找 .env.example 文件
182
+ let examplePath = path.join(exeDir, '.env.example');
183
+ if (!fs.existsSync(examplePath)) {
184
+ const cwdExamplePath = path.join(cwdDir, '.env.example');
185
+ if (fs.existsSync(cwdExamplePath)) {
186
+ examplePath = cwdExamplePath;
187
+ }
188
+ }
189
+
190
+ return { envPath, configJsonPath, examplePath };
191
+ }
192
+
193
+ // 开发环境
194
+ return {
195
+ envPath: path.join(__dirname, '../../.env'),
196
+ configJsonPath: path.join(__dirname, '../../config.json'),
197
+ examplePath: path.join(__dirname, '../../.env.example')
198
+ };
199
+ }
200
+
201
+ /**
202
+ * 计算相对路径用于日志显示
203
+ * @param {string} absolutePath - 绝对路径
204
+ * @returns {string} 相对路径或原路径
205
+ */
206
+ export function getRelativePath(absolutePath) {
207
+ if (isPkg) {
208
+ const exeDir = path.dirname(process.execPath);
209
+ if (absolutePath.startsWith(exeDir)) {
210
+ return '.' + absolutePath.slice(exeDir.length).replace(/\\/g, '/');
211
+ }
212
+ const cwdDir = process.cwd();
213
+ if (absolutePath.startsWith(cwdDir)) {
214
+ return '.' + absolutePath.slice(cwdDir.length).replace(/\\/g, '/');
215
+ }
216
+ }
217
+ return absolutePath;
218
+ }
src/utils/utils.js CHANGED
@@ -1,495 +1 @@
1
- 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 { getReasoningSignature, getToolSignature } from './thoughtSignatureCache.js';
6
- import { setToolNameMapping } from './toolNameCache.js';
7
-
8
- // 思维链签名常量
9
- // Claude 模型签名
10
- const CLAUDE_THOUGHT_SIGNATURE = 'RXFRRENrZ0lDaEFDR0FJcVFKV1Bvcy9GV20wSmtMV2FmWkFEbGF1ZTZzQTdRcFlTc1NvbklmemtSNFo4c1dqeitIRHBOYW9hS2NYTE1TeTF3bjh2T1RHdE1KVjVuYUNQclZ5cm9DMFNETHk4M0hOSWsrTG1aRUhNZ3hvTTl0ZEpXUDl6UUMzOExxc2ZJakI0UkkxWE1mdWJ1VDQrZnY0Znp0VEoyTlhtMjZKL2daYi9HL1gwcmR4b2x0VE54empLemtLcEp0ZXRia2plb3NBcWlRSWlXUHloMGhVVTk1dHNha1dyNDVWNUo3MTJjZDNxdHQ5Z0dkbjdFaFk4dUllUC9CcThVY2VZZC9YbFpYbDc2bHpEbmdzL2lDZXlNY3NuZXdQMjZBTDRaQzJReXdibVQzbXlSZmpld3ZSaUxxOWR1TVNidHIxYXRtYTJ0U1JIRjI0Z0JwUnpadE1RTmoyMjR4bTZVNUdRNXlOSWVzUXNFNmJzRGNSV0RTMGFVOEZERExybmhVQWZQT2JYMG5lTGR1QnU1VGZOWW9NZGlRbTgyUHVqVE1xaTlmN0t2QmJEUUdCeXdyVXR2eUNnTEFHNHNqeWluZDRCOEg3N2ZJamt5blI3Q3ZpQzlIOTVxSENVTCt3K3JzMmsvV0sxNlVsbGlTK0pET3UxWXpPMWRPOUp3V3hEMHd5ZVU0a0Y5MjIxaUE5Z2lUd2djZXhSU2c4TWJVMm1NSjJlaGdlY3g0YjJ3QloxR0FFPQ==';
11
- // Gemini 思维链签名
12
- const GEMINI_THOUGHT_SIGNATURE = 'EqAHCp0HAXLI2nygRbdzD4Vgzxxi7tbM87zIRkNgPLqTj+Jxv9mY8Q0G87DzbTtvsIFhWB0RZMoEK6ntm5GmUe6ADtxHk4zgHUs/FKqTu8tzUdPRDrKn3KCAtFW4LJqijZoFxNKMyQRmlgPUX4tGYE7pllD77UK6SjCwKhKZoSVZLMiPXP9YFktbida1Q5upXMrzG1t8abPmpFo983T/rgWlNqJp+Fb+bsoH0zuSpmU4cPKO3LIGsxBhvRhM/xydahZD+VpEX7TEJAN58z1RomFyx9u0IR7ukwZr2UyoNA+uj8OChUDFupQsVwbm3XE1UAt22BGvfYIyyZ42fxgOgsFFY+AZ72AOufcmZb/8vIw3uEUgxHczdl+NGLuS4Hsy/AAntdcH9sojSMF3qTf+ZK1FMav23SPxUBtU5T9HCEkKqQWRnMsVGYV1pupFisWo85hRLDTUipxVy9ug1hN8JBYBNmGLf8KtWLhVp7Z11PIAZj3C6HzoVyiVeuiorwNrn0ZaaXNe+y5LHuDF0DNZhrIfnXByq6grLLSAv4fTLeCJvfGzTWWyZDMbVXNx1HgumKq8calP9wv33t0hfEaOlcmfGIyh1J/N+rOGR0WXcuZZP5/VsFR44S2ncpwTPT+MmR0PsjocDenRY5m/X4EXbGGkZ+cfPnWoA64bn3eLeJTwxl9W1ZbmYS6kjpRGUMxExgRNOzWoGISddHCLcQvN7o50K8SF5k97rxiS5q4rqDmqgRPXzQTQnZyoL3dCxScX9cvLSjNCZDcotonDBAWHfkXZ0/EmFiONQcLJdANtAjwoA44Mbn50gubrTsNd7d0Rm/hbNEh/ZceUalV5MMcl6tJtahCJoybQMsnjWuBXl7cXiKmqAvxTDxIaBgQBYAo4FrbV4zQv35zlol+O3YiyjJn/U0oBeO5pEcH1d0vnLgYP71jZVY2FjWRKnDR9aw4JhiuqAa+i0tupkBy+H4/SVwHADFQq6wcsL8qvXlwktJL9MIAoaXDkIssw6gKE9EuGd7bSO9f+sA8CZ0I8LfJ3jcHUsE/3qd4pFrn5RaET56+1p8ZHZDDUQ0p1okApUCCYsC2WuL6O9P4fcg3yitAA/AfUUNjHKANE+ANneQ0efMG7fx9bvI+iLbXgPupApoov24JRkmhHsrJiu9bp+G/pImd2PNv7ArunJ6upl0VAUWtRyLWyGfdl6etGuY8vVJ7JdWEQ8aWzRK3g6e+8YmDtP5DAfw==';
13
- // 工具调用思维链签名
14
- const TOOL_THOUGHT_SIGNATURE = 'EqoNCqcNAXLI2nwkidsFconk7xHt7x0zIOX7n/JR7DTKiPa/03uqJ9OmZaujaw0xNQxZ0wNCx8NguJ+sAfaIpek62+aBnciUTQd5UEmwM/V5o6EA2wPvv4IpkXyl6Eyvr8G+jD/U4c2Tu4M4WzVhcImt9Lf/ZH6zydhxgU9ZgBtMwck292wuThVNqCZh9akqy12+BPHs9zW8IrPGv3h3u64Q2Ye9Mzx+EtpV2Tiz8mcq4whdUu72N6LQVQ+xLLdzZ+CQ7WgEjkqOWQs2C09DlAsdu5vjLeF5ZgpL9seZIag9Dmhuk589l/I20jGgg7EnCgojzarBPHNOCHrxTbcp325tTLPa6Y7U4PgofJEkv0MX4O22mu/On6TxAlqYkVa6twdEHYb+zMFWQl7SVFwQTY9ub7zeSaW+p/yJ+5H43LzC95aEcrfTaX0P2cDWGrQ1IVtoaEWPi7JVOtDSqchVC1YLRbIUHaWGyAysx7BRoSBIr46aVbGNy2Xvt35Vqt0tDJRyBdRuKXTmf1px6mbDpsjldxE/YLzCkCtAp1Ji1X9XPFhZbj7HTNIjCRfIeHA/6IyOB0WgBiCw5e2p50frlixd+iWD3raPeS/VvCBvn/DPCsnH8lzgpDQqaYeN/y0K5UWeMwFUg+00YFoN9D34q6q3PV9yuj1OGT2l/DzCw8eR5D460S6nQtYOaEsostvCgJGipamf/dnUzHomoiqZegJzfW7uzIQl1HJXQJTnpTmk07LarQwxIPtId9JP+dXKLZMw5OAYWITfSXF5snb7F1jdN0NydJOVkeanMsxnbIyU7/iKLDWJAmcRru/GavbJGgB0vJgY52SkPi9+uhfF8u60gLqFpbhsal3oxSPJSzeg+TN/qktBGST2YvLHxilPKmLBhggTUZhDSzSjxPfseE41FHYniyn6O+b3tujCdvexnrIjmmX+KTQC3ovjfk/ArwImI/cGihFYOc+wDnri5iHofdLbFymE/xb1Q4Sn06gVq1sgmeeS/li0F6C0v9GqOQ4olqQrTT2PPDVMbDrXgjZMfHk9ciqQ5OB6r19uyIqb6lFplKsE/ZSacAGtw1K0HENMq9q576m0beUTtNRJMktXem/OJIDbpRE0cXfBt1J9VxYHBe6aEiIZmRzJnXtJmUCjqfLPg9n0FKUIjnnln7as+aiRpItb5ZfJjrMEu154ePgUa1JYv2MA8oj5rvzpxRSxycD2p8HTxshitnLFI8Q6Kl2gUqBI27uzYSPyBtrvWZaVtrXYMiyjOFBdjUFunBIW2UvoPSKYEaNrUO3tTSYO4GjgLsfCRQ2CMfclq/TbCALjvzjMaYLrn6OKQnSDI/Tt1J6V6pDXfSyLdCIDg77NTvdqTH2Cv3yT3fE3nOOW5mUPZtXAIxPkFGo9eL+YksEgLIeZor0pdb+BHs1kQ4z7EplCYVhpTbo6fMcarW35Qew9HPMTFQ03rQaDhlNnUUI3tacnDMQvKsfo4OPTQYG2zP4lHXSsf4IpGRJyTBuMGK6siiKBiL/u73HwKTDEu2RU/4ZmM6dQJkoh+6sXCCmoZuweYOeF2cAx2AJAHD72qmEPzLihm6bWeSRXDxJGm2RO85NgK5khNfV2Mm1etmQdDdbTLJV5FTvJQJ5zVDnYQkk7SKDio9rQMBucw5M6MyvFFDFdzJQlVKZm/GZ5T21GsmNHMJNd9G2qYAKwUV3Mb64Ipk681x8TFG+1AwkfzSWCHnbXMG2bOX+JUt/4rldyRypArvxhyNimEDc7HoqSHwTVfpd6XA0u8emcQR1t+xAR2BiT/elQHecAvhRtJt+ts44elcDIzTCBiJG4DEoV8X0pHb1oTLJFcD8aF29BWczl4kYDPtR9Dtlyuvmaljt0OEeLz9zS0MGvpflvMtUmFdGq7ZP+GztIdWup4kZZ59pzTuSR9itskMAnqYj+V9YBCSUUmsxW6Zj4Uvzw0nLYsjIgTjP3SU9WvwUhvJWzu5wZkdu3e03YoGxUjLWDXMKeSZ/g2Th5iNn3xlJwp5Z2p0jsU1rH4K/iMsYiLBJkGnsYuBqqFt2UIPYziqxOKV41oSKdEU+n4mD3WarU/kR4krTkmmEj2aebWgvHpsZSW0ULaeK3QxNBdx7waBUUkZ7nnDIRDi31T/sBYl+UADEFvm2INIsFuXPUyXbAthNWn5vIQNlKNLCwpGYqhuzO4hno8vyqbxKsrMtayk1U+0TQsBbQY1VuFF2bDBNFcPQOv/7KPJDL8hal0U6J0E6DVZVcH4Gel7pgsBeC+48=';
15
- // 兜底签名(非 Claude/Gemini 时)
16
- const DEFAULT_THOUGHT_SIGNATURE = CLAUDE_THOUGHT_SIGNATURE;
17
-
18
- function getThoughtSignatureForModel(actualModelName) {
19
- if (!actualModelName) return DEFAULT_THOUGHT_SIGNATURE;
20
- const lower = actualModelName.toLowerCase();
21
- if (lower.includes('claude')) return CLAUDE_THOUGHT_SIGNATURE;
22
- if (lower.includes('gemini')) return GEMINI_THOUGHT_SIGNATURE;
23
- return DEFAULT_THOUGHT_SIGNATURE;
24
- }
25
-
26
- function extractImagesFromContent(content) {
27
- const result = { text: '', images: [] };
28
-
29
- // 如果content是字符串,直接返回
30
- if (typeof content === 'string') {
31
- result.text = content;
32
- return result;
33
- }
34
-
35
- // 如果content是数组(multimodal格式)
36
- if (Array.isArray(content)) {
37
- for (const item of content) {
38
- if (item.type === 'text') {
39
- result.text += item.text;
40
- } else if (item.type === 'image_url') {
41
- // 提取base64图片数据
42
- const imageUrl = item.image_url?.url || '';
43
-
44
- // 匹配 data:image/{format};base64,{data} 格式
45
- const match = imageUrl.match(/^data:image\/(\w+);base64,(.+)$/);
46
- if (match) {
47
- const format = match[1]; // 例如 png, jpeg, jpg
48
- const base64Data = match[2];
49
- result.images.push({
50
- inlineData: {
51
- mimeType: `image/${format}`,
52
- data: base64Data
53
- }
54
- })
55
- }
56
- }
57
- }
58
- }
59
-
60
- return result;
61
- }
62
- function handleUserMessage(extracted, antigravityMessages){
63
- antigravityMessages.push({
64
- role: "user",
65
- parts: [
66
- {
67
- text: extracted.text
68
- },
69
- ...extracted.images
70
- ]
71
- })
72
- }
73
- // 将工具名称规范为 Vertex 要求的格式:^[a-zA-Z0-9_-]{1,128}$
74
- function sanitizeToolName(name) {
75
- if (!name || typeof name !== 'string') {
76
- return 'tool';
77
- }
78
- // 替换非法字符为下划线
79
- let cleaned = name.replace(/[^a-zA-Z0-9_-]/g, '_');
80
- // 去掉首尾多余下划线
81
- cleaned = cleaned.replace(/^_+|_+$/g, '');
82
- if (!cleaned) {
83
- cleaned = 'tool';
84
- }
85
- // 限制最大长度 128
86
- if (cleaned.length > 128) {
87
- cleaned = cleaned.slice(0, 128);
88
- }
89
- return cleaned;
90
- }
91
- function handleAssistantMessage(message, antigravityMessages, enableThinking, actualModelName, sessionId){
92
- const lastMessage = antigravityMessages[antigravityMessages.length - 1];
93
- const hasToolCalls = message.tool_calls && message.tool_calls.length > 0;
94
- const hasContent = message.content && message.content.trim() !== '';
95
-
96
- const antigravityTools = hasToolCalls ? message.tool_calls.map(toolCall => {
97
- const originalName = toolCall.function.name;
98
- const safeName = sanitizeToolName(originalName);
99
-
100
- const part = {
101
- functionCall: {
102
- id: toolCall.id,
103
- name: safeName,
104
- args: {
105
- query: toolCall.function.arguments
106
- }
107
- }
108
- };
109
-
110
- // 记录原始工具名到安全名的映射(仅当确实发生了变化时)
111
- if (sessionId && actualModelName && safeName !== originalName) {
112
- setToolNameMapping(sessionId, actualModelName, safeName, originalName);
113
- }
114
-
115
- // 启用思考模型时,工具调用优先使用实时签名(如果上游带了),否则兜底用常量
116
- if (enableThinking) {
117
- const cachedToolSig = getToolSignature(sessionId, actualModelName);
118
- part.thoughtSignature = toolCall.thoughtSignature || cachedToolSig || TOOL_THOUGHT_SIGNATURE;
119
- }
120
-
121
- return part;
122
- }) : [];
123
-
124
- if (lastMessage?.role === "model" && hasToolCalls && !hasContent){
125
- lastMessage.parts.push(...antigravityTools)
126
- }else{
127
- const parts = [];
128
-
129
- // 对于启用思考的模型,在历史 assistant 消息中补一个思考块 + 签名块
130
- // 结构示例:
131
- // {
132
- // "role": "model",
133
- // "parts": [
134
- // { "text": "␈", "thought": true },
135
- // { "text": "␈", "thoughtSignature": "..." },
136
- // { "text": "正常回复..." }
137
- // ]
138
- // }
139
- if (enableThinking) {
140
- // 普通思维链签名:
141
- // 1. 优先使用消息自身携带的 thoughtSignature
142
- // 2. 其次使用缓存中的最新签名(同 session + model)
143
- // 3. 最后按模型类型选择内置兜底签名
144
- const cachedSig = getReasoningSignature(sessionId, actualModelName);
145
- const thoughtSignature = message.thoughtSignature || cachedSig || getThoughtSignatureForModel(actualModelName);
146
- // 默认思考内容不能是完全空字符串,否则上游会要求 thinking 字段
147
- // 这里用一个不可见的退格符作为占位,实际展示时等价于“空思考块”
148
- let reasoningText = '';
149
- if (typeof message.reasoning_content === 'string' && message.reasoning_content.length > 0) {
150
- reasoningText = message.reasoning_content;
151
- } else {
152
- reasoningText = ' '; // 退格符占位
153
- }
154
- parts.push({ text: reasoningText, thought: true });
155
- // 思维链签名占位,避免上游校验缺少签名字段
156
- parts.push({ text: ' ', thoughtSignature });
157
- }
158
-
159
- if (hasContent) parts.push({ text: message.content.trimEnd() });
160
- parts.push(...antigravityTools);
161
-
162
- antigravityMessages.push({
163
- role: "model",
164
- parts
165
- })
166
- }
167
- }
168
- function handleToolCall(message, antigravityMessages){
169
- // 从之前的 model 消息中找到对应的 functionCall name
170
- let functionName = '';
171
- for (let i = antigravityMessages.length - 1; i >= 0; i--) {
172
- if (antigravityMessages[i].role === 'model') {
173
- const parts = antigravityMessages[i].parts;
174
- for (const part of parts) {
175
- if (part.functionCall && part.functionCall.id === message.tool_call_id) {
176
- functionName = part.functionCall.name;
177
- break;
178
- }
179
- }
180
- if (functionName) break;
181
- }
182
- }
183
-
184
- const lastMessage = antigravityMessages[antigravityMessages.length - 1];
185
- const functionResponse = {
186
- functionResponse: {
187
- id: message.tool_call_id,
188
- name: functionName,
189
- response: {
190
- output: message.content
191
- }
192
- }
193
- };
194
-
195
- // 如果上一条消息是 user 且包含 functionResponse,则合并
196
- if (lastMessage?.role === "user" && lastMessage.parts.some(p => p.functionResponse)) {
197
- lastMessage.parts.push(functionResponse);
198
- } else {
199
- antigravityMessages.push({
200
- role: "user",
201
- parts: [functionResponse]
202
- });
203
- }
204
- }
205
- function openaiMessageToAntigravity(openaiMessages, enableThinking, actualModelName, sessionId){
206
- const antigravityMessages = [];
207
- for (const message of openaiMessages) {
208
- if (message.role === "user" || message.role === "system") {
209
- // system 消息作为 user 处理(开头的 system 已在 generateRequestBody 中过滤)
210
- const extracted = extractImagesFromContent(message.content);
211
- handleUserMessage(extracted, antigravityMessages);
212
- } else if (message.role === "assistant") {
213
- handleAssistantMessage(message, antigravityMessages, enableThinking, actualModelName, sessionId);
214
- } else if (message.role === "tool") {
215
- handleToolCall(message, antigravityMessages);
216
- }
217
- }
218
-
219
- return antigravityMessages;
220
- }
221
-
222
- /**
223
- * 从 OpenAI 消息中提取并合并 system 指令
224
- * 规则:
225
- * 1. SYSTEM_INSTRUCTION 作为基础 system,可为空
226
- * 2. 根据 useContextSystemPrompt 配置决定是否收集请求中的 system 消息
227
- * 3. 如果 useContextSystemPrompt=true,收集开头连续的 system 消息并合并
228
- * 4. 如果 useContextSystemPrompt=false,只使用基础 SYSTEM_INSTRUCTION
229
- */
230
- function extractSystemInstruction(openaiMessages) {
231
- const baseSystem = config.systemInstruction || '';
232
-
233
- // 如果不使用上下文 system,只返回基础 system
234
- if (!config.useContextSystemPrompt) {
235
- return baseSystem;
236
- }
237
-
238
- // 收集开头连续的 system 消息
239
- const systemTexts = [];
240
- for (const message of openaiMessages) {
241
- if (message.role === 'system') {
242
- const content = typeof message.content === 'string'
243
- ? message.content
244
- : (Array.isArray(message.content)
245
- ? message.content.filter(item => item.type === 'text').map(item => item.text).join('')
246
- : '');
247
- if (content.trim()) {
248
- systemTexts.push(content.trim());
249
- }
250
- } else {
251
- // 遇到非 system 消息就停止收集
252
- break;
253
- }
254
- }
255
-
256
- // 合并:基础 system + 用户的 system 消息
257
- const parts = [];
258
- if (baseSystem.trim()) {
259
- parts.push(baseSystem.trim());
260
- }
261
- if (systemTexts.length > 0) {
262
- parts.push(systemTexts.join('\n\n'));
263
- }
264
-
265
- return parts.join('\n\n');
266
- }
267
- // reasoning_effort 到 thinkingBudget 的映射
268
- const REASONING_EFFORT_MAP = {
269
- 'low': 1024,
270
- 'medium': 16000,
271
- 'high': 32000
272
- };
273
-
274
- function generateGenerationConfig(parameters, enableThinking, actualModelName){
275
- // 获取思考预算:
276
- // 1. 优先使用 thinking_budget(直接数值)
277
- // 2. 其次使用 reasoning_effort(OpenAI 格式:low/medium/high)
278
- // 3. 最后使用配置默认值或硬编码默认值
279
- const defaultThinkingBudget = config.defaults.thinking_budget ?? 1024;
280
-
281
- let thinkingBudget = 0;
282
- if (enableThinking) {
283
- if (parameters.thinking_budget !== undefined) {
284
- thinkingBudget = parameters.thinking_budget;
285
- } else if (parameters.reasoning_effort !== undefined) {
286
- thinkingBudget = REASONING_EFFORT_MAP[parameters.reasoning_effort] ?? defaultThinkingBudget;
287
- } else {
288
- thinkingBudget = defaultThinkingBudget;
289
- }
290
- }
291
-
292
- const generationConfig = {
293
- topP: parameters.top_p ?? config.defaults.top_p,
294
- topK: parameters.top_k ?? config.defaults.top_k,
295
- temperature: parameters.temperature ?? config.defaults.temperature,
296
- candidateCount: 1,
297
- maxOutputTokens: parameters.max_tokens ?? config.defaults.max_tokens,
298
- stopSequences: [
299
- "<|user|>",
300
- "<|bot|>",
301
- "<|context_request|>",
302
- "<|endoftext|>",
303
- "<|end_of_turn|>"
304
- ],
305
- thinkingConfig: {
306
- includeThoughts: enableThinking,
307
- thinkingBudget: thinkingBudget
308
- }
309
- }
310
- if (enableThinking && actualModelName.includes("claude")){
311
- delete generationConfig.topP;
312
- }
313
- return generationConfig
314
- }
315
- // 不被 Google 工具参数 Schema 支持的字段,在这里统一过滤掉
316
- // 包括:
317
- // - JSON Schema 的元信息字段:$schema, additionalProperties
318
- // - 长度/数量约束:minLength, maxLength, minItems, maxItems, uniqueItems(不必传给后端)
319
- // - 严格上下界 / 常量:exclusiveMaximum, exclusiveMinimum, const(Google Schema 不支持)
320
- // - 组合约束:anyOf/oneOf/allOf 以及其非标准写法 any_of/one_of/all_of(为避免上游实现差异,这里一律去掉)
321
- const EXCLUDED_KEYS = new Set([
322
- '$schema',
323
- 'additionalProperties',
324
- 'minLength',
325
- 'maxLength',
326
- 'minItems',
327
- 'maxItems',
328
- 'uniqueItems',
329
- 'exclusiveMaximum',
330
- 'exclusiveMinimum',
331
- 'const',
332
- 'anyOf',
333
- 'oneOf',
334
- 'allOf',
335
- 'any_of',
336
- 'one_of',
337
- 'all_of'
338
- ]);
339
-
340
- function cleanParameters(obj) {
341
- if (!obj || typeof obj !== 'object') return obj;
342
-
343
- const cleaned = Array.isArray(obj) ? [] : {};
344
-
345
- for (const [key, value] of Object.entries(obj)) {
346
- if (EXCLUDED_KEYS.has(key)) continue;
347
- const cleanedValue = (value && typeof value === 'object') ? cleanParameters(value) : value;
348
- cleaned[key] = cleanedValue;
349
- }
350
-
351
- return cleaned;
352
- }
353
-
354
- function convertOpenAIToolsToAntigravity(openaiTools, sessionId, actualModelName){
355
- if (!openaiTools || openaiTools.length === 0) return [];
356
- return openaiTools.map((tool)=>{
357
- // 先清洗一遍参数,过滤/规范化不兼容字段
358
- const rawParams = tool.function?.parameters || {};
359
- const cleanedParams = cleanParameters(rawParams) || {};
360
-
361
- // 确保顶层是一个合法的 JSON Schema 对象
362
- // 如果用户没显式指定 type,则默认按 OpenAI 习惯设为 object
363
- if (cleanedParams.type === undefined) {
364
- cleanedParams.type = 'object';
365
- }
366
- // 对于 object 类型,至少保证有 properties 字段
367
- if (cleanedParams.type === 'object' && cleanedParams.properties === undefined) {
368
- cleanedParams.properties = {};
369
- }
370
-
371
- const originalName = tool.function?.name;
372
- const safeName = sanitizeToolName(originalName);
373
-
374
- // 仅当发生转换时才缓存映射
375
- if (sessionId && actualModelName && safeName !== originalName) {
376
- setToolNameMapping(sessionId, actualModelName, safeName, originalName);
377
- }
378
-
379
- return {
380
- functionDeclarations: [
381
- {
382
- name: safeName,
383
- description: tool.function.description,
384
- parameters: cleanedParams
385
- }
386
- ]
387
- }
388
- })
389
- }
390
-
391
- function modelMapping(modelName){
392
- if (modelName === "claude-sonnet-4-5-thinking"){
393
- return "claude-sonnet-4-5";
394
- } else if (modelName === "claude-opus-4-5"){
395
- return "claude-opus-4-5-thinking";
396
- } else if (modelName === "gemini-2.5-flash-thinking"){
397
- return "gemini-2.5-flash";
398
- }
399
- return modelName;
400
- }
401
-
402
- function isEnableThinking(modelName){
403
- // 只要模型名里包含 -thinking(例如 gemini-2.0-flash-thinking-exp),就认为支持思考配置
404
- return modelName.includes('-thinking') ||
405
- modelName === 'gemini-2.5-pro' ||
406
- modelName.startsWith('gemini-3-pro-') ||
407
- modelName === "rev19-uic3-1p" ||
408
- modelName === "gpt-oss-120b-medium";
409
- }
410
-
411
- function generateRequestBody(openaiMessages,modelName,parameters,openaiTools,token){
412
-
413
- const enableThinking = isEnableThinking(modelName);
414
- const actualModelName = modelMapping(modelName);
415
-
416
- // 提取合并后的 system 指令
417
- const mergedSystemInstruction = extractSystemInstruction(openaiMessages);
418
-
419
- // 根据 useContextSystemPrompt 配置决定如何处理 system 消息
420
- let startIndex = 0;
421
- if (config.useContextSystemPrompt) {
422
- // 过滤掉开头连续的 system 消息,避免重复作为 user 发送
423
- for (let i = 0; i < openaiMessages.length; i++) {
424
- if (openaiMessages[i].role === 'system') {
425
- startIndex = i + 1;
426
- } else {
427
- break;
428
- }
429
- }
430
- }
431
- const filteredMessages = openaiMessages.slice(startIndex);
432
-
433
- const requestBody = {
434
- project: token.projectId,
435
- requestId: generateRequestId(),
436
- request: {
437
- contents: openaiMessageToAntigravity(filteredMessages, enableThinking, actualModelName, token.sessionId),
438
- tools: convertOpenAIToolsToAntigravity(openaiTools, token.sessionId, actualModelName),
439
- toolConfig: {
440
- functionCallingConfig: {
441
- mode: "VALIDATED"
442
- }
443
- },
444
- generationConfig: generateGenerationConfig(parameters, enableThinking, actualModelName),
445
- sessionId: token.sessionId
446
- },
447
- model: actualModelName,
448
- userAgent: "antigravity"
449
- };
450
-
451
- // 只有当有 system 指令时才添加 systemInstruction 字段
452
- if (mergedSystemInstruction) {
453
- requestBody.request.systemInstruction = {
454
- role: "user",
455
- parts: [{ text: mergedSystemInstruction }]
456
- };
457
- }
458
-
459
- return requestBody;
460
- }
461
- /**
462
- * 将通用文本对话请求体转换为图片生成请求体
463
- * 统一配置 image_gen 所需字段,避免在各处手动删除/覆盖字段
464
- */
465
- function prepareImageRequest(requestBody) {
466
- if (!requestBody || !requestBody.request) return requestBody;
467
-
468
- requestBody.request.generationConfig = { candidateCount: 1 };
469
- requestBody.requestType = 'image_gen';
470
-
471
- // image_gen 模式下不需要这些字段
472
- delete requestBody.request.systemInstruction;
473
- delete requestBody.request.tools;
474
- delete requestBody.request.toolConfig;
475
-
476
- return requestBody;
477
- }
478
-
479
- function getDefaultIp(){
480
- const interfaces = os.networkInterfaces();
481
- for (const iface of Object.values(interfaces)){
482
- for (const inter of iface){
483
- if (inter.family === 'IPv4' && !inter.internal){
484
- return inter.address;
485
- }
486
- }
487
- }
488
- return '127.0.0.1';
489
- }
490
- export{
491
- generateRequestId,
492
- generateRequestBody,
493
- prepareImageRequest,
494
- getDefaultIp
495
- }
 
1
+ export { generateRequestId, generateRequestBody, prepareImageRequest, getDefaultIp } from './openai_mapping.js';