llzai commited on
Commit
644c352
·
verified ·
1 Parent(s): 80ffd2e

Upload 42 files

Browse files
.env.example ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # PostgreSQL 数据库连接(必需,Hugging Face Spaces 中通过 Secrets 配置)
2
+ DATABASE_URL=postgresql://user:password@host:5432/dbname
3
+
4
+ # JWT 签名密钥(必需,生产环境请使用强密码)
5
+ JWT_SECRET=your-secret-key-change-in-production
6
+
7
+ # 默认管理员账号(首次启动时创建)
8
+ DEFAULT_ADMIN_USERNAME=admin
9
+ DEFAULT_ADMIN_PASSWORD=admin
10
+
11
+ # Cookie 验证配置
12
+ COOKIE_MAX_ERROR_COUNT=3
13
+
14
+ # 轮询策略:round_robin, priority, least_used
15
+ ROTATION_STRATEGY=priority
.gitignore CHANGED
@@ -39,4 +39,6 @@ temp/
39
 
40
  # Build artifacts
41
  dist/
42
- build/
 
 
 
39
 
40
  # Build artifacts
41
  dist/
42
+ build/
43
+
44
+ .env
Dockerfile CHANGED
@@ -35,6 +35,9 @@ COPY --from=builder /app/server .
35
  # Copy Python startup script
36
  COPY app.py .
37
 
 
 
 
38
  # Create logs directory
39
  RUN mkdir -p /app/logs
40
 
 
35
  # Copy Python startup script
36
  COPY app.py .
37
 
38
+ # Copy web static files
39
+ COPY --from=builder /app/web ./web
40
+
41
  # Create logs directory
42
  RUN mkdir -p /app/logs
43
 
README.md CHANGED
@@ -7,28 +7,98 @@ sdk: docker
7
  app_port: 7860
8
  ---
9
 
10
- # Opus API
11
 
12
- 一个用于 API 消息格式转换的服务,将 Claude API 格式转换为其他格式。
13
 
14
- ## 功能特性
15
 
16
- - Claude API 消息格式转换
17
- - 🔄 流式响应支持
 
 
 
 
 
 
18
  - 🛠️ 工具调用处理
19
- - 📊 Token 计数
20
  - 💾 请求/响应日志记录(调试模式)
 
21
 
22
- ## API 端点
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
- ### 1. 消息转换接口
25
  ```
26
- POST /v1/messages
 
 
27
  ```
28
 
29
- Claude API 格式的消息转换为目标格式。
30
 
31
- **请求示例:**
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  ```bash
33
  curl -X POST https://your-space.hf.space/v1/messages \
34
  -H "Content-Type: application/json" \
@@ -41,70 +111,182 @@ curl -X POST https://your-space.hf.space/v1/messages \
41
  }'
42
  ```
43
 
44
- ### 2. 健康检查接口
 
 
 
 
 
 
 
 
 
 
 
45
  ```
46
- GET /health
 
 
 
 
 
 
 
 
 
 
 
47
  ```
48
 
49
- 检查服务运行状态。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
 
51
- **响应示例:**
52
- ```json
53
- {
54
- "status": "healthy",
55
- "timestamp": "2024-01-01T00:00:00Z"
56
- }
 
 
 
57
  ```
58
 
59
- ## 技术栈
60
 
61
- - **后端**: Go 1.21
62
- - **Web 框架**: Gin
63
- - **Token 计数**: tiktoken-go
64
- - **容器化**: Docker
65
- - **部署平台**: Hugging Face Spaces
 
 
66
 
67
- ## 环境变量
68
 
69
- - `PORT`: 服务端口(默认: 7860)
70
- - `DEBUG_MODE`: 调试模式开关
71
- - `LOG_DIR`: 日志目录路径
 
 
 
 
 
 
72
 
73
- ## 本地开发
74
 
75
- ### 使用 Go 直接运行
 
 
 
 
 
76
  ```bash
77
- go run cmd/server/main.go
 
 
 
 
 
 
78
  ```
79
 
80
- ### 使用 Docker
81
  ```bash
 
 
 
 
82
  docker build -t opus-api .
83
- docker run -p 7860:7860 opus-api
 
 
 
84
  ```
85
 
86
- ## 项目结构
87
 
88
  ```
89
  opus-api/
90
- ├── cmd/server/ # 主程序入口
 
91
  ├── internal/
92
- │ ├── converter/ # 格式转换逻辑
93
- │ ├── handler/ # HTTP 处理器
94
- │ ├── logger/ # 日志管
95
- │ ├── parser/ # 消息解析
96
- │ ├── stream/ # 流式处理
97
- ── tokenizer/ # Token 计数
98
- ── types/ # 类型定义
99
- ── app.py # Python 启动脚本
100
- ├── Dockerfile # Docker 构建文件
101
- ── go.mod # Go 依赖管理
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  ```
103
 
104
- ## 许可证
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
  本项目遵循开源协议。
107
 
108
- ## 联系方式
 
 
 
 
109
 
110
  如有问题或建议,欢迎提交 Issue。
 
7
  app_port: 7860
8
  ---
9
 
10
+ # Opus API - 账号管理系统
11
 
12
+ 一个用于 API 消息格式转换的服务,将 Claude API 格式转换为其他格式,支持多账号 Cookie 管理和轮询
13
 
14
+ ## 功能特性
15
 
16
+ - 🔄 Claude API 消息格式转换
17
+ - 📊 Token 计数统计
18
+ - 🎯 **账号管理系统**
19
+ - 用户登录认证(JWT)
20
+ - 多 Morph 账号 Cookie 管理
21
+ - Cookie 有效性检测(手动/自动)
22
+ - Cookie 轮询策略(轮询/优先级/最少使用)
23
+ - Web 管理界面
24
  - 🛠️ 工具调用处理
 
25
  - 💾 请求/响应日志记录(调试模式)
26
+ - 🔌 支持客户端 Cookie/Authorization 请求头覆盖
27
 
28
+ ## 🚀 快速开始
29
+
30
+ ### 部署到 Hugging Face Spaces
31
+
32
+ 1. **创建 Space**
33
+ - 在 Hugging Face 上创建新 Space
34
+ - 选择 **Docker SDK**
35
+ - Space 名称例如:`your-username/opus-api`
36
+
37
+ 2. **配置环境变量**
38
+
39
+ 在 Space 的 **Settings → Repository secrets** 中添加:
40
+
41
+ | Secret 名称 | 说明 | 示例值 |
42
+ |-------------|------|--------|
43
+ | `DATABASE_URL` | PostgreSQL 数据库连接 | `postgresql://user:password@host:5432/dbname` |
44
+ | `JWT_SECRET` | JWT 签名密钥 | `your-random-secret-key-here` |
45
+ | `DEFAULT_ADMIN_PASSWORD` | 管理员密码 | `changeme123` |
46
+
47
+ 3. **推送代码**
48
+ ```bash
49
+ git remote add hf https://huggingface.co/spaces/YOUR_USERNAME/YOUR_SPACE_NAME
50
+ git push hf main
51
+ ```
52
+
53
+ 4. **访问管理界面**
54
+ - 访问 `https://YOUR_USERNAME-YOUR_SPACE_NAME.hf.space`
55
+ - 使用默认账号登录:
56
+ - 用户名:`admin`
57
+ - 密码:你设置的 `DEFAULT_ADMIN_PASSWORD`
58
+
59
+ ## 📡 API 端点
60
+
61
+ ### 认证 API
62
 
 
63
  ```
64
+ POST /api/auth/login # 用户登录
65
+ POST /api/auth/logout # 用户登出
66
+ GET /api/auth/me # 获取当前用户信息
67
  ```
68
 
69
+ ### Cookie 管理 API(需要认证)
70
 
71
+ ```
72
+ GET /api/cookies # 获取 Cookie 列表
73
+ POST /api/cookies # 添加 Cookie
74
+ GET /api/cookies/:id # 获取单个 Cookie
75
+ PUT /api/cookies/:id # 更新 Cookie
76
+ DELETE /api/cookies/:id # 删除 Cookie
77
+ POST /api/cookies/:id/validate # 验证单个 Cookie
78
+ POST /api/cookies/validate/all # 批量验证所有 Cookie
79
+ GET /api/cookies/stats # 获取统计信息
80
+ ```
81
+
82
+ ### 消息转换 API
83
+
84
+ ```
85
+ POST /v1/messages # 消息转换接口(支持客户端 Cookie 覆盖)
86
+ GET /health # 健康检查接口
87
+ ```
88
+
89
+ ## 🔧 使用方法
90
+
91
+ ### 1. Web 管理界面
92
+
93
+ 访问 `/dashboard` 或 `/static/dashboard.html`,登录后可以:
94
+ - 查看 Cookie 统计信息
95
+ - 添加/编辑/删除 Cookie
96
+ - 手动或批量验证 Cookie 有效性
97
+ - 设置 Cookie 优先级
98
+
99
+ ### 2. 调用消息 API
100
+
101
+ **基础请求:**
102
  ```bash
103
  curl -X POST https://your-space.hf.space/v1/messages \
104
  -H "Content-Type: application/json" \
 
111
  }'
112
  ```
113
 
114
+ **使用自定义 Cookie(覆盖轮询):**
115
+ ```bash
116
+ curl -X POST https://your-space.hf.space/v1/messages \
117
+ -H "Content-Type: application/json" \
118
+ -H "Cookie: _gcl_aw=GCL.17692..." \
119
+ -d '{
120
+ "model": "claude-opus-4-20250514",
121
+ "max_tokens": 1024,
122
+ "messages": [
123
+ {"role": "user", "content": "Hello!"}
124
+ ]
125
+ }'
126
  ```
127
+
128
+ ## 🗄️ 数据库结构
129
+
130
+ ### users 表
131
+ ```sql
132
+ CREATE TABLE users (
133
+ id SERIAL PRIMARY KEY,
134
+ username VARCHAR(50) UNIQUE NOT NULL,
135
+ password_hash VARCHAR(255) NOT NULL,
136
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
137
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
138
+ );
139
  ```
140
 
141
+ ### morph_cookies 表
142
+ ```sql
143
+ CREATE TABLE morph_cookies (
144
+ id SERIAL PRIMARY KEY,
145
+ user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
146
+ name VARCHAR(100) NOT NULL,
147
+ api_key TEXT NOT NULL,
148
+ session_key TEXT,
149
+ is_valid BOOLEAN DEFAULT true,
150
+ last_validated TIMESTAMP,
151
+ last_used TIMESTAMP,
152
+ priority INTEGER DEFAULT 0,
153
+ usage_count BIGINT DEFAULT 0,
154
+ error_count INTEGER DEFAULT 0,
155
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
156
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
157
+ );
158
+ ```
159
 
160
+ ### user_sessions 表
161
+ ```sql
162
+ CREATE TABLE user_sessions (
163
+ id SERIAL PRIMARY KEY,
164
+ user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
165
+ token_hash VARCHAR(255) UNIQUE NOT NULL,
166
+ expires_at TIMESTAMP NOT NULL,
167
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
168
+ );
169
  ```
170
 
171
+ ## 🔄 Cookie 轮询策略
172
 
173
+ 系统支持三种轮询策略,通过环境变量 `ROTATION_STRATEGY` 配置:
174
+
175
+ | 策略 | 说明 |
176
+ |------|------|
177
+ | `priority` | 按优先级(数字越大越优先,默认) |
178
+ | `round_robin` | 轮询(按顺序循环使用) |
179
+ | `least_used` | 使用次数最少的优先 |
180
 
181
+ ## 🔐 环境变量配置
182
 
183
+ | 变量名 | 说明 | 默认 | 必需 |
184
+ |--------|------|--------|------|
185
+ | `DATABASE_URL` | PostgreSQL 连接字符串 | - | ✅ |
186
+ | `JWT_SECRET` | JWT 签名密钥 | - | ✅ |
187
+ | `DEFAULT_ADMIN_USERNAME` | 默认管理员用户名 | `admin` | ❌ |
188
+ | `DEFAULT_ADMIN_PASSWORD` | 默认管理员密码 | `changeme123` | ❌ |
189
+ | `COOKIE_MAX_ERROR_COUNT` | Cookie 失败阈值 | `3` | ❌ |
190
+ | `ROTATION_STRATEGY` | 轮询策略 | `priority` | ❌ |
191
+ | `DEBUG_MODE` | 调试模式 | `false` | ❌ |
192
 
193
+ ## 🛠️ 本地开发
194
 
195
+ ### 前置要求
196
+ - Go 1.21+
197
+ - PostgreSQL 14+
198
+ - Python 3.9+(用于 Hugging Face 部署)
199
+
200
+ ### 安装依赖
201
  ```bash
202
+ go mod download
203
+ ```
204
+
205
+ ### 配置环境变量
206
+ ```bash
207
+ cp .env.example .env
208
+ # 编辑 .env 文件,设置数据库连接等
209
  ```
210
 
211
+ ### 运行服务
212
  ```bash
213
+ # 使用 Go 直接运行
214
+ go run cmd/server/main.go
215
+
216
+ # 或使用 Docker
217
  docker build -t opus-api .
218
+ docker run -p 7860:7860 \
219
+ -e DATABASE_URL="postgresql://..." \
220
+ -e JWT_SECRET="your-secret" \
221
+ opus-api
222
  ```
223
 
224
+ ## 📁 项目结构
225
 
226
  ```
227
  opus-api/
228
+ ├── cmd/server/ # 主程序入口
229
+ │ └── main.go
230
  ├── internal/
231
+ │ ├── converter/ # 格式转换逻辑
232
+ │ ├── handler/ # HTTP 处理器
233
+ ├── messages.go # 消息处
234
+ ├── health.go # 健康检查
235
+ ├── auth.go # 认证处理
236
+ │ └── cookies.go # Cookie 管理
237
+ ── middleware/ # 中间件
238
+ │ │ └── auth.go # JWT 认证
239
+ ├── model/ # 数据模型
240
+ │ │ ├── db.go # 数据库连接
241
+ │ │ ├── user.go # 用户模型
242
+ │ │ └── cookie.go # Cookie 模型
243
+ │ ├── service/ # 业务逻辑
244
+ │ │ ├── auth_service.go # 认证服务
245
+ │ │ ├── cookie_service.go# Cookie 服务
246
+ │ │ ├── validator.go # Cookie 验证
247
+ │ │ └── rotator.go # Cookie 轮询
248
+ │ ├── logger/ # 日志管理
249
+ │ ├── parser/ # 消息解析
250
+ │ ├── stream/ # 流式处理
251
+ │ ├── tokenizer/ # Token 计数
252
+ │ ├── types/ # 类型定义
253
+ │ └── converter/ # 格式转换
254
+ ├── web/static/ # 前端静态文件
255
+ │ ├── index.html # 登录页
256
+ │ ├── dashboard.html # 管理面板
257
+ │ ├── styles.css # 样式
258
+ │ └── app.js # 前端逻辑
259
+ ├── migrations/ # 数据库迁移(可选)
260
+ ├── .env.example # 环境变量示例
261
+ ├── app.py # Python 启动脚本
262
+ ├── Dockerfile # Docker 构建文件
263
+ └── go.mod # Go 依赖管理
264
  ```
265
 
266
+ ## 📝 技术栈
267
+
268
+ - **后端**: Go 1.21, Gin
269
+ - **数据库**: PostgreSQL, GORM
270
+ - **认证**: JWT
271
+ - **前端**: HTML, CSS, Vanilla JavaScript
272
+ - **容器化**: Docker
273
+ - **部署平台**: Hugging Face Spaces
274
+
275
+ ## 🔐 安全建议
276
+
277
+ 1. **首次登录后立即修改默认密码**
278
+ 2. **生产环境使用强 JWT_SECRET**
279
+ 3. **定期更新 Cookie**(Morph Cookie 可能会过期)
280
+ 4. **使用 HTTPS**(Hugging Face Spaces 自动提供)
281
+
282
+ ## 📄 许可证
283
 
