github-actions[bot] commited on
Commit
bae4297
·
1 Parent(s): 191a47b

Update from GitHub Actions

Browse files
Files changed (5) hide show
  1. api/handler.go +215 -32
  2. api/token_handler.go +64 -29
  3. config/redis.go +6 -0
  4. docker-compose.yml +3 -3
  5. templates/admin.html +276 -95
api/handler.go CHANGED
@@ -4,6 +4,7 @@ import (
4
  "augment2api/config"
5
  "augment2api/pkg/logger"
6
  "bufio"
 
7
  "crypto/sha256"
8
  "encoding/json"
9
  "fmt"
@@ -699,13 +700,37 @@ func ChatCompletionsHandler(c *gin.Context) {
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")
@@ -728,8 +753,8 @@ func handleStreamRequest(c *gin.Context, augmentReq AugmentRequest, model string
728
  return
729
  }
730
 
731
- // 增加token使用计数
732
- incrementTokenUsage(token, model)
733
 
734
  // 准备请求数据
735
  jsonData, err := json.Marshal(augmentReq)
@@ -738,19 +763,35 @@ func handleStreamRequest(c *gin.Context, augmentReq AugmentRequest, model string
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()
@@ -783,19 +824,21 @@ func handleStreamRequest(c *gin.Context, augmentReq AugmentRequest, model string
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)
@@ -817,6 +860,9 @@ func handleStreamRequest(c *gin.Context, augmentReq AugmentRequest, model string
817
  return
818
  }
819
 
 
 
 
820
  // 读取并转发响应
821
  reader := bufio.NewReader(resp.Body)
822
  responseID := fmt.Sprintf("chatcmpl-%d", time.Now().Unix())
@@ -854,19 +900,21 @@ func handleStreamRequest(c *gin.Context, augmentReq AugmentRequest, model string
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)
@@ -989,19 +1037,21 @@ func handleStreamRequest(c *gin.Context, augmentReq AugmentRequest, model string
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)
@@ -1022,6 +1072,9 @@ func handleStreamRequest(c *gin.Context, augmentReq AugmentRequest, model string
1022
  return
1023
  }
1024
 
 
 
 
1025
  // 读取并转发响应
1026
  reader = bufio.NewReader(resp.Body)
1027
  responseID = fmt.Sprintf("chatcmpl-%d", time.Now().Unix())
@@ -1117,7 +1170,16 @@ func estimateTokenCount(text string) int {
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")
@@ -1140,8 +1202,8 @@ func handleNonStreamRequest(c *gin.Context, augmentReq AugmentRequest, model str
1140
  return
1141
  }
1142
 
1143
- // 增加token使用计数
1144
- incrementTokenUsage(token, model)
1145
 
1146
  // 准备请求数据
1147
  jsonData, err := json.Marshal(augmentReq)
@@ -1150,19 +1212,35 @@ func handleNonStreamRequest(c *gin.Context, augmentReq AugmentRequest, model str
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)
@@ -1183,6 +1261,9 @@ func handleNonStreamRequest(c *gin.Context, augmentReq AugmentRequest, model str
1183
  return
1184
  }
1185
 
 
 
 
1186
  // 读取完整响应
1187
  reader := bufio.NewReader(resp.Body)
1188
  var fullText string
@@ -1269,6 +1350,14 @@ func handleNonStreamRequest(c *gin.Context, augmentReq AugmentRequest, model str
1269
 
1270
  // 清理请求状态
1271
  func cleanupRequestStatus(c *gin.Context) {
 
 
 
 
 
 
 
 
1272
  // 获取锁和 token
1273
  lockInterface, exists := c.Get("token_lock")
1274
  if !exists {
@@ -1300,7 +1389,9 @@ func cleanupRequestStatus(c *gin.Context) {
1300
  defer lock.Unlock()
1301
 
1302
  if err != nil {
1303
- log.Printf("清理请求状态失败: %v", err)
 
 
1304
  return
1305
  }
1306
  }
@@ -1326,6 +1417,93 @@ func createHTTPClient() *http.Client {
1326
  return client
1327
  }
1328
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1329
  // 在处理聊天请求时增加token使用计数
1330
  func incrementTokenUsage(token string, model string) {
1331
  // 先将模型名称转换为小写
@@ -1339,6 +1517,11 @@ func incrementTokenUsage(token string, model string) {
1339
  countKey = "token_usage_agent:" + token
1340
  } else {
1341
  countKey = "token_usage:" + token // 默认键
 
 
 
 
 
1342
  }
1343
 
1344
  // 使用Redis的INCR命令增加计数
 
4
  "augment2api/config"
5
  "augment2api/pkg/logger"
6
  "bufio"
7
+ "bytes"
8
  "crypto/sha256"
9
  "encoding/json"
10
  "fmt"
 
700
  handleNonStreamRequest(c, augmentReq, req.Model)
701
  }
702
 
703
+ // 异步处理token使用计数
704
+ func asyncIncrementTokenUsage(token string, model string) {
705
+ go func() {
706
+ defer func() {
707
+ if r := recover(); r != nil {
708
+ logger.Log.WithFields(logrus.Fields{
709
+ "error": r,
710
+ "token": token,
711
+ "model": model,
712
+ }).Error("system err")
713
+ }
714
+ }()
715
+
716
+ // 增加token使用计数
717
+ incrementTokenUsage(token, model)
718
+ }()
719
+ }
720
+
721
  // 处理流式请求
722
  func handleStreamRequest(c *gin.Context, augmentReq AugmentRequest, model string) {
723
+ defer func() {
724
+ if r := recover(); r != nil {
725
+ logger.Log.WithFields(logrus.Fields{
726
+ "error": r,
727
+ "model": model,
728
+ }).Error("处理流式请求时发生panic")
729
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "服务器内部错误"})
730
+ }
731
+ // 函数返回时同步清理请求状态
732
+ cleanupRequestStatus(c)
733
+ }()
734
 
735
  // 从上下文中获取token和tenant_url
736
  tokenInterface, exists := c.Get("token")
 
753
  return
754
  }
755
 
756
+ // 异步处理token使用计数
757
+ asyncIncrementTokenUsage(token, model)
758
 
759
  // 准备请求数据
760
  jsonData, err := json.Marshal(augmentReq)
 
763
  return
764
  }
765
 
766
+ // 提取主机部分
767
+ parsedURL, err := url.Parse(tenant)
768
+ if err != nil {
769
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "解析租户URL失败"})
770
+ return
771
+ }
772
+ hostName := parsedURL.Host
773
+
774
  // 创建请求
775
+ requestURL := tenant + "chat-stream"
776
+ req, err := http.NewRequest("POST", requestURL, bytes.NewReader(jsonData))
777
  if err != nil {
778
  c.JSON(http.StatusInternalServerError, gin.H{"error": "创建请求失败"})
779
  return
780
  }
781
 
782
+ // 设置请求头
783
+ req.Header.Set("Host", hostName)
784
+ req.Header.Set("Content-Length", fmt.Sprintf("%d", len(jsonData)))
785
  req.Header.Set("Content-Type", "application/json")
786
  req.Header.Set("Authorization", "Bearer "+token)
787
+ req.Header.Set("User-Agent", "augment.intellij/0.184.0 (Mac OS X; aarch64; 15.2) WebStorm/2024.3.5")
788
  req.Header.Set("x-api-version", "2")
789
+
790
+ // 生成请求ID和会话ID
791
+ requestID := uuid.New().String()
792
+ sessionID := uuid.New().String()
793
+ req.Header.Set("x-request-id", requestID)
794
+ req.Header.Set("x-request-session-id", sessionID)
795
 
796
  // 使用createHTTPClient创建客户端
797
  client := createHTTPClient()
 
824
  }
825
 
826
  // 创建新的请求
827
+ req, err = http.NewRequest("POST", requestURL, bytes.NewReader(jsonData))
828
  if err != nil {
829
  c.JSON(http.StatusInternalServerError, gin.H{"error": "创建请求失败"})
830
  return
831
  }
832
 
833
  // 设置相同的请求头
834
+ req.Header.Set("Host", hostName)
835
+ req.Header.Set("Content-Length", fmt.Sprintf("%d", len(jsonData)))
836
  req.Header.Set("Content-Type", "application/json")
837
  req.Header.Set("Authorization", "Bearer "+token)
838
+ req.Header.Set("User-Agent", "augment.intellij/0.184.0 (Mac OS X; aarch64; 15.2) WebStorm/2024.3.5")
839
  req.Header.Set("x-api-version", "2")
840
+ req.Header.Set("x-request-id", requestID)
841
+ req.Header.Set("x-request-session-id", sessionID)
842
 
843
  // 重新发送请求
844
  resp, err = client.Do(req)
 
860
  return
861
  }
