Explorar el Código

Merge pull request #4114 from RedwindA/fix/4110

feat(token): add batch API for fetching token keys
Calcium-Ion hace 1 mes
padre
commit
960bf9c49e

+ 23 - 0
controller/token.go

@@ -334,3 +334,26 @@ func DeleteTokenBatch(c *gin.Context) {
 		"data":    count,
 		"data":    count,
 	})
 	})
 }
 }
+
+func GetTokenKeysBatch(c *gin.Context) {
+	tokenBatch := TokenBatch{}
+	if err := c.ShouldBindJSON(&tokenBatch); err != nil || len(tokenBatch.Ids) == 0 {
+		common.ApiErrorI18n(c, i18n.MsgInvalidParams)
+		return
+	}
+	if len(tokenBatch.Ids) > 100 {
+		common.ApiErrorI18n(c, i18n.MsgBatchTooMany, map[string]any{"Max": 100})
+		return
+	}
+	userId := c.GetInt("id")
+	tokens, err := model.GetTokenKeysByIds(tokenBatch.Ids, userId)
+	if err != nil {
+		common.ApiError(c, err)
+		return
+	}
+	keysMap := make(map[int]string)
+	for _, t := range tokens {
+		keysMap[t.Id] = t.GetFullKey()
+	}
+	common.ApiSuccess(c, gin.H{"keys": keysMap})
+}

+ 1 - 0
i18n/keys.go

@@ -25,6 +25,7 @@ const (
 	MsgDeleteFailed      = "common.delete_failed"
 	MsgDeleteFailed      = "common.delete_failed"
 	MsgAlreadyExists     = "common.already_exists"
 	MsgAlreadyExists     = "common.already_exists"
 	MsgNameCannotBeEmpty = "common.name_cannot_be_empty"
 	MsgNameCannotBeEmpty = "common.name_cannot_be_empty"
+	MsgBatchTooMany      = "common.batch_too_many"
 )
 )
 
 
 // Token related messages
 // Token related messages

+ 1 - 0
i18n/locales/en.yaml

@@ -21,6 +21,7 @@ common.delete_success: "Deletion successful"
 common.delete_failed: "Deletion failed"
 common.delete_failed: "Deletion failed"
 common.already_exists: "Already exists"
 common.already_exists: "Already exists"
 common.name_cannot_be_empty: "Name cannot be empty"
 common.name_cannot_be_empty: "Name cannot be empty"
+common.batch_too_many: "Too many items in batch request, maximum is {{.Max}}"
 
 
 # Token messages
 # Token messages
 token.name_too_long: "Token name is too long"
 token.name_too_long: "Token name is too long"

+ 1 - 0
i18n/locales/zh-CN.yaml

@@ -22,6 +22,7 @@ common.delete_success: "删除成功"
 common.delete_failed: "删除失败"
 common.delete_failed: "删除失败"
 common.already_exists: "已存在"
 common.already_exists: "已存在"
 common.name_cannot_be_empty: "名称不能为空"
 common.name_cannot_be_empty: "名称不能为空"
+common.batch_too_many: "批量请求数量过多,最多 {{.Max}} 条"
 
 
 # Token messages
 # Token messages
 token.name_too_long: "令牌名称过长"
 token.name_too_long: "令牌名称过长"

+ 1 - 0
i18n/locales/zh-TW.yaml

@@ -22,6 +22,7 @@ common.delete_success: "刪除成功"
 common.delete_failed: "刪除失敗"
 common.delete_failed: "刪除失敗"
 common.already_exists: "已存在"
 common.already_exists: "已存在"
 common.name_cannot_be_empty: "名稱不能為空"
 common.name_cannot_be_empty: "名稱不能為空"
+common.batch_too_many: "批次請求數量過多,最多 {{.Max}} 條"
 
 
 # Token messages
 # Token messages
 token.name_too_long: "令牌名稱過長"
 token.name_too_long: "令牌名稱過長"

+ 8 - 0
model/token.go

@@ -481,3 +481,11 @@ func BatchDeleteTokens(ids []int, userId int) (int, error) {
 
 
 	return len(tokens), nil
 	return len(tokens), nil
 }
 }