284
  本项目遵循开源协议。
285
 
286
+ ## ���� 贡献
287
+
288
+ 欢迎提交 Issue 和 Pull Request。
289
+
290
+ ## 📞 联系方式
291
 
292
  如有问题或建议,欢迎提交 Issue。
cmd/server/main.go CHANGED
@@ -5,14 +5,25 @@ import (
5
  "log"
6
  "opus-api/internal/handler"
7
  "opus-api/internal/logger"
 
 
 
8
  "opus-api/internal/tokenizer"
9
  "opus-api/internal/types"
10
  "os"
11
 
12
  "github.com/gin-gonic/gin"
 
13
  )
14
 
15
  func main() {
 
 
 
 
 
 
 
16
  // Create logs directory
17
  if err := os.MkdirAll(types.LogDir, 0755); err != nil {
18
  log.Fatalf("Failed to create logs directory: %v", err)
@@ -28,16 +39,83 @@ func main() {
28
  log.Printf("[WARN] Failed to initialize tokenizer: %v (will use fallback)", err)
29
  }
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  // Set Gin mode
32
  gin.SetMode(gin.ReleaseMode)
33
 
34
  // Create Gin router
35
  router := gin.Default()
36
 
37
- // Register routes
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  router.POST("/v1/messages", handler.HandleMessages)
39
  router.GET("/health", handler.HandleHealth)
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  // Start server
42
  // Hugging Face Spaces uses port 7860
43
  port := 7860
@@ -48,6 +126,7 @@ func main() {
48
  log.Printf("Server running on http://0.0.0.0:%d", port)
49
  log.Printf("Debug mode: %v", types.DebugMode)
50
  log.Printf("Log directory: %s", types.LogDir)
 
51
 
52
  if err := router.Run(addr); err != nil {
53
  log.Fatalf("Failed to start server: %v", err)
 
5
  "log"
6
  "opus-api/internal/handler"
7
  "opus-api/internal/logger"
8
+ "opus-api/internal/middleware"
9
+ "opus-api/internal/model"
10
+ "opus-api/internal/service"
11
  "opus-api/internal/tokenizer"
12
  "opus-api/internal/types"
13
  "os"
14
 
15
  "github.com/gin-gonic/gin"
16
+ "github.com/joho/godotenv"
17
  )
18
 
19
  func main() {
20
+ // Load .env file
21
+ if err := godotenv.Load(); err != nil {
22
+ log.Printf("[INFO] No .env file found or error loading it: %v", err)
23
+ } else {
24
+ log.Printf("[INFO] .env file loaded successfully")
25
+ }
26
+
27
  // Create logs directory
28
  if err := os.MkdirAll(types.LogDir, 0755); err != nil {
29
  log.Fatalf("Failed to create logs directory: %v", err)
 
39
  log.Printf("[WARN] Failed to initialize tokenizer: %v (will use fallback)", err)
40
  }
41
 
42
+ // Initialize database
43
+ if err := model.InitDB(); err != nil {
44
+ log.Printf("[WARN] Failed to initialize database: %v (running without database)", err)
45
+ } else {
46
+ // Create default admin user
47
+ if err := model.CreateDefaultAdmin(model.DB); err != nil {
48
+ log.Printf("[WARN] Failed to create default admin: %v", err)
49
+ }
50
+ }
51
+
52
+ // Initialize services
53
+ var authService *service.AuthService
54
+ var cookieService *service.CookieService
55
+ var cookieValidator *service.CookieValidator
56
+ var cookieRotator *service.CookieRotator
57
+
58
+ if model.DB != nil {
59
+ authService = service.NewAuthService(model.DB)
60
+ cookieService = service.NewCookieService(model.DB)
61
+ cookieValidator = service.NewCookieValidator(cookieService)
62
+ cookieRotator = service.NewCookieRotator(cookieService, service.StrategyRoundRobin)
63
+
64
+ // Store rotator in types for use in messages handler
65
+ types.CookieRotatorInstance = cookieRotator
66
+ }
67
+
68
  // Set Gin mode
69
  gin.SetMode(gin.ReleaseMode)
70
 
71
  // Create Gin router
72
  router := gin.Default()
73
 
74
+ // Serve static files
75
+ router.Static("/static", "./web/static")
76
+
77
+ // Redirect root to login page or dashboard
78
+ router.GET("/", func(c *gin.Context) {
79
+ c.Redirect(302, "/static/index.html")
80
+ })
81
+
82
+ // Dashboard redirect
83
+ router.GET("/dashboard", func(c *gin.Context) {
84
+ c.Redirect(302, "/static/dashboard.html")
85
+ })
86
+
87
+ // Register API routes
88
  router.POST("/v1/messages", handler.HandleMessages)
89
  router.GET("/health", handler.HandleHealth)
90
 
91
+ // Auth routes (only if database is available)
92
+ if authService != nil {
93
+ authHandler := handler.NewAuthHandler(authService)
94
+ router.POST("/api/auth/login", authHandler.Login)
95
+ router.POST("/api/auth/logout", authHandler.Logout)
96
+
97
+ // Protected routes
98
+ authGroup := router.Group("/api")
99
+ authGroup.Use(middleware.AuthMiddleware(authService))
100
+ {
101
+ authGroup.GET("/auth/me", authHandler.Me)
102
+ authGroup.PUT("/auth/password", authHandler.ChangePassword)
103
+
104
+ // Cookie management routes
105
+ if cookieService != nil && cookieValidator != nil {
106
+ cookieHandler := handler.NewCookieHandler(cookieService, cookieValidator)
107
+ authGroup.GET("/cookies", cookieHandler.ListCookies)
108
+ authGroup.GET("/cookies/stats", cookieHandler.GetStats)
109
+ authGroup.POST("/cookies", cookieHandler.CreateCookie)
110
+ authGroup.GET("/cookies/:id", cookieHandler.GetCookie)
111
+ authGroup.PUT("/cookies/:id", cookieHandler.UpdateCookie)
112
+ authGroup.DELETE("/cookies/:id", cookieHandler.DeleteCookie)
113
+ authGroup.POST("/cookies/:id/validate", cookieHandler.ValidateCookie)
114
+ authGroup.POST("/cookies/validate/all", cookieHandler.ValidateAllCookies)
115
+ }
116
+ }
117
+ }
118
+
119
  // Start server
120
  // Hugging Face Spaces uses port 7860
121
  port := 7860
 
126
  log.Printf("Server running on http://0.0.0.0:%d", port)
127
  log.Printf("Debug mode: %v", types.DebugMode)
128
  log.Printf("Log directory: %s", types.LogDir)
129
+ log.Printf("Database connected: %v", model.DB != nil)
130
 
131
  if err := router.Run(addr); err != nil {
132
  log.Fatalf("Failed to start server: %v", err)
go.mod CHANGED
@@ -1,37 +1,50 @@
1
- module opus-api
2
-
3
- go 1.21
4
-
5
- require (
6
- github.com/gin-gonic/gin v1.9.1
7
- github.com/google/uuid v1.6.0
8
- github.com/pkoukk/tiktoken-go v0.1.8
9
- )
10
-
11
- require (
12
- github.com/bytedance/sonic v1.9.1 // indirect
13
- github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
14
- github.com/dlclark/regexp2 v1.10.0 // indirect
15
- github.com/gabriel-vasile/mimetype v1.4.2 // indirect
16
- github.com/gin-contrib/sse v0.1.0 // indirect
17
- github.com/go-playground/locales v0.14.1 // indirect
18
- github.com/go-playground/universal-translator v0.18.1 // indirect
19
- github.com/go-playground/validator/v10 v10.14.0 // indirect
20
- github.com/goccy/go-json v0.10.2 // indirect
21
- github.com/json-iterator/go v1.1.12 // indirect
22
- github.com/klauspost/cpuid/v2 v2.2.4 // indirect
23
- github.com/leodido/go-urn v1.2.4 // indirect
24
- github.com/mattn/go-isatty v0.0.19 // indirect
25
- github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
26
- github.com/modern-go/reflect2 v1.0.2 // indirect
27
- github.com/pelletier/go-toml/v2 v2.0.8 // indirect
28
- github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
29
- github.com/ugorji/go/codec v1.2.11 // indirect
30
- golang.org/x/arch v0.3.0 // indirect
31
- golang.org/x/crypto v0.9.0 // indirect
32
- golang.org/x/net v0.10.0 // indirect
33
- golang.org/x/sys v0.8.0 // indirect
34
- golang.org/x/text v0.9.0 // indirect
35
- google.golang.org/protobuf v1.30.0 // indirect
36
- gopkg.in/yaml.v3 v3.0.1 // indirect
37
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module opus-api
2
+
3
+ go 1.23
4
+
5
+ toolchain go1.24.5
6
+
7
+ require (
8
+ github.com/gin-gonic/gin v1.9.1
9
+ github.com/golang-jwt/jwt/v5 v5.2.0
10
+ github.com/google/uuid v1.6.0
11
+ github.com/pkoukk/tiktoken-go v0.1.8
12
+ golang.org/x/crypto v0.18.0
13
+ gorm.io/driver/postgres v1.5.4
14
+ gorm.io/gorm v1.25.5
15
+ )
16
+
17
+ require (
18
+ github.com/bytedance/sonic v1.9.1 // indirect
19
+ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
20
+ github.com/dlclark/regexp2 v1.10.0 // indirect
21
+ github.com/gabriel-vasile/mimetype v1.4.2 // indirect
22
+ github.com/gin-contrib/sse v0.1.0 // indirect
23
+ github.com/go-playground/locales v0.14.1 // indirect
24
+ github.com/go-playground/universal-translator v0.18.1 // indirect
25
+ github.com/go-playground/validator/v10 v10.14.0 // indirect
26
+ github.com/goccy/go-json v0.10.2 // indirect
27
+ github.com/jackc/pgpassfile v1.0.0 // indirect
28
+ github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
29
+ github.com/jackc/pgx/v5 v5.4.3 // indirect
30
+ github.com/jinzhu/inflection v1.0.0 // indirect
31
+ github.com/jinzhu/now v1.1.5 // indirect
32
+ github.com/joho/godotenv v1.5.1 // indirect
33
+ github.com/json-iterator/go v1.1.12 // indirect
34
+ github.com/klauspost/cpuid/v2 v2.2.4 // indirect
35
+ github.com/kr/text v0.2.0 // indirect
36
+ github.com/leodido/go-urn v1.2.4 // indirect
37
+ github.com/mattn/go-isatty v0.0.19 // indirect
38
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
39
+ github.com/modern-go/reflect2 v1.0.2 // indirect
40
+ github.com/pelletier/go-toml/v2 v2.0.8 // indirect
41
+ github.com/rogpeppe/go-internal v1.14.1 // indirect
42
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
43
+ github.com/ugorji/go/codec v1.2.11 // indirect
44
+ golang.org/x/arch v0.3.0 // indirect
45
+ golang.org/x/net v0.10.0 // indirect
46
+ golang.org/x/sys v0.26.0 // indirect
47
+ golang.org/x/text v0.14.0 // indirect
48
+ google.golang.org/protobuf v1.30.0 // indirect
49
+ gopkg.in/yaml.v3 v3.0.1 // indirect
50
+ )
go.sum CHANGED
@@ -1,92 +1,118 @@
1
- github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
2
- github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
3
- github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
4
- github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
5
- github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
6
- github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
7
- github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
8
- github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
9
- github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
10
- github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
11
- github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
12
- github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
13
- github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
14
- github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
15
- github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
16
- github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
17
- github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
18
- github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
19
- github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
20
- github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
21
- github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
22
- github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
23
- github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
24
- github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
25
- github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
26
- github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
27
- github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
28
- github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
29
- github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
30
- github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
31
- github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
32
- github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
33
- github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
34
- github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
35
- github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
36
- github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
37
- github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
38
- github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
39
- github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
40
- github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
41
- github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
42
- github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
43
- github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
44
- github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
45
- github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
46
- github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
47
- github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
48
- github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
49
- github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
50
- github.com/pkoukk/tiktoken-go v0.1.8 h1:85ENo+3FpWgAACBaEUVp+lctuTcYUO7BtmfhlN/QTRo=
51
- github.com/pkoukk/tiktoken-go v0.1.8/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
52
- github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
53
- github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
54
- github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
55
- github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
56
- github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
57
- github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
58
- github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
59
- github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
60
- github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
61
- github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
62
- github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
63
- github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
64
- github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
65
- github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
66
- github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
67
- github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
68
- github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
69
- golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
70
- golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
71
- golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
72
- golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
73
- golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
74
- golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
75
- golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
76
- golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
77
- golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
78
- golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
79
- golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
80
- golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
81
- golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
82
- golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
83
- golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
84
- google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
85
- google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
86
- google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
87
- gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
88
- gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
89
- gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
90
- gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
91
- gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
92
- rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
2
+ github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
3
+ github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
4
+ github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
5
+ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
6
+ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
7
+ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
8
+ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
9
+ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
10
+ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
11
+ github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
12
+ github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
13
+ github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
14
+ github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
15
+ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
16
+ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
17
+ github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
18
+ github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
19
+ github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
20
+ github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
21
+ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
22
+ github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
23
+ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
24
+ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
25
+ github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
26
+ github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
27
+ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
28
+ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
29
+ github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
30
+ github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
31
+ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
32
+ github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
33
+ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
34
+ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
35
+ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
36
+ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
37
+ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
38
+ github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
39
+ github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
40
+ github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
41
+ github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
42
+ github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
43
+ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
44
+ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
45
+ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
46
+ github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
47
+ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
48
+ github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
49
+ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
50
+ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
51
+ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
52
+ github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
53
+ github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
54
+ github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
55
+ github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
56
+ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
57
+ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
58
+ github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
59
+ github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
60
+ github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
61
+ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
62
+ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
63
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
64
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
65
+ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
66
+ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
67
+ github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
68
+ github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
69
+ github.com/pkoukk/tiktoken-go v0.1.8 h1:85ENo+3FpWgAACBaEUVp+lctuTcYUO7BtmfhlN/QTRo=
70
+ github.com/pkoukk/tiktoken-go v0.1.8/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
71
+ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
72
+ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
73
+ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
74
+ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
75
+ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
76
+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
77
+ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
78
+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
79
+ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
80
+ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
81
+ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
82
+ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
83
+ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
84
+ github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
85
+ github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
86
+ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
87
+ github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
88
+ github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
89
+ github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
90
+ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
91
+ golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
92
+ golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
93
+ golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
94
+ golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
95
+ golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
96
+ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
97
+ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
98
+ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
99
+ golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
100
+ golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
101
+ golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
102
+ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
103
+ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
104
+ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
105
+ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
106
+ google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
107
+ google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
108
+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
109
+ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
110
+ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
111
+ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
112
+ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
113
+ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
114
+ gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
115
+ gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
116
+ gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
117
+ gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
118
+ rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
internal/handler/auth.go ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package handler
2
+
3
+ import (
4
+ "net/http"
5
+ "opus-api/internal/middleware"
6
+ "opus-api/internal/service"
7
+ "opus-api/internal/types"
8
+
9
+ "github.com/gin-gonic/gin"
10
+ )
11
+
12
+ // AuthHandler 认证处理器
13
+ type AuthHandler struct {
14
+ authService *service.AuthService
15
+ }
16
+
17
+ // NewAuthHandler 创建认证处理器
18
+ func NewAuthHandler(authService *service.AuthService) *AuthHandler {
19
+ return &AuthHandler{authService: authService}
20
+ }
21
+
22
+ // LoginRequest 登录请求
23
+ type LoginRequest struct {
24
+ Username string `json:"username" binding:"required"`
25
+ Password string `json:"password" binding:"required"`
26
+ }
27
+
28
+ // LoginResponse 登录响应
29
+ type LoginResponse struct {
30
+ Token string `json:"token"`
31
+ User User `json:"user"`
32
+ }
33
+
34
+ // User 用户信息
35
+ type User struct {
36
+ ID uint `json:"id"`
37
+ Username string `json:"username"`
38
+ }
39
+
40
+ // Login 登录
41
+ func (h *AuthHandler) Login(c *gin.Context) {
42
+ var req LoginRequest
43
+ if err := c.ShouldBindJSON(&req); err != nil {
44
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
45
+ return
46
+ }
47
+
48
+ user, token, err := h.authService.Login(req.Username, req.Password)
49
+ if err != nil {
50
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid username or password"})
51
+ return
52
+ }
53
+
54
+ c.JSON(http.StatusOK, LoginResponse{
55
+ Token: token,
56
+ User: User{
57
+ ID: user.ID,
58
+ Username: user.Username,
59
+ },
60
+ })
61
+ }
62
+
63
+ // Logout 登出
64
+ func (h *AuthHandler) Logout(c *gin.Context) {
65
+ userID, ok := middleware.GetUserID(c)
66
+ if !ok {
67
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
68
+ return
69
+ }
70
+
71
+ if err := h.authService.Logout(userID); err != nil {
72
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to logout"})
73
+ return
74
+ }
75
+
76
+ c.JSON(http.StatusOK, gin.H{"message": "logged out successfully"})
77
+ }
78
+
79
+ // Me 获取当前用户信息
80
+ func (h *AuthHandler) Me(c *gin.Context) {
81
+ userID, ok := middleware.GetUserID(c)
82
+ if !ok {
83
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
84
+ return
85
+ }
86
+
87
+ user, err := h.authService.GetUserByID(userID)
88
+ if err != nil {
89
+ c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
90
+ return
91
+ }
92
+
93
+ c.JSON(http.StatusOK, User{
94
+ ID: user.ID,
95
+ Username: user.Username,
96
+ })
97
+ }
98
+
99
+ // ChangePassword 修改密码
100
+ func (h *AuthHandler) ChangePassword(c *gin.Context) {
101
+ var req types.ChangePasswordRequest
102
+ if err := c.ShouldBindJSON(&req); err != nil {
103
+ c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数错误: " + err.Error()})
104
+ return
105
+ }
106
+
107
+ // 从上下文获取当前用户ID
108
+ userID, ok := middleware.GetUserID(c)
109
+ if !ok {
110
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
111
+ return
112
+ }
113
+
114
+ // 修改密码
115
+ if err := h.authService.ChangePassword(userID, req.OldPassword, req.NewPassword); err != nil {
116
+ if err == service.ErrInvalidCredentials {
117
+ c.JSON(http.StatusBadRequest, gin.H{"error": "原密码错误"})
118
+ return
119
+ }
120
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "密码修改失败"})
121
+ return
122
+ }
123
+
124
+ c.JSON(http.StatusOK, gin.H{"message": "密码修改成功"})
125
+ }
internal/handler/cookies.go ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package handler
2
+
3
+ import (
4
+ "net/http"
5
+ "opus-api/internal/middleware"
6
+ "opus-api/internal/model"
7
+ "opus-api/internal/service"
8
+ "strconv"
9
+
10
+ "github.com/gin-gonic/gin"
11
+ )
12
+
13
+ // CookieHandler Cookie 管理处理器
14
+ type CookieHandler struct {
15
+ cookieService *service.CookieService
16
+ validator *service.CookieValidator
17
+ }
18
+
19
+ // NewCookieHandler 创建 Cookie 处理器
20
+ func NewCookieHandler(cookieService *service.CookieService, validator *service.CookieValidator) *CookieHandler {
21
+ return &CookieHandler{
22
+ cookieService: cookieService,
23
+ validator: validator,
24
+ }
25
+ }
26
+
27
+ // CreateCookieRequest 创建 Cookie 请求
28
+ type CreateCookieRequest struct {
29
+ Name string `json:"name" binding:"required"`
30
+ APIKey string `json:"api_key" binding:"required"`
31
+ SessionKey string `json:"session_key"`
32
+ Priority int `json:"priority"`
33
+ }
34
+
35
+ // UpdateCookieRequest 更新 Cookie 请求
36
+ type UpdateCookieRequest struct {
37
+ Name string `json:"name"`
38
+ APIKey string `json:"api_key"`
39
+ SessionKey string `json:"session_key"`
40
+ Priority *int `json:"priority"`
41
+ IsValid *bool `json:"is_valid"`
42
+ }
43
+
44
+ // CookieResponse Cookie 响应
45
+ type CookieResponse struct {
46
+ ID uint `json:"id"`
47
+ Name string `json:"name"`
48
+ APIKey string `json:"api_key"`
49
+ SessionKey string `json:"session_key"`
50
+ IsValid bool `json:"is_valid"`
51
+ Priority int `json:"priority"`
52
+ UsageCount int64 `json:"usage_count"`
53
+ ErrorCount int `json:"error_count"`
54
+ LastUsed string `json:"last_used,omitempty"`
55
+ LastValidated string `json:"last_validated,omitempty"`
56
+ CreatedAt string `json:"created_at"`
57
+ UpdatedAt string `json:"updated_at"`
58
+ }
59
+
60
+ // ListCookies 获取 Cookie 列表
61
+ func (h *CookieHandler) ListCookies(c *gin.Context) {
62
+ userID, ok := middleware.GetUserID(c)
63
+ if !ok {
64
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
65
+ return
66
+ }
67
+
68
+ cookies, err := h.cookieService.ListCookies(userID)
69
+ if err != nil {
70
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list cookies"})
71
+ return
72
+ }
73
+
74
+ responses := make([]CookieResponse, len(cookies))
75
+ for i, cookie := range cookies {
76
+ responses[i] = toCookieResponse(&cookie)
77
+ }
78
+
79
+ c.JSON(http.StatusOK, responses)
80
+ }
81
+
82
+ // GetCookie 获取单个 Cookie
83
+ func (h *CookieHandler) GetCookie(c *gin.Context) {
84
+ userID, ok := middleware.GetUserID(c)
85
+ if !ok {
86
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
87
+ return
88
+ }
89
+
90
+ idStr := c.Param("id")
91
+ id, err := strconv.ParseUint(idStr, 10, 32)
92
+ if err != nil {
93
+ c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
94
+ return
95
+ }
96
+
97
+ cookie, err := h.cookieService.GetCookie(uint(id), userID)
98
+ if err != nil {
99
+ if err == service.ErrCookieNotFound {
100
+ c.JSON(http.StatusNotFound, gin.H{"error": "cookie not found"})
101
+ return
102
+ }
103
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get cookie"})
104
+ return
105
+ }
106
+
107
+ c.JSON(http.StatusOK, toCookieResponse(cookie))
108
+ }
109
+
110
+ // CreateCookie 创建 Cookie
111
+ func (h *CookieHandler) CreateCookie(c *gin.Context) {
112
+ userID, ok := middleware.GetUserID(c)
113
+ if !ok {
114
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
115
+ return
116
+ }
117
+
118
+ var req CreateCookieRequest
119
+ if err := c.ShouldBindJSON(&req); err != nil {
120
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
121
+ return
122
+ }
123
+
124
+ cookie := &model.MorphCookie{
125
+ UserID: userID,
126
+ Name: req.Name,
127
+ APIKey: req.APIKey,
128
+ SessionKey: req.SessionKey,
129
+ Priority: req.Priority,
130
+ IsValid: true, // 默认有效,可以通过验证接口验证
131
+ }
132
+
133
+ if err := h.cookieService.CreateCookie(cookie); err != nil {
134
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create cookie"})
135
+ return
136
+ }
137
+
138
+ c.JSON(http.StatusCreated, toCookieResponse(cookie))
139
+ }
140
+
141
+ // UpdateCookie 更新 Cookie
142
+ func (h *CookieHandler) UpdateCookie(c *gin.Context) {
143
+ userID, ok := middleware.GetUserID(c)
144
+ if !ok {
145
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
146
+ return
147
+ }
148
+
149
+ idStr := c.Param("id")
150
+ id, err := strconv.ParseUint(idStr, 10, 32)
151
+ if err != nil {
152
+ c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
153
+ return
154
+ }
155
+
156
+ cookie, err := h.cookieService.GetCookie(uint(id), userID)
157
+ if err != nil {
158
+ if err == service.ErrCookieNotFound {
159
+ c.JSON(http.StatusNotFound, gin.H{"error": "cookie not found"})
160
+ return
161
+ }
162
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get cookie"})
163
+ return
164
+ }
165
+
166
+ var req UpdateCookieRequest
167
+ if err := c.ShouldBindJSON(&req); err != nil {
168
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
169
+ return
170
+ }
171
+
172
+ // 更新字段
173
+ if req.Name != "" {
174
+ cookie.Name = req.Name
175
+ }
176
+ if req.APIKey != "" {
177
+ cookie.APIKey = req.APIKey
178
+ }
179
+ if req.SessionKey != "" {
180
+ cookie.SessionKey = req.SessionKey
181
+ }
182
+ if req.Priority != nil {
183
+ cookie.Priority = *req.Priority
184
+ }
185
+ if req.IsValid != nil {
186
+ cookie.IsValid = *req.IsValid
187
+ }
188
+
189
+ if err := h.cookieService.UpdateCookie(cookie); err != nil {
190
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update cookie"})
191
+ return
192
+ }
193
+
194
+ c.JSON(http.StatusOK, toCookieResponse(cookie))
195
+ }
196
+
197
+ // DeleteCookie 删除 Cookie
198
+ func (h *CookieHandler) DeleteCookie(c *gin.Context) {
199
+ userID, ok := middleware.GetUserID(c)
200
+ if !ok {
201
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
202
+ return
203
+ }
204
+
205
+ idStr := c.Param("id")
206
+ id, err := strconv.ParseUint(idStr, 10, 32)
207
+ if err != nil {
208
+ c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
209
+ return
210
+ }
211
+
212
+ if err := h.cookieService.DeleteCookie(uint(id), userID); err != nil {
213
+ if err == service.ErrCookieNotFound {
214
+ c.JSON(http.StatusNotFound, gin.H{"error": "cookie not found"})
215
+ return
216
+ }
217
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete cookie"})
218
+ return
219
+ }
220
+
221
+ c.JSON(http.StatusOK, gin.H{"message": "cookie deleted successfully"})
222
+ }
223
+
224
+ // ValidateCookie 验证单个 Cookie
225
+ func (h *CookieHandler) ValidateCookie(c *gin.Context) {
226
+ userID, ok := middleware.GetUserID(c)
227
+ if !ok {
228
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
229
+ return
230
+ }
231
+
232
+ idStr := c.Param("id")
233
+ id, err := strconv.ParseUint(idStr, 10, 32)
234
+ if err != nil {
235
+ c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
236
+ return
237
+ }
238
+
239
+ cookie, err := h.cookieService.GetCookie(uint(id), userID)
240
+ if err != nil {
241
+ if err == service.ErrCookieNotFound {
242
+ c.JSON(http.StatusNotFound, gin.H{"error": "cookie not found"})
243
+ return
244
+ }
245
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get cookie"})
246
+ return
247
+ }
248
+
249
+ isValid := h.validator.ValidateCookie(cookie)
250
+
251
+ c.JSON(http.StatusOK, gin.H{
252
+ "id": cookie.ID,
253
+ "is_valid": isValid,
254
+ })
255
+ }
256
+
257
+ // ValidateAllCookies 验证所有 Cookie
258
+ func (h *CookieHandler) ValidateAllCookies(c *gin.Context) {
259
+ userID, ok := middleware.GetUserID(c)
260
+ if !ok {
261
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
262
+ return
263
+ }
264
+
265
+ results := h.validator.ValidateAllCookies(userID)
266
+
267
+ c.JSON(http.StatusOK, gin.H{
268
+ "results": results,
269
+ })
270
+ }
271
+
272
+ // GetStats 获取统计信息
273
+ func (h *CookieHandler) GetStats(c *gin.Context) {
274
+ userID, ok := middleware.GetUserID(c)
275
+ if !ok {
276
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
277
+ return
278
+ }
279
+
280
+ stats, err := h.cookieService.GetStats(userID)
281
+ if err != nil {
282
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get stats"})
283
+ return
284
+ }
285
+
286
+ c.JSON(http.StatusOK, stats)
287
+ }
288
+
289
+ // toCookieResponse 转换为响应格式
290
+ func toCookieResponse(cookie *model.MorphCookie) CookieResponse {
291
+ resp := CookieResponse{
292
+ ID: cookie.ID,
293
+ Name: cookie.Name,
294
+ APIKey: maskAPIKey(cookie.APIKey),
295
+ SessionKey: cookie.SessionKey,
296
+ IsValid: cookie.IsValid,
297
+ Priority: cookie.Priority,
298
+ UsageCount: cookie.UsageCount,
299
+ ErrorCount: cookie.ErrorCount,
300
+ CreatedAt: cookie.CreatedAt.Format("2006-01-02 15:04:05"),
301
+ UpdatedAt: cookie.UpdatedAt.Format("2006-01-02 15:04:05"),
302
+ }
303
+
304
+ if cookie.LastUsed != nil {
305
+ resp.LastUsed = cookie.LastUsed.Format("2006-01-02 15:04:05")
306
+ }
307
+ if cookie.LastValidated != nil {
308
+ resp.LastValidated = cookie.LastValidated.Format("2006-01-02 15:04:05")
309
+ }
310
+
311
+ return resp
312
+ }
313
+
314
+ // maskAPIKey 隐藏 API Key 中间部分
315
+ func maskAPIKey(apiKey string) string {
316
+ if len(apiKey) <= 8 {
317
+ return "****"
318
+ }
319
+ return apiKey[:4] + "****" + apiKey[len(apiKey)-4:]
320
+ }
internal/handler/messages.go CHANGED
@@ -9,6 +9,7 @@ import (
9
  "net/http"
10
  "opus-api/internal/converter"
11
  "opus-api/internal/logger"
 
12
  "opus-api/internal/stream"
13
  "opus-api/internal/tokenizer"
14
  "opus-api/internal/types"
@@ -35,6 +36,17 @@ func HandleMessages(c *gin.Context) {
35
  return
36
  }
37
 
 
 
 
 
 
 
 
 
 
 
 
38
  // Log Point 1: Claude request
39
  var logFolder string
40
  if types.DebugMode {
@@ -64,7 +76,29 @@ func HandleMessages(c *gin.Context) {
64
  }
65
 
66
  // Set headers
 
67
  for key, value := range types.MorphHeaders {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  req.Header.Set(key, value)
69
  }
70
 
 
9
  "net/http"
10
  "opus-api/internal/converter"
11
  "opus-api/internal/logger"
12
+ "opus-api/internal/model"
13
  "opus-api/internal/stream"
14
  "opus-api/internal/tokenizer"
15
  "opus-api/internal/types"
 
36
  return
37
  }
38
 
39
+ // 验证模型是否支持
40
+ if claudeReq.Model == "" {
41
+ claudeReq.Model = types.DefaultModel
42
+ }
43
+ if !types.IsModelSupported(claudeReq.Model) {
44
+ c.JSON(http.StatusBadRequest, gin.H{
45
+ "error": fmt.Sprintf("Model '%s' is not supported. Supported models: %v", claudeReq.Model, types.SupportedModels),
46
+ })
47
+ return
48
+ }
49
+
50
  // Log Point 1: Claude request
51
  var logFolder string
52
  if types.DebugMode {
 
76
  }
77
 
78
  // Set headers
79
+ headers := make(map[string]string)
80
  for key, value := range types.MorphHeaders {
81
+ headers[key] = value
82
+ }
83
+
84
+ // 如果启用了 Cookie 轮询器,使用轮询的 Cookie
85
+ if types.CookieRotatorInstance != nil {
86
+ cookieInterface, err := types.CookieRotatorInstance.NextCookie()
87
+ if err == nil && cookieInterface != nil {
88
+ // 类型断言为 *model.MorphCookie
89
+ if cookie, ok := cookieInterface.(*model.MorphCookie); ok {
90
+ headers["cookie"] = cookie.APIKey
91
+ log.Printf("[INFO] Using rotated cookie (ID: %d, Priority: %d)", cookie.ID, cookie.Priority)
92
+ } else {
93
+ log.Printf("[WARN] Cookie type assertion failed, using default")
94
+ }
95
+ } else {
96
+ log.Printf("[WARN] Failed to get rotated cookie: %v, using default", err)
97
+ }
98
+ }
99
+
100
+ // 应用所有请求头
101
+ for key, value := range headers {
102
  req.Header.Set(key, value)
103
  }
104
 
internal/middleware/auth.go ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package middleware
2
+
3
+ import (
4
+ "net/http"
5
+ "opus-api/internal/service"
6
+ "strings"
7
+
8
+ "github.com/gin-gonic/gin"
9
+ )
10
+
11
+ // AuthMiddleware JWT 认证中间件
12
+ func AuthMiddleware(authService *service.AuthService) gin.HandlerFunc {
13
+ return func(c *gin.Context) {
14
+ authHeader := c.GetHeader("Authorization")
15
+ if authHeader == "" {
16
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "missing authorization header"})
17
+ c.Abort()
18
+ return
19
+ }
20
+
21
+ // 解析 Bearer token
22
+ parts := strings.SplitN(authHeader, " ", 2)
23
+ if len(parts) != 2 || parts[0] != "Bearer" {
24
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid authorization header format"})
25
+ c.Abort()
26
+ return
27
+ }
28
+
29
+ token := parts[1]
30
+
31
+ // 验证 token
32
+ userID, err := authService.ValidateToken(token)
33
+ if err != nil {
34
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid or expired token"})
35
+ c.Abort()
36
+ return
37
+ }
38
+
39
+ // 将用户 ID 存储到上下文
40
+ c.Set("user_id", userID)
41
+ c.Next()
42
+ }
43
+ }
44
+
45
+ // GetUserID 从上下文获取用户 ID
46
+ func GetUserID(c *gin.Context) (uint, bool) {
47
+ userID, exists := c.Get("user_id")
48
+ if !exists {
49
+ return 0, false
50
+ }
51
+ id, ok := userID.(uint)
52
+ return id, ok
53
+ }
internal/model/cookie.go ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package model
2
+
3
+ import (
4
+ "time"
5
+ )
6
+
7
+ // MorphCookie Morph Cookie 模型
8
+ type MorphCookie struct {
9
+ ID uint `gorm:"primaryKey" json:"id"`
10
+ UserID uint `gorm:"not null;index" json:"user_id"`
11
+ User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"-"`
12
+ Name string `gorm:"size:100;not null" json:"name"`
13
+ APIKey string `gorm:"column:api_key;type:text;not null" json:"api_key"`
14
+ SessionKey string `gorm:"column:session_key;type:text" json:"session_key"`
15
+ IsValid bool `gorm:"default:true;index" json:"is_valid"`
16
+ LastValidated *time.Time `gorm:"column:last_validated" json:"last_validated"`
17
+ LastUsed *time.Time `gorm:"column:last_used" json:"last_used"`
18
+ Priority int `gorm:"default:0;index" json:"priority"`
19
+ UsageCount int64 `gorm:"default:0" json:"usage_count"`
20
+ ErrorCount int `gorm:"default:0" json:"error_count"`
21
+ CreatedAt time.Time `json:"created_at"`
22
+ UpdatedAt time.Time `json:"updated_at"`
23
+ }
24
+
25
+ // CookieStats Cookie 统计信息
26
+ type CookieStats struct {
27
+ TotalCount int64 `json:"total_count"`
28
+ ValidCount int64 `json:"valid_count"`
29
+ InvalidCount int64 `json:"invalid_count"`
30
+ TotalUsage int64 `json:"total_usage"`
31
+ }
32
+
33
+ // TableName 指定表名
34
+ func (MorphCookie) TableName() string {
35
+ return "morph_cookies"
36
+ }
37
+
38
+ // MarkUsed 标记 Cookie 已使用
39
+ func (c *MorphCookie) MarkUsed() {
40
+ now := time.Now()
41
+ c.LastUsed = &now
42
+ c.UsageCount++
43
+ c.ErrorCount = 0 // 成功使用后重置错误计数
44
+ }
45
+
46
+ // MarkError 标记 Cookie 错误
47
+ func (c *MorphCookie) MarkError() {
48
+ c.ErrorCount++
49
+ }
50
+
51
+ // MarkInvalid 标记 Cookie 无效
52
+ func (c *MorphCookie) MarkInvalid() {
53
+ c.IsValid = false
54
+ now := time.Now()
55
+ c.LastValidated = &now
56
+ }
57
+
58
+ // MarkValid 标记 Cookie 有效
59
+ func (c *MorphCookie) MarkValid() {
60
+ c.IsValid = true
61
+ c.ErrorCount = 0
62
+ now := time.Now()
63
+ c.LastValidated = &now
64
+ }
internal/model/db.go ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package model
2
+
3
+ import (
4
+ "fmt"
5
+ "log"
6
+ "os"
7
+ "time"
8
+
9
+ "gorm.io/driver/postgres"
10
+ "gorm.io/gorm"
11
+ "gorm.io/gorm/logger"
12
+ )
13
+
14
+ var DB *gorm.DB
15
+
16
+ // InitDB 初始化数据库连接
17
+ func InitDB() error {
18
+ databaseURL := os.Getenv("DATABASE_URL")
19
+ if databaseURL == "" {
20
+ return fmt.Errorf("DATABASE_URL environment variable is not set")
21
+ }
22
+
23
+ var err error
24
+ DB, err = gorm.Open(postgres.Open(databaseURL), &gorm.Config{
25
+ Logger: logger.Default.LogMode(logger.Info),
26
+ NowFunc: func() time.Time {
27
+ return time.Now().UTC()
28
+ },
29
+ })
30
+ if err != nil {
31
+ return fmt.Errorf("failed to connect to database: %w", err)
32
+ }
33
+
34
+ // 配置连接池
35
+ sqlDB, err := DB.DB()
36
+ if err != nil {
37
+ return fmt.Errorf("failed to get database instance: %w", err)
38
+ }
39
+
40
+ sqlDB.SetMaxIdleConns(10)
41
+ sqlDB.SetMaxOpenConns(100)
42
+ sqlDB.SetConnMaxLifetime(time.Hour)
43
+
44
+ // 自动迁移
45
+ if err := autoMigrate(); err != nil {
46
+ return fmt.Errorf("failed to migrate database: %w", err)
47
+ }
48
+
49
+ log.Println("Database connected and migrated successfully")
50
+ return nil
51
+ }
52
+
53
+ // autoMigrate 自动迁移数据库表结构
54
+ func autoMigrate() error {
55
+ return DB.AutoMigrate(
56
+ &User{},
57
+ &MorphCookie{},
58
+ &UserSession{},
59
+ )
60
+ }
61
+
62
+ // CloseDB 关闭数据库连接
63
+ func CloseDB() error {
64
+ sqlDB, err := DB.DB()
65
+ if err != nil {
66
+ return err
67
+ }
68
+ return sqlDB.Close()
69
+ }
internal/model/user.go ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package model
2
+
3
+ import (
4
+ "log"
5
+ "os"
6
+ "time"
7
+
8
+ "golang.org/x/crypto/bcrypt"
9
+ "gorm.io/gorm"
10
+ )
11
+
12
+ // User 用户模型
13
+ type User struct {
14
+ ID uint `gorm:"primaryKey" json:"id"`
15
+ Username string `gorm:"uniqueIndex;size:50;not null" json:"username"`
16
+ PasswordHash string `gorm:"size:255;not null" json:"-"`
17
+ CreatedAt time.Time `json:"created_at"`
18
+ UpdatedAt time.Time `json:"updated_at"`
19
+ }
20
+
21
+ // UserSession 用户会话模型
22
+ type UserSession struct {
23
+ ID uint `gorm:"primaryKey" json:"id"`
24
+ UserID uint `gorm:"not null;index" json:"user_id"`
25
+ User User `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE" json:"-"`
26
+ TokenHash string `gorm:"uniqueIndex;size:255;not null" json:"-"`
27
+ ExpiresAt time.Time `gorm:"not null;index" json:"expires_at"`
28
+ CreatedAt time.Time `json:"created_at"`
29
+ }
30
+
31
+ // SetPassword 设置用户密码(加密)
32
+ func (u *User) SetPassword(password string) error {
33
+ hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
34
+ if err != nil {
35
+ return err
36
+ }
37
+ u.PasswordHash = string(hash)
38
+ return nil
39
+ }
40
+
41
+ // CheckPassword 验证密码
42
+ func (u *User) CheckPassword(password string) bool {
43
+ err := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password))
44
+ return err == nil
45
+ }
46
+
47
+ // TableName 指定表名
48
+ func (User) TableName() string {
49
+ return "users"
50
+ }
51
+
52
+ // TableName 指定表名
53
+ func (UserSession) TableName() string {
54
+ return "user_sessions"
55
+ }
56
+
57
+ // CreateDefaultAdmin 创建默认管理员用户
58
+ func CreateDefaultAdmin(db *gorm.DB) error {
59
+ username := os.Getenv("DEFAULT_ADMIN_USERNAME")
60
+ if username == "" {
61
+ username = "admin"
62
+ }
63
+
64
+ password := os.Getenv("DEFAULT_ADMIN_PASSWORD")
65
+ if password == "" {
66
+ password = "changeme123"
67
+ }
68
+
69
+ // 检查用户是否已存在
70
+ var existingUser User
71
+ result := db.Where("username = ?", username).First(&existingUser)
72
+ if result.Error == nil {
73
+ // 用户已存在
74
+ log.Printf("Default admin user '%s' already exists", username)
75
+ return nil
76
+ }
77
+
78
+ // 创建新用户
79
+ user := &User{
80
+ Username: username,
81
+ }
82
+ if err := user.SetPassword(password); err != nil {
83
+ return err
84
+ }
85
+
86
+ if err := db.Create(user).Error; err != nil {
87
+ return err
88
+ }
89
+
90
+ log.Printf("Default admin user '%s' created successfully", username)
91
+ return nil
92
+ }
internal/service/auth_service.go ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package service
2
+
3
+ import (
4
+ "crypto/sha256"
5
+ "encoding/hex"
6
+ "errors"
7
+ "fmt"
8
+ "os"
9
+ "time"
10
+
11
+ "opus-api/internal/model"
12
+
13
+ "github.com/golang-jwt/jwt/v5"
14
+ "gorm.io/gorm"
15
+ )
16
+
17
+ var (
18
+ ErrInvalidCredentials = errors.New("invalid username or password")
19
+ ErrUserNotFound = errors.New("user not found")
20
+ ErrInvalidToken = errors.New("invalid token")
21
+ )
22
+
23
+ // AuthService 认证服务
24
+ type AuthService struct {
25
+ db *gorm.DB
26
+ jwtSecret []byte
27
+ }
28
+
29
+ // NewAuthService 创建认证服务
30
+ func NewAuthService(db *gorm.DB) *AuthService {
31
+ secret := os.Getenv("JWT_SECRET")
32
+ if secret == "" {
33
+ secret = "default-secret-change-me-in-production"
34
+ }
35
+ return &AuthService{
36
+ db: db,
37
+ jwtSecret: []byte(secret),
38
+ }
39
+ }
40
+
41
+ // Claims JWT 声明
42
+ type Claims struct {
43
+ UserID uint `json:"user_id"`
44
+ Username string `json:"username"`
45
+ jwt.RegisteredClaims
46
+ }
47
+
48
+ // Login 用户登录
49
+ func (s *AuthService) Login(username, password string) (*model.User, string, error) {
50
+ var user model.User
51
+ if err := s.db.Where("username = ?", username).First(&user).Error; err != nil {
52
+ if errors.Is(err, gorm.ErrRecordNotFound) {
53
+ return nil, "", ErrInvalidCredentials
54
+ }
55
+ return nil, "", err
56
+ }
57
+
58
+ if !user.CheckPassword(password) {
59
+ return nil, "", ErrInvalidCredentials
60
+ }
61
+
62
+ // 生成 JWT token
63
+ token, err := s.generateToken(&user)
64
+ if err != nil {
65
+ return nil, "", err
66
+ }
67
+
68
+ // 保存会话
69
+ if err := s.saveSession(&user, token); err != nil {
70
+ return nil, "", err
71
+ }
72
+
73
+ return &user, token, nil
74
+ }
75
+
76
+ // generateToken 生成 JWT token
77
+ func (s *AuthService) generateToken(user *model.User) (string, error) {
78
+ expirationTime := time.Now().Add(24 * time.Hour)
79
+ claims := &Claims{
80
+ UserID: user.ID,
81
+ Username: user.Username,
82
+ RegisteredClaims: jwt.RegisteredClaims{
83
+ ExpiresAt: jwt.NewNumericDate(expirationTime),
84
+ IssuedAt: jwt.NewNumericDate(time.Now()),
85
+ },
86
+ }
87
+
88
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
89
+ return token.SignedString(s.jwtSecret)
90
+ }
91
+
92
+ // saveSession 保存会话
93
+ func (s *AuthService) saveSession(user *model.User, token string) error {
94
+ tokenHash := hashToken(token)
95
+ session := &model.UserSession{
96
+ UserID: user.ID,
97
+ TokenHash: tokenHash,
98
+ ExpiresAt: time.Now().Add(24 * time.Hour),
99
+ }
100
+ return s.db.Create(session).Error
101
+ }
102
+
103
+ // ValidateToken 验证 token,返回用户 ID
104
+ func (s *AuthService) ValidateToken(tokenString string) (uint, error) {
105
+ claims := &Claims{}
106
+ token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
107
+ if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
108
+ return 0, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
109
+ }
110
+ return s.jwtSecret, nil
111
+ })
112
+
113
+ if err != nil {
114
+ return 0, ErrInvalidToken
115
+ }
116
+
117
+ if !token.Valid {
118
+ return 0, ErrInvalidToken
119
+ }
120
+
121
+ // 检查会话是否存在且未过期
122
+ tokenHash := hashToken(tokenString)
123
+ var session model.UserSession
124
+ if err := s.db.Where("token_hash = ? AND expires_at > ?", tokenHash, time.Now()).First(&session).Error; err != nil {
125
+ if errors.Is(err, gorm.ErrRecordNotFound) {
126
+ return 0, ErrInvalidToken
127
+ }
128
+ return 0, err
129
+ }
130
+
131
+ return claims.UserID, nil
132
+ }
133
+
134
+ // Logout 用户登出(删除用户的所有会话)
135
+ func (s *AuthService) Logout(userID uint) error {
136
+ return s.db.Where("user_id = ?", userID).Delete(&model.UserSession{}).Error
137
+ }
138
+
139
+ // GetUserByID 根据 ID 获取用户
140
+ func (s *AuthService) GetUserByID(userID uint) (*model.User, error) {
141
+ var user model.User
142
+ if err := s.db.First(&user, userID).Error; err != nil {
143
+ if errors.Is(err, gorm.ErrRecordNotFound) {
144
+ return nil, ErrUserNotFound
145
+ }
146
+ return nil, err
147
+ }
148
+ return &user, nil
149
+ }
150
+
151
+ // hashToken 对 token 进行哈希
152
+ func hashToken(token string) string {
153
+ hash := sha256.Sum256([]byte(token))
154
+ return hex.EncodeToString(hash[:])
155
+ }
156
+
157
+ // CleanExpiredSessions 清理过期会话
158
+ func (s *AuthService) CleanExpiredSessions() error {
159
+ return s.db.Where("expires_at < ?", time.Now()).Delete(&model.UserSession{}).Error
160
+ }
161
+
162
+ // ChangePassword 修改用户密码
163
+ func (s *AuthService) ChangePassword(userID uint, oldPassword, newPassword string) error {
164
+ var user model.User
165
+
166
+ // 查找用户
167
+ if err := s.db.First(&user, userID).Error; err != nil {
168
+ if errors.Is(err, gorm.ErrRecordNotFound) {
169
+ return ErrUserNotFound
170
+ }
171
+ return err
172
+ }
173
+
174
+ // 验证旧密码
175
+ if !user.CheckPassword(oldPassword) {
176
+ return ErrInvalidCredentials
177
+ }
178
+
179
+ // 设置新密码
180
+ if err := user.SetPassword(newPassword); err != nil {
181
+ return fmt.Errorf("failed to hash password: %w", err)
182
+ }
183
+
184
+ // 保存到数据库
185
+ if err := s.db.Save(&user).Error; err != nil {
186
+ return fmt.Errorf("failed to update password: %w", err)
187
+ }
188
+
189
+ return nil
190
+ }
internal/service/cookie_service.go ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package service
2
+
3
+ import (
4
+ "errors"
5
+ "opus-api/internal/model"
6
+
7
+ "gorm.io/gorm"
8
+ )
9
+
10
+ var (
11
+ ErrCookieNotFound = errors.New("cookie not found")
12
+ )
13
+
14
+ // CookieService Cookie 管理服务
15
+ type CookieService struct {
16
+ db *gorm.DB
17
+ }
18
+
19
+ // NewCookieService 创建 Cookie 服务
20
+ func NewCookieService(db *gorm.DB) *CookieService {
21
+ return &CookieService{db: db}
22
+ }
23
+
24
+ // GetDB 获取数据库连接
25
+ func (s *CookieService) GetDB() *gorm.DB {
26
+ return s.db
27
+ }
28
+
29
+ // ListCookies 获取用户的所有 Cookie
30
+ func (s *CookieService) ListCookies(userID uint) ([]model.MorphCookie, error) {
31
+ var cookies []model.MorphCookie
32
+ err := s.db.Where("user_id = ?", userID).
33
+ Order("priority DESC, created_at DESC").
34
+ Find(&cookies).Error
35
+ return cookies, err
36
+ }
37
+
38
+ // GetCookie 获取单个 Cookie
39
+ func (s *CookieService) GetCookie(id, userID uint) (*model.MorphCookie, error) {
40
+ var cookie model.MorphCookie
41
+ err := s.db.Where("id = ? AND user_id = ?", id, userID).First(&cookie).Error
42
+ if err != nil {
43
+ if errors.Is(err, gorm.ErrRecordNotFound) {
44
+ return nil, ErrCookieNotFound
45
+ }
46
+ return nil, err
47
+ }
48
+ return &cookie, nil
49
+ }
50
+
51
+ // CreateCookie 创建 Cookie
52
+ func (s *CookieService) CreateCookie(cookie *model.MorphCookie) error {
53
+ return s.db.Create(cookie).Error
54
+ }
55
+
56
+ // UpdateCookie 更新 Cookie
57
+ func (s *CookieService) UpdateCookie(cookie *model.MorphCookie) error {
58
+ return s.db.Save(cookie).Error
59
+ }
60
+
61
+ // DeleteCookie 删除 Cookie
62
+ func (s *CookieService) DeleteCookie(id, userID uint) error {
63
+ result := s.db.Where("id = ? AND user_id = ?", id, userID).Delete(&model.MorphCookie{})
64
+ if result.Error != nil {
65
+ return result.Error
66
+ }
67
+ if result.RowsAffected == 0 {
68
+ return ErrCookieNotFound
69
+ }
70
+ return nil
71
+ }
72
+
73
+ // GetStats 获取统计信息
74
+ func (s *CookieService) GetStats(userID uint) (*model.CookieStats, error) {
75
+ stats := &model.CookieStats{}
76
+
77
+ // 总数
78
+ if err := s.db.Model(&model.MorphCookie{}).
79
+ Where("user_id = ?", userID).
80
+ Count(&stats.TotalCount).Error; err != nil {
81
+ return nil, err
82
+ }
83
+
84
+ // 有效数量
85
+ if err := s.db.Model(&model.MorphCookie{}).
86
+ Where("user_id = ? AND is_valid = ?", userID, true).
87
+ Count(&stats.ValidCount).Error; err != nil {
88
+ return nil, err
89
+ }
90
+
91
+ // 无效数量
92
+ stats.InvalidCount = stats.TotalCount - stats.ValidCount
93
+
94
+ // 总使用次数
95
+ var totalUsage *int64
96
+ if err := s.db.Model(&model.MorphCookie{}).
97
+ Where("user_id = ?", userID).
98
+ Select("COALESCE(SUM(usage_count), 0)").
99
+ Scan(&totalUsage).Error; err != nil {
100
+ return nil, err
101
+ }
102
+ if totalUsage != nil {
103
+ stats.TotalUsage = *totalUsage
104
+ }
105
+
106
+ return stats, nil
107
+ }
108
+
109
+ // GetValidCookies 获取所有有效的 Cookie
110
+ func (s *CookieService) GetValidCookies(userID uint) ([]model.MorphCookie, error) {
111
+ var cookies []model.MorphCookie
112
+ err := s.db.Where("user_id = ? AND is_valid = ?", userID, true).
113
+ Order("priority DESC, usage_count ASC").
114
+ Find(&cookies).Error
115
+ return cookies, err
116
+ }
117
+
118
+ // GetAllValidCookies 获取系统中所有有效的 Cookie(用于轮询)
119
+ func (s *CookieService) GetAllValidCookies() ([]model.MorphCookie, error) {
120
+ var cookies []model.MorphCookie
121
+ err := s.db.Where("is_valid = ?", true).
122
+ Order("priority DESC, usage_count ASC").
123
+ Find(&cookies).Error
124
+ return cookies, err
125
+ }
internal/service/rotator.go ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package service
2
+
3
+ import (
4
+ "errors"
5
+ "opus-api/internal/model"
6
+ "sync"
7
+ "time"
8
+
9
+ "gorm.io/gorm"
10
+ )
11
+
12
+ // RotationStrategy Cookie 轮询策略
13
+ type RotationStrategy string
14
+
15
+ const (
16
+ StrategyRoundRobin RotationStrategy = "round_robin" // 轮询
17
+ StrategyPriority RotationStrategy = "priority" // 优先级
18
+ StrategyLeastUsed RotationStrategy = "least_used" // 最少使用
19
+ )
20
+
21
+ var (
22
+ ErrNoCookiesAvailable = errors.New("no valid cookies available")
23
+ )
24
+
25
+ // CookieRotator Cookie 轮询器
26
+ type CookieRotator struct {
27
+ service *CookieService
28
+ strategy RotationStrategy
29
+ mu sync.RWMutex
30
+ index int // 用于 round_robin 策略
31
+ }
32
+
33
+ // NewCookieRotator 创建轮询器
34
+ func NewCookieRotator(service *CookieService, strategy RotationStrategy) *CookieRotator {
35
+ if strategy == "" {
36
+ strategy = StrategyRoundRobin
37
+ }
38
+ return &CookieRotator{
39
+ service: service,
40
+ strategy: strategy,
41
+ index: 0,
42
+ }
43
+ }
44
+
45
+ // NextCookie 获取下一个可用的 Cookie
46
+ func (r *CookieRotator) NextCookie() (interface{}, error) {
47
+ r.mu.Lock()
48
+ defer r.mu.Unlock()
49
+
50
+ cookies, err := r.service.GetAllValidCookies()
51
+ if err != nil {
52
+ return nil, err
53
+ }
54
+
55
+ if len(cookies) == 0 {
56
+ return nil, ErrNoCookiesAvailable
57
+ }
58
+
59
+ var selected *model.MorphCookie
60
+
61
+ switch r.strategy {
62
+ case StrategyRoundRobin:
63
+ selected = r.roundRobin(cookies)
64
+ case StrategyPriority:
65
+ selected = r.priority(cookies)
66
+ case StrategyLeastUsed:
67
+ selected = r.leastUsed(cookies)
68
+ default:
69
+ selected = r.roundRobin(cookies)
70
+ }
71
+
72
+ if selected == nil {
73
+ return nil, ErrNoCookiesAvailable
74
+ }
75
+
76
+ return selected, nil
77
+ }
78
+
79
+ // roundRobin 轮询策略
80
+ func (r *CookieRotator) roundRobin(cookies []model.MorphCookie) *model.MorphCookie {
81
+ if len(cookies) == 0 {
82
+ return nil
83
+ }
84
+
85
+ r.index = r.index % len(cookies)
86
+ selected := &cookies[r.index]
87
+ r.index++
88
+
89
+ return selected
90
+ }
91
+
92
+ // priority 优先级策略(优先级高的优先使用)
93
+ func (r *CookieRotator) priority(cookies []model.MorphCookie) *model.MorphCookie {
94
+ if len(cookies) == 0 {
95
+ return nil
96
+ }
97
+
98
+ // cookies 已经按照 priority DESC 排序
99
+ return &cookies[0]
100
+ }
101
+
102
+ // leastUsed 最少使用策略
103
+ func (r *CookieRotator) leastUsed(cookies []model.MorphCookie) *model.MorphCookie {
104
+ if len(cookies) == 0 {
105
+ return nil
106
+ }
107
+
108
+ // cookies 已经按照 usage_count ASC 排序
109
+ return &cookies[0]
110
+ }
111
+
112
+ // MarkUsed 标记 Cookie 已使用
113
+ func (r *CookieRotator) MarkUsed(cookieID uint) error {
114
+ db := r.service.GetDB()
115
+ return db.Model(&model.MorphCookie{}).
116
+ Where("id = ?", cookieID).
117
+ Updates(map[string]interface{}{
118
+ "usage_count": gorm.Expr("usage_count + ?", 1),
119
+ "last_used": time.Now(),
120
+ }).Error
121
+ }
122
+
123
+ // MarkInvalid 标记 Cookie 无效
124
+ func (r *CookieRotator) MarkInvalid(cookieID uint) error {
125
+ db := r.service.GetDB()
126
+ return db.Model(&model.MorphCookie{}).
127
+ Where("id = ?", cookieID).
128
+ Updates(map[string]interface{}{
129
+ "is_valid": false,
130
+ }).Error
131
+ }
132
+
133
+ // MarkError 标记 Cookie 错误
134
+ func (r *CookieRotator) MarkError(cookieID uint) error {
135
+ db := r.service.GetDB()
136
+
137
+ var cookie model.MorphCookie
138
+ if err := db.First(&cookie, cookieID).Error; err != nil {
139
+ return err
140
+ }
141
+
142
+ updates := map[string]interface{}{
143
+ "error_count": gorm.Expr("error_count + ?", 1),
144
+ }
145
+
146
+ // 如果错误次数超过阈值,标记为无效
147
+ if cookie.ErrorCount >= 5 {
148
+ updates["is_valid"] = false
149
+ }
150
+
151
+ return db.Model(&model.MorphCookie{}).
152
+ Where("id = ?", cookieID).
153
+ Updates(updates).Error
154
+ }
155
+
156
+ // GetStrategy 获取当前策略
157
+ func (r *CookieRotator) GetStrategy() RotationStrategy {
158
+ return r.strategy
159
+ }
160
+
161
+ // SetStrategy 设置轮询策略
162
+ func (r *CookieRotator) SetStrategy(strategy RotationStrategy) {
163
+ r.mu.Lock()
164
+ defer r.mu.Unlock()
165
+ r.strategy = strategy
166
+ r.index = 0 // 重置索引
167
+ }
internal/service/validator.go ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package service
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/json"
6
+ "io"
7
+ "net/http"
8
+ "opus-api/internal/converter"
9
+ "opus-api/internal/model"
10
+ "opus-api/internal/types"
11
+ "time"
12
+
13
+ "gorm.io/gorm"
14
+ )
15
+
16
+ // CookieValidator Cookie 验证器
17
+ type CookieValidator struct {
18
+ service *CookieService
19
+ }
20
+
21
+ // NewCookieValidator 创建验证器
22
+ func NewCookieValidator(service *CookieService) *CookieValidator {
23
+ return &CookieValidator{service: service}
24
+ }
25
+
26
+ // ValidateCookie 验证单个 Cookie
27
+ func (v *CookieValidator) ValidateCookie(cookie *model.MorphCookie) bool {
28
+ result := v.testCookie(cookie)
29
+
30
+ db := v.service.GetDB()
31
+ if result {
32
+ db.Model(cookie).Updates(map[string]interface{}{
33
+ "is_valid": true,
34
+ "last_validated": time.Now(),
35
+ "error_count": 0,
36
+ })
37
+ } else {
38
+ db.Model(cookie).Updates(map[string]interface{}{
39
+ "is_valid": false,
40
+ "last_validated": time.Now(),
41
+ "error_count": gorm.Expr("error_count + ?", 1),
42
+ })
43
+ }
44
+
45
+ return result
46
+ }
47
+
48
+ // ValidateAllCookies 验证用户的所有 Cookie
49
+ func (v *CookieValidator) ValidateAllCookies(userID uint) map[uint]bool {
50
+ cookies, err := v.service.ListCookies(userID)
51
+ if err != nil {
52
+ return nil
53
+ }
54
+
55
+ results := make(map[uint]bool)
56
+ for _, cookie := range cookies {
57
+ results[cookie.ID] = v.ValidateCookie(&cookie)
58
+ }
59
+
60
+ return results
61
+ }
62
+
63
+ // testCookie 测试 Cookie 是否有效(与 /v1/messages 逻辑完全一致)
64
+ func (v *CookieValidator) testCookie(cookie *model.MorphCookie) bool {
65
+ client := &http.Client{
66
+ Timeout: 30 * time.Second,
67
+ }
68
+
69
+ // 创建 Claude 格式的测试请求
70
+ claudeReq := types.ClaudeRequest{
71
+ Model: types.DefaultModel,
72
+ MaxTokens: 1024,
73
+ Messages: []types.ClaudeMessage{
74
+ {
75
+ Role: "user",
76
+ Content: "Hello!",
77
+ },
78
+ },
79
+ }
80
+
81
+ // 使用与 /v1/messages 相同的转换逻辑,将 Claude 格式转换为 Morph 格式
82
+ morphReq := converter.ClaudeToMorph(claudeReq)
83
+
84
+ reqBody, _ := json.Marshal(morphReq)
85
+ req, err := http.NewRequest("POST", types.MorphAPIURL, bytes.NewReader(reqBody))
86
+ if err != nil {
87
+ return false
88
+ }
89
+
90
+ // 使用与 /v1/messages 相同的请求头
91
+ for key, value := range types.MorphHeaders {
92
+ req.Header.Set(key, value)
93
+ }
94
+ // 覆盖 Cookie
95
+ req.Header.Set("cookie", cookie.APIKey)
96
+
97
+ resp, err := client.Do(req)
98
+ if err != nil {
99
+ return false
100
+ }
101
+ defer resp.Body.Close()
102
+
103
+ // 检查响应状态码
104
+ // 200 OK 表示成功,401/403 表示认证失败
105
+ if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
106
+ return false
107
+ }
108
+
109
+ // 尝试读取响应体
110
+ body, _ := io.ReadAll(resp.Body)
111
+
112
+ // 检查是否是有效的 API 响应(SSE 流格式)
113
+ bodyStr := string(body)
114
+
115
+ // Morph API 返回 SSE 流,有效的响应会包含 "data:" 前缀
116
+ if !containsPrefix(bodyStr, "data:") {
117
+ return false
118
+ }
119
+
120
+ return true
121
+ }
122
+
123
+ // containsPrefix 检查字符串是否包含指定前缀(忽略空白字符)
124
+ func containsPrefix(s, prefix string) bool {
125
+ // 去除前导空白字符
126
+ start := 0
127
+ for start < len(s) && (s[start] == ' ' || s[start] == '\t' || s[start] == '\n' || s[start] == '\r') {
128
+ start++
129
+ }
130
+
131
+ if start >= len(s) {
132
+ return false
133
+ }
134
+
135
+ return len(s[start:]) >= len(prefix) && s[start:start+len(prefix)] == prefix
136
+ }
137
+
138
+ // validateCookieQuiet 静默验证 Cookie(不更新数据库)
139
+ func (v *CookieValidator) validateCookieQuiet(cookie *model.MorphCookie) bool {
140
+ return v.testCookie(cookie)
141
+ }
internal/types/common.go CHANGED
@@ -1,9 +1,13 @@
1
  package types
