diff --git a/frontend/src/api/endpoints/keys.ts b/frontend/src/api/endpoints/keys.ts index 4dcd3a3..3e33500 100644 --- a/frontend/src/api/endpoints/keys.ts +++ b/frontend/src/api/endpoints/keys.ts @@ -110,6 +110,14 @@ export async function updateEndpointKey( return response.data } +/** + * 获取完整的 API Key(用于查看和复制) + */ +export async function revealEndpointKey(keyId: string): Promise<{ api_key: string }> { + const response = await client.get(`/api/admin/endpoints/keys/${keyId}/reveal`) + return response.data +} + /** * 删除 Endpoint Key */ diff --git a/frontend/src/features/providers/components/ProviderDetailDrawer.vue b/frontend/src/features/providers/components/ProviderDetailDrawer.vue index efb15af..a74ea77 100644 --- a/frontend/src/features/providers/components/ProviderDetailDrawer.vue +++ b/frontend/src/features/providers/components/ProviderDetailDrawer.vue @@ -337,8 +337,40 @@ {{ key.is_active ? '活跃' : '禁用' }} -
- {{ key.api_key_masked }} +
+ + {{ revealedKeys.has(key.id) ? revealedKeys.get(key.id) : key.api_key_masked }} + + +
@@ -654,7 +686,9 @@ import { Power, Layers, GripVertical, - Copy + Copy, + Eye, + EyeOff } from 'lucide-vue-next' import { useEscapeKey } from '@/composables/useEscapeKey' import Button from '@/components/ui/button.vue' @@ -681,6 +715,7 @@ import { updateEndpoint, updateEndpointKey, batchUpdateKeyPriority, + revealEndpointKey, type ProviderEndpoint, type EndpointAPIKey, type Model @@ -731,6 +766,10 @@ const recoveringEndpointId = ref(null) const togglingEndpointId = ref(null) const togglingKeyId = ref(null) +// 密钥显示状态:key_id -> 完整密钥 +const revealedKeys = ref>(new Map()) +const revealingKeyId = ref(null) + // 模型相关状态 const modelFormDialogOpen = ref(false) const editingModel = ref(null) @@ -800,6 +839,9 @@ watch(() => props.open, (newOpen) => { currentEndpoint.value = null editingKey.value = null keyToDelete.value = null + + // 清除已显示的密钥(安全考虑) + revealedKeys.value.clear() } }) @@ -888,6 +930,43 @@ function handleConfigKeyModels(key: EndpointAPIKey) { keyAllowedModelsDialogOpen.value = true } +// 切换密钥显示/隐藏 +async function toggleKeyReveal(key: EndpointAPIKey) { + if (revealedKeys.value.has(key.id)) { + // 已显示,隐藏它 + revealedKeys.value.delete(key.id) + return + } + + // 未显示,调用 API 获取完整密钥 + revealingKeyId.value = key.id + try { + const result = await revealEndpointKey(key.id) + revealedKeys.value.set(key.id, result.api_key) + } catch (err: any) { + showError(err.response?.data?.detail || '获取密钥失败', '错误') + } finally { + revealingKeyId.value = null + } +} + +// 复制完整密钥 +async function copyFullKey(key: EndpointAPIKey) { + // 如果已经显示了,直接复制 + if (revealedKeys.value.has(key.id)) { + copyToClipboard(revealedKeys.value.get(key.id)!) + return + } + + // 否则先获取再复制 + try { + const result = await revealEndpointKey(key.id) + copyToClipboard(result.api_key) + } catch (err: any) { + showError(err.response?.data?.detail || '获取密钥失败', '错误') + } +} + function handleDeleteKey(key: EndpointAPIKey) { keyToDelete.value = key deleteKeyConfirmOpen.value = true diff --git a/src/api/admin/endpoints/keys.py b/src/api/admin/endpoints/keys.py index f5e39ae..d5fed62 100644 --- a/src/api/admin/endpoints/keys.py +++ b/src/api/admin/endpoints/keys.py @@ -80,6 +80,17 @@ async def get_keys_grouped_by_format( return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode) +@router.get("/keys/{key_id}/reveal") +async def reveal_endpoint_key( + key_id: str, + request: Request, + db: Session = Depends(get_db), +) -> dict: + """获取完整的 API Key(用于查看和复制)""" + adapter = AdminRevealEndpointKeyAdapter(key_id=key_id) + return await pipeline.run(adapter=adapter, http_request=request, db=db, mode=adapter.mode) + + @router.delete("/keys/{key_id}") async def delete_endpoint_key( key_id: str, @@ -293,6 +304,30 @@ class AdminUpdateEndpointKeyAdapter(AdminApiAdapter): return EndpointAPIKeyResponse(**response_dict) +@dataclass +class AdminRevealEndpointKeyAdapter(AdminApiAdapter): + """获取完整的 API Key(用于查看和复制)""" + + key_id: str + + async def handle(self, context): # type: ignore[override] + db = context.db + key = db.query(ProviderAPIKey).filter(ProviderAPIKey.id == self.key_id).first() + if not key: + raise NotFoundException(f"Key {self.key_id} 不存在") + + try: + decrypted_key = crypto_service.decrypt(key.api_key) + except Exception as e: + logger.error(f"解密 Key 失败: ID={self.key_id}, Error={e}") + raise InvalidRequestException( + "无法解密 API Key,可能是加密密钥已更改。请重新添加该密钥。" + ) + + logger.info(f"[REVEAL] 查看完整 Key: ID={self.key_id}, Name={key.name}") + return {"api_key": decrypted_key} + + @dataclass class AdminDeleteEndpointKeyAdapter(AdminApiAdapter): key_id: str