+
+func GetTokenKeysByIds(ids []int, userId int) ([]Token, error) {
+	var tokens []Token
+	err := DB.Select("id", commonKeyCol).
+		Where("user_id = ? AND id IN (?)", userId, ids).
+		Find(&tokens).Error
+	return tokens, err
+}

+ 1 - 0
router/api-router.go

@@ -257,6 +257,7 @@ func SetApiRouter(router *gin.Engine) {
 			tokenRoute.PUT("/", controller.UpdateToken)
 			tokenRoute.PUT("/", controller.UpdateToken)
 			tokenRoute.DELETE("/:id", controller.DeleteToken)
 			tokenRoute.DELETE("/:id", controller.DeleteToken)
 			tokenRoute.POST("/batch", controller.DeleteTokenBatch)
 			tokenRoute.POST("/batch", controller.DeleteTokenBatch)
+			tokenRoute.POST("/batch/keys", middleware.CriticalRateLimit(), middleware.DisableCache(), controller.GetTokenKeysBatch)
 		}
 		}
 
 
 		usageRoute := apiRouter.Group("/usage")
 		usageRoute := apiRouter.Group("/usage")

+ 14 - 0
web/src/helpers/token.js

@@ -33,6 +33,20 @@ export async function fetchTokenKey(tokenId) {
   return data.key;
   return data.key;
 }
 }
 
 
+/**
+ * 批量获取多个令牌的真实 key
+ * @param {number[]} tokenIds
+ * @returns {Promise<Record<number, string>>} 返回 {id: key} map,key 不带 sk- 前缀
+ */
+export async function fetchTokenKeysBatch(tokenIds) {
+  const response = await API.post('/api/token/batch/keys', { ids: tokenIds });
+  const { success, data, message } = response.data || {};
+  if (!success || !data?.keys) {
+    throw new Error(message || 'Failed to fetch token keys');
+  }
+  return data.keys;
+}
+
 /**
 /**
  * 获取可用的 token keys
  * 获取可用的 token keys
  * @returns {Promise<string[]>} 返回 active 状态的不带 sk- 前缀的真实 token key 数组
  * @returns {Promise<string[]>} 返回 active 状态的不带 sk- 前缀的真实 token key 数组

+ 10 - 6
web/src/hooks/tokens/useTokensData.jsx

@@ -31,6 +31,7 @@ import { ITEMS_PER_PAGE } from '../../constants';
 import { useTableCompactMode } from '../common/useTableCompactMode';
 import { useTableCompactMode } from '../common/useTableCompactMode';
 import {
 import {
   fetchTokenKey as fetchTokenKeyById,
   fetchTokenKey as fetchTokenKeyById,
+  fetchTokenKeysBatch,
   getServerAddress,
   getServerAddress,
   encodeChannelConnectionString,
   encodeChannelConnectionString,
 } from '../../helpers/token';
 } from '../../helpers/token';
@@ -408,14 +409,17 @@ export const useTokensData = (openFluentNotification, openCCSwitchModal) => {
       return;
       return;
     }
     }
     try {
     try {
-      const keys = await Promise.all(
-        selectedKeys.map((token) => fetchTokenKey(token, { suppressError: true })),
-      );
+      const ids = selectedKeys.map((token) => token.id);
+      const keysMap = await fetchTokenKeysBatch(ids);
+
+      setResolvedTokenKeys((prev) => ({ ...prev, ...keysMap }));
+
       let content = '';
       let content = '';
-      for (let i = 0; i < selectedKeys.length; i++) {
-        const fullKey = keys[i];
+      for (const token of selectedKeys) {
+        const fullKey = keysMap[token.id];
+        if (!fullKey) continue;
         if (copyType === 'name+key') {
         if (copyType === 'name+key') {
-          content += `${selectedKeys[i].name}    sk-${fullKey}\n`;
+          content += `${token.name}    sk-${fullKey}\n`;
         } else {
         } else {
           content += `sk-${fullKey}\n`;
           content += `sk-${fullKey}\n`;
         }
         }