liuw15 commited on
Commit
d244d35
·
1 Parent(s): 89083cb

增加一个简易的前端方便管理,增加dockerfile和docker-compose.yml

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 CHANGED
@@ -27,5 +27,9 @@ TIMEOUT=180000
27
  # PROXY=http://127.0.0.1:7897
28
  SKIP_PROJECT_ID_FETCH=false # 跳过从API获取projectId,直接随机生成(适用于Pro订阅账号)
29
 
 
 
 
 
30
  # 系统提示词
31
  SYSTEM_INSTRUCTION=你是聊天机器人,名字叫萌萌,如同名字这般,你的性格是软软糯糯萌萌哒的,专门为用户提供聊天和情绪价值,协助进行小说创作或者角色扮演
 
27
  # PROXY=http://127.0.0.1:7897
28
  SKIP_PROJECT_ID_FETCH=false # 跳过从API获取projectId,直接随机生成(适用于Pro订阅账号)
29
 
30
+ # 管理员认证
31
+ ADMIN_USERNAME=admin
32
+ ADMIN_PASSWORD=admin123
33
+
34
  # 系统提示词
35
  SYSTEM_INSTRUCTION=你是聊天机器人,名字叫萌萌,如同名字这般,你的性格是软软糯糯萌萌哒的,专门为用户提供聊天和情绪价值,协助进行小说创作或者角色扮演
.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 CHANGED
@@ -19,4 +19,4 @@ Thumbs.db
19
  data/
