huanbao commited on
Commit
6e70660
·
verified ·
1 Parent(s): 0ca2e05

Upload 12 files

Browse files
Files changed (12) hide show
  1. Dockerfile +45 -1
  2. api/auth.go +43 -0
  3. api/handler.go +511 -0
  4. api/token_handler.go +183 -0
  5. config/config.go +50 -0
  6. config/redis.go +94 -0
  7. go.mod +44 -0
  8. go.sum +116 -0
  9. main.go +212 -0
  10. middleware/cors.go +15 -0
  11. pkg/logger/logger.go +61 -0
  12. templates/admin.html +660 -0
Dockerfile CHANGED
@@ -1 +1,45 @@
1
- FROM linqiu1199/augment2api:latest
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 使用官方 Golang 镜像作为构建环境
2
+ FROM golang:1.23-alpine AS builder
3
+
4
+ # 设置工作目录
5
+ WORKDIR /app
6
+
7
+ # 复制 go.mod 和 go.sum 文件
8
+ COPY go.mod go.sum ./
9
+
10
+ # 下载依赖
11
+ RUN go mod download
12
+
13
+ # 复制源代码
14
+ COPY . .
15
+
16
+ # 构建应用
17
+ RUN CGO_ENABLED=0 GOOS=linux go build -o augment2api
18
+
19
+ # 使用轻量级的 alpine 镜像
20
+ FROM alpine:latest
21
+
22
+ # 安装 ca-certificates 以支持 HTTPS
23
+ RUN apk --no-cache add ca-certificates tzdata
24
+
25
+ # 创建非 root 用户
26
+ RUN adduser -D -g '' appuser
27
+
28
+ # 从构建阶段复制二进制文件
29
+ COPY --from=builder /app/augment2api /app/augment2api
30
+
31
+ # 复制静态文件和模板
32
+ COPY --from=builder /app/templates /app/templates
33
+
34
+ # 设置工作目录
35
+ WORKDIR /app
36
+
37
+ # 使用非 root 用户运行
38
+ USER appuser
39
+
40
+ # 暴露端口
41
+ EXPOSE 27080
42
+
43
+
44
+ # 运行应用
45
+ CMD ["/app/augment2api"]
api/auth.go ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package api
2
+
3
+ import (
4
+ "augment2api/config"
5
+ "augment2api/pkg/logger"
6
+ "fmt"
7
+ "net/http"
8
+ "strings"
9
+
10
+ "github.com/gin-gonic/gin"
11
+ )
12
+
13
+ // AuthMiddleware 验证请求的Authorization header
14
+ func AuthMiddleware() gin.HandlerFunc {
15
+ return func(c *gin.Context) {
16
+ // 如果未设置 AuthToken,则不启用鉴权
17
+ if config.AppConfig.AuthToken == "" {
18
+ c.Next()
19
+ return
20
+ }
21
+
22
+ authHeader := c.GetHeader("Authorization")
23
+ if authHeader == "" {
24
+ logger.Log.Error("Authorization is empty")
25
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"})
26
+ c.Abort()
27
+ return
28
+ }
29
+
30
+ // 支持 "Bearer <token>" 格式
31
+ token := strings.TrimPrefix(authHeader, "Bearer ")
32
+ token = strings.TrimSpace(token)
33
+
34
+ if token != config.AppConfig.AuthToken {
35
+ logger.Log.Error(fmt.Sprintf("Invalid authorization token:%s", token))
36
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorization token"})
37
+ c.Abort()
38
+ return
39
+ }
40
+
41
+ c.Next()
42
+ }
43
+ }
api/handler.go ADDED
@@ -0,0 +1,511 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package api
2
+
3
+ import (
4
+ "augment2api/config"
5
+ "bufio"
6
+ "encoding/json"
7
+ "fmt"
8
+ "io"
9
+ "log"
10
+ "net/http"
11
+ "strings"
12
+ "time"
13
+
14
+ "github.com/gin-gonic/gin"
15
+ )
16
+
17
+ // OpenAIRequest OpenAI兼容的请求结构
18
+ type OpenAIRequest struct {
19
+ Model string `json:"model,omitempty"`
20
+ Messages []ChatMessage `json:"messages,omitempty"`
21
+ Stream bool `json:"stream,omitempty"`
22
+ Temperature float64 `json:"temperature,omitempty"`
23
+ MaxTokens int `json:"max_tokens,omitempty"`
24
+ }
25
+
26
+ // OpenAIResponse OpenAI兼容的响应结构
27
+ type OpenAIResponse struct {
28
+ ID string `json:"id"`
29
+ Object string `json:"object"`
30
+ Created int64 `json:"created"`
31
+ Model string `json:"model"`
32
+ Choices []Choice `json:"choices"`
33
+ Usage Usage `json:"usage"`
34
+ }
35
+
36
+ // OpenAIStreamResponse OpenAI兼容的流式响应结构
37
+ type OpenAIStreamResponse struct {
38
+ ID string `json:"id"`
39
+ Object string `json:"object"`
40
+ Created int64 `json:"created"`
41
+ Model string `json:"model"`
42
+ Choices []StreamChoice `json:"choices"`
43
+ }
44
+
45
+ type StreamChoice struct {
46
+ Index int `json:"index"`
47
+ Delta ChatMessage `json:"delta"`
48
+ FinishReason *string `json:"finish_reason"`
49
+ }
50
+
51
+ type Choice struct {
52
+ Index int `json:"index"`
53
+ Message ChatMessage `json:"message"`
54
+ FinishReason *string `json:"finish_reason"`
55
+ }
56
+
57
+ type ChatMessage struct {
58
+ Role string `json:"role"`
59
+ Content interface{} `json:"content"`
60
+ }
61
+
62
+ // GetContent 添加一个辅助方法来获取消息内容
63
+ func (m ChatMessage) GetContent() string {
64
+ switch v := m.Content.(type) {
65
+ case string:
66
+ return v
67
+ case []interface{}:
68
+ var result string
69
+ for _, item := range v {
70
+ if contentMap, ok := item.(map[string]interface{}); ok {
71
+ if text, exists := contentMap["text"]; exists {
72
+ if textStr, ok := text.(string); ok {
73
+ result += textStr
74
+ }
75
+ }
76
+ }
77
+ }
78
+ return result
79
+ default:
80
+ return ""
81
+ }
82
+ }
83
+
84
+ type Usage struct {
85
+ PromptTokens int `json:"prompt_tokens"`
86
+ CompletionTokens int `json:"completion_tokens"`
87
+ TotalTokens int `json:"total_tokens"`
88
+ }
89
+
90
+ // AugmentRequest Augment API请求结构
91
+ type AugmentRequest struct {
92
+ ChatHistory []AugmentChatHistory `json:"chat_history"`
93
+ Message string `json:"message"`
94
+ Mode string `json:"mode"`
95
+ }
96
+
97
+ type AugmentChatHistory struct {
98
+ ResponseText string `json:"response_text"`
99
+ RequestMessage string `json:"request_message"`
100
+ }
101
+
102
+ // AugmentResponse Augment API响应结构
103
+ type AugmentResponse struct {
104
+ Text string `json:"text"`
105
+ Done bool `json:"done"`
106
+ }
107
+
108
+ // CodeResponse 用于解析从授权服务返回的代码
109
+ type CodeResponse struct {
110
+ Code string `json:"code"`
111
+ State string `json:"state"`
112
+ TenantURL string `json:"tenant_url"`
113
+ }
114
+
115
+ // ModelObject OpenAI模型对象结构
116
+ type ModelObject struct {
117
+ ID string `json:"id"`
118
+ Object string `json:"object"`
119
+ Created int `json:"created"`
120
+ OwnedBy string `json:"owned_by"`
121
+ }
122
+
123
+ // ModelsResponse OpenAI模型列表响应结构
124
+ type ModelsResponse struct {
125
+ Object string `json:"object"`
126
+ Data []ModelObject `json:"data"`
127
+ }
128
+
129
+ // 全局变量
130
+ var (
131
+ accessToken string
132
+ tenantURL string
133
+ )
134
+
135
+ // SetAuthInfo 设置认证信息
136
+ func SetAuthInfo(token, tenant string) {
137
+ accessToken = token
138
+ tenantURL = tenant
139
+ }
140
+
141
+ // GetAuthInfo 获取认证信息
142
+ func GetAuthInfo() (string, string) {
143
+ if config.AppConfig.CodingMode == "true" {
144
+ // 调试模式
145
+ return config.AppConfig.CodingToken, config.AppConfig.TenantURL
146
+ }
147
+
148
+ // 随机获取一个token
149
+ token, tenantURL := GetRandomToken()
150
+ if token != "" && tenantURL != "" {
151
+ return token, tenantURL
152
+ }
153
+
154
+ // 如果没有可用的token,则使用内存中的token
155
+ return accessToken, tenantURL
156
+ }
157
+
158
+ // convertToAugmentRequest 将OpenAI请求转换为Augment请求
159
+ func convertToAugmentRequest(req OpenAIRequest) AugmentRequest {
160
+ augmentReq := AugmentRequest{
161
+ Mode: "CHAT",
162
+ }
163
+
164
+ if len(req.Messages) > 0 {
165
+ lastMsg := req.Messages[len(req.Messages)-1]
166
+ augmentReq.Message = lastMsg.GetContent()
167
+ }
168
+
169
+ var history []AugmentChatHistory
170
+ for i := 0; i < len(req.Messages)-1; i += 2 {
171
+ if i+1 < len(req.Messages) {
172
+ history = append(history, AugmentChatHistory{
173
+ RequestMessage: req.Messages[i].GetContent(),
174
+ ResponseText: req.Messages[i+1].GetContent(),
175
+ })
176
+ }
177
+ }
178
+
179
+ augmentReq.ChatHistory = history
180
+ return augmentReq
181
+ }
182
+
183
+ // AuthHandler 处理授权请求
184
+ func AuthHandler(c *gin.Context, authorizeURL string) {
185
+ c.JSON(http.StatusOK, gin.H{
186
+ "authorize_url": authorizeURL,
187
+ })
188
+ }
189
+
190
+ // CallbackHandler 处理回调请求
191
+ func CallbackHandler(c *gin.Context, getAccessTokenFunc func(string, string, string) (string, error)) {
192
+ // 1. 解析请求数据
193
+ var codeResp CodeResponse
194
+ if err := c.ShouldBindJSON(&codeResp); err != nil {
195
+ c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求数据"})
196
+ return
197
+ }
198
+
199
+ // 2. 使用授权码获取访问令牌
200
+ token, err := getAccessTokenFunc(codeResp.TenantURL, "", codeResp.Code)
201
+ if err != nil {
202
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
203
+ return
204
+ }
205
+
206
+ // 3. 保存令牌和租户URL
207
+ SetAuthInfo(token, codeResp.TenantURL)
208
+
209
+ // 4. 保存到Redis
210
+ if err := SaveTokenToRedis(token, codeResp.TenantURL); err != nil {
211
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "保存token到Redis失败: " + err.Error()})
212
+ return
213
+ }
214
+
215
+ // 5. 返回成功响应
216
+ c.JSON(http.StatusOK, gin.H{
217
+ "status": "success",
218
+ "token": token,
219
+ })
220
+ }
221
+
222
+ // ModelsHandler 处理模型请求
223
+ func ModelsHandler(c *gin.Context) {
224
+ // 创建符合OpenAI格式的模型列表响应
225
+ response := ModelsResponse{
226
+ Object: "list",
227
+ Data: []ModelObject{
228
+ {
229
+ ID: "claude-3-7-sonnet-20250219",
230
+ Object: "model",
231
+ Created: 1708387201,
232
+ OwnedBy: "anthropic",
233
+ },
234
+ {
235
+ ID: "claude-3.7",
236
+ Object: "model",
237
+ Created: 1708387200,
238
+ OwnedBy: "anthropic",
239
+ },
240
+ },
241
+ }
242
+
243
+ c.JSON(http.StatusOK, response)
244
+ }
245
+
246
+ // ChatCompletionsHandler 处理聊天完成请求
247
+ func ChatCompletionsHandler(c *gin.Context) {
248
+ token, tenant := GetAuthInfo()
249
+ if token == "" || tenant == "" {
250
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "无可用Token,请先在管理页面获取"})
251
+ return
252
+ }
253
+
254
+ var openAIReq OpenAIRequest
255
+ if err := c.ShouldBindJSON(&openAIReq); err != nil {
256
+ c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求数据"})
257
+ return
258
+ }
259
+
260
+ // 转换为Augment请求格式
261
+ augmentReq := convertToAugmentRequest(openAIReq)
262
+
263
+ // 处理流式请求
264
+ if openAIReq.Stream {
265
+ handleStreamRequest(c, augmentReq, openAIReq.Model)
266
+ return
267
+ }
268
+
269
+ // 处理非流式请求
270
+ handleNonStreamRequest(c, augmentReq, openAIReq.Model)
271
+ }
272
+
273
+ // 处理流式请求
274
+ func handleStreamRequest(c *gin.Context, augmentReq AugmentRequest, model string) {
275
+ c.Header("Content-Type", "text/event-stream")
276
+ c.Header("Cache-Control", "no-cache")
277
+ c.Header("Connection", "keep-alive")
278
+
279
+ // 获取token和tenant_url
280
+ token, tenant := GetAuthInfo()
281
+ if token == "" || tenant == "" {
282
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "无可用Token,请先在管理页面获取"})
283
+ return
284
+ }
285
+
286
+ // 准备请求数据
287
+ jsonData, err := json.Marshal(augmentReq)
288
+ if err != nil {
289
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "序列化请求失败"})
290
+ return
291
+ }
292
+
293
+ // 创建请求 - 使用获取到的tenant_url
294
+ req, err := http.NewRequest("POST", tenant+"chat-stream", strings.NewReader(string(jsonData)))
295
+ if err != nil {
296
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "创建请求失败"})
297
+ return
298
+ }
299
+
300
+ req.Header.Set("Content-Type", "application/json")
301
+ req.Header.Set("Authorization", "Bearer "+token) // 使用获取到的token
302
+
303
+ // 发送请求
304
+ client := &http.Client{}
305
+ resp, err := client.Do(req)
306
+ if err != nil {
307
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "请求失败: " + err.Error()})
308
+ return
309
+ }
310
+ defer resp.Body.Close()
311
+
312
+ // 设置刷新器以确保数据立即发送
313
+ flusher, ok := c.Writer.(http.Flusher)
314
+ if !ok {
315
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "流式传输不支持"})
316
+ return
317
+ }
318
+
319
+ // 读取并转发响应
320
+ reader := bufio.NewReader(resp.Body)
321
+ responseID := fmt.Sprintf("chatcmpl-%d", time.Now().Unix())
322
+
323
+ var fullText string
324
+ for {
325
+ line, err := reader.ReadString('\n')
326
+ if err != nil {
327
+ if err == io.EOF {
328
+ break
329
+ }
330
+ log.Printf("读取响应失败: %v", err)
331
+ break
332
+ }
333
+
334
+ line = strings.TrimSpace(line)
335
+ if line == "" {
336
+ continue
337
+ }
338
+
339
+ var augmentResp AugmentResponse
340
+ if err := json.Unmarshal([]byte(line), &augmentResp); err != nil {
341
+ log.Printf("解析响应失败: %v", err)
342
+ continue
343
+ }
344
+
345
+ fullText += augmentResp.Text
346
+
347
+ // 创建OpenAI兼容的流式响应
348
+ streamResp := OpenAIStreamResponse{
349
+ ID: responseID,
350
+ Object: "chat.completion.chunk",
351
+ Created: time.Now().Unix(),
352
+ Model: model,
353
+ Choices: []StreamChoice{
354
+ {
355
+ Index: 0,
356
+ Delta: ChatMessage{
357
+ Role: "assistant",
358
+ Content: augmentResp.Text,
359
+ },
360
+ FinishReason: nil,
361
+ },
362
+ },
363
+ }
364
+
365
+ // 如果是最后一条消息,设置完成原因
366
+ if augmentResp.Done {
367
+ finishReason := "stop"
368
+ streamResp.Choices[0].FinishReason = &finishReason
369
+ }
370
+
371
+ // 序列化并发送响应
372
+ jsonResp, err := json.Marshal(streamResp)
373
+ if err != nil {
374
+ log.Printf("序列化响应失败: %v", err)
375
+ continue
376
+ }
377
+
378
+ fmt.Fprintf(c.Writer, "data: %s\n\n", jsonResp)
379
+ flusher.Flush()
380
+
381
+ // 如果完成,发送最后的[DONE]标记
382
+ if augmentResp.Done {
383
+ fmt.Fprintf(c.Writer, "data: [DONE]\n\n")
384
+ flusher.Flush()
385
+ break
386
+ }
387
+ }
388
+ }
389
+
390
+ // estimateTokenCount 粗略估计文本中的token数量
391
+ // 这是一个简单的估算方法,实际token数量取决于具体的分词算法
392
+ func estimateTokenCount(text string) int {
393
+ // 英文单词和标点符号大约是1个token
394
+ // 中文字符大约是1.5个token(每个字符约为0.75个token)
395
+ // 按空格分割英文单词
396
+ words := strings.Fields(text)
397
+ wordCount := len(words)
398
+
399
+ // 计算中文字符数量
400
+ chineseCount := 0
401
+ for _, r := range text {
402
+ if r >= 0x4E00 && r <= 0x9FFF {
403
+ chineseCount++
404
+ }
405
+ }
406
+
407
+ // 粗略估计:英文单词按1个token计算,中文字符按0.75个token计算
408
+ return wordCount + int(float64(chineseCount)*0.75)
409
+ }
410
+
411
+ // 处理非流式请求
412
+ func handleNonStreamRequest(c *gin.Context, augmentReq AugmentRequest, model string) {
413
+ // 获取token和tenant_url
414
+ token, tenant := GetAuthInfo()
415
+ if token == "" || tenant == "" {
416
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "无可用Token,请先在管理页面获取"})
417
+ return
418
+ }
419
+
420
+ // 准备请求数据
421
+ jsonData, err := json.Marshal(augmentReq)
422
+ if err != nil {
423
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "序列化请求失败"})
424
+ return
425
+ }
426
+
427
+ // 创建请求 - 使用获取到的tenant_url
428
+ req, err := http.NewRequest("POST", tenant+"chat-stream", strings.NewReader(string(jsonData)))
429
+ if err != nil {
430
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "创建请求失败"})
431
+ return
432
+ }
433
+
434
+ req.Header.Set("Content-Type", "application/json")
435
+ req.Header.Set("Authorization", "Bearer "+token) // 使用获取到的token
436
+
437
+ // 发送请求
438
+ client := &http.Client{}
439
+ resp, err := client.Do(req)
440
+ if err != nil {
441
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "请求失败: " + err.Error()})
442
+ return
443
+ }
444
+ defer resp.Body.Close()
445
+
446
+ // 读取完整响应
447
+ reader := bufio.NewReader(resp.Body)
448
+ var fullText string
449
+
450
+ for {
451
+ line, err := reader.ReadString('\n')
452
+ if err != nil {
453
+ if err == io.EOF {
454
+ break
455
+ }
456
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "读取响应失败: " + err.Error()})
457
+ return
458
+ }
459
+
460
+ line = strings.TrimSpace(line)
461
+ if line == "" {
462
+ continue
463
+ }
464
+
465
+ var augmentResp AugmentResponse
466
+ if err := json.Unmarshal([]byte(line), &augmentResp); err != nil {
467
+ continue
468
+ }
469
+
470
+ fullText += augmentResp.Text
471
+
472
+ if augmentResp.Done {
473
+ break
474
+ }
475
+ }
476
+
477
+ // 创建OpenAI兼容的响应
478
+ finishReason := "stop"
479
+
480
+ // 估算token数量
481
+ promptTokens := estimateTokenCount(augmentReq.Message)
482
+ for _, history := range augmentReq.ChatHistory {
483
+ promptTokens += estimateTokenCount(history.RequestMessage)
484
+ promptTokens += estimateTokenCount(history.ResponseText)
485
+ }
486
+ completionTokens := estimateTokenCount(fullText)
487
+
488
+ openAIResp := OpenAIResponse{
489
+ ID: fmt.Sprintf("chatcmpl-%d", time.Now().Unix()),
490
+ Object: "chat.completion",
491
+ Created: time.Now().Unix(),
492
+ Model: model,
493
+ Choices: []Choice{
494
+ {
495
+ Index: 0,
496
+ Message: ChatMessage{
497
+ Role: "assistant",
498
+ Content: fullText,
499
+ },
500
+ FinishReason: &finishReason,
501
+ },
502
+ },
503
+ Usage: Usage{
504
+ PromptTokens: promptTokens,
505
+ CompletionTokens: completionTokens,
506
+ TotalTokens: promptTokens + completionTokens,
507
+ },
508
+ }
509
+
510
+ c.JSON(http.StatusOK, openAIResp)
511
+ }
api/token_handler.go ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package api
2
+
3
+ import (
4
+ "augment2api/config"
5
+ "math/rand"
6
+ "net/http"
7
+
8
+ "github.com/gin-gonic/gin"
9
+ )
10
+
11
+ // TokenInfo 存储token信息
12
+ type TokenInfo struct {
13
+ Token string `json:"token"`
14
+ TenantURL string `json:"tenant_url"`
15
+ }
16
+
17
+ // GetRedisTokenHandler 从Redis获取token列表
18
+ func GetRedisTokenHandler(c *gin.Context) {
19
+ // 获取所有token的key (使用通配符模式)
20
+ keys, err := config.RedisKeys("token:*")
21
+ if err != nil {
22
+ c.JSON(http.StatusOK, gin.H{
23
+ "status": "error",
24
+ "error": "获取token列表失败: " + err.Error(),
25
+ })
26
+ return
27
+ }
28
+
29
+ // 如果没有token
30
+ if len(keys) == 0 {
31
+ c.JSON(http.StatusOK, gin.H{
32
+ "status": "success",
33
+ "tokens": []TokenInfo{},
34
+ })
35
+ return
36
+ }
37
+
38
+ // 构建token列表
39
+ var tokenList []TokenInfo
40
+ for _, key := range keys {
41
+ // 从key中提取token (格式: "token:{token}")
42
+ token := key[6:] // 去掉前缀 "token:"
43
+
44
+ // 获取对应的tenant_url
45
+ tenantURL, err := config.RedisHGet(key, "tenant_url")
46
+ if err != nil {
47
+ continue // 跳过无效的token
48
+ }
49
+
50
+ tokenList = append(tokenList, TokenInfo{
51
+ Token: token,
52
+ TenantURL: tenantURL,
53
+ })
54
+ }
55
+
56
+ c.JSON(http.StatusOK, gin.H{
57
+ "status": "success",
58
+ "tokens": tokenList,
59
+ })
60
+ }
61
+
62
+ // SaveTokenToRedis 保存token到Redis
63
+ func SaveTokenToRedis(token, tenantURL string) error {
64
+ // 创建一个唯一的key,包含token和tenant_url
65
+ tokenKey := "token:" + token
66
+
67
+ // 将tenant_url存储在token对应的哈希表中
68
+ return config.RedisHSet(tokenKey, "tenant_url", tenantURL)
69
+ }
70
+
71
+ // GetRandomToken 从Redis中随机获取一个token
72
+ func GetRandomToken() (string, string) {
73
+ // 获取所有token的key
74
+ keys, err := config.RedisKeys("token:*")
75
+ if err != nil || len(keys) == 0 {
76
+ return "", ""
77
+ }
78
+
79
+ // 随机选择一个token
80
+ randomIndex := rand.Intn(len(keys))
81
+ randomKey := keys[randomIndex]
82
+
83
+ // 从key中提取token
84
+ token := randomKey[6:] // 去掉前缀 "token:"
85
+
86
+ // 获取对应的tenant_url
87
+ tenantURL, err := config.RedisHGet(randomKey, "tenant_url")
88
+ if err != nil {
89
+ return "", ""
90
+ }
91
+
92
+ return token, tenantURL
93
+ }
94
+
95
+ // DeleteTokenHandler 删除指定的token
96
+ func DeleteTokenHandler(c *gin.Context) {
97
+ token := c.Param("token")
98
+ if token == "" {
99
+ c.JSON(http.StatusBadRequest, gin.H{
100
+ "status": "error",
101
+ "error": "未指定token",
102
+ })
103
+ return
104
+ }
105
+
106
+ tokenKey := "token:" + token
107
+
108
+ // 检查token是否存在
109
+ exists, err := config.RedisExists(tokenKey)
110
+ if err != nil {
111
+ c.JSON(http.StatusInternalServerError, gin.H{
112
+ "status": "error",
113
+ "error": "检查token失败: " + err.Error(),
114
+ })
115
+ return
116
+ }
117
+
118
+ if !exists {
119
+ c.JSON(http.StatusNotFound, gin.H{
120
+ "status": "error",
121
+ "error": "token不存在",
122
+ })
123
+ return
124
+ }
125
+
126
+ // 删除token
127
+ if err := config.RedisDel(tokenKey); err != nil {
128
+ c.JSON(http.StatusInternalServerError, gin.H{
129
+ "status": "error",
130
+ "error": "删除token失败: " + err.Error(),
131
+ })
132
+ return
133
+ }
134
+
135
+ c.JSON(http.StatusOK, gin.H{
136
+ "status": "success",
137
+ })
138
+ }
139
+
140
+ // UseTokenHandler 设置指定的token为当前活跃token
141
+ func UseTokenHandler(c *gin.Context) {
142
+ token := c.Param("token")
143
+ if token == "" {
144
+ c.JSON(http.StatusBadRequest, gin.H{
145
+ "status": "error",
146
+ "error": "未指定token",
147
+ })
148
+ return
149
+ }
150
+
151
+ tokenKey := "token:" + token
152
+
153
+ // 检查token是否存在
154
+ exists, err := config.RedisExists(tokenKey)
155
+ if err != nil {
156
+ c.JSON(http.StatusInternalServerError, gin.H{
157
+ "status": "error",
158
+ "error": "检查token失败: " + err.Error(),
159
+ })
160
+ return
161
+ }
162
+
163
+ if !exists {
164
+ c.JSON(http.StatusNotFound, gin.H{
165
+ "status": "error",
166
+ "error": "token不存在",
167
+ })
168
+ return
169
+ }
170
+
171
+ // 设置当前活跃token
172
+ if err := config.RedisSet("current_token", token, 0); err != nil {
173
+ c.JSON(http.StatusInternalServerError, gin.H{
174
+ "status": "error",
175
+ "error": "设置当前token失败: " + err.Error(),
176
+ })
177
+ return
178
+ }
179
+
180
+ c.JSON(http.StatusOK, gin.H{
181
+ "status": "success",
182
+ })
183
+ }
config/config.go ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package config
2
+
3
+ import (
4
+ "augment2api/pkg/logger"
5
+ "os"
6
+ )
7
+
8
+ type Config struct {
9
+ RedisConnString string
10
+ AuthToken string
11
+ CodingMode string
12
+ CodingToken string
13
+ TenantURL string
14
+ }
15
+
16
+ var AppConfig Config
17
+
18
+ func InitConfig() error {
19
+ // 从环境变量读取配置
20
+ AppConfig = Config{
21
+ // 必填配置
22
+ RedisConnString: getEnv("REDIS_CONN_STRING", ""),
23
+ // 非必填配置
24
+ AuthToken: getEnv("AUTH_TOKEN", ""),
25
+ CodingMode: getEnv("CODING_MODE", "false"),
26
+ CodingToken: getEnv("CODING_TOKEN", ""),
27
+ TenantURL: getEnv("TENANT_URL", ""),
28
+ }
29
+
30
+ // redis连接字符串 示例: redis://default:pwd@localhost:6379
31
+ if AppConfig.RedisConnString == "" {
32
+ logger.Log.Fatalln("未配置环境变量 REDIS_CONN_STRING")
33
+ }
34
+
35
+ logger.Log.Info("Augment2Api配置加载完成:\n" +
36
+ "----------------------------------------\n" +
37
+ "AuthToken: " + AppConfig.AuthToken + "\n" +
38
+ "RedisConnString: " + AppConfig.RedisConnString + "\n" +
39
+ "----------------------------------------")
40
+
41
+ return nil
42
+ }
43
+
44
+ func getEnv(key, defaultValue string) string {
45
+ value := os.Getenv(key)
46
+ if value == "" {
47
+ return defaultValue
48
+ }
49
+ return value
50
+ }
config/redis.go ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package config
2
+
3
+ import (
4
+ "augment2api/pkg/logger"
5
+ "context"
6
+ "os"
7
+ "time"
8
+
9
+ "github.com/go-redis/redis/v8"
10
+ )
11
+
12
+ var RDB redis.Cmdable
13
+
14
+ // InitRedisClient This function is called after init()
15
+ func InitRedisClient() (err error) {
16
+
17
+ RedisConnString := AppConfig.RedisConnString
18
+ if RedisConnString == "" {
19
+ logger.Log.Debug("REDIS_CONN_STRING not set, Redis is not enabled")
20
+ return nil
21
+ }
22
+
23
+ opt, err := redis.ParseURL(RedisConnString)
24
+ if err != nil {
25
+ logger.Log.Fatalln("failed to parse Redis connection string: " + err.Error())
26
+ }
27
+ RDB = redis.NewClient(opt)
28
+
29
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
30
+ defer cancel()
31
+
32
+ _, err = RDB.Ping(ctx).Result()
33
+ if err != nil {
34
+ logger.Log.Fatalln("Redis ping test failed: " + err.Error())
35
+ }
36
+ return err
37
+ }
38
+
39
+ func ParseRedisOption() *redis.Options {
40
+ opt, err := redis.ParseURL(os.Getenv("REDIS_CONN_STRING"))
41
+ if err != nil {
42
+ logger.Log.Fatalln("failed to parse Redis connection string: " + err.Error())
43
+ }
44
+ return opt
45
+ }
46
+
47
+ func RedisSet(key string, value string, expiration time.Duration) error {
48
+ ctx := context.Background()
49
+ return RDB.Set(ctx, key, value, expiration).Err()
50
+ }
51
+
52
+ func RedisGet(key string) (string, error) {
53
+ ctx := context.Background()
54
+ return RDB.Get(ctx, key).Result()
55
+ }
56
+
57
+ func RedisDel(key string) error {
58
+ ctx := context.Background()
59
+ return RDB.Del(ctx, key).Err()
60
+ }
61
+
62
+ // RedisHSet 设置哈希表字段值
63
+ func RedisHSet(key, field, value string) error {
64
+ ctx := context.Background()
65
+ return RDB.HSet(ctx, key, field, value).Err()
66
+ }
67
+
68
+ // RedisHGet 获取哈希表字段值
69
+ func RedisHGet(key, field string) (string, error) {
70
+ ctx := context.Background()
71
+ return RDB.HGet(ctx, key, field).Result()
72
+ }
73
+
74
+ // RedisExpire 设置键的过期时间
75
+ func RedisExpire(key string, expiration time.Duration) error {
76
+ ctx := context.Background()
77
+ return RDB.Expire(ctx, key, expiration).Err()
78
+ }
79
+
80
+ // RedisKeys 获取匹配指定模式的所有键
81
+ func RedisKeys(pattern string) ([]string, error) {
82
+ ctx := context.Background()
83
+ return RDB.Keys(ctx, pattern).Result()
84
+ }
85
+
86
+ // RedisExists 检查键是否存在
87
+ func RedisExists(key string) (bool, error) {
88
+ ctx := context.Background()
89
+ result, err := RDB.Exists(ctx, key).Result()
90
+ if err != nil {
91
+ return false, err
92
+ }
93
+ return result > 0, nil
94
+ }
go.mod ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module augment2api
2
+
3
+ go 1.23.0
4
+
5
+ toolchain go1.23.6
6
+
7
+ require (
8
+ github.com/gin-contrib/cors v1.7.4
9
+ github.com/gin-gonic/gin v1.10.0
10
+ github.com/go-redis/redis/v8 v8.11.5
11
+ github.com/sirupsen/logrus v1.9.3
12
+ )
13
+
14
+ require (
15
+ github.com/bytedance/sonic v1.12.6 // indirect
16
+ github.com/bytedance/sonic/loader v0.2.1 // indirect
17
+ github.com/cespare/xxhash/v2 v2.1.2 // indirect
18
+ github.com/cloudwego/base64x v0.1.4 // indirect
19
+ github.com/cloudwego/iasm v0.2.0 // indirect
20
+ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
21
+ github.com/gabriel-vasile/mimetype v1.4.7 // 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.23.0 // indirect
26
+ github.com/goccy/go-json v0.10.4 // indirect
27
+ github.com/json-iterator/go v1.1.12 // indirect
28
+ github.com/klauspost/cpuid/v2 v2.2.9 // indirect
29
+ github.com/kr/text v0.2.0 // indirect
30
+ github.com/leodido/go-urn v1.4.0 // indirect
31
+ github.com/mattn/go-isatty v0.0.20 // indirect
32
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
33
+ github.com/modern-go/reflect2 v1.0.2 // indirect
34
+ github.com/pelletier/go-toml/v2 v2.2.3 // indirect
35
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
36
+ github.com/ugorji/go/codec v1.2.12 // indirect
37
+ golang.org/x/arch v0.12.0 // indirect
38
+ golang.org/x/crypto v0.36.0 // indirect
39
+ golang.org/x/net v0.37.0 // indirect
40
+ golang.org/x/sys v0.31.0 // indirect
41
+ golang.org/x/text v0.23.0 // indirect
42
+ google.golang.org/protobuf v1.36.1 // indirect
43
+ gopkg.in/yaml.v3 v3.0.1 // indirect
44
+ )
go.sum ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ github.com/bytedance/sonic v1.12.6 h1:/isNmCUF2x3Sh8RAp/4mh4ZGkcFAX/hLrzrK3AvpRzk=
2
+ github.com/bytedance/sonic v1.12.6/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
3
+ github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
4
+ github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
5
+ github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
6
+ github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
7
+ github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
8
+ github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
9
+ github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
10
+ github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
11
+ github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
12
+ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
13
+ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
14
+ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
15
+ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
16
+ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
17
+ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
18
+ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
19
+ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
20
+ github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA=
21
+ github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU=
22
+ github.com/gin-contrib/cors v1.7.4 h1:/fC6/wk7rCRtqKqki8lLr2Xq+hnV49aXDLIuSek9g4k=
23
+ github.com/gin-contrib/cors v1.7.4/go.mod h1:vGc/APSgLMlQfEJV5NAzkrAHb0C8DetL3K6QZuvGii0=
24
+ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
25
+ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
26
+ github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
27
+ github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
28
+ github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
29
+ github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
30
+ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
31
+ github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
32
+ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
33
+ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
34
+ github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
35
+ github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
36
+ github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
37
+ github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
38
+ github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM=
39
+ github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
40
+ github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
41
+ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
42
+ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
43
+ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
44
+ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
45
+ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
46
+ github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
47
+ github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
48
+ github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
49
+ github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
50
+ github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
51
+ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
52
+ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
53
+ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
54
+ github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
55
+ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
56
+ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
57
+ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
58
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
59
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
60
+ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
61
+ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
62
+ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
63
+ github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
64
+ github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
65
+ github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
66
+ github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
67
+ github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
68
+ github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
69
+ github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
70
+ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
71
+ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
72
+ github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
73
+ github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
74
+ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
75
+ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
76
+ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
77
+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
78
+ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
79
+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
80
+ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
81
+ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
82
+ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
83
+ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
84
+ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
85
+ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
89
+ github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
90
+ golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg=
91
+ golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
92
+ golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
93
+ golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
94
+ golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
95
+ golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
96
+ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
97
+ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
98
+ golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
99
+ golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
100
+ golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
101
+ golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
102
+ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
103
+ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
104
+ google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
105
+ google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
106
+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
107
+ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
108
+ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
109
+ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
110
+ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
111
+ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
112
+ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
113
+ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
114
+ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
115
+ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
116
+ nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
main.go ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package main
2
+
3
+ import (
4
+ "augment2api/api"
5
+ "augment2api/config"
6
+ "augment2api/middleware"
7
+ "augment2api/pkg/logger"
8
+ "crypto/rand"
9
+ "crypto/sha256"
10
+ "encoding/base64"
11
+ "encoding/json"
12
+ "fmt"
13
+ "log"
14
+ "net/http"
15
+ "net/url"
16
+ "strings"
17
+ "time"
18
+
19
+ "github.com/gin-gonic/gin"
20
+ )
21
+
22
+ const clientID = "v"
23
+
24
+ // OAuthState 存储OAuth状态信息
25
+ type OAuthState struct {
26
+ CodeVerifier string `json:"code_verifier"`
27
+ CodeChallenge string `json:"code_challenge"`
28
+ State string `json:"state"`
29
+ CreationTime time.Time `json:"creation_time"`
30
+ }
31
+
32
+ // 全局变量存储OAuth状态
33
+ var (
34
+ globalOAuthState OAuthState
35
+ )
36
+
37
+ // base64URLEncode 编码Buffer为base64 URL安全格式
38
+ func base64URLEncode(data []byte) string {
39
+ encoded := base64.StdEncoding.EncodeToString(data)
40
+ encoded = strings.ReplaceAll(encoded, "+", "-")
41
+ encoded = strings.ReplaceAll(encoded, "/", "_")
42
+ encoded = strings.ReplaceAll(encoded, "=", "")
43
+ return encoded
44
+ }
45
+
46
+ // sha256Hash 计算SHA256哈希
47
+ func sha256Hash(input []byte) []byte {
48
+ hash := sha256.Sum256(input)
49
+ return hash[:]
50
+ }
51
+
52
+ // createOAuthState 创建OAuth状态
53
+ func createOAuthState() OAuthState {
54
+ codeVerifierBytes := make([]byte, 32)
55
+ _, err := rand.Read(codeVerifierBytes)
56
+ if err != nil {
57
+ log.Fatalf("生成随机字节失败: %v", err)
58
+ }
59
+
60
+ codeVerifier := base64URLEncode(codeVerifierBytes)
61
+ codeChallenge := base64URLEncode(sha256Hash([]byte(codeVerifier)))
62
+
63
+ stateBytes := make([]byte, 8)
64
+ _, err = rand.Read(stateBytes)
65
+ if err != nil {
66
+ log.Fatalf("生成随机状态失败: %v", err)
67
+ }
68
+ state := base64URLEncode(stateBytes)
69
+
70
+ return OAuthState{
71
+ CodeVerifier: codeVerifier,
72
+ CodeChallenge: codeChallenge,
73
+ State: state,
74
+ CreationTime: time.Now(),
75
+ }
76
+ }
77
+
78
+ // generateAuthorizeURL 生成授权URL
79
+ func generateAuthorizeURL(oauthState OAuthState) string {
80
+ params := url.Values{}
81
+ params.Add("response_type", "code")
82
+ params.Add("code_challenge", oauthState.CodeChallenge)
83
+ params.Add("client_id", clientID)
84
+ params.Add("state", oauthState.State)
85
+ params.Add("prompt", "login")
86
+
87
+ authorizeURL := fmt.Sprintf("https://auth.augmentcode.com/authorize?%s", params.Encode())
88
+ return authorizeURL
89
+ }
90
+
91
+ // getAccessToken 获取访问令牌
92
+ func getAccessToken(tenantURL, codeVerifier, code string) (string, error) {
93
+ data := map[string]string{
94
+ "grant_type": "authorization_code",
95
+ "client_id": clientID,
96
+ "code_verifier": codeVerifier,
97
+ "redirect_uri": "",
98
+ "code": code,
99
+ }
100
+
101
+ jsonData, err := json.Marshal(data)
102
+ if err != nil {
103
+ return "", fmt.Errorf("序列化数据失败: %v", err)
104
+ }
105
+
106
+ resp, err := http.Post(tenantURL+"token", "application/json", strings.NewReader(string(jsonData)))
107
+ if err != nil {
108
+ return "", fmt.Errorf("请求令牌失败: %v", err)
109
+ }
110
+ defer resp.Body.Close()
111
+
112
+ var result map[string]interface{}
113
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
114
+ return "", fmt.Errorf("解析响应失败: %v", err)
115
+ }
116
+
117
+ token, ok := result["access_token"].(string)
118
+ if !ok {
119
+ return "", fmt.Errorf("响应中没有访问令牌")
120
+ }
121
+
122
+ return token, nil
123
+ }
124
+
125
+ // 初始化路由
126
+ func setupRouter() *gin.Engine {
127
+ r := gin.Default()
128
+
129
+ // 跨域
130
+ r.Use(middleware.CORS())
131
+
132
+ // 初始化OAuth状态
133
+ globalOAuthState = createOAuthState()
134
+
135
+ // 静态文件服务
136
+ r.Static("/static", "./static")
137
+ r.LoadHTMLGlob("templates/*")
138
+
139
+ r.GET("/", func(c *gin.Context) {
140
+ c.HTML(http.StatusOK, "admin.html", gin.H{})
141
+ })
142
+
143
+ // 授权端点
144
+ r.GET("/auth", func(c *gin.Context) {
145
+ authorizeURL := generateAuthorizeURL(globalOAuthState)
146
+ api.AuthHandler(c, authorizeURL)
147
+ })
148
+
149
+ // 获取token
150
+ r.GET("/api/tokens", api.GetRedisTokenHandler)
151
+
152
+ // 删除token
153
+ r.DELETE("/api/token/:token", api.DeleteTokenHandler)
154
+
155
+ // 回调端点,用于处理授权码
156
+ r.POST("/callback", func(c *gin.Context) {
157
+ api.CallbackHandler(c, func(tenantURL, _, code string) (string, error) {
158
+ return getAccessToken(tenantURL, globalOAuthState.CodeVerifier, code)
159
+ })
160
+ })
161
+
162
+ // 鉴权路由组
163
+ authGroup := r.Group("")
164
+ authGroup.Use(api.AuthMiddleware())
165
+ {
166
+ // OpenAI兼容的聊天端点
167
+ authGroup.POST("/v1/chat/completions", api.ChatCompletionsHandler)
168
+ authGroup.POST("/v1", api.ChatCompletionsHandler)
169
+ authGroup.POST("/v1/chat", api.ChatCompletionsHandler)
170
+
171
+ // OpenAI兼容的模型接口
172
+ authGroup.GET("/v1/models", api.ModelsHandler)
173
+ }
174
+
175
+ return r
176
+ }
177
+
178
+ func main() {
179
+ // 设置全局时区为东八区(CST)
180
+ time.Local = time.FixedZone("CST", 8*3600)
181
+
182
+ // 设置 Gin 为发布模式
183
+ gin.SetMode(gin.ReleaseMode)
184
+
185
+ // 初始化日志
186
+ logger.Init()
187
+
188
+ // 初始化配置
189
+ err := config.InitConfig()
190
+ if err != nil {
191
+ logger.Log.Fatalln("failed to initialize config: " + err.Error())
192
+ return
193
+ }
194
+
195
+ // 初始化Redis
196
+ err = config.InitRedisClient()
197
+ if err != nil {
198
+ logger.Log.Fatalln("failed to initialize Redis: " + err.Error())
199
+ }
200
+
201
+ r := setupRouter()
202
+
203
+ // 启动服务器
204
+ if err := r.Run(":27080"); err != nil {
205
+ logger.Log.Fatalf("启动服务失败: %v", err)
206
+ }
207
+
208
+ logger.Log.WithFields(map[string]interface{}{
209
+ "port": 27080,
210
+ "mode": gin.Mode(),
211
+ }).Info("Augment2API 服务启动成功")
212
+ }
middleware/cors.go ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package middleware
2
+
3
+ import (
4
+ "github.com/gin-contrib/cors"
5
+ "github.com/gin-gonic/gin"
6
+ )
7
+
8
+ func CORS() gin.HandlerFunc {
9
+ config := cors.DefaultConfig()
10
+ config.AllowAllOrigins = true
11
+ config.AllowCredentials = true
12
+ config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}
13
+ config.AllowHeaders = []string{"*"}
14
+ return cors.New(config)
15
+ }
pkg/logger/logger.go ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package logger
2
+
3
+ import (
4
+ "fmt"
5
+ "github.com/sirupsen/logrus"
6
+ "os"
7
+ "strings"
8
+ "time"
9
+ )
10
+
11
+ // CustomFormatter 自定义格式化器
12
+ type CustomFormatter struct {
13
+ TimestampFormat string
14
+ }
15
+
16
+ func (f *CustomFormatter) Format(entry *logrus.Entry) ([]byte, error) {
17
+ // 将时间调整为东八区
18
+ localTime := entry.Time.In(time.FixedZone("CST", 8*3600))
19
+ // 构建日志消息
20
+ timestamp := localTime.Format(f.TimestampFormat)
21
+ level := strings.ToUpper(entry.Level.String())
22
+
23
+ // 将所有字段合并到一个字符串中,添加适当的分隔
24
+ var fieldsStr string
25
+ if len(entry.Data) > 0 {
26
+ pairs := make([]string, 0, len(entry.Data))
27
+ for k, v := range entry.Data {
28
+ pairs = append(pairs, fmt.Sprintf("%s: %v", k, v))
29
+ }
30
+ fieldsStr = " | " + strings.Join(pairs, " | ")
31
+ }
32
+
33
+ // 简化的日志格式,移除文件名和行号
34
+ logMsg := fmt.Sprintf("[%s] %-5s %s%s\n",
35
+ timestamp,
36
+ level,
37
+ entry.Message,
38
+ fieldsStr,
39
+ )
40
+
41
+ return []byte(logMsg), nil
42
+ }
43
+
44
+ var Log = logrus.New()
45
+
46
+ func Init() {
47
+ // 使用自定义格式化器
48
+ Log.SetFormatter(&CustomFormatter{
49
+ TimestampFormat: "2006-01-02 15:04:05",
50
+ })
51
+
52
+ // 设置输出到标准输出
53
+ Log.SetOutput(os.Stdout)
54
+
55
+ // 设置日志级别
56
+ if os.Getenv("DEBUG") == "true" {
57
+ Log.SetLevel(logrus.DebugLevel)
58
+ } else {
59
+ Log.SetLevel(logrus.InfoLevel)
60
+ }
61
+ }
templates/admin.html ADDED
@@ -0,0 +1,660 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Augment2Api</title>
7
+ <style>
8
+ :root {
9
+ --primary-color: #4361ee;
10
+ --secondary-color: #3f37c9;
11
+ --success-color: #4caf50;
12
+ --error-color: #f44336;
13
+ --bg-color: #f8f9fa;
14
+ --card-bg: #ffffff;
15
+ --text-color: #333333;
16
+ --border-color: #e0e0e0;
17
+ --shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
18
+ --radius: 8px;
19
+ }
20
+
21
+ * {
22
+ box-sizing: border-box;
23
+ margin: 0;
24
+ padding: 0;
25
+ }
26
+
27
+ body {
28
+ font-family: 'PingFang SC', 'Microsoft YaHei', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
29
+ background-color: var(--bg-color);
30
+ color: var(--text-color);
31
+ line-height: 1.6;
32
+ padding: 20px;
33
+ min-height: 100vh;
34
+ display: flex;
35
+ flex-direction: column;
36
+ }
37
+
38
+ .container {
39
+ max-width: 1200px;
40
+ margin: 0 auto;
41
+ flex: 1;
42
+ display: flex;
43
+ flex-direction: column;
44
+ }
45
+
46
+ header {
47
+ text-align: center;
48
+ margin-bottom: 30px;
49
+ padding-bottom: 15px;
50
+ border-bottom: 1px solid var(--border-color);
51
+ }
52
+
53
+ h1 {
54
+ font-size: 28px;
55
+ font-weight: 600;
56
+ color: black;
57
+ }
58
+
59
+ h2 {
60
+ font-size: 20px;
61
+ font-weight: 500;
62
+ margin-bottom: 15px;
63
+ color: var(--secondary-color);
64
+ }
65
+
66
+ .dashboard {
67
+ display: flex;
68
+ flex-direction: row;
69
+ gap: 20px;
70
+ align-items: stretch;
71
+ min-height: 500px;
72
+ }
73
+
74
+ @media (max-width: 768px) {
75
+ .dashboard {
76
+ flex-direction: column;
77
+ }
78
+ }
79
+
80
+ .panel {
81
+ background: var(--card-bg);
82
+ border-radius: var(--radius);
83
+ box-shadow: var(--shadow);
84
+ padding: 25px;
85
+ display: flex;
86
+ flex-direction: column;
87
+ }
88
+
89
+ .panel-left {
90
+ width: 50%;
91
+ max-width: 580px;
92
+ flex: 1;
93
+ }
94
+
95
+ .panel-right {
96
+ width: 50%;
97
+ max-width: 580px;
98
+ flex: 1;
99
+ position: sticky;
100
+ top: 20px;
101
+ }
102
+
103
+ @media (max-width: 768px) {
104
+ .panel-left, .panel-right {
105
+ width: 100%;
106
+ max-width: none;
107
+ }
108
+ }
109
+
110
+ .panel-title {
111
+ display: flex;
112
+ align-items: center;
113
+ margin-bottom: 20px;
114
+ }
115
+
116
+ .panel-title i {
117
+ margin-right: 10px;
118
+ font-size: 20px;
119
+ color: var(--primary-color);
120
+ }
121
+
122
+ .panel-title h2 {
123
+ margin-bottom: 0;
124
+ }
125
+
126
+ .token-display {
127
+ background: var(--bg-color);
128
+ padding: 15px;
129
+ border-radius: var(--radius);
130
+ word-break: break-all;
131
+ margin: 15px 0;
132
+ border: 1px solid var(--border-color);
133
+ font-family: monospace;
134
+ font-size: 14px;
135
+ position: relative;
136
+ }
137
+
138
+ .token-label {
139
+ font-weight: 500;
140
+ margin-bottom: 5px;
141
+ color: #666;
142
+ }
143
+
144
+ button {
145
+ background: var(--primary-color);
146
+ color: white;
147
+ border: none;
148
+ padding: 10px 15px;
149
+ border-radius: var(--radius);
150
+ cursor: pointer;
151
+ font-size: 14px;
152
+ margin-top: 10px;
153
+ transition: background 0.3s;
154
+ display: inline-flex;
155
+ align-items: center;
156
+ justify-content: center;
157
+ }
158
+
159
+ button:hover {
160
+ background: var(--secondary-color);
161
+ }
162
+
163
+ button i {
164
+ margin-right: 8px;
165
+ }
166
+
167
+ input, textarea {
168
+ width: 100%;
169
+ padding: 12px;
170
+ margin: 10px 0;
171
+ border: 1px solid var(--border-color);
172
+ border-radius: var(--radius);
173
+ font-size: 14px;
174
+ transition: border 0.3s;
175
+ }
176
+
177
+ input:focus, textarea:focus {
178
+ outline: none;
179
+ border-color: var(--primary-color);
180
+ }
181
+
182
+ textarea {
183
+ height: 120px;
184
+ resize: vertical;
185
+ }
186
+
187
+ .error {
188
+ color: var(--error-color);
189
+ margin: 10px 0;
190
+ padding: 10px;
191
+ background-color: rgba(244, 67, 54, 0.1);
192
+ border-radius: var(--radius);
193
+ display: none;
194
+ }
195
+
196
+ .success {
197
+ color: var(--success-color);
198
+ margin: 10px 0;
199
+ padding: 10px;
200
+ background-color: rgba(76, 175, 80, 0.1);
201
+ border-radius: var(--radius);
202
+ display: none;
203
+ }
204
+
205
+ .step {
206
+ margin-bottom: 20px;
207
+ padding-bottom: 20px;
208
+ border-bottom: 1px dashed var(--border-color);
209
+ }
210
+
211
+ .step:last-child {
212
+ border-bottom: none;
213
+ }
214
+
215
+ .step-number {
216
+ display: inline-block;
217
+ width: 24px;
218
+ height: 24px;
219
+ background-color: var(--primary-color);
220
+ color: white;
221
+ border-radius: 50%;
222
+ text-align: center;
223
+ line-height: 24px;
224
+ margin-right: 10px;
225
+ font-size: 14px;
226
+ }
227
+
228
+ /* 添加一些图标 */
229
+ .icon {
230
+ display: inline-block;
231
+ width: 1em;
232
+ height: 1em;
233
+ stroke-width: 0;
234
+ stroke: currentColor;
235
+ fill: currentColor;
236
+ vertical-align: middle;
237
+ }
238
+
239
+ /* 添加Token列表样式 */
240
+ .token-list {
241
+ display: flex;
242
+ flex-direction: column;
243
+ gap: 15px;
244
+ margin-bottom: 15px;
245
+ max-height: 500px;
246
+ overflow-y: auto;
247
+ padding-right: 10px;
248
+ }
249
+
250
+ .token-item {
251
+ background: var(--bg-color);
252
+ border-radius: var(--radius);
253
+ border: 1px solid var(--border-color);
254
+ overflow: hidden;
255
+ transition: all 0.3s ease;
256
+ }
257
+
258
+ .current-token {
259
+ border: 2px solid var(--primary-color);
260
+ }
261
+
262
+ .token-header {
263
+ display: flex;
264
+ padding: 12px 15px;
265
+ cursor: pointer;
266
+ align-items: center;
267
+ background: #f0f2f5;
268
+ }
269
+
270
+ .token-number {
271
+ background: var(--primary-color);
272
+ color: white;
273
+ padding: 6px 10px;
274
+ display: flex;
275
+ align-items: center;
276
+ justify-content: center;
277
+ font-weight: bold;
278
+ min-width: 30px;
279
+ border-radius: 4px;
280
+ margin-right: 12px;
281
+ }
282
+
283
+ .token-summary {
284
+ flex: 1;
285
+ font-weight: 500;
286
+ white-space: nowrap;
287
+ overflow: hidden;
288
+ text-overflow: ellipsis;
289
+ }
290
+
291
+ .token-toggle {
292
+ margin-left: 10px;
293
+ transition: transform 0.3s;
294
+ }
295
+
296
+ .token-toggle.open {
297
+ transform: rotate(180deg);
298
+ }
299
+
300
+ .token-details {
301
+ padding: 0;
302
+ max-height: 0;
303
+ overflow: hidden;
304
+ transition: all 0.3s ease;
305
+ }
306
+
307
+ .token-details.open {
308
+ padding: 12px;
309
+ max-height: none;
310
+ overflow: visible;
311
+ }
312
+
313
+ .token-details .token-label {
314
+ font-size: 13px;
315
+ margin-bottom: 3px;
316
+ }
317
+
318
+ .token-details .token-display {
319
+ font-size: 13px;
320
+ padding: 10px;
321
+ margin: 5px 0 10px 0;
322
+ }
323
+
324
+ .token-actions {
325
+ display: flex;
326
+ justify-content: flex-end;
327
+ margin-top: 8px;
328
+ }
329
+
330
+ .token-actions button {
331
+ margin-left: 10px;
332
+ padding: 6px 12px;
333
+ font-size: 13px;
334
+ }
335
+
336
+ .no-tokens {
337
+ padding: 20px;
338
+ text-align: center;
339
+ background: var(--bg-color);
340
+ border-radius: var(--radius);
341
+ color: #666;
342
+ margin-bottom: 15px;
343
+ }
344
+
345
+ /* 添加加载动画样式 */
346
+ .loading {
347
+ position: relative;
348
+ pointer-events: none;
349
+ }
350
+
351
+ .loading:after {
352
+ content: '';
353
+ display: inline-block;
354
+ width: 1em;
355
+ height: 1em;
356
+ border: 2px solid rgba(255, 255, 255, 0.3);
357
+ border-radius: 50%;
358
+ border-top-color: white;
359
+ animation: spin 0.8s linear infinite;
360
+ margin-left: 8px;
361
+ vertical-align: middle;
362
+ }
363
+
364
+ @keyframes spin {
365
+ to {
366
+ transform: rotate(360deg);
367
+ }
368
+ }
369
+
370
+ .btn-text {
371
+ display: inline-block;
372
+ }
373
+
374
+ .loading .btn-icon {
375
+ display: none;
376
+ }
377
+
378
+ /* 添加页脚样式 */
379
+ footer {
380
+ text-align: center;
381
+ margin-top: 30px;
382
+ padding: 15px 0;
383
+ color: #666;
384
+ font-size: 14px;
385
+ border-top: 1px solid var(--border-color);
386
+ margin-top: auto;
387
+ }
388
+
389
+ footer a {
390
+ color: var(--primary-color);
391
+ text-decoration: none;
392
+ transition: color 0.3s;
393
+ }
394
+
395
+ footer a:hover {
396
+ color: var(--secondary-color);
397
+ text-decoration: underline;
398
+ }
399
+ </style>
400
+ <!-- 添加图标 -->
401
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
402
+ </head>
403
+ <body>
404
+ <div class="container">
405
+ <header>
406
+ <h1>Augment面板</h1>
407
+ </header>
408
+
409
+ <div class="dashboard">
410
+ <!-- 左侧面板:查看当前Token -->
411
+ <div class="panel panel-left">
412
+ <div class="panel-title">
413
+ <i class="bi bi-key-fill"></i>
414
+ <h2>当前Token列表</h2>
415
+ </div>
416
+
417
+ <div id="token-list">加载中...</div>
418
+ <button id="refresh-token"><i class="bi bi-arrow-clockwise btn-icon"></i> <span class="btn-text">刷新</span></button>
419
+ </div>
420
+
421
+ <!-- 右侧面板:授权获取Token -->
422
+ <div class="panel panel-right">
423
+ <div class="panel-title">
424
+ <i class="bi bi-shield-lock-fill"></i>
425
+ <h2>授权获取Token</h2>
426
+ </div>
427
+
428
+ <div class="auth-steps">
429
+ <div class="step">
430
+ <h3><span class="step-number">1</span> 获取授权地址</h3>
431
+ <p>点击下方按钮获取授权地址,然后在浏览器中打开该地址进行授权。</p>
432
+ <button id="get-auth-url"><i class="bi bi-link-45deg" class="btn-icon"></i> <span class="btn-text">获取授权地址</span></button>
433
+ <div id="auth-url" class="token-display" style="display: none;"></div>
434
+ </div>
435
+
436
+ <div class="step">
437
+ <h3><span class="step-number">2</span> 提交授权响应</h3>
438
+ <p>完成授权后,将获得的授权响应粘贴到下面的文本框中:</p>
439
+ <textarea id="auth-response" placeholder='{"code":"_000baec407c57c4bf9xxxxxxxxxxxxxx","state":"0uXxxxxxxxx","tenant_url":"https://d20.api.augmentcode.com/"}'></textarea>
440
+ <div id="validation-message" class="error"></div>
441
+ <button id="submit-auth"><i class="bi bi-check2-circle" class="btn-icon"></i> <span class="btn-text">获取Token</span></button>
442
+ <div id="submit-result" class="success"></div>
443
+ </div>
444
+ </div>
445
+ </div>
446
+ </div>
447
+
448
+ <!-- 添加页脚 -->
449
+ <footer>
450
+ Designed by <a href="https://linux.do/u/bifang/summary" target="_blank">彼方</a>
451
+ </footer>
452
+ </div>
453
+
454
+ <script>
455
+ document.addEventListener('DOMContentLoaded', function() {
456
+ // 获取当前Token列表
457
+ function fetchCurrentToken() {
458
+ fetch('/api/tokens')
459
+ .then(response => response.json())
460
+ .then(data => {
461
+ if (data.status === 'success') {
462
+ const tokenListElement = document.getElementById('token-list');
463
+
464
+ if (data.tokens.length === 0) {
465
+ tokenListElement.innerHTML = '<div class="no-tokens">暂无可用Token,请点击右侧面板获取</div>';
466
+ return;
467
+ }
468
+
469
+ // 创建token列表容器
470
+ tokenListElement.innerHTML = '<div class="token-list"></div>';
471
+ const listContainer = tokenListElement.querySelector('.token-list');
472
+
473
+ // 添加每个token项
474
+ data.tokens.forEach((tokenInfo, index) => {
475
+ // 截断token以便显示
476
+ const shortToken = tokenInfo.token.length > 15
477
+ ? tokenInfo.token.substring(0, 15) + '...'
478
+ : tokenInfo.token;
479
+
480
+ const tokenItem = document.createElement('div');
481
+ tokenItem.className = 'token-item';
482
+ tokenItem.innerHTML = `
483
+ <div class="token-header">
484
+ <div class="token-number">${index + 1}</div>
485
+ <div class="token-summary">${shortToken}</div>
486
+ <div class="token-toggle"><i class="bi bi-chevron-down"></i></div>
487
+ </div>
488
+ <div class="token-details">
489
+ <div class="token-label">Token:</div>
490
+ <div class="token-display">${tokenInfo.token}</div>
491
+ <div class="token-label">租户URL:</div>
492
+ <div class="token-display">${tokenInfo.tenant_url}</div>
493
+ <div class="token-actions">
494
+ <button class="delete-token" data-token="${tokenInfo.token}">
495
+ <i class="bi bi-trash"></i> 删除
496
+ </button>
497
+ </div>
498
+ </div>
499
+ `;
500
+
501
+ listContainer.appendChild(tokenItem);
502
+
503
+ // 添加点击事件
504
+ const header = tokenItem.querySelector('.token-header');
505
+ const details = tokenItem.querySelector('.token-details');
506
+ const toggle = tokenItem.querySelector('.token-toggle');
507
+
508
+ header.addEventListener('click', function() {
509
+ details.classList.toggle('open');
510
+ toggle.classList.toggle('open');
511
+ });
512
+ });
513
+ } else {
514
+ showError('获取Token列表失败: ' + (data.error || '未知错误'));
515
+ }
516
+ })
517
+ .catch(error => {
518
+ document.getElementById('token-list').innerHTML =
519
+ '<div class="error" style="display:block;">请求失败: ' + error.message + '</div>';
520
+ });
521
+
522
+ return Promise.resolve(); // 确保可以使用.finally()
523
+ }
524
+
525
+ // 显示错误信息的辅助函数
526
+ function showError(message) {
527
+ const tokenListElement = document.getElementById('token-list');
528
+ tokenListElement.innerHTML = `<div class="error" style="display:block;">${message}</div>`;
529
+ }
530
+
531
+ // 获取授权地址
532
+ document.getElementById('get-auth-url').addEventListener('click', function() {
533
+ const button = this;
534
+ button.classList.add('loading');
535
+
536
+ fetch('/auth')
537
+ .then(response => response.json())
538
+ .then(data => {
539
+ const authUrlElement = document.getElementById('auth-url');
540
+ authUrlElement.textContent = data.authorize_url;
541
+ authUrlElement.style.display = 'block';
542
+ })
543
+ .catch(error => {
544
+ alert('获取授权地址失败: ' + error.message);
545
+ })
546
+ .finally(() => {
547
+ button.classList.remove('loading');
548
+ });
549
+ });
550
+
551
+ // 验证授权响应格式
552
+ function validateAuthResponse(response) {
553
+ try {
554
+ const data = JSON.parse(response);
555
+ if (!data.code || !data.state || !data.tenant_url) {
556
+ return { valid: false, message: '缺少必要字段: code, state 或 tenant_url' };
557
+ }
558
+ return { valid: true };
559
+ } catch (e) {
560
+ return { valid: false, message: 'JSON格式无效: ' + e.message };
561
+ }
562
+ }
563
+
564
+ // 提交授权响应
565
+ document.getElementById('submit-auth').addEventListener('click', function() {
566
+ const button = this;
567
+ const authResponse = document.getElementById('auth-response').value.trim();
568
+ const validationMessage = document.getElementById('validation-message');
569
+
570
+ // 检查是否为空
571
+ if (!authResponse) {
572
+ validationMessage.textContent = '请输入授权响应';
573
+ validationMessage.style.display = 'block';
574
+ return;
575
+ }
576
+
577
+ const validationResult = validateAuthResponse(authResponse);
578
+
579
+ if (!validationResult.valid) {
580
+ validationMessage.textContent = validationResult.message;
581
+ validationMessage.style.display = 'block';
582
+ return;
583
+ }
584
+
585
+ validationMessage.style.display = 'none';
586
+ button.classList.add('loading');
587
+
588
+ fetch('/callback', {
589
+ method: 'POST',
590
+ headers: {
591
+ 'Content-Type': 'application/json'
592
+ },
593
+ body: authResponse
594
+ })
595
+ .then(response => response.json())
596
+ .then(data => {
597
+ const submitResult = document.getElementById('submit-result');
598
+ if (data.status === 'success') {
599
+ submitResult.textContent = 'Token获取成功!';
600
+ submitResult.className = 'success';
601
+ fetchCurrentToken(); // 刷新当前Token显示
602
+ } else {
603
+ submitResult.textContent = '获取失败: ' + (data.error || '未知错误');
604
+ submitResult.className = 'error';
605
+ }
606
+ submitResult.style.display = 'block';
607
+ })
608
+ .catch(error => {
609
+ const submitResult = document.getElementById('submit-result');
610
+ submitResult.textContent = '请求失败: ' + error.message;
611
+ submitResult.className = 'error';
612
+ submitResult.style.display = 'block';
613
+ })
614
+ .finally(() => {
615
+ button.classList.remove('loading');
616
+ });
617
+ });
618
+
619
+ // 刷新Token
620
+ document.getElementById('refresh-token').addEventListener('click', function() {
621
+ const button = this;
622
+ button.classList.add('loading');
623
+
624
+ fetchCurrentToken()
625
+ .finally(() => {
626
+ button.classList.remove('loading');
627
+ });
628
+ });
629
+
630
+ // 绑定删除按钮事件
631
+ document.addEventListener('click', function(e) {
632
+ if (e.target.closest('.delete-token')) {
633
+ const button = e.target.closest('.delete-token');
634
+ const token = button.dataset.token;
635
+
636
+ if (confirm('确定要删除此Token吗?')) {
637
+ fetch(`/api/token/${token}`, {
638
+ method: 'DELETE'
639
+ })
640
+ .then(response => response.json())
641
+ .then(data => {
642
+ if (data.status === 'success') {
643
+ fetchCurrentToken(); // 刷新列表
644
+ } else {
645
+ alert('删除失败: ' + (data.error || '未知错误'));
646
+ }
647
+ })
648
+ .catch(error => {
649
+ alert('请求失败: ' + error.message);
650
+ });
651
+ }
652
+ }
653
+ });
654
+
655
+ // 初始加载
656
+ fetchCurrentToken();
657
+ });
658
+ </script>
659
+ </body>
660
+ </html>