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 +3 -2
- public/assets/bg.jpg +3 -0
- public/index.html +60 -21
- public/js/config.js +3 -6
- public/js/main.js +5 -1
- public/js/ui.js +30 -2
- public/style.css +22 -17
- src/api/client.js +14 -164
- src/api/stream_parser.js +156 -0
- src/auth/quota_manager.js +8 -18
- src/auth/token_manager.js +314 -165
- src/auth/token_store.js +117 -0
- src/config/config.js +36 -72
- src/constants/index.js +190 -0
- src/routes/admin.js +34 -44
- src/server/index.js +27 -96
- src/utils/errors.js +231 -0
- src/utils/imageStorage.js +2 -49
- src/utils/memoryManager.js +16 -9
- src/utils/openai_generation.js +58 -0
- src/utils/openai_mapping.js +81 -0
- src/utils/openai_messages.js +178 -0
- src/utils/openai_signatures.js +28 -0
- src/utils/openai_system.js +33 -0
- src/utils/openai_tools.js +83 -0
- src/utils/paths.js +218 -0
- src/utils/utils.js +1 -495
config.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
| 7 |
"memoryThreshold": 25
|
| 8 |
},
|
| 9 |
"rotation": {
|
| 10 |
-
"strategy": "
|
| 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
|
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 |
-
<!--
|
| 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 |
-
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
</script>
|
| 18 |
<style>
|
| 19 |
-
|
| 20 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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-
|
|
|
|
| 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:
|
| 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
|
| 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
|
| 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
|
| 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
|
| 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
|
| 9 |
import { buildAxiosRequestConfig } from '../utils/httpClient.js';
|
| 10 |
-
import {
|
| 11 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 ||
|
| 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 |
-
//
|
| 142 |
-
|
| 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 |
-
|
| 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 =
|
| 27 |
-
this.CLEANUP_INTERVAL =
|
| 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 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 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 |
-
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
this.tokens = [];
|
|
|
|
| 63 |
this.currentIndex = 0;
|
| 64 |
|
| 65 |
// 轮询策略相关 - 使用原子操作避免锁
|
|
|
|
| 66 |
this.rotationStrategy = RotationStrategy.ROUND_ROBIN;
|
| 67 |
-
|
| 68 |
-
this.
|
|
|
|
|
|
|
| 69 |
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
}
|
| 79 |
-
if (!fs.existsSync(this.filePath)) {
|
| 80 |
-
fs.writeFileSync(this.filePath, '[]', 'utf8');
|
| 81 |
-
log.info('✓ 已创建账号配置文件');
|
| 82 |
-
}
|
| 83 |
}
|
| 84 |
|
| 85 |
-
async
|
| 86 |
try {
|
| 87 |
log.info('正在初始化token管理器...');
|
| 88 |
-
const
|
| 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 -
|
| 170 |
}
|
| 171 |
|
| 172 |
async refreshToken(token) {
|
|
@@ -197,37 +272,19 @@ class TokenManager {
|
|
| 197 |
this.saveToFile(token);
|
| 198 |
return token;
|
| 199 |
} catch (error) {
|
| 200 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
}
|
| 202 |
}
|
| 203 |
|
| 204 |
saveToFile(tokenToUpdate = null) {
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 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 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 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) %
|
| 348 |
}
|
| 349 |
-
|
| 350 |
return token;
|
| 351 |
} catch (error) {
|
| 352 |
-
|
| 353 |
-
|
| 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 |
-
|
|
|
|
| 386 |
log.info('Token已热重载');
|
| 387 |
}
|
| 388 |
|
| 389 |
-
addToken(tokenData) {
|
| 390 |
try {
|
| 391 |
-
this.
|
| 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 |
-
|
| 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.
|
| 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 |
-
|
| 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.
|
| 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 |
-
|
| 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.
|
| 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 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 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 ||
|
| 108 |
-
host: jsonConfig.server?.host ||
|
| 109 |
-
heartbeatInterval: jsonConfig.server?.heartbeatInterval ||
|
| 110 |
memoryThreshold: jsonConfig.server?.memoryThreshold || 500
|
| 111 |
},
|
| 112 |
cache: {
|
| 113 |
-
modelListTTL: jsonConfig.cache?.modelListTTL ||
|
| 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 ||
|
| 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
|
| 130 |
-
top_p: jsonConfig.defaults?.topP
|
| 131 |
-
top_k: jsonConfig.defaults?.topK
|
| 132 |
-
max_tokens: jsonConfig.defaults?.maxTokens
|
| 133 |
-
thinking_budget: jsonConfig.defaults?.thinkingBudget ??
|
| 134 |
},
|
| 135 |
security: {
|
| 136 |
-
maxRequestSize: jsonConfig.server?.maxRequestSize ||
|
| 137 |
apiKey: process.env.API_KEY || null
|
| 138 |
},
|
| 139 |
admin: {
|
| 140 |
-
username: process.env.ADMIN_USERNAME ||
|
| 141 |
-
password: process.env.ADMIN_PASSWORD ||
|
| 142 |
-
jwtSecret: process.env.JWT_SECRET ||
|
| 143 |
},
|
| 144 |
useNativeAxios: jsonConfig.other?.useNativeAxios !== false,
|
| 145 |
-
timeout: jsonConfig.other?.timeout ||
|
| 146 |
-
retryTimes: Number.isFinite(jsonConfig.other?.retryTimes) ? jsonConfig.other.retryTimes :
|
| 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
|
| 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 |
-
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
});
|
| 81 |
|
| 82 |
-
router.put('/tokens/:refreshToken', authMiddleware, (req, res) => {
|
| 83 |
const { refreshToken } = req.params;
|
| 84 |
const updates = req.body;
|
| 85 |
-
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
});
|
| 88 |
|
| 89 |
-
router.delete('/tokens/:refreshToken', authMiddleware, (req, res) => {
|
| 90 |
const { refreshToken } = req.params;
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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, {
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 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 ||
|
| 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(
|
| 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 |
-
|
| 216 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 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';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|