862
 
863
+ // 异步记录会话事件
864
+ asyncRecordSessionEvent(token, tenant, requestID, sessionID)
865
+
866
  // 读取并转发响应
867
  reader := bufio.NewReader(resp.Body)
868
  responseID := fmt.Sprintf("chatcmpl-%d", time.Now().Unix())
 
900
  }
901
 
902
  // 创建新的请求
903
+ req, err = http.NewRequest("POST", requestURL, bytes.NewReader(jsonData))
904
  if err != nil {
905
  c.JSON(http.StatusInternalServerError, gin.H{"error": "创建请求失败"})
906
  return
907
  }
908
 
909
  // 设置相同的请求头
910
+ req.Header.Set("Host", hostName)
911
+ req.Header.Set("Content-Length", fmt.Sprintf("%d", len(jsonData)))
912
  req.Header.Set("Content-Type", "application/json")
913
  req.Header.Set("Authorization", "Bearer "+token)
914
+ req.Header.Set("User-Agent", "augment.intellij/0.184.0 (Mac OS X; aarch64; 15.2) WebStorm/2024.3.5")
915
  req.Header.Set("x-api-version", "2")
916
+ req.Header.Set("x-request-id", requestID)
917
+ req.Header.Set("x-request-session-id", sessionID)
918
 
