Spaces:
Paused
Paused
github-actions[bot]
commited on
Commit
·
bae4297
1
Parent(s):
191a47b
Update from GitHub Actions
Browse files- api/handler.go +215 -32
- api/token_handler.go +64 -29
- config/redis.go +6 -0
- docker-compose.yml +3 -3
- 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
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
//
|
| 732 |
-
|
| 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 |
-
|
|
|
|
| 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.
|
| 751 |
req.Header.Set("x-api-version", "2")
|
| 752 |
-
|
| 753 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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",
|
| 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.
|
| 796 |
req.Header.Set("x-api-version", "2")
|
| 797 |
-
req.Header.Set("x-request-id",
|
| 798 |
-
req.Header.Set("x-request-session-id",
|
| 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",
|
| 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.
|
| 867 |
req.Header.Set("x-api-version", "2")
|
| 868 |
-
req.Header.Set("x-request-id",
|
| 869 |
-
req.Header.Set("x-request-session-id",
|
| 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",
|
| 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.
|
| 1002 |
req.Header.Set("x-api-version", "2")
|
| 1003 |
-
req.Header.Set("x-request-id",
|
| 1004 |
-
req.Header.Set("x-request-session-id",
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
//
|
| 1144 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1155 |
|
| 1156 |
-
// 创建请求
|
| 1157 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
//
|
| 89 |
-
var
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
for _, key := range keys {
|
| 91 |
// 从key中提取token (格式: "token:{token}")
|
| 92 |
token := key[6:] // 去掉前缀 "token:"
|
| 93 |
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
if err != nil {
|
| 97 |
-
continue // 跳过无效的token
|
| 98 |
-
}
|
| 99 |
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
continue // 跳过被标记为不可用的token
|
| 104 |
-
}
|
| 105 |
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 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
|
| 10 |
healthcheck:
|
| 11 |
-
test: ["CMD", "redis-cli", "-a", "
|
| 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 |
-
|
| 252 |
overflow-y: auto;
|
| 253 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
}
|
| 255 |
|
| 256 |
/* 面板标题样式优化 */
|
|
@@ -1019,19 +1044,71 @@
|
|
| 1019 |
fetchCurrentToken();
|
| 1020 |
});
|
| 1021 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1022 |
// 修改获取当前Token列表函数,使用后端分页
|
| 1023 |
-
|
| 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 |
-
//
|
| 1096 |
-
|
| 1097 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1098 |
|
| 1099 |
-
//
|
| 1100 |
-
|
| 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 |
-
|
| 1153 |
-
|
| 1154 |
-
|
| 1155 |
-
|
| 1156 |
-
|
| 1157 |
-
|
| 1158 |
-
|
| 1159 |
-
|
| 1160 |
-
|
| 1161 |
-
|
| 1162 |
-
|
| 1163 |
-
|
| 1164 |
-
|
| 1165 |
-
|
| 1166 |
-
|
| 1167 |
-
|
| 1168 |
-
const modal = document.createElement('div');
|
| 1169 |
-
modal.className = 'remark-input-modal';
|
| 1170 |
|
| 1171 |
-
|
| 1172 |
-
|
| 1173 |
-
|
| 1174 |
-
|
| 1175 |
-
|
| 1176 |
-
|
| 1177 |
-
|
| 1178 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1179 |
</div>
|
| 1180 |
-
|
| 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 |
-
|
| 1208 |
-
|
| 1209 |
-
|
| 1210 |
-
|
| 1211 |
-
|
| 1212 |
-
|
| 1213 |
-
|
| 1214 |
-
|
| 1215 |
-
|
| 1216 |
-
|
| 1217 |
-
|
| 1218 |
-
|
|
|
|
|
|
|
|
|
|
| 1219 |
modal.remove();
|
| 1220 |
-
// 刷新列表
|
| 1221 |
-
fetchCurrentToken();
|
| 1222 |
-
} else {
|
| 1223 |
-
alert('更新备注失败: ' + (data.error || '未知错误'));
|
| 1224 |
}
|
| 1225 |
-
}
|
| 1226 |
-
|
| 1227 |
-
|
| 1228 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1229 |
});
|
| 1230 |
|
| 1231 |
-
//
|
| 1232 |
-
|
| 1233 |
-
|
| 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('确定要登出吗?')) {
|