Merge pull request #67 from ZhaoShanGeng/binary-build
Browse files- .github/workflows/docker-publish.yml +1 -0
- API.md +51 -3
- README.md +129 -5
- config.json +3 -2
- package.json +4 -1
- public/assets/bg.jpg +3 -0
- public/index.html +67 -21
- public/js/config.js +3 -6
- public/js/main.js +5 -1
- public/js/ui.js +30 -2
- public/style.css +22 -16
- scripts/build.js +8 -13
- src/api/client.js +15 -161
- src/api/stream_parser.js +156 -0
- src/auth/oauth_manager.js +1 -1
- 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 +26 -95
- 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 -1
.github/workflows/docker-publish.yml
CHANGED
|
@@ -72,6 +72,7 @@ jobs:
|
|
| 72 |
tags: |
|
| 73 |
type=ref,event=branch,suffix=${{ matrix.tag_suffix }}
|
| 74 |
type=raw,value=binary${{ matrix.tag_suffix }},enable=true
|
|
|
|
| 75 |
|
| 76 |
- name: Build and push (${{ matrix.arch }})
|
| 77 |
uses: docker/build-push-action@v5
|
|
|
|
| 72 |
tags: |
|
| 73 |
type=ref,event=branch,suffix=${{ matrix.tag_suffix }}
|
| 74 |
type=raw,value=binary${{ matrix.tag_suffix }},enable=true
|
| 75 |
+
type=raw,value=latest${{ matrix.tag_suffix }},enable=${{ github.ref == 'refs/heads/binary-build' }}
|
| 76 |
|
| 77 |
- name: Build and push (${{ matrix.arch }})
|
| 78 |
uses: docker/build-push-action@v5
|
API.md
CHANGED
|
@@ -162,7 +162,7 @@ curl http://localhost:8045/v1/chat/completions \
|
|
| 162 |
| `top_p` | number | ❌ | Top P 参数,默认 1 |
|
| 163 |
| `top_k` | number | ❌ | Top K 参数,默认 50 |
|
| 164 |
| `max_tokens` | number | ❌ | 最大 token 数,默认 32000 |
|
| 165 |
-
| `thinking_budget` | number | ❌ | 思考预算(仅对思考模型生效),可为 0 或 1024-32000,默认
|
| 166 |
| `reasoning_effort` | string | ❌ | 思维链强度(OpenAI 格式),可选值:`low`(1024)、`medium`(16000)、`high`(32000) |
|
| 167 |
| `tools` | array | ❌ | 工具列表(Function Calling) |
|
| 168 |
|
|
@@ -246,8 +246,8 @@ curl http://localhost:8045/v1/chat/completions \
|
|
| 246 |
|
| 247 |
| reasoning_effort | thinking_budget | 说明 |
|
| 248 |
|-----------------|-----------------|------|
|
| 249 |
-
| `low` | 1024 |
|
| 250 |
-
| `medium` | 16000 |
|
| 251 |
| `high` | 32000 | 深度思考,适合复杂推理 |
|
| 252 |
|
| 253 |
### 使用 thinking_budget(直接数值)
|
|
@@ -505,6 +505,53 @@ for await (const chunk of stream) {
|
|
| 505 |
}
|
| 506 |
```
|
| 507 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
## 注意事项
|
| 509 |
|
| 510 |
1. 所有 `/v1/*` 请求必须携带有效的 API Key
|
|
@@ -515,3 +562,4 @@ for await (const chunk of stream) {
|
|
| 515 |
6. 图片生成仅支持 `gemini-3-pro-image` 模型
|
| 516 |
7. 模型列表会缓存 1 小时,可通过配置调整
|
| 517 |
8. 思维链内容通过 `reasoning_content` 字段输出(兼容 DeepSeek 格式)
|
|
|
|
|
|
| 162 |
| `top_p` | number | ❌ | Top P 参数,默认 1 |
|
| 163 |
| `top_k` | number | ❌ | Top K 参数,默认 50 |
|
| 164 |
| `max_tokens` | number | ❌ | 最大 token 数,默认 32000 |
|
| 165 |
+
| `thinking_budget` | number | ❌ | 思考预算(仅对思考模型生效),可为 0 或 1024-32000,默认 1024(0 表示关闭思考预算限制) |
|
| 166 |
| `reasoning_effort` | string | ❌ | 思维链强度(OpenAI 格式),可选值:`low`(1024)、`medium`(16000)、`high`(32000) |
|
| 167 |
| `tools` | array | ❌ | 工具列表(Function Calling) |
|
| 168 |
|
|
|
|
| 246 |
|
| 247 |
| reasoning_effort | thinking_budget | 说明 |
|
| 248 |
|-----------------|-----------------|------|
|
| 249 |
+
| `low` | 1024 | 快速响应,适合简单问题(默认) |
|
| 250 |
+
| `medium` | 16000 | 平衡模式 |
|
| 251 |
| `high` | 32000 | 深度思考,适合复杂推理 |
|
| 252 |
|
| 253 |
### 使用 thinking_budget(直接数值)
|
|
|
|
| 505 |
}
|
| 506 |
```
|
| 507 |
|
| 508 |
+
## 配置选项
|
| 509 |
+
|
| 510 |
+
### passSignatureToClient
|
| 511 |
+
|
| 512 |
+
控制是否将 `thoughtSignature` 透传到客户端响应中。
|
| 513 |
+
|
| 514 |
+
在 `config.json` 中配置:
|
| 515 |
+
|
| 516 |
+
```json
|
| 517 |
+
{
|
| 518 |
+
"other": {
|
| 519 |
+
"passSignatureToClient": false
|
| 520 |
+
}
|
| 521 |
+
}
|
| 522 |
+
```
|
| 523 |
+
|
| 524 |
+
- `false`(默认):不透传签名,响应中不包含 `thoughtSignature` 字段
|
| 525 |
+
- `true`:透传签名,响应中包含 `thoughtSignature` 字段
|
| 526 |
+
|
| 527 |
+
**启用透传后的响应示例**:
|
| 528 |
+
|
| 529 |
+
```json
|
| 530 |
+
{
|
| 531 |
+
"choices": [{
|
| 532 |
+
"delta": {
|
| 533 |
+
"reasoning_content": "让我思考...",
|
| 534 |
+
"thoughtSignature": "RXFRRENrZ0lDaEFD..."
|
| 535 |
+
}
|
| 536 |
+
}]
|
| 537 |
+
}
|
| 538 |
+
```
|
| 539 |
+
|
| 540 |
+
### useContextSystemPrompt
|
| 541 |
+
|
| 542 |
+
控制是否将请求中的 system 消息合并到 SystemInstruction。
|
| 543 |
+
|
| 544 |
+
```json
|
| 545 |
+
{
|
| 546 |
+
"other": {
|
| 547 |
+
"useContextSystemPrompt": false
|
| 548 |
+
}
|
| 549 |
+
}
|
| 550 |
+
```
|
| 551 |
+
|
| 552 |
+
- `false`(默认):仅使用全局 `SYSTEM_INSTRUCTION` 环境变量
|
| 553 |
+
- `true`:将请求开头连续的 system 消息与全局配置合并
|
| 554 |
+
|
| 555 |
## 注意事项
|
| 556 |
|
| 557 |
1. 所有 `/v1/*` 请求必须携带有效的 API Key
|
|
|
|
| 562 |
6. 图片生成仅支持 `gemini-3-pro-image` 模型
|
| 563 |
7. 模型列表会缓存 1 小时,可通过配置调整
|
| 564 |
8. 思维链内容通过 `reasoning_content` 字段输出(兼容 DeepSeek 格式)
|
| 565 |
+
9. 默认轮询策略为 `request_count`,每 50 次请求切换 Token
|
README.md
CHANGED
|
@@ -23,6 +23,8 @@
|
|
| 23 |
- ✅ 隐私模式(自动隐藏敏感信息)
|
| 24 |
- ✅ 内存优化(从 8+ 进程减少为 2 个进程,内存占用从 100MB+ 降为 50MB+)
|
| 25 |
- ✅ 对象池复用(减少 50%+ 临时对象创建,降低 GC 频率)
|
|
|
|
|
|
|
| 26 |
|
| 27 |
## 环境要求
|
| 28 |
|
|
@@ -75,6 +77,114 @@ npm start
|
|
| 75 |
|
| 76 |
服务将在 `http://localhost:8045` 启动。
|
| 77 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
## Docker 部署
|
| 79 |
|
| 80 |
### 使用 Docker Compose(推荐)
|
|
@@ -365,7 +475,9 @@ curl http://localhost:8045/v1/chat/completions \
|
|
| 365 |
"other": {
|
| 366 |
"timeout": 300000, // 请求超时时间(毫秒)
|
| 367 |
"skipProjectIdFetch": false,// 跳过 ProjectId 获取,直接随机生成(仅 Pro 账号有效)
|
| 368 |
-
"useNativeAxios": false
|
|
|
|
|
|
|
| 369 |
}
|
| 370 |
}
|
| 371 |
```
|
|
@@ -375,8 +487,8 @@ curl http://localhost:8045/v1/chat/completions \
|
|
| 375 |
| 策略 | 说明 |
|
| 376 |
|------|------|
|
| 377 |
| `round_robin` | 均衡负载:每次请求后切换到下一个 Token |
|
| 378 |
-
| `quota_exhausted` | 额度耗尽才切换:持续使用当前 Token
|
| 379 |
-
| `request_count` | 自定义次数:每个 Token
|
| 380 |
|
| 381 |
### 2. .env(敏感配置)
|
| 382 |
|
|
@@ -425,10 +537,12 @@ npm run login
|
|
| 425 |
│ └── refresh-tokens.js # Token 刷新脚本
|
| 426 |
├── src/
|
| 427 |
│ ├── api/
|
| 428 |
-
│ │
|
|
|
|
| 429 |
│ ├── auth/
|
| 430 |
│ │ ├── jwt.js # JWT 认证
|
| 431 |
│ │ ├── token_manager.js # Token 管理(含轮询策略)
|
|
|
|
| 432 |
│ │ └── quota_manager.js # 额度缓存管理
|
| 433 |
│ ├── routes/
|
| 434 |
│ │ ├── admin.js # 管理接口路由
|
|
@@ -441,6 +555,7 @@ npm run login
|
|
| 441 |
│ │ ├── config.js # 配置加载
|
| 442 |
│ │ └── init-env.js # 环境变量初始化
|
| 443 |
│ ├── constants/
|
|
|
|
| 444 |
│ │ └── oauth.js # OAuth 常量
|
| 445 |
│ ├── server/
|
| 446 |
│ │ └── index.js # 主服务器(含内存管理和心跳)
|
|
@@ -448,10 +563,19 @@ npm run login
|
|
| 448 |
│ │ ├── configReloader.js # 配置热重载
|
| 449 |
│ │ ├── deepMerge.js # 深度合并工具
|
| 450 |
│ │ ├── envParser.js # 环境变量解析
|
|
|
|
| 451 |
│ │ ├── idGenerator.js # ID 生成器
|
| 452 |
│ │ ├── imageStorage.js # 图片存储
|
| 453 |
│ │ ├── logger.js # 日志模块
|
| 454 |
-
│ │
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 455 |
│ └── AntigravityRequester.js # TLS 指纹请求器封装
|
| 456 |
├── test/
|
| 457 |
│ ├── test-request.js # 请求测试
|
|
|
|
| 23 |
- ✅ 隐私模式(自动隐藏敏感信息)
|
| 24 |
- ✅ 内存优化(从 8+ 进程减少为 2 个进程,内存占用从 100MB+ 降为 50MB+)
|
| 25 |
- ✅ 对象池复用(减少 50%+ 临时对象创建,降低 GC 频率)
|
| 26 |
+
- ✅ 签名透传控制(可配置是否将 thoughtSignature 透传到客户端)
|
| 27 |
+
- ✅ 预编译二进制文件(支持 Windows/Linux/Android,无需 Node.js 环境)
|
| 28 |
|
| 29 |
## 环境要求
|
| 30 |
|
|
|
|
| 77 |
|
| 78 |
服务将在 `http://localhost:8045` 启动。
|
| 79 |
|
| 80 |
+
## 二进制文件部署(推荐)
|
| 81 |
+
|
| 82 |
+
无需安装 Node.js,直接下载预编译的二进制文件即可运行。
|
| 83 |
+
|
| 84 |
+
### 下载二进制文件
|
| 85 |
+
|
| 86 |
+
从 [GitHub Releases](https://github.com/ZhaoShanGeng/antigravity2api-nodejs/releases) 下载对应平台的二进制文件:
|
| 87 |
+
|
| 88 |
+
| 平台 | 文件名 |
|
| 89 |
+
|------|--------|
|
| 90 |
+
| Windows x64 | `antigravity2api-win-x64.exe` |
|
| 91 |
+
| Linux x64 | `antigravity2api-linux-x64` |
|
| 92 |
+
| Linux ARM64 | `antigravity2api-linux-arm64` |
|
| 93 |
+
| macOS x64 | `antigravity2api-macos-x64` |
|
| 94 |
+
| macOS ARM64 | `antigravity2api-macos-arm64` |
|
| 95 |
+
|
| 96 |
+
### 准备配置文件
|
| 97 |
+
|
| 98 |
+
将以下文件放在二进制文件同目录下:
|
| 99 |
+
|
| 100 |
+
```
|
| 101 |
+
├── antigravity2api-win-x64.exe # 二进制文件
|
| 102 |
+
├── .env # 环境变量配置(必需)
|
| 103 |
+
├── config.json # 基础配置(必需)
|
| 104 |
+
├── public/ # 静态文件目录(必需)
|
| 105 |
+
│ ├── index.html
|
| 106 |
+
│ ├── style.css
|
| 107 |
+
│ ├── assets/
|
| 108 |
+
│ │ └── bg.jpg
|
| 109 |
+
│ └── js/
|
| 110 |
+
│ ├── auth.js
|
| 111 |
+
│ ├── config.js
|
| 112 |
+
│ ├── main.js
|
| 113 |
+
│ ├── quota.js
|
| 114 |
+
│ ├── tokens.js
|
| 115 |
+
│ ├── ui.js
|
| 116 |
+
│ └── utils.js
|
| 117 |
+
└── data/ # 数据目录(自动创建)
|
| 118 |
+
└── accounts.json
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
### 配置环境变量
|
| 122 |
+
|
| 123 |
+
创建 `.env` 文件:
|
| 124 |
+
|
| 125 |
+
```env
|
| 126 |
+
API_KEY=sk-your-api-key
|
| 127 |
+
ADMIN_USERNAME=admin
|
| 128 |
+
ADMIN_PASSWORD=admin123
|
| 129 |
+
JWT_SECRET=your-jwt-secret-key-change-this-in-production
|
| 130 |
+
# IMAGE_BASE_URL=http://your-domain.com
|
| 131 |
+
# PROXY=http://127.0.0.1:7890
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
### 运行
|
| 135 |
+
|
| 136 |
+
**Windows**:
|
| 137 |
+
```bash
|
| 138 |
+
# 直接双击运行,或在命令行执行
|
| 139 |
+
antigravity2api-win-x64.exe
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
**Linux/macOS**:
|
| 143 |
+
```bash
|
| 144 |
+
# 添加执行权限
|
| 145 |
+
chmod +x antigravity2api-linux-x64
|
| 146 |
+
|
| 147 |
+
# 运行
|
| 148 |
+
./antigravity2api-linux-x64
|
| 149 |
+
```
|
| 150 |
+
|
| 151 |
+
### 二进制部署说明
|
| 152 |
+
|
| 153 |
+
- **无需 Node.js**:二进制文件已包含 Node.js 运行时
|
| 154 |
+
- **配置文件**:`.env` 和 `config.json` 必须与二进制文件在同一目录
|
| 155 |
+
- **静态文件**:`public/` 目录必须与二进制文件在同一目录
|
| 156 |
+
- **数据持久化**:`data/` 目录会自动创建,用于存储 Token 数据
|
| 157 |
+
- **跨平台**:支持 Windows、Linux、macOS(x64 和 ARM64)
|
| 158 |
+
|
| 159 |
+
### 作为系统服务运行(Linux)
|
| 160 |
+
|
| 161 |
+
创建 systemd 服务文件 `/etc/systemd/system/antigravity2api.service`:
|
| 162 |
+
|
| 163 |
+
```ini
|
| 164 |
+
[Unit]
|
| 165 |
+
Description=Antigravity2API Service
|
| 166 |
+
After=network.target
|
| 167 |
+
|
| 168 |
+
[Service]
|
| 169 |
+
Type=simple
|
| 170 |
+
User=www-data
|
| 171 |
+
WorkingDirectory=/opt/antigravity2api
|
| 172 |
+
ExecStart=/opt/antigravity2api/antigravity2api-linux-x64
|
| 173 |
+
Restart=always
|
| 174 |
+
RestartSec=10
|
| 175 |
+
|
| 176 |
+
[Install]
|
| 177 |
+
WantedBy=multi-user.target
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
启动服务:
|
| 181 |
+
|
| 182 |
+
```bash
|
| 183 |
+
sudo systemctl daemon-reload
|
| 184 |
+
sudo systemctl enable antigravity2api
|
| 185 |
+
sudo systemctl start antigravity2api
|
| 186 |
+
```
|
| 187 |
+
|
| 188 |
## Docker 部署
|
| 189 |
|
| 190 |
### 使用 Docker Compose(推荐)
|
|
|
|
| 475 |
"other": {
|
| 476 |
"timeout": 300000, // 请求超时时间(毫秒)
|
| 477 |
"skipProjectIdFetch": false,// 跳过 ProjectId 获取,直接随机生成(仅 Pro 账号有效)
|
| 478 |
+
"useNativeAxios": false, // 使用原生 axios 而非 AntigravityRequester
|
| 479 |
+
"useContextSystemPrompt": false, // 是否将请求中的 system 消息合并到 SystemInstruction
|
| 480 |
+
"passSignatureToClient": false // 是否将 thoughtSignature 透传到客户端
|
| 481 |
}
|
| 482 |
}
|
| 483 |
```
|
|
|
|
| 487 |
| 策略 | 说明 |
|
| 488 |
|------|------|
|
| 489 |
| `round_robin` | 均衡负载:每次请求后切换到下一个 Token |
|
| 490 |
+
| `quota_exhausted` | 额度耗尽才切换:持续使用当前 Token 直到额度用完(高性能优化) |
|
| 491 |
+
| `request_count` | 自定义次数:每个 Token 使用指定次数后切换(默认策略) |
|
| 492 |
|
| 493 |
### 2. .env(敏感配置)
|
| 494 |
|
|
|
|
| 537 |
│ └── refresh-tokens.js # Token 刷新脚本
|
| 538 |
├── src/
|
| 539 |
│ ├── api/
|
| 540 |
+
│ │ ├── client.js # API 调用逻辑(含模型列表缓存)
|
| 541 |
+
│ │ └── stream_parser.js # 流式响应解析(对象池优化)
|
| 542 |
│ ├── auth/
|
| 543 |
│ │ ├── jwt.js # JWT 认证
|
| 544 |
│ │ ├── token_manager.js # Token 管理(含轮询策略)
|
| 545 |
+
│ │ ├── token_store.js # Token 文件存储(异步读写)
|
| 546 |
│ │ └── quota_manager.js # 额度缓存管理
|
| 547 |
│ ├── routes/
|
| 548 |
│ │ ├── admin.js # 管理接口路由
|
|
|
|
| 555 |
│ │ ├── config.js # 配置加载
|
| 556 |
│ │ └── init-env.js # 环境变量初始化
|
| 557 |
│ ├── constants/
|
| 558 |
+
│ │ ├── index.js # 应用常量定义
|
| 559 |
│ │ └── oauth.js # OAuth 常量
|
| 560 |
│ ├── server/
|
| 561 |
│ │ └── index.js # 主服务器(含内存管理和心跳)
|
|
|
|
| 563 |
│ │ ├── configReloader.js # 配置热重载
|
| 564 |
│ │ ├── deepMerge.js # 深度合并工具
|
| 565 |
│ │ ├── envParser.js # 环境变量解析
|
| 566 |
+
│ │ ├── errors.js # 统一错误处理
|
| 567 |
│ │ ├── idGenerator.js # ID 生成器
|
| 568 |
│ │ ├── imageStorage.js # 图片存储
|
| 569 |
│ │ ├── logger.js # 日志模块
|
| 570 |
+
│ │ ├── memoryManager.js # 智能内存管理
|
| 571 |
+
│ │ ├── openai_generation.js # 生成配置
|
| 572 |
+
│ │ ├── openai_mapping.js # 请求体构建
|
| 573 |
+
│ │ ├── openai_messages.js # 消息格式转换
|
| 574 |
+
│ │ ├── openai_signatures.js # 签名常量
|
| 575 |
+
│ │ ├── openai_system.js # 系统指令提取
|
| 576 |
+
│ │ ├── openai_tools.js # 工具格式转换
|
| 577 |
+
│ │ ├── paths.js # 路径工具(支持 pkg 打包)
|
| 578 |
+
│ │ └── utils.js # 工具函数(重导出)
|
| 579 |
│ └── AntigravityRequester.js # TLS 指纹请求器封装
|
| 580 |
├── test/
|
| 581 |
│ ├── test-request.js # 请求测试
|
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 |
}
|
package.json
CHANGED
|
@@ -47,7 +47,10 @@
|
|
| 47 |
"scripts/**/*.js"
|
| 48 |
],
|
| 49 |
"assets": [
|
| 50 |
-
"public
|
|
|
|
|
|
|
|
|
|
| 51 |
"src/bin/**/*",
|
| 52 |
".env.example",
|
| 53 |
"config.json"
|
|
|
|
| 47 |
"scripts/**/*.js"
|
| 48 |
],
|
| 49 |
"assets": [
|
| 50 |
+
"public/*.html",
|
| 51 |
+
"public/*.css",
|
| 52 |
+
"public/js/**/*",
|
| 53 |
+
"public/assets/**/*",
|
| 54 |
"src/bin/**/*",
|
| 55 |
".env.example",
|
| 56 |
"config.json"
|
public/assets/bg.jpg
ADDED
|
Git LFS Details
|
public/index.html
CHANGED
|
@@ -4,21 +4,61 @@
|
|
| 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 +82,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 +214,13 @@
|
|
| 175 |
<span class="slider"></span>
|
| 176 |
</label>
|
| 177 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
</div>
|
| 179 |
<div class="form-group compact">
|
| 180 |
<label>系统提示词</label>
|
|
@@ -244,12 +290,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 |
+
/* 防止闪烁的关键样式 - 来自 binary-build */
|
| 31 |
+
html:not(.auth-ready) #loginForm,
|
| 32 |
+
html:not(.auth-ready) #mainContent { visibility: hidden; }
|
| 33 |
+
/* 登录状态显示控制 - 合并两个分支 *
|
| 34 |
+
.logged-in #loginForm { display: none !important; }
|
| 35 |
+
.logged-in #mainContent { display: flex !important; }
|
| 36 |
+
html:not(.logged-in) #mainContent { display: none !important; }
|
| 37 |
+
/* Tab状态管理 - 来自 binary-build */
|
| 38 |
+
html.tab-settings #tokensPage { display: none !important; }
|
| 39 |
+
html.tab-settings #settingsPage { display: block !important; }
|
| 40 |
+
html.tab-settings .tab[data-tab="tokens"] {
|
| 41 |
+
background: transparent !important;
|
| 42 |
+
color: var(--text-light, #888) !important;
|
| 43 |
+
}
|
| 44 |
+
html.tab-settings .tab[data-tab="settings"] {
|
| 45 |
+
background: var(--primary, #4f46e5) !important;
|
| 46 |
+
color: white !important;
|
| 47 |
+
}
|
| 48 |
</style>
|
| 49 |
+
<!-- 主样式表 - 优先加载 -->
|
| 50 |
+
<link rel="stylesheet" href="style.css">
|
| 51 |
+
<!-- 预连接字体服务器 -->
|
| 52 |
+
<link rel="preconnect" href="https://font.sec.miui.com" crossorigin>
|
| 53 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
|
| 54 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 55 |
+
<!-- 字体异步加载 - 不阻塞渲染 -->
|
| 56 |
+
<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'">
|
| 57 |
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Ubuntu+Mono:wght@400;700&display=swap" media="print" onload="this.media='all'">
|
| 58 |
+
<noscript>
|
| 59 |
+
<link rel="stylesheet" href="https://font.sec.miui.com/font/css?family=MiSans:400,500,600,700:Chinese_Simplify,Latin&display=swap">
|
| 60 |
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Ubuntu+Mono:wght@400;700&display=swap">
|
| 61 |
+
</noscript>
|
| 62 |
</head>
|
| 63 |
<body>
|
| 64 |
<div class="container">
|
|
|
|
| 82 |
<div id="mainContent" class="main-content hidden">
|
| 83 |
<div class="header">
|
| 84 |
<div class="tabs">
|
| 85 |
+
<button class="tab active" data-tab="tokens" onclick="switchTab('tokens')">🎯 Token</button>
|
| 86 |
+
<button class="tab" data-tab="settings" onclick="switchTab('settings')">⚙️ 设置</button>
|
| 87 |
</div>
|
| 88 |
<div class="header-right">
|
|
|
|
| 89 |
<button onclick="logout()">🚪 退出</button>
|
| 90 |
</div>
|
| 91 |
</div>
|
|
|
|
| 214 |
<span class="slider"></span>
|
| 215 |
</label>
|
| 216 |
</div>
|
| 217 |
+
<div class="form-group compact switch-group">
|
| 218 |
+
<label>透传签名 <span class="help-tip" data-tooltip="将响应中的thoughtSignature透传到客户端">?</span></label>
|
| 219 |
+
<label class="switch">
|
| 220 |
+
<input type="checkbox" name="PASS_SIGNATURE_TO_CLIENT">
|
| 221 |
+
<span class="slider"></span>
|
| 222 |
+
</label>
|
| 223 |
+
</div>
|
| 224 |
</div>
|
| 225 |
<div class="form-group compact">
|
| 226 |
<label>系统提示词</label>
|
|
|
|
| 290 |
</div>
|
| 291 |
|
| 292 |
<!-- 按依赖顺序加载模块 -->
|
| 293 |
+
<script src="js/utils.js" defer></script>
|
| 294 |
+
<script src="js/ui.js" defer></script>
|
| 295 |
+
<script src="js/auth.js" defer></script>
|
| 296 |
+
<script src="js/quota.js" defer></script>
|
| 297 |
+
<script src="js/tokens.js" defer></script>
|
| 298 |
+
<script src="js/config.js" defer></script>
|
| 299 |
+
<script src="js/main.js" defer></script>
|
| 300 |
</body>
|
| 301 |
</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;
|
|
@@ -102,6 +103,11 @@ body {
|
|
| 102 |
font-weight: 400;
|
| 103 |
}
|
| 104 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
/* 确保所有元素继承字体 */
|
| 106 |
*, *::before, *::after {
|
| 107 |
font-family: inherit;
|
|
@@ -161,16 +167,16 @@ label {
|
|
| 161 |
color: var(--text);
|
| 162 |
font-size: 0.875rem;
|
| 163 |
}
|
| 164 |
-
input, select, textarea {
|
| 165 |
-
width: 100%;
|
| 166 |
-
min-height: 40px;
|
| 167 |
-
padding: 0.5rem 0.75rem
|
| 168 |
-
border: 1.5px solid var(--border);
|
| 169 |
-
border-radius: 0.5rem;
|
| 170 |
-
font-size: 0.875rem;
|
| 171 |
-
background: var(--card);
|
| 172 |
-
color: var(--text);
|
| 173 |
-
transition: all 0.2s;
|
| 174 |
}
|
| 175 |
input:focus, select:focus, textarea:focus {
|
| 176 |
outline: none;
|
|
@@ -493,7 +499,7 @@ button.loading::after {
|
|
| 493 |
.inline-edit-input {
|
| 494 |
flex: 1;
|
| 495 |
min-height: 24px;
|
| 496 |
-
padding: 0.125rem 0.375rem
|
| 497 |
font-size: 0.75rem;
|
| 498 |
border: 1px solid var(--primary);
|
| 499 |
border-radius: 0.25rem;
|
|
@@ -607,13 +613,13 @@ button.loading::after {
|
|
| 607 |
.form-group.compact input,
|
| 608 |
.form-group.compact select {
|
| 609 |
min-height: 36px;
|
| 610 |
-
padding: 0.375rem 0.5rem
|
| 611 |
font-size: 0.8rem;
|
| 612 |
}
|
| 613 |
.form-group.compact textarea {
|
| 614 |
min-height: 60px;
|
| 615 |
max-height: 300px;
|
| 616 |
-
padding: 0.375rem 0.5rem
|
| 617 |
font-size: 0.8rem;
|
| 618 |
resize: vertical;
|
| 619 |
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;
|
|
|
|
| 103 |
font-weight: 400;
|
| 104 |
}
|
| 105 |
|
| 106 |
+
/* 字体加载完成后应用 */
|
| 107 |
+
.fonts-loaded body {
|
| 108 |
+
font-family: 'Ubuntu Mono', 'MiSans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
/* 确保所有元素继承字体 */
|
| 112 |
*, *::before, *::after {
|
| 113 |
font-family: inherit;
|
|
|
|
| 167 |
color: var(--text);
|
| 168 |
font-size: 0.875rem;
|
| 169 |
}
|
| 170 |
+
input, select, textarea {
|
| 171 |
+
width: 100%;
|
| 172 |
+
min-height: 40px;
|
| 173 |
+
padding: 0.5rem 0.75rem;
|
| 174 |
+
border: 1.5px solid var(--border);
|
| 175 |
+
border-radius: 0.5rem;
|
| 176 |
+
font-size: 0.875rem;
|
| 177 |
+
background: var(--card);
|
| 178 |
+
color: var(--text);
|
| 179 |
+
transition: all 0.2s;
|
| 180 |
}
|
| 181 |
input:focus, select:focus, textarea:focus {
|
| 182 |
outline: none;
|
|
|
|
| 499 |
.inline-edit-input {
|
| 500 |
flex: 1;
|
| 501 |
min-height: 24px;
|
| 502 |
+
padding: 0.125rem 0.375rem;
|
| 503 |
font-size: 0.75rem;
|
| 504 |
border: 1px solid var(--primary);
|
| 505 |
border-radius: 0.25rem;
|
|
|
|
| 613 |
.form-group.compact input,
|
| 614 |
.form-group.compact select {
|
| 615 |
min-height: 36px;
|
| 616 |
+
padding: 0.375rem 0.5rem;
|
| 617 |
font-size: 0.8rem;
|
| 618 |
}
|
| 619 |
.form-group.compact textarea {
|
| 620 |
min-height: 60px;
|
| 621 |
max-height: 300px;
|
| 622 |
+
padding: 0.375rem 0.5rem;
|
| 623 |
font-size: 0.8rem;
|
| 624 |
resize: vertical;
|
| 625 |
height: auto;
|
scripts/build.js
CHANGED
|
@@ -98,8 +98,9 @@ const pkgJson = {
|
|
| 98 |
pkg: {
|
| 99 |
assets: [
|
| 100 |
toSlash(path.join(rootDir, 'public', '*.html')),
|
| 101 |
-
toSlash(path.join(rootDir, 'public', '*.js')),
|
| 102 |
toSlash(path.join(rootDir, 'public', '*.css')),
|
|
|
|
|
|
|
| 103 |
toSlash(path.join(rootDir, 'src', 'bin', '*'))
|
| 104 |
]
|
| 105 |
}
|
|
@@ -189,18 +190,12 @@ try {
|
|
| 189 |
if (fs.existsSync(publicDestDir)) {
|
| 190 |
fs.rmSync(publicDestDir, { recursive: true, force: true });
|
| 191 |
}
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
const stat = fs.statSync(srcPath);
|
| 199 |
-
if (stat.isFile()) {
|
| 200 |
-
fs.copyFileSync(srcPath, destPath);
|
| 201 |
-
} else if (stat.isDirectory()) {
|
| 202 |
-
fs.cpSync(srcPath, destPath, { recursive: true });
|
| 203 |
-
}
|
| 204 |
}
|
| 205 |
console.log(' ✓ Copied public directory');
|
| 206 |
}
|
|
|
|
| 98 |
pkg: {
|
| 99 |
assets: [
|
| 100 |
toSlash(path.join(rootDir, 'public', '*.html')),
|
|
|
|
| 101 |
toSlash(path.join(rootDir, 'public', '*.css')),
|
| 102 |
+
toSlash(path.join(rootDir, 'public', 'js', '*.js')),
|
| 103 |
+
toSlash(path.join(rootDir, 'public', 'assets', '*')),
|
| 104 |
toSlash(path.join(rootDir, 'src', 'bin', '*'))
|
| 105 |
]
|
| 106 |
}
|
|
|
|
| 190 |
if (fs.existsSync(publicDestDir)) {
|
| 191 |
fs.rmSync(publicDestDir, { recursive: true, force: true });
|
| 192 |
}
|
| 193 |
+
// 直接全复制 public 目录
|
| 194 |
+
fs.cpSync(publicSrcDir, publicDestDir, { recursive: true });
|
| 195 |
+
// 删除 images 目录(运行时生成,不需要打包)
|
| 196 |
+
const imagesDir = path.join(publicDestDir, 'images');
|
| 197 |
+
if (fs.existsSync(imagesDir)) {
|
| 198 |
+
fs.rmSync(imagesDir, { recursive: true, force: true });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
}
|
| 200 |
console.log(' ✓ Copied public directory');
|
| 201 |
}
|
src/api/client.js
CHANGED
|
@@ -1,9 +1,19 @@
|
|
| 1 |
import tokenManager from '../auth/token_manager.js';
|
| 2 |
import config from '../config/config.js';
|
| 3 |
-
import { generateToolCallId } from '../utils/idGenerator.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, registerMemoryPoolCleanup } from '../utils/memoryManager.js';
|
| 8 |
import { buildAxiosRequestConfig, httpRequest, httpStreamRequest } from '../utils/httpClient.js';
|
| 9 |
import { setReasoningSignature, setToolSignature } from '../utils/thoughtSignatureCache.js';
|
|
@@ -16,7 +26,7 @@ let useAxios = false;
|
|
| 16 |
// ==================== 模型列表缓存(智能管理) ====================
|
| 17 |
// 缓存过期时间根据内存压力动态调整
|
| 18 |
const getModelCacheTTL = () => {
|
| 19 |
-
const baseTTL = config.cache?.modelListTTL ||
|
| 20 |
const pressure = memoryManager.currentPressure;
|
| 21 |
// 高压力时缩短缓存时间
|
| 22 |
if (pressure === MemoryPressure.CRITICAL) return Math.min(baseTTL, 5 * 60 * 1000);
|
|
@@ -71,75 +81,10 @@ if (config.useNativeAxios === true) {
|
|
| 71 |
}
|
| 72 |
}
|
| 73 |
|
| 74 |
-
//
|
| 75 |
-
|
| 76 |
-
// 预编译的常量(避免重复创建字符串)
|
| 77 |
-
const DATA_PREFIX = 'data: ';
|
| 78 |
-
const DATA_PREFIX_LEN = DATA_PREFIX.length;
|
| 79 |
-
|
| 80 |
-
// 高效的行分割器(零拷贝,避免 split 创建新数组)
|
| 81 |
-
// 使用对象池复用 LineBuffer 实例
|
| 82 |
-
class LineBuffer {
|
| 83 |
-
constructor() {
|
| 84 |
-
this.buffer = '';
|
| 85 |
-
this.lines = [];
|
| 86 |
-
}
|
| 87 |
-
|
| 88 |
-
// 追加数据并返回完整的行
|
| 89 |
-
append(chunk) {
|
| 90 |
-
this.buffer += chunk;
|
| 91 |
-
this.lines.length = 0; // 重用数组
|
| 92 |
-
|
| 93 |
-
let start = 0;
|
| 94 |
-
let end;
|
| 95 |
-
while ((end = this.buffer.indexOf('\n', start)) !== -1) {
|
| 96 |
-
this.lines.push(this.buffer.slice(start, end));
|
| 97 |
-
start = end + 1;
|
| 98 |
-
}
|
| 99 |
-
|
| 100 |
-
// 保留未完成的部分
|
| 101 |
-
this.buffer = start < this.buffer.length ? this.buffer.slice(start) : '';
|
| 102 |
-
return this.lines;
|
| 103 |
-
}
|
| 104 |
-
|
| 105 |
-
// 清空缓冲区(用于归还到池之前)
|
| 106 |
-
clear() {
|
| 107 |
-
this.buffer = '';
|
| 108 |
-
this.lines.length = 0;
|
| 109 |
-
}
|
| 110 |
-
}
|
| 111 |
-
|
| 112 |
-
// LineBuffer 对象池
|
| 113 |
-
const lineBufferPool = [];
|
| 114 |
-
const getLineBuffer = () => {
|
| 115 |
-
const buffer = lineBufferPool.pop();
|
| 116 |
-
if (buffer) {
|
| 117 |
-
buffer.clear();
|
| 118 |
-
return buffer;
|
| 119 |
-
}
|
| 120 |
-
return new LineBuffer();
|
| 121 |
-
};
|
| 122 |
-
const releaseLineBuffer = (buffer) => {
|
| 123 |
-
const maxSize = memoryManager.getPoolSizes().lineBuffer;
|
| 124 |
-
if (lineBufferPool.length < maxSize) {
|
| 125 |
-
buffer.clear();
|
| 126 |
-
lineBufferPool.push(buffer);
|
| 127 |
-
}
|
| 128 |
-
};
|
| 129 |
-
|
| 130 |
-
// 对象池:复用 toolCall 对象
|
| 131 |
-
const toolCallPool = [];
|
| 132 |
-
const getToolCallObject = () => toolCallPool.pop() || { id: '', type: 'function', function: { name: '', arguments: '' } };
|
| 133 |
-
const releaseToolCallObject = (obj) => {
|
| 134 |
-
const maxSize = memoryManager.getPoolSizes().toolCall;
|
| 135 |
-
if (toolCallPool.length < maxSize) toolCallPool.push(obj);
|
| 136 |
-
};
|
| 137 |
-
|
| 138 |
-
// 注册内存清理回调
|
| 139 |
function registerMemoryCleanup() {
|
| 140 |
-
//
|
| 141 |
-
|
| 142 |
-
registerMemoryPoolCleanup(lineBufferPool, () => memoryManager.getPoolSizes().lineBuffer);
|
| 143 |
|
| 144 |
memoryManager.registerCleanup((pressure) => {
|
| 145 |
// 高压力或紧急时清理模型缓存
|
|
@@ -153,7 +98,6 @@ function registerMemoryCleanup() {
|
|
| 153 |
}
|
| 154 |
}
|
| 155 |
|
| 156 |
-
// 紧急时强制清理模型缓存
|
| 157 |
if (pressure === MemoryPressure.CRITICAL && modelListCache) {
|
| 158 |
modelListCache = null;
|
| 159 |
modelListCacheTime = 0;
|
|
@@ -188,14 +132,6 @@ function buildRequesterConfig(headers, body = null) {
|
|
| 188 |
return reqConfig;
|
| 189 |
}
|
| 190 |
|
| 191 |
-
// 统一构造上游 API 错误对象,方便服务器层识别并透传
|
| 192 |
-
function createApiError(message, status, rawBody) {
|
| 193 |
-
const err = new Error(message);
|
| 194 |
-
err.status = status;
|
| 195 |
-
err.rawBody = rawBody;
|
| 196 |
-
err.isUpstreamApiError = true;
|
| 197 |
-
return err;
|
| 198 |
-
}
|
| 199 |
|
| 200 |
// 统一错误处理
|
| 201 |
async function handleApiError(error, token) {
|
|
@@ -225,88 +161,6 @@ async function handleApiError(error, token) {
|
|
| 225 |
throw createApiError(`API请求失败 (${status}): ${errorBody}`, status, errorBody);
|
| 226 |
}
|
| 227 |
|
| 228 |
-
// 转换 functionCall 为 OpenAI 格式(使用对象池)
|
| 229 |
-
// 会尝试将安全工具名还原为原始工具名
|
| 230 |
-
function convertToToolCall(functionCall, sessionId, model) {
|
| 231 |
-
const toolCall = getToolCallObject();
|
| 232 |
-
toolCall.id = functionCall.id || generateToolCallId();
|
| 233 |
-
let name = functionCall.name;
|
| 234 |
-
if (sessionId && model) {
|
| 235 |
-
const original = getOriginalToolName(sessionId, model, functionCall.name);
|
| 236 |
-
if (original) name = original;
|
| 237 |
-
}
|
| 238 |
-
toolCall.function.name = name;
|
| 239 |
-
toolCall.function.arguments = JSON.stringify(functionCall.args);
|
| 240 |
-
return toolCall;
|
| 241 |
-
}
|
| 242 |
-
|
| 243 |
-
// 解析并发送流式响应片段(会修改 state 并触发 callback)
|
| 244 |
-
// 支持 DeepSeek 格式:思维链内容通过 reasoning_content 字段输出
|
| 245 |
-
// 同时透传 thoughtSignature,方便客户端后续复用
|
| 246 |
-
function parseAndEmitStreamChunk(line, state, callback) {
|
| 247 |
-
if (!line.startsWith(DATA_PREFIX)) return;
|
| 248 |
-
|
| 249 |
-
try {
|
| 250 |
-
const data = JSON.parse(line.slice(DATA_PREFIX_LEN));
|
| 251 |
-
//console.log(JSON.stringify(data));
|
| 252 |
-
const parts = data.response?.candidates?.[0]?.content?.parts;
|
| 253 |
-
|
| 254 |
-
if (parts) {
|
| 255 |
-
for (const part of parts) {
|
| 256 |
-
if (part.thought === true) {
|
| 257 |
-
// 思维链内容 - 使用 DeepSeek 格式的 reasoning_content
|
| 258 |
-
// 缓存最新的签名,方便后续片段缺省时复用,并写入全局缓存
|
| 259 |
-
if (part.thoughtSignature) {
|
| 260 |
-
state.reasoningSignature = part.thoughtSignature;
|
| 261 |
-
if (state.sessionId && state.model) {
|
| 262 |
-
setReasoningSignature(state.sessionId, state.model, part.thoughtSignature);
|
| 263 |
-
}
|
| 264 |
-
}
|
| 265 |
-
callback({
|
| 266 |
-
type: 'reasoning',
|
| 267 |
-
reasoning_content: part.text || '',
|
| 268 |
-
thoughtSignature: part.thoughtSignature || state.reasoningSignature || null
|
| 269 |
-
});
|
| 270 |
-
} else if (part.text !== undefined) {
|
| 271 |
-
// 普通文本内容
|
| 272 |
-
callback({ type: 'text', content: part.text });
|
| 273 |
-
} else if (part.functionCall) {
|
| 274 |
-
// 工具调用,透传工具签名,并写入全局缓存
|
| 275 |
-
const toolCall = convertToToolCall(part.functionCall, state.sessionId, state.model);
|
| 276 |
-
if (part.thoughtSignature) {
|
| 277 |
-
toolCall.thoughtSignature = part.thoughtSignature;
|
| 278 |
-
if (state.sessionId && state.model) {
|
| 279 |
-
setToolSignature(state.sessionId, state.model, part.thoughtSignature);
|
| 280 |
-
}
|
| 281 |
-
}
|
| 282 |
-
state.toolCalls.push(toolCall);
|
| 283 |
-
}
|
| 284 |
-
}
|
| 285 |
-
}
|
| 286 |
-
|
| 287 |
-
// 响应结束时发送工具调用和使用统计
|
| 288 |
-
if (data.response?.candidates?.[0]?.finishReason) {
|
| 289 |
-
if (state.toolCalls.length > 0) {
|
| 290 |
-
callback({ type: 'tool_calls', tool_calls: state.toolCalls });
|
| 291 |
-
state.toolCalls = [];
|
| 292 |
-
}
|
| 293 |
-
// 提取 token 使用统计
|
| 294 |
-
const usage = data.response?.usageMetadata;
|
| 295 |
-
if (usage) {
|
| 296 |
-
callback({
|
| 297 |
-
type: 'usage',
|
| 298 |
-
usage: {
|
| 299 |
-
prompt_tokens: usage.promptTokenCount || 0,
|
| 300 |
-
completion_tokens: usage.candidatesTokenCount || 0,
|
| 301 |
-
total_tokens: usage.totalTokenCount || 0
|
| 302 |
-
}
|
| 303 |
-
});
|
| 304 |
-
}
|
| 305 |
-
}
|
| 306 |
-
} catch (e) {
|
| 307 |
-
// 忽略 JSON 解析错误
|
| 308 |
-
}
|
| 309 |
-
}
|
| 310 |
|
| 311 |
// ==================== 导出函数 ====================
|
| 312 |
|
|
|
|
| 1 |
import tokenManager from '../auth/token_manager.js';
|
| 2 |
import config from '../config/config.js';
|
|
|
|
| 3 |
import AntigravityRequester from '../AntigravityRequester.js';
|
| 4 |
import { saveBase64Image } from '../utils/imageStorage.js';
|
| 5 |
import logger from '../utils/logger.js';
|
| 6 |
+
import memoryManager, { MemoryPressure } from '../utils/memoryManager.js';
|
| 7 |
+
import { buildAxiosRequestConfig } from '../utils/httpClient.js';
|
| 8 |
+
import { MODEL_LIST_CACHE_TTL } from '../constants/index.js';
|
| 9 |
+
import { createApiError, UpstreamApiError } from '../utils/errors.js';
|
| 10 |
+
import {
|
| 11 |
+
getLineBuffer,
|
| 12 |
+
releaseLineBuffer,
|
| 13 |
+
parseAndEmitStreamChunk,
|
| 14 |
+
convertToToolCall,
|
| 15 |
+
registerStreamMemoryCleanup
|
| 16 |
+
} from './stream_parser.js';
|
| 17 |
import memoryManager, { MemoryPressure, registerMemoryPoolCleanup } from '../utils/memoryManager.js';
|
| 18 |
import { buildAxiosRequestConfig, httpRequest, httpStreamRequest } from '../utils/httpClient.js';
|
| 19 |
import { setReasoningSignature, setToolSignature } from '../utils/thoughtSignatureCache.js';
|
|
|
|
| 26 |
// ==================== 模型列表缓存(智能管理) ====================
|
| 27 |
// 缓存过期时间根据内存压力动态调整
|
| 28 |
const getModelCacheTTL = () => {
|
| 29 |
+
const baseTTL = config.cache?.modelListTTL || MODEL_LIST_CACHE_TTL;
|
| 30 |
const pressure = memoryManager.currentPressure;
|
| 31 |
// 高压力时缩短缓存时间
|
| 32 |
if (pressure === MemoryPressure.CRITICAL) return Math.min(baseTTL, 5 * 60 * 1000);
|
|
|
|
| 81 |
}
|
| 82 |
}
|
| 83 |
|
| 84 |
+
// 注册对象池与模型缓存的内存清理回调
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
function registerMemoryCleanup() {
|
| 86 |
+
// 由流式解析模块管理自身对象池大小
|
| 87 |
+
registerStreamMemoryCleanup();
|
|
|
|
| 88 |
|
| 89 |
memoryManager.registerCleanup((pressure) => {
|
| 90 |
// 高压力或紧急时清理模型缓存
|
|
|
|
| 98 |
}
|
| 99 |
}
|
| 100 |
|
|
|
|
| 101 |
if (pressure === MemoryPressure.CRITICAL && modelListCache) {
|
| 102 |
modelListCache = null;
|
| 103 |
modelListCacheTime = 0;
|
|
|
|
| 132 |
return reqConfig;
|
| 133 |
}
|
| 134 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
|
| 136 |
// 统一错误处理
|
| 137 |
async function handleApiError(error, token) {
|
|
|
|
| 161 |
throw createApiError(`API请求失败 (${status}): ${errorBody}`, status, errorBody);
|
| 162 |
}
|
| 163 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
|
| 165 |
// ==================== 导出函数 ====================
|
| 166 |
|
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/oauth_manager.js
CHANGED
|
@@ -82,7 +82,7 @@ class OAuthManager {
|
|
| 82 |
if (config.skipProjectIdFetch) {
|
| 83 |
const projectId = generateProjectId();
|
| 84 |
log.info('已跳过API验证,使用随机生成的projectId: ' + projectId);
|
| 85 |
-
return { projectId, hasQuota:
|
| 86 |
}
|
| 87 |
|
| 88 |
// 尝试从API获取projectId
|
|
|
|
| 82 |
if (config.skipProjectIdFetch) {
|
| 83 |
const projectId = generateProjectId();
|
| 84 |
log.info('已跳过API验证,使用随机生成的projectId: ' + projectId);
|
| 85 |
+
return { projectId, hasQuota: true };
|
| 86 |
}
|
| 87 |
|
| 88 |
// 尝试从API获取projectId
|
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, generateGeminiRequestBody, generateClaudeRequestBody, prepareImageRequest } from '../utils/utils.js';
|
| 8 |
import logger from '../utils/logger.js';
|
|
@@ -11,53 +9,12 @@ import tokenManager from '../auth/token_manager.js';
|
|
| 11 |
import adminRouter from '../routes/admin.js';
|
| 12 |
import sdRouter from '../routes/sd.js';
|
| 13 |
import memoryManager, { registerMemoryPoolCleanup } from '../utils/memoryManager.js';
|
| 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 |
// Gemini 兼容错误响应构造
|
| 206 |
const buildGeminiErrorPayload = (error, statusCode) => {
|
|
@@ -278,12 +198,8 @@ app.use(express.static(publicDir));
|
|
| 278 |
// 管理路由
|
| 279 |
app.use('/admin', adminRouter);
|
| 280 |
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
return res.status(413).json({ error: `请求体过大,最大支持 ${config.security.maxRequestSize}` });
|
| 284 |
-
}
|
| 285 |
-
next(err);
|
| 286 |
-
});
|
| 287 |
|
| 288 |
app.use((req, res, next) => {
|
| 289 |
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'];
|
|
@@ -390,13 +306,21 @@ app.post('/v1/chat/completions', async (req, res) => {
|
|
| 390 |
usageData = data.usage;
|
| 391 |
} else if (data.type === 'reasoning') {
|
| 392 |
const delta = { reasoning_content: data.reasoning_content };
|
| 393 |
-
if (data.thoughtSignature) {
|
| 394 |
delta.thoughtSignature = data.thoughtSignature;
|
| 395 |
}
|
| 396 |
writeStreamData(res, createStreamChunk(id, created, model, delta));
|
| 397 |
} else if (data.type === 'tool_calls') {
|
| 398 |
hasToolCall = true;
|
| 399 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
const delta = { tool_calls: toolCallsWithIndex };
|
| 401 |
writeStreamData(res, createStreamChunk(id, created, model, delta));
|
| 402 |
} else {
|
|
@@ -430,9 +354,16 @@ app.post('/v1/chat/completions', async (req, res) => {
|
|
| 430 |
// DeepSeek 格式:reasoning_content 在 content 之前
|
| 431 |
const message = { role: 'assistant' };
|
| 432 |
if (reasoningContent) message.reasoning_content = reasoningContent;
|
| 433 |
-
if (reasoningSignature) message.thoughtSignature = reasoningSignature;
|
| 434 |
message.content = content;
|
| 435 |
-
if (toolCalls.length > 0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 436 |
|
| 437 |
// 使用预构建的响应对象,减少内存分配
|
| 438 |
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, generateGeminiRequestBody, generateClaudeRequestBody, prepareImageRequest } from '../utils/utils.js';
|
| 6 |
import logger from '../utils/logger.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 |
// Gemini 兼容错误响应构造
|
| 126 |
const buildGeminiErrorPayload = (error, statusCode) => {
|
|
|
|
| 198 |
// 管理路由
|
| 199 |
app.use('/admin', adminRouter);
|
| 200 |
|
| 201 |
+
// 使用统一错误处理中间件
|
| 202 |
+
app.use(errorHandler);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
|
| 204 |
app.use((req, res, next) => {
|
| 205 |
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'];
|
|
|
|
| 306 |
usageData = data.usage;
|
| 307 |
} else if (data.type === 'reasoning') {
|
| 308 |
const delta = { reasoning_content: data.reasoning_content };
|
| 309 |
+
if (data.thoughtSignature && config.passSignatureToClient) {
|
| 310 |
delta.thoughtSignature = data.thoughtSignature;
|
| 311 |
}
|
| 312 |
writeStreamData(res, createStreamChunk(id, created, model, delta));
|
| 313 |
} else if (data.type === 'tool_calls') {
|
| 314 |
hasToolCall = true;
|
| 315 |
+
// 根据配置决定是否透传工具调用中的签名
|
| 316 |
+
const toolCallsWithIndex = data.tool_calls.map((toolCall, index) => {
|
| 317 |
+
if (config.passSignatureToClient) {
|
| 318 |
+
return { index, ...toolCall };
|
| 319 |
+
} else {
|
| 320 |
+
const { thoughtSignature, ...rest } = toolCall;
|
| 321 |
+
return { index, ...rest };
|
| 322 |
+
}
|
| 323 |
+
});
|
| 324 |
const delta = { tool_calls: toolCallsWithIndex };
|
| 325 |
writeStreamData(res, createStreamChunk(id, created, model, delta));
|
| 326 |
} else {
|
|
|
|
| 354 |
// DeepSeek 格式:reasoning_content 在 content 之前
|
| 355 |
const message = { role: 'assistant' };
|
| 356 |
if (reasoningContent) message.reasoning_content = reasoningContent;
|
| 357 |
+
if (reasoningSignature && config.passSignatureToClient) message.thoughtSignature = reasoningSignature;
|
| 358 |
message.content = content;
|
| 359 |
+
if (toolCalls.length > 0) {
|
| 360 |
+
// 根据配置决定是否透传工具调用中的签名
|
| 361 |
+
if (config.passSignatureToClient) {
|
| 362 |
+
message.tool_calls = toolCalls;
|
| 363 |
+
} else {
|
| 364 |
+
message.tool_calls = toolCalls.map(({ thoughtSignature, ...rest }) => rest);
|
| 365 |
+
}
|
| 366 |
+
}
|
| 367 |
|
| 368 |
// 使用预构建的响应对象,减少内存分配
|
| 369 |
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
|
@@ -865,4 +865,4 @@ export{
|
|
| 865 |
generateClaudeRequestBody,
|
| 866 |
prepareImageRequest,
|
| 867 |
getDefaultIp
|
| 868 |
-
}
|
|
|
|
| 865 |
generateClaudeRequestBody,
|
| 866 |
prepareImageRequest,
|
| 867 |
getDefaultIp
|
| 868 |
+
}
|