919
  // 重新发送请求
920
  resp, err = client.Do(req)
 
1037
  }
1038
 
1039
  // 创建新的请求
1040
+ req, err = http.NewRequest("POST", requestURL, bytes.NewReader(jsonData))
1041
  if err != nil {
1042
  c.JSON(http.StatusInternalServerError, gin.H{"error": "创建请求失败"})
1043
  return
1044
  }
1045
 
1046
  // 设置相同的请求头
1047
+ req.Header.Set("Host", hostName)
1048
+ req.Header.Set("Content-Length", fmt.Sprintf("%d", len(jsonData)))
1049
  req.Header.Set("Content-Type", "application/json")
1050
  req.Header.Set("Authorization", "Bearer "+token)
1051
+ req.Header.Set("User-Agent", "augment.intellij/0.184.0 (Mac OS X; aarch64; 15.2) WebStorm/2024.3.5")
1052
  req.Header.Set("x-api-version", "2")
1053
+ req.Header.Set("x-request-id", requestID)
1054
+ req.Header.Set("x-request-session-id", sessionID)
1055
 
1056
  // 重新发送请求
1057
  resp, err = client.Do(req)
 
1072
  return
1073
  }
1074
 
1075
+ // 异步记录会话事件
1076
+ asyncRecordSessionEvent(token, tenant, requestID, sessionID)
1077
+
1078
  // 读取并转发响应
1079
  reader = bufio.NewReader(resp.Body)
1080
  responseID = fmt.Sprintf("chatcmpl-%d", time.Now().Unix())
 
1170
 
1171
  // 处理非流式请求
1172
  func handleNonStreamRequest(c *gin.Context, augmentReq AugmentRequest, model string) {
1173
+ defer func() {
1174
+ if r := recover(); r != nil {
1175
+ logger.Log.WithFields(logrus.Fields{
1176
+ "error": r,
1177
+ "model": model,
1178
+ }).Error("处理非流式请求时发生panic")
1179
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "服务器内部错误"})
1180
+ }
1181
+ cleanupRequestStatus(c) // 确保在函数返回时同步清理请求状态
1182
+ }()
1183
 
1184
  // 从上下文中获取token和tenant_url
1185
  tokenInterface, exists := c.Get("token")
 
1202
  return
1203
  }
1204
 
1205
+ // 异步处理token使用计数
1206
+ asyncIncrementTokenUsage(token, model)
1207
 
1208
  // 准备请求数据
1209
  jsonData, err := json.Marshal(augmentReq)
 
1212
  return
1213
  }
1214
 
1215
+ // 提取主机部分
1216
+ parsedURL, err := url.Parse(tenant)
1217
+ if err != nil {
1218
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "解析租户URL失败"})
1219
+ return
1220
+ }
1221
+ hostName := parsedURL.Host
1222
 
1223
+ // 创建请求
1224
+ requestURL := tenant + "chat-stream"
1225
+ req, err := http.NewRequest("POST", requestURL, bytes.NewReader(jsonData))
1226
  if err != nil {
1227
  c.JSON(http.StatusInternalServerError, gin.H{"error": "创建请求失败"})
1228
  return
1229
  }
1230
 
1231
+ // 设置请求头
1232
+ req.Header.Set("Host", hostName)
1233
+ req.Header.Set("Content-Length", fmt.Sprintf("%d", len(jsonData)))
1234
  req.Header.Set("Content-Type", "application/json")
 
1235
  req.Header.Set("Authorization", "Bearer "+token)
1236
+ req.Header.Set("User-Agent", "augment.intellij/0.184.0 (Mac OS X; aarch64; 15.2) WebStorm/2024.3.5")
1237
+ req.Header.Set("x-api-version", "2")
1238
+
1239
+ // 生成请求ID和会话ID
1240
+ requestID := uuid.New().String()
1241
+ sessionID := uuid.New().String()
1242
+ req.Header.Set("x-request-id", requestID)
1243
+ req.Header.Set("x-request-session-id", sessionID)
1244
 
1245
  client := createHTTPClient()
1246
  resp, err := client.Do(req)
 
1261
  return
1262
  }
1263
 
1264
+ // 异步记录会话事件
1265
+ asyncRecordSessionEvent(token, tenant, requestID, sessionID)
1266
+
1267
  // 读取完整响应
1268
  reader := bufio.NewReader(resp.Body)
1269
  var fullText string
 
1350
 
1351
  // 清理请求状态