2
 
 
 
 
 
 
3
  const (
4
- MorphAPIURL = "https://www.morphllm.com/api/warpgrep-chat"
5
- MorphCookies = "_gcl_aw=GCL.1769242305.Cj0KCQiA-NHLBhDSARIsAIhe9X36gjH12LKRxYZ1bqy4c8wATix3qqxauv9-7H-rewEfnKOW0npi5ZMaAjfHEALw_wcB; _gcl_gs=2.1.k1$i1769242301$u142736937; _ga=GA1.1.1601700642.1769242305; __client_uat=1769312595; __client_uat_B3JMRlZP=1769312595; __refresh_B3JMRlZP=mCRNoZazsM9bFPAAxEwv; clerk_active_context=sess_38jUhuq7Hkg8fiqhHMduDWfmycM:; _rdt_uuid=1769242305266.9f8d183d-a987-4e5b-9dee-336b47494719; _rdt_em=:3a02869661fc8f1da6409edc313bf251ebc4586f202b2b84a31646b48d1beca7,3a02869661fc8f1da6409edc313bf251ebc4586f202b2b84a31646b48d1beca7,320e81b4145c1933c25a4ee8275675397a23cb7594b9ac52006f470805bbbe42,3eb2031bc3c935a9a6f57367e9d851317a41280a8ac0693930942a8ee631e447,a03bc4dd2a023bebb17e8f64b22f9803685055600064656f8f85675a4dd3622d; __session=eyJhbGciOiJSUzI1NiIsImNhdCI6ImNsX0I3ZDRQRDExMUFBQSIsImtpZCI6Imluc18yc2NFSEpuWHRhREZVVXhVQ1habldocTVYS0MiLCJ0eXAiOiJKV1QifQ.eyJhenAiOiJodHRwczovL3d3dy5tb3JwaGxsbS5jb20iLCJleHAiOjE3NjkzMjU5MzIsImZ2YSI6WzIyMSwtMV0sImlhdCI6MTc2OTMyNTg3MiwiaXNzIjoiaHR0cHM6Ly9jbGVyay5tb3JwaGxsbS5jb20iLCJuYmYiOjE3NjkzMjU4NjIsInNpZCI6InNlc3NfMzhqVWh1cTdIa2c4ZmlxaEhNZHVEV2ZteWNNIiwic3RzIjoiYWN0aXZlIiwic3ViIjoidXNlcl8zOGpVaHRFRTBTTzNaaTFZaVhFZmI5UjQ3RVoifQ.HhJ-kSkAOlU7xofFZOjHEjH_RbzkEtJwdjxTJ_C09_VgLCOv1dd7OOKtMt5sRXpIl80aF4FrWt8_XaBd4bx63JDzZUC1len0b1CIubu1n6t6vNRts_0hHsaxVo6NTxiElJzZkBZfoZIdnUC5zsEPDldct3wW9C0Jb77Em_uDWlDvs1D-5UF0Hol75v8wj-dnys01IVUqN0svt-QlJ0mKTwbCMeFh4mh1UbE8rMKassgblVJpGfNWWr3pzscuS3yxRbvq9URrr-HoweybWlq57SaJJMxpwopGdnRx2jrKrw_IcJQlQ_Ug_8HxF69i3Upu_rVFIdbIx2hsV0o7LfHzGg; __session_B3JMRlZP=eyJhbGciOiJSUzI1NiIsImNhdCI6ImNsX0I3ZDRQRDExMUFBQSIsImtpZCI6Imluc18yc2NFSEpuWHRhREZVVXhVQ1habldocTVYS0MiLCJ0eXAiOiJKV1QifQ.eyJhenAiOiJodHRwczovL3d3dy5tb3JwaGxsbS5jb20iLCJleHAiOjE3NjkzMjU5MzIsImZ2YSI6WzIyMSwtMV0sImlhdCI6MTc2OTMyNTg3MiwiaXNzIjoiaHR0cHM6Ly9jbGVyay5tb3JwaGxsbS5jb20iLCJuYmYiOjE3NjkzMjU4NjIsInNpZCI6InNlc3NfMzhqVWh1cTdIa2c4ZmlxaEhNZHVEV2ZteWNNIiwic3RzIjoiYWN0aXZlIiwic3ViIjoidXNlcl8zOGpVaHRFRTBTTzNaaTFZaVhFZmI5UjQ3RVoifQ.HhJ-kSkAOlU7xofFZOjHEjH_RbzkEtJwdjxTJ_C09_VgLCOv1dd7OOKtMt5sRXpIl80aF4FrWt8_XaBd4bx63JDzZUC1len0b1CIubu1n6t6vNRts_0hHsaxVo6NTxiElJzZkBZfoZIdnUC5zsEPDldct3wW9C0Jb77Em_uDWlDvs1D-5UF0Hol75v8wj-dnys01IVUqN0svt-QlJ0mKTwbCMeFh4mh1UbE8rMKassgblVJpGfNWWr3pzscuS3yxRbvq9URrr-HoweybWlq57SaJJMxpwopGdnRx2jrKrw_IcJQlQ_Ug_8HxF69i3Upu_rVFIdbIx2hsV0o7LfHzGg; _ga_5ET01XBKB1=GS2.1.s1769324224$o6$g1$t1769325875$j59$l0$h0; ph_phc_i9YGegL9gG85W32ArqnIiBECNTAzUYlFFK8B0Odbhk8_posthog=%7B%22%24device_id%22%3A%22019bef0f-25f6-7e51-b718-8db6665d4470%22%2C%22distinct_id%22%3A%22user_38jUhtEE0SO3Zi1YiXEfb9R47EZ%22%2C%22%24sesid%22%3A%5B1769325875736%2C%22019bf3f1-202a-7fd6-8dbb-6cbea73ffa8f%22%2C1769324224553%5D%2C%22%24epp%22%3Atrue%2C%22%24initial_person_info%22%3A%7B%22r%22%3A%22https%3A%2F%2Fwww.google.com%2F%22%2C%22u%22%3A%22https%3A%2F%2Fwww.morphllm.com%2Fpricing%3Fgad_source%3D1%26gad_campaignid%3D23473818447%26gbraid%3D0AAAAA_wYB2fufbMARVR2fJKKHNzc8Ovu-%26gclid%3DCj0KCQiA-NHLBhDSARIsAIhe9X36gjH12LKRxYZ1bqy4c8wATix3qqxauv9-7H-rewEfnKOW0npi5ZMaAjfHEALw_wcB%22%7D%7D"
6
- LogDir = "./logs"
7
  )
