diff --git a/controller/token.go b/controller/token.go index 889b962a..836e9b29 100644 --- a/controller/token.go +++ b/controller/token.go @@ -334,3 +334,26 @@ func DeleteTokenBatch(c *gin.Context) { "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}) +} diff --git a/i18n/keys.go b/i18n/keys.go index 4d98540a..8118dff9 100644 --- a/i18n/keys.go +++ b/i18n/keys.go @@ -25,6 +25,7 @@ const ( MsgDeleteFailed = "common.delete_failed" MsgAlreadyExists = "common.already_exists" MsgNameCannotBeEmpty = "common.name_cannot_be_empty" + MsgBatchTooMany = "common.batch_too_many" ) // Token related messages diff --git a/i18n/locales/en.yaml b/i18n/locales/en.yaml index 54dbf918..75a8bc6e 100644 --- a/i18n/locales/en.yaml +++ b/i18n/locales/en.yaml @@ -21,6 +21,7 @@ common.delete_success: "Deletion successful" common.delete_failed: "Deletion failed" common.already_exists: "Already exists" 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.name_too_long: "Token name is too long" diff --git a/i18n/locales/zh-CN.yaml b/i18n/locales/zh-CN.yaml index 4e0b5cd1..1f3b5a7b 100644 --- a/i18n/locales/zh-CN.yaml +++ b/i18n/locales/zh-CN.yaml @@ -22,6 +22,7 @@ common.delete_success: "删除成功" common.delete_failed: "删除失败" common.already_exists: "已存在" common.name_cannot_be_empty: "名称不能为空" +common.batch_too_many: "批量请求数量过多,最多 {{.Max}} 条" # Token messages token.name_too_long: "令牌名称过长" diff --git a/i18n/locales/zh-TW.yaml b/i18n/locales/zh-TW.yaml index dcdd331b..1231c0e2 100644 --- a/i18n/locales/zh-TW.yaml +++ b/i18n/locales/zh-TW.yaml @@ -22,6 +22,7 @@ common.delete_success: "刪除成功" common.delete_failed: "刪除失敗" common.already_exists: "已存在" common.name_cannot_be_empty: "名稱不能為空" +common.batch_too_many: "批次請求數量過多,最多 {{.Max}} 條" # Token messages token.name_too_long: "令牌名稱過長" diff --git a/model/token.go b/model/token.go index 91e5fe1d..b7989ad1 100644 --- a/model/token.go +++ b/model/token.go @@ -481,3 +481,11 @@ func BatchDeleteTokens(ids []int, userId int) (int, error) { 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 +} diff --git a/router/api-router.go b/router/api-router.go index 35d11376..bff158a8 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -257,6 +257,7 @@ func SetApiRouter(router *gin.Engine) { tokenRoute.PUT("/", controller.UpdateToken) tokenRoute.DELETE("/:id", controller.DeleteToken) tokenRoute.POST("/batch", controller.DeleteTokenBatch) + tokenRoute.POST("/batch/keys", middleware.CriticalRateLimit(), middleware.DisableCache(), controller.GetTokenKeysBatch) } usageRoute := apiRouter.Group("/usage") diff --git a/web/src/helpers/token.js b/web/src/helpers/token.js index 84abacf4..a491f384 100644 --- a/web/src/helpers/token.js +++ b/web/src/helpers/token.js @@ -33,6 +33,20 @@ export async function fetchTokenKey(tokenId) { return data.key; } +/** + * 批量获取多个令牌的真实 key + * @param {number[]} tokenIds + * @returns {Promise>} 返回 {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 * @returns {Promise} 返回 active 状态的不带 sk- 前缀的真实 token key 数组 diff --git a/web/src/hooks/tokens/useTokensData.jsx b/web/src/hooks/tokens/useTokensData.jsx index 9d0770e9..2caffcad 100644 --- a/web/src/hooks/tokens/useTokensData.jsx +++ b/web/src/hooks/tokens/useTokensData.jsx @@ -31,6 +31,7 @@ import { ITEMS_PER_PAGE } from '../../constants'; import { useTableCompactMode } from '../common/useTableCompactMode'; import { fetchTokenKey as fetchTokenKeyById, + fetchTokenKeysBatch, getServerAddress, encodeChannelConnectionString, } from '../../helpers/token'; @@ -408,14 +409,17 @@ export const useTokensData = (openFluentNotification, openCCSwitchModal) => { return; } 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 = ''; - 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') { - content += `${selectedKeys[i].name} sk-${fullKey}\n`; + content += `${token.name} sk-${fullKey}\n`; } else { content += `sk-${fullKey}\n`; }