1352
  func cleanupRequestStatus(c *gin.Context) {
1353
+ defer func() {
1354
+ if r := recover(); r != nil {
1355
+ logger.Log.WithFields(logrus.Fields{
1356
+ "error": r,
1357
+ }).Error("清理请求状态时发生panic")
1358
+ }
1359
+ }()
1360
+
1361
  // 获取锁和 token
1362
  lockInterface, exists := c.Get("token_lock")
1363
  if !exists {
 
1389
  defer lock.Unlock()
1390
 
1391
  if err != nil {
1392
+ logger.Log.WithFields(logrus.Fields{
1393
+ "error": err.Error(),
1394
+ }).Error("清理请求状态失败")
1395
  return
1396
  }
1397
  }
 
1417
  return client
1418
  }
1419
 
1420
+ // 异步记录用户会话事件
1421
+ func asyncRecordSessionEvent(token, tenantURL, requestID, sessionID string) {
1422
+ go func() {
1423
+ defer func() {
1424
+ if r := recover(); r != nil {
1425
+ logger.Log.WithFields(logrus.Fields{
1426
+ "error": r,
1427
+ "token": token,
1428
+ "tenant_url": tenantURL,
1429
+ }).Error("记录会话事件时发生panic")
1430
+ }
1431
+ }()
1432
+
1433
+ // 提取主机部分
1434
+ parsedURL, err := url.Parse(tenantURL)
1435
+ if err != nil {
1436
+ logger.Log.WithFields(logrus.Fields{
1437
+ "error": err.Error(),
1438
+ "tenant_url": tenantURL,
1439
+ }).Error("解析租户URL失败")
1440
+ return
1441
+ }
1442
+ hostName := parsedURL.Host
1443
+
1444
+ // 构建事件数据
1445
+ currentTime := time.Now()
1446
+ eventData := map[string]interface{}{
1447
+ "events": []map[string]interface{}{
1448
+ {
1449
+ "event_name": "used-chat",
1450
+ "event_time_sec": currentTime.Unix(),
1451
+ "event_time_nsec": currentTime.UnixNano() % 1000000000,
1452
+ },
1453
+ },
1454
+ }
1455
+
1456
+ // 序列化请求数据
1457
+ jsonData, err := json.Marshal(eventData)
1458
+ if err != nil {
1459
+ logger.Log.WithFields(logrus.Fields{
1460
+ "error": err.Error(),
1461
+ }).Error("序列化事件数据失败")
1462
+ return
1463
+ }
1464
+
1465
+ // 构建请求URL
1466
+ requestURL := tenantURL + "record-onboarding-session-event"
1467
+
1468
+ // 创建请求
1469
+ req, err := http.NewRequest("POST", requestURL, bytes.NewReader(jsonData))
1470
+ if err != nil {
1471
+ logger.Log.WithFields(logrus.Fields{
1472
+ "error": err.Error(),
1473
+ }).Error("创建记录事件请求失败")
1474
+ return
1475
+ }
1476
+
1477
+ // 设置请求头
1478
+ req.Header.Set("Host", hostName)
1479
+ req.Header.Set("Content-Length", fmt.Sprintf("%d", len(jsonData)))
1480
+ req.Header.Set("Content-Type", "application/json")
1481
+ req.Header.Set("Authorization", "Bearer "+token)
1482
+ req.Header.Set("User-Agent", "augment.intellij/0.184.0 (Mac OS X; aarch64; 15.2) WebStorm/2024.3.5")
1483
+ req.Header.Set("x-api-version", "2")
1484
+ req.Header.Set("x-request-id", requestID)
1485
+ req.Header.Set("x-request-session-id", sessionID)
1486
+ req.Header.Set("Accept-Charset", "UTF-8")
1487
+
1488
+ // 发送请求
1489
+ client := createHTTPClient()
1490
+ resp, err := client.Do(req)
1491
+ if err != nil {
1492
+ logger.Log.WithFields(logrus.Fields{
1493
+ "error": err.Error(),
1494
+ }).Error("发送记录事件请求失败")
1495
+ return
1496
+ }
1497
+ defer resp.Body.Close()
1498
+
1499
+ // 记录响应状态
1500
+ logger.Log.WithFields(logrus.Fields{
1501
+ "status_code": resp.StatusCode,
1502
+ "tenant_url": tenantURL,
1503
+ }).Info("记录会话事件完成")
1504
+ }()
1505
+ }
1506
+
1507
  // 在处理聊天请求时增加token使用计数
1508
  func incrementTokenUsage(token string, model string) {
1509
  // 先将模型名称转换为小写
 
1517
  countKey = "token_usage_agent:" + token
1518
  } else {
1519
  countKey = "token_usage:" + token // 默认键
1520
+ // 非特定结尾的模型,增加chat计数
1521
+ err := config.RedisIncr("token_usage_chat:" + token)
1522
+ if err != nil {
1523
+ logger.Log.Error("增加token chat使用计数失败: %v", err)
1524
+ }
1525
  }