8
 
9
  var MorphHeaders = map[string]string{
@@ -22,12 +26,59 @@ var MorphHeaders = map[string]string{
22
  "sec-fetch-mode": "cors",
23
  "sec-fetch-site": "same-origin",
24
  "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36",
25
- "cookie": MorphCookies,
26
  }
27
 
28
  var DebugMode = true
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  type ParsedToolCall struct {
31
  Name string `json:"name"`
32
  Input map[string]interface{} `json:"input"`
33
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  package types
2
 
3
+ import (
4
+ "errors"
5
+ "strings"
6
+ )
7
+
8
  const (
9
+ MorphAPIURL = "https://www.morphllm.com/api/warpgrep-chat"
10
+ LogDir = "./logs"
 
11
  )
12
 
13
  var MorphHeaders = map[string]string{
 
26
  "sec-fetch-mode": "cors",
27
  "sec-fetch-site": "same-origin",
28
  "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36",
 
29
  }
30
 
31
  var DebugMode = true
32
 
33
+ // CookieRotatorInstance is a global reference to the cookie rotator service
34
+ // It's set in main.go after initialization
35
+ var CookieRotatorInstance interface {
36
+ NextCookie() (cookie interface{}, err error)
37
+ MarkUsed(cookieID uint) error
38
+ MarkError(cookieID uint) error
39
+ }
40
+
41
+ // GetNextCookieFromRotator 从轮询器获取下一个 Cookie
42
+ // 这是一个辅助函数,用于从全局轮询器实例获取 Cookie
43
+ func GetNextCookieFromRotator() (interface{}, error) {
44
+ if CookieRotatorInstance == nil {
45
+ return nil, errors.New("cookie rotator not initialized")
46
+ }
47
+ return CookieRotatorInstance.NextCookie()
48
+ }
49
+
50
  type ParsedToolCall struct {
51
  Name string `json:"name"`
52
  Input map[string]interface{} `json:"input"`
53
  }
54
+
55
+ // ========== 模型配置 ==========
56
+
57
+ // DefaultModel 默认使用的模型
58
+ const DefaultModel = "claude-opus-4-5-20251101"
59
+
60
+ // SupportedModels 支持的模型列表
61
+ var SupportedModels = []string{
62
+ "claude-opus-4-5-20251101",
63
+ }
64
+
65
+ // IsModelSupported 检查模型是否在支持列表中
66
+ func IsModelSupported(model string) bool {
67
+ if model == "" {
68
+ return false
69
+ }
70
+ for _, supported := range SupportedModels {
71
+ if strings.EqualFold(supported, model) {
72
+ return true
73
+ }
74
+ }
75
+ return false
76
+ }
77
+
78
+ // ========== 认证请求/响应类型 ==========
79
+
80
+ // ChangePasswordRequest 修改密码请求
81
+ type ChangePasswordRequest struct {
82
+ OldPassword string `json:"old_password" binding:"required"`
83
+ NewPassword string `json:"new_password" binding:"required,min=6"`
84
+ }
web/static/app.js ADDED
@@ -0,0 +1,484 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // API 基础路径
2
+ const API_BASE = window.location.origin;
3
+
4
+ // 存储认证 token
5
+ let authToken = localStorage.getItem('auth_token');
6
+
7
+ // 页面加载时检查认证状态
8
+ document.addEventListener('DOMContentLoaded', function() {
9
+ const currentPage = window.location.pathname;
10
+ console.log('[DEBUG] Current page:', currentPage);
11
+ console.log('[DEBUG] Auth token:', authToken ? 'exists' : 'none');
12
+
13
+ if (currentPage.includes('dashboard')) {
14
+ console.log('[DEBUG] Loading dashboard...');
15
+ if (!authToken) {
16
+ window.location.href = '/static/index.html';
17
+ return;
18
+ }
19
+ loadDashboard();
20
+ } else {
21
+ // 登录页面(包括 /static/index.html, /, 或其他)
22
+ console.log('[DEBUG] Loading login page...');
23
+ if (authToken) {
24
+ console.log('[DEBUG] Already logged in, redirecting to dashboard...');
25
+ window.location.href = '/dashboard';
26
+ return;
27
+ }
28
+ setupLoginForm();
29
+ }
30
+ });
31
+
32
+ // ========== 认证功能 ==========
33
+
34
+ // 设置登录表单
35
+ function setupLoginForm() {
36
+ console.log('[DEBUG] setupLoginForm called');
37
+ const loginForm = document.getElementById('loginForm');
38
+ console.log('[DEBUG] loginForm element:', loginForm);
39
+ if (loginForm) {
40
+ console.log('[DEBUG] Adding submit event listener to loginForm');
41
+ loginForm.addEventListener('submit', async (e) => {
42
+ e.preventDefault();
43
+ console.log('[DEBUG] Form submitted!');
44
+
45
+ const username = document.getElementById('username').value;
46
+ const password = document.getElementById('password').value;
47
+ const errorDiv = document.getElementById('loginError');
48
+
49
+ console.log('[DEBUG] Attempting login with username:', username);
50
+
51
+ try {
52
+ console.log('[DEBUG] Sending request to:', `${API_BASE}/api/auth/login`);
53
+ const response = await fetch(`${API_BASE}/api/auth/login`, {
54
+ method: 'POST',
55
+ headers: {
56
+ 'Content-Type': 'application/json'
57
+ },
58
+ body: JSON.stringify({ username, password })
59
+ });
60
+
61
+ const data = await response.json();
62
+
63
+ if (response.ok) {
64
+ authToken = data.token;
65
+ localStorage.setItem('auth_token', authToken);
66
+ window.location.href = '/dashboard';
67
+ } else {
68
+ errorDiv.textContent = data.error || '登录失败';
69
+ errorDiv.style.display = 'block';
70
+ }
71
+ } catch (error) {
72
+ errorDiv.textContent = '网络错误,请稍后重试';
73
+ errorDiv.style.display = 'block';
74
+ }
75
+ });
76
+ }
77
+ }
78
+
79
+ // 登出
80
+ function logout() {
81
+ if (confirm('确定要退出吗?')) {
82
+ localStorage.removeItem('auth_token');
83
+ authToken = null;
84
+ window.location.href = '/';
85
+ }
86
+ }
87
+
88
+ // 显示修改密码弹窗
89
+ function showChangePasswordModal() {
90
+ document.getElementById('changePasswordModal').style.display = 'flex';
91
+ document.getElementById('changePasswordForm').reset();
92
+ document.getElementById('changePasswordError').style.display = 'none';
93
+ }
94
+
95
+ // 关闭修改密码弹窗
96
+ function closeChangePasswordModal() {
97
+ document.getElementById('changePasswordModal').style.display = 'none';
98
+ }
99
+
100
+ // 修改密码
101
+ async function changePassword(oldPassword, newPassword) {
102
+ try {
103
+ const response = await apiRequest('/api/auth/password', {
104
+ method: 'PUT',
105
+ body: JSON.stringify({
106
+ old_password: oldPassword,
107
+ new_password: newPassword
108
+ })
109
+ });
110
+
111
+ if (response.ok) {
112
+ showToast('密码修改成功', 'success');
113
+ closeChangePasswordModal();
114
+ return true;
115
+ } else {
116
+ const data = await response.json();
117
+ return { error: data.error || '密码修改失败' };
118
+ }
119
+ } catch (error) {
120
+ console.error('修改密码错误:', error);
121
+ return { error: '网络错误,请重试' };
122
+ }
123
+ }
124
+
125
+ // 设置修改密码表单
126
+ function setupChangePasswordForm() {
127
+ const form = document.getElementById('changePasswordForm');
128
+ if (form) {
129
+ form.addEventListener('submit', async (e) => {
130
+ e.preventDefault();
131
+
132
+ const oldPassword = document.getElementById('oldPassword').value;
133
+ const newPassword = document.getElementById('newPassword').value;
134
+ const confirmPassword = document.getElementById('confirmPassword').value;
135
+ const errorDiv = document.getElementById('changePasswordError');
136
+
137
+ // 验证新密码长度
138
+ if (newPassword.length < 6) {
139
+ errorDiv.textContent = '新密码至少需要6位字符';
140
+ errorDiv.style.display = 'block';
141
+ return;
142
+ }
143
+
144
+ // 验证两次密码是否一致
145
+ if (newPassword !== confirmPassword) {
146
+ errorDiv.textContent = '两次输入的密码不一致';
147
+ errorDiv.style.display = 'block';
148
+ return;
149
+ }
150
+
151
+ const result = await changePassword(oldPassword, newPassword);
152
+ if (result === true) {
153
+ // 成功
154
+ } else if (result && result.error) {
155
+ errorDiv.textContent = result.error;
156
+ errorDiv.style.display = 'block';
157
+ }
158
+ });
159
+ }
160
+ }
161
+
162
+ // API 请求封装
163
+ async function apiRequest(url, options = {}) {
164
+ const headers = {
165
+ 'Content-Type': 'application/json',
166
+ ...options.headers
167
+ };
168
+
169
+ if (authToken) {
170
+ headers['Authorization'] = `Bearer ${authToken}`;
171
+ }
172
+
173
+ const response = await fetch(`${API_BASE}${url}`, {
174
+ ...options,
175
+ headers
176
+ });
177
+
178
+ // 如果 401,跳转到登录页
179
+ if (response.status === 401) {
180
+ localStorage.removeItem('auth_token');
181
+ authToken = null;
182
+ window.location.href = '/';
183
+ return;
184
+ }
185
+
186
+ return response;
187
+ }
188
+
189
+ // ========== Dashboard 功能 ==========
190
+
191
+ // 加载 Dashboard
192
+ async function loadDashboard() {
193
+ setupChangePasswordForm();
194
+ await loadUserInfo();
195
+ await loadStats();
196
+ await loadCookies();
197
+ }
198
+
199
+ // 加载用户信息
200
+ async function loadUserInfo() {
201
+ try {
202
+ const response = await apiRequest('/api/auth/me');
203
+ if (response.ok) {
204
+ const user = await response.json();
205
+ document.getElementById('currentUser').textContent = user.username;
206
+ }
207
+ } catch (error) {
208
+ console.error('加载用户信息失败:', error);
209
+ }
210
+ }
211
+
212
+ // 加载统计信息
213
+ async function loadStats() {
214
+ try {
215
+ const response = await apiRequest('/api/cookies/stats');
216
+ if (response.ok) {
217
+ const stats = await response.json();
218
+ document.getElementById('totalCount').textContent = stats.total_count;
219
+ document.getElementById('validCount').textContent = stats.valid_count;
220
+ document.getElementById('invalidCount').textContent = stats.invalid_count;
221
+ document.getElementById('totalUsage').textContent = stats.total_usage.toLocaleString();
222
+ }
223
+ } catch (error) {
224
+ console.error('加载统计信息失败:', error);
225
+ }
226
+ }
227
+
228
+ // 加载 Cookie 列表
229
+ async function loadCookies() {
230
+ try {
231
+ const response = await apiRequest('/api/cookies');
232
+ if (response.ok) {
233
+ const data = await response.json();
234
+ // API 返回的是数组而不是对象
235
+ renderCookieTable(Array.isArray(data) ? data : (data.cookies || []));
236
+ }
237
+ } catch (error) {
238
+ console.error('加载 Cookie 列表失败:', error);
239
+ }
240
+ }
241
+
242
+ // 渲染 Cookie 表格
243
+ function renderCookieTable(cookies) {
244
+ const tbody = document.getElementById('cookieTableBody');
245
+ const emptyState = document.getElementById('emptyState');
246
+
247
+ if (cookies.length === 0) {
248
+ tbody.innerHTML = '';
249
+ emptyState.style.display = 'block';
250
+ return;
251
+ }
252
+
253
+ emptyState.style.display = 'none';
254
+
255
+ tbody.innerHTML = cookies.map((cookie, index) => `
256
+ <tr>
257
+ <td>${index + 1}</td>
258
+ <td>${escapeHtml(cookie.name)}</td>
259
+ <td>
260
+ <span class="status-badge ${cookie.is_valid ? 'status-valid' : 'status-invalid'}">
261
+ ${cookie.is_valid ? '✅ 有效' : '❌ 无效'}
262
+ </span>
263
+ </td>
264
+ <td>${(cookie.usage_count || 0).toLocaleString()}</td>
265
+ <td>${cookie.priority || 0}</td>
266
+ <td>${formatTime(cookie.last_validated)}</td>
267
+ <td>
268
+ <div class="action-buttons">
269
+ <button class="btn btn-secondary btn-sm" onclick="validateCookie(${cookie.id})">🔄</button>
270
+ <button class="btn btn-secondary btn-sm" onclick="editCookie(${cookie.id})">⚙️</button>
271
+ <button class="btn btn-danger btn-sm" onclick="deleteCookie(${cookie.id})">🗑️</button>
272
+ </div>
273
+ </td>
274
+ </tr>
275
+ `).join('');
276
+ }
277
+
278
+ // 刷新 Cookie 列表
279
+ function refreshCookies() {
280
+ loadCookies();
281
+ loadStats();
282
+ }
283
+
284
+ // ========== Cookie 操作 ==========
285
+
286
+ // 显示添加弹窗
287
+ function showAddModal() {
288
+ document.getElementById('addModal').style.display = 'flex';
289
+ document.getElementById('addCookieForm').reset();
290
+ }
291
+
292
+ // 关闭添加弹窗
293
+ function closeAddModal() {
294
+ document.getElementById('addModal').style.display = 'none';
295
+ }
296
+
297
+ // 添加 Cookie
298
+ document.getElementById('addCookieForm')?.addEventListener('submit', async (e) => {
299
+ e.preventDefault();
300
+
301
+ const formData = new FormData(e.target);
302
+ const data = {
303
+ name: formData.get('name'),
304
+ api_key: formData.get('api_key'),
305
+ session_key: formData.get('session_key') || '',
306
+ priority: parseInt(formData.get('priority') || '0')
307
+ };
308
+
309
+ try {
310
+ const response = await apiRequest('/api/cookies', {
311
+ method: 'POST',
312
+ body: JSON.stringify(data)
313
+ });
314
+
315
+ if (response.ok) {
316
+ showToast('Cookie 添加成功', 'success');
317
+ closeAddModal();
318
+ refreshCookies();
319
+ } else {
320
+ const error = await response.json();
321
+ showToast(error.error || '添加失败', 'error');
322
+ }
323
+ } catch (error) {
324
+ showToast('网络错误', 'error');
325
+ }
326
+ });
327
+
328
+ // 显示编辑弹窗
329
+ async function editCookie(id) {
330
+ try {
331
+ const response = await apiRequest(`/api/cookies/${id}`);
332
+ if (response.ok) {
333
+ const cookie = await response.json();
334
+ document.getElementById('editCookieId').value = cookie.id;
335
+ document.getElementById('editCookieName').value = cookie.name;
336
+ document.getElementById('editApiKey').value = cookie.api_key || '';
337
+ document.getElementById('editSessionKey').value = cookie.session_key || '';
338
+ document.getElementById('editCookiePriority').value = cookie.priority || 0;
339
+ document.getElementById('editModal').style.display = 'flex';
340
+ }
341
+ } catch (error) {
342
+ showToast('加载失败', 'error');
343
+ }
344
+ }
345
+
346
+ // 关闭编辑弹窗
347
+ function closeEditModal() {
348
+ document.getElementById('editModal').style.display = 'none';
349
+ }
350
+
351
+ // 更新 Cookie
352
+ document.getElementById('editCookieForm')?.addEventListener('submit', async (e) => {
353
+ e.preventDefault();
354
+
355
+ const id = document.getElementById('editCookieId').value;
356
+ const formData = new FormData(e.target);
357
+ const data = {
358
+ name: formData.get('name'),
359
+ api_key: formData.get('api_key'),
360
+ session_key: formData.get('session_key') || '',
361
+ priority: parseInt(formData.get('priority') || '0')
362
+ };
363
+
364
+ try {
365
+ const response = await apiRequest(`/api/cookies/${id}`, {
366
+ method: 'PUT',
367
+ body: JSON.stringify(data)
368
+ });
369
+
370
+ if (response.ok) {
371
+ showToast('Cookie 更新成功', 'success');
372
+ closeEditModal();
373
+ refreshCookies();
374
+ } else {
375
+ const error = await response.json();
376
+ showToast(error.error || '更新失败', 'error');
377
+ }
378
+ } catch (error) {
379
+ showToast('网络错误', 'error');
380
+ }
381
+ });
382
+
383
+ // 删除 Cookie
384
+ async function deleteCookie(id) {
385
+ if (!confirm('确定要删除这个 Cookie 吗?')) {
386
+ return;
387
+ }
388
+
389
+ try {
390
+ const response = await apiRequest(`/api/cookies/${id}`, {
391
+ method: 'DELETE'
392
+ });
393
+
394
+ if (response.ok) {
395
+ showToast('Cookie 删除成功', 'success');
396
+ refreshCookies();
397
+ } else {
398
+ const error = await response.json();
399
+ showToast(error.error || '删除失败', 'error');
400
+ }
401
+ } catch (error) {
402
+ showToast('网络错误', 'error');
403
+ }
404
+ }
405
+
406
+ // 验证单个 Cookie
407
+ async function validateCookie(id) {
408
+ try {
409
+ showToast('正在验证...', 'success');
410
+ const response = await apiRequest(`/api/cookies/${id}/validate`, {
411
+ method: 'POST'
412
+ });
413
+
414
+ if (response.ok) {
415
+ const result = await response.json();
416
+ showToast(result.is_valid ? '✅ Cookie 有效' : '❌ Cookie 无效', result.is_valid ? 'success' : 'error');
417
+ refreshCookies();
418
+ } else {
419
+ const error = await response.json();
420
+ showToast(error.error || '验证失败', 'error');
421
+ }
422
+ } catch (error) {
423
+ showToast('网络错误', 'error');
424
+ }
425
+ }
426
+
427
+ // 批量验证所有 Cookie
428
+ async function validateAll() {
429
+ if (!confirm('确定要验证所有 Cookie 吗?这可能需要一些时间。')) {
430
+ return;
431
+ }
432
+
433
+ try {
434
+ showToast('正在批量验证...', 'success');
435
+ const response = await apiRequest('/api/cookies/validate/all', {
436
+ method: 'POST'
437
+ });
438
+
439
+ if (response.ok) {
440
+ const result = await response.json();
441
+ showToast(`验证完成:${result.valid_count} 个有效,${result.invalid_count} 个无效`, 'success');
442
+ refreshCookies();
443
+ } else {
444
+ const error = await response.json();
445
+ showToast(error.error || '验证失败', 'error');
446
+ }
447
+ } catch (error) {
448
+ showToast('网络错误', 'error');
449
+ }
450
+ }
451
+
452
+ // ========== 工具函数 ==========
453
+
454
+ // 显示 Toast 通知
455
+ function showToast(message, type = 'success') {
456
+ const toast = document.getElementById('toast');
457
+ toast.textContent = message;
458
+ toast.className = `toast ${type}`;
459
+ toast.style.display = 'block';
460
+
461
+ setTimeout(() => {
462
+ toast.style.display = 'none';
463
+ }, 3000);
464
+ }
465
+
466
+ // 格式化时间
467
+ function formatTime(timeStr) {
468
+ if (!timeStr) return '-';
469
+ const date = new Date(timeStr);
470
+ const now = new Date();
471
+ const diff = Math.floor((now - date) / 1000);
472
+
473
+ if (diff < 60) return '刚刚';
474
+ if (diff < 3600) return `${Math.floor(diff / 60)} 分钟前`;
475
+ if (diff < 86400) return `${Math.floor(diff / 3600)} 小时前`;
476
+ return `${Math.floor(diff / 86400)} 天前`;
477
+ }
478
+
479
+ // HTML 转义
480
+ function escapeHtml(text) {
481
+ const div = document.createElement('div');
482
+ div.textContent = text;
483
+ return div.innerHTML;
484
+ }
web/static/dashboard.html ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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">
6
+ <title>Opus API 管理系统 - Cookie 管理</title>
7
+ <link rel="stylesheet" href="/static/styles.css">
8
+ </head>
9
+ <body>
10
+ <div class="dashboard-container">
11
+ <!-- 顶部导航 -->
12
+ <header class="header">
13
+ <h1>🚀 Opus API 管理系统</h1>
14
+ <div class="user-menu">
15
+ <span id="currentUser">admin</span>
16
+ <button class="btn btn-secondary" onclick="showChangePasswordModal()">修改密码</button>
17
+ <button class="btn btn-secondary" onclick="logout()">退出</button>
18
+ </div>
19
+ </header>
20
+
21
+ <!-- 主内容区 -->
22
+ <main class="main-content">
23
+ <!-- 统计卡片 -->
24
+ <section class="stats-section">
25
+ <div class="stats-grid">
26
+ <div class="stat-card">
27
+ <div class="stat-icon">📊</div>
28
+ <div class="stat-info">
29
+ <h3 id="totalCount">0</h3>
30
+ <p>总账号数</p>
31
+ </div>
32
+ </div>
33
+ <div class="stat-card valid">
34
+ <div class="stat-icon">✅</div>
35
+ <div class="stat-info">
36
+ <h3 id="validCount">0</h3>
37
+ <p>有效账号</p>
38
+ </div>
39
+ </div>
40
+ <div class="stat-card invalid">
41
+ <div class="stat-icon">❌</div>
42
+ <div class="stat-info">
43
+ <h3 id="invalidCount">0</h3>
44
+ <p>无效账号</p>
45
+ </div>
46
+ </div>
47
+ <div class="stat-card">
48
+ <div class="stat-icon">📈</div>
49
+ <div class="stat-info">
50
+ <h3 id="totalUsage">0</h3>
51
+ <p>总请求数</p>
52
+ </div>
53
+ </div>
54
+ </div>
55
+ </section>
56
+
57
+ <!-- 操作按钮 -->
58
+ <section class="actions-section">
59
+ <button class="btn btn-primary" onclick="showAddModal()">
60
+ ➕ 添加 Cookie
61
+ </button>
62
+ <button class="btn btn-secondary" onclick="validateAll()">
63
+ 🔄 批量验证
64
+ </button>
65
+ <button class="btn btn-secondary" onclick="refreshCookies()">
66
+ 🔃 刷新列表
67
+ </button>
68
+ </section>
69
+
70
+ <!-- Cookie 列表 -->
71
+ <section class="table-section">
72
+ <h2>Cookie 列表</h2>
73
+ <div class="table-container">
74
+ <table id="cookieTable">
75
+ <thead>
76
+ <tr>
77
+ <th>#</th>
78
+ <th>名称</th>
79
+ <th>状态</th>
80
+ <th>使用次数</th>
81
+ <th>优先级</th>
82
+ <th>最后验证</th>
83
+ <th>操作</th>
84
+ </tr>
85
+ </thead>
86
+ <tbody id="cookieTableBody">
87
+ <!-- 动态填充 -->
88
+ </tbody>
89
+ </table>
90
+ <div id="emptyState" class="empty-state" style="display: none;">
91
+ <p>暂无 Cookie,点击上方按钮添加</p>
92
+ </div>
93
+ </div>
94
+ </section>
95
+ </main>
96
+
97
+ <!-- 添加 Cookie 弹窗 -->
98
+ <div id="addModal" class="modal" style="display: none;">
99
+ <div class="modal-content">
100
+ <div class="modal-header">
101
+ <h2>添加 Morph Cookie</h2>
102
+ <button class="modal-close" onclick="closeAddModal()">&times;</button>
103
+ </div>
104
+ <form id="addCookieForm">
105
+ <div class="form-group">
106
+ <label for="cookieName">账号名称</label>
107
+ <input type="text" id="cookieName" name="name" placeholder="例如:主账号" required>
108
+ </div>
109
+ <div class="form-group">
110
+ <label for="apiKey">API Key</label>
111
+ <input type="text" id="apiKey" name="api_key" placeholder="输入 API Key..." required>
112
+ </div>
113
+ <div class="form-group">
114
+ <label for="sessionKey">Session Key (可选)</label>
115
+ <input type="text" id="sessionKey" name="session_key" placeholder="输入 Session Key...">
116
+ </div>
117
+ <div class="form-group">
118
+ <label for="cookiePriority">优先级 (0-100)</label>
119
+ <input type="number" id="cookiePriority" name="priority" value="0" min="0" max="100">
120
+ </div>
121
+ <div class="modal-footer">
122
+ <button type="button" class="btn btn-secondary" onclick="closeAddModal()">取消</button>
123
+ <button type="submit" class="btn btn-primary">确定</button>
124
+ </div>
125
+ </form>
126
+ </div>
127
+ </div>
128
+
129
+ <!-- 编辑 Cookie 弹窗 -->
130
+ <div id="editModal" class="modal" style="display: none;">
131
+ <div class="modal-content">
132
+ <div class="modal-header">
133
+ <h2>编辑 Cookie</h2>
134
+ <button class="modal-close" onclick="closeEditModal()">&times;</button>
135
+ </div>
136
+ <form id="editCookieForm">
137
+ <input type="hidden" id="editCookieId">
138
+ <div class="form-group">
139
+ <label for="editCookieName">账号名称</label>
140
+ <input type="text" id="editCookieName" name="name" placeholder="例如:主账号">
141
+ </div>
142
+ <div class="form-group">
143
+ <label for="editApiKey">API Key</label>
144
+ <input type="text" id="editApiKey" name="api_key" placeholder="输入 API Key...">
145
+ </div>
146
+ <div class="form-group">
147
+ <label for="editSessionKey">Session Key (可选)</label>
148
+ <input type="text" id="editSessionKey" name="session_key" placeholder="输入 Session Key...">
149
+ </div>
150
+ <div class="form-group">
151
+ <label for="editCookiePriority">优先级 (0-100)</label>
152
+ <input type="number" id="editCookiePriority" name="priority" min="0" max="100">
153
+ </div>
154
+ <div class="modal-footer">
155
+ <button type="button" class="btn btn-secondary" onclick="closeEditModal()">取消</button>
156
+ <button type="submit" class="btn btn-primary">保存</button>
157
+ </div>
158
+ </form>
159
+ </div>
160
+ </div>
161
+
162
+ <!-- 修改密码弹窗 -->
163
+ <div id="changePasswordModal" class="modal" style="display: none;">
164
+ <div class="modal-content">
165
+ <div class="modal-header">
166
+ <h2>修改密码</h2>
167
+ <button class="modal-close" onclick="closeChangePasswordModal()">&times;</button>
168
+ </div>
169
+ <form id="changePasswordForm">
170
+ <div class="form-group">
171
+ <label for="oldPassword">原密码</label>
172
+ <input type="password" id="oldPassword" name="old_password" placeholder="请输入原密码" required>
173
+ </div>
174
+ <div class="form-group">
175
+ <label for="newPassword">新密码</label>
176
+ <input type="password" id="newPassword" name="new_password" placeholder="请输入新密码 (至少6位)" required minlength="6">
177
+ </div>
178
+ <div class="form-group">
179
+ <label for="confirmPassword">确认新密码</label>
180
+ <input type="password" id="confirmPassword" name="confirm_password" placeholder="请再次输入新密码" required>
181
+ </div>
182
+ <div class="error" id="changePasswordError" style="display: none;"></div>
183
+ <div class="modal-footer">
184
+ <button type="button" class="btn btn-secondary" onclick="closeChangePasswordModal()">取消</button>
185
+ <button type="submit" class="btn btn-primary">确定</button>
186
+ </div>
187
+ </form>
188
+ </div>
189
+ </div>
190
+
191
+ <!-- Toast 通知 -->
192
+ <div id="toast" class="toast" style="display: none;"></div>
193
+ </div>
194
+ <script src="/static/app.js"></script>
195
+ </body>
196
+ </html>
web/static/index.html ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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">
6
+ <title>Opus API 管理系统 - 登录</title>
7
+ <link rel="stylesheet" href="/static/styles.css">
8
+ </head>
9
+ <body>
10
+ <div class="login-container">
11
+ <div class="login-box">
12
+ <h1>🚀 Opus API 管理系统</h1>
13
+ <form id="loginForm">
14
+ <div class="form-group">
15
+ <input type="text" id="username" name="username" placeholder="用户名" required autocomplete="username">
16
+ </div>
17
+ <div class="form-group">
18
+ <input type="password" id="password" name="password" placeholder="密码" required autocomplete="current-password">
19
+ </div>
20
+ <button type="submit" class="btn btn-primary btn-block">登录</button>
21
+ </form>
22
+ <div class="error" id="loginError"></div>
23
+ </div>
24
+ </div>
25
+ <script src="/static/app.js"></script>
26
+ </body>
27
+ </html>
web/static/styles.css ADDED
@@ -0,0 +1,447 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* 全局样式 */
2
+ * {
3
+ margin: 0;
4
+ padding: 0;
5
+ box-sizing: border-box;
6
+ }
7
+
8
+ body {
9
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
10
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
11
+ min-height: 100vh;
12
+ color: #333;
13
+ }
14
+
15
+ /* 登录页面 */
16
+ .login-container {
17
+ display: flex;
18
+ justify-content: center;
19
+ align-items: center;
20
+ min-height: 100vh;
21
+ padding: 20px;
22
+ }
23
+
24
+ .login-box {
25
+ background: white;
26
+ border-radius: 12px;
27
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
28
+ padding: 40px;
29
+ width: 100%;
30
+ max-width: 400px;
31
+ }
32
+
33
+ .login-box h1 {
34
+ text-align: center;
35
+ margin-bottom: 30px;
36
+ color: #667eea;
37
+ font-size: 28px;
38
+ }
39
+
40
+ /* 表单样式 */
41
+ .form-group {
42
+ margin-bottom: 20px;
43
+ }
44
+
45
+ .form-group label {
46
+ display: block;
47
+ margin-bottom: 8px;
48
+ font-weight: 500;
49
+ color: #555;
50
+ }
51
+
52
+ .form-group input,
53
+ .form-group textarea,
54
+ .form-group select {
55
+ width: 100%;
56
+ padding: 12px 16px;
57
+ border: 2px solid #e0e0e0;
58
+ border-radius: 8px;
59
+ font-size: 14px;
60
+ transition: border-color 0.3s;
61
+ }
62
+
63
+ .form-group input:focus,
64
+ .form-group textarea:focus,
65
+ .form-group select:focus {
66
+ outline: none;
67
+ border-color: #667eea;
68
+ }
69
+
70
+ .form-group.checkbox {
71
+ display: flex;
72
+ align-items: center;
73
+ }
74
+
75
+ .form-group.checkbox label {
76
+ margin: 0;
77
+ margin-left: 8px;
78
+ font-weight: normal;
79
+ }
80
+
81
+ .form-group.checkbox input {
82
+ width: auto;
83
+ }
84
+
85
+ /* 按钮样式 */
86
+ .btn {
87
+ padding: 12px 24px;
88
+ border: none;
89
+ border-radius: 8px;
90
+ font-size: 14px;
91
+ font-weight: 600;
92
+ cursor: pointer;
93
+ transition: all 0.3s;
94
+ text-decoration: none;
95
+ display: inline-block;
96
+ }
97
+
98
+ .btn-primary {
99
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
100
+ color: white;
101
+ }
102
+
103
+ .btn-primary:hover {
104
+ transform: translateY(-2px);
105
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
106
+ }
107
+
108
+ .btn-secondary {
109
+ background: #f5f5f5;
110
+ color: #333;
111
+ }
112
+
113
+ .btn-secondary:hover {
114
+ background: #e0e0e0;
115
+ }
116
+
117
+ .btn-danger {
118
+ background: #ff4757;
119
+ color: white;
120
+ }
121
+
122
+ .btn-danger:hover {
123
+ background: #ee5a6f;
124
+ }
125
+
126
+ .btn-block {
127
+ width: 100%;
128
+ }
129
+
130
+ .btn:disabled {
131
+ opacity: 0.6;
132
+ cursor: not-allowed;
133
+ }
134
+
135
+ /* 错误提示 */
136
+ .error {
137
+ color: #ff4757;
138
+ font-size: 14px;
139
+ margin-top: 10px;
140
+ text-align: center;
141
+ }
142
+
143
+ /* Dashboard 样式 */
144
+ .dashboard-container {
145
+ min-height: 100vh;
146
+ background: #f5f7fa;
147
+ }
148
+
149
+ .header {
150
+ background: white;
151
+ padding: 20px 40px;
152
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
153
+ display: flex;
154
+ justify-content: space-between;
155
+ align-items: center;
156
+ }
157
+
158
+ .header h1 {
159
+ color: #667eea;
160
+ font-size: 24px;
161
+ }
162
+
163
+ .user-menu {
164
+ display: flex;
165
+ align-items: center;
166
+ gap: 15px;
167
+ }
168
+
169
+ .user-menu span {
170
+ font-weight: 500;
171
+ color: #555;
172
+ }
173
+
174
+ .main-content {
175
+ max-width: 1200px;
176
+ margin: 0 auto;
177
+ padding: 40px 20px;
178
+ }
179
+
180
+ /* 统计卡片 */
181
+ .stats-section {
182
+ margin-bottom: 30px;
183
+ }
184
+
185
+ .stats-grid {
186
+ display: grid;
187
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
188
+ gap: 20px;
189
+ }
190
+
191
+ .stat-card {
192
+ background: white;
193
+ border-radius: 12px;
194
+ padding: 24px;
195
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
196
+ display: flex;
197
+ align-items: center;
198
+ gap: 20px;
199
+ transition: transform 0.3s;
200
+ }
201
+
202
+ .stat-card:hover {
203
+ transform: translateY(-4px);
204
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
205
+ }
206
+
207
+ .stat-card.valid {
208
+ border-left: 4px solid #2ecc71;
209
+ }
210
+
211
+ .stat-card.invalid {
212
+ border-left: 4px solid #ff4757;
213
+ }
214
+
215
+ .stat-icon {
216
+ font-size: 40px;
217
+ }
218
+
219
+ .stat-info h3 {
220
+ font-size: 32px;
221
+ color: #333;
222
+ margin-bottom: 4px;
223
+ }
224
+
225
+ .stat-info p {
226
+ color: #888;
227
+ font-size: 14px;
228
+ }
229
+
230
+ /* 操作按钮区 */
231
+ .actions-section {
232
+ margin-bottom: 30px;
233
+ display: flex;
234
+ gap: 15px;
235
+ flex-wrap: wrap;
236
+ }
237
+
238
+ /* 表格样式 */
239
+ .table-section {
240
+ background: white;
241
+ border-radius: 12px;
242
+ padding: 24px;
243
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
244
+ }
245
+
246
+ .table-section h2 {
247
+ margin-bottom: 20px;
248
+ color: #333;
249
+ }
250
+
251
+ .table-container {
252
+ overflow-x: auto;
253
+ }
254
+
255
+ table {
256
+ width: 100%;
257
+ border-collapse: collapse;
258
+ }
259
+
260
+ thead {
261
+ background: #f8f9fa;
262
+ }
263
+
264
+ th {
265
+ padding: 12px;
266
+ text-align: left;
267
+ font-weight: 600;
268
+ color: #555;
269
+ border-bottom: 2px solid #e0e0e0;
270
+ }
271
+
272
+ td {
273
+ padding: 12px;
274
+ border-bottom: 1px solid #f0f0f0;
275
+ }
276
+
277
+ tr:hover {
278
+ background: #f8f9fa;
279
+ }
280
+
281
+ .status-badge {
282
+ display: inline-block;
283
+ padding: 4px 12px;
284
+ border-radius: 12px;
285
+ font-size: 12px;
286
+ font-weight: 600;
287
+ }
288
+
289
+ .status-valid {
290
+ background: #d4edda;
291
+ color: #155724;
292
+ }
293
+
294
+ .status-invalid {
295
+ background: #f8d7da;
296
+ color: #721c24;
297
+ }
298
+
299
+ .action-buttons {
300
+ display: flex;
301
+ gap: 8px;
302
+ }
303
+
304
+ .btn-sm {
305
+ padding: 6px 12px;
306
+ font-size: 12px;
307
+ }
308
+
309
+ .empty-state {
310
+ text-align: center;
311
+ padding: 60px 20px;
312
+ color: #999;
313
+ }
314
+
315
+ /* 模态框 */
316
+ .modal {
317
+ position: fixed;
318
+ top: 0;
319
+ left: 0;
320
+ width: 100%;
321
+ height: 100%;
322
+ background: rgba(0, 0, 0, 0.5);
323
+ display: flex;
324
+ justify-content: center;
325
+ align-items: center;
326
+ z-index: 1000;
327
+ }
328
+
329
+ .modal-content {
330
+ background: white;
331
+ border-radius: 12px;
332
+ width: 90%;
333
+ max-width: 600px;
334
+ max-height: 90vh;
335
+ overflow-y: auto;
336
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
337
+ }
338
+
339
+ .modal-header {
340
+ padding: 24px;
341
+ border-bottom: 1px solid #e0e0e0;
342
+ display: flex;
343
+ justify-content: space-between;
344
+ align-items: center;
345
+ }
346
+
347
+ .modal-header h2 {
348
+ color: #333;
349
+ font-size: 20px;
350
+ }
351
+
352
+ .modal-close {
353
+ background: none;
354
+ border: none;
355
+ font-size: 28px;
356
+ color: #999;
357
+ cursor: pointer;
358
+ padding: 0;
359
+ width: 32px;
360
+ height: 32px;
361
+ display: flex;
362
+ align-items: center;
363
+ justify-content: center;
364
+ }
365
+
366
+ .modal-close:hover {
367
+ color: #333;
368
+ }
369
+
370
+ .modal-content form {
371
+ padding: 24px;
372
+ }
373
+
374
+ .modal-footer {
375
+ display: flex;
376
+ justify-content: flex-end;
377
+ gap: 12px;
378
+ margin-top: 24px;
379
+ }
380
+
381
+ /* Toast 通知 */
382
+ .toast {
383
+ position: fixed;
384
+ bottom: 30px;
385
+ right: 30px;
386
+ background: #333;
387
+ color: white;
388
+ padding: 16px 24px;
389
+ border-radius: 8px;
390
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
391
+ z-index: 2000;
392
+ animation: slideIn 0.3s ease-out;
393
+ }
394
+
395
+ .toast.success {
396
+ background: #2ecc71;
397
+ }
398
+
399
+ .toast.error {
400
+ background: #ff4757;
401
+ }
402
+
403
+ @keyframes slideIn {
404
+ from {
405
+ transform: translateX(400px);
406
+ opacity: 0;
407
+ }
408
+ to {
409
+ transform: translateX(0);
410
+ opacity: 1;
411
+ }
412
+ }
413
+
414
+ /* 响应式设计 */
415
+ @media (max-width: 768px) {
416
+ .header {
417
+ padding: 15px 20px;
418
+ }
419
+
420
+ .header h1 {
421
+ font-size: 20px;
422
+ }
423
+
424
+ .main-content {
425
+ padding: 20px 15px;
426
+ }
427
+
428
+ .stats-grid {
429
+ grid-template-columns: 1fr;
430
+ }
431
+
432
+ .actions-section {
433
+ flex-direction: column;
434
+ }
435
+
436
+ .actions-section .btn {
437
+ width: 100%;
438
+ }
439
+
440
+ table {
441
+ font-size: 14px;
442
+ }
443
+
444
+ th, td {
445
+ padding: 8px;
446
+ }
447
+ }