20
  test/*.png
21
  test/*.jpeg
22
- public/
 
19
  data/
20
  test/*.png
21
  test/*.jpeg
22
+ public/images
Dockerfile ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # 创建数据和图片目录
15
+ RUN mkdir -p data public/images
16
+
17
+ # 暴露端口
18
+ EXPOSE 8045
19
+
20
+ # 启动应用
21
+ CMD ["npm", "start"]
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 部署生产环境
docker-compose.yml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ antigravity2api:
5
+ build: .
6
+ ports:
7
+ - "8045:8045"
8
+ env_file:
9
+ - .env
10
+ volumes:
11
+ - ./data:/app/data
12
+ - ./public/images:/app/public/images
13
+ - ./.env:/app/.env
14
+ restart: unless-stopped
package-lock.json CHANGED
@@ -11,7 +11,8 @@
11
  "dependencies": {
12
  "axios": "^1.13.2",
13
  "dotenv": "^17.2.3",
14
- "express": "^5.1.0"
 
15
  },
16
  "engines": {
17
  "node": ">=18.0.0"
@@ -67,6 +68,12 @@
67
  "node": ">=18"
68
  }
69
  },
 
 
 
 
 
 
70
  "node_modules/bytes": {
71
  "version": "3.1.2",
72
  "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -218,6 +225,15 @@
218
  "node": ">= 0.4"
219
  }
220
  },
 
 
 
 
 
 
 
 
 
221
  "node_modules/ee-first": {
222
  "version": "1.1.1",
223
  "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -582,6 +598,91 @@
582
  "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
583
  "license": "MIT"
584
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
585
  "node_modules/math-intrinsics": {
586
  "version": "1.1.0",
587
  "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -790,12 +891,44 @@
790
  "node": ">= 18"
791
  }
792
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
793
  "node_modules/safer-buffer": {
794
  "version": "2.1.2",
795
  "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
796
  "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
797
  "license": "MIT"
798
  },
 
 
 
 
 
 
 
 
 
 
 
 
799
  "node_modules/send": {
800
  "version": "1.2.0",
801
  "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
 
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"
 
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",
 
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",
 
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",
 
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",
package.json CHANGED
@@ -21,7 +21,8 @@
21
  "dependencies": {
22
  "axios": "^1.13.2",
23
  "dotenv": "^17.2.3",
24
- "express": "^5.1.0"
 
25
  },
26
  "engines": {
27
  "node": ">=18.0.0"
 
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"
public/app.js ADDED
@@ -0,0 +1,465 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ Object.entries(data.data).forEach(([key, value]) => {
429
+ const input = form.elements[key];
430
+ if (input) input.value = value || '';
431
+ });
432
+ }
433
+ } catch (error) {
434
+ showToast('加载配置失败: ' + error.message, 'error');
435
+ }
436
+ }
437
+
438
+ document.getElementById('configForm').addEventListener('submit', async (e) => {
439
+ e.preventDefault();
440
+ const formData = new FormData(e.target);
441
+ const config = Object.fromEntries(formData);
442
+
443
+ showLoading('正在保存配置...');
444
+ try {
445
+ const response = await fetch('/admin/config', {
446
+ method: 'PUT',
447
+ headers: {
448
+ 'Content-Type': 'application/json',
449
+ 'Authorization': `Bearer ${authToken}`
450
+ },
451
+ body: JSON.stringify(config)
452
+ });
453
+
454
+ const data = await response.json();
455
+ hideLoading();
456
+ if (data.success) {
457
+ showToast(data.message, 'success');
458
+ } else {
459
+ showToast(data.message || '保存失败', 'error');
460
+ }
461
+ } catch (error) {
462
+ hideLoading();
463
+ showToast('保存失败: ' + error.message, 'error');
464
+ }
465
+ });
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
+ }
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 CHANGED
@@ -189,6 +189,100 @@ class TokenManager {
189
  this.disableToken(found);
190
  }
191
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  }
193
  const tokenManager = new TokenManager();
194
  export default tokenManager;
 
189
  this.disableToken(found);
190
  }
191
  }
192
+
193
+ // API管理方法
194
+ async reload() {
195
+ await this.initialize();
196
+ log.info('Token已热重载');
197
+ }
198
+
199
+ addToken(tokenData) {
200
+ try {
201
+ const data = fs.readFileSync(this.filePath, 'utf8');
202
+ const allTokens = JSON.parse(data);
203
+
204
+ const newToken = {
205
+ access_token: tokenData.access_token,
206
+ refresh_token: tokenData.refresh_token,
207
+ expires_in: tokenData.expires_in || 3599,
208
+ timestamp: tokenData.timestamp || Date.now(),
209
+ enable: tokenData.enable !== undefined ? tokenData.enable : true
210
+ };
211
+
212
+ if (tokenData.projectId) {
213
+ newToken.projectId = tokenData.projectId;
214
+ }
215
+
216
+ allTokens.push(newToken);
217
+ fs.writeFileSync(this.filePath, JSON.stringify(allTokens, null, 2), 'utf8');
218
+
219
+ this.reload();
220
+ return { success: true, message: 'Token添加成功' };
221
+ } catch (error) {
222
+ log.error('添加Token失败:', error.message);
223
+ return { success: false, message: error.message };
224
+ }
225
+ }
226
+
227
+ updateToken(refreshToken, updates) {
228
+ try {
229
+ const data = fs.readFileSync(this.filePath, 'utf8');
230
+ const allTokens = JSON.parse(data);
231
+
232
+ const index = allTokens.findIndex(t => t.refresh_token === refreshToken);
233
+ if (index === -1) {
234
+ return { success: false, message: 'Token不存在' };
235
+ }
236
+
237
+ allTokens[index] = { ...allTokens[index], ...updates };
238
+ fs.writeFileSync(this.filePath, JSON.stringify(allTokens, null, 2), 'utf8');
239
+
240
+ this.reload();
241
+ return { success: true, message: 'Token更新成功' };
242
+ } catch (error) {
243
+ log.error('更新Token失败:', error.message);
244
+ return { success: false, message: error.message };
245
+ }
246
+ }
247
+
248
+ deleteToken(refreshToken) {
249
+ try {
250
+ const data = fs.readFileSync(this.filePath, 'utf8');
251
+ const allTokens = JSON.parse(data);
252
+
253
+ const filteredTokens = allTokens.filter(t => t.refresh_token !== refreshToken);
254
+ if (filteredTokens.length === allTokens.length) {
255
+ return { success: false, message: 'Token不存在' };
256
+ }
257
+
258
+ fs.writeFileSync(this.filePath, JSON.stringify(filteredTokens, 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
+ getTokenList() {
269
+ try {
270
+ const data = fs.readFileSync(this.filePath, 'utf8');
271
+ const allTokens = JSON.parse(data);
272
+
273
+ return allTokens.map(token => ({
274
+ refresh_token: token.refresh_token,
275
+ access_token_suffix: token.access_token ? `...${token.access_token.slice(-8)}` : 'N/A',
276
+ expires_in: token.expires_in,
277
+ timestamp: token.timestamp,
278
+ enable: token.enable !== false,
279
+ projectId: token.projectId || null
280
+ }));
281
+ } catch (error) {
282
+ log.error('获取Token列表失败:', error.message);
283
+ return [];
284
+ }
285
+ }
286
  }
287
  const tokenManager = new TokenManager();
288
  export default tokenManager;
src/config/config.js CHANGED
@@ -24,6 +24,11 @@ DEFAULT_MAX_TOKENS=8096
24
  MAX_REQUEST_SIZE=50mb
25
  API_KEY=sk-text
26
 
 
 
 
 
 
27
  # 其他配置
28
  USE_NATIVE_AXIOS=false
29
  TIMEOUT=180000
@@ -67,6 +72,11 @@ const config = {
67
  maxRequestSize: process.env.MAX_REQUEST_SIZE || '50mb',
68
  apiKey: process.env.API_KEY || null
69
  },
 
 
 
 
 
70
  useNativeAxios: process.env.USE_NATIVE_AXIOS !== 'false',
71
  timeout: parseInt(process.env.TIMEOUT) || 30000,
72
  proxy: process.env.PROXY || null,
 
24
  MAX_REQUEST_SIZE=50mb
25
  API_KEY=sk-text
26
 
27
+ # 管理员认证
28
+ ADMIN_USERNAME=admin
29
+ ADMIN_PASSWORD=admin123
30
+ JWT_SECRET=your-jwt-secret-key-change-this-in-production
31
+
32
  # 其他配置
33
  USE_NATIVE_AXIOS=false
34
  TIMEOUT=180000
 
72
  maxRequestSize: process.env.MAX_REQUEST_SIZE || '50mb',
73
  apiKey: process.env.API_KEY || null
74
  },
75
+ admin: {
76
+ username: process.env.ADMIN_USERNAME || 'admin',
77
+ password: process.env.ADMIN_PASSWORD || 'admin123',
78
+ jwtSecret: process.env.JWT_SECRET || 'your-jwt-secret-key-change-this-in-production'
79
+ },
80
  useNativeAxios: process.env.USE_NATIVE_AXIOS !== 'false',
81
  timeout: parseInt(process.env.TIMEOUT) || 30000,
82
  proxy: process.env.PROXY || null,
src/routes/admin.js ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import express from 'express';
2
+ import { generateToken, authMiddleware } from '../auth/jwt.js';
3
+ import tokenManager from '../auth/token_manager.js';
4
+ import config 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
+
17
+ const router = express.Router();
18
+
19
+ // 登录接口
20
+ router.post('/login', (req, res) => {
21
+ const { username, password } = req.body;
22
+
23
+ if (username === config.admin.username && password === config.admin.password) {
24
+ const token = generateToken({ username, role: 'admin' });
25
+ res.json({ success: true, token });
26
+ } else {
27
+ res.status(401).json({ success: false, message: '用户名或密码错误' });
28
+ }
29
+ });
30
+
31
+ // Token管理API - 需要JWT认证
32
+ router.get('/tokens', authMiddleware, (req, res) => {
33
+ const tokens = tokenManager.getTokenList();
34
+ res.json({ success: true, data: tokens });
35
+ });
36
+
37
+ router.post('/tokens', authMiddleware, (req, res) => {
38
+ const { access_token, refresh_token, expires_in, timestamp, enable, projectId } = req.body;
39
+ if (!access_token || !refresh_token) {
40
+ return res.status(400).json({ success: false, message: 'access_token和refresh_token必填' });
41
+ }
42
+ const tokenData = { access_token, refresh_token, expires_in };
43
+ if (timestamp) tokenData.timestamp = timestamp;
44
+ if (enable !== undefined) tokenData.enable = enable;
45
+ if (projectId) tokenData.projectId = projectId;
46
+
47
+ const result = tokenManager.addToken(tokenData);
48
+ res.json(result);
49
+ });
50
+
51
+ router.put('/tokens/:refreshToken', authMiddleware, (req, res) => {
52
+ const { refreshToken } = req.params;
53
+ const updates = req.body;
54
+ const result = tokenManager.updateToken(refreshToken, updates);
55
+ res.json(result);
56
+ });
57
+
58
+ router.delete('/tokens/:refreshToken', authMiddleware, (req, res) => {
59
+ const { refreshToken } = req.params;
60
+ const result = tokenManager.deleteToken(refreshToken);
61
+ res.json(result);
62
+ });
63
+
64
+ router.post('/tokens/reload', authMiddleware, async (req, res) => {
65
+ try {
66
+ await tokenManager.reload();
67
+ res.json({ success: true, message: 'Token已热重载' });
68
+ } catch (error) {
69
+ logger.error('热重载失败:', error.message);
70
+ res.status(500).json({ success: false, message: error.message });
71
+ }
72
+ });
73
+
74
+ router.post('/oauth/exchange', authMiddleware, async (req, res) => {
75
+ const { code, port } = req.body;
76
+ if (!code || !port) {
77
+ return res.status(400).json({ success: false, message: 'code和port必填' });
78
+ }
79
+
80
+ const CLIENT_ID = '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com';
81
+ const CLIENT_SECRET = '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';
82
+
83
+ try {
84
+ const postData = new URLSearchParams({
85
+ code,
86
+ client_id: CLIENT_ID,
87
+ client_secret: CLIENT_SECRET,
88
+ redirect_uri: `http://localhost:${port}/oauth-callback`,
89
+ grant_type: 'authorization_code'
90
+ });
91
+
92
+ const response = await fetch('https://oauth2.googleapis.com/token', {
93
+ method: 'POST',
94
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
95
+ body: postData.toString()
96
+ });
97
+
98
+ const tokenData = await response.json();
99
+
100
+ if (!tokenData.access_token) {
101
+ return res.status(400).json({ success: false, message: 'Token交换失败' });
102
+ }
103
+
104
+ const account = {
105
+ access_token: tokenData.access_token,
106
+ refresh_token: tokenData.refresh_token,
107
+ expires_in: tokenData.expires_in,
108
+ timestamp: Date.now(),
109
+ enable: true
110
+ };
111
+
112
+ if (config.skipProjectIdFetch) {
113
+ account.projectId = generateProjectId();
114
+ logger.info('使用随机生成的projectId: ' + account.projectId);
115
+ } else {
116
+ try {
117
+ const projectResponse = await axios({
118
+ method: 'POST',
119
+ url: 'https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:loadCodeAssist',
120
+ headers: {
121
+ 'Host': 'daily-cloudcode-pa.sandbox.googleapis.com',
122
+ 'User-Agent': 'antigravity/1.11.9 windows/amd64',
123
+ 'Authorization': `Bearer ${account.access_token}`,
124
+ 'Content-Type': 'application/json',
125
+ 'Accept-Encoding': 'gzip'
126
+ },
127
+ data: JSON.stringify({ metadata: { ideType: 'ANTIGRAVITY' } }),
128
+ timeout: config.timeout,
129
+ proxy: config.proxy ? (() => {
130
+ const proxyUrl = new URL(config.proxy);
131
+ return { protocol: proxyUrl.protocol.replace(':', ''), host: proxyUrl.hostname, port: parseInt(proxyUrl.port) };
132
+ })() : false
133
+ });
134
+
135
+ const projectId = projectResponse.data?.cloudaicompanionProject;
136
+ if (projectId === undefined) {
137
+ return res.status(400).json({ success: false, message: '该��号无资格使用(无法获取projectId)' });
138
+ }
139
+ account.projectId = projectId;
140
+ logger.info('账号验证通过,projectId: ' + projectId);
141
+ } catch (error) {
142
+ logger.error('验证账号资格失败:', error.message);
143
+ return res.status(500).json({ success: false, message: '验证账号资格失败: ' + error.message });
144
+ }
145
+ }
146
+
147
+ res.json({ success: true, data: account });
148
+ } catch (error) {
149
+ logger.error('Token交换失败:', error.message);
150
+ res.status(500).json({ success: false, message: error.message });
151
+ }
152
+ });
153
+
154
+ // 获取配置
155
+ router.get('/config', authMiddleware, (req, res) => {
156
+ try {
157
+ const envContent = fs.readFileSync(envPath, 'utf8');
158
+ const configData = {};
159
+ envContent.split('\n').forEach(line => {
160
+ line = line.trim();
161
+ if (line && !line.startsWith('#')) {
162
+ const [key, ...valueParts] = line.split('=');
163
+ if (key) configData[key.trim()] = valueParts.join('=').trim();
164
+ }
165
+ });
166
+ res.json({ success: true, data: configData });
167
+ } catch (error) {
168
+ logger.error('读取配置失败:', error.message);
169
+ res.status(500).json({ success: false, message: error.message });
170
+ }
171
+ });
172
+
173
+ // 更新配置
174
+ router.put('/config', authMiddleware, (req, res) => {
175
+ try {
176
+ const updates = req.body;
177
+ let envContent = fs.readFileSync(envPath, 'utf8');
178
+
179
+ Object.entries(updates).forEach(([key, value]) => {
180
+ const regex = new RegExp(`^${key}=.*$`, 'm');
181
+ if (regex.test(envContent)) {
182
+ envContent = envContent.replace(regex, `${key}=${value}`);
183
+ } else {
184
+ envContent += `\n${key}=${value}`;
185
+ }
186
+ });
187
+
188
+ fs.writeFileSync(envPath, envContent, 'utf8');
189
+
190
+ // 重新加载环境变量
191
+ dotenv.config({ override: true });
192
+
193
+ // 更新config对象
194
+ config.server.port = parseInt(process.env.PORT) || 8045;
195
+ config.server.host = process.env.HOST || '127.0.0.1';
196
+ config.defaults.temperature = parseFloat(process.env.DEFAULT_TEMPERATURE) || 1;
197
+ config.defaults.top_p = parseFloat(process.env.DEFAULT_TOP_P) || 0.85;
198
+ config.defaults.top_k = parseInt(process.env.DEFAULT_TOP_K) || 50;
199
+ config.defaults.max_tokens = parseInt(process.env.DEFAULT_MAX_TOKENS) || 8096;
200
+ config.security.apiKey = process.env.API_KEY || null;
201
+ config.timeout = parseInt(process.env.TIMEOUT) || 30000;
202
+ config.proxy = process.env.PROXY || null;
203
+ config.systemInstruction = process.env.SYSTEM_INSTRUCTION || '';
204
+ config.skipProjectIdFetch = process.env.SKIP_PROJECT_ID_FETCH === 'true';
205
+
206
+ logger.info('配置已更新并热重载');
207
+ res.json({ success: true, message: '配置已保存并生效(端口/HOST修改需重启)' });
208
+ } catch (error) {
209
+ logger.error('更新配置失败:', error.message);
210
+ res.status(500).json({ success: false, message: error.message });
211
+ }
212
+ });
213
+
214
+ export default router;
src/server/index.js CHANGED
@@ -6,6 +6,7 @@ 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
 
10
  const __filename = fileURLToPath(import.meta.url);
11
  const __dirname = path.dirname(__filename);
@@ -48,8 +49,12 @@ const endStream = (res, id, created, model, finish_reason) => {
48
 
49
  app.use(express.json({ limit: config.security.maxRequestSize }));
50
 
51
- // 静态文件服务:提供图片访问
52
  app.use('/images', express.static(path.join(__dirname, '../../public/images')));
 
 
 
 
53
 
54
  app.use((err, req, res, next) => {
55
  if (err.type === 'entity.too.large') {
@@ -59,7 +64,8 @@ app.use((err, req, res, next) => {
59
  });
60
 
61
  app.use((req, res, next) => {
62
- if (!req.path.startsWith('/images' || !req.path.startsWith('/favicon.ico'))) {
 
63
  const start = Date.now();
64
  res.on('finish', () => {
65
  logger.request(req.method, req.path, res.statusCode, Date.now() - start);
@@ -93,6 +99,8 @@ app.get('/v1/models', async (req, res) => {
93
  }
94
  });
95
 
 
 
96
  app.post('/v1/chat/completions', async (req, res) => {
97
  const { messages, model, stream = true, tools, ...params} = req.body;
98
  try {
 
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);
 
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') {
 
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);
 
99
  }
100
  });
101
 
102
+
103
+
104
  app.post('/v1/chat/completions', async (req, res) => {
105
  const { messages, model, stream = true, tools, ...params} = req.body;
106
  try {