1526
 
1527
  // 使用Redis的INCR命令增加计数
api/token_handler.go CHANGED
@@ -85,41 +85,76 @@ func GetRedisTokenHandler(c *gin.Context) {
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
  // 计算总页数和分页数据
 
85
  return
86
  }
87
 
88
+ // 使用并发方式批量获取token信息
89
+ var wg sync.WaitGroup
90
+ tokenList := make([]TokenInfo, 0, len(keys))
91
+ tokenListChan := make(chan TokenInfo, len(keys))
92
+ concurrencyLimit := 10 // 限制并发数
93
+ sem := make(chan struct{}, concurrencyLimit)
94
+
95
  for _, key := range keys {
96
  // 从key中提取token (格式: "token:{token}")
97
  token := key[6:] // 去掉前缀 "token:"
98
 
99
+ wg.Add(1)
100
+ sem <- struct{}{} // 获取信号量
 
 
 
101
 
102
+ go func(tokenKey string, tokenValue string) {
103
+ defer wg.Done()
104
+ defer func() { <-sem }() // 释放信号量
 
 
105
 
106
+ // 使用HGETALL一次性获取所有字段,减少网络往返
107
+ fields, err := config.RedisHGetAll(tokenKey)
108
+ if err != nil {
109
+ return // 跳过无效的token
110
+ }
111
+
112
+ // 检查必要字段
113
+ tenantURL, ok := fields["tenant_url"]
114
+ if !ok {
115
+ return
116
+ }
117
+
118
+ // 检查token状态
119
+ status, ok := fields["status"]
120
+ if ok && status == "disabled" {
121
+ return // 跳过被标记为不可用的token
122
+ }
123
+
124
+ // 获取备注信息
125
+ remark := fields["remark"]
126
+
127
+ // 获取token的冷却状态 (异步获取)
128
+ coolStatus, _ := GetTokenCoolStatus(tokenValue)
129
+
130
+ // 获取使用次数 (可以考虑将这些计数缓存在Redis中)
131
+ chatCount := getTokenChatUsageCount(tokenValue)
132
+ agentCount := getTokenAgentUsageCount(tokenValue)
133
+ totalCount := chatCount + agentCount
134
+
135
+ // 构建token信息并发送到channel
136
+ tokenListChan <- TokenInfo{
137
+ Token: tokenValue,
138
+ TenantURL: tenantURL,
139
+ UsageCount: totalCount,
140
+ ChatUsageCount: chatCount,
141
+ AgentUsageCount: agentCount,
142
+ Remark: remark,
143
+ InCool: coolStatus.InCool,
144
+ CoolEnd: coolStatus.CoolEnd,
145
+ }
146
+ }(key, token)
147
+ }
148
+
149
+ // 启动一个goroutine来收集结果
150
+ go func() {
151
+ wg.Wait()
152
+ close(tokenListChan)
153
+ }()
154
+
155
+ // 从channel中收集结果
156
+ for info := range tokenListChan {
157
+ tokenList = append(tokenList, info)
158
  }
159
 
160
  // 计算总页数和分页数据
config/redis.go CHANGED
@@ -105,3 +105,9 @@ func RedisHExists(key, field string) (bool, error) {
105
  ctx := context.Background()
106
  return RDB.HExists(ctx, key, field).Result()
107
  }
 
 
 
 
 
 
 
105
  ctx := context.Background()
106
  return RDB.HExists(ctx, key, field).Result()
107
  }
108
+
109
+ // RedisHGetAll 获取哈希表中的所有字段和值
110
+ func RedisHGetAll(key string) (map[string]string, error) {
111
+ ctx := context.Background()
112
+ return RDB.HGetAll(ctx, key).Result()
113
+ }
docker-compose.yml CHANGED
@@ -6,9 +6,9 @@ services:
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
@@ -35,4 +35,4 @@ volumes:
35
 
36
  networks:
37
  default:
38
- driver: bridge
 
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
 
35
 
36
  networks:
37
  default:
38
+ driver: bridge
templates/admin.html CHANGED
@@ -248,9 +248,34 @@
248
 
249
  /* 添加token列表容器样式,使其可滚动 */
250
  .token-list-container {
251
- flex: 1;
252
  overflow-y: auto;
253
- margin-bottom: 15px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  }
255
 
256
  /* 面板标题样式优化 */
