github-actions[bot] commited on
Commit
3392510
·
1 Parent(s): 8210f8c

Update from GitHub Actions

Browse files
.env.example ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Redis 连接配置
2
+ # 格式: redis://default:password@host:port
3
+ # 必填项
4
+ REDIS_CONN_STRING=redis://default:your_secure_password@redis:6379
5
+
6
+
7
+ # 面板访问密码
8
+ # 为了安全,此配置必填!
9
+ ACCESS_PWD=your_secure_access_password
10
+
11
+
12
+ # API 认证令牌
13
+ # 如果设置,则所有 API 请求需要在 Authorization 头中提供此令牌
14
+ # 格式: 随机数字+字母组合即可
15
+ # 可选项,如果不设置则不启用认证
16
+ AUTH_TOKEN=your_secure_auth_token
17
+
18
+
19
+ # API 请求前缀
20
+ # 可自定义,默认为空
21
+ ROUTE_PREFIX=your_api_prefix
22
+
23
+ # 调试模式
24
+ # 设置为 true 时启用更详细的日志输出
25
+ # 可选项,默认为 false
26
+ DEBUG=false
27
+
28
+ # 编码模式配置
29
+ # 仅用于开发和调试
30
+ # 可选项,默认为 false
31
+ CODING_MODE=false
32
+ CODING_TOKEN=
33
+ TENANT_URL=
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.png filter=lfs diff=lfs merge=lfs -text
37
+ *.webp filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 7860
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,1358 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package api
2
+
3
+ import (
4
+ "augment2api/config"
5
+ "augment2api/pkg/logger"
6
+ "bufio"
7
+ "crypto/sha256"
8
+ "encoding/json"
9
+ "fmt"
10
+ "io"
11
+ "log"
12
+ "math/rand"
13
+ "net/http"
14
+ "net/url"
15
+ "strings"
16
+ "sync"
17
+ "time"
18
+
19
+ "github.com/gin-gonic/gin"
20
+ "github.com/google/uuid"
21
+ "github.com/sirupsen/logrus"
22
+ )
23
+
24
+ // OpenAIRequest OpenAI兼容的请求结构
25
+ type OpenAIRequest struct {
26
+ Model string `json:"model,omitempty"`
27
+ Messages []ChatMessage `json:"messages,omitempty"`
28
+ Stream bool `json:"stream,omitempty"`
29
+ Temperature float64 `json:"temperature,omitempty"`
30
+ MaxTokens int `json:"max_tokens,omitempty"`
31
+ }
32
+
33
+ // OpenAIResponse OpenAI兼容的响应结构
34
+ type OpenAIResponse struct {
35
+ ID string `json:"id"`
36
+ Object string `json:"object"`
37
+ Created int64 `json:"created"`
38
+ Model string `json:"model"`
39
+ Choices []Choice `json:"choices"`
40
+ Usage Usage `json:"usage"`
41
+ }
42
+
43
+ // OpenAIStreamResponse OpenAI兼容的流式响应结构
44
+ type OpenAIStreamResponse struct {
45
+ ID string `json:"id"`
46
+ Object string `json:"object"`
47
+ Created int64 `json:"created"`
48
+ Model string `json:"model"`
49
+ Choices []StreamChoice `json:"choices"`
50
+ }
51
+
52
+ type StreamChoice struct {
53
+ Index int `json:"index"`
54
+ Delta ChatMessage `json:"delta"`
55
+ FinishReason *string `json:"finish_reason"`
56
+ }
57
+
58
+ type Choice struct {
59
+ Index int `json:"index"`
60
+ Message ChatMessage `json:"message"`
61
+ FinishReason *string `json:"finish_reason"`
62
+ }
63
+
64
+ type ChatMessage struct {
65
+ Role string `json:"role"`
66
+ Content interface{} `json:"content"`
67
+ }
68
+
69
+ // GetContent 添加一个辅助方法来获取消息内容
70
+ func (m ChatMessage) GetContent() string {
71
+ switch v := m.Content.(type) {
72
+ case string:
73
+ return v
74
+ case []interface{}:
75
+ var result string
76
+ for _, item := range v {
77
+ if contentMap, ok := item.(map[string]interface{}); ok {
78
+ if text, exists := contentMap["text"]; exists {
79
+ if textStr, ok := text.(string); ok {
80
+ result += textStr
81
+ }
82
+ }
83
+ }
84
+ }
85
+ return result
86
+ default:
87
+ return ""
88
+ }
89
+ }
90
+
91
+ type Usage struct {
92
+ PromptTokens int `json:"prompt_tokens"`
93
+ CompletionTokens int `json:"completion_tokens"`
94
+ TotalTokens int `json:"total_tokens"`
95
+ }
96
+
97
+ // ToolDefinition 工具定义结构
98
+ type ToolDefinition struct {
99
+ Name string `json:"name"`
100
+ Description string `json:"description"`
101
+ InputSchemaJSON string `json:"input_schema_json"`
102
+ ToolSafety int `json:"tool_safety"`
103
+ }
104
+
105
+ // Node 节点结构
106
+ type Node struct {
107
+ ID int `json:"id"`
108
+ Type int `json:"type"`
109
+ Content string `json:"content"`
110
+ ToolUse ToolUse `json:"tool_use"`
111
+ AgentMemory AgentMemory `json:"agent_memory"`
112
+ }
113
+
114
+ type ToolUse struct {
115
+ ToolUseID string `json:"tool_use_id"`
116
+ ToolName string `json:"tool_name"`
117
+ InputJSON string `json:"input_json"`
118
+ }
119
+
120
+ type AgentMemory struct {
121
+ Content string `json:"content"`
122
+ }
123
+
124
+ // AugmentRequest Augment API请求结构
125
+ type AugmentRequest struct {
126
+ ChatHistory []AugmentChatHistory `json:"chat_history"`
127
+ Message string `json:"message"`
128
+ AgentMemories string `json:"agent_memories"`
129
+ Mode string `json:"mode"`
130
+ Prefix string `json:"prefix"`
131
+ Suffix string `json:"suffix"`
132
+ Lang string `json:"lang"`
133
+ Path string `json:"path"`
134
+ UserGuideLines string `json:"user_guidelines"`
135
+ Blobs struct {
136
+ CheckpointID string `json:"checkpoint_id"`
137
+ AddedBlobs []interface{} `json:"added_blobs"`
138
+ DeletedBlobs []interface{} `json:"deleted_blobs"`
139
+ } `json:"blobs"`
140
+ UserGuidedBlobs []interface{} `json:"user_guided_blobs"`
141
+ ExternalSourceIds []interface{} `json:"external_source_ids"`
142
+ FeatureDetectionFlags struct {
143
+ SupportRawOutput bool `json:"support_raw_output"`
144
+ } `json:"feature_detection_flags"`
145
+ ToolDefinitions []ToolDefinition `json:"tool_definitions"`
146
+ Nodes []Node `json:"nodes"`
147
+ }
148
+
149
+ type AugmentChatHistory struct {
150
+ ResponseText string `json:"response_text"`
151
+ RequestMessage string `json:"request_message"`
152
+ RequestID string `json:"request_id"`
153
+ RequestNodes []Node `json:"request_nodes"`
154
+ ResponseNodes []Node `json:"response_nodes"`
155
+ }
156
+
157
+ // AugmentResponse Augment API响应结构
158
+ type AugmentResponse struct {
159
+ Text string `json:"text"`
160
+ Done bool `json:"done"`
161
+ }
162
+
163
+ // CodeResponse 用于解析从授权服务返回的代码
164
+ type CodeResponse struct {
165
+ Code string `json:"code"`
166
+ State string `json:"state"`
167
+ TenantURL string `json:"tenant_url"`
168
+ }
169
+
170
+ // ModelObject OpenAI模型对象结构
171
+ type ModelObject struct {
172
+ ID string `json:"id"`
173
+ Object string `json:"object"`
174
+ Created int `json:"created"`
175
+ OwnedBy string `json:"owned_by"`
176
+ }
177
+
178
+ // ModelsResponse OpenAI模型列表响应结构
179
+ type ModelsResponse struct {
180
+ Object string `json:"object"`
181
+ Data []ModelObject `json:"data"`
182
+ }
183
+
184
+ // 全局变量
185
+ var (
186
+ accessToken string
187
+ tenantURL string
188
+ )
189
+
190
+ const (
191
+ // 错误信息
192
+ errBlocked = "Request blocked. Please reach out to support@augmentcode.com if you think this was a mistake."
193
+ )
194
+
195
+ // SetAuthInfo 设置认证信息
196
+ func SetAuthInfo(token, tenant string) {
197
+ accessToken = token
198
+ tenantURL = tenant
199
+ }
200
+
201
+ // GetAuthInfo 获取认证信息
202
+ func GetAuthInfo() (string, string) {
203
+ if config.AppConfig.CodingMode == "true" {
204
+ // 调试模式
205
+ return config.AppConfig.CodingToken, config.AppConfig.TenantURL
206
+ }
207
+
208
+ // 直接返回内存中的token和tenantURL
209
+ return accessToken, tenantURL
210
+ }
211
+
212
+ const (
213
+ // 默认提示,不加这个会导致Agent触发文件创建,回复截断
214
+ defaultPrompt = "Your are claude3.7, All replies cannot create, modify, or delete files, and must provide content directly!"
215
+ // 默认上下文,影响模型回复风格
216
+ defaultPrefix = "You are AI assistant,help me to solve problems!"
217
+ )
218
+
219
+ // generateCheckpointID 生成一个基于时间戳的SHA-256哈希值作为CheckpointID
220
+ func generateCheckpointID() string {
221
+ // 使用当前时间戳作为输入
222
+ timestamp := fmt.Sprintf("%d", time.Now().UnixNano())
223
+ hash := sha256.New()
224
+ hash.Write([]byte(timestamp))
225
+ // 将哈希值转换为十六进制字符串
226
+ return fmt.Sprintf("%x", hash.Sum(nil))
227
+ }
228
+
229
+ // generatePath 生成一个随机文件路径(暂时无用)
230
+ func generatePath() string {
231
+ extensions := []string{".txt", ".md", ".go", ".py", ".js", ".html", ".css"}
232
+ dirs := []string{"src", "docs", "test", "lib", "utils"}
233
+ dir := dirs[rand.Intn(len(dirs))]
234
+ ext := extensions[rand.Intn(len(extensions))]
235
+ filename := fmt.Sprintf("%x", rand.Int31())
236
+ return fmt.Sprintf("%s/%s%s", dir, filename, ext)
237
+ }
238
+
239
+ // convertToAugmentRequest 将OpenAI请求转换为Augment请求
240
+ func convertToAugmentRequest(req OpenAIRequest) AugmentRequest {
241
+ // 确定模式和其他参数基于模型名称
242
+ mode := "CHAT" // 默认使用CHAT模式
243
+ userGuideLines := "must answer in Chinese."
244
+ includeToolDefinitions := false
245
+ includeDefaultPrompt := false
246
+
247
+ // 将模型名称转换为小写,然后检查后缀
248
+ modelLower := strings.ToLower(req.Model)
249
+
250
+ // 检查模型名称后缀 (不区分大小写)
251
+ if strings.HasSuffix(modelLower, "-chat") {
252
+ // 保持使用CHAT模式的默认设置
253
+ mode = "CHAT"
254
+ } else if strings.HasSuffix(modelLower, "-agent") {
255
+ // 使用AGENT模式
256
+ mode = "AGENT"
257
+ userGuideLines = "Answer in Chinese, do not use any tools, and for questions involving internet searches, please answer based on your existing knowledge."
258
+ includeToolDefinitions = true
259
+ includeDefaultPrompt = true
260
+ }
261
+
262
+ augmentReq := AugmentRequest{
263
+ Path: "", // 这个是关联的项目文件路径,暂时传空,不影响对话
264
+ Mode: mode, // 根据模型名称决定模式
265
+ Prefix: defaultPrefix, // 固定前缀,影响模型回复风格
266
+ Suffix: " ", // 固定后缀,暂时传空,不影响对话
267
+ Lang: detectLanguage(req), // 简单检测当前对话语言类型,不传好像回答有问题
268
+ Message: "", // 当前对话消息
269
+ UserGuideLines: userGuideLines, // 根据模型类型设置指南
270
+ // 初始化为空列表
271
+ ChatHistory: make([]AugmentChatHistory, 0),
272
+ Blobs: struct {
273
+ CheckpointID string `json:"checkpoint_id"`
274
+ AddedBlobs []interface{} `json:"added_blobs"`
275
+ DeletedBlobs []interface{} `json:"deleted_blobs"`
276
+ }{
277
+ CheckpointID: generateCheckpointID(),
278
+ AddedBlobs: make([]interface{}, 0),
279
+ DeletedBlobs: make([]interface{}, 0),
280
+ },
281
+ UserGuidedBlobs: make([]interface{}, 0),
282
+ ExternalSourceIds: make([]interface{}, 0),
283
+ FeatureDetectionFlags: struct {
284
+ SupportRawOutput bool `json:"support_raw_output"`
285
+ }{
286
+ SupportRawOutput: true,
287
+ },
288
+ ToolDefinitions: []ToolDefinition{}, // 初始化为空
289
+ Nodes: make([]Node, 0),
290
+ }
291
+
292
+ // 根据模型类型决定是否包含工具定义
293
+ if includeToolDefinitions {
294
+ augmentReq.ToolDefinitions = getFullToolDefinitions()
295
+ }
296
+
297
+ // 处理消息历史
298
+ if len(req.Messages) > 1 { // 有历史消息
299
+ // 每次处理一对消息(用户问题和助手回答)
300
+ for i := 0; i < len(req.Messages)-1; i += 2 {
301
+ if i+1 < len(req.Messages) {
302
+ userMsg := req.Messages[i]
303
+ assistantMsg := req.Messages[i+1]
304
+
305
+ chatHistory := AugmentChatHistory{
306
+ RequestMessage: userMsg.GetContent(),
307
+ ResponseText: assistantMsg.GetContent(),
308
+ RequestID: generateRequestID(), // 生成唯一的请求ID
309
+ RequestNodes: make([]Node, 0),
310
+ ResponseNodes: []Node{
311
+ {
312
+ ID: 0,
313
+ Type: 0,
314
+ Content: assistantMsg.GetContent(),
315
+ ToolUse: ToolUse{
316
+ ToolUseID: "",
317
+ ToolName: "",
318
+ InputJSON: "",
319
+ },
320
+ AgentMemory: AgentMemory{
321
+ Content: "",
322
+ },
323
+ },
324
+ },
325
+ }
326
+ augmentReq.ChatHistory = append(augmentReq.ChatHistory, chatHistory)
327
+ }
328
+ }
329
+ }
330
+
331
+ // 设置当前消息
332
+ if len(req.Messages) > 0 {
333
+ lastMsg := req.Messages[len(req.Messages)-1]
334
+ if includeDefaultPrompt {
335
+ augmentReq.Message = defaultPrompt + "\n" + lastMsg.GetContent()
336
+ } else {
337
+ augmentReq.Message = lastMsg.GetContent()
338
+ }
339
+ }
340
+
341
+ return augmentReq
342
+ }
343
+
344
+ // generateRequestID 生成唯一的请求ID
345
+ func generateRequestID() string {
346
+ // 使用UUID v4生成唯一ID
347
+ return uuid.New().String()
348
+ }
349
+
350
+ // detectLanguage 检测编程语言
351
+ func detectLanguage(req OpenAIRequest) string {
352
+ if len(req.Messages) == 0 {
353
+ return ""
354
+ }
355
+
356
+ content := req.Messages[len(req.Messages)-1].GetContent()
357
+ // 简单判断一下当前对话语言类型
358
+ if strings.Contains(strings.ToLower(content), "html") {
359
+ return "HTML"
360
+ } else if strings.Contains(strings.ToLower(content), "python") {
361
+ return "Python"
362
+ } else if strings.Contains(strings.ToLower(content), "javascript") {
363
+ return "JavaScript"
364
+ } else if strings.Contains(strings.ToLower(content), "go") {
365
+ return "Go"
366
+ } else if strings.Contains(strings.ToLower(content), "rust") {
367
+ return "Rust"
368
+ } else if strings.Contains(strings.ToLower(content), "java") {
369
+ return "Java"
370
+ } else if strings.Contains(strings.ToLower(content), "c++") {
371
+ return "C++"
372
+ } else if strings.Contains(strings.ToLower(content), "c#") {
373
+ return "C#"
374
+ } else if strings.Contains(strings.ToLower(content), "php") {
375
+ return "PHP"
376
+ } else if strings.Contains(strings.ToLower(content), "ruby") {
377
+ return "Ruby"
378
+ } else if strings.Contains(strings.ToLower(content), "swift") {
379
+ return "Swift"
380
+ } else if strings.Contains(strings.ToLower(content), "kotlin") {
381
+ return "Kotlin"
382
+ } else if strings.Contains(strings.ToLower(content), "typescript") {
383
+ return "TypeScript"
384
+ } else if strings.Contains(strings.ToLower(content), "c") {
385
+ return "C"
386
+ }
387
+ return "HTML"
388
+ }
389
+
390
+ // getFullToolDefinitions 返回官方定义的完整工具定义列表
391
+ // TODO 验证实际作用
392
+ func getFullToolDefinitions() []ToolDefinition {
393
+ return []ToolDefinition{
394
+ {
395
+ Name: "web-search",
396
+ Description: "Search the web for information. Returns results in markdown format.\nEach result includes the URL, title, and a snippet from the page if available.\n\nThis tool uses Google's Custom Search API to find relevant web pages.",
397
+ InputSchemaJSON: `{
398
+ "description": "Input schema for the web search tool.",
399
+ "properties": {
400
+ "query": {
401
+ "description": "The search query to send.",
402
+ "title": "Query",
403
+ "type": "string"
404
+ },
405
+ "num_results": {
406
+ "default": 5,
407
+ "description": "Number of results to return",
408
+ "maximum": 10,
409
+ "minimum": 1,
410
+ "title": "Num Results",
411
+ "type": "integer"
412
+ }
413
+ },
414
+ "required": ["query"],
415
+ "title": "WebSearchInput",
416
+ "type": "object"
417
+ }`,
418
+ ToolSafety: 0,
419
+ },
420
+ {
421
+ Name: "web-fetch",
422
+ Description: "Fetches data from a webpage and converts it into Markdown.\n\n1. The tool takes in a URL and returns the content of the page in Markdown format;\n2. If the return is not valid Markdown, it means the tool cannot successfully parse this page.",
423
+ InputSchemaJSON: `{
424
+ "type": "object",
425
+ "properties": {
426
+ "url": {
427
+ "type": "string",
428
+ "description": "The URL to fetch."
429
+ }
430
+ },
431
+ "required": ["url"]
432
+ }`,
433
+ ToolSafety: 0,
434
+ },
435
+ {
436
+ Name: "codebase-retrieval",
437
+ Description: "This tool is Augment's context engine, the world's best codebase context engine. It:\n1. Takes in a natural language description of the code you are looking for;\n2. Uses a proprietary retrieval/embedding model suite that produces the highest-quality recall of relevant code snippets from across the codebase;\n3. Maintains a real-time index of the codebase, so the results are always up-to-date and reflects the current state of the codebase;\n4. Can retrieve across different programming languages;\n5. Only reflects the current state of the codebase on the disk, and has no information on version control or code history.",
438
+ InputSchemaJSON: `{
439
+ "type": "object",
440
+ "properties": {
441
+ "information_request": {
442
+ "type": "string",
443
+ "description": "A description of the information you need."
444
+ }
445
+ },
446
+ "required": ["information_request"]
447
+ }`,
448
+ ToolSafety: 1,
449
+ },
450
+ {
451
+ Name: "shell",
452
+ Description: "Execute a shell command.\n\n- You can use this tool to interact with the user's local version control system. Do not use the\nretrieval tool for that purpose.\n- If there is a more specific tool available that can perform the function, use that tool instead of\nthis one.\n\nThe OS is darwin. The shell is 'bash'.",
453
+ InputSchemaJSON: `{
454
+ "type": "object",
455
+ "properties": {
456
+ "command": {
457
+ "type": "string",
458
+ "description": "The shell command to execute."
459
+ }
460
+ },
461
+ "required": ["command"]
462
+ }`,
463
+ ToolSafety: 2,
464
+ },
465
+ {
466
+ Name: "str-replace-editor",
467
+ Description: "Custom editing tool for viewing, creating and editing files\n* `path` is a file path relative to the workspace root\n* command `view` displays the result of applying `cat -n`.\n* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`\n* `insert` and `str_replace` commands output a snippet of the edited section for each entry. This snippet reflects the final state of the file after all edits and IDE auto-formatting have been applied.\n\n\nNotes for using the `str_replace` command:\n* Use the `str_replace_entries` parameter with an array of objects\n* Each object should have `old_str`, `new_str`, `old_str_start_line_number` and `old_str_end_line_number` properties\n* The `old_str_start_line_number` and `old_str_end_line_number` parameters are 1-based line numbers\n* Both `old_str_start_line_number` and `old_str_end_line_number` are INCLUSIVE\n* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespace!\n* Empty `old_str` is allowed only when the file is empty or contains only whitespaces\n* It is important to specify `old_str_start_line_number` and `old_str_end_line_number` to disambiguate between multiple occurrences of `old_str` in the file\n* Make sure that `old_str_start_line_number` and `old_str_end_line_number` do not overlap with other entries in `str_replace_entries`\n* The `new_str` parameter should contain the edited lines that should replace the `old_str`. Can be an empty string to delete content\n\nNotes for using the `insert` command:\n* Use the `insert_line_entries` parameter with an array of objects\n* Each object should have `insert_line` and `new_str` properties\n* The `insert_line` parameter specifies the line number after which to insert the new string\n* The `insert_line` parameter is 1-based line number\n* To insert at the very beginning of the file, use `insert_line: 0`\n\nNotes for using the `view` command:\n* Strongly prefer to use larger ranges of at least 1000 lines when scanning through files. One call with large range is much more efficient than many calls with small ranges\n* Prefer to use grep instead of view when looking for a specific symbol in the file\n\nIMPORTANT:\n* This is the only tool you should use for editing files.\n* If it fails try your best to fix inputs and retry.\n* DO NOT fall back to removing the whole file and recreating it from scratch.\n* DO NOT use sed or any other command line tools for editing files.\n* Try to fit as many edits in one tool call as possible\n* Use view command to read the file before editing it.\n",
468
+ InputSchemaJSON: `{
469
+ "type": "object",
470
+ "properties": {
471
+ "command": {
472
+ "type": "string",
473
+ "enum": ["view", "str_replace", "insert"],
474
+ "description": "The commands to run. Allowed options are: 'view', 'str_replace', 'insert'."
475
+ },
476
+ "path": {
477
+ "description": "Full path to file relative to the workspace root, e.g. 'services/api_proxy/file.py' or 'services/api_proxy'.",
478
+ "type": "string"
479
+ },
480
+ "view_range": {
481
+ "description": "Optional parameter of 'view' command when 'path' points to a file. If none is given, the full file is shown. If provided, the file will be shown in the indicated line number range, e.g. [11, 12] will show lines 11 and 12. Indexing at 1 to start. Setting '[start_line, -1]' shows all lines from 'start_line' to the end of the file.",
482
+ "type": "array",
483
+ "items": {
484
+ "type": "integer"
485
+ }
486
+ },
487
+ "insert_line_entries": {
488
+ "description": "Required parameter of 'insert' command. A list of entries to insert. Each entry is a dictionary with keys 'insert_line' and 'new_str'.",
489
+ "type": "array",
490
+ "items": {
491
+ "type": "object",
492
+ "properties": {
493
+ "insert_line": {
494
+ "description": "The line number after which to insert the new string. This line number is relative to the state of the file before any insertions in the current tool call have been applied.",
495
+ "type": "integer"
496
+ },
497
+ "new_str": {
498
+ "description": "The string to insert. Can be an empty string.",
499
+ "type": "string"
500
+ }
501
+ },
502
+ "required": ["insert_line", "new_str"]
503
+ }
504
+ },
505
+ "str_replace_entries": {
506
+ "description": "Required parameter of 'str_replace' command. A list of entries to replace. Each entry is a dictionary with keys 'old_str', 'old_str_start_line_number', 'old_str_end_line_number' and 'new_str'. 'old_str' from different entries should not overlap.",
507
+ "type": "array",
508
+ "items": {
509
+ "type": "object",
510
+ "properties": {
511
+ "old_str": {
512
+ "description": "The string in 'path' to replace.",
513
+ "type": "string"
514
+ },
515
+ "old_str_start_line_number": {
516
+ "description": "The line number of the first line of 'old_str' in the file. This is used to disambiguate between multiple occurrences of 'old_str' in the file.",
517
+ "type": "integer"
518
+ },
519
+ "old_str_end_line_number": {
520
+ "description": "The line number of the last line of 'old_str' in the file. This is used to disambiguate between multiple occurrences of 'old_str' in the file.",
521
+ "type": "integer"
522
+ },
523
+ "new_str": {
524
+ "description": "The string to replace 'old_str' with. Can be an empty string to delete content.",
525
+ "type": "string"
526
+ }
527
+ },
528
+ "required": ["old_str", "new_str", "old_str_start_line_number", "old_str_end_line_number"]
529
+ }
530
+ }
531
+ },
532
+ "required": ["command", "path"]
533
+ }`,
534
+ ToolSafety: 1,
535
+ },
536
+ {
537
+ Name: "save-file",
538
+ Description: "Save a file.",
539
+ InputSchemaJSON: `{
540
+ "type": "object",
541
+ "properties": {
542
+ "file_path": {
543
+ "type": "string",
544
+ "description": "The path of the file to save."
545
+ },
546
+ "file_content": {
547
+ "type": "string",
548
+ "description": "The content of the file to save."
549
+ },
550
+ "add_last_line_newline": {
551
+ "type": "boolean",
552
+ "description": "Whether to add a newline at the end of the file (default: true)."
553
+ }
554
+ },
555
+ "required": ["file_path", "file_content"]
556
+ }`,
557
+ ToolSafety: 1,
558
+ },
559
+ {
560
+ Name: "launch-process",
561
+ Description: "Launch a new process.\nIf wait is specified, waits up to that many seconds for the process to complete.\nIf the process completes within wait seconds, returns its output.\nIf it doesn't complete within wait seconds, returns partial output and process ID.\nIf wait is not specified, returns immediately with just the process ID.\nThe process's stdin is always enbled, so you can use write_process to send input if needed.",
562
+ InputSchemaJSON: `{
563
+ "type": "object",
564
+ "properties": {
565
+ "command": {
566
+ "type": "string",
567
+ "description": "The shell command to execute"
568
+ },
569
+ "wait": {
570
+ "type": "number",
571
+ "description": "Optional: number of seconds to wait for the command to complete."
572
+ },
573
+ "cwd": {
574
+ "type": "string",
575
+ "description": "Working directory for the command. If not supplied, uses the current working directory."
576
+ }
577
+ },
578
+ "required": ["command"]
579
+ }`,
580
+ ToolSafety: 2,
581
+ },
582
+ {
583
+ Name: "read-process",
584
+ Description: "Read output from a terminal.",
585
+ InputSchemaJSON: `{
586
+ "type": "object",
587
+ "properties": {
588
+ "terminal_id": {
589
+ "type": "number",
590
+ "description": "Terminal ID to read from."
591
+ }
592
+ },
593
+ "required": ["terminal_id"]
594
+ }`,
595
+ ToolSafety: 1,
596
+ },
597
+ {
598
+ Name: "kill-process",
599
+ Description: "Kill a process by its terminal ID.",
600
+ InputSchemaJSON: `{
601
+ "type": "object",
602
+ "properties": {
603
+ "terminal_id": {
604
+ "type": "number",
605
+ "description": "Terminal ID to kill."
606
+ }
607
+ },
608
+ "required": ["terminal_id"]
609
+ }`,
610
+ ToolSafety: 1,
611
+ },
612
+ }
613
+ }
614
+
615
+ // AuthHandler 处理授权请求
616
+ func AuthHandler(c *gin.Context, authorizeURL string) {
617
+ c.JSON(http.StatusOK, gin.H{
618
+ "authorize_url": authorizeURL,
619
+ })
620
+ }
621
+
622
+ // CallbackHandler 处理回调请求
623
+ func CallbackHandler(c *gin.Context, getAccessTokenFunc func(string, string, string) (string, error)) {
624
+ // 1. 解析请求数据
625
+ var codeResp CodeResponse
626
+ if err := c.ShouldBindJSON(&codeResp); err != nil {
627
+ c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求数据"})
628
+ return
629
+ }
630
+
631
+ // 2. 使用授权码获取访问令牌
632
+ token, err := getAccessTokenFunc(codeResp.TenantURL, "", codeResp.Code)
633
+ if err != nil {
634
+ c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
635
+ return
636
+ }
637
+
638
+ // 3. 保存令牌和租户URL
639
+ SetAuthInfo(token, codeResp.TenantURL)
640
+
641
+ // 4. 保存到Redis
642
+ if err := SaveTokenToRedis(token, codeResp.TenantURL); err != nil {
643
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "保存token到Redis失败: " + err.Error()})
644
+ return
645
+ }
646
+
647
+ // 5. 返回成功响应
648
+ c.JSON(http.StatusOK, gin.H{
649
+ "status": "success",
650
+ "token": token,
651
+ })
652
+ }
653
+
654
+ // ModelsHandler 处理模型请求
655
+ func ModelsHandler(c *gin.Context) {
656
+ // 这里直接返回写死的模型
657
+ response := ModelsResponse{
658
+ Object: "list",
659
+ Data: []ModelObject{
660
+ {
661
+ ID: "claude-3.7-agent",
662
+ Object: "model",
663
+ Created: 1708387200,
664
+ OwnedBy: "anthropic",
665
+ },
666
+ {
667
+ ID: "augment-chat",
668
+ Object: "model",
669
+ Created: 1708387200,
670
+ OwnedBy: "augment",
671
+ },
672
+ },
673
+ }
674
+
675
+ c.JSON(http.StatusOK, response)
676
+ }
677
+
678
+ // ChatCompletionsHandler 处理OpenAI兼容的聊天完成请求
679
+ func ChatCompletionsHandler(c *gin.Context) {
680
+ // 获取请求数据
681
+ var req OpenAIRequest
682
+ if err := c.ShouldBindJSON(&req); err != nil {
683
+ c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求数据"})
684
+ // 确保在错误情况下也清理请求状态
685
+ cleanupRequestStatus(c)
686
+ return
687
+ }
688
+
689
+ // 转换为Augment请求格式
690
+ augmentReq := convertToAugmentRequest(req)
691
+
692
+ // 处理流式请求
693
+ if req.Stream {
694
+ handleStreamRequest(c, augmentReq, req.Model)
695
+ return
696
+ }
697
+
698
+ // 处理非流式请���
699
+ handleNonStreamRequest(c, augmentReq, req.Model)
700
+ }
701
+
702
+ // 处理流式请求
703
+ func handleStreamRequest(c *gin.Context, augmentReq AugmentRequest, model string) {
704
+ defer cleanupRequestStatus(c)
705
+
706
+ c.Header("Content-Type", "text/event-stream")
707
+ c.Header("Cache-Control", "no-cache")
708
+ c.Header("Connection", "keep-alive")
709
+
710
+ // 从上下文中获取token和tenant_url
711
+ tokenInterface, exists := c.Get("token")
712
+ tenantURLInterface, exists2 := c.Get("tenant_url")
713
+
714
+ var token, tenant string
715
+
716
+ if exists && exists2 {
717
+ token, _ = tokenInterface.(string)
718
+ tenant, _ = tenantURLInterface.(string)
719
+ }
720
+
721
+ // 如果上下文中没有,则使用GetAuthInfo获取
722
+ if token == "" || tenant == "" {
723
+ token, tenant = GetAuthInfo()
724
+ }
725
+
726
+ if token == "" || tenant == "" {
727
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "无可用Token,请先在管理页面获取"})
728
+ return
729
+ }
730
+
731
+ // 增加token使用计数
732
+ incrementTokenUsage(token, model)
733
+
734
+ // 准备请求数据
735
+ jsonData, err := json.Marshal(augmentReq)
736
+ if err != nil {
737
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "序列化请求失败"})
738
+ return
739
+ }
740
+
741
+ // 创建请求
742
+ req, err := http.NewRequest("POST", tenant+"chat-stream", strings.NewReader(string(jsonData)))
743
+ if err != nil {
744
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "创建请求失败"})
745
+ return
746
+ }
747
+
748
+ req.Header.Set("Content-Type", "application/json")
749
+ req.Header.Set("Authorization", "Bearer "+token)
750
+ req.Header.Set("User-Agent", "augment.intellij/0.160.0 (Mac OS X; aarch64; 15.2) WebStorm/2024.3.5")
751
+ req.Header.Set("x-api-version", "2")
752
+ req.Header.Set("x-request-id", uuid.New().String())
753
+ req.Header.Set("x-request-session-id", uuid.New().String())
754
+
755
+ // 使用createHTTPClient创建客户端
756
+ client := createHTTPClient()
757
+
758
+ // 设置刷新器以确保数据立即发送
759
+ flusher, ok := c.Writer.(http.Flusher)
760
+ if !ok {
761
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "流式传输不支持"})
762
+ return
763
+ }
764
+
765
+ // 第一次尝试使用原始模式请求
766
+ resp, err := client.Do(req)
767
+ if err != nil {
768
+ logger.Log.WithFields(logrus.Fields{
769
+ "error": err.Error(),
770
+ "mode": augmentReq.Mode,
771
+ }).Error("请求失败")
772
+
773
+ // 切换到CHAT模式
774
+ augmentReq.Mode = "CHAT"
775
+ augmentReq.UserGuideLines = "使用中文回答"
776
+ augmentReq.ToolDefinitions = []ToolDefinition{}
777
+
778
+ // 重新准备请求数据
779
+ jsonData, err = json.Marshal(augmentReq)
780
+ if err != nil {
781
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "序列化请求失败"})
782
+ return
783
+ }
784
+
785
+ // 创建新的请求
786
+ req, err = http.NewRequest("POST", tenant+"chat-stream", strings.NewReader(string(jsonData)))
787
+ if err != nil {
788
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "创建请求失败"})
789
+ return
790
+ }
791
+
792
+ // 设置相同的请求头
793
+ req.Header.Set("Content-Type", "application/json")
794
+ req.Header.Set("Authorization", "Bearer "+token)
795
+ req.Header.Set("User-Agent", "augment.intellij/0.160.0 (Mac OS X; aarch64; 15.2) WebStorm/2024.3.5")
796
+ req.Header.Set("x-api-version", "2")
797
+ req.Header.Set("x-request-id", uuid.New().String())
798
+ req.Header.Set("x-request-session-id", uuid.New().String())
799
+
800
+ // 重新发送请求
801
+ resp, err = client.Do(req)
802
+ if err != nil {
803
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "请求失败: " + err.Error()})
804
+ return
805
+ }
806
+ }
807
+ defer resp.Body.Close()
808
+
809
+ // 检查响应状态码
810
+ if resp.StatusCode != http.StatusOK {
811
+ body, err := io.ReadAll(resp.Body)
812
+ errMsg := "Augment response error"
813
+ if err == nil {
814
+ errMsg = errMsg + ": " + string(body)
815
+ }
816
+ c.JSON(resp.StatusCode, gin.H{"error": errMsg})
817
+ return
818
+ }
819
+
820
+ // 读取并转发响应
821
+ reader := bufio.NewReader(resp.Body)
822
+ responseID := fmt.Sprintf("chatcmpl-%d", time.Now().Unix())
823
+
824
+ var fullText string
825
+ var hasError bool
826
+
827
+ for {
828
+ line, err := reader.ReadString('\n')
829
+ if err != nil {
830
+ if err == io.EOF {
831
+ break
832
+ }
833
+ logger.Log.WithFields(logrus.Fields{
834
+ "error": err.Error(),
835
+ "mode": augmentReq.Mode,
836
+ }).Error("读取响应失败")
837
+
838
+ // 切换到CHAT模式
839
+ if augmentReq.Mode != "CHAT" {
840
+ logger.Log.WithFields(logrus.Fields{
841
+ "error": err.Error(),
842
+ "mode": augmentReq.Mode,
843
+ }).Info("切换到CHAT模式")
844
+
845
+ augmentReq.Mode = "CHAT"
846
+ augmentReq.UserGuideLines = "使用中文回答"
847
+ augmentReq.ToolDefinitions = []ToolDefinition{}
848
+
849
+ // 重新准备请求数据
850
+ jsonData, err = json.Marshal(augmentReq)
851
+ if err != nil {
852
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "序列化请求失败"})
853
+ return
854
+ }
855
+
856
+ // 创建新的请求
857
+ req, err = http.NewRequest("POST", tenant+"chat-stream", strings.NewReader(string(jsonData)))
858
+ if err != nil {
859
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "创建请求失败"})
860
+ return
861
+ }
862
+
863
+ // 设置相同的请求头
864
+ req.Header.Set("Content-Type", "application/json")
865
+ req.Header.Set("Authorization", "Bearer "+token)
866
+ req.Header.Set("User-Agent", "augment.intellij/0.160.0 (Mac OS X; aarch64; 15.2) WebStorm/2024.3.5")
867
+ req.Header.Set("x-api-version", "2")
868
+ req.Header.Set("x-request-id", uuid.New().String())
869
+ req.Header.Set("x-request-session-id", uuid.New().String())
870
+
871
+ // 重新发送请求
872
+ resp, err = client.Do(req)
873
+ if err != nil {
874
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "请求失败: " + err.Error()})
875
+ return
876
+ }
877
+ defer resp.Body.Close()
878
+
879
+ // 检查响应状态码
880
+ if resp.StatusCode != http.StatusOK {
881
+ body, err := io.ReadAll(resp.Body)
882
+ errMsg := "Augment response error"
883
+ if err == nil {
884
+ errMsg = errMsg + ": " + string(body)
885
+ }
886
+ c.JSON(resp.StatusCode, gin.H{"error": errMsg})
887
+ return
888
+ }
889
+
890
+ // 重新设置reader
891
+ reader = bufio.NewReader(resp.Body)
892
+ continue
893
+ }
894
+ break
895
+ }
896
+
897
+ line = strings.TrimSpace(line)
898
+ if line == "" {
899
+ continue
900
+ }
901
+
902
+ var augmentResp AugmentResponse
903
+ if err := json.Unmarshal([]byte(line), &augmentResp); err != nil {
904
+ log.Printf("解析响应失败: %v", err)
905
+ continue
906
+ }
907
+
908
+ // 检查响应内容是否包含错误信息
909
+ if strings.Contains(augmentResp.Text, errBlocked) {
910
+ hasError = true
911
+
912
+ // 将当前token加入冷却队列,冷却时间10分钟
913
+ logger.Log.WithFields(logrus.Fields{
914
+ "token": token,
915
+ "mode": augmentReq.Mode,
916
+ }).Info("检测到block信息,将token加入冷却队列10分钟")
917
+
918
+ err := SetTokenCoolStatus(token, 10*time.Minute)
919
+ if err != nil {
920
+ logger.Log.WithFields(logrus.Fields{
921
+ "token": token,
922
+ "error": err.Error(),
923
+ }).Error("将token加入冷却队列失败")
924
+ }
925
+
926
+ break
927
+ }
928
+
929
+ fullText += augmentResp.Text
930
+
931
+ // 创建OpenAI兼容的流式响应
932
+ streamResp := OpenAIStreamResponse{
933
+ ID: responseID,
934
+ Object: "chat.completion.chunk",
935
+ Created: time.Now().Unix(),
936
+ Model: model,
937
+ Choices: []StreamChoice{
938
+ {
939
+ Index: 0,
940
+ Delta: ChatMessage{
941
+ Role: "assistant",
942
+ Content: augmentResp.Text,
943
+ },
944
+ FinishReason: nil,
945
+ },
946
+ },
947
+ }
948
+
949
+ // 如果是最后一条消息,设置完成原因
950
+ if augmentResp.Done {
951
+ finishReason := "stop"
952
+ streamResp.Choices[0].FinishReason = &finishReason
953
+ }
954
+
955
+ // 序列化并发送响应
956
+ jsonResp, err := json.Marshal(streamResp)
957
+ if err != nil {
958
+ log.Printf("序列化响应失败: %v", err)
959
+ continue
960
+ }
961
+
962
+ fmt.Fprintf(c.Writer, "data: %s\n\n", jsonResp)
963
+ flusher.Flush()
964
+
965
+ // 如果完成,发送最后的[DONE]标记
966
+ if augmentResp.Done {
967
+ fmt.Fprintf(c.Writer, "data: [DONE]\n\n")
968
+ flusher.Flush()
969
+ break
970
+ }
971
+ }
972
+
973
+ // 如果检测到错误信息,尝试切换到CHAT模式重新请求
974
+ if hasError && augmentReq.Mode != "CHAT" {
975
+ logger.Log.WithFields(logrus.Fields{
976
+ "mode": augmentReq.Mode,
977
+ }).Info("检测到block信息,尝试切换到 CHAT 模式回复!")
978
+
979
+ // 切换到CHAT模式
980
+ augmentReq.Mode = "CHAT"
981
+ augmentReq.UserGuideLines = "使用中文回答"
982
+ augmentReq.ToolDefinitions = []ToolDefinition{}
983
+
984
+ // 重新准备请求数据
985
+ jsonData, err = json.Marshal(augmentReq)
986
+ if err != nil {
987
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "序列化请求失败"})
988
+ return
989
+ }
990
+
991
+ // 创建新的请求
992
+ req, err = http.NewRequest("POST", tenant+"chat-stream", strings.NewReader(string(jsonData)))
993
+ if err != nil {
994
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "创建请求失败"})
995
+ return
996
+ }
997
+
998
+ // 设置相同的请求头
999
+ req.Header.Set("Content-Type", "application/json")
1000
+ req.Header.Set("Authorization", "Bearer "+token)
1001
+ req.Header.Set("User-Agent", "augment.intellij/0.160.0 (Mac OS X; aarch64; 15.2) WebStorm/2024.3.5")
1002
+ req.Header.Set("x-api-version", "2")
1003
+ req.Header.Set("x-request-id", uuid.New().String())
1004
+ req.Header.Set("x-request-session-id", uuid.New().String())
1005
+
1006
+ // 重新发送请求
1007
+ resp, err = client.Do(req)
1008
+ if err != nil {
1009
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "请求失败: " + err.Error()})
1010
+ return
1011
+ }
1012
+ defer resp.Body.Close()
1013
+
1014
+ // 检查响应状态码
1015
+ if resp.StatusCode != http.StatusOK {
1016
+ body, err := io.ReadAll(resp.Body)
1017
+ errMsg := "Augment response error"
1018
+ if err == nil {
1019
+ errMsg = errMsg + ": " + string(body)
1020
+ }
1021
+ c.JSON(resp.StatusCode, gin.H{"error": errMsg})
1022
+ return
1023
+ }
1024
+
1025
+ // 读取并转发响应
1026
+ reader = bufio.NewReader(resp.Body)
1027
+ responseID = fmt.Sprintf("chatcmpl-%d", time.Now().Unix())
1028
+
1029
+ fullText = ""
1030
+ for {
1031
+ line, err := reader.ReadString('\n')
1032
+ if err != nil {
1033
+ if err == io.EOF {
1034
+ break
1035
+ }
1036
+ log.Printf("读取响应失败: %v", err)
1037
+ break
1038
+ }
1039
+
1040
+ line = strings.TrimSpace(line)
1041
+ if line == "" {
1042
+ continue
1043
+ }
1044
+
1045
+ var augmentResp AugmentResponse
1046
+ if err := json.Unmarshal([]byte(line), &augmentResp); err != nil {
1047
+ log.Printf("解析响应失败: %v", err)
1048
+ continue
1049
+ }
1050
+
1051
+ fullText += augmentResp.Text
1052
+
1053
+ // 创建OpenAI兼容的流式响应
1054
+ streamResp := OpenAIStreamResponse{
1055
+ ID: responseID,
1056
+ Object: "chat.completion.chunk",
1057
+ Created: time.Now().Unix(),
1058
+ Model: model,
1059
+ Choices: []StreamChoice{
1060
+ {
1061
+ Index: 0,
1062
+ Delta: ChatMessage{
1063
+ Role: "assistant",
1064
+ Content: augmentResp.Text,
1065
+ },
1066
+ FinishReason: nil,
1067
+ },
1068
+ },
1069
+ }
1070
+
1071
+ // 如果是最后一条消息,设置完成原因
1072
+ if augmentResp.Done {
1073
+ finishReason := "stop"
1074
+ streamResp.Choices[0].FinishReason = &finishReason
1075
+ }
1076
+
1077
+ // 序列化并发送响应
1078
+ jsonResp, err := json.Marshal(streamResp)
1079
+ if err != nil {
1080
+ log.Printf("序列化响应失败: %v", err)
1081
+ continue
1082
+ }
1083
+
1084
+ fmt.Fprintf(c.Writer, "data: %s\n\n", jsonResp)
1085
+ flusher.Flush()
1086
+
1087
+ // 如果完成,发送最后的[DONE]标记
1088
+ if augmentResp.Done {
1089
+ fmt.Fprintf(c.Writer, "data: [DONE]\n\n")
1090
+ flusher.Flush()
1091
+ break
1092
+ }
1093
+ }
1094
+ }
1095
+ }
1096
+
1097
+ // estimateTokenCount 粗略估计文本中的token数量
1098
+ // 这是一个简单的估算方法,实际token数量取决于具体的分词算法
1099
+ func estimateTokenCount(text string) int {
1100
+ // 英文单词和标点符号大约是1个token
1101
+ // 中文字符大约是1.5个token(每个字符约为0.75个token)
1102
+ // 按空格分割英文单词
1103
+ words := strings.Fields(text)
1104
+ wordCount := len(words)
1105
+
1106
+ // 计算中文字符数量
1107
+ chineseCount := 0
1108
+ for _, r := range text {
1109
+ if r >= 0x4E00 && r <= 0x9FFF {
1110
+ chineseCount++
1111
+ }
1112
+ }
1113
+
1114
+ // 粗略估计:英文单词按1个token计算,中文字符按0.75个token计算
1115
+ return wordCount + int(float64(chineseCount)*0.75)
1116
+ }
1117
+
1118
+ // 处理非流式请求
1119
+ func handleNonStreamRequest(c *gin.Context, augmentReq AugmentRequest, model string) {
1120
+ defer cleanupRequestStatus(c)
1121
+
1122
+ // 从上下文中获取token和tenant_url
1123
+ tokenInterface, exists := c.Get("token")
1124
+ tenantURLInterface, exists2 := c.Get("tenant_url")
1125
+
1126
+ var token, tenant string
1127
+
1128
+ if exists && exists2 {
1129
+ token, _ = tokenInterface.(string)
1130
+ tenant, _ = tenantURLInterface.(string)
1131
+ }
1132
+
1133
+ // 如果上下文中没有,则使用GetAuthInfo获取
1134
+ if token == "" || tenant == "" {
1135
+ token, tenant = GetAuthInfo()
1136
+ }
1137
+
1138
+ if token == "" || tenant == "" {
1139
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "无可用Token,请先在管理页面获取"})
1140
+ return
1141
+ }
1142
+
1143
+ // 增加token使用计数
1144
+ incrementTokenUsage(token, model)
1145
+
1146
+ // 准备请求数据
1147
+ jsonData, err := json.Marshal(augmentReq)
1148
+ if err != nil {
1149
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "序列化请求失败"})
1150
+ return
1151
+ }
1152
+
1153
+ // 打印请求参数
1154
+ //log.Printf("发送到远程接口的请求参数: %s", string(jsonData))
1155
+
1156
+ // 创建请求 - 使用获取到的tenant_url
1157
+ req, err := http.NewRequest("POST", tenant+"chat-stream", strings.NewReader(string(jsonData)))
1158
+ if err != nil {
1159
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "创建请求失败"})
1160
+ return
1161
+ }
1162
+
1163
+ req.Header.Set("Content-Type", "application/json")
1164
+ // 使用获取到的token
1165
+ req.Header.Set("Authorization", "Bearer "+token)
1166
+
1167
+ client := createHTTPClient()
1168
+ resp, err := client.Do(req)
1169
+ if err != nil {
1170
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "请求失败: " + err.Error()})
1171
+ return
1172
+ }
1173
+ defer resp.Body.Close()
1174
+
1175
+ // 检查响应状态码
1176
+ if resp.StatusCode != http.StatusOK {
1177
+ body, err := io.ReadAll(resp.Body)
1178
+ errMsg := "Augment response error"
1179
+ if err == nil {
1180
+ errMsg = errMsg + ": " + string(body)
1181
+ }
1182
+ c.JSON(resp.StatusCode, gin.H{"error": errMsg})
1183
+ return
1184
+ }
1185
+
1186
+ // 读取完整响应
1187
+ reader := bufio.NewReader(resp.Body)
1188
+ var fullText string
1189
+
1190
+ for {
1191
+ line, err := reader.ReadString('\n')
1192
+ if err != nil {
1193
+ if err == io.EOF {
1194
+ break
1195
+ }
1196
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "读取响应失败: " + err.Error()})
1197
+ return
1198
+ }
1199
+
1200
+ line = strings.TrimSpace(line)
1201
+ if line == "" {
1202
+ continue
1203
+ }
1204
+
1205
+ var augmentResp AugmentResponse
1206
+ if err := json.Unmarshal([]byte(line), &augmentResp); err != nil {
1207
+ continue
1208
+ }
1209
+
1210
+ fullText += augmentResp.Text
1211
+
1212
+ // 检查响应内容是否包含错误信息
1213
+ if strings.Contains(augmentResp.Text, errBlocked) {
1214
+ // 将当前token加入冷却队列,冷却时间10分钟
1215
+ logger.Log.WithFields(logrus.Fields{
1216
+ "token": token,
1217
+ "mode": augmentReq.Mode,
1218
+ }).Info("检测到block信息,将token加入冷却队列10分钟")
1219
+
1220
+ err := SetTokenCoolStatus(token, 10*time.Minute)
1221
+ if err != nil {
1222
+ logger.Log.WithFields(logrus.Fields{
1223
+ "token": token,
1224
+ "error": err.Error(),
1225
+ }).Error("将token加入冷却队列失败")
1226
+ }
1227
+ }
1228
+
1229
+ if augmentResp.Done {
1230
+ break
1231
+ }
1232
+ }
1233
+
1234
+ // 创建OpenAI兼容的响应
1235
+ finishReason := "stop"
1236
+
1237
+ // 估算token数量
1238
+ promptTokens := estimateTokenCount(augmentReq.Message)
1239
+ for _, history := range augmentReq.ChatHistory {
1240
+ promptTokens += estimateTokenCount(history.RequestMessage)
1241
+ promptTokens += estimateTokenCount(history.ResponseText)
1242
+ }
1243
+ completionTokens := estimateTokenCount(fullText)
1244
+
1245
+ openAIResp := OpenAIResponse{
1246
+ ID: fmt.Sprintf("chatcmpl-%d", time.Now().Unix()),
1247
+ Object: "chat.completion",
1248
+ Created: time.Now().Unix(),
1249
+ Model: model,
1250
+ Choices: []Choice{
1251
+ {
1252
+ Index: 0,
1253
+ Message: ChatMessage{
1254
+ Role: "assistant",
1255
+ Content: fullText,
1256
+ },
1257
+ FinishReason: &finishReason,
1258
+ },
1259
+ },
1260
+ Usage: Usage{
1261
+ PromptTokens: promptTokens,
1262
+ CompletionTokens: completionTokens,
1263
+ TotalTokens: promptTokens + completionTokens,
1264
+ },
1265
+ }
1266
+
1267
+ c.JSON(http.StatusOK, openAIResp)
1268
+ }
1269
+
1270
+ // 清理请求状态
1271
+ func cleanupRequestStatus(c *gin.Context) {
1272
+ // 获取锁和 token
1273
+ lockInterface, exists := c.Get("token_lock")
1274
+ if !exists {
1275
+ return
1276
+ }
1277
+
1278
+ tokenInterface, exists := c.Get("token")
1279
+ if !exists {
1280
+ return
1281
+ }
1282
+
1283
+ lock, ok := lockInterface.(*sync.Mutex)
1284
+ if !ok {
1285
+ return
1286
+ }
1287
+
1288
+ token, ok := tokenInterface.(string)
1289
+ if !ok {
1290
+ return
1291
+ }
1292
+
1293
+ // 更新请求状态为已完成
1294
+ err := SetTokenRequestStatus(token, TokenRequestStatus{
1295
+ InProgress: false,
1296
+ LastRequestAt: time.Now(),
1297
+ })
1298
+
1299
+ // 无论更新状态是否成功,都要释放锁
1300
+ defer lock.Unlock()
1301
+
1302
+ if err != nil {
1303
+ log.Printf("清理请求状态失败: %v", err)
1304
+ return
1305
+ }
1306
+ }
1307
+
1308
+ // 创建 HTTP 客户端,如果配置了代理则使用
1309
+ func createHTTPClient() *http.Client {
1310
+ client := &http.Client{}
1311
+
1312
+ // 检查是否配置了代理
1313
+ if config.AppConfig.ProxyURL != "" {
1314
+ proxyURL, err := url.Parse(config.AppConfig.ProxyURL)
1315
+ if err == nil {
1316
+ transport := &http.Transport{
1317
+ Proxy: http.ProxyURL(proxyURL),
1318
+ }
1319
+ client.Transport = transport
1320
+ log.Printf("使用代理: %s", config.AppConfig.ProxyURL)
1321
+ } else {
1322
+ log.Printf("代理URL格式错误: %v", err)
1323
+ }
1324
+ }
1325
+
1326
+ return client
1327
+ }
1328
+
1329
+ // 在处理聊天请求时增加token使用计数
1330
+ func incrementTokenUsage(token string, model string) {
1331
+ // 先将模型名称转换为小写
1332
+ modelLower := strings.ToLower(model)
1333
+
1334
+ // 根据模型类型确定计数键 (不区分大小写)
1335
+ var countKey string
1336
+ if strings.HasSuffix(modelLower, "-chat") {
1337
+ countKey = "token_usage_chat:" + token
1338
+ } else if strings.HasSuffix(modelLower, "-agent") {
1339
+ countKey = "token_usage_agent:" + token
1340
+ } else {
1341
+ countKey = "token_usage:" + token // 默认键
1342
+ }
1343
+
1344
+ // 使用Redis的INCR命令增加计数
1345
+ err := config.RedisIncr(countKey)
1346
+ if err != nil {
1347
+ logger.Log.Error("增加token使用计数失败: %v", err)
1348
+ }
1349
+
1350
+ // 同时增加总使用计数
1351
+ totalCountKey := "token_usage:" + token
1352
+ if countKey != totalCountKey { // 避免重复计数
1353
+ err = config.RedisIncr(totalCountKey)
1354
+ if err != nil {
1355
+ logger.Log.Error("增加token总使用计数失败: %v", err)
1356
+ }
1357
+ }
1358
+ }
api/login.go ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package api
2
+
3
+ import (
4
+ "augment2api/config"
5
+ "augment2api/pkg/logger"
6
+ "net/http"
7
+ "time"
8
+
9
+ "github.com/gin-gonic/gin"
10
+ "github.com/google/uuid"
11
+ )
12
+
13
+ const (
14
+ TokenKey = "login:token:"
15
+ )
16
+
17
+ // 生成随机会话令牌
18
+ func generateSessionToken() string {
19
+ tokenUUID := uuid.New()
20
+ return tokenUUID.String()
21
+ }
22
+
23
+ // LoginRequest 登录请求结构
24
+ type LoginRequest struct {
25
+ Password string `json:"password"`
26
+ }
27
+
28
+ // LoginHandler 处理登录请求
29
+ func LoginHandler(c *gin.Context) {
30
+ var req LoginRequest
31
+ if err := c.ShouldBindJSON(&req); err != nil {
32
+ c.JSON(http.StatusBadRequest, gin.H{
33
+ "status": "error",
34
+ "error": "无效的请求数据",
35
+ })
36
+ return
37
+ }
38
+
39
+ // 验证密码
40
+ if req.Password == config.AppConfig.AccessPwd {
41
+ // 生成会话令牌
42
+ token := generateSessionToken()
43
+
44
+ // 将会话令牌保存到Redis,有效期24小时
45
+ sessionKey := TokenKey + token
46
+ err := config.RedisSet(sessionKey, "valid", 24*time.Hour)
47
+ if err != nil {
48
+ c.JSON(http.StatusInternalServerError, gin.H{
49
+ "status": "error",
50
+ "error": "保存会话失败: " + err.Error(),
51
+ })
52
+ return
53
+ }
54
+
55
+ c.JSON(http.StatusOK, gin.H{
56
+ "status": "success",
57
+ "token": token,
58
+ })
59
+ return
60
+ }
61
+
62
+ // 密码错误
63
+ c.JSON(http.StatusUnauthorized, gin.H{
64
+ "status": "error",
65
+ "error": "密码错误",
66
+ })
67
+ }
68
+
69
+ // ValidateToken 验证Token
70
+ func ValidateToken(token string) bool {
71
+ if token == "" {
72
+ return false
73
+ }
74
+
75
+ // 检查Redis中是否存在该token
76
+ tokenKey := TokenKey + token
77
+ exists, err := config.RedisExists(tokenKey)
78
+ if err != nil || !exists {
79
+ return false
80
+ }
81
+
82
+ return true
83
+ }
84
+
85
+ // AuthTokenMiddleware 会话认证中间件
86
+ func AuthTokenMiddleware() gin.HandlerFunc {
87
+ return func(c *gin.Context) {
88
+ // 如果未设置访问密码,则不需要验证
89
+ if config.AppConfig.AccessPwd == "" {
90
+ c.Next()
91
+ return
92
+ }
93
+
94
+ // 从查询参数或Cookie中获取会话令牌
95
+ token := c.GetHeader("X-Auth-Token")
96
+ if token == "" {
97
+ token = c.Query("token")
98
+ }
99
+ if token == "" {
100
+ token, _ = c.Cookie("auth_token")
101
+ }
102
+
103
+ // 验证会话令牌
104
+ if !ValidateToken(token) {
105
+ logger.Log.Info("无效的会话令牌:", token)
106
+ c.Redirect(http.StatusFound, "/login?error=token_expired")
107
+ c.Abort()
108
+ return
109
+ }
110
+
111
+ c.Next()
112
+ }
113
+ }
114
+
115
+ // LogoutHandler 处理登出请求
116
+ func LogoutHandler(c *gin.Context) {
117
+ token := c.GetHeader("X-Auth-Token")
118
+ if token != "" {
119
+ // 从Redis中删除会话token
120
+ tokenKey := TokenKey + token
121
+ err := config.RedisDel(tokenKey)
122
+ if err != nil {
123
+ c.JSON(http.StatusInternalServerError, gin.H{
124
+ "status": "error",
125
+ "error": "删除会话失败: " + err.Error(),
126
+ })
127
+ return
128
+ }
129
+ }
130
+
131
+ c.JSON(http.StatusOK, gin.H{
132
+ "status": "success",
133
+ })
134
+ }
api/token_handler.go ADDED
@@ -0,0 +1,869 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package api
2
+
3
+ import (
4
+ "augment2api/config"
5
+ "augment2api/pkg/logger"
6
+ "bytes"
7
+ "encoding/json"
8
+ "errors"
9
+ "fmt"
10
+ "math/rand"
11
+ "net/http"
12
+ "strconv"
13
+ "sync"
14
+ "time"
15
+
16
+ "github.com/gin-gonic/gin"
17
+ "github.com/go-redis/redis/v8"
18
+ "github.com/google/uuid"
19
+ "github.com/sirupsen/logrus"
20
+ )
21
+
22
+ // TokenInfo 存储token信息
23
+ type TokenInfo struct {
24
+ Token string `json:"token"`
25
+ TenantURL string `json:"tenant_url"`
26
+ UsageCount int `json:"usage_count"` // 总对话次数
27
+ ChatUsageCount int `json:"chat_usage_count"` // CHAT模式对话次数
28
+ AgentUsageCount int `json:"agent_usage_count"` // AGENT模式对话次数
29
+ Remark string `json:"remark"` // 备注字段
30
+ InCool bool `json:"in_cool"` // 是否在冷却中
31
+ CoolEnd time.Time `json:"cool_end,omitempty"` // 冷却结束时间
32
+ }
33
+
34
+ // TokenItem token项结构
35
+ type TokenItem struct {
36
+ Token string `json:"token"`
37
+ TenantUrl string `json:"tenantUrl"`
38
+ }
39
+
40
+ // TokenRequestStatus 记录 token 请求状态
41
+ type TokenRequestStatus struct {
42
+ InProgress bool `json:"in_progress"`
43
+ LastRequestAt time.Time `json:"last_request_at"`
44
+ }
45
+
46
+ // TokenCoolStatus 记录 token 冷却状态
47
+ type TokenCoolStatus struct {
48
+ InCool bool `json:"in_cool"`
49
+ CoolEnd time.Time `json:"cool_end"`
50
+ }
51
+
52
+ // GetRedisTokenHandler 从Redis获取token列表,支持分页
53
+ func GetRedisTokenHandler(c *gin.Context) {
54
+ // 获取分页参数(可选)
55
+ page := c.DefaultQuery("page", "1")
56
+ pageSize := c.DefaultQuery("page_size", "0") // 0表示不分页,返回所有
57
+
58
+ pageNum, _ := strconv.Atoi(page)
59
+ pageSizeNum, _ := strconv.Atoi(pageSize)
60
+
61
+ if pageNum < 1 {
62
+ pageNum = 1
63
+ }
64
+
65
+ // 获取所有token的key (使用通配符模式)
66
+ keys, err := config.RedisKeys("token:*")
67
+ if err != nil {
68
+ c.JSON(http.StatusOK, gin.H{
69
+ "status": "error",
70
+ "error": "获取token列表失败: " + err.Error(),
71
+ })
72
+ return
73
+ }
74
+
75
+ // 如果没有token
76
+ if len(keys) == 0 {
77
+ c.JSON(http.StatusOK, gin.H{
78
+ "status": "success",
79
+ "tokens": []TokenInfo{},
80
+ "total": 0,
81
+ "page": pageNum,
82
+ "page_size": pageSizeNum,
83
+ "total_pages": 0,
84
+ })
85
+ return
86
+ }
87
+
88
+ // 构建token列表
89
+ var tokenList []TokenInfo
90
+ for _, key := range keys {
91
+ // 从key中提取token (格式: "token:{token}")
92
+ token := key[6:] // 去掉前缀 "token:"
93
+
94
+ // 获取对应的tenant_url
95
+ tenantURL, err := config.RedisHGet(key, "tenant_url")
96
+ if err != nil {
97
+ continue // 跳过无效的token
98
+ }
99
+
100
+ // 获取token状态
101
+ status, err := config.RedisHGet(key, "status")
102
+ if err == nil && status == "disabled" {
103
+ continue // 跳过被标记为不可用的token
104
+ }
105
+
106
+ // 获取备注信息
107
+ remark, _ := config.RedisHGet(key, "remark")
108
+
109
+ // 获取token的冷却状态
110
+ coolStatus, _ := GetTokenCoolStatus(token)
111
+
112
+ // 在获取token信息时,同时获取对话次数、备注和冷却状态
113
+ tokenList = append(tokenList, TokenInfo{
114
+ Token: token,
115
+ TenantURL: tenantURL,
116
+ UsageCount: getTokenUsageCount(token),
117
+ ChatUsageCount: getTokenChatUsageCount(token),
118
+ AgentUsageCount: getTokenAgentUsageCount(token),
119
+ Remark: remark,
120
+ InCool: coolStatus.InCool,
121
+ CoolEnd: coolStatus.CoolEnd,
122
+ })
123
+ }
124
+
125
+ // 计算总页数和分页数据
126
+ totalItems := len(tokenList)
127
+ totalPages := 1
128
+
129
+ // 如果需要分页
130
+ if pageSizeNum > 0 {
131
+ totalPages = (totalItems + pageSizeNum - 1) / pageSizeNum
132
+
133
+ // 确保页码有效
134
+ if pageNum > totalPages && totalPages > 0 {
135
+ pageNum = totalPages
136
+ }
137
+
138
+ // 计算分页的起始和结束索引
139
+ startIndex := (pageNum - 1) * pageSizeNum
140
+ endIndex := startIndex + pageSizeNum
141
+
142
+ if startIndex < totalItems {
143
+ if endIndex > totalItems {
144
+ endIndex = totalItems
145
+ }
146
+ tokenList = tokenList[startIndex:endIndex]
147
+ } else {
148
+ tokenList = []TokenInfo{}
149
+ }
150
+ }
151
+
152
+ c.JSON(http.StatusOK, gin.H{
153
+ "status": "success",
154
+ "tokens": tokenList,
155
+ "total": totalItems,
156
+ "page": pageNum,
157
+ "page_size": pageSizeNum,
158
+ "total_pages": totalPages,
159
+ })
160
+ }
161
+
162
+ // SaveTokenToRedis 保存token到Redis
163
+ func SaveTokenToRedis(token, tenantURL string) error {
164
+ // 创建一个唯一的key,包含token和tenant_url
165
+ tokenKey := "token:" + token
166
+
167
+ // token已存在,则跳过
168
+ exists, err := config.RedisExists(tokenKey)
169
+ if err != nil {
170
+ return err
171
+ }
172
+ if exists {
173
+ return nil
174
+ }
175
+
176
+ // 将tenant_url存储在token对应的哈希表中
177
+ err = config.RedisHSet(tokenKey, "tenant_url", tenantURL)
178
+ if err != nil {
179
+ return err
180
+ }
181
+
182
+ // 默认将新添加的token标记为活跃状态
183
+ err = config.RedisHSet(tokenKey, "status", "active")
184
+ if err != nil {
185
+ return err
186
+ }
187
+
188
+ // 初始化备注为空字符串
189
+ return config.RedisHSet(tokenKey, "remark", "")
190
+ }
191
+
192
+ // DeleteTokenHandler 删除指定的token
193
+ func DeleteTokenHandler(c *gin.Context) {
194
+ token := c.Param("token")
195
+ if token == "" {
196
+ c.JSON(http.StatusBadRequest, gin.H{
197
+ "status": "error",
198
+ "error": "未指定token",
199
+ })
200
+ return
201
+ }
202
+
203
+ tokenKey := "token:" + token
204
+
205
+ // 检查token是否存在
206
+ exists, err := config.RedisExists(tokenKey)
207
+ if err != nil {
208
+ c.JSON(http.StatusInternalServerError, gin.H{
209
+ "status": "error",
210
+ "error": "检查token失败: " + err.Error(),
211
+ })
212
+ return
213
+ }
214
+
215
+ if !exists {
216
+ c.JSON(http.StatusNotFound, gin.H{
217
+ "status": "error",
218
+ "error": "token不存在",
219
+ })
220
+ return
221
+ }
222
+
223
+ // 删除token
224
+ if err := config.RedisDel(tokenKey); err != nil {
225
+ c.JSON(http.StatusInternalServerError, gin.H{
226
+ "status": "error",
227
+ "error": "删除token失败: " + err.Error(),
228
+ })
229
+ return
230
+ }
231
+
232
+ // 删除token关联的使用次数(如果存在)
233
+ // 删除总使用次数
234
+ tokenUsageKey := "token_usage:" + token
235
+ exists, err = config.RedisExists(tokenUsageKey)
236
+ if err != nil {
237
+ c.JSON(http.StatusInternalServerError, gin.H{
238
+ "status": "error",
239
+ "error": "检查token使用次数失败: " + err.Error(),
240
+ })
241
+ return
242
+ }
243
+ if exists {
244
+ if err := config.RedisDel(tokenUsageKey); err != nil {
245
+ c.JSON(http.StatusInternalServerError, gin.H{
246
+ "status": "error",
247
+ "error": "删除token使用次数失败: " + err.Error(),
248
+ })
249
+ }
250
+ }
251
+
252
+ // 删除CHAT模式使用次数
253
+ tokenChatUsageKey := "token_usage_chat:" + token
254
+ exists, err = config.RedisExists(tokenChatUsageKey)
255
+ if err == nil && exists {
256
+ config.RedisDel(tokenChatUsageKey)
257
+ }
258
+
259
+ // 删除AGENT模式使用次数
260
+ tokenAgentUsageKey := "token_usage_agent:" + token
261
+ exists, err = config.RedisExists(tokenAgentUsageKey)
262
+ if err == nil && exists {
263
+ config.RedisDel(tokenAgentUsageKey)
264
+ }
265
+
266
+ c.JSON(http.StatusOK, gin.H{
267
+ "status": "success",
268
+ })
269
+ }
270
+
271
+ // AddTokenHandler 批量添加token到Redis
272
+ func AddTokenHandler(c *gin.Context) {
273
+ var tokens []TokenItem
274
+ if err := c.ShouldBindJSON(&tokens); err != nil {
275
+ c.JSON(http.StatusBadRequest, gin.H{
276
+ "status": "error",
277
+ "error": "无效的请求数据",
278
+ })
279
+ return
280
+ }
281
+
282
+ // 检查是否有token数据
283
+ if len(tokens) == 0 {
284
+ c.JSON(http.StatusBadRequest, gin.H{
285
+ "status": "error",
286
+ "error": "token列表为空",
287
+ })
288
+ return
289
+ }
290
+
291
+ // 批量保存token
292
+ successCount := 0
293
+ failedTokens := make([]string, 0)
294
+
295
+ for _, item := range tokens {
296
+ // 验证token格式
297
+ if item.Token == "" || item.TenantUrl == "" {
298
+ failedTokens = append(failedTokens, item.Token)
299
+ continue
300
+ }
301
+
302
+ // 保存到Redis
303
+ err := SaveTokenToRedis(item.Token, item.TenantUrl)
304
+ if err != nil {
305
+ failedTokens = append(failedTokens, item.Token)
306
+ continue
307
+ }
308
+ successCount++
309
+ }
310
+
311
+ // 返回处理结果
312
+ result := gin.H{
313
+ "status": "success",
314
+ "total": len(tokens),
315
+ "success_count": successCount,
316
+ }
317
+
318
+ if len(failedTokens) > 0 {
319
+ result["failed_tokens"] = failedTokens
320
+ result["failed_count"] = len(failedTokens)
321
+ }
322
+
323
+ c.JSON(http.StatusOK, result)
324
+ }
325
+
326
+ // CheckTokenTenantURL 检测token的租户地址
327
+ func CheckTokenTenantURL(token string) (string, error) {
328
+ // 构建测试消息
329
+ testMsg := map[string]interface{}{
330
+ "message": "hello,what is your name",
331
+ "mode": "CHAT",
332
+ "prefix": "You are AI assistant,help me to solve problems!",
333
+ "suffix": " ",
334
+ "lang": "HTML",
335
+ "user_guidelines": "You are a helpful assistant, you can help me to solve problems and always answer in Chinese.",
336
+ "workspace_guidelines": "",
337
+ "feature_detection_flags": map[string]interface{}{
338
+ "support_raw_output": true,
339
+ },
340
+ "tool_definitions": []map[string]interface{}{},
341
+ "blobs": map[string]interface{}{
342
+ "checkpoint_id": nil,
343
+ "added_blobs": []string{},
344
+ "deleted_blobs": []string{},
345
+ },
346
+ }
347
+
348
+ jsonData, err := json.Marshal(testMsg)
349
+ if err != nil {
350
+ return "", fmt.Errorf("序列化测试消息失败: %v", err)
351
+ }
352
+
353
+ tokenKey := "token:" + token
354
+
355
+ currentTenantURL, err := config.RedisHGet(tokenKey, "tenant_url")
356
+
357
+ var tenantURLResult string
358
+ var foundValid bool
359
+ var tenantURLsToTest []string
360
+
361
+ // 如果Redis中有有效的租户地址,优先测试该地址
362
+ if err == nil && currentTenantURL != "" {
363
+ tenantURLsToTest = append(tenantURLsToTest, currentTenantURL)
364
+ }
365
+
366
+ // 添加其他租户地址
367
+ for i := 20; i >= 1; i-- {
368
+ newTenantURL := fmt.Sprintf("https://d%d.api.augmentcode.com/", i)
369
+ // 避免重复测试已有的租户地址
370
+ if newTenantURL != currentTenantURL {
371
+ tenantURLsToTest = append(tenantURLsToTest, newTenantURL)
372
+ }
373
+ }
374
+
375
+ // 测试租户地址
376
+ for _, tenantURL := range tenantURLsToTest {
377
+ // 创建请求
378
+ req, err := http.NewRequest("POST", tenantURL+"chat-stream", bytes.NewReader(jsonData))
379
+ if err != nil {
380
+ continue
381
+ }
382
+
383
+ req.Header.Set("Content-Type", "application/json")
384
+ req.Header.Set("Authorization", "Bearer "+token)
385
+ req.Header.Set("User-Agent", "augment.intellij/0.160.0 (Mac OS X; aarch64; 15.2) WebStorm/2024.3.5")
386
+ req.Header.Set("x-api-version", "2")
387
+ req.Header.Set("x-request-id", uuid.New().String())
388
+ req.Header.Set("x-request-session-id", uuid.New().String())
389
+
390
+ client := createHTTPClient()
391
+ resp, err := client.Do(req)
392
+ if err != nil {
393
+ fmt.Printf("请求失败: %v\n", err)
394
+ continue
395
+ }
396
+
397
+ isInvalid := false
398
+ func() {
399
+ defer resp.Body.Close()
400
+
401
+ // 检查是否返回401状态码(未授权)
402
+ if resp.StatusCode == http.StatusUnauthorized {
403
+ // 读取响应体内容
404
+ buf := make([]byte, 1024)
405
+ n, readErr := resp.Body.Read(buf)
406
+ responseBody := ""
407
+ if readErr == nil && n > 0 {
408
+ responseBody = string(buf[:n])
409
+ }
410
+
411
+ // 只有当响应中包含"Invalid token"时才标记为不可用
412
+ if readErr == nil && n > 0 && bytes.Contains(buf[:n], []byte("Invalid token")) {
413
+ // 将token标记为不可用
414
+ err = config.RedisHSet(tokenKey, "status", "disabled")
415
+ if err != nil {
416
+ fmt.Printf("标记token为不可用失败: %v\n", err)
417
+ }
418
+ logger.Log.WithFields(logrus.Fields{
419
+ "token": token,
420
+ "response_body": responseBody,
421
+ }).Info("token: 已被标记为不可用,返回401未授权")
422
+ isInvalid = true
423
+ }
424
+ return
425
+ }
426
+
427
+ // 检查响应状态
428
+ if resp.StatusCode == http.StatusOK {
429
+ // 尝试读取一小部分响应以确认是否有效
430
+ buf := make([]byte, 1024)
431
+ n, err := resp.Body.Read(buf)
432
+ if err == nil && n > 0 {
433
+ // 更新Redis中的租户地址和状态
434
+ err = config.RedisHSet(tokenKey, "tenant_url", tenantURL)
435
+ if err != nil {
436
+ return
437
+ }
438
+ // 将token标记为可用
439
+ err = config.RedisHSet(tokenKey, "status", "active")
440
+ if err != nil {
441
+ fmt.Printf("标记token为可用失败: %v\n", err)
442
+ }
443
+ logger.Log.WithFields(logrus.Fields{
444
+ "token": token,
445
+ "new_tenant_url": tenantURL,
446
+ }).Info("token: 更新租户地址成功")
447
+ tenantURLResult = tenantURL
448
+ foundValid = true
449
+ }
450
+ }
451
+ }()
452
+
453
+ // 如果token无效,立即返回错误,不再测试其他地址
454
+ if isInvalid {
455
+ return "", fmt.Errorf("token被标记为不可用")
456
+ }
457
+
458
+ // 如果找到有效的租户地址,跳出循环
459
+ if foundValid {
460
+ return tenantURLResult, nil
461
+ }
462
+ }
463
+
464
+ return "", fmt.Errorf("未找到有效的租户地址")
465
+ }
466
+
467
+ // CheckAllTokensHandler 批量检测所有token的租户地址
468
+ func CheckAllTokensHandler(c *gin.Context) {
469
+ // 获取所有token的key
470
+ keys, err := config.RedisKeys("token:*")
471
+ if err != nil {
472
+ c.JSON(http.StatusInternalServerError, gin.H{
473
+ "status": "error",
474
+ "error": "获取token列表失败: " + err.Error(),
475
+ })
476
+ return
477
+ }
478
+
479
+ if len(keys) == 0 {
480
+ c.JSON(http.StatusOK, gin.H{
481
+ "status": "success",
482
+ "total": 0,
483
+ "updated": 0,
484
+ "disabled": 0,
485
+ })
486
+ return
487
+ }
488
+
489
+ var wg sync.WaitGroup
490
+ // 使用互斥锁保护计数器
491
+ var mu sync.Mutex
492
+ var updatedCount int
493
+ var disabledCount int
494
+
495
+ for _, key := range keys {
496
+ // 获取token状态,跳过已标记为不可用的token
497
+ status, err := config.RedisHGet(key, "status")
498
+ if err == nil && status == "disabled" {
499
+ mu.Lock()
500
+ mu.Unlock()
501
+ continue // 跳过此token
502
+ }
503
+
504
+ wg.Add(1)
505
+ go func(key string) {
506
+ defer wg.Done()
507
+
508
+ // 从key中提取token
509
+ token := key[6:] // 去掉前缀 "token:"
510
+
511
+ // 获取当前的租户地址
512
+ oldTenantURL, _ := config.RedisHGet(key, "tenant_url")
513
+
514
+ // 检测租户地址
515
+ newTenantURL, err := CheckTokenTenantURL(token)
516
+ logger.Log.WithFields(logrus.Fields{
517
+ "token": token,
518
+ "old_tenant_url": oldTenantURL,
519
+ "new_tenant_url": newTenantURL,
520
+ }).Info("检测token租户地址")
521
+
522
+ mu.Lock()
523
+ if err != nil && err.Error() == "token被标记为不可用" {
524
+ disabledCount++
525
+ } else if err == nil && newTenantURL != oldTenantURL {
526
+ updatedCount++
527
+ }
528
+ mu.Unlock()
529
+ }(key)
530
+ }
531
+
532
+ wg.Wait()
533
+
534
+ c.JSON(http.StatusOK, gin.H{
535
+ "status": "success",
536
+ "total": len(keys),
537
+ "updated": updatedCount,
538
+ "disabled": disabledCount,
539
+ })
540
+ }
541
+
542
+ // SetTokenRequestStatus 设置token请求状态
543
+ func SetTokenRequestStatus(token string, status TokenRequestStatus) error {
544
+ // 使用Redis存储token请求状态
545
+ key := "token_status:" + token
546
+
547
+ // 将状态转换为JSON
548
+ statusJSON, err := json.Marshal(status)
549
+ if err != nil {
550
+ return err
551
+ }
552
+
553
+ // 存储到Redis,设置过期时间为1小时
554
+ return config.RedisSet(key, string(statusJSON), time.Hour)
555
+ }
556
+
557
+ // GetTokenRequestStatus 获取token请求状态
558
+ func GetTokenRequestStatus(token string) (TokenRequestStatus, error) {
559
+ key := "token_status:" + token
560
+
561
+ // 从Redis获取状态
562
+ statusJSON, err := config.RedisGet(key)
563
+ if err != nil {
564
+ // 如果key不存在,返回默认状态
565
+ if errors.Is(err, redis.Nil) {
566
+ return TokenRequestStatus{
567
+ InProgress: false,
568
+ LastRequestAt: time.Time{}, // 零值时间
569
+ }, nil
570
+ }
571
+ return TokenRequestStatus{}, err
572
+ }
573
+
574
+ // 解析JSON
575
+ var status TokenRequestStatus
576
+ if err := json.Unmarshal([]byte(statusJSON), &status); err != nil {
577
+ return TokenRequestStatus{}, err
578
+ }
579
+
580
+ return status, nil
581
+ }
582
+
583
+ // SetTokenCoolStatus 将token加入冷却队列
584
+ func SetTokenCoolStatus(token string, duration time.Duration) error {
585
+ // 使用Redis存储token冷却状态
586
+ key := "token_cool_status:" + token
587
+
588
+ coolStatus := TokenCoolStatus{
589
+ InCool: true,
590
+ CoolEnd: time.Now().Add(duration),
591
+ }
592
+
593
+ // 将状态转换为JSON
594
+ coolStatusJSON, err := json.Marshal(coolStatus)
595
+ if err != nil {
596
+ return err
597
+ }
598
+
599
+ // 存储到Redis,设置过期时间与冷却时间相同
600
+ return config.RedisSet(key, string(coolStatusJSON), duration)
601
+ }
602
+
603
+ // GetTokenCoolStatus 获取token冷却状态
604
+ func GetTokenCoolStatus(token string) (TokenCoolStatus, error) {
605
+ key := "token_cool_status:" + token
606
+
607
+ // 从Redis获取状态
608
+ coolStatusJSON, err := config.RedisGet(key)
609
+ if err != nil {
610
+ // 如果key不存在,返回默认状态
611
+ if errors.Is(err, redis.Nil) {
612
+ return TokenCoolStatus{
613
+ InCool: false,
614
+ CoolEnd: time.Time{}, // 零值时间
615
+ }, nil
616
+ }
617
+ return TokenCoolStatus{}, err
618
+ }
619
+
620
+ // 解析JSON
621
+ var coolStatus TokenCoolStatus
622
+ if err := json.Unmarshal([]byte(coolStatusJSON), &coolStatus); err != nil {
623
+ return TokenCoolStatus{}, err
624
+ }
625
+
626
+ // 检查冷却时间是否已过
627
+ if time.Now().After(coolStatus.CoolEnd) {
628
+ coolStatus.InCool = false
629
+ }
630
+
631
+ return coolStatus, nil
632
+ }
633
+
634
+ // GetAvailableToken 获取一个可用的token(未在使用中且冷却时间已过)
635
+ func GetAvailableToken() (string, string) {
636
+ // 获取所有token的key
637
+ keys, err := config.RedisKeys("token:*")
638
+ if err != nil || len(keys) == 0 {
639
+ return "No token", ""
640
+ }
641
+
642
+ // 筛选可用的token
643
+ var availableTokens []string
644
+ var availableTenantURLs []string
645
+ var cooldownTokens []string
646
+ var cooldownTenantURLs []string
647
+
648
+ for _, key := range keys {
649
+ // 获取token状态
650
+ status, err := config.RedisHGet(key, "status")
651
+ if err == nil && status == "disabled" {
652
+ continue // 跳过被标记为不可用的token
653
+ }
654
+
655
+ // 从key中提取token
656
+ token := key[6:] // 去掉前缀 "token:"
657
+
658
+ // 获取token的请求状态
659
+ requestStatus, err := GetTokenRequestStatus(token)
660
+ if err != nil {
661
+ continue
662
+ }
663
+
664
+ // 如果token正在使用中,跳过
665
+ if requestStatus.InProgress {
666
+ continue
667
+ }
668
+
669
+ // 如果距离上次请求不足3秒,跳过
670
+ if time.Since(requestStatus.LastRequestAt) < 3*time.Second {
671
+ continue
672
+ }
673
+
674
+ // 检查CHAT模式和AGENT模式的使用次数限制
675
+ chatUsageCount := getTokenChatUsageCount(token)
676
+ agentUsageCount := getTokenAgentUsageCount(token)
677
+
678
+ // 如果CHAT模式已达到3000次限制,跳过
679
+ if chatUsageCount >= 3000 {
680
+ continue
681
+ }
682
+
683
+ // 如果AGENT模式已达到50次限制,跳过
684
+ if agentUsageCount >= 50 {
685
+ continue
686
+ }
687
+
688
+ // 获取对应的tenant_url
689
+ tenantURL, err := config.RedisHGet(key, "tenant_url")
690
+ if err != nil {
691
+ continue
692
+ }
693
+
694
+ // 检查token是否在冷却中
695
+ coolStatus, err := GetTokenCoolStatus(token)
696
+ if err != nil {
697
+ continue
698
+ }
699
+
700
+ // 如果token在冷却中,放入冷却队列
701
+ if coolStatus.InCool {
702
+ cooldownTokens = append(cooldownTokens, token)
703
+ cooldownTenantURLs = append(cooldownTenantURLs, tenantURL)
704
+ } else {
705
+ // 否则放入可用队列
706
+ availableTokens = append(availableTokens, token)
707
+ availableTenantURLs = append(availableTenantURLs, tenantURL)
708
+ }
709
+ }
710
+
711
+ // 优先从可用队列中选择token
712
+ if len(availableTokens) > 0 {
713
+ // 随机选择一个token
714
+ randomIndex := rand.Intn(len(availableTokens))
715
+ return availableTokens[randomIndex], availableTenantURLs[randomIndex]
716
+ }
717
+
718
+ // 如果没有非冷却token可用,则从冷却队列中选择
719
+ if len(cooldownTokens) > 0 {
720
+ // 随机选择一个token
721
+ randomIndex := rand.Intn(len(cooldownTokens))
722
+ return cooldownTokens[randomIndex], cooldownTenantURLs[randomIndex]
723
+ }
724
+
725
+ // 如果没有任何可用的token
726
+ return "No available token", ""
727
+ }
728
+
729
+ // getTokenUsageCount 获取token的使用次数
730
+ func getTokenUsageCount(token string) int {
731
+ // 使用Redis中的计数器获取使用次数
732
+ countKey := "token_usage:" + token
733
+ count, err := config.RedisGet(countKey)
734
+ if err != nil {
735
+ return 0 // 如果出错或不存在,返回0
736
+ }
737
+
738
+ // 将字符串转换为整数
739
+ countInt, err := strconv.Atoi(count)
740
+ if err != nil {
741
+ return 0
742
+ }
743
+
744
+ return countInt
745
+ }
746
+
747
+ // getTokenChatUsageCount 获取token的CHAT模式使用次数
748
+ func getTokenChatUsageCount(token string) int {
749
+ // 使用Redis中的计数器获取使用次数
750
+ countKey := "token_usage_chat:" + token
751
+ count, err := config.RedisGet(countKey)
752
+ if err != nil {
753
+ return 0 // 如果出错或不存在,返回0
754
+ }
755
+
756
+ // 将字符串转换为整数
757
+ countInt, err := strconv.Atoi(count)
758
+ if err != nil {
759
+ return 0
760
+ }
761
+
762
+ return countInt
763
+ }
764
+
765
+ // getTokenAgentUsageCount 获取token的AGENT模式使用次数
766
+ func getTokenAgentUsageCount(token string) int {
767
+ // 使用Redis中的计数器获取使用次数
768
+ countKey := "token_usage_agent:" + token
769
+ count, err := config.RedisGet(countKey)
770
+ if err != nil {
771
+ return 0 // 如果出错或不存在,返回0
772
+ }
773
+
774
+ // 将字符串转换为整数
775
+ countInt, err := strconv.Atoi(count)
776
+ if err != nil {
777
+ return 0
778
+ }
779
+
780
+ return countInt
781
+ }
782
+
783
+ // UpdateTokenRemark 更新token的备注信息
784
+ func UpdateTokenRemark(c *gin.Context) {
785
+ token := c.Param("token")
786
+ if token == "" {
787
+ c.JSON(http.StatusBadRequest, gin.H{
788
+ "status": "error",
789
+ "error": "未指定token",
790
+ })
791
+ return
792
+ }
793
+
794
+ var req struct {
795
+ Remark string `json:"remark"`
796
+ }
797
+ if err := c.ShouldBindJSON(&req); err != nil {
798
+ c.JSON(http.StatusBadRequest, gin.H{
799
+ "status": "error",
800
+ "error": "无效的请求数据",
801
+ })
802
+ return
803
+ }
804
+
805
+ tokenKey := "token:" + token
806
+
807
+ // 检查token是否存在
808
+ exists, err := config.RedisExists(tokenKey)
809
+ if err != nil {
810
+ c.JSON(http.StatusInternalServerError, gin.H{
811
+ "status": "error",
812
+ "error": "检查token失败: " + err.Error(),
813
+ })
814
+ return
815
+ }
816
+
817
+ if !exists {
818
+ c.JSON(http.StatusNotFound, gin.H{
819
+ "status": "error",
820
+ "error": "token不存在",
821
+ })
822
+ return
823
+ }
824
+
825
+ // 更新备注
826
+ err = config.RedisHSet(tokenKey, "remark", req.Remark)
827
+ if err != nil {
828
+ c.JSON(http.StatusInternalServerError, gin.H{
829
+ "status": "error",
830
+ "error": "更新备注失败: " + err.Error(),
831
+ })
832
+ return
833
+ }
834
+
835
+ c.JSON(http.StatusOK, gin.H{
836
+ "status": "success",
837
+ })
838
+ }
839
+
840
+ // MigrateTokensRemark 确保所有token都有remark字段
841
+ func MigrateTokensRemark() error {
842
+ // 获取所有token的key
843
+ keys, err := config.RedisKeys("token:*")
844
+ if err != nil {
845
+ return fmt.Errorf("获取token列表失败: %v", err)
846
+ }
847
+
848
+ for _, key := range keys {
849
+ // 检查是否已有remark字段
850
+ exists, err := config.RedisHExists(key, "remark")
851
+ if err != nil {
852
+ logger.Log.Error("check remark field of token %s failed: %v", key, err)
853
+ continue
854
+ }
855
+
856
+ // 如果没有remark字段,添加一个空的remark
857
+ if !exists {
858
+ err = config.RedisHSet(key, "remark", "")
859
+ if err != nil {
860
+ logger.Log.Error("add remark field to token %s failed: %v", key, err)
861
+ continue
862
+ }
863
+ logger.Log.Info("add remark field to token %s success", key)
864
+ }
865
+ }
866
+ logger.Log.Info("migrate remark field to all tokens success!")
867
+
868
+ return nil
869
+ }
api/token_reset.go ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package api
2
+
3
+ import (
4
+ "augment2api/config"
5
+ "augment2api/pkg/logger"
6
+
7
+ "github.com/robfig/cron/v3"
8
+ "github.com/sirupsen/logrus"
9
+ )
10
+
11
+ // ResetTokenUsage 重置所有token的使用次数
12
+ func ResetTokenUsage() error {
13
+ // 获取所有token的key
14
+ keys, err := config.RedisKeys("token:*")
15
+ if err != nil {
16
+ return err
17
+ }
18
+
19
+ for _, key := range keys {
20
+ // 从key中提取token
21
+ token := key[6:] // 去掉前缀 "token:"
22
+
23
+ // 重置总使用次数
24
+ totalUsageKey := "token_usage:" + token
25
+ err = config.RedisSet(totalUsageKey, "0", 0) // 0表示永不过期
26
+ if err != nil {
27
+ logger.Log.WithFields(logrus.Fields{
28
+ "token": token,
29
+ "error": err,
30
+ }).Error("重置Token总使用次数失败")
31
+ continue
32
+ }
33
+
34
+ // 重置CHAT模式使用次数
35
+ chatUsageKey := "token_usage_chat:" + token
36
+ err = config.RedisSet(chatUsageKey, "0", 0) // 0表示永不过期
37
+ if err != nil {
38
+ logger.Log.WithFields(logrus.Fields{
39
+ "token": token,
40
+ "error": err,
41
+ }).Error("重置Token CHAT模式使用次数失败")
42
+ continue
43
+ }
44
+
45
+ // 重置AGENT模式使用次数
46
+ agentUsageKey := "token_usage_agent:" + token
47
+ err = config.RedisSet(agentUsageKey, "0", 0) // 0表示永不过期
48
+ if err != nil {
49
+ logger.Log.WithFields(logrus.Fields{
50
+ "token": token,
51
+ "error": err,
52
+ }).Error("重置Token AGENT模式使用次数失败")
53
+ continue
54
+ }
55
+
56
+ logger.Log.WithFields(logrus.Fields{
57
+ "token": token,
58
+ }).Info("重置token使用次数成功")
59
+ }
60
+
61
+ return nil
62
+ }
63
+
64
+ // StartTokenUsageResetScheduler 启动token使用次数重置调度器
65
+ func StartTokenUsageResetScheduler() {
66
+ // 创建cron调度器
67
+ c := cron.New(cron.WithSeconds()) // 启用秒级精度
68
+
69
+ // 添加定时任务,每月1号零点一分执行
70
+ // 格式:秒 分 时 日 月 周
71
+ _, err := c.AddFunc("0 1 0 1 * *", func() {
72
+ logger.Log.Info("开始执行Token使用次数重置任务")
73
+ err := ResetTokenUsage()
74
+ if err != nil {
75
+ logger.Log.WithFields(logrus.Fields{
76
+ "error": err,
77
+ }).Error("执行Token使用次数重置任务失败")
78
+ } else {
79
+ logger.Log.Info("Token使用次数重置任务执行完成")
80
+ }
81
+ })
82
+
83
+ if err != nil {
84
+ logger.Log.WithFields(logrus.Fields{
85
+ "error": err,
86
+ }).Error("添加Token使用次数重置定时任务失败")
87
+ return
88
+ }
89
+
90
+ // 启动cron调度器
91
+ c.Start()
92
+ logger.Log.Info("Token使用次数重置调度器启动成功!")
93
+
94
+ // 保持程序运行
95
+ select {}
96
+ }
config/config.go ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ AccessPwd string
15
+ RoutePrefix string
16
+ ProxyURL string
17
+ }
18
+
19
+ const version = "v1.0.2"
20
+
21
+ var AppConfig Config
22
+
23
+ func InitConfig() error {
24
+ // 从环境变量读取配置
25
+ AppConfig = Config{
26
+ // 必填配置
27
+ RedisConnString: getEnv("REDIS_CONN_STRING", ""),
28
+ AccessPwd: getEnv("ACCESS_PWD", ""),
29
+ // 非必填配置
30
+ AuthToken: getEnv("AUTH_TOKEN", ""), // api鉴权token
31
+ RoutePrefix: getEnv("ROUTE_PREFIX", ""), // 自定义openai接口路由前缀
32
+ CodingMode: getEnv("CODING_MODE", "false"),
33
+ CodingToken: getEnv("CODING_TOKEN", ""),
34
+ TenantURL: getEnv("TENANT_URL", ""),
35
+ ProxyURL: getEnv("PROXY_URL", ""), // 代理URL配置
36
+ }
37
+
38
+ if AppConfig.CodingMode == "false" {
39
+
40
+ // redis连接字符串 示例: redis://default:pwd@localhost:6379
41
+ if AppConfig.RedisConnString == "" {
42
+ logger.Log.Fatalln("未配置环境变量 REDIS_CONN_STRING")
43
+ }
44
+
45
+ }
46
+
47
+ // 为了安全,必须配置访问密码
48
+ if AppConfig.AccessPwd == "" {
49
+ logger.Log.Fatalln("未配置环境变量 ACCESS_PWD")
50
+ }
51
+
52
+ // 打印欢迎信息
53
+ logger.Log.Info("Welcome to use Augment2Api! Current Version: " + version)
54
+
55
+ logger.Log.Info("Augment2Api配置加载完成:\n" +
56
+ "----------------------------------------\n" +
57
+ "AuthToken: " + AppConfig.AuthToken + "\n" +
58
+ "AccessPwd: " + AppConfig.AccessPwd + "\n" +
59
+ "RedisConnString: " + AppConfig.RedisConnString + "\n" +
60
+ "RoutePrefix: " + AppConfig.RoutePrefix + "\n" +
61
+ "ProxyURL: " + AppConfig.ProxyURL + "\n" +
62
+ "----------------------------------------")
63
+
64
+ logger.Log.Info("Everything is set up, now start to fully enjoy the charm of AI !")
65
+
66
+ return nil
67
+ }
68
+
69
+ func getEnv(key, defaultValue string) string {
70
+ value := os.Getenv(key)
71
+ if value == "" {
72
+ return defaultValue
73
+ }
74
+ return value
75
+ }
config/redis.go ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }
95
+
96
+ // RedisIncr 增加Redis中的计数器
97
+ func RedisIncr(key string) error {
98
+ ctx := context.Background()
99
+ _, err := RDB.Incr(ctx, key).Result()
100
+ return err
101
+ }
102
+
103
+ // RedisHExists 检查哈希表字段是否存在
104
+ func RedisHExists(key, field string) (bool, error) {
105
+ ctx := context.Background()
106
+ return RDB.HExists(ctx, key, field).Result()
107
+ }
docker-compose.yml ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ redis:
5
+ image: redis:alpine
6
+ restart: always
7
+ volumes:
8
+ - redis_data:/data
9
+ command: redis-server --requirepass $${REDIS_PASSWORD:-yourpassword}
10
+ healthcheck:
11
+ test: ["CMD", "redis-cli", "-a", "$${REDIS_PASSWORD:-yourpassword}", "ping"]
12
+ interval: 5s
13
+ timeout: 3s
14
+ retries: 5
15
+ environment:
16
+ - TZ=Asia/Shanghai
17
+ - REDIS_PASSWORD=${REDIS_PASSWORD:-yourpassword}
18
+
19
+ augment2api:
20
+ build: .
21
+ restart: always
22
+ ports:
23
+ - "27080:27080"
24
+ environment:
25
+ - REDIS_CONN_STRING=redis://default:${REDIS_PASSWORD:-yourpassword}@redis:6379
26
+ - ACCESS_PWD=${ACCESS_PWD:-your-access-password}
27
+ - AUTH_TOKEN=${AUTH_TOKEN:-your-auth-token}
28
+ - TZ=Asia/Shanghai
29
+ depends_on:
30
+ redis:
31
+ condition: service_healthy
32
+
33
+ volumes:
34
+ redis_data:
35
+
36
+ networks:
37
+ default:
38
+ driver: bridge
go.mod ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/google/uuid v1.6.0
12
+ github.com/robfig/cron/v3 v3.0.1
13
+ github.com/sirupsen/logrus v1.9.3
14
+ )
15
+
16
+ require (
17
+ github.com/bytedance/sonic v1.12.6 // indirect
18
+ github.com/bytedance/sonic/loader v0.2.1 // indirect
19
+ github.com/cespare/xxhash/v2 v2.1.2 // indirect
20
+ github.com/cloudwego/base64x v0.1.4 // indirect
21
+ github.com/cloudwego/iasm v0.2.0 // indirect
22
+ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
23
+ github.com/gabriel-vasile/mimetype v1.4.7 // indirect
24
+ github.com/gin-contrib/sse v0.1.0 // indirect
25
+ github.com/go-playground/locales v0.14.1 // indirect
26
+ github.com/go-playground/universal-translator v0.18.1 // indirect
27
+ github.com/go-playground/validator/v10 v10.23.0 // indirect
28
+ github.com/goccy/go-json v0.10.4 // indirect
29
+ github.com/json-iterator/go v1.1.12 // indirect
30
+ github.com/klauspost/cpuid/v2 v2.2.9 // indirect
31
+ github.com/kr/text v0.2.0 // indirect
32
+ github.com/leodido/go-urn v1.4.0 // indirect
33
+ github.com/mattn/go-isatty v0.0.20 // indirect
34
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
35
+ github.com/modern-go/reflect2 v1.0.2 // indirect
36
+ github.com/pelletier/go-toml/v2 v2.2.3 // indirect
37
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
38
+ github.com/ugorji/go/codec v1.2.12 // indirect
39
+ golang.org/x/arch v0.12.0 // indirect
40
+ golang.org/x/crypto v0.36.0 // indirect
41
+ golang.org/x/net v0.37.0 // indirect
42
+ golang.org/x/sys v0.31.0 // indirect
43
+ golang.org/x/text v0.23.0 // indirect
44
+ google.golang.org/protobuf v1.36.1 // indirect
45
+ gopkg.in/yaml.v3 v3.0.1 // indirect
46
+ )
go.sum ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
44
+ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
45
+ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
46
+ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
47
+ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
48
+ github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
49
+ github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
50
+ github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
51
+ github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
52
+ github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
53
+ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
54
+ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
55
+ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
56
+ github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
57
+ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
58
+ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
59
+ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
60
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
61
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
62
+ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
63
+ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
64
+ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
65
+ github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
66
+ github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
67
+ github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
68
+ github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
69
+ github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
70
+ github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
71
+ github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
72
+ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
73
+ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
74
+ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
75
+ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
76
+ github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
77
+ github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
78
+ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
79
+ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
80
+ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
81
+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
82
+ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
83
+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
84
+ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
85
+ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
86
+ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
87
+ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
88
+ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
89
+ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
90
+ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
91
+ github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
92
+ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
93
+ github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
94
+ golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg=
95
+ golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
96
+ golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
97
+ golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
98
+ golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
99
+ golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
100
+ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
101
+ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
102
+ golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
103
+ golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
104
+ golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
105
+ golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
106
+ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
107
+ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
108
+ google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
109
+ google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
110
+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
111
+ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
112
+ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
113
+ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
114
+ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
115
+ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
116
+ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
117
+ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
118
+ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
119
+ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
120
+ nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
main.go ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ // 登录页面
140
+ r.GET("/login", func(c *gin.Context) {
141
+ c.HTML(http.StatusOK, "login.html", gin.H{})
142
+ })
143
+
144
+ // 登录
145
+ r.POST("/api/login", api.LoginHandler)
146
+
147
+ // 登出
148
+ r.POST("/api/logout", api.LogoutHandler)
149
+
150
+ // 管理页面 - 需要会话验证
151
+ r.GET("/", func(c *gin.Context) {
152
+ // 如果设置了访问密码,检查是否已登录
153
+ if config.AppConfig.AccessPwd != "" {
154
+ // 从查询参数或Cookie中获取会话令牌
155
+ token := c.Query("token")
156
+ if token == "" {
157
+ // 尝试从Cookie获取
158
+ token, _ = c.Cookie("auth_token")
159
+ }
160
+
161
+ // 从请求头获取
162
+ if token == "" {
163
+ token = c.GetHeader("X-Auth-Token")
164
+ }
165
+
166
+ // 验证会话令牌
167
+ if !api.ValidateToken(token) {
168
+ c.Redirect(http.StatusFound, "/login")
169
+ return
170
+ }
171
+ }
172
+ c.HTML(http.StatusOK, "admin.html", gin.H{})
173
+ })
174
+
175
+ // 管理页面 - 需要会话验证
176
+ r.GET("/admin", api.AuthTokenMiddleware(), func(c *gin.Context) {
177
+ c.HTML(http.StatusOK, "admin.html", gin.H{})
178
+ })
179
+
180
+ // 授权端点 - 需要会话验证
181
+ r.GET("/auth", api.AuthTokenMiddleware(), func(c *gin.Context) {
182
+ authorizeURL := generateAuthorizeURL(globalOAuthState)
183
+ api.AuthHandler(c, authorizeURL)
184
+ })
185
+
186
+ // 获取token - 需要会话验证
187
+ r.GET("/api/tokens", api.AuthTokenMiddleware(), api.GetRedisTokenHandler)
188
+
189
+ // 删除token - 需要会话验证
190
+ r.DELETE("/api/token/:token", api.AuthTokenMiddleware(), api.DeleteTokenHandler)
191
+
192
+ // 更新token备注 - 需要会话验证
193
+ r.PUT("/api/token/:token/remark", api.AuthTokenMiddleware(), api.UpdateTokenRemark)
194
+
195
+ // 批量检测token - 需要会话���证
196
+ r.GET("/api/check-tokens", api.AuthTokenMiddleware(), api.CheckAllTokensHandler)
197
+
198
+ // 回调端点,用于处理授权码 - 需要会话验证
199
+ r.POST("/callback", api.AuthTokenMiddleware(), func(c *gin.Context) {
200
+ api.CallbackHandler(c, func(tenantURL, _, code string) (string, error) {
201
+ return getAccessToken(tenantURL, globalOAuthState.CodeVerifier, code)
202
+ })
203
+ })
204
+
205
+ // 鉴权路由组
206
+ authGroup := r.Group(ProcessPath(config.AppConfig.RoutePrefix))
207
+ authGroup.Use(api.AuthMiddleware())
208
+ {
209
+ // OpenAI兼容的聊天端点
210
+ chatGroup := authGroup.Group("/")
211
+ // 并发控制
212
+ chatGroup.Use(middleware.TokenConcurrencyMiddleware())
213
+ {
214
+ chatGroup.POST("/v1/chat/completions", api.ChatCompletionsHandler)
215
+ chatGroup.POST("/v1", api.ChatCompletionsHandler)
216
+ chatGroup.POST("/v1/chat", api.ChatCompletionsHandler)
217
+ }
218
+
219
+ authGroup.GET("/v1/models", api.ModelsHandler)
220
+ authGroup.POST("/api/add/tokens", api.AddTokenHandler)
221
+ }
222
+
223
+ return r
224
+ }
225
+
226
+ func ProcessPath(path string) string {
227
+ // 判断字符串是否为空
228
+ if path == "" {
229
+ return ""
230
+ }
231
+
232
+ // 判断开头是否为/,不是则添加
233
+ if !strings.HasPrefix(path, "/") {
234
+ path = "/" + path
235
+ }
236
+
237
+ // 判断结尾是否为/,是则去掉
238
+ if strings.HasSuffix(path, "/") {
239
+ path = path[:len(path)-1]
240
+ }
241
+
242
+ return path
243
+ }
244
+
245
+ func main() {
246
+ // 设置全局时区为东八区(CST)
247
+ time.Local = time.FixedZone("CST", 8*3600)
248
+
249
+ // 设置 Gin 为发布模式
250
+ gin.SetMode(gin.ReleaseMode)
251
+
252
+ // 初始化日志
253
+ logger.Init()
254
+
255
+ // 初始化配置
256
+ err := config.InitConfig()
257
+ if err != nil {
258
+ logger.Log.Fatalln("failed to initialize config: " + err.Error())
259
+ return
260
+ }
261
+
262
+ // 初始化Redis
263
+ err = config.InitRedisClient()
264
+ if err != nil {
265
+ logger.Log.Fatalln("failed to initialize Redis: " + err.Error())
266
+ }
267
+
268
+ // token备注字段迁移
269
+ err = api.MigrateTokensRemark()
270
+ if err != nil {
271
+ logger.Log.Error("Token备注字段迁移失败: %v", err)
272
+ }
273
+
274
+ // 启动token使用次数重置调度器
275
+ go api.StartTokenUsageResetScheduler()
276
+
277
+ r := setupRouter()
278
+
279
+ // 启动服务器
280
+ if err := r.Run(":7860"); err != nil {
281
+ logger.Log.Fatalf("启动服务失败: %v", err)
282
+ }
283
+
284
+ logger.Log.WithFields(map[string]interface{}{
285
+ "port": 27080,
286
+ "mode": gin.Mode(),
287
+ }).Info("Augment2API 服务启动成功")
288
+ }
middleware/concurrency.go ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package middleware
2
+
3
+ import (
4
+ "augment2api/api"
5
+ "augment2api/config"
6
+ "augment2api/pkg/logger"
7
+ "net/http"
8
+ "strings"
9
+ "sync"
10
+ "time"
11
+
12
+ "github.com/gin-gonic/gin"
13
+ "github.com/sirupsen/logrus"
14
+ )
15
+
16
+ // 全局锁映射,用于控制每个 token 的并发请求
17
+ var (
18
+ tokenLocks = make(map[string]*sync.Mutex)
19
+ tokenLocksGuard = sync.Mutex{}
20
+ )
21
+
22
+ // getTokenLock 获取指定 token 的锁
23
+ func getTokenLock(token string) *sync.Mutex {
24
+ tokenLocksGuard.Lock()
25
+ defer tokenLocksGuard.Unlock()
26
+
27
+ if lock, exists := tokenLocks[token]; exists {
28
+ return lock
29
+ }
30
+
31
+ lock := &sync.Mutex{}
32
+ tokenLocks[token] = lock
33
+ return lock
34
+ }
35
+
36
+ // TokenConcurrencyMiddleware 控制Redis中token的使用频率
37
+ func TokenConcurrencyMiddleware() gin.HandlerFunc {
38
+ return func(c *gin.Context) {
39
+ // 只对聊天完成请求进行并发控制
40
+ if !strings.HasSuffix(c.Request.URL.Path, "/chat/completions") {
41
+ c.Next()
42
+ return
43
+ }
44
+
45
+ // 调试模式无需限制
46
+ if config.AppConfig.CodingMode == "true" {
47
+ token := config.AppConfig.CodingToken
48
+ tenantURL := config.AppConfig.TenantURL
49
+ c.Set("token", token)
50
+ c.Set("tenant_url", tenantURL)
51
+ c.Next()
52
+ }
53
+
54
+ // 获取一个可用的token
55
+ token, tenantURL := api.GetAvailableToken()
56
+ if token == "No token" {
57
+ c.JSON(http.StatusTooManyRequests, gin.H{"error": "当前无可用token,请在页面添加"})
58
+ c.Abort()
59
+ return
60
+ }
61
+ if token == "No available token" || tenantURL == "" {
62
+ c.JSON(http.StatusTooManyRequests, gin.H{"error": "当前请求过多,请稍后再试"})
63
+ c.Abort()
64
+ return
65
+ }
66
+
67
+ // 获取该token的锁
68
+ lock := getTokenLock(token)
69
+
70
+ // 尝试获取锁,会阻塞直到获取到锁
71
+ lock.Lock()
72
+
73
+ // 更新请求状态
74
+ err := api.SetTokenRequestStatus(token, api.TokenRequestStatus{
75
+ InProgress: true,
76
+ LastRequestAt: time.Now(),
77
+ })
78
+
79
+ if err != nil {
80
+ lock.Unlock()
81
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "更新token请求状态失败"})
82
+ c.Abort()
83
+ return
84
+ }
85
+
86
+ logger.Log.WithFields(logrus.Fields{
87
+ "token": token,
88
+ }).Info("本次请求使用的token: ")
89
+
90
+ // 在请求完成后释放锁
91
+ c.Set("token_lock", lock)
92
+ c.Set("token", token)
93
+ c.Set("tenant_url", tenantURL)
94
+
95
+ c.Next()
96
+ }
97
+ }
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
+ }
static/augment.svg ADDED
templates/admin.html ADDED
@@ -0,0 +1,1529 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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-Panel</title>
7
+ <link rel="icon" href="../static/augment.svg" type="image/svg+xml">
8
+ <link rel="alternate icon" href="../static/augment.svg" type="image/x-icon">
9
+ <style>
10
+ :root {
11
+ --primary-color: #4a6cf7;
12
+ --primary-hover: #3a5ce4;
13
+ --bg-color: #f5f7fa;
14
+ --card-bg: #ffffff;
15
+ --text-color: #333333;
16
+ --text-secondary: #6c757d;
17
+ --border-color: #e9ecef;
18
+ --header-bg: #ffffff;
19
+ --header-color: #333333;
20
+ --sidebar-bg: #ffffff;
21
+ --sidebar-color: #333333;
22
+ --sidebar-hover: #f0f4ff;
23
+ --sidebar-active: #e6edff;
24
+ --footer-bg: #ffffff;
25
+ --footer-color: #6c757d;
26
+ --success-color: #28a745;
27
+ --error-color: #dc3545;
28
+ --warning-color: #ffc107;
29
+ --radius: 8px;
30
+ --shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
31
+ --transition: all 0.3s ease;
32
+ }
33
+
34
+ html, body {
35
+ height: 100%;
36
+ margin: 0;
37
+ padding: 0;
38
+ overflow: hidden;
39
+ }
40
+
41
+ body {
42
+ display: flex;
43
+ flex-direction: column;
44
+ background-color: var(--bg-color);
45
+ color: var(--text-color);
46
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
47
+ line-height: 1.6;
48
+ }
49
+
50
+ .container {
51
+ display: flex;
52
+ flex-direction: column;
53
+ height: 100vh;
54
+ width: 100%;
55
+ max-width: 100%;
56
+ margin: 0;
57
+ padding: 0;
58
+ }
59
+
60
+ header {
61
+ background-color: var(--header-bg);
62
+ color: var(--header-color);
63
+ padding: 8px 20px;
64
+ box-shadow: var(--shadow);
65
+ z-index: 10;
66
+ border-bottom: 1px solid var(--border-color);
67
+ }
68
+
69
+ .header-content {
70
+ display: flex;
71
+ justify-content: space-between;
72
+ align-items: center;
73
+ width: 100%;
74
+ height: 40px; /* 固定高度 */
75
+ }
76
+
77
+ .header-content h1 {
78
+ font-size: 18px;
79
+ margin: 0;
80
+ font-weight: 600;
81
+ }
82
+
83
+ .logout-btn {
84
+ background-color: var(--primary-color);
85
+ color: white;
86
+ border: none;
87
+ border-radius: var(--radius);
88
+ padding: 6px 12px;
89
+ cursor: pointer;
90
+ font-size: 14px;
91
+ transition: var(--transition);
92
+ display: flex;
93
+ align-items: center;
94
+ gap: 5px;
95
+ }
96
+
97
+ .logout-btn:hover {
98
+ background-color: var(--primary-hover);
99
+ }
100
+
101
+ .dashboard {
102
+ display: flex;
103
+ flex: 1;
104
+ height: calc(100vh - 100px);
105
+ overflow: hidden;
106
+ margin: 0;
107
+ padding: 15px;
108
+ gap: 15px;
109
+ }
110
+
111
+ .sidebar {
112
+ width: 200px;
113
+ height: 100%;
114
+ background-color: var(--sidebar-bg);
115
+ color: var(--sidebar-color);
116
+ transition: width 0.3s ease;
117
+ border-radius: var(--radius);
118
+ box-shadow: var(--shadow);
119
+ overflow: hidden;
120
+ display: flex;
121
+ flex-direction: column;
122
+ }
123
+
124
+ .sidebar.collapsed {
125
+ width: 80px;
126
+ }
127
+
128
+ .sidebar.collapsed .sidebar-header h3 {
129
+ display: none;
130
+ }
131
+
132
+ .sidebar-header {
133
+ padding: 15px;
134
+ display: flex;
135
+ justify-content: space-between;
136
+ align-items: center;
137
+ border-bottom: 1px solid var(--border-color);
138
+ }
139
+
140
+ .sidebar-header h3 {
141
+ margin: 0;
142
+ color: var(--text-color);
143
+ font-size: 16px;
144
+ font-weight: 600;
145
+ white-space: nowrap;
146
+ }
147
+
148
+ .toggle-btn {
149
+ background: transparent;
150
+ border: none;
151
+ color: var(--text-secondary);
152
+ cursor: pointer;
153
+ font-size: 16px;
154
+ padding: 5px;
155
+ display: flex;
156
+ align-items: center;
157
+ justify-content: center;
158
+ transition: transform 0.3s, background-color 0.2s;
159
+ border-radius: 4px;
160
+ }
161
+
162
+ .toggle-btn:hover {
163
+ background-color: #f1f1f1;
164
+ }
165
+
166
+ .sidebar.collapsed .toggle-btn {
167
+ transform: rotate(180deg);
168
+ }
169
+
170
+ .sidebar-menu {
171
+ display: flex;
172
+ flex-direction: column;
173
+ padding: 10px 0;
174
+ flex: 1;
175
+ }
176
+
177
+ .menu-item {
178
+ padding: 10px 15px;
179
+ display: flex;
180
+ align-items: center;
181
+ cursor: pointer;
182
+ transition: var(--transition);
183
+ white-space: nowrap;
184
+ border-radius: 4px;
185
+ margin: 2px 8px;
186
+ }
187
+
188
+ .menu-item:hover {
189
+ background-color: var(--sidebar-hover);
190
+ color: var(--primary-color);
191
+ }
192
+
193
+ .menu-item.active {
194
+ background-color: var(--sidebar-active);
195
+ color: var(--primary-color);
196
+ font-weight: 500;
197
+ }
198
+
199
+ .menu-item i {
200
+ font-size: 18px;
201
+ margin-right: 12px;
202
+ }
203
+
204
+ .sidebar.collapsed .menu-item {
205
+ padding: 10px 8px;
206
+ justify-content: center;
207
+ }
208
+
209
+ .sidebar.collapsed .menu-text {
210
+ display: none;
211
+ }
212
+
213
+ .sidebar.collapsed .menu-item i {
214
+ margin-right: 0;
215
+ font-size: 20px;
216
+ }
217
+
218
+ .sidebar.collapsed .sidebar-header {
219
+ justify-content: center;
220
+ padding: 15px 5px;
221
+ }
222
+
223
+ /* 主内容区样式优化 */
224
+ .main-content {
225
+ flex: 1;
226
+ height: 100%;
227
+ overflow: hidden;
228
+ display: flex;
229
+ flex-direction: column;
230
+ background-color: var(--card-bg);
231
+ border-radius: var(--radius);
232
+ box-shadow: var(--shadow);
233
+ }
234
+
235
+ .content-panel {
236
+ padding: 20px;
237
+ display: none;
238
+ flex: 1;
239
+ overflow-y: auto;
240
+ height: 100%;
241
+ flex-direction: column;
242
+ }
243
+
244
+ .content-panel.active {
245
+ display: flex;
246
+ flex-direction: column;
247
+ }
248
+
249
+ /* 添加token列表容器样式,使其可滚动 */
250
+ .token-list-container {
251
+ flex: 1;
252
+ overflow-y: auto;
253
+ margin-bottom: 15px;
254
+ }
255
+
256
+ /* 面板标题样式优化 */
257
+ .panel-title {
258
+ display: flex;
259
+ align-items: center;
260
+ margin-bottom: 20px;
261
+ padding-bottom: 12px;
262
+ border-bottom: 1px solid var(--border-color);
263
+ }
264
+
265
+ .panel-title h2 {
266
+ margin: 0 0 0 10px;
267
+ font-size: 18px;
268
+ font-weight: 600;
269
+ color: var(--text-color);
270
+ }
271
+
272
+ .panel-title i {
273
+ font-size: 20px;
274
+ color: var(--primary-color);
275
+ }
276
+
277
+ .panel-actions {
278
+ margin-left: auto;
279
+ display: flex;
280
+ gap: 10px;
281
+ }
282
+
283
+ /* 按钮样式统一 */
284
+ button {
285
+ background-color: var(--primary-color);
286
+ color: white;
287
+ border: none;
288
+ border-radius: var(--radius);
289
+ padding: 8px 12px;
290
+ cursor: pointer;
291
+ font-size: 14px;
292
+ transition: var(--transition);
293
+ display: flex;
294
+ align-items: center;
295
+ gap: 5px;
296
+ }
297
+
298
+ button:hover {
299
+ background-color: var(--primary-hover);
300
+ }
301
+
302
+ button.secondary {
303
+ background-color: transparent;
304
+ color: var(--text-color);
305
+ border: 1px solid var(--border-color);
306
+ }
307
+
308
+ button.secondary:hover {
309
+ background-color: #f8f9fa;
310
+ }
311
+
312
+ button.secondary i {
313
+ color: var(--text-color);
314
+ }
315
+
316
+ /* Token列表样式优化 */
317
+ .token-list {
318
+ display: flex;
319
+ flex-direction: column;
320
+ gap: 10px;
321
+ margin-bottom: 15px;
322
+ }
323
+
324
+ .token-item {
325
+ border: 1px solid var(--border-color);
326
+ border-radius: var(--radius);
327
+ overflow: hidden;
328
+ transition: var(--transition);
329
+ margin-bottom: 10px;
330
+ }
331
+
332
+ .token-item:hover {
333
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
334
+ }
335
+
336
+ .token-header {
337
+ display: flex;
338
+ align-items: center;
339
+ padding: 12px 15px;
340
+ background-color: #f8f9fa;
341
+ cursor: pointer;
342
+ }
343
+
344
+ .token-number {
345
+ width: 30px;
346
+ font-weight: 500;
347
+ color: var(--text-secondary);
348
+ }
349
+
350
+ .token-summary {
351
+ flex: 1;
352
+ font-family: monospace;
353
+ color: var(--text-color);
354
+ white-space: nowrap;
355
+ overflow: hidden;
356
+ text-overflow: ellipsis;
357
+ font-size: 13px;
358
+ display: flex;
359
+ align-items: center;
360
+ gap: 8px;
361
+ }
362
+
363
+ .token-remark {
364
+ background-color: #e3f2fd;
365
+ color: #1976d2;
366
+ padding: 2px 8px;
367
+ border-radius: 4px;
368
+ font-size: 12px;
369
+ font-family: system-ui;
370
+ cursor: pointer;
371
+ border: 1px solid transparent;
372
+ transition: all 0.2s;
373
+ }
374
+
375
+ .token-remark:hover {
376
+ border-color: #1976d2;
377
+ }
378
+
379
+ .token-remark.empty {
380
+ background-color: #f5f5f5;
381
+ color: #9e9e9e;
382
+ }
383
+
384
+ .token-remark input {
385
+ background: none;
386
+ border: none;
387
+ outline: none;
388
+ font-size: inherit;
389
+ font-family: inherit;
390
+ color: inherit;
391
+ width: 100%;
392
+ min-width: 100px;
393
+ }
394
+
395
+ .token-usage-count {
396
+ display: flex;
397
+ align-items: center;
398
+ justify-content: center;
399
+ color: var(--text-color);
400
+ font-size: 13px;
401
+ font-weight: 500;
402
+ margin-left: 10px;
403
+ }
404
+
405
+ /* 根据使用次数变化颜色 - 只应用于数字 */
406
+ .token-usage-count .low {
407
+ color: #28a745; /* 绿色 - 使用次数少 */
408
+ }
409
+
410
+ .token-usage-count .medium {
411
+ color: #ffc107; /* 黄色 - 使用次数中等 */
412
+ }
413
+
414
+ .token-usage-count .high {
415
+ color: #dc3545; /* 红色 - 使用次数多 */
416
+ }
417
+
418
+ .token-toggle {
419
+ margin-left: 10px;
420
+ transition: transform 0.3s;
421
+ }
422
+
423
+ .token-toggle.open i {
424
+ transform: rotate(180deg);
425
+ }
426
+
427
+ .token-details {
428
+ padding: 0;
429
+ max-height: 0;
430
+ overflow: hidden;
431
+ transition: all 0.3s ease;
432
+ background-color: #ffffff;
433
+ }
434
+
435
+ .token-details.open {
436
+ padding: 15px;
437
+ max-height: 200px;
438
+ border-top: 1px solid var(--border-color);
439
+ overflow-y: auto;
440
+ }
441
+
442
+ .token-label {
443
+ font-weight: 500;
444
+ margin-bottom: 5px;
445
+ color: var(--text-secondary);
446
+ font-size: 13px;
447
+ }
448
+
449
+ .token-display {
450
+ padding: 8px 10px;
451
+ background-color: #f8f9fa;
452
+ border-radius: 4px;
453
+ font-family: monospace;
454
+ margin-bottom: 10px;
455
+ word-break: break-all;
456
+ font-size: 13px;
457
+ overflow-x: auto;
458
+ }
459
+
460
+ .token-actions {
461
+ display: flex;
462
+ justify-content: flex-end;
463
+ margin-top: 10px;
464
+ }
465
+
466
+ .delete-token {
467
+ background-color: var(--error-color);
468
+ }
469
+
470
+ .delete-token:hover {
471
+ background-color: #c82333;
472
+ }
473
+
474
+ /* 分页控件样式优化 */
475
+ .pagination-container {
476
+ display: flex;
477
+ justify-content: center;
478
+ align-items: center;
479
+ padding: 15px 0;
480
+ border-top: 1px solid var(--border-color);
481
+ }
482
+
483
+ .pagination-btn {
484
+ background-color: transparent;
485
+ border: 1px solid var(--border-color);
486
+ color: var(--text-color);
487
+ border-radius: var(--radius);
488
+ padding: 6px 10px;
489
+ margin: 0 5px;
490
+ cursor: pointer;
491
+ transition: var(--transition);
492
+ }
493
+
494
+ .pagination-btn:hover:not([disabled]) {
495
+ background-color: var(--primary-color);
496
+ color: white;
497
+ border-color: var(--primary-color);
498
+ }
499
+
500
+ .pagination-btn[disabled] {
501
+ opacity: 0.5;
502
+ cursor: not-allowed;
503
+ }
504
+
505
+ #page-info {
506
+ margin: 0 15px;
507
+ font-size: 14px;
508
+ color: var(--text-secondary);
509
+ }
510
+
511
+ .page-size-select {
512
+ margin-left: 15px;
513
+ padding: 6px 8px;
514
+ border-radius: var(--radius);
515
+ border: 1px solid var(--border-color);
516
+ background-color: white;
517
+ color: var(--text-color);
518
+ font-size: 14px;
519
+ cursor: pointer;
520
+ }
521
+
522
+ /* 页脚样式优化 */
523
+ footer {
524
+ text-align: center;
525
+ padding: 10px 0;
526
+ background-color: var(--footer-bg);
527
+ color: var(--footer-color);
528
+ font-size: 13px;
529
+ border-top: 1px solid var(--border-color);
530
+ }
531
+
532
+ footer a {
533
+ color: var(--primary-color);
534
+ text-decoration: none;
535
+ }
536
+
537
+ footer a:hover {
538
+ text-decoration: underline;
539
+ }
540
+
541
+ /* 响应式设计优化 */
542
+ @media (max-width: 768px) {
543
+ .dashboard {
544
+ flex-direction: column;
545
+ height: auto;
546
+ padding: 10px;
547
+ }
548
+
549
+ .sidebar {
550
+ width: 100% !important;
551
+ margin-bottom: 15px;
552
+ max-height: 200px;
553
+ }
554
+
555
+ .main-content {
556
+ height: calc(100vh - 300px);
557
+ overflow-y: auto;
558
+ }
559
+
560
+ .content-panel {
561
+ padding: 15px;
562
+ max-height: none;
563
+ }
564
+
565
+ .sidebar.collapsed .menu-text {
566
+ display: inline;
567
+ }
568
+
569
+ .toggle-btn {
570
+ display: none;
571
+ }
572
+ }
573
+
574
+ @media (max-width: 1200px) {
575
+ .token-display {
576
+ max-width: 100%;
577
+ white-space: nowrap;
578
+ }
579
+ }
580
+
581
+ @media (min-width: 1201px) {
582
+ .token-display {
583
+ white-space: normal;
584
+ }
585
+ }
586
+
587
+ .token-details.open .token-display {
588
+ white-space: normal;
589
+ word-break: break-all;
590
+ }
591
+
592
+ /* 添加Token面板样式优化 */
593
+ .auth-steps {
594
+ display: flex;
595
+ flex-direction: column;
596
+ gap: 25px;
597
+ padding-bottom: 20px;
598
+ }
599
+
600
+ .step {
601
+ background-color: #f8f9fa;
602
+ border-radius: var(--radius);
603
+ padding: 20px;
604
+ border: 1px solid var(--border-color);
605
+ }
606
+
607
+ .step h3 {
608
+ display: flex;
609
+ align-items: center;
610
+ margin-top: 0;
611
+ margin-bottom: 15px;
612
+ font-size: 16px;
613
+ font-weight: 600;
614
+ }
615
+
616
+ .step-number {
617
+ display: inline-flex;
618
+ align-items: center;
619
+ justify-content: center;
620
+ width: 24px;
621
+ height: 24px;
622
+ background-color: var(--primary-color);
623
+ color: white;
624
+ border-radius: 50%;
625
+ margin-right: 10px;
626
+ font-size: 14px;
627
+ }
628
+
629
+ .step p {
630
+ margin-bottom: 15px;
631
+ color: var(--text-secondary);
632
+ }
633
+
634
+ textarea {
635
+ width: 100%;
636
+ padding: 10px;
637
+ border: 1px solid var(--border-color);
638
+ border-radius: var(--radius);
639
+ font-family: monospace;
640
+ min-height: 100px;
641
+ margin-bottom: 10px;
642
+ resize: vertical;
643
+ }
644
+
645
+ .error {
646
+ color: var(--error-color);
647
+ margin: 10px 0;
648
+ display: none;
649
+ }
650
+
651
+ .success {
652
+ color: var(--success-color);
653
+ margin: 10px 0;
654
+ display: none;
655
+ }
656
+
657
+ button:not(.secondary):not(.pagination-btn):not(.toggle-btn) i {
658
+ color: white;
659
+ }
660
+
661
+ /* 刷新按钮加载动画 */
662
+ .refresh-btn, #check-all-tokens {
663
+ position: relative;
664
+ }
665
+
666
+ .refresh-btn i, #check-all-tokens i {
667
+ transition: transform 0.3s ease;
668
+ }
669
+
670
+ .refresh-btn.loading i, #check-all-tokens.loading i {
671
+ animation: spin 1s linear infinite;
672
+ }
673
+
674
+ @keyframes spin {
675
+ 0% { transform: rotate(0deg); }
676
+ 100% { transform: rotate(360deg); }
677
+ }
678
+
679
+ /* 禁用状态 */
680
+ .refresh-btn.loading, #check-all-tokens.loading {
681
+ pointer-events: none;
682
+ opacity: 0.7;
683
+ }
684
+
685
+ /* 检测结果样式 */
686
+ .check-result {
687
+ color: var(--success-color);
688
+ background-color: rgba(40, 167, 69, 0.1);
689
+ border: 1px solid var(--success-color);
690
+ border-radius: var(--radius);
691
+ padding: 10px 15px;
692
+ margin: 10px 0;
693
+ display: none;
694
+ }
695
+
696
+
697
+ /* 弹出输入框样式 */
698
+ .remark-input-modal {
699
+ position: fixed;
700
+ top: 0;
701
+ left: 0;
702
+ width: 100%;
703
+ height: 100%;
704
+ background-color: rgba(0, 0, 0, 0.5);
705
+ display: flex;
706
+ justify-content: center;
707
+ align-items: center;
708
+ z-index: 1000;
709
+ }
710
+
711
+ .remark-input-container {
712
+ background-color: white;
713
+ padding: 20px;
714
+ border-radius: var(--radius);
715
+ box-shadow: var(--shadow);
716
+ width: 250px;
717
+ }
718
+
719
+ .remark-input-container h3 {
720
+ margin: 0 0 15px 0;
721
+ font-size: 16px;
722
+ color: var(--text-color);
723
+ }
724
+
725
+ .remark-input-container input {
726
+ width: 100%;
727
+ padding: 8px 10px;
728
+ border: 1px solid var(--border-color);
729
+ border-radius: 4px;
730
+ margin-bottom: 15px;
731
+ font-size: 14px;
732
+ box-sizing: border-box;
733
+ }
734
+
735
+ .remark-input-container .char-count {
736
+ font-size: 12px;
737
+ color: var(--text-secondary);
738
+ margin-bottom: 15px;
739
+ text-align: right;
740
+ }
741
+
742
+ .remark-input-actions {
743
+ display: flex;
744
+ justify-content: flex-end;
745
+ gap: 10px;
746
+ }
747
+
748
+ /* Token模糊化样式 */
749
+ .token-blur {
750
+ filter: blur(3px);
751
+ transition: all 0.3s ease;
752
+ }
753
+
754
+ .token-blur:hover {
755
+ filter: blur(0);
756
+ }
757
+
758
+ /* 删除背景颜色变化样式 */
759
+ #toggle-token-visibility.active i {
760
+ color: var(--primary-color);
761
+ }
762
+
763
+ /* 冷却状态图标样式 */
764
+ .cool-status {
765
+ color: #1e88e5;
766
+ font-size: 18px;
767
+ margin-left: 5px;
768
+ vertical-align: middle;
769
+ }
770
+
771
+ .cool-status-tooltip {
772
+ position: relative;
773
+ display: inline-block;
774
+ }
775
+
776
+ .cool-status-tooltip .tooltip-text {
777
+ visibility: hidden;
778
+ width: 200px;
779
+ background-color: #333;
780
+ color: #fff;
781
+ text-align: center;
782
+ border-radius: 6px;
783
+ padding: 5px;
784
+ position: absolute;
785
+ z-index: 1;
786
+ bottom: 125%;
787
+ left: 50%;
788
+ margin-left: -100px;
789
+ opacity: 0;
790
+ transition: opacity 0.3s;
791
+ font-size: 12px;
792
+ }
793
+
794
+ .cool-status-tooltip:hover .tooltip-text {
795
+ visibility: visible;
796
+ opacity: 1;
797
+ }
798
+ </style>
799
+ <!-- 添加图标 -->
800
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
801
+ </head>
802
+ <body>
803
+ <div class="container">
804
+ <header>
805
+ <div class="header-content">
806
+ <h1>Augment面板|v1.0.2</h1>
807
+ <button id="logout-btn" class="logout-btn">
808
+ <i class="bi bi-box-arrow-right"></i> 登出
809
+ </button>
810
+ </div>
811
+ </header>
812
+
813
+ <div class="dashboard">
814
+ <!-- 左侧导航栏 -->
815
+ <div class="sidebar" id="sidebar">
816
+ <div class="sidebar-header">
817
+ <h3>面板功能导航</h3>
818
+ <button id="toggle-sidebar" class="toggle-btn">
819
+ <i class="bi bi-chevron-left"></i>
820
+ </button>
821
+ </div>
822
+ <div class="sidebar-menu">
823
+ <div class="menu-item active" data-target="token-list-panel">
824
+ <i class="bi bi-list-ul"></i>
825
+ <span class="menu-text">Token列表</span>
826
+ </div>
827
+ <div class="menu-item" data-target="token-add-panel">
828
+ <i class="bi bi-plus-circle"></i>
829
+ <span class="menu-text">添加Token</span>
830
+ </div>
831
+ </div>
832
+ </div>
833
+
834
+ <!-- 右侧主内容区 -->
835
+ <div class="main-content" id="main-content">
836
+ <!-- Token列表面板 -->
837
+ <div class="content-panel active" id="token-list-panel">
838
+ <div class="panel-title">
839
+ <i class="bi bi-key-fill"></i>
840
+ <h2>Token列表</h2>
841
+ <div class="panel-actions">
842
+ <button id="toggle-token-visibility" class="secondary">
843
+ <i class="bi bi-eye-slash"></i> 隐藏Token
844
+ </button>
845
+ <button id="refresh-token" class="refresh-btn">
846
+ <i class="bi bi-arrow-clockwise"></i> 刷新列表
847
+ </button>
848
+ <button id="check-all-tokens"><i class="bi bi-shield-check btn-icon"></i> <span class="btn-text">批量检测</span></button>
849
+ </div>
850
+ </div>
851
+
852
+ <!-- 添加可滚动容器 -->
853
+ <div class="token-list-container">
854
+ <div id="token-list">加载中...</div>
855
+ </div>
856
+
857
+ <!-- 分页控件 -->
858
+ <div class="pagination-container" id="pagination-container">
859
+ <button class="pagination-btn" id="prev-page" disabled><i class="bi bi-chevron-left"></i></button>
860
+ <span id="page-info">第 <span id="current-page">1</span> 页,共 <span id="total-pages">1</span> 页</span>
861
+ <button class="pagination-btn" id="next-page"><i class="bi bi-chevron-right"></i></button>
862
+ <select id="page-size" class="page-size-select">
863
+ <option value="10">10条/页</option>
864
+ <option value="20">20条/页</option>
865
+ <option value="50">50条/页</option>
866
+ </select>
867
+ </div>
868
+ </div>
869
+
870
+ <!-- 添加Token面板 -->
871
+ <div class="content-panel" id="token-add-panel">
872
+ <div class="panel-title">
873
+ <i class="bi bi-shield-lock-fill"></i>
874
+ <h2>授权获取Token</h2>
875
+ </div>
876
+
877
+ <div class="auth-steps">
878
+ <div class="step">
879
+ <h3><span class="step-number">1</span> 获取授权地址</h3>
880
+ <p>点击下方按钮获取授权���址,然后在浏览器中打开该地址进行授权。</p>
881
+ <button id="get-auth-url"><i class="bi bi-link-45deg" class="btn-icon"></i> <span class="btn-text">获取授权地址</span></button>
882
+ <div id="auth-url" class="token-display" style="display: none;"></div>
883
+ </div>
884
+
885
+ <div class="step">
886
+ <h3><span class="step-number">2</span> 提交授权响应</h3>
887
+ <p>完成授权后,将获得的授权响应粘贴到下面的文本框中:</p>
888
+ <textarea id="auth-response" placeholder='{"code":"_000baec407c57c4bf9xxxxxxxxxxxxxx","state":"0uXxxxxxxxx","tenant_url":"https://dxx.api.augmentcode.com/"}'></textarea>
889
+ <div id="validation-message" class="error"></div>
890
+ <button id="submit-auth"><i class="bi bi-check2-circle" class="btn-text"></i> <span class="btn-text">获取Token</span></button>
891
+ <div id="submit-result" class="success"></div>
892
+ </div>
893
+ </div>
894
+ </div>
895
+ </div>
896
+ </div>
897
+
898
+ <!-- 添加页脚 -->
899
+ <footer>
900
+ <a href="https://linux.do/u/bifang/summary" target="_blank">开发者:彼方</a> | <a href="https://2api-docs.pages.dev/page/augment2api/func-intro" target="_blank">文档中心</a>
901
+ </footer>
902
+ </div>
903
+
904
+ <script>
905
+ // 检查会话是否有效
906
+ function checkSession() {
907
+ // 从Cookie中获取token
908
+ const cookies = document.cookie.split(';');
909
+ let token = null;
910
+
911
+ for (let i = 0; i < cookies.length; i++) {
912
+ const cookie = cookies[i].trim();
913
+ if (cookie.startsWith('auth_token=')) {
914
+ token = cookie.substring('auth_token='.length);
915
+ break;
916
+ }
917
+ }
918
+
919
+ if (!token) {
920
+ window.location.href = '/login';
921
+ return false;
922
+ }
923
+
924
+ // 将token添加到所有API请求中
925
+ return token;
926
+ }
927
+
928
+ // 页面加载时检查会话
929
+ const authToken = checkSession();
930
+ if (!authToken) {
931
+ // 如果没有有效会话,不继续执行后续代码
932
+ throw new Error('No valid session');
933
+ }
934
+
935
+ // 为所有fetch请求添加认证头
936
+ const originalFetch = window.fetch;
937
+ window.fetch = function(url, options = {}) {
938
+ // 创建新的options对象,避免修改原始对象
939
+ const newOptions = { ...options };
940
+
941
+ // 确保headers对象存在
942
+ newOptions.headers = newOptions.headers || {};
943
+
944
+ // 如果是对象形式,转换为Headers对象
945
+ if (!(newOptions.headers instanceof Headers)) {
946
+ const headers = new Headers(newOptions.headers);
947
+ headers.append('X-Auth-Token', authToken);
948
+ newOptions.headers = headers;
949
+ } else {
950
+ newOptions.headers.append('X-Auth-Token', authToken);
951
+ }
952
+
953
+ return originalFetch(url, newOptions);
954
+ };
955
+
956
+ document.addEventListener('DOMContentLoaded', function() {
957
+ // 侧边栏切换
958
+ const sidebar = document.getElementById('sidebar');
959
+ const toggleBtn = document.getElementById('toggle-sidebar');
960
+ const menuItems = document.querySelectorAll('.menu-item');
961
+ const contentPanels = document.querySelectorAll('.content-panel');
962
+
963
+ // 分页变量
964
+ let currentPage = 1;
965
+ let pageSize = 10;
966
+ let allTokens = [];
967
+
968
+ // 侧边栏折叠/展开
969
+ toggleBtn.addEventListener('click', function() {
970
+ sidebar.classList.toggle('collapsed');
971
+ });
972
+
973
+ // 菜单项切换
974
+ menuItems.forEach(item => {
975
+ item.addEventListener('click', function() {
976
+ // 移除所有菜单项的active类
977
+ menuItems.forEach(i => i.classList.remove('active'));
978
+ // 为当前点击的菜单项添加active类
979
+ this.classList.add('active');
980
+
981
+ // 获取目标面板ID
982
+ const targetId = this.getAttribute('data-target');
983
+
984
+ // 隐藏所有内容面板
985
+ contentPanels.forEach(panel => {
986
+ panel.classList.remove('active');
987
+ });
988
+
989
+ // 显示目标面板
990
+ document.getElementById(targetId).classList.add('active');
991
+ });
992
+ });
993
+
994
+ // 分页功能
995
+ const prevPageBtn = document.getElementById('prev-page');
996
+ const nextPageBtn = document.getElementById('next-page');
997
+ const currentPageSpan = document.getElementById('current-page');
998
+ const totalPagesSpan = document.getElementById('total-pages');
999
+ const pageSizeSelect = document.getElementById('page-size');
1000
+
1001
+ // 页面大小变化
1002
+ pageSizeSelect.addEventListener('change', function() {
1003
+ pageSize = parseInt(this.value);
1004
+ currentPage = 1;
1005
+ fetchCurrentToken();
1006
+ });
1007
+
1008
+ // 上一页
1009
+ prevPageBtn.addEventListener('click', function() {
1010
+ if (currentPage > 1) {
1011
+ currentPage--;
1012
+ fetchCurrentToken();
1013
+ }
1014
+ });
1015
+
1016
+ // 下一页
1017
+ nextPageBtn.addEventListener('click', function() {
1018
+ currentPage++;
1019
+ fetchCurrentToken();
1020
+ });
1021
+
1022
+ // 修改获取当前Token列表函数,使用后端分页
1023
+ function fetchCurrentToken() {
1024
+ // 显示加载动画
1025
+ const refreshBtn = document.getElementById('refresh-token');
1026
+ const tokenListElement = document.getElementById('token-list');
1027
+ refreshBtn.classList.add('loading');
1028
+
1029
+ // 设置超时处理
1030
+ const timeoutId = setTimeout(() => {
1031
+ refreshBtn.classList.remove('loading');
1032
+ tokenListElement.innerHTML = '<div class="error" style="display:block;">请求超时,请重试</div>';
1033
+ }, 10000); // 10秒超时
1034
+
1035
+ fetch(`/api/tokens?page=${currentPage}&page_size=${pageSize}`)
1036
+ .then(response => {
1037
+ if (!response.ok) {
1038
+ throw new Error(`HTTP error! status: ${response.status}`);
1039
+ }
1040
+ return response.json();
1041
+ })
1042
+ .then(data => {
1043
+ clearTimeout(timeoutId); // 清除超时定时器
1044
+
1045
+ if (data.status === 'success') {
1046
+ // 使用后端返回的token列表
1047
+ allTokens = data.tokens || [];
1048
+
1049
+ // 更新分页信息
1050
+ const totalItems = data.total || 0;
1051
+ const totalPages = data.total_pages || 1;
1052
+ currentPage = data.page || 1;
1053
+
1054
+ // 渲染token列表和分页控件
1055
+ renderTokenList(totalItems, totalPages);
1056
+ } else {
1057
+ tokenListElement.innerHTML =
1058
+ '<div class="error" style="display:block;">获取Token列表失败: ' + (data.error || '未知错误') + '</div>';
1059
+ }
1060
+ })
1061
+ .catch(error => {
1062
+ clearTimeout(timeoutId); // 清除超时定时器
1063
+ tokenListElement.innerHTML =
1064
+ '<div class="error" style="display:block;">请求失败: ' + error.message + '</div>';
1065
+ })
1066
+ .finally(() => {
1067
+ refreshBtn.classList.remove('loading');
1068
+ });
1069
+ }
1070
+
1071
+ // 修改渲染token列表函数,使用后端返回的分页信息
1072
+ function renderTokenList(totalItems, totalPages) {
1073
+ const tokenListElement = document.getElementById('token-list');
1074
+
1075
+ // 如果没有token
1076
+ if (totalItems === 0) {
1077
+ tokenListElement.innerHTML = '<div class="no-tokens">暂无可用Token,请点击"添加Token"获取</div>';
1078
+
1079
+ // 隐藏分页控件
1080
+ document.getElementById('pagination-container').style.display = 'none';
1081
+ return;
1082
+ }
1083
+
1084
+ // 显示分页控件
1085
+ document.getElementById('pagination-container').style.display = 'flex';
1086
+
1087
+ // 更新分页信息显示
1088
+ totalPagesSpan.textContent = totalPages;
1089
+ currentPageSpan.textContent = currentPage;
1090
+
1091
+ // 更新分页按钮状态
1092
+ prevPageBtn.disabled = currentPage === 1;
1093
+ nextPageBtn.disabled = currentPage === totalPages;
1094
+
1095
+ // 创建token列表容器
1096
+ tokenListElement.innerHTML = '<div class="token-list"></div>';
1097
+ const listContainer = tokenListElement.querySelector('.token-list');
1098
+
1099
+ // 添加每个token项
1100
+ allTokens.forEach((tokenInfo, index) => {
1101
+ // 计算在当前页中���索引
1102
+ const displayIndex = index + 1 + (currentPage - 1) * pageSize;
1103
+
1104
+ // 获取使用次数并设置样式类
1105
+ const usageCount = tokenInfo.usage_count || 0;
1106
+ const chatUsageCount = tokenInfo.chat_usage_count || 0;
1107
+ const agentUsageCount = tokenInfo.agent_usage_count || 0;
1108
+ let usageClass = '';
1109
+
1110
+ // 根据CHAT和AGENT模式的使用次数来确定样式类
1111
+ if (chatUsageCount < 1000 && agentUsageCount < 20) {
1112
+ usageClass = 'low';
1113
+ } else if (chatUsageCount < 2000 && agentUsageCount < 40) {
1114
+ usageClass = 'medium';
1115
+ } else {
1116
+ usageClass = 'high';
1117
+ }
1118
+
1119
+ // 显示完整token,不再截断
1120
+ const tokenItem = document.createElement('div');
1121
+ tokenItem.className = 'token-item';
1122
+ tokenItem.innerHTML = `
1123
+ <div class="token-header">
1124
+ <div class="token-number">${displayIndex}</div>
1125
+ <div class="token-summary">
1126
+ ${tokenInfo.token}
1127
+ <span class="token-remark${!tokenInfo.remark ? ' empty' : ''}" data-token="${tokenInfo.token}" data-remark="${tokenInfo.remark || ''}">${tokenInfo.remark || '添加备注'}</span>
1128
+ ${tokenInfo.in_cool ? `
1129
+ <span class="cool-status-tooltip">
1130
+ <i class="bi bi-snow cool-status"></i>
1131
+ <span class="tooltip-text">冷却中,直到: ${new Date(tokenInfo.cool_end).toLocaleString()}</span>
1132
+ </span>` : ''}
1133
+ </div>
1134
+ <div class="token-usage-count">
1135
+ CHAT使用:&nbsp;&nbsp; <span class="${usageClass}">${chatUsageCount}</span>&nbsp;&nbsp;次 | AGENT使用:&nbsp;&nbsp; <span class="${usageClass}">${agentUsageCount}</span>&nbsp;&nbsp;次
1136
+ </div>
1137
+ <div class="token-toggle"><i class="bi bi-chevron-down"></i></div>
1138
+ </div>
1139
+ <div class="token-details">
1140
+ <div class="token-label">Token:</div>
1141
+ <div class="token-display">${tokenInfo.token}</div>
1142
+ <div class="token-label">租户URL:</div>
1143
+ <div class="token-display">${tokenInfo.tenant_url}</div>
1144
+ <div class="token-actions">
1145
+ <button class="delete-token" data-token="${tokenInfo.token}">
1146
+ <i class="bi bi-trash"></i> 删除
1147
+ </button>
1148
+ </div>
1149
+ </div>
1150
+ `;
1151
+
1152
+ listContainer.appendChild(tokenItem);
1153
+
1154
+ // 添加事件处理
1155
+ const header = tokenItem.querySelector('.token-header');
1156
+ const details = tokenItem.querySelector('.token-details');
1157
+ const toggle = tokenItem.querySelector('.token-toggle');
1158
+ const remarkSpan = tokenItem.querySelector('.token-remark');
1159
+
1160
+ // 为备注添加点击事件
1161
+ remarkSpan.addEventListener('click', function(e) {
1162
+ e.stopPropagation(); // 阻止事件冒泡到header
1163
+
1164
+ const token = this.dataset.token;
1165
+ const currentRemark = this.dataset.remark;
1166
+
1167
+ // 创建弹出层
1168
+ const modal = document.createElement('div');
1169
+ modal.className = 'remark-input-modal';
1170
+
1171
+ modal.innerHTML = `
1172
+ <div class="remark-input-container">
1173
+ <h3>编辑备注</h3>
1174
+ <input type="text" maxlength="30" placeholder="请输入备注(30字以内)" value="${currentRemark}">
1175
+ <div class="char-count"><span>${currentRemark.length}</span>/30</div>
1176
+ <div class="remark-input-actions">
1177
+ <button class="secondary" onclick="this.closest('.remark-input-modal').remove()">取消</button>
1178
+ <button class="save-remark" data-token="${token}">保存</button>
1179
+ </div>
1180
+ </div>
1181
+ `;
1182
+
1183
+ document.body.appendChild(modal);
1184
+
1185
+ // 获取输入框并聚焦
1186
+ const input = modal.querySelector('input');
1187
+ input.focus();
1188
+
1189
+ // 更新字符计数
1190
+ input.addEventListener('input', () => {
1191
+ const count = input.value.length;
1192
+ modal.querySelector('.char-count span').textContent = count;
1193
+ });
1194
+
1195
+ // 点击背景关闭弹窗
1196
+ modal.addEventListener('click', (e) => {
1197
+ if (e.target === modal) {
1198
+ modal.remove();
1199
+ }
1200
+ });
1201
+
1202
+ // 处理保存按钮点击
1203
+ modal.querySelector('.save-remark').addEventListener('click', async function() {
1204
+ const token = this.dataset.token;
1205
+ const newRemark = input.value.trim();
1206
+
1207
+ try {
1208
+ const response = await fetch(`/api/token/${token}/remark`, {
1209
+ method: 'PUT',
1210
+ headers: {
1211
+ 'Content-Type': 'application/json'
1212
+ },
1213
+ body: JSON.stringify({ remark: newRemark })
1214
+ });
1215
+
1216
+ const data = await response.json();
1217
+ if (data.status === 'success') {
1218
+ // 关闭弹窗
1219
+ modal.remove();
1220
+ // 刷新列表
1221
+ fetchCurrentToken();
1222
+ } else {
1223
+ alert('更新备注失败: ' + (data.error || '未知错误'));
1224
+ }
1225
+ } catch (error) {
1226
+ alert('请求失败: ' + error.message);
1227
+ }
1228
+ });
1229
+ });
1230
+
1231
+ // 为header添加点击事件(展开/折叠详情)
1232
+ header.addEventListener('click', function() {
1233
+ details.classList.toggle('open');
1234
+ toggle.classList.toggle('open');
1235
+ });
1236
+ });
1237
+ }
1238
+
1239
+ // 为token列表添加事件委托,只处理删除按钮
1240
+ document.getElementById('token-list').addEventListener('click', function(e) {
1241
+ // 检查点击的是否是删除按钮
1242
+ if (e.target.closest('.delete-token')) {
1243
+ const deleteBtn = e.target.closest('.delete-token');
1244
+ const token = deleteBtn.getAttribute('data-token');
1245
+
1246
+ if (confirm('确定要删除此Token吗?')) {
1247
+ // 发送删除请求
1248
+ fetch(`/api/token/${encodeURIComponent(token)}`, {
1249
+ method: 'DELETE'
1250
+ })
1251
+ .then(response => response.json())
1252
+ .then(data => {
1253
+ if (data.status === 'success') {
1254
+ // 刷新token列表
1255
+ fetchCurrentToken();
1256
+ } else {
1257
+ alert('删除失败: ' + (data.error || '未知错误'));
1258
+ }
1259
+ })
1260
+ .catch(error => {
1261
+ alert('请求失败: ' + error.message);
1262
+ });
1263
+ }
1264
+ }
1265
+ });
1266
+
1267
+ // 刷新Token按钮事件
1268
+ document.getElementById('refresh-token').addEventListener('click', function() {
1269
+ fetchCurrentToken();
1270
+ });
1271
+
1272
+ // 添加登出处理逻辑
1273
+ document.getElementById('logout-btn').addEventListener('click', function() {
1274
+ if(confirm('确定要登出吗?')) {
1275
+ fetch('/api/logout', {
1276
+ method: 'POST',
1277
+ headers: {
1278
+ 'X-Auth-Token': authToken
1279
+ }
1280
+ })
1281
+ .then(response => response.json())
1282
+ .then(data => {
1283
+ if(data.status === 'success') {
1284
+ // 清除Cookie
1285
+ document.cookie = "auth_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT";
1286
+ // 重定向到登录页
1287
+ window.location.href = '/login';
1288
+ } else {
1289
+ alert('登出失败: ' + (data.error || '未知错误'));
1290
+ }
1291
+ })
1292
+ .catch(error => {
1293
+ alert('登出请求失败: ' + error.message);
1294
+ });
1295
+ }
1296
+ });
1297
+
1298
+ // 批量检测token
1299
+ document.getElementById('check-all-tokens').addEventListener('click', function() {
1300
+ const button = this;
1301
+ button.classList.add('loading');
1302
+
1303
+ fetch('/api/check-tokens')
1304
+ .then(response => response.json())
1305
+ .then(data => {
1306
+ if(data.status === 'success') {
1307
+ // 创建或获取检测结果显示元素
1308
+ let checkResult = document.querySelector('.check-result');
1309
+ if(!checkResult) {
1310
+ checkResult = document.createElement('div');
1311
+ checkResult.className = 'check-result';
1312
+ document.querySelector('.panel-title').after(checkResult);
1313
+ }
1314
+
1315
+ // 显示检测结果
1316
+ checkResult.textContent = `检测完成! 共检测 ${data.total} 个Token,更新 ${data.updated} 个Token租户地址,禁用 ${data.disabled} 个无效Token`;
1317
+ checkResult.style.display = 'block';
1318
+
1319
+ // 如果有更新或禁用,则刷新token列表
1320
+ if(data.updated > 0 || data.disabled > 0) {
1321
+ fetchCurrentToken();
1322
+ }
1323
+
1324
+ // 5秒后隐藏提示
1325
+ setTimeout(() => {
1326
+ checkResult.style.display = 'none';
1327
+ }, 5000);
1328
+ } else {
1329
+ alert('检测失败: ' + (data.error || '未知错误'));
1330
+ }
1331
+ })
1332
+ .catch(error => {
1333
+ alert('请求失败: ' + error.message);
1334
+ })
1335
+ .finally(() => {
1336
+ button.classList.remove('loading');
1337
+ });
1338
+ });
1339
+
1340
+ // 获取授权地址按钮事件
1341
+ document.getElementById('get-auth-url').addEventListener('click', function() {
1342
+ const button = this;
1343
+ const btnText = button.querySelector('.btn-text');
1344
+ const originalText = btnText.textContent;
1345
+
1346
+ // 显示加载状态
1347
+ button.disabled = true;
1348
+ btnText.textContent = '获取中...';
1349
+
1350
+ // 请求授权地址
1351
+ fetch('/auth')
1352
+ .then(response => response.json())
1353
+ .then(data => {
1354
+ if (data.authorize_url) {
1355
+ // 显示授权地址
1356
+ const authUrlElement = document.getElementById('auth-url');
1357
+ authUrlElement.textContent = data.authorize_url;
1358
+ authUrlElement.style.display = 'block';
1359
+ } else {
1360
+ alert('获取授权地址失败: ' + (data.error || '未知错误'));
1361
+ }
1362
+ })
1363
+ .catch(error => {
1364
+ alert('请求失败: ' + error.message);
1365
+ })
1366
+ .finally(() => {
1367
+ // 恢复按钮状态
1368
+ button.disabled = false;
1369
+ btnText.textContent = originalText;
1370
+ });
1371
+ });
1372
+
1373
+ // 验证JSON格式
1374
+ function isValidJSON(str) {
1375
+ try {
1376
+ JSON.parse(str);
1377
+ return true;
1378
+ } catch (e) {
1379
+ return false;
1380
+ }
1381
+ }
1382
+
1383
+ // 验证授权响应格式
1384
+ function validateAuthResponse(response) {
1385
+ try {
1386
+ const data = JSON.parse(response);
1387
+ // 检查必要字段
1388
+ if (!data.code || !data.state || !data.tenant_url) {
1389
+ return { valid: false, message: '缺少必要字段 (code, state, tenant_url)' };
1390
+ }
1391
+ return { valid: true, data: data };
1392
+ } catch (e) {
1393
+ return { valid: false, message: 'JSON格式无效: ' + e.message };
1394
+ }
1395
+ }
1396
+
1397
+ // 提��授权响应按钮事件
1398
+ document.getElementById('submit-auth').addEventListener('click', function() {
1399
+ const button = this;
1400
+ const btnText = button.querySelector('.btn-text');
1401
+ const originalText = btnText.textContent;
1402
+ const responseText = document.getElementById('auth-response').value.trim();
1403
+ const validationMessage = document.getElementById('validation-message');
1404
+ const submitResult = document.getElementById('submit-result');
1405
+
1406
+ // 重置消息
1407
+ validationMessage.style.display = 'none';
1408
+ submitResult.style.display = 'none';
1409
+
1410
+ // 验证输入
1411
+ if (!responseText) {
1412
+ validationMessage.textContent = '请输入授权响应';
1413
+ validationMessage.style.display = 'block';
1414
+ return;
1415
+ }
1416
+
1417
+ // 验证JSON格式
1418
+ if (!isValidJSON(responseText)) {
1419
+ validationMessage.textContent = 'JSON格式无效,请检查输入';
1420
+ validationMessage.style.display = 'block';
1421
+ return;
1422
+ }
1423
+
1424
+ // 验证授权响应格式
1425
+ const validation = validateAuthResponse(responseText);
1426
+ if (!validation.valid) {
1427
+ validationMessage.textContent = validation.message;
1428
+ validationMessage.style.display = 'block';
1429
+ return;
1430
+ }
1431
+
1432
+ // 显示加载状态
1433
+ button.disabled = true;
1434
+ btnText.textContent = '处理中...';
1435
+
1436
+ // 提交授权响应
1437
+ fetch('/callback', {
1438
+ method: 'POST',
1439
+ headers: {
1440
+ 'Content-Type': 'application/json'
1441
+ },
1442
+ body: responseText
1443
+ })
1444
+ .then(response => response.json())
1445
+ .then(data => {
1446
+ if (data.status === 'success') {
1447
+ // 显示成功消息
1448
+ submitResult.textContent = 'Token获取成功!';
1449
+ submitResult.style.display = 'block';
1450
+
1451
+ // 清空输入框
1452
+ document.getElementById('auth-response').value = '';
1453
+
1454
+ // 刷新Token列表
1455
+ setTimeout(() => {
1456
+ fetchCurrentToken();
1457
+
1458
+ // 切换到Token列表面板
1459
+ document.querySelector('.menu-item[data-target="token-list-panel"]').click();
1460
+ }, 1000);
1461
+ } else {
1462
+ // 显示错误消息但不影响样式
1463
+ validationMessage.textContent = '获取Token失败: ' + (data.error || '未知错误');
1464
+ validationMessage.style.display = 'block';
1465
+ }
1466
+ })
1467
+ .catch(error => {
1468
+ // 显示错误消息但不影响样式
1469
+ validationMessage.textContent = '请求失败: ' + error.message;
1470
+ validationMessage.style.display = 'block';
1471
+ })
1472
+ .finally(() => {
1473
+ // 恢复按钮状态
1474
+ button.disabled = false;
1475
+ btnText.textContent = originalText;
1476
+ });
1477
+ });
1478
+
1479
+ // 初始加载token列表
1480
+ fetchCurrentToken();
1481
+
1482
+ // 添加Token隐藏/显示功能
1483
+ let tokenVisible = true;
1484
+ const toggleTokenVisibilityBtn = document.getElementById('toggle-token-visibility');
1485
+
1486
+ toggleTokenVisibilityBtn.addEventListener('click', function() {
1487
+ tokenVisible = !tokenVisible;
1488
+
1489
+ // 更新按钮状态和文本
1490
+ if (tokenVisible) {
1491
+ this.innerHTML = '<i class="bi bi-eye-slash"></i> 隐藏Token';
1492
+ this.classList.remove('active');
1493
+ } else {
1494
+ this.innerHTML = '<i class="bi bi-eye"></i> 显示Token';
1495
+ this.classList.add('active');
1496
+ }
1497
+
1498
+ // 应用模糊效果到所有Token展示区域
1499
+ applyTokenVisibility();
1500
+ });
1501
+
1502
+ // 应用Token可见性设置到列表中
1503
+ function applyTokenVisibility() {
1504
+ // 获取所有Token显示元素
1505
+ const tokenElements = document.querySelectorAll('.token-summary, .token-display');
1506
+
1507
+ tokenElements.forEach(element => {
1508
+ if (tokenVisible) {
1509
+ element.classList.remove('token-blur');
1510
+ } else {
1511
+ element.classList.add('token-blur');
1512
+ }
1513
+ });
1514
+ }
1515
+
1516
+ // 在渲染Token列表后应用可见性设置
1517
+ const originalRenderTokenList = renderTokenList;
1518
+ renderTokenList = function(totalItems, totalPages) {
1519
+ originalRenderTokenList(totalItems, totalPages);
1520
+
1521
+ // 如果当前状态是隐藏的,则应用模糊效果
1522
+ if (!tokenVisible) {
1523
+ applyTokenVisibility();
1524
+ }
1525
+ };
1526
+ });
1527
+ </script>
1528
+ </body>
1529
+ </html>
templates/login.html ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ <link rel="icon" href="../static/augment.svg" type="image/svg+xml">
8
+ <link rel="alternate icon" href="../static/augment.svg" type="image/x-icon">
9
+ <style>
10
+ :root {
11
+ --primary-color: #4361ee;
12
+ --secondary-color: #3f37c9;
13
+ --success-color: #4caf50;
14
+ --error-color: #f44336;
15
+ --bg-color: #f8f9fa;
16
+ --card-bg: #ffffff;
17
+ --text-color: #333333;
18
+ --border-color: #e0e0e0;
19
+ --shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
20
+ --radius: 12px;
21
+ }
22
+
23
+ * {
24
+ box-sizing: border-box;
25
+ margin: 0;
26
+ padding: 0;
27
+ }
28
+
29
+ body {
30
+ font-family: 'PingFang SC', 'Microsoft YaHei', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
31
+ background-color: var(--bg-color);
32
+ color: var(--text-color);
33
+ line-height: 1.6;
34
+ padding: 20px;
35
+ min-height: 100vh;
36
+ display: flex;
37
+ flex-direction: column;
38
+ justify-content: center;
39
+ align-items: center;
40
+ position: relative;
41
+ overflow: hidden;
42
+ }
43
+
44
+ #bg-canvas {
45
+ position: fixed;
46
+ top: 0;
47
+ left: 0;
48
+ width: 100%;
49
+ height: 100%;
50
+ z-index: -1;
51
+ }
52
+
53
+ .login-container {
54
+ width: 100%;
55
+ max-width: 400px;
56
+ background: var(--card-bg);
57
+ border-radius: var(--radius);
58
+ box-shadow: var(--shadow);
59
+ padding: 30px;
60
+ margin-bottom: 20px;
61
+ }
62
+
63
+ h1 {
64
+ font-size: 24px;
65
+ font-weight: 600;
66
+ color: black;
67
+ text-align: center;
68
+ margin-bottom: 20px;
69
+ }
70
+
71
+ .form-group {
72
+ margin-bottom: 20px;
73
+ }
74
+
75
+ label {
76
+ display: block;
77
+ margin-bottom: 8px;
78
+ font-weight: 500;
79
+ text-align: center;
80
+ }
81
+
82
+ input[type="password"] {
83
+ width: 100%;
84
+ padding: 10px 12px;
85
+ border: 1px solid var(--border-color);
86
+ border-radius: 8px;
87
+ font-size: 16px;
88
+ transition: border-color 0.3s;
89
+ }
90
+
91
+ input[type="password"]:focus {
92
+ border-color: var(--primary-color);
93
+ outline: none;
94
+ }
95
+
96
+ button {
97
+ display: block;
98
+ width: 100%;
99
+ padding: 12px;
100
+ background-color: var(--primary-color);
101
+ color: white;
102
+ border: none;
103
+ border-radius: 8px;
104
+ font-size: 16px;
105
+ font-weight: 500;
106
+ cursor: pointer;
107
+ transition: background-color 0.3s;
108
+ }
109
+
110
+ button:hover {
111
+ background-color: var(--secondary-color);
112
+ }
113
+
114
+ .error-message {
115
+ color: var(--error-color);
116
+ margin-top: 15px;
117
+ text-align: center;
118
+ display: none;
119
+ }
120
+
121
+ footer {
122
+ text-align: center;
123
+ margin-top: 20px;
124
+ color: #666;
125
+ font-size: 14px;
126
+ }
127
+
128
+ footer a {
129
+ color: var(--primary-color);
130
+ text-decoration: none;
131
+ transition: color 0.3s;
132
+ }
133
+
134
+ footer a:hover {
135
+ color: var(--secondary-color);
136
+ text-decoration: underline;
137
+ }
138
+ </style>
139
+ </head>
140
+ <body>
141
+ <canvas id="bg-canvas"></canvas>
142
+ <div class="login-container">
143
+ <h1>Augment面板登录</h1>
144
+ <form id="login-form">
145
+ <div class="form-group">
146
+ <label for="password">访问密码</label>
147
+ <input type="password" id="password" name="password" placeholder="请输入访问密码" required>
148
+ </div>
149
+ <button type="submit">登录</button>
150
+ <div id="error-message" class="error-message">密码错误,请重试</div>
151
+ </form>
152
+ </div>
153
+
154
+ <script>
155
+ document.addEventListener('DOMContentLoaded', function() {
156
+ const loginForm = document.getElementById('login-form');
157
+ const errorMessage = document.getElementById('error-message');
158
+
159
+ // 检查是否有错误消息参数
160
+ const urlParams = new URLSearchParams(window.location.search);
161
+ if (urlParams.get('error') === 'invalid_password') {
162
+ errorMessage.style.display = 'block';
163
+ errorMessage.textContent = '密码错误,请重试';
164
+ } else if (urlParams.get('error') === 'token_expired') {
165
+ errorMessage.style.display = 'block';
166
+ errorMessage.textContent = '会话已过期,请重新登录';
167
+ }
168
+
169
+ // 背景动画
170
+ const canvas = document.getElementById('bg-canvas');
171
+ const ctx = canvas.getContext('2d');
172
+ let width = window.innerWidth;
173
+ let height = window.innerHeight;
174
+
175
+ canvas.width = width;
176
+ canvas.height = height;
177
+
178
+ // 线条数量
179
+ const lineCount = 15;
180
+ const lines = [];
181
+
182
+ // 创建线条
183
+ for (let i = 0; i < lineCount; i++) {
184
+ lines.push({
185
+ x: Math.random() * width,
186
+ y: Math.random() * height,
187
+ length: Math.random() * 100 + 50,
188
+ angle: Math.random() * Math.PI * 2,
189
+ speed: Math.random() * 0.5 + 0.1,
190
+ opacity: Math.random() * 0.2 + 0.1
191
+ });
192
+ }
193
+
194
+ function drawLines() {
195
+ ctx.clearRect(0, 0, width, height);
196
+
197
+ for (let i = 0; i < lineCount; i++) {
198
+ const line = lines[i];
199
+
200
+ ctx.beginPath();
201
+ ctx.moveTo(line.x, line.y);
202
+ ctx.lineTo(
203
+ line.x + Math.cos(line.angle) * line.length,
204
+ line.y + Math.sin(line.angle) * line.length
205
+ );
206
+
207
+ ctx.strokeStyle = `rgba(67, 97, 238, ${line.opacity})`;
208
+ ctx.lineWidth = 1;
209
+ ctx.stroke();
210
+
211
+ // 更新线条位置
212
+ line.x += Math.cos(line.angle) * line.speed;
213
+ line.y += Math.sin(line.angle) * line.speed;
214
+
215
+ // 如果线条移出画布,重新放置
216
+ if (line.x < -line.length || line.x > width + line.length ||
217
+ line.y < -line.length || line.y > height + line.length) {
218
+ line.x = Math.random() * width;
219
+ line.y = Math.random() * height;
220
+ line.angle = Math.random() * Math.PI * 2;
221
+ }
222
+ }
223
+
224
+ requestAnimationFrame(drawLines);
225
+ }
226
+
227
+ drawLines();
228
+
229
+ // 窗口大小变化时重新设置画布尺寸
230
+ window.addEventListener('resize', function() {
231
+ width = window.innerWidth;
232
+ height = window.innerHeight;
233
+ canvas.width = width;
234
+ canvas.height = height;
235
+ });
236
+
237
+ loginForm.addEventListener('submit', function(e) {
238
+ e.preventDefault();
239
+ const password = document.getElementById('password').value;
240
+
241
+ // 发送登录请求
242
+ fetch('/api/login', {
243
+ method: 'POST',
244
+ headers: {
245
+ 'Content-Type': 'application/json'
246
+ },
247
+ body: JSON.stringify({ password: password })
248
+ })
249
+ .then(response => response.json())
250
+ .then(data => {
251
+ if (data.status === 'success') {
252
+ // 登录成功,保存会话到Cookie并跳转到管理页面
253
+ // 设置安全的Cookie,确保路径正确
254
+ document.cookie = "auth_token=" + data.token + "; path=/; max-age=86400;";
255
+ console.log("设置Cookie成功: auth_token=" + data.token);
256
+
257
+ setTimeout(() => {
258
+ window.location.href = '/admin';
259
+ }, 300);
260
+ } else {
261
+ // 显示错误消息
262
+ errorMessage.style.display = 'block';
263
+ errorMessage.textContent = data.error || '登录失败,请重试';
264
+ }
265
+ })
266
+ .catch(error => {
267
+ console.error('登录请求失败:', error);
268
+ errorMessage.style.display = 'block';
269
+ errorMessage.textContent = '网络错误,请重试';
270
+ });
271
+ });
272
+ });
273
+ </script>
274
+ </body>
275
+ </html>