lin7zhi commited on
Commit
97ec0e5
·
verified ·
1 Parent(s): 997fcbc

Upload folder using huggingface_hub

Browse files
.dockerignore ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ node_modules
2
+ npm-debug.log
3
+ .git
4
+ .gitignore
5
+ README.md
6
+ .env
7
+ .nyc_output
8
+ coverage
9
+ .DS_Store
10
+ data
11
+ public/images/*.jpg
12
+ public/images/*.png
13
+ public/images/*.gif
14
+ public/images/*.webp
15
+ test
.env.example ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # 敏感配置(只在 .env 中配置)
2
+ API_KEY=sk-text
3
+ ADMIN_USERNAME=admin
4
+ ADMIN_PASSWORD=admin123
5
+ JWT_SECRET=your-jwt-secret-key-change-this-in-production
6
+
7
+ # 可选配置
8
+ # PROXY=http://127.0.0.1:7897
9
+ SYSTEM_INSTRUCTION=你是聊天机器人,名字叫萌萌,如同名字这般,你的性格是软软糯糯萌萌哒的,专门为用户提供聊天和情绪价值,协助进行小说创作或者角色扮演
10
+ # IMAGE_BASE_URL=http://your-domain.com
.gitattributes CHANGED
@@ -33,3 +33,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ src/bin/antigravity_requester_android_arm64 filter=lfs diff=lfs merge=lfs -text
37
+ src/bin/antigravity_requester_linux_amd64 filter=lfs diff=lfs merge=lfs -text
38
+ src/bin/antigravity_requester_windows_amd64.exe filter=lfs diff=lfs merge=lfs -text
.github/workflows/docker-publish.yml ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Docker Build and Push
2
+
3
+ on:
4
+ push:
5
+ branches: [ main, master ]
6
+ tags: [ 'v*' ]
7
+ pull_request:
8
+ branches: [ main, master ]
9
+
10
+ env:
11
+ REGISTRY: ghcr.io
12
+ IMAGE_NAME: ${{ github.repository }}
13
+
14
+ jobs:
15
+ build:
16
+ runs-on: ubuntu-latest
17
+ permissions:
18
+ contents: read
19
+ packages: write
20
+
21
+ steps:
22
+ - name: Checkout
23
+ uses: actions/checkout@v4
24
+
25
+ - name: Set up Docker Buildx
26
+ uses: docker/setup-buildx-action@v3
27
+
28
+ - name: Log in to GitHub Container Registry
29
+ uses: docker/login-action@v3
30
+ with:
31
+ registry: ${{ env.REGISTRY }}
32
+ username: ${{ github.actor }}
33
+ password: ${{ secrets.GITHUB_TOKEN }}
34
+
35
+ - name: Extract metadata
36
+ id: meta
37
+ uses: docker/metadata-action@v5
38
+ with:
39
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
40
+ tags: |
41
+ type=ref,event=branch
42
+ type=ref,event=pr
43
+ type=semver,pattern={{version}}
44
+ type=semver,pattern={{major}}.{{minor}}
45
+ type=raw,value=latest,enable={{is_default_branch}}
46
+
47
+ - name: Build and push
48
+ uses: docker/build-push-action@v5
49
+ with:
50
+ context: .
51
+ push: true
52
+ tags: ${{ steps.meta.outputs.tags }}
53
+ labels: ${{ steps.meta.outputs.labels }}
54
+ cache-from: type=gha
55
+ cache-to: type=gha,mode=max
.gitignore ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Node.js
2
+ node_modules/
3
+
4
+ # 账户信息
5
+ # accounts.json
6
+
7
+ # 日志
8
+ *.log
9
+ logs/
10
+
11
+ # 环境变量
12
+ .env
13
+ .env.local
14
+
15
+ # 系统文件
16
+ .DS_Store
17
+ Thumbs.db
18
+
19
+ data/
20
+ test/*.png
21
+ test/*.jpeg
22
+ public/images
Dockerfile ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:18-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ # 复制 package.json 和 package-lock.json
6
+ COPY package*.json ./
7
+
8
+ # 安装依赖
9
+ RUN npm ci --only=production
10
+
11
+ # 复制源代码
12
+ COPY . .
13
+
14
+ # 复制 .env.example 为默认 .env
15
+ RUN cp .env.example .env
16
+
17
+ # 创建数据和图片目录
18
+ RUN mkdir -p data public/images
19
+
20
+ # 暴露端口
21
+ EXPOSE 8045
22
+
23
+ # 启动应用
24
+ CMD ["sh", "-c", "node src/config/init-env.js && npm start"]
LICENSE ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## 📜 许可证
2
+ 本项目采用 CC BY-NC-SA 4.0 协议,仅供学习使用,禁止商用
3
+
4
+ 知识共享 署名-非商业性使用-相同方式共享 4.0 国际许可协议
5
+ Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
6
+
7
+ 版权所有 © 2025 [liuw1535]
8
+
9
+ 本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
10
+
11
+ 您可以自由地:
12
+ ✓ 共享 — 在任何媒介以任何形式复制、发行本作品
13
+ ✓ 演绎 — 修改、转换或以本作品为基础进行创作
14
+
15
+ 惟须遵守下列条件:
16
+ ✓ 署名 — 您必须给出适当的署名,提供指向本许可协议的链接
17
+ ✓ 非商业性使用 — 您不得将本作品用于商业目的
18
+ ✓ 相同方式共享 — 如果您再混合、转换或者基于本作品进行创作,
19
+ 您必须基于与原先许可协议相同的许可协议分发您贡献的作品
20
+
21
+ 完整协议文本请访问:
22
+ https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.zh-Hans
23
+
README.md CHANGED
@@ -1,10 +1,15 @@
1
  ---
2
- title: Antigravity2api
3
- emoji: 🚀
4
  colorFrom: blue
5
  colorTo: green
6
  sdk: docker
7
- pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
1
  ---
2
+ title: "antigravity2api"
3
+ emoji: "🚀"
4
  colorFrom: blue
5
  colorTo: green
6
  sdk: docker
7
+ app_port: 8045
8
  ---
9
 
10
+ ### 🚀 一键部署
11
+ [![Deploy with HFSpaceDeploy](https://img.shields.io/badge/Deploy_with-HFSpaceDeploy-green?style=social&logo=rocket)](https://github.com/kfcx/HFSpaceDeploy)
12
+
13
+ 本项目由[HFSpaceDeploy](https://github.com/kfcx/HFSpaceDeploy)一键部署
14
+
15
+
TOKEN_API.md ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Token 管理 API 文档
2
+
3
+ ## 概述
4
+
5
+ 提供对本地 Token 凭证的完整管理功能,支持增删改查和热重载。所有操作都会自动更新内存中的 Token 池。
6
+
7
+ ## 认证
8
+
9
+ 所有 API 请求需要在请求头中包含 API Key:
10
+
11
+ ```
12
+ Authorization: Bearer sk-text // 配置内文件配置
13
+ ```
14
+
15
+ ## 接口列表
16
+
17
+ ### 1. 获取 Token 列表
18
+
19
+ **请求**
20
+ ```bash
21
+ GET /v1/tokens
22
+ ```
23
+
24
+ **响应**
25
+ ```json
26
+ {
27
+ "success": true,
28
+ "data": [
29
+ {
30
+ "refresh_token": "1//xxx",
31
+ "access_token_suffix": "...abc12345",
32
+ "expires_in": 3599,
33
+ "timestamp": 1234567890000,
34
+ "enable": true,
35
+ "projectId": "project-123"
36
+ }
37
+ ]
38
+ }
39
+ ```
40
+
41
+ ### 2. 添加新 Token
42
+
43
+ **请求**
44
+ ```bash
45
+ POST /v1/tokens
46
+ Content-Type: application/json
47
+
48
+ {
49
+ "access_token": "ya29.xxx",
50
+ "refresh_token": "1//xxx",
51
+ "expires_in": 3599
52
+ }
53
+ ```
54
+
55
+ **响应**
56
+ ```json
57
+ {
58
+ "success": true,
59
+ "message": "Token添加成功"
60
+ }
61
+ ```
62
+
63
+ ### 3. 更新 Token
64
+
65
+ **请求**
66
+ ```bash
67
+ PUT /v1/tokens/{refresh_token}
68
+ Content-Type: application/json
69
+
70
+ {
71
+ "enable": false,
72
+ "access_token": "new_token"
73
+ }
74
+ ```
75
+
76
+ **响应**
77
+ ```json
78
+ {
79
+ "success": true,
80
+ "message": "Token更新成功"
81
+ }
82
+ ```
83
+
84
+ ### 4. 删除 Token
85
+
86
+ **请求**
87
+ ```bash
88
+ DELETE /v1/tokens/{refresh_token}
89
+ ```
90
+
91
+ **响应**
92
+ ```json
93
+ {
94
+ "success": true,
95
+ "message": "Token删除成功"
96
+ }
97
+ ```
98
+
99
+ ### 5. 热重载 Token
100
+
101
+ **请求**
102
+ ```bash
103
+ POST /v1/tokens/reload
104
+ ```
105
+
106
+ **响应**
107
+ ```json
108
+ {
109
+ "success": true,
110
+ "message": "Token已热重载"
111
+ }
112
+ ```
113
+
114
+ ## 使用示例
115
+
116
+ ### 查看当前 Token 状态
117
+ ```bash
118
+ curl http://localhost:8045/v1/tokens \
119
+ -H "Authorization: Bearer sk-text"
120
+ ```
121
+
122
+ ### 添加新账号
123
+ ```bash
124
+ curl -X POST http://localhost:8045/v1/tokens \
125
+ -H "Authorization: Bearer sk-text" \
126
+ -H "Content-Type: application/json" \
127
+ -d '{
128
+ "access_token": "ya29.a0ARrdaM...",
129
+ "refresh_token": "1//0GWI4...",
130
+ "expires_in": 3599
131
+ }'
132
+ ```
133
+
134
+ ### 禁用某个账号
135
+ ```bash
136
+ curl -X PUT http://localhost:8045/v1/tokens/1//0GWI4... \
137
+ -H "Authorization: Bearer sk-text" \
138
+ -H "Content-Type: application/json" \
139
+ -d '{"enable": false}'
140
+ ```
141
+
142
+ ### 删除账号
143
+ ```bash
144
+ curl -X DELETE http://localhost:8045/v1/tokens/1//0GWI4... \
145
+ -H "Authorization: Bearer sk-text"
146
+ ```
147
+
148
+ ### 重新加载配置
149
+ ```bash
150
+ curl -X POST http://localhost:8045/v1/tokens/reload \
151
+ -H "Authorization: Bearer sk-text"
152
+ ```
153
+
154
+ ## 注意事项
155
+
156
+ 1. **refresh_token** 作为唯一标识符,不可重复
157
+ 2. 所有操作会立即生效,无需重启服务
158
+ 3. 删除操作不可恢复,请谨慎使用
159
+ 4. Token 过期会自动刷新,无需手动维护
160
+ 5. 禁用的 Token 不会参与轮换,但仍保存在文件中
161
+
162
+ ## 错误码
163
+
164
+ - `400` - 请求参数错误
165
+ - `401` - API Key 验证失败
166
+ - `500` - 服务器内部错误
167
+
168
+ ## 安全建议
169
+
170
+ - 定期备份 `data/accounts.json` 文件
171
+ - 不要在日志中暴露完整的 Token 信息
172
+ - 建议使用 HTTPS 部署生产环境
config.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "server": {
3
+ "port": 8045,
4
+ "host": "0.0.0.0",
5
+ "maxRequestSize": "500mb"
6
+ },
7
+ "api": {},
8
+ "defaults": {
9
+ "temperature": 1,
10
+ "topP": 0.85,
11
+ "topK": 50,
12
+ "maxTokens": 8096
13
+ },
14
+ "other": {
15
+ "timeout": 180000,
16
+ "skipProjectIdFetch": true
17
+ }
18
+ }
docker-compose.yml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ antigravity2api:
5
+ build: .
6
+ ports:
7
+ - "8045:8045"
8
+ environment:
9
+ - API_KEY=${API_KEY:-sk-text}
10
+ - ADMIN_USERNAME=${ADMIN_USERNAME:-admin}
11
+ - ADMIN_PASSWORD=${ADMIN_PASSWORD:-admin123}
12
+ - JWT_SECRET=${JWT_SECRET:-your-jwt-secret-key-change-this-in-production}
13
+ - PROXY=${PROXY:-}
14
+ - SYSTEM_INSTRUCTION=${SYSTEM_INSTRUCTION:-}
15
+ - IMAGE_BASE_URL=${IMAGE_BASE_URL:-}
16
+ volumes:
17
+ - ./data:/app/data
18
+ - ./public/images:/app/public/images
19
+ - ./.env:/app/.env
20
+ - ./config.json:/app/config.json
21
+ restart: unless-stopped
package-lock.json ADDED
@@ -0,0 +1,1104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "antigravity-to-openai",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "antigravity-to-openai",
9
+ "version": "1.0.0",
10
+ "license": "MIT",
11
+ "dependencies": {
12
+ "axios": "^1.13.2",
13
+ "dotenv": "^17.2.3",
14
+ "express": "^5.1.0",
15
+ "jsonwebtoken": "^9.0.2"
16
+ },
17
+ "engines": {
18
+ "node": ">=18.0.0"
19
+ }
20
+ },
21
+ "node_modules/accepts": {
22
+ "version": "2.0.0",
23
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
24
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
25
+ "license": "MIT",
26
+ "dependencies": {
27
+ "mime-types": "^3.0.0",
28
+ "negotiator": "^1.0.0"
29
+ },
30
+ "engines": {
31
+ "node": ">= 0.6"
32
+ }
33
+ },
34
+ "node_modules/asynckit": {
35
+ "version": "0.4.0",
36
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
37
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
38
+ "license": "MIT"
39
+ },
40
+ "node_modules/axios": {
41
+ "version": "1.13.2",
42
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
43
+ "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
44
+ "license": "MIT",
45
+ "dependencies": {
46
+ "follow-redirects": "^1.15.6",
47
+ "form-data": "^4.0.4",
48
+ "proxy-from-env": "^1.1.0"
49
+ }
50
+ },
51
+ "node_modules/body-parser": {
52
+ "version": "2.2.0",
53
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
54
+ "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
55
+ "license": "MIT",
56
+ "dependencies": {
57
+ "bytes": "^3.1.2",
58
+ "content-type": "^1.0.5",
59
+ "debug": "^4.4.0",
60
+ "http-errors": "^2.0.0",
61
+ "iconv-lite": "^0.6.3",
62
+ "on-finished": "^2.4.1",
63
+ "qs": "^6.14.0",
64
+ "raw-body": "^3.0.0",
65
+ "type-is": "^2.0.0"
66
+ },
67
+ "engines": {
68
+ "node": ">=18"
69
+ }
70
+ },
71
+ "node_modules/buffer-equal-constant-time": {
72
+ "version": "1.0.1",
73
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
74
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
75
+ "license": "BSD-3-Clause"
76
+ },
77
+ "node_modules/bytes": {
78
+ "version": "3.1.2",
79
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
80
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
81
+ "license": "MIT",
82
+ "engines": {
83
+ "node": ">= 0.8"
84
+ }
85
+ },
86
+ "node_modules/call-bind-apply-helpers": {
87
+ "version": "1.0.2",
88
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
89
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
90
+ "license": "MIT",
91
+ "dependencies": {
92
+ "es-errors": "^1.3.0",
93
+ "function-bind": "^1.1.2"
94
+ },
95
+ "engines": {
96
+ "node": ">= 0.4"
97
+ }
98
+ },
99
+ "node_modules/call-bound": {
100
+ "version": "1.0.4",
101
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
102
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
103
+ "license": "MIT",
104
+ "dependencies": {
105
+ "call-bind-apply-helpers": "^1.0.2",
106
+ "get-intrinsic": "^1.3.0"
107
+ },
108
+ "engines": {
109
+ "node": ">= 0.4"
110
+ },
111
+ "funding": {
112
+ "url": "https://github.com/sponsors/ljharb"
113
+ }
114
+ },
115
+ "node_modules/combined-stream": {
116
+ "version": "1.0.8",
117
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
118
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
119
+ "license": "MIT",
120
+ "dependencies": {
121
+ "delayed-stream": "~1.0.0"
122
+ },
123
+ "engines": {
124
+ "node": ">= 0.8"
125
+ }
126
+ },
127
+ "node_modules/content-disposition": {
128
+ "version": "1.0.1",
129
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
130
+ "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
131
+ "license": "MIT",
132
+ "engines": {
133
+ "node": ">=18"
134
+ },
135
+ "funding": {
136
+ "type": "opencollective",
137
+ "url": "https://opencollective.com/express"
138
+ }
139
+ },
140
+ "node_modules/content-type": {
141
+ "version": "1.0.5",
142
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
143
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
144
+ "license": "MIT",
145
+ "engines": {
146
+ "node": ">= 0.6"
147
+ }
148
+ },
149
+ "node_modules/cookie": {
150
+ "version": "0.7.2",
151
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
152
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
153
+ "license": "MIT",
154
+ "engines": {
155
+ "node": ">= 0.6"
156
+ }
157
+ },
158
+ "node_modules/cookie-signature": {
159
+ "version": "1.2.2",
160
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
161
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
162
+ "license": "MIT",
163
+ "engines": {
164
+ "node": ">=6.6.0"
165
+ }
166
+ },
167
+ "node_modules/debug": {
168
+ "version": "4.4.3",
169
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
170
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
171
+ "license": "MIT",
172
+ "dependencies": {
173
+ "ms": "^2.1.3"
174
+ },
175
+ "engines": {
176
+ "node": ">=6.0"
177
+ },
178
+ "peerDependenciesMeta": {
179
+ "supports-color": {
180
+ "optional": true
181
+ }
182
+ }
183
+ },
184
+ "node_modules/delayed-stream": {
185
+ "version": "1.0.0",
186
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
187
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
188
+ "license": "MIT",
189
+ "engines": {
190
+ "node": ">=0.4.0"
191
+ }
192
+ },
193
+ "node_modules/depd": {
194
+ "version": "2.0.0",
195
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
196
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
197
+ "license": "MIT",
198
+ "engines": {
199
+ "node": ">= 0.8"
200
+ }
201
+ },
202
+ "node_modules/dotenv": {
203
+ "version": "17.2.3",
204
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
205
+ "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
206
+ "license": "BSD-2-Clause",
207
+ "engines": {
208
+ "node": ">=12"
209
+ },
210
+ "funding": {
211
+ "url": "https://dotenvx.com"
212
+ }
213
+ },
214
+ "node_modules/dunder-proto": {
215
+ "version": "1.0.1",
216
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
217
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
218
+ "license": "MIT",
219
+ "dependencies": {
220
+ "call-bind-apply-helpers": "^1.0.1",
221
+ "es-errors": "^1.3.0",
222
+ "gopd": "^1.2.0"
223
+ },
224
+ "engines": {
225
+ "node": ">= 0.4"
226
+ }
227
+ },
228
+ "node_modules/ecdsa-sig-formatter": {
229
+ "version": "1.0.11",
230
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
231
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
232
+ "license": "Apache-2.0",
233
+ "dependencies": {
234
+ "safe-buffer": "^5.0.1"
235
+ }
236
+ },
237
+ "node_modules/ee-first": {
238
+ "version": "1.1.1",
239
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
240
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
241
+ "license": "MIT"
242
+ },
243
+ "node_modules/encodeurl": {
244
+ "version": "2.0.0",
245
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
246
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
247
+ "license": "MIT",
248
+ "engines": {
249
+ "node": ">= 0.8"
250
+ }
251
+ },
252
+ "node_modules/es-define-property": {
253
+ "version": "1.0.1",
254
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
255
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
256
+ "license": "MIT",
257
+ "engines": {
258
+ "node": ">= 0.4"
259
+ }
260
+ },
261
+ "node_modules/es-errors": {
262
+ "version": "1.3.0",
263
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
264
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
265
+ "license": "MIT",
266
+ "engines": {
267
+ "node": ">= 0.4"
268
+ }
269
+ },
270
+ "node_modules/es-object-atoms": {
271
+ "version": "1.1.1",
272
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
273
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
274
+ "license": "MIT",
275
+ "dependencies": {
276
+ "es-errors": "^1.3.0"
277
+ },
278
+ "engines": {
279
+ "node": ">= 0.4"
280
+ }
281
+ },
282
+ "node_modules/es-set-tostringtag": {
283
+ "version": "2.1.0",
284
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
285
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
286
+ "license": "MIT",
287
+ "dependencies": {
288
+ "es-errors": "^1.3.0",
289
+ "get-intrinsic": "^1.2.6",
290
+ "has-tostringtag": "^1.0.2",
291
+ "hasown": "^2.0.2"
292
+ },
293
+ "engines": {
294
+ "node": ">= 0.4"
295
+ }
296
+ },
297
+ "node_modules/escape-html": {
298
+ "version": "1.0.3",
299
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
300
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
301
+ "license": "MIT"
302
+ },
303
+ "node_modules/etag": {
304
+ "version": "1.8.1",
305
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
306
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
307
+ "license": "MIT",
308
+ "engines": {
309
+ "node": ">= 0.6"
310
+ }
311
+ },
312
+ "node_modules/express": {
313
+ "version": "5.1.0",
314
+ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
315
+ "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
316
+ "license": "MIT",
317
+ "dependencies": {
318
+ "accepts": "^2.0.0",
319
+ "body-parser": "^2.2.0",
320
+ "content-disposition": "^1.0.0",
321
+ "content-type": "^1.0.5",
322
+ "cookie": "^0.7.1",
323
+ "cookie-signature": "^1.2.1",
324
+ "debug": "^4.4.0",
325
+ "encodeurl": "^2.0.0",
326
+ "escape-html": "^1.0.3",
327
+ "etag": "^1.8.1",
328
+ "finalhandler": "^2.1.0",
329
+ "fresh": "^2.0.0",
330
+ "http-errors": "^2.0.0",
331
+ "merge-descriptors": "^2.0.0",
332
+ "mime-types": "^3.0.0",
333
+ "on-finished": "^2.4.1",
334
+ "once": "^1.4.0",
335
+ "parseurl": "^1.3.3",
336
+ "proxy-addr": "^2.0.7",
337
+ "qs": "^6.14.0",
338
+ "range-parser": "^1.2.1",
339
+ "router": "^2.2.0",
340
+ "send": "^1.1.0",
341
+ "serve-static": "^2.2.0",
342
+ "statuses": "^2.0.1",
343
+ "type-is": "^2.0.1",
344
+ "vary": "^1.1.2"
345
+ },
346
+ "engines": {
347
+ "node": ">= 18"
348
+ },
349
+ "funding": {
350
+ "type": "opencollective",
351
+ "url": "https://opencollective.com/express"
352
+ }
353
+ },
354
+ "node_modules/finalhandler": {
355
+ "version": "2.1.0",
356
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
357
+ "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
358
+ "license": "MIT",
359
+ "dependencies": {
360
+ "debug": "^4.4.0",
361
+ "encodeurl": "^2.0.0",
362
+ "escape-html": "^1.0.3",
363
+ "on-finished": "^2.4.1",
364
+ "parseurl": "^1.3.3",
365
+ "statuses": "^2.0.1"
366
+ },
367
+ "engines": {
368
+ "node": ">= 0.8"
369
+ }
370
+ },
371
+ "node_modules/follow-redirects": {
372
+ "version": "1.15.11",
373
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
374
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
375
+ "funding": [
376
+ {
377
+ "type": "individual",
378
+ "url": "https://github.com/sponsors/RubenVerborgh"
379
+ }
380
+ ],
381
+ "license": "MIT",
382
+ "engines": {
383
+ "node": ">=4.0"
384
+ },
385
+ "peerDependenciesMeta": {
386
+ "debug": {
387
+ "optional": true
388
+ }
389
+ }
390
+ },
391
+ "node_modules/form-data": {
392
+ "version": "4.0.5",
393
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
394
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
395
+ "license": "MIT",
396
+ "dependencies": {
397
+ "asynckit": "^0.4.0",
398
+ "combined-stream": "^1.0.8",
399
+ "es-set-tostringtag": "^2.1.0",
400
+ "hasown": "^2.0.2",
401
+ "mime-types": "^2.1.12"
402
+ },
403
+ "engines": {
404
+ "node": ">= 6"
405
+ }
406
+ },
407
+ "node_modules/form-data/node_modules/mime-db": {
408
+ "version": "1.52.0",
409
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
410
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
411
+ "license": "MIT",
412
+ "engines": {
413
+ "node": ">= 0.6"
414
+ }
415
+ },
416
+ "node_modules/form-data/node_modules/mime-types": {
417
+ "version": "2.1.35",
418
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
419
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
420
+ "license": "MIT",
421
+ "dependencies": {
422
+ "mime-db": "1.52.0"
423
+ },
424
+ "engines": {
425
+ "node": ">= 0.6"
426
+ }
427
+ },
428
+ "node_modules/forwarded": {
429
+ "version": "0.2.0",
430
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
431
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
432
+ "license": "MIT",
433
+ "engines": {
434
+ "node": ">= 0.6"
435
+ }
436
+ },
437
+ "node_modules/fresh": {
438
+ "version": "2.0.0",
439
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
440
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
441
+ "license": "MIT",
442
+ "engines": {
443
+ "node": ">= 0.8"
444
+ }
445
+ },
446
+ "node_modules/function-bind": {
447
+ "version": "1.1.2",
448
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
449
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
450
+ "license": "MIT",
451
+ "funding": {
452
+ "url": "https://github.com/sponsors/ljharb"
453
+ }
454
+ },
455
+ "node_modules/get-intrinsic": {
456
+ "version": "1.3.0",
457
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
458
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
459
+ "license": "MIT",
460
+ "dependencies": {
461
+ "call-bind-apply-helpers": "^1.0.2",
462
+ "es-define-property": "^1.0.1",
463
+ "es-errors": "^1.3.0",
464
+ "es-object-atoms": "^1.1.1",
465
+ "function-bind": "^1.1.2",
466
+ "get-proto": "^1.0.1",
467
+ "gopd": "^1.2.0",
468
+ "has-symbols": "^1.1.0",
469
+ "hasown": "^2.0.2",
470
+ "math-intrinsics": "^1.1.0"
471
+ },
472
+ "engines": {
473
+ "node": ">= 0.4"
474
+ },
475
+ "funding": {
476
+ "url": "https://github.com/sponsors/ljharb"
477
+ }
478
+ },
479
+ "node_modules/get-proto": {
480
+ "version": "1.0.1",
481
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
482
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
483
+ "license": "MIT",
484
+ "dependencies": {
485
+ "dunder-proto": "^1.0.1",
486
+ "es-object-atoms": "^1.0.0"
487
+ },
488
+ "engines": {
489
+ "node": ">= 0.4"
490
+ }
491
+ },
492
+ "node_modules/gopd": {
493
+ "version": "1.2.0",
494
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
495
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
496
+ "license": "MIT",
497
+ "engines": {
498
+ "node": ">= 0.4"
499
+ },
500
+ "funding": {
501
+ "url": "https://github.com/sponsors/ljharb"
502
+ }
503
+ },
504
+ "node_modules/has-symbols": {
505
+ "version": "1.1.0",
506
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
507
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
508
+ "license": "MIT",
509
+ "engines": {
510
+ "node": ">= 0.4"
511
+ },
512
+ "funding": {
513
+ "url": "https://github.com/sponsors/ljharb"
514
+ }
515
+ },
516
+ "node_modules/has-tostringtag": {
517
+ "version": "1.0.2",
518
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
519
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
520
+ "license": "MIT",
521
+ "dependencies": {
522
+ "has-symbols": "^1.0.3"
523
+ },
524
+ "engines": {
525
+ "node": ">= 0.4"
526
+ },
527
+ "funding": {
528
+ "url": "https://github.com/sponsors/ljharb"
529
+ }
530
+ },
531
+ "node_modules/hasown": {
532
+ "version": "2.0.2",
533
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
534
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
535
+ "license": "MIT",
536
+ "dependencies": {
537
+ "function-bind": "^1.1.2"
538
+ },
539
+ "engines": {
540
+ "node": ">= 0.4"
541
+ }
542
+ },
543
+ "node_modules/http-errors": {
544
+ "version": "2.0.0",
545
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
546
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
547
+ "license": "MIT",
548
+ "dependencies": {
549
+ "depd": "2.0.0",
550
+ "inherits": "2.0.4",
551
+ "setprototypeof": "1.2.0",
552
+ "statuses": "2.0.1",
553
+ "toidentifier": "1.0.1"
554
+ },
555
+ "engines": {
556
+ "node": ">= 0.8"
557
+ }
558
+ },
559
+ "node_modules/http-errors/node_modules/statuses": {
560
+ "version": "2.0.1",
561
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
562
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
563
+ "license": "MIT",
564
+ "engines": {
565
+ "node": ">= 0.8"
566
+ }
567
+ },
568
+ "node_modules/iconv-lite": {
569
+ "version": "0.6.3",
570
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
571
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
572
+ "license": "MIT",
573
+ "dependencies": {
574
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
575
+ },
576
+ "engines": {
577
+ "node": ">=0.10.0"
578
+ }
579
+ },
580
+ "node_modules/inherits": {
581
+ "version": "2.0.4",
582
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
583
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
584
+ "license": "ISC"
585
+ },
586
+ "node_modules/ipaddr.js": {
587
+ "version": "1.9.1",
588
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
589
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
590
+ "license": "MIT",
591
+ "engines": {
592
+ "node": ">= 0.10"
593
+ }
594
+ },
595
+ "node_modules/is-promise": {
596
+ "version": "4.0.0",
597
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
598
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
599
+ "license": "MIT"
600
+ },
601
+ "node_modules/jsonwebtoken": {
602
+ "version": "9.0.3",
603
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
604
+ "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
605
+ "license": "MIT",
606
+ "dependencies": {
607
+ "jws": "^4.0.1",
608
+ "lodash.includes": "^4.3.0",
609
+ "lodash.isboolean": "^3.0.3",
610
+ "lodash.isinteger": "^4.0.4",
611
+ "lodash.isnumber": "^3.0.3",
612
+ "lodash.isplainobject": "^4.0.6",
613
+ "lodash.isstring": "^4.0.1",
614
+ "lodash.once": "^4.0.0",
615
+ "ms": "^2.1.1",
616
+ "semver": "^7.5.4"
617
+ },
618
+ "engines": {
619
+ "node": ">=12",
620
+ "npm": ">=6"
621
+ }
622
+ },
623
+ "node_modules/jwa": {
624
+ "version": "2.0.1",
625
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
626
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
627
+ "license": "MIT",
628
+ "dependencies": {
629
+ "buffer-equal-constant-time": "^1.0.1",
630
+ "ecdsa-sig-formatter": "1.0.11",
631
+ "safe-buffer": "^5.0.1"
632
+ }
633
+ },
634
+ "node_modules/jws": {
635
+ "version": "4.0.1",
636
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
637
+ "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
638
+ "license": "MIT",
639
+ "dependencies": {
640
+ "jwa": "^2.0.1",
641
+ "safe-buffer": "^5.0.1"
642
+ }
643
+ },
644
+ "node_modules/lodash.includes": {
645
+ "version": "4.3.0",
646
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
647
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
648
+ "license": "MIT"
649
+ },
650
+ "node_modules/lodash.isboolean": {
651
+ "version": "3.0.3",
652
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
653
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
654
+ "license": "MIT"
655
+ },
656
+ "node_modules/lodash.isinteger": {
657
+ "version": "4.0.4",
658
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
659
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
660
+ "license": "MIT"
661
+ },
662
+ "node_modules/lodash.isnumber": {
663
+ "version": "3.0.3",
664
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
665
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
666
+ "license": "MIT"
667
+ },
668
+ "node_modules/lodash.isplainobject": {
669
+ "version": "4.0.6",
670
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
671
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
672
+ "license": "MIT"
673
+ },
674
+ "node_modules/lodash.isstring": {
675
+ "version": "4.0.1",
676
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
677
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
678
+ "license": "MIT"
679
+ },
680
+ "node_modules/lodash.once": {
681
+ "version": "4.1.1",
682
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
683
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
684
+ "license": "MIT"
685
+ },
686
+ "node_modules/math-intrinsics": {
687
+ "version": "1.1.0",
688
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
689
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
690
+ "license": "MIT",
691
+ "engines": {
692
+ "node": ">= 0.4"
693
+ }
694
+ },
695
+ "node_modules/media-typer": {
696
+ "version": "1.1.0",
697
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
698
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
699
+ "license": "MIT",
700
+ "engines": {
701
+ "node": ">= 0.8"
702
+ }
703
+ },
704
+ "node_modules/merge-descriptors": {
705
+ "version": "2.0.0",
706
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
707
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
708
+ "license": "MIT",
709
+ "engines": {
710
+ "node": ">=18"
711
+ },
712
+ "funding": {
713
+ "url": "https://github.com/sponsors/sindresorhus"
714
+ }
715
+ },
716
+ "node_modules/mime-db": {
717
+ "version": "1.54.0",
718
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
719
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
720
+ "license": "MIT",
721
+ "engines": {
722
+ "node": ">= 0.6"
723
+ }
724
+ },
725
+ "node_modules/mime-types": {
726
+ "version": "3.0.1",
727
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
728
+ "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
729
+ "license": "MIT",
730
+ "dependencies": {
731
+ "mime-db": "^1.54.0"
732
+ },
733
+ "engines": {
734
+ "node": ">= 0.6"
735
+ }
736
+ },
737
+ "node_modules/ms": {
738
+ "version": "2.1.3",
739
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
740
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
741
+ "license": "MIT"
742
+ },
743
+ "node_modules/negotiator": {
744
+ "version": "1.0.0",
745
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
746
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
747
+ "license": "MIT",
748
+ "engines": {
749
+ "node": ">= 0.6"
750
+ }
751
+ },
752
+ "node_modules/object-inspect": {
753
+ "version": "1.13.4",
754
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
755
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
756
+ "license": "MIT",
757
+ "engines": {
758
+ "node": ">= 0.4"
759
+ },
760
+ "funding": {
761
+ "url": "https://github.com/sponsors/ljharb"
762
+ }
763
+ },
764
+ "node_modules/on-finished": {
765
+ "version": "2.4.1",
766
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
767
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
768
+ "license": "MIT",
769
+ "dependencies": {
770
+ "ee-first": "1.1.1"
771
+ },
772
+ "engines": {
773
+ "node": ">= 0.8"
774
+ }
775
+ },
776
+ "node_modules/once": {
777
+ "version": "1.4.0",
778
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
779
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
780
+ "license": "ISC",
781
+ "dependencies": {
782
+ "wrappy": "1"
783
+ }
784
+ },
785
+ "node_modules/parseurl": {
786
+ "version": "1.3.3",
787
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
788
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
789
+ "license": "MIT",
790
+ "engines": {
791
+ "node": ">= 0.8"
792
+ }
793
+ },
794
+ "node_modules/path-to-regexp": {
795
+ "version": "8.3.0",
796
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
797
+ "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
798
+ "license": "MIT",
799
+ "funding": {
800
+ "type": "opencollective",
801
+ "url": "https://opencollective.com/express"
802
+ }
803
+ },
804
+ "node_modules/proxy-addr": {
805
+ "version": "2.0.7",
806
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
807
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
808
+ "license": "MIT",
809
+ "dependencies": {
810
+ "forwarded": "0.2.0",
811
+ "ipaddr.js": "1.9.1"
812
+ },
813
+ "engines": {
814
+ "node": ">= 0.10"
815
+ }
816
+ },
817
+ "node_modules/proxy-from-env": {
818
+ "version": "1.1.0",
819
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
820
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
821
+ "license": "MIT"
822
+ },
823
+ "node_modules/qs": {
824
+ "version": "6.14.0",
825
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
826
+ "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
827
+ "license": "BSD-3-Clause",
828
+ "dependencies": {
829
+ "side-channel": "^1.1.0"
830
+ },
831
+ "engines": {
832
+ "node": ">=0.6"
833
+ },
834
+ "funding": {
835
+ "url": "https://github.com/sponsors/ljharb"
836
+ }
837
+ },
838
+ "node_modules/range-parser": {
839
+ "version": "1.2.1",
840
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
841
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
842
+ "license": "MIT",
843
+ "engines": {
844
+ "node": ">= 0.6"
845
+ }
846
+ },
847
+ "node_modules/raw-body": {
848
+ "version": "3.0.1",
849
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz",
850
+ "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==",
851
+ "license": "MIT",
852
+ "dependencies": {
853
+ "bytes": "3.1.2",
854
+ "http-errors": "2.0.0",
855
+ "iconv-lite": "0.7.0",
856
+ "unpipe": "1.0.0"
857
+ },
858
+ "engines": {
859
+ "node": ">= 0.10"
860
+ }
861
+ },
862
+ "node_modules/raw-body/node_modules/iconv-lite": {
863
+ "version": "0.7.0",
864
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
865
+ "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
866
+ "license": "MIT",
867
+ "dependencies": {
868
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
869
+ },
870
+ "engines": {
871
+ "node": ">=0.10.0"
872
+ },
873
+ "funding": {
874
+ "type": "opencollective",
875
+ "url": "https://opencollective.com/express"
876
+ }
877
+ },
878
+ "node_modules/router": {
879
+ "version": "2.2.0",
880
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
881
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
882
+ "license": "MIT",
883
+ "dependencies": {
884
+ "debug": "^4.4.0",
885
+ "depd": "^2.0.0",
886
+ "is-promise": "^4.0.0",
887
+ "parseurl": "^1.3.3",
888
+ "path-to-regexp": "^8.0.0"
889
+ },
890
+ "engines": {
891
+ "node": ">= 18"
892
+ }
893
+ },
894
+ "node_modules/safe-buffer": {
895
+ "version": "5.2.1",
896
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
897
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
898
+ "funding": [
899
+ {
900
+ "type": "github",
901
+ "url": "https://github.com/sponsors/feross"
902
+ },
903
+ {
904
+ "type": "patreon",
905
+ "url": "https://www.patreon.com/feross"
906
+ },
907
+ {
908
+ "type": "consulting",
909
+ "url": "https://feross.org/support"
910
+ }
911
+ ],
912
+ "license": "MIT"
913
+ },
914
+ "node_modules/safer-buffer": {
915
+ "version": "2.1.2",
916
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
917
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
918
+ "license": "MIT"
919
+ },
920
+ "node_modules/semver": {
921
+ "version": "7.7.3",
922
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
923
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
924
+ "license": "ISC",
925
+ "bin": {
926
+ "semver": "bin/semver.js"
927
+ },
928
+ "engines": {
929
+ "node": ">=10"
930
+ }
931
+ },
932
+ "node_modules/send": {
933
+ "version": "1.2.0",
934
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
935
+ "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
936
+ "license": "MIT",
937
+ "dependencies": {
938
+ "debug": "^4.3.5",
939
+ "encodeurl": "^2.0.0",
940
+ "escape-html": "^1.0.3",
941
+ "etag": "^1.8.1",
942
+ "fresh": "^2.0.0",
943
+ "http-errors": "^2.0.0",
944
+ "mime-types": "^3.0.1",
945
+ "ms": "^2.1.3",
946
+ "on-finished": "^2.4.1",
947
+ "range-parser": "^1.2.1",
948
+ "statuses": "^2.0.1"
949
+ },
950
+ "engines": {
951
+ "node": ">= 18"
952
+ }
953
+ },
954
+ "node_modules/serve-static": {
955
+ "version": "2.2.0",
956
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
957
+ "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
958
+ "license": "MIT",
959
+ "dependencies": {
960
+ "encodeurl": "^2.0.0",
961
+ "escape-html": "^1.0.3",
962
+ "parseurl": "^1.3.3",
963
+ "send": "^1.2.0"
964
+ },
965
+ "engines": {
966
+ "node": ">= 18"
967
+ }
968
+ },
969
+ "node_modules/setprototypeof": {
970
+ "version": "1.2.0",
971
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
972
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
973
+ "license": "ISC"
974
+ },
975
+ "node_modules/side-channel": {
976
+ "version": "1.1.0",
977
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
978
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
979
+ "license": "MIT",
980
+ "dependencies": {
981
+ "es-errors": "^1.3.0",
982
+ "object-inspect": "^1.13.3",
983
+ "side-channel-list": "^1.0.0",
984
+ "side-channel-map": "^1.0.1",
985
+ "side-channel-weakmap": "^1.0.2"
986
+ },
987
+ "engines": {
988
+ "node": ">= 0.4"
989
+ },
990
+ "funding": {
991
+ "url": "https://github.com/sponsors/ljharb"
992
+ }
993
+ },
994
+ "node_modules/side-channel-list": {
995
+ "version": "1.0.0",
996
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
997
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
998
+ "license": "MIT",
999
+ "dependencies": {
1000
+ "es-errors": "^1.3.0",
1001
+ "object-inspect": "^1.13.3"
1002
+ },
1003
+ "engines": {
1004
+ "node": ">= 0.4"
1005
+ },
1006
+ "funding": {
1007
+ "url": "https://github.com/sponsors/ljharb"
1008
+ }
1009
+ },
1010
+ "node_modules/side-channel-map": {
1011
+ "version": "1.0.1",
1012
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
1013
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
1014
+ "license": "MIT",
1015
+ "dependencies": {
1016
+ "call-bound": "^1.0.2",
1017
+ "es-errors": "^1.3.0",
1018
+ "get-intrinsic": "^1.2.5",
1019
+ "object-inspect": "^1.13.3"
1020
+ },
1021
+ "engines": {
1022
+ "node": ">= 0.4"
1023
+ },
1024
+ "funding": {
1025
+ "url": "https://github.com/sponsors/ljharb"
1026
+ }
1027
+ },
1028
+ "node_modules/side-channel-weakmap": {
1029
+ "version": "1.0.2",
1030
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
1031
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
1032
+ "license": "MIT",
1033
+ "dependencies": {
1034
+ "call-bound": "^1.0.2",
1035
+ "es-errors": "^1.3.0",
1036
+ "get-intrinsic": "^1.2.5",
1037
+ "object-inspect": "^1.13.3",
1038
+ "side-channel-map": "^1.0.1"
1039
+ },
1040
+ "engines": {
1041
+ "node": ">= 0.4"
1042
+ },
1043
+ "funding": {
1044
+ "url": "https://github.com/sponsors/ljharb"
1045
+ }
1046
+ },
1047
+ "node_modules/statuses": {
1048
+ "version": "2.0.2",
1049
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
1050
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
1051
+ "license": "MIT",
1052
+ "engines": {
1053
+ "node": ">= 0.8"
1054
+ }
1055
+ },
1056
+ "node_modules/toidentifier": {
1057
+ "version": "1.0.1",
1058
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
1059
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
1060
+ "license": "MIT",
1061
+ "engines": {
1062
+ "node": ">=0.6"
1063
+ }
1064
+ },
1065
+ "node_modules/type-is": {
1066
+ "version": "2.0.1",
1067
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
1068
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
1069
+ "license": "MIT",
1070
+ "dependencies": {
1071
+ "content-type": "^1.0.5",
1072
+ "media-typer": "^1.1.0",
1073
+ "mime-types": "^3.0.0"
1074
+ },
1075
+ "engines": {
1076
+ "node": ">= 0.6"
1077
+ }
1078
+ },
1079
+ "node_modules/unpipe": {
1080
+ "version": "1.0.0",
1081
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
1082
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
1083
+ "license": "MIT",
1084
+ "engines": {
1085
+ "node": ">= 0.8"
1086
+ }
1087
+ },
1088
+ "node_modules/vary": {
1089
+ "version": "1.1.2",
1090
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
1091
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
1092
+ "license": "MIT",
1093
+ "engines": {
1094
+ "node": ">= 0.8"
1095
+ }
1096
+ },
1097
+ "node_modules/wrappy": {
1098
+ "version": "1.0.2",
1099
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
1100
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
1101
+ "license": "ISC"
1102
+ }
1103
+ }
1104
+ }
package.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "antigravity-to-openai",
3
+ "version": "1.0.0",
4
+ "description": "Antigravity API 转 OpenAI 格式的代理服务",
5
+ "type": "module",
6
+ "main": "src/server/index.js",
7
+ "scripts": {
8
+ "start": "node src/server/index.js",
9
+ "login": "node scripts/oauth-server.js",
10
+ "refresh": "node scripts/refresh-tokens.js",
11
+ "dev": "node --watch src/server/index.js"
12
+ },
13
+ "keywords": [
14
+ "antigravity",
15
+ "openai",
16
+ "api",
17
+ "proxy"
18
+ ],
19
+ "author": "",
20
+ "license": "MIT",
21
+ "dependencies": {
22
+ "axios": "^1.13.2",
23
+ "dotenv": "^17.2.3",
24
+ "express": "^5.1.0",
25
+ "jsonwebtoken": "^9.0.2"
26
+ },
27
+ "engines": {
28
+ "node": ">=18.0.0"
29
+ }
30
+ }
public/app.js ADDED
@@ -0,0 +1,522 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ let authToken = localStorage.getItem('authToken');
2
+ let oauthPort = null;
3
+ const CLIENT_ID = '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com';
4
+ const SCOPES = [
5
+ 'https://www.googleapis.com/auth/cloud-platform',
6
+ 'https://www.googleapis.com/auth/userinfo.email',
7
+ 'https://www.googleapis.com/auth/userinfo.profile',
8
+ 'https://www.googleapis.com/auth/cclog',
9
+ 'https://www.googleapis.com/auth/experimentsandconfigs'
10
+ ].join(' ');
11
+
12
+ function showToast(message, type = 'info', title = '') {
13
+ const icons = { success: '✅', error: '❌', warning: '⚠️', info: 'ℹ️' };
14
+ const titles = { success: '成功', error: '错误', warning: '警告', info: '提示' };
15
+ const toast = document.createElement('div');
16
+ toast.className = `toast ${type}`;
17
+ toast.innerHTML = `
18
+ <div class="toast-icon">${icons[type]}</div>
19
+ <div class="toast-content">
20
+ <div class="toast-title">${title || titles[type]}</div>
21
+ <div class="toast-message">${message}</div>
22
+ </div>
23
+ `;
24
+ document.body.appendChild(toast);
25
+ setTimeout(() => {
26
+ toast.style.animation = 'slideOut 0.3s ease';
27
+ setTimeout(() => toast.remove(), 300);
28
+ }, 3000);
29
+ }
30
+
31
+ function showConfirm(message, title = '确认操作') {
32
+ return new Promise((resolve) => {
33
+ const modal = document.createElement('div');
34
+ modal.className = 'modal';
35
+ modal.innerHTML = `
36
+ <div class="modal-content">
37
+ <div class="modal-title">${title}</div>
38
+ <div class="modal-message">${message}</div>
39
+ <div class="modal-actions">
40
+ <button class="btn btn-secondary" onclick="this.closest('.modal').remove(); window.modalResolve(false)">取消</button>
41
+ <button class="btn btn-danger" onclick="this.closest('.modal').remove(); window.modalResolve(true)">确定</button>
42
+ </div>
43
+ </div>
44
+ `;
45
+ document.body.appendChild(modal);
46
+ modal.onclick = (e) => { if (e.target === modal) { modal.remove(); resolve(false); } };
47
+ window.modalResolve = resolve;
48
+ });
49
+ }
50
+
51
+ function showLoading(text = '处理中...') {
52
+ const overlay = document.createElement('div');
53
+ overlay.className = 'loading-overlay';
54
+ overlay.id = 'loadingOverlay';
55
+ overlay.innerHTML = `<div class="spinner"></div><div class="loading-text">${text}</div>`;
56
+ document.body.appendChild(overlay);
57
+ }
58
+
59
+ function hideLoading() {
60
+ const overlay = document.getElementById('loadingOverlay');
61
+ if (overlay) overlay.remove();
62
+ }
63
+
64
+ if (authToken) {
65
+ showMainContent();
66
+ loadTokens();
67
+ loadConfig();
68
+ }
69
+
70
+ document.getElementById('login').addEventListener('submit', async (e) => {
71
+ e.preventDefault();
72
+ const btn = e.target.querySelector('button[type="submit"]');
73
+ if (btn.disabled) return;
74
+
75
+ const username = document.getElementById('username').value;
76
+ const password = document.getElementById('password').value;
77
+
78
+ btn.disabled = true;
79
+ btn.classList.add('loading');
80
+ const originalText = btn.textContent;
81
+ btn.textContent = '登录中';
82
+
83
+ try {
84
+ const response = await fetch('/admin/login', {
85
+ method: 'POST',
86
+ headers: { 'Content-Type': 'application/json' },
87
+ body: JSON.stringify({ username, password })
88
+ });
89
+
90
+ const data = await response.json();
91
+ if (data.success) {
92
+ authToken = data.token;
93
+ localStorage.setItem('authToken', authToken);
94
+ showToast('登录成功,欢迎回来!', 'success');
95
+ showMainContent();
96
+ loadTokens();
97
+ } else {
98
+ showToast(data.message || '用户名或密码错误', 'error');
99
+ }
100
+ } catch (error) {
101
+ showToast('登录失败: ' + error.message, 'error');
102
+ } finally {
103
+ btn.disabled = false;
104
+ btn.classList.remove('loading');
105
+ btn.textContent = originalText;
106
+ }
107
+ });
108
+
109
+ function showOAuthModal() {
110
+ showToast('点击后请在新窗口完成授权', 'info', '提示');
111
+ const modal = document.createElement('div');
112
+ modal.className = 'modal form-modal';
113
+ modal.innerHTML = `
114
+ <div class="modal-content">
115
+ <div class="modal-title">🔐 OAuth授权登录</div>
116
+ <div class="oauth-steps">
117
+ <p><strong>📝 授权流程:</strong></p>
118
+ <p>1️⃣ 点击下方按钮打开Google授权页面</p>
119
+ <p>2️⃣ 完成授权后,复制浏览器地址栏的完整URL</p>
120
+ <p>3️⃣ 粘贴URL到下方输入框并提交</p>
121
+ </div>
122
+ <button type="button" onclick="openOAuthWindow()" class="btn btn-success" style="width: 100%; margin-bottom: 16px;">🔐 打开授权页面</button>
123
+ <input type="text" id="modalCallbackUrl" placeholder="粘贴完整的回调URL (http://localhost:xxxxx/oauth-callback?code=...)">
124
+ <div class="modal-actions">
125
+ <button class="btn btn-secondary" onclick="this.closest('.modal').remove()">取消</button>
126
+ <button class="btn btn-success" onclick="processOAuthCallbackModal()">✅ 提交</button>
127
+ </div>
128
+ </div>
129
+ `;
130
+ document.body.appendChild(modal);
131
+ modal.onclick = (e) => { if (e.target === modal) modal.remove(); };
132
+ }
133
+
134
+ function showManualModal() {
135
+ const modal = document.createElement('div');
136
+ modal.className = 'modal form-modal';
137
+ modal.innerHTML = `
138
+ <div class="modal-content">
139
+ <div class="modal-title">✏️ 手动填入Token</div>
140
+ <div class="form-row">
141
+ <input type="text" id="modalAccessToken" placeholder="Access Token (必填)">
142
+ <input type="text" id="modalRefreshToken" placeholder="Refresh Token (必填)">
143
+ <input type="number" id="modalExpiresIn" placeholder="过期时间(秒)" value="3599">
144
+ </div>
145
+ <p style="font-size: 0.85rem; color: var(--text-light); margin-bottom: 16px;">💡 提示:过期时间默认3599秒(约1小时)</p>
146
+ <div class="modal-actions">
147
+ <button class="btn btn-secondary" onclick="this.closest('.modal').remove()">取消</button>
148
+ <button class="btn btn-success" onclick="addTokenFromModal()">✅ 添加</button>
149
+ </div>
150
+ </div>
151
+ `;
152
+ document.body.appendChild(modal);
153
+ modal.onclick = (e) => { if (e.target === modal) modal.remove(); };
154
+ }
155
+
156
+ function openOAuthWindow() {
157
+ oauthPort = Math.floor(Math.random() * 10000) + 50000;
158
+ const redirectUri = `http://localhost:${oauthPort}/oauth-callback`;
159
+ const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?` +
160
+ `access_type=offline&client_id=${CLIENT_ID}&prompt=consent&` +
161
+ `redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&` +
162
+ `scope=${encodeURIComponent(SCOPES)}&state=${Date.now()}`;
163
+ window.open(authUrl, '_blank');
164
+ }
165
+
166
+ async function processOAuthCallbackModal() {
167
+ const modal = document.querySelector('.form-modal');
168
+ const callbackUrl = document.getElementById('modalCallbackUrl').value.trim();
169
+ if (!callbackUrl) {
170
+ showToast('请输入回调URL', 'warning');
171
+ return;
172
+ }
173
+
174
+ showLoading('正在处理授权...');
175
+
176
+ try {
177
+ const url = new URL(callbackUrl);
178
+ const code = url.searchParams.get('code');
179
+ const port = new URL(url.origin).port || (url.protocol === 'https:' ? 443 : 80);
180
+
181
+ if (!code) {
182
+ hideLoading();
183
+ showToast('URL中未找到授权码,请检查URL是否完整', 'error');
184
+ return;
185
+ }
186
+
187
+ const response = await fetch('/admin/oauth/exchange', {
188
+ method: 'POST',
189
+ headers: {
190
+ 'Content-Type': 'application/json',
191
+ 'Authorization': `Bearer ${authToken}`
192
+ },
193
+ body: JSON.stringify({ code, port })
194
+ });
195
+
196
+ const result = await response.json();
197
+ if (result.success) {
198
+ const account = result.data;
199
+ const addResponse = await fetch('/admin/tokens', {
200
+ method: 'POST',
201
+ headers: {
202
+ 'Content-Type': 'application/json',
203
+ 'Authorization': `Bearer ${authToken}`
204
+ },
205
+ body: JSON.stringify(account)
206
+ });
207
+
208
+ const addResult = await addResponse.json();
209
+ hideLoading();
210
+ if (addResult.success) {
211
+ modal.remove();
212
+ showToast('Token添加成功!', 'success');
213
+ loadTokens();
214
+ } else {
215
+ showToast('Token添加失败: ' + addResult.message, 'error');
216
+ }
217
+ } else {
218
+ hideLoading();
219
+ showToast('Token交换失败: ' + result.message, 'error');
220
+ }
221
+ } catch (error) {
222
+ hideLoading();
223
+ showToast('处理失败: ' + error.message, 'error');
224
+ }
225
+ }
226
+
227
+ async function addTokenFromModal() {
228
+ const modal = document.querySelector('.form-modal');
229
+ const accessToken = document.getElementById('modalAccessToken').value.trim();
230
+ const refreshToken = document.getElementById('modalRefreshToken').value.trim();
231
+ const expiresIn = parseInt(document.getElementById('modalExpiresIn').value);
232
+
233
+ if (!accessToken || !refreshToken) {
234
+ showToast('请填写完整的Token信息', 'warning');
235
+ return;
236
+ }
237
+
238
+ showLoading('正在添加Token...');
239
+ try {
240
+ const response = await fetch('/admin/tokens', {
241
+ method: 'POST',
242
+ headers: {
243
+ 'Content-Type': 'application/json',
244
+ 'Authorization': `Bearer ${authToken}`
245
+ },
246
+ body: JSON.stringify({ access_token: accessToken, refresh_token: refreshToken, expires_in: expiresIn })
247
+ });
248
+
249
+ const data = await response.json();
250
+ hideLoading();
251
+ if (data.success) {
252
+ modal.remove();
253
+ showToast('Token添加成功!', 'success');
254
+ loadTokens();
255
+ } else {
256
+ showToast(data.message || '添加失败', 'error');
257
+ }
258
+ } catch (error) {
259
+ hideLoading();
260
+ showToast('添加失败: ' + error.message, 'error');
261
+ }
262
+ }
263
+
264
+ function showMainContent() {
265
+ document.getElementById('loginForm').classList.add('hidden');
266
+ document.getElementById('mainContent').classList.remove('hidden');
267
+ }
268
+
269
+ function switchTab(tab) {
270
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
271
+ event.target.classList.add('active');
272
+
273
+ if (tab === 'tokens') {
274
+ document.getElementById('tokensPage').classList.remove('hidden');
275
+ document.getElementById('settingsPage').classList.add('hidden');
276
+ } else if (tab === 'settings') {
277
+ document.getElementById('tokensPage').classList.add('hidden');
278
+ document.getElementById('settingsPage').classList.remove('hidden');
279
+ loadConfig();
280
+ }
281
+ }
282
+
283
+ async function logout() {
284
+ const confirmed = await showConfirm('确定要退出登录吗?', '退出确认');
285
+ if (!confirmed) return;
286
+
287
+ localStorage.removeItem('authToken');
288
+ authToken = null;
289
+ document.getElementById('loginForm').classList.remove('hidden');
290
+ document.getElementById('mainContent').classList.add('hidden');
291
+ showToast('已退出登录', 'info');
292
+ }
293
+
294
+ async function loadTokens() {
295
+ try {
296
+ const response = await fetch('/admin/tokens', {
297
+ headers: { 'Authorization': `Bearer ${authToken}` }
298
+ });
299
+
300
+ if (response.status === 401) {
301
+ logout();
302
+ return;
303
+ }
304
+
305
+ const data = await response.json();
306
+ if (data.success) {
307
+ renderTokens(data.data);
308
+ } else {
309
+ showToast('加载失败: ' + (data.message || '未知错误'), 'error');
310
+ }
311
+ } catch (error) {
312
+ showToast('加载Token失败: ' + error.message, 'error');
313
+ }
314
+ }
315
+
316
+ function renderTokens(tokens) {
317
+ document.getElementById('totalTokens').textContent = tokens.length;
318
+ document.getElementById('enabledTokens').textContent = tokens.filter(t => t.enable).length;
319
+ document.getElementById('disabledTokens').textContent = tokens.filter(t => !t.enable).length;
320
+
321
+ const tokenList = document.getElementById('tokenList');
322
+ if (tokens.length === 0) {
323
+ tokenList.innerHTML = `
324
+ <div class="empty-state">
325
+ <div class="empty-state-icon">📦</div>
326
+ <div class="empty-state-text">暂无Token</div>
327
+ <div class="empty-state-hint">点击上方按钮添加您的第一个Token</div>
328
+ </div>
329
+ `;
330
+ return;
331
+ }
332
+
333
+ tokenList.innerHTML = tokens.map(token => `
334
+ <div class="token-card">
335
+ <div class="token-header">
336
+ <span class="status ${token.enable ? 'enabled' : 'disabled'}">
337
+ ${token.enable ? '✅ 启用' : '❌ 禁用'}
338
+ </span>
339
+ <span class="token-id">#${token.refresh_token.substring(0, 8)}</span>
340
+ </div>
341
+ <div class="token-info">
342
+ <div class="info-row">
343
+ <span class="info-label">🎫 Access</span>
344
+ <span class="info-value">${token.access_token_suffix}</span>
345
+ </div>
346
+ <div class="info-row">
347
+ <span class="info-label">📦 Project</span>
348
+ <span class="info-value">${token.projectId || 'N/A'}</span>
349
+ </div>
350
+ <div class="info-row">
351
+ <span class="info-label">⏰ 过期</span>
352
+ <span class="info-value">${new Date(token.timestamp + token.expires_in * 1000).toLocaleString('zh-CN', {month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit'})}</span>
353
+ </div>
354
+ </div>
355
+ <div class="token-actions">
356
+ <button class="btn ${token.enable ? 'btn-warning' : 'btn-success'}" onclick="toggleToken('${token.refresh_token}', ${!token.enable})">
357
+ ${token.enable ? '⏸️ 禁用' : '▶️ 启用'}
358
+ </button>
359
+ <button class="btn btn-danger" onclick="deleteToken('${token.refresh_token}')">🗑️ 删除</button>
360
+ </div>
361
+ </div>
362
+ `).join('');
363
+ }
364
+
365
+ async function toggleToken(refreshToken, enable) {
366
+ const action = enable ? '启用' : '禁用';
367
+ const confirmed = await showConfirm(`确定要${action}这个Token吗?`, `${action}确认`);
368
+ if (!confirmed) return;
369
+
370
+ showLoading(`正在${action}Token...`);
371
+ try {
372
+ const response = await fetch(`/admin/tokens/${encodeURIComponent(refreshToken)}`, {
373
+ method: 'PUT',
374
+ headers: {
375
+ 'Content-Type': 'application/json',
376
+ 'Authorization': `Bearer ${authToken}`
377
+ },
378
+ body: JSON.stringify({ enable })
379
+ });
380
+
381
+ const data = await response.json();
382
+ hideLoading();
383
+ if (data.success) {
384
+ showToast(`Token已${enable ? '启用' : '禁用'}`, 'success');
385
+ loadTokens();
386
+ } else {
387
+ showToast(data.message || '操作失败', 'error');
388
+ }
389
+ } catch (error) {
390
+ hideLoading();
391
+ showToast('操作失败: ' + error.message, 'error');
392
+ }
393
+ }
394
+
395
+ async function deleteToken(refreshToken) {
396
+ const confirmed = await showConfirm('删除后无法恢复,确定要删除这个Token吗?', '⚠️ 删除确认');
397
+ if (!confirmed) return;
398
+
399
+ showLoading('正在删除Token...');
400
+ try {
401
+ const response = await fetch(`/admin/tokens/${encodeURIComponent(refreshToken)}`, {
402
+ method: 'DELETE',
403
+ headers: { 'Authorization': `Bearer ${authToken}` }
404
+ });
405
+
406
+ const data = await response.json();
407
+ hideLoading();
408
+ if (data.success) {
409
+ showToast('Token已删除', 'success');
410
+ loadTokens();
411
+ } else {
412
+ showToast(data.message || '删除失败', 'error');
413
+ }
414
+ } catch (error) {
415
+ hideLoading();
416
+ showToast('删除失败: ' + error.message, 'error');
417
+ }
418
+ }
419
+
420
+ async function loadConfig() {
421
+ try {
422
+ const response = await fetch('/admin/config', {
423
+ headers: { 'Authorization': `Bearer ${authToken}` }
424
+ });
425
+ const data = await response.json();
426
+ if (data.success) {
427
+ const form = document.getElementById('configForm');
428
+ const { env, json } = data.data;
429
+
430
+ // 加载 .env 配置
431
+ Object.entries(env).forEach(([key, value]) => {
432
+ const input = form.elements[key];
433
+ if (input) input.value = value || '';
434
+ });
435
+
436
+ // 加载 config.json 配置
437
+ if (json.server) {
438
+ if (form.elements['PORT']) form.elements['PORT'].value = json.server.port || '';
439
+ if (form.elements['HOST']) form.elements['HOST'].value = json.server.host || '';
440
+ if (form.elements['MAX_REQUEST_SIZE']) form.elements['MAX_REQUEST_SIZE'].value = json.server.maxRequestSize || '';
441
+ }
442
+ if (json.defaults) {
443
+ if (form.elements['DEFAULT_TEMPERATURE']) form.elements['DEFAULT_TEMPERATURE'].value = json.defaults.temperature ?? '';
444
+ if (form.elements['DEFAULT_TOP_P']) form.elements['DEFAULT_TOP_P'].value = json.defaults.topP ?? '';
445
+ if (form.elements['DEFAULT_TOP_K']) form.elements['DEFAULT_TOP_K'].value = json.defaults.topK ?? '';
446
+ if (form.elements['DEFAULT_MAX_TOKENS']) form.elements['DEFAULT_MAX_TOKENS'].value = json.defaults.maxTokens ?? '';
447
+ }
448
+ if (json.other) {
449
+ if (form.elements['TIMEOUT']) form.elements['TIMEOUT'].value = json.other.timeout ?? '';
450
+ if (form.elements['MAX_IMAGES']) form.elements['MAX_IMAGES'].value = json.other.maxImages ?? '';
451
+ if (form.elements['USE_NATIVE_AXIOS']) form.elements['USE_NATIVE_AXIOS'].value = json.other.useNativeAxios ? 'true' : 'false';
452
+ if (form.elements['SKIP_PROJECT_ID_FETCH']) form.elements['SKIP_PROJECT_ID_FETCH'].value = json.other.skipProjectIdFetch ? 'true' : 'false';
453
+ }
454
+ }
455
+ } catch (error) {
456
+ showToast('加载配置失败: ' + error.message, 'error');
457
+ }
458
+ }
459
+
460
+ document.getElementById('configForm').addEventListener('submit', async (e) => {
461
+ e.preventDefault();
462
+ const formData = new FormData(e.target);
463
+ const allConfig = Object.fromEntries(formData);
464
+
465
+ // 分离敏感和非敏感配置
466
+ const sensitiveKeys = ['API_KEY', 'ADMIN_USERNAME', 'ADMIN_PASSWORD', 'JWT_SECRET', 'PROXY', 'SYSTEM_INSTRUCTION', 'IMAGE_BASE_URL'];
467
+ const envConfig = {};
468
+ const jsonConfig = {
469
+ server: {},
470
+ api: {},
471
+ defaults: {},
472
+ other: {}
473
+ };
474
+
475
+ Object.entries(allConfig).forEach(([key, value]) => {
476
+ if (sensitiveKeys.includes(key)) {
477
+ envConfig[key] = value;
478
+ } else {
479
+ // 映射到 config.json 结构
480
+ if (key === 'PORT') jsonConfig.server.port = parseInt(value);
481
+ else if (key === 'HOST') jsonConfig.server.host = value;
482
+ else if (key === 'MAX_REQUEST_SIZE') jsonConfig.server.maxRequestSize = value;
483
+ else if (key === 'API_URL') jsonConfig.api.url = value;
484
+ else if (key === 'API_MODELS_URL') jsonConfig.api.modelsUrl = value;
485
+ else if (key === 'API_NO_STREAM_URL') jsonConfig.api.noStreamUrl = value;
486
+ else if (key === 'API_HOST') jsonConfig.api.host = value;
487
+ else if (key === 'API_USER_AGENT') jsonConfig.api.userAgent = value;
488
+ else if (key === 'DEFAULT_TEMPERATURE') jsonConfig.defaults.temperature = parseFloat(value);
489
+ else if (key === 'DEFAULT_TOP_P') jsonConfig.defaults.topP = parseFloat(value);
490
+ else if (key === 'DEFAULT_TOP_K') jsonConfig.defaults.topK = parseInt(value);
491
+ else if (key === 'DEFAULT_MAX_TOKENS') jsonConfig.defaults.maxTokens = parseInt(value);
492
+ else if (key === 'USE_NATIVE_AXIOS') jsonConfig.other.useNativeAxios = value !== 'false';
493
+ else if (key === 'TIMEOUT') jsonConfig.other.timeout = parseInt(value);
494
+ else if (key === 'MAX_IMAGES') jsonConfig.other.maxImages = parseInt(value);
495
+ else if (key === 'SKIP_PROJECT_ID_FETCH') jsonConfig.other.skipProjectIdFetch = value === 'true';
496
+ else envConfig[key] = value;
497
+ }
498
+ });
499
+
500
+ showLoading('正在保存配置...');
501
+ try {
502
+ const response = await fetch('/admin/config', {
503
+ method: 'PUT',
504
+ headers: {
505
+ 'Content-Type': 'application/json',
506
+ 'Authorization': `Bearer ${authToken}`
507
+ },
508
+ body: JSON.stringify({ env: envConfig, json: jsonConfig })
509
+ });
510
+
511
+ const data = await response.json();
512
+ hideLoading();
513
+ if (data.success) {
514
+ showToast(data.message, 'success');
515
+ } else {
516
+ showToast(data.message || '保存失败', 'error');
517
+ }
518
+ } catch (error) {
519
+ hideLoading();
520
+ showToast('保存失败: ' + error.message, 'error');
521
+ }
522
+ });
public/index.html ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
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
+ <link rel="stylesheet" href="style.css">
8
+ </head>
9
+ <body>
10
+ <div class="container">
11
+ <!-- 登录表单 -->
12
+ <div id="loginForm" class="login-form">
13
+ <h2>🔐 Token 管理系统</h2>
14
+ <form id="login">
15
+ <div class="form-group">
16
+ <label>👤 用户名</label>
17
+ <input type="text" id="username" required autocomplete="username">
18
+ </div>
19
+ <div class="form-group">
20
+ <label>🔑 密码</label>
21
+ <input type="password" id="password" required autocomplete="current-password">
22
+ </div>
23
+ <button type="submit">登录</button>
24
+ </form>
25
+ </div>
26
+
27
+ <!-- 主内容 -->
28
+ <div id="mainContent" class="main-content hidden">
29
+ <div class="header">
30
+ <div class="tabs">
31
+ <button class="tab active" onclick="switchTab('tokens')">🎯 Token管理</button>
32
+ <button class="tab" onclick="switchTab('settings')">⚙️ 设置</button>
33
+ </div>
34
+ <button onclick="logout()">🚪 退出</button>
35
+ </div>
36
+ <div class="content">
37
+ <!-- Token管理页面 -->
38
+ <div id="tokensPage">
39
+ <!-- 统计卡片 -->
40
+ <div class="stats">
41
+ <div class="stat-card">
42
+ <div class="stat-value" id="totalTokens">0</div>
43
+ <div class="stat-label">总Token数</div>
44
+ </div>
45
+ <div class="stat-card" style="background: linear-gradient(135deg, var(--success), #059669);">
46
+ <div class="stat-value" id="enabledTokens">0</div>
47
+ <div class="stat-label">已启用</div>
48
+ </div>
49
+ <div class="stat-card" style="background: linear-gradient(135deg, var(--danger), #dc2626);">
50
+ <div class="stat-value" id="disabledTokens">0</div>
51
+ <div class="stat-label">已禁用</div>
52
+ </div>
53
+ </div>
54
+
55
+ <!-- 添加Token按钮组 -->
56
+ <div class="add-form">
57
+ <h3>➕ 添加新Token<span class="help-tip" title="支持OAuth登录或手动填入Token">?</span></h3>
58
+ <div class="btn-group">
59
+ <button type="button" onclick="showOAuthModal()" class="btn btn-success">🔐 OAuth登录</button>
60
+ <button type="button" onclick="showManualModal()" class="btn btn-secondary">✏️ 手动填入</button>
61
+ <button type="button" onclick="loadTokens()" class="btn btn-warning">🔄 刷新</button>
62
+ </div>
63
+ </div>
64
+
65
+ <!-- Token网格 -->
66
+ <div id="tokenList" class="token-grid">
67
+ <div class="empty-state">
68
+ <div class="empty-state-icon">📦</div>
69
+ <div class="empty-state-text">暂无Token</div>
70
+ <div class="empty-state-hint">点击上方按钮添加您的第一个Token</div>
71
+ </div>
72
+ </div>
73
+ </div>
74
+
75
+ <!-- 设置页面 -->
76
+ <div id="settingsPage" class="hidden">
77
+ <h3>⚙️ 系统配置</h3>
78
+ <form id="configForm" class="config-form">
79
+ <div class="config-section">
80
+ <h4>服务器配置</h4>
81
+ <div class="form-group"><label>端口 PORT</label><input type="number" name="PORT"></div>
82
+ <div class="form-group"><label>监听地址 HOST</label><input type="text" name="HOST"></div>
83
+ </div>
84
+ <div class="config-section">
85
+ <h4>默认参数</h4>
86
+ <div class="form-group"><label>温度 TEMPERATURE</label><input type="number" step="0.1" name="DEFAULT_TEMPERATURE"></div>
87
+ <div class="form-group"><label>Top P</label><input type="number" step="0.01" name="DEFAULT_TOP_P"></div>
88
+ <div class="form-group"><label>Top K</label><input type="number" name="DEFAULT_TOP_K"></div>
89
+ <div class="form-group"><label>最大Token数</label><input type="number" name="DEFAULT_MAX_TOKENS"></div>
90
+ </div>
91
+ <div class="config-section">
92
+ <h4>安全配置</h4>
93
+ <div class="form-group"><label>API密钥</label><input type="text" name="API_KEY"></div>
94
+ <div class="form-group"><label>最大请求大小</label><input type="text" name="MAX_REQUEST_SIZE"></div>
95
+ </div>
96
+ <div class="config-section">
97
+ <h4>其他配置</h4>
98
+ <div class="form-group"><label>超时时间(ms)</label><input type="number" name="TIMEOUT"></div>
99
+ <div class="form-group"><label>代理地址</label><input type="text" name="PROXY" placeholder="http://127.0.0.1:7897"></div>
100
+ <div class="form-group"><label>跳过ProjectId验证</label><select name="SKIP_PROJECT_ID_FETCH"><option value="false">否</option><option value="true">是</option></select></div>
101
+ <div class="form-group"><label>系统提示词</label><textarea name="SYSTEM_INSTRUCTION" rows="3"></textarea></div>
102
+ </div>
103
+ <button type="submit" class="btn btn-success">💾 保存配置</button>
104
+ </form>
105
+ </div>
106
+ </div>
107
+ </div>
108
+ </div>
109
+
110
+ <script src="app.js"></script>
111
+ </body>
112
+ </html>
public/style.css ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --primary: #6366f1;
3
+ --primary-dark: #4f46e5;
4
+ --success: #10b981;
5
+ --danger: #ef4444;
6
+ --warning: #f59e0b;
7
+ --info: #3b82f6;
8
+ --bg: #f8fafc;
9
+ --card: #ffffff;
10
+ --text: #1e293b;
11
+ --text-light: #64748b;
12
+ --border: #e2e8f0;
13
+ --shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -1px rgba(0,0,0,0.06);
14
+ }
15
+
16
+ @media (prefers-color-scheme: dark) {
17
+ :root {
18
+ --bg: #0f172a;
19
+ --card: #1e293b;
20
+ --text: #f1f5f9;
21
+ --text-light: #94a3b8;
22
+ --border: #334155;
23
+ }
24
+ }
25
+
26
+ .toast { position: fixed; top: 20px; right: 20px; background: var(--card); border-radius: 12px; padding: 16px 20px; box-shadow: 0 10px 40px rgba(0,0,0,0.2); z-index: 9999; display: flex; align-items: center; gap: 12px; min-width: 280px; max-width: 400px; animation: slideIn 0.3s ease; border-left: 4px solid var(--primary); }
27
+ .toast.success { border-left-color: var(--success); }
28
+ .toast.error { border-left-color: var(--danger); }
29
+ .toast.warning { border-left-color: var(--warning); }
30
+ .toast.info { border-left-color: var(--info); }
31
+ .toast-icon { font-size: 24px; flex-shrink: 0; }
32
+ .toast-content { flex: 1; }
33
+ .toast-title { font-weight: 600; margin-bottom: 4px; color: var(--text); }
34
+ .toast-message { font-size: 14px; color: var(--text-light); }
35
+
36
+ @keyframes slideIn { from { transform: translateX(400px); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
37
+ @keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(400px); opacity: 0; } }
38
+
39
+ .modal { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 9998; display: flex; align-items: center; justify-content: center; padding: 20px; animation: fadeIn 0.2s; }
40
+ .modal-content { background: var(--card); border-radius: 16px; padding: 24px; max-width: 400px; width: 100%; box-shadow: 0 20px 60px rgba(0,0,0,0.3); animation: scaleIn 0.2s; }
41
+ .modal-title { font-size: 20px; font-weight: 700; margin-bottom: 12px; color: var(--text); }
42
+ .modal-message { color: var(--text-light); margin-bottom: 24px; line-height: 1.6; }
43
+ .modal-actions { display: flex; gap: 12px; }
44
+ .modal-actions button { flex: 1; }
45
+ .form-modal .modal-content { max-width: 500px; }
46
+ .form-modal input { margin-bottom: 12px; }
47
+ .form-modal .form-row { display: flex; flex-direction: column; gap: 12px; margin-bottom: 16px; }
48
+ .form-modal .oauth-steps { background: rgba(59,130,246,0.1); padding: 16px; border-radius: 8px; margin-bottom: 16px; border: 2px solid var(--info); }
49
+ .form-modal .oauth-steps p { margin-bottom: 8px; font-size: 14px; }
50
+ .form-modal .oauth-steps p:last-child { margin-bottom: 0; }
51
+
52
+ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
53
+ @keyframes scaleIn { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } }
54
+
55
+ .loading-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 9999; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 16px; }
56
+ .spinner { width: 48px; height: 48px; border: 4px solid rgba(255,255,255,0.2); border-top-color: white; border-radius: 50%; animation: spin 0.8s linear infinite; }
57
+ .loading-text { color: white; font-size: 16px; font-weight: 500; }
58
+ .help-tip { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; border-radius: 50%; background: var(--info); color: white; font-size: 12px; font-weight: 600; cursor: help; margin-left: 6px; }
59
+ .empty-state { text-align: center; padding: 60px 20px; color: var(--text-light); }
60
+ .empty-state-icon { font-size: 64px; margin-bottom: 16px; opacity: 0.5; }
61
+ .empty-state-text { font-size: 18px; font-weight: 500; margin-bottom: 8px; }
62
+ .empty-state-hint { font-size: 14px; }
63
+
64
+ * { margin: 0; padding: 0; box-sizing: border-box; }
65
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--bg); color: var(--text); line-height: 1.6; transition: background 0.3s; min-height: 100vh; }
66
+ .container { max-width: 1400px; margin: 0 auto; padding: 1rem; height: 100vh; display: flex; align-items: center; justify-content: center; }
67
+
68
+ .login-form { background: var(--card); border-radius: 1.5rem; box-shadow: var(--shadow); max-width: 480px; width: 100%; padding: 3rem; }
69
+ .login-form h2 { text-align: center; margin-bottom: 2.5rem; background: linear-gradient(135deg, var(--primary), var(--primary-dark)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; font-size: 2rem; font-weight: 700; }
70
+ .form-group { margin-bottom: 1.5rem; }
71
+ label { display: block; margin-bottom: 0.75rem; font-weight: 600; color: var(--text); font-size: 0.95rem; }
72
+ input { width: 100%; min-height: 48px; padding: 0.875rem 1rem; border: 2px solid var(--border); border-radius: 0.75rem; font-size: 0.95rem; line-height: 1.5; background: var(--card); color: var(--text); transition: all 0.2s; text-indent: 0.25rem; -webkit-appearance: none; }
73
+ input:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 4px rgba(99,102,241,0.1); }
74
+ input::placeholder { color: var(--text-light); opacity: 0.6; }
75
+
76
+ button { width: 100%; min-height: 52px; padding: 1rem; background: linear-gradient(135deg, var(--primary), var(--primary-dark)); color: white; border: none; border-radius: 0.75rem; cursor: pointer; font-size: 1.05rem; font-weight: 600; letter-spacing: 0.5px; transition: all 0.2s; box-shadow: 0 4px 12px rgba(99,102,241,0.2); position: relative; }
77
+ button:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(99,102,241,0.35); }
78
+ button:active:not(:disabled) { transform: translateY(0); box-shadow: 0 2px 8px rgba(99,102,241,0.25); }
79
+ button:disabled { opacity: 0.7; cursor: not-allowed; }
80
+ button.loading::after { content: ''; position: absolute; width: 16px; height: 16px; margin-left: 10px; border: 2px solid rgba(255,255,255,0.3); border-top-color: white; border-radius: 50%; animation: spin 0.6s linear infinite; }
81
+
82
+ @keyframes spin { to { transform: rotate(360deg); } }
83
+
84
+ .main-content { background: var(--card); border-radius: 1.5rem; box-shadow: var(--shadow); width: 100%; max-width: 1400px; height: calc(100vh - 2rem); display: flex; flex-direction: column; }
85
+ .header { display: flex; justify-content: space-between; align-items: center; padding: 1.5rem 2rem; border-bottom: 2px solid var(--border); flex-shrink: 0; }
86
+ .header h1 { font-size: 1.75rem; background: linear-gradient(135deg, var(--primary), var(--primary-dark)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; font-weight: 700; }
87
+ .header button { width: auto; padding: 0.625rem 1.5rem; font-size: 0.9rem; }
88
+ .tabs { display: flex; gap: 0.5rem; }
89
+ .tab { background: transparent; color: var(--text-light); border: none; padding: 0.625rem 1.25rem; border-radius: 0.5rem; cursor: pointer; font-weight: 600; transition: all 0.2s; }
90
+ .tab:hover { background: var(--bg); color: var(--text); }
91
+ .tab.active { background: linear-gradient(135deg, var(--primary), var(--primary-dark)); color: white; box-shadow: 0 2px 8px rgba(99,102,241,0.3); }
92
+ .content { padding: 2rem; flex: 1; overflow-y: auto; }
93
+ .content::-webkit-scrollbar { width: 8px; }
94
+ .content::-webkit-scrollbar-track { background: var(--bg); border-radius: 4px; }
95
+ .content::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
96
+ .content::-webkit-scrollbar-thumb:hover { background: var(--text-light); }
97
+
98
+ .add-form { background: var(--bg); padding: 1.5rem; border-radius: 1rem; margin-bottom: 2rem; border: 2px solid var(--border); }
99
+ .add-form h3 { margin-bottom: 1.25rem; font-size: 1.25rem; font-weight: 600; }
100
+ .btn-group { display: flex; gap: 0.75rem; margin-bottom: 1.25rem; flex-wrap: wrap; }
101
+ .btn-group .btn { width: auto; flex: 1; min-width: 140px; }
102
+ .oauth-box { background: rgba(251,191,36,0.1); padding: 1.5rem; border-radius: 1rem; margin-bottom: 1.25rem; border: 2px solid var(--warning); }
103
+ .oauth-box p { margin-bottom: 1rem; color: var(--text); font-size: 0.95rem; }
104
+ .form-row { display: flex; gap: 0.75rem; margin-bottom: 1rem; flex-wrap: wrap; }
105
+ .form-row input { flex: 1; min-width: 200px; }
106
+
107
+ .token-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 1.25rem; }
108
+ .token-card { background: var(--bg); border: 2px solid var(--border); border-radius: 1rem; padding: 1.5rem; transition: all 0.3s; position: relative; overflow: hidden; }
109
+ .token-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 4px; background: linear-gradient(90deg, var(--primary), var(--primary-dark)); opacity: 0; transition: opacity 0.3s; }
110
+ .token-card:hover { border-color: var(--primary); box-shadow: var(--shadow); transform: translateY(-2px); }
111
+ .token-card:hover::before { opacity: 1; }
112
+ .token-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
113
+ .token-id { font-size: 0.75rem; color: var(--text-light); font-family: monospace; }
114
+ .token-info { display: flex; flex-direction: column; gap: 0.75rem; margin-bottom: 1.25rem; }
115
+ .info-row { display: flex; align-items: center; gap: 0.5rem; font-size: 0.875rem; }
116
+ .info-label { color: var(--text-light); min-width: 80px; font-weight: 500; }
117
+ .info-value { color: var(--text); font-family: monospace; font-size: 0.8rem; }
118
+ .token-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; }
119
+
120
+ .btn { padding: 0.625rem 1rem; border: none; border-radius: 0.5rem; cursor: pointer; font-size: 0.875rem; font-weight: 600; transition: all 0.2s; white-space: nowrap; text-align: center; }
121
+ .btn:hover { transform: translateY(-1px); }
122
+ .btn-danger { background: var(--danger); color: white; }
123
+ .btn-warning { background: var(--warning); color: white; }
124
+ .btn-success { background: var(--success); color: white; }
125
+ .btn-secondary { background: #6b7280; color: white; }
126
+
127
+ .hidden { display: none; }
128
+ .status { padding: 0.375rem 0.875rem; border-radius: 9999px; font-size: 0.75rem; font-weight: 600; display: inline-flex; align-items: center; gap: 0.25rem; }
129
+ .status.enabled { background: rgba(16,185,129,0.15); color: var(--success); }
130
+ .status.disabled { background: rgba(239,68,68,0.15); color: var(--danger); }
131
+
132
+ .stats { display: flex; gap: 1.5rem; margin-bottom: 2rem; flex-wrap: wrap; }
133
+ .stat-card { flex: 1; min-width: 150px; background: linear-gradient(135deg, var(--primary), var(--primary-dark)); padding: 1.25rem; border-radius: 1rem; color: white; }
134
+ .stat-value { font-size: 2rem; font-weight: 700; margin-bottom: 0.25rem; }
135
+ .stat-label { font-size: 0.875rem; opacity: 0.9; }
136
+
137
+ .config-form { max-width: 800px; }
138
+ .config-form h3 { margin-bottom: 1.5rem; font-size: 1.5rem; }
139
+ .config-section { background: var(--bg); padding: 1.5rem; border-radius: 1rem; margin-bottom: 1.5rem; border: 2px solid var(--border); }
140
+ .config-section h4 { margin-bottom: 1rem; color: var(--primary); font-size: 1.1rem; }
141
+ .config-form .form-group { margin-bottom: 1rem; }
142
+ .config-form label { display: block; margin-bottom: 0.5rem; font-weight: 600; font-size: 0.9rem; }
143
+ .config-form input, .config-form select, .config-form textarea { width: 100%; padding: 0.75rem; border: 2px solid var(--border); border-radius: 0.5rem; background: var(--card); color: var(--text); font-size: 0.9rem; }
144
+ .config-form textarea { resize: vertical; font-family: inherit; }
145
+ .config-form button[type="submit"] { margin-top: 1rem; }
146
+
147
+ @media (max-width: 768px) {
148
+ .container { padding: 0; height: 100vh; }
149
+ .login-form { margin: 1rem; padding: 2rem; border-radius: 1rem; }
150
+ .login-form h2 { font-size: 1.5rem; }
151
+ .main-content { height: 100vh; border-radius: 0; }
152
+ .header { padding: 1rem; flex-wrap: wrap; gap: 1rem; }
153
+ .header h1 { font-size: 1.25rem; }
154
+ .tabs { width: 100%; }
155
+ .tab { flex: 1; }
156
+ .content { padding: 1rem; }
157
+ .token-grid { grid-template-columns: 1fr; }
158
+ .form-row { flex-direction: column; }
159
+ .form-row input { min-width: 100%; }
160
+ .btn-group { flex-direction: column; }
161
+ .btn-group .btn { width: 100%; }
162
+ .stats { flex-direction: column; }
163
+ .toast { right: 10px; left: 10px; min-width: auto; max-width: none; }
164
+ .modal { padding: 10px; }
165
+ }
scripts/oauth-server.js ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import http from 'http';
2
+ import { URL } from 'url';
3
+ import crypto from 'crypto';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import { fileURLToPath } from 'url';
7
+ import log from '../src/utils/logger.js';
8
+ import axios from 'axios';
9
+ import config from '../src/config/config.js';
10
+ import { generateProjectId } from '../src/utils/idGenerator.js';
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+ const ACCOUNTS_FILE = path.join(__dirname, '..', 'data', 'accounts.json');
15
+
16
+ const CLIENT_ID = '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com';
17
+ const CLIENT_SECRET = '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';
18
+ const STATE = crypto.randomUUID();
19
+
20
+ const SCOPES = [
21
+ 'https://www.googleapis.com/auth/cloud-platform',
22
+ 'https://www.googleapis.com/auth/userinfo.email',
23
+ 'https://www.googleapis.com/auth/userinfo.profile',
24
+ 'https://www.googleapis.com/auth/cclog',
25
+ 'https://www.googleapis.com/auth/experimentsandconfigs'
26
+ ];
27
+
28
+ function generateAuthUrl(port) {
29
+ const params = new URLSearchParams({
30
+ access_type: 'offline',
31
+ client_id: CLIENT_ID,
32
+ prompt: 'consent',
33
+ redirect_uri: `http://localhost:${port}/oauth-callback`,
34
+ response_type: 'code',
35
+ scope: SCOPES.join(' '),
36
+ state: STATE
37
+ });
38
+ return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
39
+ }
40
+
41
+ function getAxiosConfig() {
42
+ const axiosConfig = { timeout: config.timeout };
43
+ if (config.proxy) {
44
+ const proxyUrl = new URL(config.proxy);
45
+ axiosConfig.proxy = {
46
+ protocol: proxyUrl.protocol.replace(':', ''),
47
+ host: proxyUrl.hostname,
48
+ port: parseInt(proxyUrl.port)
49
+ };
50
+ }
51
+ return axiosConfig;
52
+ }
53
+
54
+ async function exchangeCodeForToken(code, port) {
55
+ const postData = new URLSearchParams({
56
+ code,
57
+ client_id: CLIENT_ID,
58
+ client_secret: CLIENT_SECRET,
59
+ redirect_uri: `http://localhost:${port}/oauth-callback`,
60
+ grant_type: 'authorization_code'
61
+ });
62
+
63
+ const response = await axios({
64
+ method: 'POST',
65
+ url: 'https://oauth2.googleapis.com/token',
66
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
67
+ data: postData.toString(),
68
+ ...getAxiosConfig()
69
+ });
70
+
71
+ return response.data;
72
+ }
73
+
74
+ async function fetchProjectId(accessToken) {
75
+ const response = await axios({
76
+ method: 'POST',
77
+ url: 'https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:loadCodeAssist',
78
+ headers: {
79
+ 'Host': 'daily-cloudcode-pa.sandbox.googleapis.com',
80
+ 'User-Agent': 'antigravity/1.11.9 windows/amd64',
81
+ 'Authorization': `Bearer ${accessToken}`,
82
+ 'Content-Type': 'application/json',
83
+ 'Accept-Encoding': 'gzip'
84
+ },
85
+ data: JSON.stringify({ metadata: { ideType: 'ANTIGRAVITY' } }),
86
+ ...getAxiosConfig()
87
+ });
88
+ return response.data?.cloudaicompanionProject;
89
+ }
90
+
91
+ const server = http.createServer((req, res) => {
92
+ const port = server.address().port;
93
+ const url = new URL(req.url, `http://localhost:${port}`);
94
+
95
+ if (url.pathname === '/oauth-callback') {
96
+ const code = url.searchParams.get('code');
97
+ const error = url.searchParams.get('error');
98
+
99
+ if (code) {
100
+ log.info('收到授权码,正在交换 Token...');
101
+ exchangeCodeForToken(code, port).then(async (tokenData) => {
102
+ const account = {
103
+ access_token: tokenData.access_token,
104
+ refresh_token: tokenData.refresh_token,
105
+ expires_in: tokenData.expires_in,
106
+ timestamp: Date.now()
107
+ };
108
+
109
+ if (config.skipProjectIdFetch) {
110
+ account.projectId = generateProjectId();
111
+ account.enable = true;
112
+ log.info('已跳过API验证,使用随机生成的projectId: ' + account.projectId);
113
+ } else {
114
+ log.info('正在验证账号资格...');
115
+ try {
116
+ const projectId = await fetchProjectId(account.access_token);
117
+ if (projectId === undefined) {
118
+ log.warn('该账号无资格使用(无法获取projectId),已跳过保存');
119
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
120
+ res.end('<h1>账号无资格</h1><p>该账号无法获取projectId,未保存。</p>');
121
+ setTimeout(() => server.close(), 1000);
122
+ return;
123
+ }
124
+ account.projectId = projectId;
125
+ account.enable = true;
126
+ log.info('账号验证通过');
127
+ } catch (err) {
128
+ log.error('验证账号资格失败:', err.message);
129
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
130
+ res.end('<h1>验证失败</h1><p>无法验证账号资格,请查看控制台。</p>');
131
+ setTimeout(() => server.close(), 1000);
132
+ return;
133
+ }
134
+ }
135
+
136
+ let accounts = [];
137
+ try {
138
+ if (fs.existsSync(ACCOUNTS_FILE)) {
139
+ accounts = JSON.parse(fs.readFileSync(ACCOUNTS_FILE, 'utf-8'));
140
+ }
141
+ } catch (err) {
142
+ log.warn('读取 accounts.json 失败,将创建新文件');
143
+ }
144
+
145
+ accounts.push(account);
146
+
147
+ const dir = path.dirname(ACCOUNTS_FILE);
148
+ if (!fs.existsSync(dir)) {
149
+ fs.mkdirSync(dir, { recursive: true });
150
+ }
151
+
152
+ fs.writeFileSync(ACCOUNTS_FILE, JSON.stringify(accounts, null, 2));
153
+
154
+ log.info(`Token 已保存到 ${ACCOUNTS_FILE}`);
155
+
156
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
157
+ res.end('<h1>授权成功!</h1><p>Token 已保存,可以关闭此页面。</p>');
158
+
159
+ setTimeout(() => server.close(), 1000);
160
+ }).catch(err => {
161
+ log.error('Token 交换失败:', err.message);
162
+
163
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
164
+ res.end('<h1>Token 获取失败</h1><p>查看控制台错误信息</p>');
165
+
166
+ setTimeout(() => server.close(), 1000);
167
+ });
168
+ } else {
169
+ log.error('授权失败:', error || '未收到授权码');
170
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
171
+ res.end('<h1>授权失败</h1>');
172
+ setTimeout(() => server.close(), 1000);
173
+ }
174
+ } else {
175
+ res.writeHead(404);
176
+ res.end('Not Found');
177
+ }
178
+ });
179
+
180
+ server.listen(0, () => {
181
+ const port = server.address().port;
182
+ const authUrl = generateAuthUrl(port);
183
+ log.info(`服务器运行在 http://localhost:${port}`);
184
+ log.info('请在浏览器中打开以下链接进行登录:');
185
+ console.log(`\n${authUrl}\n`);
186
+ log.info('等待授权回调...');
187
+ });
scripts/refresh-tokens.js ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import log from '../src/utils/logger.js';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const ACCOUNTS_FILE = path.join(__dirname, '..', 'data', 'accounts.json');
9
+
10
+ const CLIENT_ID = '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com';
11
+ const CLIENT_SECRET = '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';
12
+
13
+ async function refreshToken(refreshToken) {
14
+ const body = new URLSearchParams({
15
+ client_id: CLIENT_ID,
16
+ client_secret: CLIENT_SECRET,
17
+ grant_type: 'refresh_token',
18
+ refresh_token: refreshToken
19
+ });
20
+
21
+ const response = await fetch('https://oauth2.googleapis.com/token', {
22
+ method: 'POST',
23
+ headers: {
24
+ 'Host': 'oauth2.googleapis.com',
25
+ 'User-Agent': 'Go-http-client/1.1',
26
+ 'Content-Length': body.toString().length.toString(),
27
+ 'Content-Type': 'application/x-www-form-urlencoded',
28
+ 'Accept-Encoding': 'gzip'
29
+ },
30
+ body: body.toString()
31
+ });
32
+
33
+ if (!response.ok) {
34
+ throw new Error(`HTTP ${response.status}: ${await response.text()}`);
35
+ }
36
+
37
+ return await response.json();
38
+ }
39
+
40
+ async function refreshAllTokens() {
41
+ if (!fs.existsSync(ACCOUNTS_FILE)) {
42
+ log.error(`文件不存在: ${ACCOUNTS_FILE}`);
43
+ process.exit(1);
44
+ }
45
+
46
+ const accounts = JSON.parse(fs.readFileSync(ACCOUNTS_FILE, 'utf-8'));
47
+ log.info(`找到 ${accounts.length} 个账号`);
48
+
49
+ let successCount = 0;
50
+ let failCount = 0;
51
+
52
+ for (let i = 0; i < accounts.length; i++) {
53
+ const account = accounts[i];
54
+
55
+ if (account.enable === false) {
56
+ log.warn(`账号 ${i + 1}: 已禁用,跳过`);
57
+ continue;
58
+ }
59
+
60
+ try {
61
+ log.info(`刷新账号 ${i + 1}...`);
62
+ const tokenData = await refreshToken(account.refresh_token);
63
+ account.access_token = tokenData.access_token;
64
+ account.expires_in = tokenData.expires_in;
65
+ account.timestamp = Date.now();
66
+
67
+ successCount++;
68
+ log.info(`账号 ${i + 1}: 刷新成功`);
69
+ } catch (error) {
70
+ failCount++;
71
+ log.error(`账号 ${i + 1}: 刷新失败 - ${error.message}`);
72
+
73
+ if (error.message.includes('invalid_grant') || error.message.includes('400')) {
74
+ account.enable = false;
75
+ log.warn(`账号 ${i + 1}: Token 已失效或错误,已自动禁用该账号`);
76
+ }
77
+ }
78
+ }
79
+
80
+ fs.writeFileSync(ACCOUNTS_FILE, JSON.stringify(accounts, null, 2));
81
+ log.info(`刷新完成: 成功 ${successCount} 个, 失败 ${failCount} 个`);
82
+ }
83
+
84
+ refreshAllTokens().catch(err => {
85
+ log.error('刷新失败:', err.message);
86
+ process.exit(1);
87
+ });
src/AntigravityRequester.js ADDED
@@ -0,0 +1,292 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { spawn } from 'child_process';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import fs from 'fs';
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+
9
+ class antigravityRequester {
10
+ constructor(options = {}) {
11
+ this.binPath = options.binPath;
12
+ this.executablePath = options.executablePath || this._getExecutablePath();
13
+ this.proc = null;
14
+ this.requestId = 0;
15
+ this.pendingRequests = new Map();
16
+ this.buffer = '';
17
+ this.writeQueue = Promise.resolve();
18
+ }
19
+
20
+ _getExecutablePath() {
21
+ const platform = os.platform();
22
+ const arch = os.arch();
23
+
24
+ let filename;
25
+ if (platform === 'win32') {
26
+ filename = 'antigravity_requester_windows_amd64.exe';
27
+ } else if (platform === 'android') {
28
+ filename = 'antigravity_requester_android_arm64';
29
+ } else if (platform === 'linux') {
30
+ filename = 'antigravity_requester_linux_amd64';
31
+ } else {
32
+ throw new Error(`Unsupported platform: ${platform}`);
33
+ }
34
+
35
+ const binPath = this.binPath || path.join(__dirname, 'bin');
36
+ const requester_execPath = path.join(binPath, filename);
37
+ // 设置执行权限(非Windows平台)
38
+ if (platform !== 'win32') {
39
+ try {
40
+ fs.chmodSync(requester_execPath, 0o755);
41
+ } catch (error) {
42
+ console.warn(`Could not set executable permissions: ${error.message}`);
43
+ }
44
+ }
45
+ return requester_execPath;
46
+ }
47
+
48
+ _ensureProcess() {
49
+ if (this.proc) return;
50
+
51
+ this.proc = spawn(this.executablePath, [], {
52
+ stdio: ['pipe', 'pipe', 'pipe']
53
+ });
54
+
55
+ // 设置 stdin 为非阻塞模式
56
+ if (this.proc.stdin.setDefaultEncoding) {
57
+ this.proc.stdin.setDefaultEncoding('utf8');
58
+ }
59
+
60
+ // 增大 stdout 缓冲区以减少背压
61
+ if (this.proc.stdout.setEncoding) {
62
+ this.proc.stdout.setEncoding('utf8');
63
+ }
64
+
65
+ // 使用 setImmediate 异步处理数据,避免阻塞
66
+ this.proc.stdout.on('data', (data) => {
67
+ this.buffer += data.toString();
68
+
69
+ // 使用 setImmediate 异步处理,避免阻塞 stdout 读取
70
+ setImmediate(() => {
71
+ const lines = this.buffer.split('\n');
72
+ this.buffer = lines.pop();
73
+
74
+ for (const line of lines) {
75
+ if (!line.trim()) continue;
76
+ try {
77
+ const response = JSON.parse(line);
78
+ const pending = this.pendingRequests.get(response.id);
79
+ if (!pending) continue;
80
+
81
+ if (pending.streamResponse) {
82
+ pending.streamResponse._handleChunk(response);
83
+ if (response.type === 'end' || response.type === 'error') {
84
+ this.pendingRequests.delete(response.id);
85
+ }
86
+ } else {
87
+ this.pendingRequests.delete(response.id);
88
+ if (response.ok) {
89
+ pending.resolve(new antigravityResponse(response));
90
+ } else {
91
+ pending.reject(new Error(response.error || 'Request failed'));
92
+ }
93
+ }
94
+ } catch (e) {
95
+ console.error('Failed to parse response:', e, 'Line:', line);
96
+ }
97
+ }
98
+ });
99
+ });
100
+
101
+ this.proc.stderr.on('data', (data) => {
102
+ console.error('antigravityRequester stderr:', data.toString());
103
+ });
104
+
105
+ this.proc.on('close', () => {
106
+ this.proc = null;
107
+ for (const [id, pending] of this.pendingRequests) {
108
+ if (pending.reject) {
109
+ pending.reject(new Error('Process closed'));
110
+ } else if (pending.streamResponse && pending.streamResponse._onError) {
111
+ pending.streamResponse._onError(new Error('Process closed'));
112
+ }
113
+ }
114
+ this.pendingRequests.clear();
115
+ });
116
+ }
117
+
118
+ async antigravity_fetch(url, options = {}) {
119
+ this._ensureProcess();
120
+
121
+ const id = `req-${++this.requestId}`;
122
+ const request = {
123
+ id,
124
+ url,
125
+ method: options.method || 'GET',
126
+ headers: options.headers,
127
+ body: options.body,
128
+ timeout_ms: options.timeout || 30000,
129
+ proxy: options.proxy,
130
+ response_format: 'text',
131
+ ...options
132
+ };
133
+
134
+ return new Promise((resolve, reject) => {
135
+ this.pendingRequests.set(id, { resolve, reject });
136
+ this._writeRequest(request);
137
+ });
138
+ }
139
+
140
+ antigravity_fetchStream(url, options = {}) {
141
+ this._ensureProcess();
142
+
143
+ const id = `req-${++this.requestId}`;
144
+ const request = {
145
+ id,
146
+ url,
147
+ method: options.method || 'GET',
148
+ headers: options.headers,
149
+ body: options.body,
150
+ timeout_ms: options.timeout || 30000,
151
+ proxy: options.proxy,
152
+ stream: true,
153
+ ...options
154
+ };
155
+
156
+ const streamResponse = new StreamResponse(id);
157
+ this.pendingRequests.set(id, { streamResponse });
158
+ this._writeRequest(request);
159
+
160
+ return streamResponse;
161
+ }
162
+
163
+ _writeRequest(request) {
164
+ this.writeQueue = this.writeQueue.then(() => {
165
+ return new Promise((resolve, reject) => {
166
+ const data = JSON.stringify(request) + '\n';
167
+ const canWrite = this.proc.stdin.write(data);
168
+ if (canWrite) {
169
+ resolve();
170
+ } else {
171
+ // 等待 drain 事件
172
+ this.proc.stdin.once('drain', resolve);
173
+ this.proc.stdin.once('error', reject);
174
+ }
175
+ });
176
+ }).catch(err => {
177
+ console.error('Write request failed:', err);
178
+ });
179
+ }
180
+
181
+ close() {
182
+ if (this.proc) {
183
+ this.proc.stdin.end();
184
+ this.proc = null;
185
+ }
186
+ }
187
+ }
188
+
189
+ class StreamResponse {
190
+ constructor(id) {
191
+ this.id = id;
192
+ this.status = null;
193
+ this.statusText = null;
194
+ this.headers = null;
195
+ this.chunks = [];
196
+ this._onStart = null;
197
+ this._onData = null;
198
+ this._onEnd = null;
199
+ this._onError = null;
200
+ this._ended = false;
201
+ this._error = null;
202
+ this._textPromiseResolve = null;
203
+ this._textPromiseReject = null;
204
+ }
205
+
206
+ _handleChunk(chunk) {
207
+ if (chunk.type === 'start') {
208
+ this.status = chunk.status;
209
+ this.headers = new Map(Object.entries(chunk.headers || {}));
210
+ if (this._onStart) this._onStart({ status: chunk.status, headers: this.headers });
211
+ } else if (chunk.type === 'data') {
212
+ const data = chunk.encoding === 'base64'
213
+ ? Buffer.from(chunk.data, 'base64').toString('utf8')
214
+ : chunk.data;
215
+ this.chunks.push(data);
216
+ if (this._onData) this._onData(data);
217
+ } else if (chunk.type === 'end') {
218
+ this._ended = true;
219
+ if (this._textPromiseResolve) this._textPromiseResolve(this.chunks.join(''));
220
+ if (this._onEnd) this._onEnd();
221
+ } else if (chunk.type === 'error') {
222
+ this._ended = true;
223
+ this._error = new Error(chunk.error);
224
+ if (this._textPromiseReject) this._textPromiseReject(this._error);
225
+ if (this._onError) this._onError(this._error);
226
+ }
227
+ }
228
+
229
+ onStart(callback) {
230
+ this._onStart = callback;
231
+ return this;
232
+ }
233
+
234
+ onData(callback) {
235
+ this._onData = callback;
236
+ return this;
237
+ }
238
+
239
+ onEnd(callback) {
240
+ this._onEnd = callback;
241
+ return this;
242
+ }
243
+
244
+ onError(callback) {
245
+ this._onError = callback;
246
+ return this;
247
+ }
248
+
249
+ async text() {
250
+ if (this._ended) {
251
+ if (this._error) throw this._error;
252
+ return this.chunks.join('');
253
+ }
254
+ return new Promise((resolve, reject) => {
255
+ this._textPromiseResolve = resolve;
256
+ this._textPromiseReject = reject;
257
+ });
258
+ }
259
+ }
260
+
261
+ class antigravityResponse {
262
+ constructor(response) {
263
+ this._response = response;
264
+ this.ok = response.ok;
265
+ this.status = response.status;
266
+ this.statusText = response.status_text;
267
+ this.url = response.url;
268
+ this.headers = new Map(Object.entries(response.headers || {}));
269
+ this.redirected = response.redirected;
270
+ }
271
+
272
+ async text() {
273
+ if (this._response.body_encoding === 'base64') {
274
+ return Buffer.from(this._response.body, 'base64').toString('utf8');
275
+ }
276
+ return this._response.body;
277
+ }
278
+
279
+ async json() {
280
+ const text = await this.text();
281
+ return JSON.parse(text);
282
+ }
283
+
284
+ async buffer() {
285
+ if (this._response.body_encoding === 'base64') {
286
+ return Buffer.from(this._response.body, 'base64');
287
+ }
288
+ return Buffer.from(this._response.body, 'utf8');
289
+ }
290
+ }
291
+
292
+ export default antigravityRequester;
src/api/client.js ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import axios from 'axios';
2
+ import tokenManager from '../auth/token_manager.js';
3
+ import config from '../config/config.js';
4
+ import { generateToolCallId } from '../utils/idGenerator.js';
5
+ import AntigravityRequester from '../AntigravityRequester.js';
6
+ import { saveBase64Image } from '../utils/imageStorage.js';
7
+
8
+ // 请求客户端:优先使用 AntigravityRequester,失败则降级到 axios
9
+ let requester = null;
10
+ let useAxios = false;
11
+
12
+ if (config.useNativeAxios === true) {
13
+ useAxios = true;
14
+ } else {
15
+ try {
16
+ requester = new AntigravityRequester();
17
+ } catch (error) {
18
+ console.warn('AntigravityRequester 初始化失败,降级使用 axios:', error.message);
19
+ useAxios = true;
20
+ }
21
+ }
22
+
23
+ // ==================== 辅助函数 ====================
24
+
25
+ function buildHeaders(token) {
26
+ return {
27
+ 'Host': config.api.host,
28
+ 'User-Agent': config.api.userAgent,
29
+ 'Authorization': `Bearer ${token.access_token}`,
30
+ 'Content-Type': 'application/json',
31
+ 'Accept-Encoding': 'gzip'
32
+ };
33
+ }
34
+
35
+ function buildAxiosConfig(url, headers, body = null) {
36
+ const axiosConfig = {
37
+ method: 'POST',
38
+ url,
39
+ headers,
40
+ timeout: config.timeout,
41
+ proxy: config.proxy ? (() => {
42
+ const proxyUrl = new URL(config.proxy);
43
+ return { protocol: proxyUrl.protocol.replace(':', ''), host: proxyUrl.hostname, port: parseInt(proxyUrl.port) };
44
+ })() : false
45
+ };
46
+ if (body !== null) axiosConfig.data = body;
47
+ return axiosConfig;
48
+ }
49
+
50
+ function buildRequesterConfig(headers, body = null) {
51
+ const reqConfig = {
52
+ method: 'POST',
53
+ headers,
54
+ timeout_ms: config.timeout,
55
+ proxy: config.proxy
56
+ };
57
+ if (body !== null) reqConfig.body = JSON.stringify(body);
58
+ return reqConfig;
59
+ }
60
+
61
+ // 统一错误处理
62
+ async function handleApiError(error, token) {
63
+ const status = error.response?.status || error.status || 'Unknown';
64
+ let errorBody = error.message;
65
+
66
+ if (error.response?.data?.readable) {
67
+ const chunks = [];
68
+ for await (const chunk of error.response.data) {
69
+ chunks.push(chunk);
70
+ }
71
+ errorBody = Buffer.concat(chunks).toString();
72
+ } else if (typeof error.response?.data === 'object') {
73
+ errorBody = JSON.stringify(error.response.data, null, 2);
74
+ } else if (error.response?.data) {
75
+ errorBody = error.response.data;
76
+ }
77
+
78
+ if (status === 403) {
79
+ tokenManager.disableCurrentToken(token);
80
+ throw new Error(`该账号没有使用权限,已自动禁用。错误详情: ${errorBody}`);
81
+ }
82
+
83
+ throw new Error(`API请求失败 (${status}): ${errorBody}`);
84
+ }
85
+
86
+ // 转换 functionCall 为 OpenAI 格式
87
+ function convertToToolCall(functionCall) {
88
+ return {
89
+ id: functionCall.id || generateToolCallId(),
90
+ type: 'function',
91
+ function: {
92
+ name: functionCall.name,
93
+ arguments: JSON.stringify(functionCall.args)
94
+ }
95
+ };
96
+ }
97
+
98
+ // 解析并发送流式响应片段(会修改 state 并触发 callback)
99
+ function parseAndEmitStreamChunk(line, state, callback) {
100
+ if (!line.startsWith('data: ')) return;
101
+
102
+ try {
103
+ const data = JSON.parse(line.slice(6));
104
+ const parts = data.response?.candidates?.[0]?.content?.parts;
105
+
106
+ if (parts) {
107
+ for (const part of parts) {
108
+ if (part.thought === true) {
109
+ // 思维链内容
110
+ if (!state.thinkingStarted) {
111
+ callback({ type: 'thinking', content: '<think>\n' });
112
+ state.thinkingStarted = true;
113
+ }
114
+ callback({ type: 'thinking', content: part.text || '' });
115
+ } else if (part.text !== undefined) {
116
+ // 普通文本内容
117
+ if (state.thinkingStarted) {
118
+ callback({ type: 'thinking', content: '\n</think>\n' });
119
+ state.thinkingStarted = false;
120
+ }
121
+ callback({ type: 'text', content: part.text });
122
+ } else if (part.functionCall) {
123
+ // 工具调用
124
+ state.toolCalls.push(convertToToolCall(part.functionCall));
125
+ }
126
+ }
127
+ }
128
+
129
+ // 响应结束时发送工具调用
130
+ if (data.response?.candidates?.[0]?.finishReason && state.toolCalls.length > 0) {
131
+ if (state.thinkingStarted) {
132
+ callback({ type: 'thinking', content: '\n</think>\n' });
133
+ state.thinkingStarted = false;
134
+ }
135
+ callback({ type: 'tool_calls', tool_calls: state.toolCalls });
136
+ state.toolCalls = [];
137
+ }
138
+ } catch (e) {
139
+ // 忽略 JSON 解析错误
140
+ }
141
+ }
142
+
143
+ // ==================== 导出函数 ====================
144
+
145
+ export async function generateAssistantResponse(requestBody, token, callback) {
146
+
147
+ const headers = buildHeaders(token);
148
+ const state = { thinkingStarted: false, toolCalls: [] };
149
+ let buffer = ''; // 缓冲区:处理跨 chunk 的不完整行
150
+
151
+ const processChunk = (chunk) => {
152
+ buffer += chunk;
153
+ const lines = buffer.split('\n');
154
+ buffer = lines.pop(); // 保留最后一行(可能不完整)
155
+ lines.forEach(line => parseAndEmitStreamChunk(line, state, callback));
156
+ };
157
+
158
+ if (useAxios) {
159
+ try {
160
+ const axiosConfig = { ...buildAxiosConfig(config.api.url, headers, requestBody), responseType: 'stream' };
161
+ const response = await axios(axiosConfig);
162
+
163
+ response.data.on('data', chunk => processChunk(chunk.toString()));
164
+ await new Promise((resolve, reject) => {
165
+ response.data.on('end', resolve);
166
+ response.data.on('error', reject);
167
+ });
168
+ } catch (error) {
169
+ await handleApiError(error, token);
170
+ }
171
+ } else {
172
+ try {
173
+ const streamResponse = requester.antigravity_fetchStream(config.api.url, buildRequesterConfig(headers, requestBody));
174
+ let errorBody = '';
175
+ let statusCode = null;
176
+
177
+ await new Promise((resolve, reject) => {
178
+ streamResponse
179
+ .onStart(({ status }) => { statusCode = status; })
180
+ .onData((chunk) => statusCode !== 200 ? errorBody += chunk : processChunk(chunk))
181
+ .onEnd(() => statusCode !== 200 ? reject({ status: statusCode, message: errorBody }) : resolve())
182
+ .onError(reject);
183
+ });
184
+ } catch (error) {
185
+ await handleApiError(error, token);
186
+ }
187
+ }
188
+ }
189
+
190
+ export async function getAvailableModels() {
191
+ const token = await tokenManager.getToken();
192
+ if (!token) throw new Error('没有可用的token,请运行 npm run login 获取token');
193
+
194
+ const headers = buildHeaders(token);
195
+
196
+ try {
197
+ let data;
198
+ if (useAxios) {
199
+ data = (await axios(buildAxiosConfig(config.api.modelsUrl, headers, {}))).data;
200
+ } else {
201
+ const response = await requester.antigravity_fetch(config.api.modelsUrl, buildRequesterConfig(headers, {}));
202
+ if (response.status !== 200) {
203
+ const errorBody = await response.text();
204
+ throw { status: response.status, message: errorBody };
205
+ }
206
+ data = await response.json();
207
+ }
208
+ const modelList = Object.keys(data.models).map(id => ({
209
+ id,
210
+ object: 'model',
211
+ created: Math.floor(Date.now() / 1000),
212
+ owned_by: 'google'
213
+ }));
214
+ modelList.push({
215
+ id: "claude-opus-4-5",
216
+ object: 'model',
217
+ created: Math.floor(Date.now() / 1000),
218
+ owned_by: 'google'
219
+ })
220
+
221
+ return {
222
+ object: 'list',
223
+ data: modelList
224
+ };
225
+ } catch (error) {
226
+ await handleApiError(error, token);
227
+ }
228
+ }
229
+
230
+ export async function generateAssistantResponseNoStream(requestBody, token) {
231
+
232
+ const headers = buildHeaders(token);
233
+ let data;
234
+
235
+ try {
236
+ if (useAxios) {
237
+ data = (await axios(buildAxiosConfig(config.api.noStreamUrl, headers, requestBody))).data;
238
+ } else {
239
+ const response = await requester.antigravity_fetch(config.api.noStreamUrl, buildRequesterConfig(headers, requestBody));
240
+ if (response.status !== 200) {
241
+ const errorBody = await response.text();
242
+ throw { status: response.status, message: errorBody };
243
+ }
244
+ data = await response.json();
245
+ }
246
+ } catch (error) {
247
+ await handleApiError(error, token);
248
+ }
249
+
250
+ // 解析响应内容
251
+ const parts = data.response?.candidates?.[0]?.content?.parts || [];
252
+ let content = '';
253
+ let thinkingContent = '';
254
+ const toolCalls = [];
255
+ const imageUrls = [];
256
+
257
+ for (const part of parts) {
258
+ if (part.thought === true) {
259
+ thinkingContent += part.text || '';
260
+ } else if (part.text !== undefined) {
261
+ content += part.text;
262
+ } else if (part.functionCall) {
263
+ toolCalls.push(convertToToolCall(part.functionCall));
264
+ } else if (part.inlineData) {
265
+ // 保存图片到本地并获取 URL
266
+ const imageUrl = saveBase64Image(part.inlineData.data, part.inlineData.mimeType);
267
+ imageUrls.push(imageUrl);
268
+ }
269
+ }
270
+
271
+ // 拼接思维链标签
272
+ if (thinkingContent) {
273
+ content = `<think>\n${thinkingContent}\n</think>\n${content}`;
274
+ }
275
+
276
+ // 生图模型:转换为 markdown 格式
277
+ if (imageUrls.length > 0) {
278
+ let markdown = content ? content + '\n\n' : '';
279
+ markdown += imageUrls.map(url => `![image](${url})`).join('\n\n');
280
+ return { content: markdown, toolCalls };
281
+ }
282
+
283
+ return { content, toolCalls };
284
+ }
285
+
286
+ export function closeRequester() {
287
+ if (requester) requester.close();
288
+ }
src/auth/jwt.js ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import jwt from 'jsonwebtoken';
2
+ import config from '../config/config.js';
3
+
4
+ export const generateToken = (payload) => {
5
+ return jwt.sign(payload, config.admin.jwtSecret, { expiresIn: '24h' });
6
+ };
7
+
8
+ export const verifyToken = (token) => {
9
+ return jwt.verify(token, config.admin.jwtSecret);
10
+ };
11
+
12
+ export const authMiddleware = (req, res, next) => {
13
+ const authHeader = req.headers.authorization;
14
+ const token = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null;
15
+
16
+ if (!token) {
17
+ return res.status(401).json({ error: 'Token required' });
18
+ }
19
+
20
+ try {
21
+ const decoded = verifyToken(token);
22
+ req.user = decoded;
23
+ next();
24
+ } catch (error) {
25
+ return res.status(401).json({ error: 'Invalid token' });
26
+ }
27
+ };
src/auth/token_manager.js ADDED
@@ -0,0 +1,310 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 from '../config/config.js';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+
12
+ const CLIENT_ID = '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com';
13
+ const CLIENT_SECRET = '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';
14
+
15
+ class TokenManager {
16
+ constructor(filePath = path.join(__dirname,'..','..','data' ,'accounts.json')) {
17
+ this.filePath = filePath;
18
+ this.tokens = [];
19
+ this.currentIndex = 0;
20
+ this.ensureFileExists();
21
+ this.initialize();
22
+ }
23
+
24
+ ensureFileExists() {
25
+ const dir = path.dirname(this.filePath);
26
+ if (!fs.existsSync(dir)) {
27
+ fs.mkdirSync(dir, { recursive: true });
28
+ }
29
+ if (!fs.existsSync(this.filePath)) {
30
+ fs.writeFileSync(this.filePath, '[]', 'utf8');
31
+ log.info('✓ 已创建账号配置文件');
32
+ }
33
+ }
34
+
35
+ async initialize() {
36
+ try {
37
+ log.info('正在初始化token管理器...');
38
+ const data = fs.readFileSync(this.filePath, 'utf8');
39
+ let tokenArray = JSON.parse(data);
40
+
41
+ this.tokens = tokenArray.filter(token => token.enable !== false).map(token => ({
42
+ ...token,
43
+ sessionId: generateSessionId()
44
+ }));
45
+
46
+ this.currentIndex = 0;
47
+ if (this.tokens.length === 0) {
48
+ log.warn('⚠ 暂无可用账号,请使用以下方式添加:');
49
+ log.warn(' 方式1: 运行 npm run login 命令登录');
50
+ log.warn(' 方式2: 访问前端管理页面添加账号');
51
+ } else {
52
+ log.info(`成功加载 ${this.tokens.length} 个可用token`);
53
+ }
54
+ } catch (error) {
55
+ log.error('初始化token失败:', error.message);
56
+ this.tokens = [];
57
+ }
58
+ }
59
+
60
+ async fetchProjectId(token) {
61
+ const response = await axios({
62
+ method: 'POST',
63
+ url: 'https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:loadCodeAssist',
64
+ headers: {
65
+ 'Host': 'daily-cloudcode-pa.sandbox.googleapis.com',
66
+ 'User-Agent': 'antigravity/1.11.9 windows/amd64',
67
+ 'Authorization': `Bearer ${token.access_token}`,
68
+ 'Content-Type': 'application/json',
69
+ 'Accept-Encoding': 'gzip'
70
+ },
71
+ data: JSON.stringify({ metadata: { ideType: 'ANTIGRAVITY' } }),
72
+ timeout: config.timeout,
73
+ proxy: config.proxy ? (() => {
74
+ const proxyUrl = new URL(config.proxy);
75
+ return { protocol: proxyUrl.protocol.replace(':', ''), host: proxyUrl.hostname, port: parseInt(proxyUrl.port) };
76
+ })() : false
77
+ });
78
+ return response.data?.cloudaicompanionProject;
79
+ }
80
+
81
+ isExpired(token) {
82
+ if (!token.timestamp || !token.expires_in) return true;
83
+ const expiresAt = token.timestamp + (token.expires_in * 1000);
84
+ return Date.now() >= expiresAt - 300000;
85
+ }
86
+
87
+ async refreshToken(token) {
88
+ log.info('正在刷新token...');
89
+ const body = new URLSearchParams({
90
+ client_id: CLIENT_ID,
91
+ client_secret: CLIENT_SECRET,
92
+ grant_type: 'refresh_token',
93
+ refresh_token: token.refresh_token
94
+ });
95
+
96
+ try {
97
+ const response = await axios({
98
+ method: 'POST',
99
+ url: 'https://oauth2.googleapis.com/token',
100
+ headers: {
101
+ 'Host': 'oauth2.googleapis.com',
102
+ 'User-Agent': 'Go-http-client/1.1',
103
+ 'Content-Type': 'application/x-www-form-urlencoded',
104
+ 'Accept-Encoding': 'gzip'
105
+ },
106
+ data: body.toString(),
107
+ timeout: config.timeout,
108
+ proxy: config.proxy ? (() => {
109
+ const proxyUrl = new URL(config.proxy);
110
+ return { protocol: proxyUrl.protocol.replace(':', ''), host: proxyUrl.hostname, port: parseInt(proxyUrl.port) };
111
+ })() : false
112
+ });
113
+
114
+ token.access_token = response.data.access_token;
115
+ token.expires_in = response.data.expires_in;
116
+ token.timestamp = Date.now();
117
+ this.saveToFile();
118
+ return token;
119
+ } catch (error) {
120
+ throw { statusCode: error.response?.status, message: error.response?.data || error.message };
121
+ }
122
+ }
123
+
124
+ saveToFile() {
125
+ try {
126
+ const data = fs.readFileSync(this.filePath, 'utf8');
127
+ const allTokens = JSON.parse(data);
128
+
129
+ this.tokens.forEach(memToken => {
130
+ const index = allTokens.findIndex(t => t.refresh_token === memToken.refresh_token);
131
+ if (index !== -1) {
132
+ const { sessionId, ...tokenToSave } = memToken;
133
+ allTokens[index] = tokenToSave;
134
+ }
135
+ });
136
+
137
+ fs.writeFileSync(this.filePath, JSON.stringify(allTokens, null, 2), 'utf8');
138
+ } catch (error) {
139
+ log.error('保存文件失败:', error.message);
140
+ }
141
+ }
142
+
143
+ disableToken(token) {
144
+ log.warn(`禁用token ...${token.access_token.slice(-8)}`)
145
+ token.enable = false;
146
+ this.saveToFile();
147
+ this.tokens = this.tokens.filter(t => t.refresh_token !== token.refresh_token);
148
+ this.currentIndex = this.currentIndex % Math.max(this.tokens.length, 1);
149
+ }
150
+
151
+ async getToken() {
152
+ if (this.tokens.length === 0) return null;
153
+
154
+ //const startIndex = this.currentIndex;
155
+ const totalTokens = this.tokens.length;
156
+
157
+ for (let i = 0; i < totalTokens; i++) {
158
+ const token = this.tokens[this.currentIndex];
159
+
160
+ try {
161
+ if (this.isExpired(token)) {
162
+ await this.refreshToken(token);
163
+ }
164
+ if (!token.projectId) {
165
+ if (config.skipProjectIdFetch) {
166
+ token.projectId = generateProjectId();
167
+ this.saveToFile();
168
+ log.info(`...${token.access_token.slice(-8)}: 使用随机生成的projectId: ${token.projectId}`);
169
+ } else {
170
+ try {
171
+ const projectId = await this.fetchProjectId(token);
172
+ if (projectId === undefined) {
173
+ log.warn(`...${token.access_token.slice(-8)}: 无资格获取projectId,跳过保存`);
174
+ this.disableToken(token);
175
+ if (this.tokens.length === 0) return null;
176
+ continue;
177
+ }
178
+ token.projectId = projectId;
179
+ this.saveToFile();
180
+ } catch (error) {
181
+ log.error(`...${token.access_token.slice(-8)}: 获取projectId失败:`, error.message);
182
+ this.currentIndex = (this.currentIndex + 1) % this.tokens.length;
183
+ continue;
184
+ }
185
+ }
186
+ }
187
+ this.currentIndex = (this.currentIndex + 1) % this.tokens.length;
188
+ return token;
189
+ } catch (error) {
190
+ if (error.statusCode === 403 || error.statusCode === 400) {
191
+ log.warn(`...${token.access_token.slice(-8)}: Token 已失效或错误,已自动禁用该账号`);
192
+ this.disableToken(token);
193
+ if (this.tokens.length === 0) return null;
194
+ } else {
195
+ log.error(`...${token.access_token.slice(-8)} 刷新失败:`, error.message);
196
+ this.currentIndex = (this.currentIndex + 1) % this.tokens.length;
197
+ }
198
+ }
199
+ }
200
+
201
+ return null;
202
+ }
203
+
204
+ disableCurrentToken(token) {
205
+ const found = this.tokens.find(t => t.access_token === token.access_token);
206
+ if (found) {
207
+ this.disableToken(found);
208
+ }
209
+ }
210
+
211
+ // API管理方法
212
+ async reload() {
213
+ await this.initialize();
214
+ log.info('Token已热重载');
215
+ }
216
+
217
+ addToken(tokenData) {
218
+ try {
219
+ this.ensureFileExists();
220
+ const data = fs.readFileSync(this.filePath, 'utf8');
221
+ const allTokens = JSON.parse(data);
222
+
223
+ const newToken = {
224
+ access_token: tokenData.access_token,
225
+ refresh_token: tokenData.refresh_token,
226
+ expires_in: tokenData.expires_in || 3599,
227
+ timestamp: tokenData.timestamp || Date.now(),
228
+ enable: tokenData.enable !== undefined ? tokenData.enable : true
229
+ };
230
+
231
+ if (tokenData.projectId) {
232
+ newToken.projectId = tokenData.projectId;
233
+ }
234
+
235
+ allTokens.push(newToken);
236
+ fs.writeFileSync(this.filePath, JSON.stringify(allTokens, null, 2), 'utf8');
237
+
238
+ this.reload();
239
+ return { success: true, message: 'Token添加成功' };
240
+ } catch (error) {
241
+ log.error('添加Token失败:', error.message);
242
+ return { success: false, message: error.message };
243
+ }
244
+ }
245
+
246
+ updateToken(refreshToken, updates) {
247
+ try {
248
+ this.ensureFileExists();
249
+ const data = fs.readFileSync(this.filePath, 'utf8');
250
+ const allTokens = JSON.parse(data);
251
+
252
+ const index = allTokens.findIndex(t => t.refresh_token === refreshToken);
253
+ if (index === -1) {
254
+ return { success: false, message: 'Token不存在' };
255
+ }
256
+
257
+ allTokens[index] = { ...allTokens[index], ...updates };
258
+ fs.writeFileSync(this.filePath, JSON.stringify(allTokens, null, 2), 'utf8');
259
+
260
+ this.reload();
261
+ return { success: true, message: 'Token更新成功' };
262
+ } catch (error) {
263
+ log.error('更新Token失败:', error.message);
264
+ return { success: false, message: error.message };
265
+ }
266
+ }
267
+
268
+ deleteToken(refreshToken) {
269
+ try {
270
+ this.ensureFileExists();
271
+ const data = fs.readFileSync(this.filePath, 'utf8');
272
+ const allTokens = JSON.parse(data);
273
+
274
+ const filteredTokens = allTokens.filter(t => t.refresh_token !== refreshToken);
275
+ if (filteredTokens.length === allTokens.length) {
276
+ return { success: false, message: 'Token不存在' };
277
+ }
278
+
279
+ fs.writeFileSync(this.filePath, JSON.stringify(filteredTokens, null, 2), 'utf8');
280
+
281
+ this.reload();
282
+ return { success: true, message: 'Token删除成功' };
283
+ } catch (error) {
284
+ log.error('删除Token失败:', error.message);
285
+ return { success: false, message: error.message };
286
+ }
287
+ }
288
+
289
+ getTokenList() {
290
+ try {
291
+ this.ensureFileExists();
292
+ const data = fs.readFileSync(this.filePath, 'utf8');
293
+ const allTokens = JSON.parse(data);
294
+
295
+ return allTokens.map(token => ({
296
+ refresh_token: token.refresh_token,
297
+ access_token_suffix: token.access_token ? `...${token.access_token.slice(-8)}` : 'N/A',
298
+ expires_in: token.expires_in,
299
+ timestamp: token.timestamp,
300
+ enable: token.enable !== false,
301
+ projectId: token.projectId || null
302
+ }));
303
+ } catch (error) {
304
+ log.error('获取Token列表失败:', error.message);
305
+ return [];
306
+ }
307
+ }
308
+ }
309
+ const tokenManager = new TokenManager();
310
+ export default tokenManager;
src/bin/antigravity_requester_android_arm64 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:198c940794a575933d68803f15280658a3674eafb45c90e76b6c8f7cf360da94
3
+ size 8192353
src/bin/antigravity_requester_linux_amd64 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1375c1b833468b4b04998753f76697412920b1d32aeddfc8ba6daaf98723d63f
3
+ size 7930040
src/bin/antigravity_requester_windows_amd64.exe ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e957f08b72e519e470641ab327dcd59514d6fe5005b238379e501672fa67bb41
3
+ size 8097792
src/config/config.js ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = path.dirname(__filename);
9
+ const envPath = path.join(__dirname, '../../.env');
10
+ const configJsonPath = path.join(__dirname, '../../config.json');
11
+
12
+ // 确保 .env 存在
13
+ if (!fs.existsSync(envPath)) {
14
+ const examplePath = path.join(__dirname, '../../.env.example');
15
+ if (fs.existsSync(examplePath)) {
16
+ fs.copyFileSync(examplePath, envPath);
17
+ log.info('✓ 已从 .env.example 创建 .env 文件');
18
+ }
19
+ }
20
+
21
+ // 加载 config.json
22
+ let jsonConfig = {};
23
+ if (fs.existsSync(configJsonPath)) {
24
+ jsonConfig = JSON.parse(fs.readFileSync(configJsonPath, 'utf8'));
25
+ }
26
+
27
+ // 加载 .env
28
+ dotenv.config();
29
+
30
+ const config = {
31
+ server: {
32
+ port: jsonConfig.server?.port || 8045,
33
+ host: jsonConfig.server?.host || '0.0.0.0'
34
+ },
35
+ imageBaseUrl: process.env.IMAGE_BASE_URL || null,
36
+ maxImages: jsonConfig.other?.maxImages || 10,
37
+ api: {
38
+ url: jsonConfig.api?.url || 'https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:streamGenerateContent?alt=sse',
39
+ modelsUrl: jsonConfig.api?.modelsUrl || 'https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:fetchAvailableModels',
40
+ noStreamUrl: jsonConfig.api?.noStreamUrl || 'https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:generateContent',
41
+ host: jsonConfig.api?.host || 'daily-cloudcode-pa.sandbox.googleapis.com',
42
+ userAgent: jsonConfig.api?.userAgent || 'antigravity/1.11.3 windows/amd64'
43
+ },
44
+ defaults: {
45
+ temperature: jsonConfig.defaults?.temperature || 1,
46
+ top_p: jsonConfig.defaults?.topP || 0.85,
47
+ top_k: jsonConfig.defaults?.topK || 50,
48
+ max_tokens: jsonConfig.defaults?.maxTokens || 8096
49
+ },
50
+ security: {
51
+ maxRequestSize: jsonConfig.server?.maxRequestSize || '50mb',
52
+ apiKey: process.env.API_KEY || null
53
+ },
54
+ admin: {
55
+ username: process.env.ADMIN_USERNAME || 'admin',
56
+ password: process.env.ADMIN_PASSWORD || 'admin123',
57
+ jwtSecret: process.env.JWT_SECRET || 'your-jwt-secret-key-change-this-in-production'
58
+ },
59
+ useNativeAxios: jsonConfig.other?.useNativeAxios !== false,
60
+ timeout: jsonConfig.other?.timeout || 180000,
61
+ proxy: process.env.PROXY || null,
62
+ systemInstruction: process.env.SYSTEM_INSTRUCTION || '',
63
+ skipProjectIdFetch: jsonConfig.other?.skipProjectIdFetch === true
64
+ };
65
+
66
+ log.info('✓ 配置加载成功');
67
+
68
+ export default config;
69
+
70
+ export function getConfigJson() {
71
+ if (fs.existsSync(configJsonPath)) {
72
+ return JSON.parse(fs.readFileSync(configJsonPath, 'utf8'));
73
+ }
74
+ return {};
75
+ }
76
+
77
+ export function saveConfigJson(data) {
78
+ fs.writeFileSync(configJsonPath, JSON.stringify(data, null, 2), 'utf8');
79
+ }
src/config/init-env.js ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+ const envPath = path.join(__dirname, '../../.env');
8
+
9
+ const sensitiveKeys = ['API_KEY', 'ADMIN_USERNAME', 'ADMIN_PASSWORD', 'JWT_SECRET', 'PROXY', 'SYSTEM_INSTRUCTION', 'IMAGE_BASE_URL'];
10
+
11
+ if (fs.existsSync(envPath)) {
12
+ let envContent = fs.readFileSync(envPath, 'utf8');
13
+ let updated = false;
14
+
15
+ sensitiveKeys.forEach(key => {
16
+ const value = process.env[key];
17
+ if (value !== undefined && value !== '') {
18
+ const regex = new RegExp(`^${key}=.*$`, 'm');
19
+ if (regex.test(envContent)) {
20
+ envContent = envContent.replace(regex, `${key}=${value}`);
21
+ } else {
22
+ envContent += `\n${key}=${value}`;
23
+ }
24
+ updated = true;
25
+ }
26
+ });
27
+
28
+ if (updated) {
29
+ fs.writeFileSync(envPath, envContent, 'utf8');
30
+ console.log('✓ 环境变量已同步到 .env');
31
+ }
32
+ }
src/routes/admin.js ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import { generateToken, authMiddleware } from '../auth/jwt.js';
3
+ import tokenManager from '../auth/token_manager.js';
4
+ import config, { getConfigJson, saveConfigJson } from '../config/config.js';
5
+ import logger from '../utils/logger.js';
6
+ import { generateProjectId } from '../utils/idGenerator.js';
7
+ import axios from 'axios';
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import { fileURLToPath } from 'url';
11
+ import dotenv from 'dotenv';
12
+
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+ const envPath = path.join(__dirname, '../../.env');
16
+ const configJsonPath = path.join(__dirname, '../../config.json');
17
+
18
+ const router = express.Router();
19
+
20
+ // 登录接口
21
+ router.post('/login', (req, res) => {
22
+ const { username, password } = req.body;
23
+
24
+ if (username === config.admin.username && password === config.admin.password) {
25
+ const token = generateToken({ username, role: 'admin' });
26
+ res.json({ success: true, token });
27
+ } else {
28
+ res.status(401).json({ success: false, message: '用户名或密码错误' });
29
+ }
30
+ });
31
+
32
+ // Token管理API - 需要JWT认证
33
+ router.get('/tokens', authMiddleware, (req, res) => {
34
+ const tokens = tokenManager.getTokenList();
35
+ res.json({ success: true, data: tokens });
36
+ });
37
+
38
+ router.post('/tokens', authMiddleware, (req, res) => {
39
+ const { access_token, refresh_token, expires_in, timestamp, enable, projectId } = req.body;
40
+ if (!access_token || !refresh_token) {
41
+ return res.status(400).json({ success: false, message: 'access_token和refresh_token必填' });
42
+ }
43
+ const tokenData = { access_token, refresh_token, expires_in };
44
+ if (timestamp) tokenData.timestamp = timestamp;
45
+ if (enable !== undefined) tokenData.enable = enable;
46
+ if (projectId) tokenData.projectId = projectId;
47
+
48
+ const result = tokenManager.addToken(tokenData);
49
+ res.json(result);
50
+ });
51
+
52
+ router.put('/tokens/:refreshToken', authMiddleware, (req, res) => {
53
+ const { refreshToken } = req.params;
54
+ const updates = req.body;
55
+ const result = tokenManager.updateToken(refreshToken, updates);
56
+ res.json(result);
57
+ });
58
+
59
+ router.delete('/tokens/:refreshToken', authMiddleware, (req, res) => {
60
+ const { refreshToken } = req.params;
61
+ const result = tokenManager.deleteToken(refreshToken);
62
+ res.json(result);
63
+ });
64
+
65
+ router.post('/tokens/reload', authMiddleware, async (req, res) => {
66
+ try {
67
+ await tokenManager.reload();
68
+ res.json({ success: true, message: 'Token已热重载' });
69
+ } catch (error) {
70
+ logger.error('热重载失败:', error.message);
71
+ res.status(500).json({ success: false, message: error.message });
72
+ }
73
+ });
74
+
75
+ router.post('/oauth/exchange', authMiddleware, async (req, res) => {
76
+ const { code, port } = req.body;
77
+ if (!code || !port) {
78
+ return res.status(400).json({ success: false, message: 'code和port必填' });
79
+ }
80
+
81
+ const CLIENT_ID = '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com';
82
+ const CLIENT_SECRET = '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';
83
+
84
+ try {
85
+ const postData = new URLSearchParams({
86
+ code,
87
+ client_id: CLIENT_ID,
88
+ client_secret: CLIENT_SECRET,
89
+ redirect_uri: `http://localhost:${port}/oauth-callback`,
90
+ grant_type: 'authorization_code'
91
+ });
92
+
93
+ const response = await fetch('https://oauth2.googleapis.com/token', {
94
+ method: 'POST',
95
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
96
+ body: postData.toString()
97
+ });
98
+
99
+ const tokenData = await response.json();
100
+
101
+ if (!tokenData.access_token) {
102
+ return res.status(400).json({ success: false, message: 'Token交换失败' });
103
+ }
104
+
105
+ const account = {
106
+ access_token: tokenData.access_token,
107
+ refresh_token: tokenData.refresh_token,
108
+ expires_in: tokenData.expires_in,
109
+ timestamp: Date.now(),
110
+ enable: true
111
+ };
112
+
113
+ if (config.skipProjectIdFetch) {
114
+ account.projectId = generateProjectId();
115
+ logger.info('使用随机生成的projectId: ' + account.projectId);
116
+ } else {
117
+ try {
118
+ const projectResponse = await axios({
119
+ method: 'POST',
120
+ url: 'https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:loadCodeAssist',
121
+ headers: {
122
+ 'Host': 'daily-cloudcode-pa.sandbox.googleapis.com',
123
+ 'User-Agent': 'antigravity/1.11.9 windows/amd64',
124
+ 'Authorization': `Bearer ${account.access_token}`,
125
+ 'Content-Type': 'application/json',
126
+ 'Accept-Encoding': 'gzip'
127
+ },
128
+ data: JSON.stringify({ metadata: { ideType: 'ANTIGRAVITY' } }),
129
+ timeout: config.timeout,
130
+ proxy: config.proxy ? (() => {
131
+ const proxyUrl = new URL(config.proxy);
132
+ return { protocol: proxyUrl.protocol.replace(':', ''), host: proxyUrl.hostname, port: parseInt(proxyUrl.port) };
133
+ })() : false
134
+ });
135
+
136
+ const projectId = projectResponse.data?.cloudaicompanionProject;
137
+ if (projectId === undefined) {
138
+ return res.status(400).json({ success: false, message: '该账号无资格使用(无法获取projectId)' });
139
+ }
140
+ account.projectId = projectId;
141
+ logger.info('账号验证通过,projectId: ' + projectId);
142
+ } catch (error) {
143
+ logger.error('验证账号资格失败:', error.message);
144
+ return res.status(500).json({ success: false, message: '验证账号资格失败: ' + error.message });
145
+ }
146
+ }
147
+
148
+ res.json({ success: true, data: account });
149
+ } catch (error) {
150
+ logger.error('Token交换失败:', error.message);
151
+ res.status(500).json({ success: false, message: error.message });
152
+ }
153
+ });
154
+
155
+ // 获取配置
156
+ router.get('/config', authMiddleware, (req, res) => {
157
+ try {
158
+ const envData = {};
159
+ const envContent = fs.readFileSync(envPath, 'utf8');
160
+ envContent.split('\n').forEach(line => {
161
+ line = line.trim();
162
+ if (line && !line.startsWith('#')) {
163
+ const [key, ...valueParts] = line.split('=');
164
+ if (key) envData[key.trim()] = valueParts.join('=').trim();
165
+ }
166
+ });
167
+
168
+ const jsonData = getConfigJson();
169
+ res.json({ success: true, data: { env: envData, json: jsonData } });
170
+ } catch (error) {
171
+ logger.error('读取配置失败:', error.message);
172
+ res.status(500).json({ success: false, message: error.message });
173
+ }
174
+ });
175
+
176
+ // 更新配置
177
+ router.put('/config', authMiddleware, (req, res) => {
178
+ try {
179
+ const { env: envUpdates, json: jsonUpdates } = req.body;
180
+
181
+ // 更新 .env(只保留敏感信息)
182
+ if (envUpdates) {
183
+ let envContent = fs.readFileSync(envPath, 'utf8');
184
+ Object.entries(envUpdates).forEach(([key, value]) => {
185
+ const regex = new RegExp(`^${key}=.*$`, 'm');
186
+ if (regex.test(envContent)) {
187
+ envContent = envContent.replace(regex, `${key}=${value}`);
188
+ } else {
189
+ envContent += `\n${key}=${value}`;
190
+ }
191
+ });
192
+ fs.writeFileSync(envPath, envContent, 'utf8');
193
+ }
194
+
195
+ // 更新 config.json
196
+ if (jsonUpdates) {
197
+ saveConfigJson(jsonUpdates);
198
+ }
199
+
200
+ // 重新加载环境变量
201
+ dotenv.config({ override: true });
202
+
203
+ // 更新config对象
204
+ const jsonConfig = getConfigJson();
205
+ config.server.port = jsonConfig.server?.port || 8045;
206
+ config.server.host = jsonConfig.server?.host || '0.0.0.0';
207
+ config.defaults.temperature = jsonConfig.defaults?.temperature || 1;
208
+ config.defaults.top_p = jsonConfig.defaults?.topP || 0.85;
209
+ config.defaults.top_k = jsonConfig.defaults?.topK || 50;
210
+ config.defaults.max_tokens = jsonConfig.defaults?.maxTokens || 8096;
211
+ config.security.apiKey = process.env.API_KEY || null;
212
+ config.timeout = jsonConfig.other?.timeout || 180000;
213
+ config.proxy = process.env.PROXY || null;
214
+ config.systemInstruction = process.env.SYSTEM_INSTRUCTION || '';
215
+ config.skipProjectIdFetch = jsonConfig.other?.skipProjectIdFetch === true;
216
+ config.maxImages = jsonConfig.other?.maxImages || 10;
217
+ config.useNativeAxios = jsonConfig.other?.useNativeAxios !== false;
218
+ config.api.url = jsonConfig.api?.url || 'https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:streamGenerateContent?alt=sse';
219
+ config.api.modelsUrl = jsonConfig.api?.modelsUrl || 'https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:fetchAvailableModels';
220
+ config.api.noStreamUrl = jsonConfig.api?.noStreamUrl || 'https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:generateContent';
221
+ config.api.host = jsonConfig.api?.host || 'daily-cloudcode-pa.sandbox.googleapis.com';
222
+ config.api.userAgent = jsonConfig.api?.userAgent || 'antigravity/1.11.3 windows/amd64';
223
+
224
+ logger.info('配置已更新并热重载');
225
+ res.json({ success: true, message: '配置已保存并生效(端口/HOST修改需重启)' });
226
+ } catch (error) {
227
+ logger.error('更新配置失败:', error.message);
228
+ res.status(500).json({ success: false, message: error.message });
229
+ }
230
+ });
231
+
232
+ export default router;
src/server/index.js ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { generateAssistantResponse, generateAssistantResponseNoStream, getAvailableModels, closeRequester } from '../api/client.js';
5
+ import { generateRequestBody } from '../utils/utils.js';
6
+ import logger from '../utils/logger.js';
7
+ import config from '../config/config.js';
8
+ import tokenManager from '../auth/token_manager.js';
9
+ import adminRouter from '../routes/admin.js';
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+
14
+ const app = express();
15
+
16
+ // 工具函数:生成响应元数据
17
+ const createResponseMeta = () => ({
18
+ id: `chatcmpl-${Date.now()}`,
19
+ created: Math.floor(Date.now() / 1000)
20
+ });
21
+
22
+ // 工具函数:设置流式响应头
23
+ const setStreamHeaders = (res) => {
24
+ res.setHeader('Content-Type', 'text/event-stream');
25
+ res.setHeader('Cache-Control', 'no-cache');
26
+ res.setHeader('Connection', 'keep-alive');
27
+ };
28
+
29
+ // 工具函数:构建流式数据块
30
+ const createStreamChunk = (id, created, model, delta, finish_reason = null) => ({
31
+ id,
32
+ object: 'chat.completion.chunk',
33
+ created,
34
+ model,
35
+ choices: [{ index: 0, delta, finish_reason }]
36
+ });
37
+
38
+ // 工具函数:写入流式数据
39
+ const writeStreamData = (res, data) => {
40
+ res.write(`data: ${JSON.stringify(data)}\n\n`);
41
+ };
42
+
43
+ // 工具函数:结束流式响应
44
+ const endStream = (res, id, created, model, finish_reason) => {
45
+ writeStreamData(res, createStreamChunk(id, created, model, {}, finish_reason));
46
+ res.write('data: [DONE]\n\n');
47
+ res.end();
48
+ };
49
+
50
+ app.use(express.json({ limit: config.security.maxRequestSize }));
51
+
52
+ // 静态文件服务
53
+ app.use('/images', express.static(path.join(__dirname, '../../public/images')));
54
+ app.use(express.static(path.join(__dirname, '../../public')));
55
+
56
+ // 管理路由
57
+ app.use('/admin', adminRouter);
58
+
59
+ app.use((err, req, res, next) => {
60
+ if (err.type === 'entity.too.large') {
61
+ return res.status(413).json({ error: `请求体过大,最大支持 ${config.security.maxRequestSize}` });
62
+ }
63
+ next(err);
64
+ });
65
+
66
+ app.use((req, res, next) => {
67
+ const ignorePaths = ['/images', '/favicon.ico', '/.well-known'];
68
+ if (!ignorePaths.some(path => req.path.startsWith(path))) {
69
+ const start = Date.now();
70
+ res.on('finish', () => {
71
+ logger.request(req.method, req.path, res.statusCode, Date.now() - start);
72
+ });
73
+ }
74
+ next();
75
+ });
76
+
77
+ app.use((req, res, next) => {
78
+ if (req.path.startsWith('/v1/')) {
79
+ const apiKey = config.security?.apiKey;
80
+ if (apiKey) {
81
+ const authHeader = req.headers.authorization;
82
+ const providedKey = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : authHeader;
83
+ if (providedKey !== apiKey) {
84
+ logger.warn(`API Key 验证失败: ${req.method} ${req.path}`);
85
+ return res.status(401).json({ error: 'Invalid API Key' });
86
+ }
87
+ }
88
+ }
89
+ next();
90
+ });
91
+
92
+ app.get('/v1/models', async (req, res) => {
93
+ try {
94
+ const models = await getAvailableModels();
95
+ res.json(models);
96
+ } catch (error) {
97
+ logger.error('获取模型列表失败:', error.message);
98
+ res.status(500).json({ error: error.message });
99
+ }
100
+ });
101
+
102
+
103
+
104
+ app.post('/v1/chat/completions', async (req, res) => {
105
+ const { messages, model, stream = false, tools, ...params} = req.body;
106
+ try {
107
+ if (!messages) {
108
+ return res.status(400).json({ error: 'messages is required' });
109
+ }
110
+ const token = await tokenManager.getToken();
111
+ if (!token) {
112
+ throw new Error('没有可用的token,请运行 npm run login 获取token');
113
+ }
114
+ const isImageModel = model.includes('-image');
115
+ const requestBody = generateRequestBody(messages, model, params, tools, token);
116
+ if (isImageModel) {
117
+ requestBody.request.generationConfig={
118
+ candidateCount: 1,
119
+ // imageConfig:{
120
+ // aspectRatio: "1:1"
121
+ // }
122
+ }
123
+ requestBody.requestType="image_gen";
124
+ //requestBody.request.systemInstruction.parts[0].text += "现在你作为绘画模型聚焦于帮助用户生成图片";
125
+ delete requestBody.request.systemInstruction;
126
+ delete requestBody.request.tools;
127
+ delete requestBody.request.toolConfig;
128
+ }
129
+ //console.log(JSON.stringify(requestBody,null,2))
130
+
131
+ const { id, created } = createResponseMeta();
132
+
133
+ if (stream) {
134
+ setStreamHeaders(res);
135
+
136
+ if (isImageModel) {
137
+ const { content } = await generateAssistantResponseNoStream(requestBody, token);
138
+ writeStreamData(res, createStreamChunk(id, created, model, { content }));
139
+ endStream(res, id, created, model, 'stop');
140
+ } else {
141
+ let hasToolCall = false;
142
+ await generateAssistantResponse(requestBody, token, (data) => {
143
+ const delta = data.type === 'tool_calls'
144
+ ? { tool_calls: data.tool_calls }
145
+ : { content: data.content };
146
+ if (data.type === 'tool_calls') hasToolCall = true;
147
+ writeStreamData(res, createStreamChunk(id, created, model, delta));
148
+ });
149
+ endStream(res, id, created, model, hasToolCall ? 'tool_calls' : 'stop');
150
+ }
151
+ } else {
152
+ const { content, toolCalls } = await generateAssistantResponseNoStream(requestBody, token);
153
+ const message = { role: 'assistant', content };
154
+ if (toolCalls.length > 0) message.tool_calls = toolCalls;
155
+
156
+ res.json({
157
+ id,
158
+ object: 'chat.completion',
159
+ created,
160
+ model,
161
+ choices: [{
162
+ index: 0,
163
+ message,
164
+ finish_reason: toolCalls.length > 0 ? 'tool_calls' : 'stop'
165
+ }]
166
+ });
167
+ }
168
+ } catch (error) {
169
+ logger.error('生成响应失败:', error.message);
170
+ if (!res.headersSent) {
171
+ const { id, created } = createResponseMeta();
172
+ const errorContent = `错误: ${error.message}`;
173
+
174
+ if (stream) {
175
+ setStreamHeaders(res);
176
+ writeStreamData(res, createStreamChunk(id, created, model, { content: errorContent }));
177
+ endStream(res, id, created, model, 'stop');
178
+ } else {
179
+ res.json({
180
+ id,
181
+ object: 'chat.completion',
182
+ created,
183
+ model,
184
+ choices: [{
185
+ index: 0,
186
+ message: { role: 'assistant', content: errorContent },
187
+ finish_reason: 'stop'
188
+ }]
189
+ });
190
+ }
191
+ }
192
+ }
193
+ });
194
+
195
+ const server = app.listen(config.server.port, config.server.host, () => {
196
+ logger.info(`服务器已启动: ${config.server.host}:${config.server.port}`);
197
+ });
198
+
199
+ server.on('error', (error) => {
200
+ if (error.code === 'EADDRINUSE') {
201
+ logger.error(`端口 ${config.server.port} 已被占用`);
202
+ process.exit(1);
203
+ } else if (error.code === 'EACCES') {
204
+ logger.error(`端口 ${config.server.port} 无权限访问`);
205
+ process.exit(1);
206
+ } else {
207
+ logger.error('服务器启动失败:', error.message);
208
+ process.exit(1);
209
+ }
210
+ });
211
+
212
+ const shutdown = () => {
213
+ logger.info('正在关闭服务器...');
214
+ closeRequester();
215
+ server.close(() => {
216
+ logger.info('服务器已关闭');
217
+ process.exit(0);
218
+ });
219
+ setTimeout(() => process.exit(0), 5000);
220
+ };
221
+
222
+ process.on('SIGINT', shutdown);
223
+ process.on('SIGTERM', shutdown);
src/utils/idGenerator.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { randomUUID } from 'crypto';
2
+
3
+ function generateRequestId() {
4
+ return `agent-${randomUUID()}`;
5
+ }
6
+
7
+ function generateSessionId() {
8
+ return String(-Math.floor(Math.random() * 9e18));
9
+ }
10
+
11
+ function generateProjectId() {
12
+ const adjectives = ['useful', 'bright', 'swift', 'calm', 'bold'];
13
+ const nouns = ['fuze', 'wave', 'spark', 'flow', 'core'];
14
+ const randomAdj = adjectives[Math.floor(Math.random() * adjectives.length)];
15
+ const randomNoun = nouns[Math.floor(Math.random() * nouns.length)];
16
+ const randomNum = Math.random().toString(36).substring(2, 7);
17
+ return `${randomAdj}-${randomNoun}-${randomNum}`;
18
+ }
19
+
20
+ function generateToolCallId() {
21
+ return `call_${randomUUID().replace(/-/g, '')}`;
22
+ }
23
+
24
+ export {
25
+ generateProjectId,
26
+ generateSessionId,
27
+ generateRequestId,
28
+ generateToolCallId
29
+ }
src/utils/imageStorage.js ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ const IMAGE_DIR = path.join(__dirname, '../../public/images');
11
+
12
+ // 确保图片目录存在
13
+ if (!fs.existsSync(IMAGE_DIR)) {
14
+ fs.mkdirSync(IMAGE_DIR, { recursive: true });
15
+ }
16
+
17
+ // MIME 类型到文件扩展名映射
18
+ const MIME_TO_EXT = {
19
+ 'image/jpeg': 'jpg',
20
+ 'image/png': 'png',
21
+ 'image/gif': 'gif',
22
+ 'image/webp': 'webp'
23
+ };
24
+
25
+ /**
26
+ * 清理超过限制数量的旧图片
27
+ * @param {number} maxCount - 最大保留图片数量
28
+ */
29
+ function cleanOldImages(maxCount = 10) {
30
+ const files = fs.readdirSync(IMAGE_DIR)
31
+ .filter(f => /\.(jpg|jpeg|png|gif|webp)$/i.test(f))
32
+ .map(f => ({
33
+ name: f,
34
+ path: path.join(IMAGE_DIR, f),
35
+ mtime: fs.statSync(path.join(IMAGE_DIR, f)).mtime.getTime()
36
+ }))
37
+ .sort((a, b) => b.mtime - a.mtime);
38
+
39
+ if (files.length > maxCount) {
40
+ files.slice(maxCount).forEach(f => fs.unlinkSync(f.path));
41
+ }
42
+ }
43
+
44
+ /**
45
+ * 保存 base64 图片到本地并返回访问 URL
46
+ * @param {string} base64Data - base64 编码的图片数据
47
+ * @param {string} mimeType - 图片 MIME 类型
48
+ * @returns {string} 图片访问 URL
49
+ */
50
+ export function saveBase64Image(base64Data, mimeType) {
51
+ const ext = MIME_TO_EXT[mimeType] || 'jpg';
52
+ const filename = `${Date.now()}_${Math.random().toString(36).slice(2, 9)}.${ext}`;
53
+ const filepath = path.join(IMAGE_DIR, filename);
54
+
55
+ // 解码并保存
56
+ const buffer = Buffer.from(base64Data, 'base64');
57
+ fs.writeFileSync(filepath, buffer);
58
+
59
+ // 清理旧图片
60
+ cleanOldImages(config.maxImages);
61
+
62
+ // 返回访问 URL
63
+ const baseUrl = config.imageBaseUrl || `http://${getDefaultIp()}:${config.server.port}`;
64
+ return `${baseUrl}/images/${filename}`;
65
+ }
src/utils/logger.js ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const colors = {
2
+ reset: '\x1b[0m',
3
+ green: '\x1b[32m',
4
+ yellow: '\x1b[33m',
5
+ red: '\x1b[31m',
6
+ cyan: '\x1b[36m',
7
+ gray: '\x1b[90m'
8
+ };
9
+
10
+ function logMessage(level, ...args) {
11
+ const timestamp = new Date().toLocaleTimeString('zh-CN', { hour12: false });
12
+ const color = { info: colors.green, warn: colors.yellow, error: colors.red }[level];
13
+ console.log(`${colors.gray}${timestamp}${colors.reset} ${color}[${level}]${colors.reset}`, ...args);
14
+ }
15
+
16
+ function logRequest(method, path, status, duration) {
17
+ const statusColor = status >= 500 ? colors.red : status >= 400 ? colors.yellow : colors.green;
18
+ console.log(`${colors.cyan}[${method}]${colors.reset} - ${path} ${statusColor}${status}${colors.reset} ${colors.gray}${duration}ms${colors.reset}`);
19
+ }
20
+
21
+ export const log = {
22
+ info: (...args) => logMessage('info', ...args),
23
+ warn: (...args) => logMessage('warn', ...args),
24
+ error: (...args) => logMessage('error', ...args),
25
+ request: logRequest
26
+ };
27
+
28
+ export default log;
src/utils/utils.js ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import config from '../config/config.js';
2
+ import tokenManager from '../auth/token_manager.js';
3
+ import { generateRequestId } from './idGenerator.js';
4
+ import os from 'os';
5
+
6
+ function extractImagesFromContent(content) {
7
+ const result = { text: '', images: [] };
8
+
9
+ // 如果content是字符串,直接返回
10
+ if (typeof content === 'string') {
11
+ result.text = content;
12
+ return result;
13
+ }
14
+
15
+ // 如果content是数组(multimodal格式)
16
+ if (Array.isArray(content)) {
17
+ for (const item of content) {
18
+ if (item.type === 'text') {
19
+ result.text += item.text;
20
+ } else if (item.type === 'image_url') {
21
+ // 提取base64图片数据
22
+ const imageUrl = item.image_url?.url || '';
23
+
24
+ // 匹配 data:image/{format};base64,{data} 格式
25
+ const match = imageUrl.match(/^data:image\/(\w+);base64,(.+)$/);
26
+ if (match) {
27
+ const format = match[1]; // 例如 png, jpeg, jpg
28
+ const base64Data = match[2];
29
+ result.images.push({
30
+ inlineData: {
31
+ mimeType: `image/${format}`,
32
+ data: base64Data
33
+ }
34
+ })
35
+ }
36
+ }
37
+ }
38
+ }
39
+
40
+ return result;
41
+ }
42
+ function handleUserMessage(extracted, antigravityMessages){
43
+ antigravityMessages.push({
44
+ role: "user",
45
+ parts: [
46
+ {
47
+ text: extracted.text
48
+ },
49
+ ...extracted.images
50
+ ]
51
+ })
52
+ }
53
+ function handleAssistantMessage(message, antigravityMessages){
54
+ const lastMessage = antigravityMessages[antigravityMessages.length - 1];
55
+ const hasToolCalls = message.tool_calls && message.tool_calls.length > 0;
56
+ const hasContent = message.content && message.content.trim() !== '';
57
+
58
+ const antigravityTools = hasToolCalls ? message.tool_calls.map(toolCall => ({
59
+ functionCall: {
60
+ id: toolCall.id,
61
+ name: toolCall.function.name,
62
+ args: {
63
+ query: toolCall.function.arguments
64
+ }
65
+ }
66
+ })) : [];
67
+
68
+ if (lastMessage?.role === "model" && hasToolCalls && !hasContent){
69
+ lastMessage.parts.push(...antigravityTools)
70
+ }else{
71
+ const parts = [];
72
+ if (hasContent) parts.push({ text: message.content });
73
+ parts.push(...antigravityTools);
74
+
75
+ antigravityMessages.push({
76
+ role: "model",
77
+ parts
78
+ })
79
+ }
80
+ }
81
+ function handleToolCall(message, antigravityMessages){
82
+ // 从之前的 model 消息中找到对应的 functionCall name
83
+ let functionName = '';
84
+ for (let i = antigravityMessages.length - 1; i >= 0; i--) {
85
+ if (antigravityMessages[i].role === 'model') {
86
+ const parts = antigravityMessages[i].parts;
87
+ for (const part of parts) {
88
+ if (part.functionCall && part.functionCall.id === message.tool_call_id) {
89
+ functionName = part.functionCall.name;
90
+ break;
91
+ }
92
+ }
93
+ if (functionName) break;
94
+ }
95
+ }
96
+
97
+ const lastMessage = antigravityMessages[antigravityMessages.length - 1];
98
+ const functionResponse = {
99
+ functionResponse: {
100
+ id: message.tool_call_id,
101
+ name: functionName,
102
+ response: {
103
+ output: message.content
104
+ }
105
+ }
106
+ };
107
+
108
+ // 如果上一条消息是 user 且包含 functionResponse,则合并
109
+ if (lastMessage?.role === "user" && lastMessage.parts.some(p => p.functionResponse)) {
110
+ lastMessage.parts.push(functionResponse);
111
+ } else {
112
+ antigravityMessages.push({
113
+ role: "user",
114
+ parts: [functionResponse]
115
+ });
116
+ }
117
+ }
118
+ function openaiMessageToAntigravity(openaiMessages){
119
+ const antigravityMessages = [];
120
+ for (const message of openaiMessages) {
121
+ if (message.role === "user" || message.role === "system") {
122
+ const extracted = extractImagesFromContent(message.content);
123
+ handleUserMessage(extracted, antigravityMessages);
124
+ } else if (message.role === "assistant") {
125
+ handleAssistantMessage(message, antigravityMessages);
126
+ } else if (message.role === "tool") {
127
+ handleToolCall(message, antigravityMessages);
128
+ }
129
+ }
130
+
131
+ return antigravityMessages;
132
+ }
133
+ function generateGenerationConfig(parameters, enableThinking, actualModelName){
134
+ const generationConfig = {
135
+ topP: parameters.top_p ?? config.defaults.top_p,
136
+ topK: parameters.top_k ?? config.defaults.top_k,
137
+ temperature: parameters.temperature ?? config.defaults.temperature,
138
+ candidateCount: 1,
139
+ maxOutputTokens: parameters.max_tokens ?? config.defaults.max_tokens,
140
+ stopSequences: [
141
+ "<|user|>",
142
+ "<|bot|>",
143
+ "<|context_request|>",
144
+ "<|endoftext|>",
145
+ "<|end_of_turn|>"
146
+ ],
147
+ thinkingConfig: {
148
+ includeThoughts: enableThinking,
149
+ thinkingBudget: enableThinking ? 1024 : 0
150
+ }
151
+ }
152
+ if (enableThinking && actualModelName.includes("claude")){
153
+ delete generationConfig.topP;
154
+ }
155
+ return generationConfig
156
+ }
157
+ function convertOpenAIToolsToAntigravity(openaiTools){
158
+ if (!openaiTools || openaiTools.length === 0) return [];
159
+ return openaiTools.map((tool)=>{
160
+ delete tool.function.parameters.$schema;
161
+ return {
162
+ functionDeclarations: [
163
+ {
164
+ name: tool.function.name,
165
+ description: tool.function.description,
166
+ parameters: tool.function.parameters
167
+ }
168
+ ]
169
+ }
170
+ })
171
+ }
172
+
173
+ function modelMapping(modelName){
174
+ if (modelName === "claude-sonnet-4-5-thinking"){
175
+ return "claude-sonnet-4-5";
176
+ } else if (modelName === "claude-opus-4-5"){
177
+ return "claude-opus-4-5-thinking";
178
+ } else if (modelName === "gemini-2.5-flash-thinking"){
179
+ return "gemini-2.5-flash";
180
+ }
181
+ return modelName;
182
+ }
183
+
184
+ function isEnableThinking(modelName){
185
+ return modelName.endsWith('-thinking') ||
186
+ modelName === 'gemini-2.5-pro' ||
187
+ modelName.startsWith('gemini-3-pro-') ||
188
+ modelName === "rev19-uic3-1p" ||
189
+ modelName === "gpt-oss-120b-medium"
190
+ }
191
+
192
+ function generateRequestBody(openaiMessages,modelName,parameters,openaiTools,token){
193
+
194
+ const enableThinking = isEnableThinking(modelName);
195
+ const actualModelName = modelMapping(modelName);
196
+
197
+ return{
198
+ project: token.projectId,
199
+ requestId: generateRequestId(),
200
+ request: {
201
+ contents: openaiMessageToAntigravity(openaiMessages),
202
+ systemInstruction: {
203
+ role: "user",
204
+ parts: [{ text: config.systemInstruction }]
205
+ },
206
+ tools: convertOpenAIToolsToAntigravity(openaiTools),
207
+ toolConfig: {
208
+ functionCallingConfig: {
209
+ mode: "VALIDATED"
210
+ }
211
+ },
212
+ generationConfig: generateGenerationConfig(parameters, enableThinking, actualModelName),
213
+ sessionId: token.sessionId
214
+ },
215
+ model: actualModelName,
216
+ userAgent: "antigravity"
217
+ }
218
+ }
219
+ function getDefaultIp(){
220
+ const interfaces = os.networkInterfaces();
221
+ if (interfaces.WLAN){
222
+ for (const inter of interfaces.WLAN){
223
+ if (inter.family === 'IPv4' && !inter.internal){
224
+ return inter.address;
225
+ }
226
+ }
227
+ } else if (interfaces.wlan2) {
228
+ for (const inter of interfaces.wlan2) {
229
+ if (inter.family === 'IPv4' && !inter.internal) {
230
+ return inter.address;
231
+ }
232
+ }
233
+ }
234
+ return '127.0.0.1';
235
+ }
236
+ export{
237
+ generateRequestId,
238
+ generateRequestBody,
239
+ getDefaultIp
240
+ }
test/test-get-ip.js ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os from 'os';
2
+ function getLocalIp() {
3
+ const interfaces = os.networkInterfaces();
4
+ console.log(JSON.stringify(interfaces, null, 2))
5
+ if (interfaces.WLAN) {
6
+ for (const inter of interfaces.WLAN) {
7
+ if (inter.family === 'IPv4' && !inter.internal) {
8
+ return inter.address;
9
+ }
10
+ }
11
+ } else if (interfaces.wlan2) {
12
+ for (const inter of interfaces.wlan2) {
13
+ if (inter.family === 'IPv4' && !inter.internal) {
14
+ return inter.address;
15
+ }
16
+ }
17
+ return '127.0.0.1';
18
+ }
19
+ }
20
+
21
+ console.log(getLocalIp());
test/test-image-generation.js ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ const API_URL = 'http://localhost:8045/v1/chat/completions';
5
+ const API_KEY = 'sk-text';
6
+
7
+ async function testImageGeneration(stream = true) {
8
+ console.log(`测试生图模型 (${stream ? '流式' : '非流式'})...\n`);
9
+
10
+ const response = await fetch(API_URL, {
11
+ method: 'POST',
12
+ headers: {
13
+ 'Content-Type': 'application/json',
14
+ 'Authorization': `Bearer ${API_KEY}`
15
+ },
16
+ body: JSON.stringify({
17
+ model: 'gemini-2.5-flash-image',
18
+ messages: [{ role: 'user', content: '画一个二次元美少女' }],
19
+ stream
20
+ })
21
+ });
22
+
23
+ let fullContent = '';
24
+
25
+ if (stream) {
26
+ let buffer = '';
27
+ const reader = response.body.getReader();
28
+ const decoder = new TextDecoder();
29
+
30
+ while (true) {
31
+ const { done, value } = await reader.read();
32
+ if (done) break;
33
+
34
+ buffer += decoder.decode(value, { stream: true });
35
+ const lines = buffer.split('\n');
36
+ buffer = lines.pop();
37
+
38
+ for (const line of lines) {
39
+ if (!line.startsWith('data: ') || line.includes('[DONE]')) continue;
40
+ try {
41
+ const data = JSON.parse(line.slice(6));
42
+ const content = data.choices[0]?.delta?.content;
43
+ if (content) fullContent = content;
44
+ } catch (e) {}
45
+ }
46
+ }
47
+ } else {
48
+ const data = await response.json();
49
+ fullContent = data.choices[0]?.message?.content || '';
50
+ }
51
+
52
+ console.log('响应内容:\n', fullContent.substring(0, 200), '...\n');
53
+
54
+ // 提取markdown中的图片
55
+ const imageRegex = /!\[.*?\]\((data:image\/(.*?);base64,([^)]+))\)/g;
56
+ let match;
57
+ let imageCount = 0;
58
+
59
+ while ((match = imageRegex.exec(fullContent)) !== null) {
60
+ imageCount++;
61
+ const base64Data = match[3];
62
+ const ext = match[2];
63
+ const filename = `generated_${Date.now()}_${imageCount}.${ext}`;
64
+ const filepath = path.join('test', filename);
65
+
66
+ fs.writeFileSync(filepath, Buffer.from(base64Data, 'base64'));
67
+ console.log(`✓ 图片已保存: ${filepath}`);
68
+ }
69
+
70
+ if (imageCount === 0) {
71
+ console.log('✗ 未找到图片');
72
+ } else {
73
+ console.log(`\n✓ 共保存 ${imageCount} 张图片`);
74
+ }
75
+ }
76
+
77
+ (async () => {
78
+ // await testImageGeneration(true);
79
+ // console.log('\n' + '='.repeat(50) + '\n');
80
+ await testImageGeneration(false);
81
+ })().catch(console.error);
test/test-request.js ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { generateRequestBody } from '../src/utils/utils.js';
2
+ import tokenManager from '../src/auth/token_manager.js';
3
+ import config from '../src/config/config.js';
4
+
5
+ async function testRequest() {
6
+ try {
7
+ const token = await tokenManager.getToken();
8
+
9
+ const tools = [{
10
+ type: 'function',
11
+ function: {
12
+ name: 'get_weather',
13
+ description: '获取天气信息',
14
+ parameters: {
15
+ type: 'object',
16
+ properties: {
17
+ location: { type: 'string', description: '城市名称' }
18
+ },
19
+ required: ['location']
20
+ }
21
+ }
22
+ }];
23
+
24
+ const requestBody = await generateRequestBody(
25
+ [{ role: 'user', content: '你是谁?' }],
26
+ 'gemini-3-pro-high',
27
+ {},
28
+ []
29
+ //tools
30
+ );
31
+
32
+ const response = await fetch('https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:generateContent', {
33
+ method: 'POST',
34
+ headers: {
35
+ 'Host': config.api.host,
36
+ 'User-Agent': config.api.userAgent,
37
+ 'Authorization': `Bearer ${token.access_token}`,
38
+ 'Content-Type': 'application/json',
39
+ 'Accept-Encoding': 'gzip'
40
+ },
41
+ body: JSON.stringify(requestBody)
42
+ });
43
+
44
+ const result = await response.json();
45
+ console.log(JSON.stringify(result, null, 2));
46
+ } catch (error) {
47
+ console.error('Error:', error.message);
48
+ }
49
+ }
50
+
51
+ testRequest();
test/test-token-rotation.js ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import tokenManager from '../src/auth/token_manager.js';
2
+
3
+ async function testTokenRotation() {
4
+ console.log('=== 开始测试 Token 轮询 ===\n');
5
+
6
+ const totalTests = 10;
7
+ const usedTokens = new Map();
8
+
9
+ console.log(`初始状态: ${tokenManager.tokens.length} 个可用账号\n`);
10
+
11
+ for (let i = 1; i <= totalTests; i++) {
12
+ console.log(`--- 第 ${i} 次请求 ---`);
13
+
14
+ const token = await tokenManager.getToken();
15
+
16
+ if (!token) {
17
+ console.log('❌ 无可用 token\n');
18
+ break;
19
+ }
20
+
21
+ const tokenId = token.refresh_token.slice(-8);
22
+ console.log(`✓ 获取到 token: ...${tokenId}`);
23
+ console.log(` 当前索引: ${tokenManager.currentIndex}`);
24
+ console.log(` 剩余账号: ${tokenManager.tokens.length}\n`);
25
+
26
+ usedTokens.set(tokenId, (usedTokens.get(tokenId) || 0) + 1);
27
+ }
28
+
29
+ console.log('=== 轮询统计 ===');
30
+ console.log(`总请求次数: ${totalTests}`);
31
+ console.log(`使用的不同账号数: ${usedTokens.size}`);
32
+ console.log('\n各账号使用次数:');
33
+ usedTokens.forEach((count, tokenId) => {
34
+ console.log(` ...${tokenId}: ${count} 次`);
35
+ });
36
+
37
+ if (usedTokens.size === tokenManager.tokens.length) {
38
+ console.log('\n✅ 所有账号都被正确轮换使用');
39
+ } else {
40
+ console.log('\n⚠️ 部分账号未被使用');
41
+ }
42
+ }
43
+
44
+ testTokenRotation().catch(console.error);
test/test-transform.js ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { generateRequestBody } from './utils.js';
2
+
3
+ // 测试场景:user -> assistant -> assistant(工具调用,无content) -> tool1结果 -> tool2结果
4
+ const testMessages = [
5
+ {
6
+ role: "user",
7
+ content: "帮我查询天气和新闻"
8
+ },
9
+ {
10
+ role: "assistant",
11
+ content: "好的,我来帮你查询。"
12
+ },
13
+ {
14
+ role: "assistant",
15
+ content: "",
16
+ tool_calls: [
17
+ {
18
+ id: "call_001",
19
+ type: "function",
20
+ function: {
21
+ name: "get_weather",
22
+ arguments: JSON.stringify({ city: "北京" })
23
+ }
24
+ },
25
+ {
26
+ id: "call_002",
27
+ type: "function",
28
+ function: {
29
+ name: "get_news",
30
+ arguments: JSON.stringify({ category: "科技" })
31
+ }
32
+ }
33
+ ]
34
+ },
35
+ {
36
+ role: "tool",
37
+ tool_call_id: "call_001",
38
+ content: "北京今天晴,温度25度"
39
+ },
40
+ {
41
+ role: "tool",
42
+ tool_call_id: "call_002",
43
+ content: "最新科技新闻:AI技术突破"
44
+ }
45
+ ];
46
+
47
+ const testTools = [
48
+ {
49
+ type: "function",
50
+ function: {
51
+ name: "get_weather",
52
+ description: "获取天气信息",
53
+ parameters: {
54
+ type: "object",
55
+ properties: {
56
+ city: { type: "string" }
57
+ }
58
+ }
59
+ }
60
+ },
61
+ {
62
+ type: "function",
63
+ function: {
64
+ name: "get_news",
65
+ description: "获取新闻",
66
+ parameters: {
67
+ type: "object",
68
+ properties: {
69
+ category: { type: "string" }
70
+ }
71
+ }
72
+ }
73
+ }
74
+ ];
75
+
76
+ console.log("=== 测试消息转换 ===\n");
77
+ console.log("输入 OpenAI 格式消息:");
78
+ console.log(JSON.stringify(testMessages, null, 2));
79
+
80
+ const result = generateRequestBody(testMessages, "claude-sonnet-4-5", {}, testTools);
81
+
82
+ console.log("\n=== 转换后的 Antigravity 格式 ===\n");
83
+ console.log(JSON.stringify(result.request.contents, null, 2));
84
+
85
+ console.log("\n=== 验证结果 ===");
86
+ const contents = result.request.contents;
87
+ console.log(`✓ 消息数量: ${contents.length}`);
88
+ console.log(`✓ 第1条 (user): ${contents[0]?.role === 'user' ? '✓' : '✗'}`);
89
+ console.log(`✓ 第2条 (model): ${contents[1]?.role === 'model' ? '✓' : '✗'}`);
90
+ console.log(`✓ 第3条 (model+tools): ${contents[2]?.role === 'model' && contents[2]?.parts?.length === 2 ? '✓' : '✗'}`);
91
+ console.log(`✓ 第4条 (tool1 response): ${contents[3]?.role === 'user' && contents[3]?.parts[0]?.functionResponse ? '✓' : '✗'}`);
92
+ console.log(`✓ 第5条 (tool2 response): ${contents[4]?.role === 'user' && contents[4]?.parts[0]?.functionResponse ? '✓' : '✗'}`);