@@ -1019,19 +1044,71 @@
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) {
@@ -1042,7 +1119,14 @@
1042
  .then(data => {
1043
  clearTimeout(timeoutId); // 清除超时定时器
1044
 
 
 
 
 
1045
  if (data.status === 'success') {
 
 
 
1046
  // 使用后端返回的token列表
1047
  allTokens = data.tokens || [];
1048
 
@@ -1066,8 +1150,17 @@
1066
  .finally(() => {
1067
  refreshBtn.classList.remove('loading');
1068
  });
1069
- }
1070
 
 
 
 
 
 
 
 
 
 
1071
  // 修改渲染token列表函数,使用后端返回的分页信息
1072
  function renderTokenList(totalItems, totalPages) {
1073
  const tokenListElement = document.getElementById('token-list');
@@ -1092,17 +1185,91 @@
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 = '';
@@ -1116,11 +1283,11 @@
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}
@@ -1149,91 +1316,110 @@
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列表添加事件委托,只处理删除按钮
@@ -1264,11 +1450,6 @@
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('确定要登出吗?')) {
 
248
 
249
  /* 添加token列表容器样式,使其可滚动 */
250
  .token-list-container {
251
+ max-height: calc(100vh - 250px);
252
  overflow-y: auto;
253
+ overflow-x: hidden;
254
+ padding-right: 10px;
255
+ position: relative;
256
+ }
257
+
258
+ /* 添加列表加载效果 */
259
+ .token-list-loading {
260
+ display: flex;
261
+ justify-content: center;
262
+ align-items: center;
263
+ padding: 20px;
264
+ color: var(--text-secondary);
265
+ }
266
+
267
+ .token-list-loading .spinner {
268
+ width: 20px;
269
+ height: 20px;
270
+ border: 2px solid var(--border-color);
271
+ border-top-color: var(--primary-color);
272
+ border-radius: 50%;
273
+ animation: spin 1s linear infinite;
274
+ margin-right: 10px;
275
+ }
276
+
277
+ @keyframes spin {
278
+ to { transform: rotate(360deg); }
279
  }
280
 
281
  /* 面板标题样式优化 */
 
1044
  fetchCurrentToken();
1045
  });
1046
 
1047
+ // 添加节流功能,避免短时间内多次请求
1048
+ function throttle(func, delay) {
1049
+ let lastCall = 0;
1050
+ return function(...args) {
1051
+ const now = new Date().getTime();
1052
+ if (now - lastCall < delay) {
1053
+ return;
1054
+ }
1055
+ lastCall = now;
1056
+ return func.apply(this, args);
1057
+ };
1058
+ }
1059
+
1060
+ // 添加简单的缓存机制
1061
+ const tokenCache = {
1062
+ data: {},
1063
+ timestamp: 0,
1064
+ ttl: 10000, // 缓存有效期10秒
1065
+
1066
+ get: function(key) {
1067
+ const now = new Date().getTime();
1068
+ if (this.data[key] && (now - this.timestamp < this.ttl)) {
1069
+ return this.data[key];
1070
+ }
1071
+ return null;
1072
+ },
1073
+
1074
+ set: function(key, data) {
1075
+ this.data[key] = data;
1076
+ this.timestamp = new Date().getTime();
1077
+ }
1078
+ };
1079
+
1080
  // 修改获取当前Token列表函数,使用后端分页
1081
+ const fetchCurrentToken = throttle(function() {
1082
  // 显示加载动画
1083
  const refreshBtn = document.getElementById('refresh-token');
1084
  const tokenListElement = document.getElementById('token-list');
1085
  refreshBtn.classList.add('loading');
1086
 
1087
+ // 构造缓存键
1088
+ const cacheKey = `tokens_${currentPage}_${pageSize}`;
1089
+
1090
+ // 检查缓存
1091
+ const cachedData = tokenCache.get(cacheKey);
1092
+ if (cachedData && !forceFresh) {
1093
+ // 使用缓存数据
1094
+ allTokens = cachedData.tokens || [];
1095
+ renderTokenList(cachedData.total || 0, cachedData.total_pages || 1);
1096
+ refreshBtn.classList.remove('loading');
1097
+ return;
1098
+ }
1099
+
1100
+ // 重置强制刷新标志
1101
+ forceFresh = false;
1102
+
1103
  // 设置超时处理
1104
  const timeoutId = setTimeout(() => {
1105
  refreshBtn.classList.remove('loading');
1106
  tokenListElement.innerHTML = '<div class="error" style="display:block;">请求超时,请重试</div>';
1107
  }, 10000); // 10秒超时
1108
 
1109
+ // 添加性能标记
1110
+ const startTime = performance.now();
1111
+
1112
  fetch(`/api/tokens?page=${currentPage}&page_size=${pageSize}`)
1113
  .then(response => {
1114
  if (!response.ok) {
 
1119
  .then(data => {
1120
  clearTimeout(timeoutId); // 清除超时定时器
1121
 
1122
+ // 记录加载时间
1123
+ const loadTime = performance.now() - startTime;
1124
+ console.log(`Token列表加载耗时: ${loadTime.toFixed(2)}ms`);
1125
+
1126
  if (data.status === 'success') {
1127
+ // 缓存结果
1128
+ tokenCache.set(cacheKey, data);
1129
+
1130
  // 使用后端返回的token列表
1131
  allTokens = data.tokens || [];
1132
 
 
1150
  .finally(() => {
1151
  refreshBtn.classList.remove('loading');
1152
  });
1153
+ }, 300); // 300ms节流
1154
 
1155
+ // 添加强制刷新标记
1156
+ let forceFresh = false;
1157
+
1158
+ // 刷新Token按钮事件
1159
+ document.getElementById('refresh-token').addEventListener('click', function() {
1160
+ forceFresh = true; // 强制刷新,忽略缓存
1161
+ fetchCurrentToken();
1162
+ });
1163
+
1164
  // 修改渲染token列表函数,使用后端返回的分页信息
1165
  function renderTokenList(totalItems, totalPages) {
1166
  const tokenListElement = document.getElementById('token-list');
 
1185
  prevPageBtn.disabled = currentPage === 1;
1186
  nextPageBtn.disabled = currentPage === totalPages;
1187
 
1188
+ // 检查token数量,如果过多则使用分批渲染
1189
+ const useBatchRendering = allTokens.length > 50;
1190
+
1191
+ // 优化:使用文档片段减少DOM重绘次数
1192
+ const fragment = document.createDocumentFragment();
1193
+ const tokenList = document.createElement('div');
1194
+ tokenList.className = 'token-list';
1195
+ fragment.appendChild(tokenList);
1196
+
1197
+ // 清空现有内容并显示加载中
1198
+ tokenListElement.innerHTML = '';
1199
+
1200
+ if (useBatchRendering) {
1201
+ // 先显示加载状态
1202
+ const loadingEl = document.createElement('div');
1203
+ loadingEl.className = 'token-list-loading';
1204
+ loadingEl.innerHTML = '<div class="spinner"></div> <span>正在加载Token列表...</span>';
1205
+ tokenListElement.appendChild(loadingEl);
1206
+
1207
+ // 使用requestAnimationFrame和分批处理来渲染大列表
1208
+ setTimeout(() => {
1209
+ renderTokensBatch(tokenList, 0, 20);
1210
+ }, 10);
1211
+
1212
+ function renderTokensBatch(container, startIdx, batchSize) {
1213
+ // 移除加载状态
1214
+ const loadingEl = tokenListElement.querySelector('.token-list-loading');
1215
+ if (loadingEl) {
1216
+ loadingEl.remove();
1217
+ }
1218
+
1219
+ // 添加当前批次的token
1220
+ const endIdx = Math.min(startIdx + batchSize, allTokens.length);
1221
+
1222
+ for (let i = startIdx; i < endIdx; i++) {
1223
+ const tokenItem = createTokenItem(allTokens[i], i);
1224
+ container.appendChild(tokenItem);
1225
+ }
1226
+
1227
+ // 如果还有更多token待渲染,安排下一批
1228
+ if (endIdx < allTokens.length) {
1229
+ // 添加临时"加载更多"指示器
1230
+ if (startIdx === 0) { // 只在第一批后添加DOM
1231
+ tokenListElement.appendChild(fragment);
1232
+ }
1233
+
1234
+ setTimeout(() => {
1235
+ requestAnimationFrame(() => {
1236
+ renderTokensBatch(container, endIdx, batchSize);
1237
+ });
1238
+ }, 0);
1239
+ } else {
1240
+ // 全部渲染完成,如果是第一批,添加到DOM
1241
+ if (startIdx === 0) {
1242
+ tokenListElement.appendChild(fragment);
1243
+ }
1244
+
1245
+ // 应用Token可见性
1246
+ if (!tokenVisible) {
1247
+ applyTokenVisibility();
1248
+ }
1249
+ }
1250
+ }
1251
+ } else {
1252
+ // 直接渲染所有token
1253
+ allTokens.forEach((tokenInfo, index) => {
1254
+ const tokenItem = createTokenItem(tokenInfo, index);
1255
+ tokenList.appendChild(tokenItem);
1256
+ });
1257
+
1258
+ // 一次性替换DOM内容
1259
+ tokenListElement.appendChild(fragment);
1260
+
1261
+ // 应用Token可见性
1262
+ if (!tokenVisible) {
1263
+ applyTokenVisibility();
1264
+ }
1265
+ }
1266
 
1267
+ // 辅助函数:创建token项元素
1268
+ function createTokenItem(tokenInfo, index) {
1269
  // 计算在当前页中的索引
1270
  const displayIndex = index + 1 + (currentPage - 1) * pageSize;
1271
 
1272
  // 获取使用次数并设置样式类
 
1273
  const chatUsageCount = tokenInfo.chat_usage_count || 0;
1274
  const agentUsageCount = tokenInfo.agent_usage_count || 0;
1275
  let usageClass = '';
 
1283
  usageClass = 'high';
1284
  }
1285
 
 
1286
  const tokenItem = document.createElement('div');
1287
  tokenItem.className = 'token-item';
1288
+ tokenItem.dataset.index = index;
1289
  tokenItem.innerHTML = `
1290
+ <div class="token-header" data-index="${index}">
1291
  <div class="token-number">${displayIndex}</div>
1292
  <div class="token-summary">
1293
  ${tokenInfo.token}
 
1316
  </div>
1317
  `;
1318
 
1319
+ return tokenItem;
1320
+ }
1321
+
1322
+ // 使用事件委托处理所有点击事件
1323
+ if (!document.querySelector('.token-list').hasEventListener) {
1324
+ document.querySelector('.token-list').addEventListener('click', function(e) {
1325
+ // 处理折叠/展开
1326
+ const headerElement = e.target.closest('.token-header');
1327
+ if (headerElement) {
1328
+ const tokenItem = headerElement.closest('.token-item');
1329
+ const details = tokenItem.querySelector('.token-details');
1330
+ const toggle = tokenItem.querySelector('.token-toggle');
1331
+
1332
+ details.classList.toggle('open');
1333
+ toggle.classList.toggle('open');
1334
+ }
 
 
1335
 
1336
+ // 处理备注点击
1337
+ const remarkElement = e.target.closest('.token-remark');
1338
+ if (remarkElement) {
1339
+ e.stopPropagation(); // 阻止事件冒泡到header
1340
+
1341
+ const token = remarkElement.dataset.token;
1342
+ const currentRemark = remarkElement.dataset.remark;
1343
+
1344
+ // 创建弹出层
1345
+ const modal = document.createElement('div');
1346
+ modal.className = 'remark-input-modal';
1347
+
1348
+ modal.innerHTML = `
1349
+ <div class="remark-input-container">
1350
+ <h3>编辑备注</h3>
1351
+ <input type="text" maxlength="30" placeholder="请输入备注(30字以内)" value="${currentRemark}">
1352
+ <div class="char-count"><span>${currentRemark.length}</span>/30</div>
1353
+ <div class="remark-input-actions">
1354
+ <button class="secondary" onclick="this.closest('.remark-input-modal').remove()">取消</button>
1355
+ <button class="save-remark" data-token="${token}">保存</button>
1356
+ </div>
1357
  </div>
1358
+ `;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1359
 
1360
+ document.body.appendChild(modal);
1361
+
1362
+ // 获取输入框并聚焦
1363
+ const input = modal.querySelector('input');
1364
+ input.focus();
1365
+
1366
+ // 更新字符计数
1367
+ input.addEventListener('input', () => {
1368
+ const count = input.value.length;
1369
+ modal.querySelector('.char-count span').textContent = count;
1370
+ });
1371
+
1372
+ // 点击背景关闭弹窗
1373
+ modal.addEventListener('click', (e) => {
1374
+ if (e.target === modal) {
1375
  modal.remove();
 
 
 
 
1376
  }
1377
+ });
1378
+
1379
+ // 处理保存按钮点击
1380
+ modal.querySelector('.save-remark').addEventListener('click', async function() {
1381
+ const token = this.dataset.token;
1382
+ const newRemark = input.value.trim();
1383
+
1384
+ try {
1385
+ const response = await fetch(`/api/token/${token}/remark`, {
1386
+ method: 'PUT',
1387
+ headers: {
1388
+ 'Content-Type': 'application/json'
1389
+ },
1390
+ body: JSON.stringify({ remark: newRemark })
1391
+ });
1392
+
1393
+ const data = await response.json();
1394
+
1395
+ if (data.status === 'success') {
1396
+ // 更新所有具有相同token的备注元素
1397
+ document.querySelectorAll(`.token-remark[data-token="${token}"]`).forEach(el => {
1398
+ el.textContent = newRemark || '添加备��';
1399
+ el.dataset.remark = newRemark;
1400
+
1401
+ if (newRemark) {
1402
+ el.classList.remove('empty');
1403
+ } else {
1404
+ el.classList.add('empty');
1405
+ }
1406
+ });
1407
+
1408
+ // 关闭弹窗
1409
+ modal.remove();
1410
+ } else {
1411
+ alert('更新备注失败: ' + (data.error || '未知错误'));
1412
+ }
1413
+ } catch (error) {
1414
+ alert('请求失败: ' + error.message);
1415
+ }
1416
+ });
1417
+ }
1418
  });
1419
 
1420
+ // 标记已添加过事件监听器
1421
+ document.querySelector('.token-list').hasEventListener = true;
1422
+ }
 
 
 
1423
  }
1424
 
1425
  // 为token列表添加事件委托,只处理删除按钮
 
1450
  }
1451
  });
1452
 
 
 
 
 
 
1453
  // 添加登出处理逻辑
1454
  document.getElementById('logout-btn').addEventListener('click', function() {
1455
  if(confirm('确定要登出吗?')) {