增加一个简易的前端方便管理,增加dockerfile和docker-compose.yml
Browse files- .dockerignore +15 -0
- .env.example +4 -0
- .github/workflows/docker-publish.yml +55 -0
- .gitignore +1 -1
- Dockerfile +21 -0
- TOKEN_API.md +172 -0
- docker-compose.yml +14 -0
- package-lock.json +134 -1
- package.json +2 -1
- public/app.js +465 -0
- public/index.html +112 -0
- public/style.css +165 -0
- src/auth/jwt.js +27 -0
- src/auth/token_manager.js +94 -0
- src/config/config.js +10 -0
- src/routes/admin.js +214 -0
- src/server/index.js +10 -2
.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 |
-
|
|
|
|
| 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 {
|