liuw1535 commited on
Commit
08f35a8
·
unverified ·
2 Parent(s): bae8863 70217c5

Merge pull request #67 from ZhaoShanGeng/binary-build

Browse files
.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,默认 16000(0 表示关闭思考预算限制) |
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 // 使用原生 axios 而非 AntigravityRequester
 
 
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
- │ │ └── client.js # API 调用逻辑(含模型列表缓存)
 
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
- │ │ └── utils.js # 工具函数
 
 
 
 
 
 
 
 
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": "round_robin",
11
  "requestCount": 50
12
  },
13
  "api": {
@@ -32,6 +32,7 @@
32
  "retryTimes": 3,
33
  "skipProjectIdFetch": false,
34
  "useNativeAxios": false,
35
- "useContextSystemPrompt": false
 
36
  }
37
  }
 
7
  "memoryThreshold": 25
8
  },
9
  "rotation": {
10
+ "strategy": "request_count",
11
  "requestCount": 50
12
  },
13
  "api": {
 
32
  "retryTimes": 3,
33
  "skipProjectIdFetch": false,
34
  "useNativeAxios": false,
35
+ "useContextSystemPrompt": false,
36
+ "passSignatureToClient": false
37
  }
38
  }
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

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