mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat: add admin api key install dialog
This commit is contained in:
@@ -317,6 +317,15 @@
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell class="py-4">
|
<TableCell class="py-4">
|
||||||
<div class="flex justify-center gap-1">
|
<div class="flex justify-center gap-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-7 w-7"
|
||||||
|
title="一键安装并配置 CLI"
|
||||||
|
@click="openInstallDialog(apiKey)"
|
||||||
|
>
|
||||||
|
<Terminal class="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
@@ -533,6 +542,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-2 pt-0.5">
|
<div class="grid grid-cols-2 gap-2 pt-0.5">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
class="h-8 text-xs"
|
||||||
|
@click="openInstallDialog(apiKey)"
|
||||||
|
>
|
||||||
|
<Terminal class="mr-1.5 h-3.5 w-3.5" />
|
||||||
|
安装
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -652,6 +670,123 @@
|
|||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<!-- 一键安装并配置 CLI 对话框 -->
|
||||||
|
<Dialog
|
||||||
|
v-model="showInstallDialog"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="border-b border-border px-6 py-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10 flex-shrink-0">
|
||||||
|
<Terminal class="h-5 w-5 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<h3 class="text-lg font-semibold text-foreground leading-tight">
|
||||||
|
一键安装并配置 CLI
|
||||||
|
</h3>
|
||||||
|
<p class="text-xs text-muted-foreground truncate">
|
||||||
|
当前密钥:{{ selectedInstallApiKey?.name || selectedInstallApiKey?.key_display || '未选择' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="space-y-5">
|
||||||
|
<div class="rounded-lg border border-border/60 bg-muted/30 p-3 text-xs text-muted-foreground">
|
||||||
|
选择要配置的 CLI 和目标系统,Aether 会生成 15 分钟内有效的一次性 install code。页面命令不会包含原始 API Key。
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label class="text-sm font-semibold">目标 CLI</Label>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||||
|
<Button
|
||||||
|
v-for="option in installCliOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:variant="installCli === option.value ? 'default' : 'outline'"
|
||||||
|
class="justify-start h-auto py-3"
|
||||||
|
@click="selectInstallCli(option.value)"
|
||||||
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Label class="text-sm font-semibold">目标系统</Label>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||||
|
<Button
|
||||||
|
v-for="option in installSystemOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:variant="installSystem === option.value ? 'default' : 'outline'"
|
||||||
|
class="justify-start h-auto py-3"
|
||||||
|
@click="selectInstallSystem(option.value)"
|
||||||
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex items-center justify-between gap-2">
|
||||||
|
<Label class="text-sm font-semibold">复制到目标机器执行</Label>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
class="gap-1.5"
|
||||||
|
:disabled="installLoading || !installCommand"
|
||||||
|
:title="installCopied ? '已复制' : '一键复制安装命令'"
|
||||||
|
@click="copyInstallCommand"
|
||||||
|
>
|
||||||
|
<CheckCircle
|
||||||
|
v-if="installCopied"
|
||||||
|
class="h-3.5 w-3.5 text-emerald-600 dark:text-emerald-400"
|
||||||
|
/>
|
||||||
|
<Copy
|
||||||
|
v-else
|
||||||
|
class="h-3.5 w-3.5"
|
||||||
|
/>
|
||||||
|
{{ installCopied ? '已复制' : '一键复制' }}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
:disabled="installLoading || !selectedInstallApiKey"
|
||||||
|
@click="refreshInstallCommand"
|
||||||
|
>
|
||||||
|
{{ installLoading ? '生成中...' : '重新生成' }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-lg border border-border/60 bg-background overflow-hidden">
|
||||||
|
<pre class="max-h-32 overflow-x-auto whitespace-pre-wrap break-all p-3 text-xs font-mono">{{ installCommand || '正在生成短命令...' }}</pre>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
{{ installCommandHint }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
class="h-10 px-5"
|
||||||
|
@click="showInstallDialog = false"
|
||||||
|
>
|
||||||
|
关闭
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
class="h-10 px-5 shadow-lg shadow-primary/20"
|
||||||
|
:disabled="!installCommand || installLoading"
|
||||||
|
@click="copyInstallCommand"
|
||||||
|
>
|
||||||
|
{{ installCopied ? '已复制' : '复制命令' }}
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<WalletOpsDrawer
|
<WalletOpsDrawer
|
||||||
:open="showWalletActionDrawer"
|
:open="showWalletActionDrawer"
|
||||||
:wallet="walletActionTarget?.wallet || null"
|
:wallet="walletActionTarget?.wallet || null"
|
||||||
@@ -667,11 +802,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
import { useConfirm } from '@/composables/useConfirm'
|
import { useConfirm } from '@/composables/useConfirm'
|
||||||
import { useClipboard } from '@/composables/useClipboard'
|
import { useClipboard } from '@/composables/useClipboard'
|
||||||
import { adminApi, type AdminApiKey, type CreateStandaloneApiKeyRequest } from '@/api/admin'
|
import { adminApi, type AdminApiKey, type CreateStandaloneApiKeyRequest } from '@/api/admin'
|
||||||
|
import type { ApiKeyInstallSession, InstallSessionTargetSystem, InstallTargetCli } from '@/api/me'
|
||||||
import type { AdminWallet } from '@/api/admin-wallets'
|
import type { AdminWallet } from '@/api/admin-wallets'
|
||||||
import { walletStatusBadge, walletStatusLabel } from '@/utils/walletDisplay'
|
import { walletStatusBadge, walletStatusLabel } from '@/utils/walletDisplay'
|
||||||
import WalletOpsDrawer from '@/features/wallet/components/WalletOpsDrawer.vue'
|
import WalletOpsDrawer from '@/features/wallet/components/WalletOpsDrawer.vue'
|
||||||
@@ -710,7 +846,8 @@ import {
|
|||||||
Copy,
|
Copy,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
SquarePen,
|
SquarePen,
|
||||||
Search
|
Search,
|
||||||
|
Terminal
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
|
|
||||||
import { StandaloneKeyFormDialog, type StandaloneKeyFormData } from '@/features/api-keys'
|
import { StandaloneKeyFormDialog, type StandaloneKeyFormData } from '@/features/api-keys'
|
||||||
@@ -729,8 +866,16 @@ const total = ref(0)
|
|||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const limit = ref(100)
|
const limit = ref(100)
|
||||||
const showNewKeyDialog = ref(false)
|
const showNewKeyDialog = ref(false)
|
||||||
|
const showInstallDialog = ref(false)
|
||||||
const newKeyValue = ref('')
|
const newKeyValue = ref('')
|
||||||
const keyInput = ref<HTMLInputElement>()
|
const keyInput = ref<HTMLInputElement>()
|
||||||
|
const selectedInstallApiKey = ref<AdminApiKey | null>(null)
|
||||||
|
const installCli = ref<InstallTargetCli>('claude_code')
|
||||||
|
const installSystem = ref<InstallSessionTargetSystem>('linux')
|
||||||
|
const installSession = ref<ApiKeyInstallSession | null>(null)
|
||||||
|
const installLoading = ref(false)
|
||||||
|
const installCopied = ref(false)
|
||||||
|
let installCopiedResetTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
// 统一的表单对话框状态
|
// 统一的表单对话框状态
|
||||||
const showKeyFormDialog = ref(false)
|
const showKeyFormDialog = ref(false)
|
||||||
@@ -750,6 +895,18 @@ const statusFilters = [
|
|||||||
{ value: 'inactive' as const, label: '禁用' }
|
{ value: 'inactive' as const, label: '禁用' }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const installCliOptions: Array<{ value: InstallTargetCli; label: string }> = [
|
||||||
|
{ value: 'claude_code', label: 'Claude Code' },
|
||||||
|
{ value: 'codex_cli', label: 'Codex CLI' },
|
||||||
|
{ value: 'gemini_cli', label: 'Gemini CLI' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const installSystemOptions: Array<{ value: InstallSessionTargetSystem; label: string }> = [
|
||||||
|
{ value: 'macos', label: 'macOS' },
|
||||||
|
{ value: 'linux', label: 'Linux' },
|
||||||
|
{ value: 'windows', label: 'Windows' }
|
||||||
|
]
|
||||||
|
|
||||||
const balanceFilters = [
|
const balanceFilters = [
|
||||||
{ value: 'all' as const, label: '全部类型' },
|
{ value: 'all' as const, label: '全部类型' },
|
||||||
{ value: 'limited' as const, label: '限额' },
|
{ value: 'limited' as const, label: '限额' },
|
||||||
@@ -760,6 +917,20 @@ const hasActiveFilters = computed(() => {
|
|||||||
return searchQuery.value !== '' || filterStatus.value !== 'all' || filterBalance.value !== 'all'
|
return searchQuery.value !== '' || filterStatus.value !== 'all' || filterBalance.value !== 'all'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const installCommand = computed(() => {
|
||||||
|
if (!installSession.value) return ''
|
||||||
|
return installSystem.value === 'windows'
|
||||||
|
? installSession.value.powershell_command
|
||||||
|
: installSession.value.unix_command
|
||||||
|
})
|
||||||
|
|
||||||
|
const installCommandHint = computed(() => {
|
||||||
|
if (installSystem.value === 'windows') {
|
||||||
|
return 'Windows 请在 PowerShell 中执行。install code 使用后立即失效,如需再次执行请重新生成。'
|
||||||
|
}
|
||||||
|
return 'macOS / Linux 请在 sh 兼容终端中执行。install code 使用后立即失效,如需再次执行请重新生成。'
|
||||||
|
})
|
||||||
|
|
||||||
function clearFilters() {
|
function clearFilters() {
|
||||||
searchQuery.value = ''
|
searchQuery.value = ''
|
||||||
filterStatus.value = 'all'
|
filterStatus.value = 'all'
|
||||||
@@ -808,9 +979,32 @@ const showWalletActionDrawer = ref(false)
|
|||||||
const walletActionTarget = ref<{ apiKey: AdminApiKey; wallet: AdminWallet } | null>(null)
|
const walletActionTarget = ref<{ apiKey: AdminApiKey; wallet: AdminWallet } | null>(null)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
installSystem.value = detectCurrentSystem()
|
||||||
await refreshApiKeys()
|
await refreshApiKeys()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
resetInstallCopiedState()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(showInstallDialog, (isOpen) => {
|
||||||
|
if (!isOpen) {
|
||||||
|
resetInstallCopiedState()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function clearInstallCopiedResetTimer() {
|
||||||
|
if (installCopiedResetTimer) {
|
||||||
|
clearTimeout(installCopiedResetTimer)
|
||||||
|
installCopiedResetTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetInstallCopiedState() {
|
||||||
|
clearInstallCopiedResetTimer()
|
||||||
|
installCopied.value = false
|
||||||
|
}
|
||||||
|
|
||||||
function buildAdminWalletFromApiKey(apiKey: AdminApiKey): AdminWallet | null {
|
function buildAdminWalletFromApiKey(apiKey: AdminApiKey): AdminWallet | null {
|
||||||
if (!apiKey.wallet?.id) {
|
if (!apiKey.wallet?.id) {
|
||||||
return null
|
return null
|
||||||
@@ -867,6 +1061,64 @@ function handlePageChange(page: number) {
|
|||||||
refreshApiKeys()
|
refreshApiKeys()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function detectCurrentSystem(): InstallSessionTargetSystem {
|
||||||
|
const platform = window.navigator.platform.toLowerCase()
|
||||||
|
const userAgent = window.navigator.userAgent.toLowerCase()
|
||||||
|
if (platform.includes('mac')) return 'macos'
|
||||||
|
if (platform.includes('win') || userAgent.includes('windows')) return 'windows'
|
||||||
|
return 'linux'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openInstallDialog(apiKey: AdminApiKey) {
|
||||||
|
selectedInstallApiKey.value = apiKey
|
||||||
|
installSession.value = null
|
||||||
|
resetInstallCopiedState()
|
||||||
|
showInstallDialog.value = true
|
||||||
|
await refreshInstallCommand()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectInstallCli(value: InstallTargetCli) {
|
||||||
|
installCli.value = value
|
||||||
|
await refreshInstallCommand()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectInstallSystem(value: InstallSessionTargetSystem) {
|
||||||
|
installSystem.value = value
|
||||||
|
await refreshInstallCommand()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshInstallCommand() {
|
||||||
|
if (!selectedInstallApiKey.value) return
|
||||||
|
installLoading.value = true
|
||||||
|
installSession.value = null
|
||||||
|
resetInstallCopiedState()
|
||||||
|
try {
|
||||||
|
installSession.value = await adminApi.createApiKeyInstallSession(selectedInstallApiKey.value.id, {
|
||||||
|
target_cli: installCli.value,
|
||||||
|
target_system: installSystem.value,
|
||||||
|
})
|
||||||
|
} catch (err: unknown) {
|
||||||
|
log.error('生成 CLI 安装命令失败:', err)
|
||||||
|
error(parseApiError(err, '生成 CLI 安装命令失败'))
|
||||||
|
} finally {
|
||||||
|
installLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyInstallCommand() {
|
||||||
|
if (!installCommand.value) return
|
||||||
|
const copied = await copyToClipboard(installCommand.value, false)
|
||||||
|
if (!copied) return
|
||||||
|
|
||||||
|
installCopied.value = true
|
||||||
|
success('安装命令已复制到剪贴板')
|
||||||
|
clearInstallCopiedResetTimer()
|
||||||
|
installCopiedResetTimer = setTimeout(() => {
|
||||||
|
installCopied.value = false
|
||||||
|
installCopiedResetTimer = null
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
async function toggleApiKey(apiKey: AdminApiKey) {
|
async function toggleApiKey(apiKey: AdminApiKey) {
|
||||||
try {
|
try {
|
||||||
const response = await adminApi.toggleApiKey(apiKey.id)
|
const response = await adminApi.toggleApiKey(apiKey.id)
|
||||||
@@ -1063,7 +1315,12 @@ async function copyKeyPrefix(apiKey: AdminApiKey) {
|
|||||||
try {
|
try {
|
||||||
// 调用后端 API 获取完整密钥
|
// 调用后端 API 获取完整密钥
|
||||||
const response = await adminApi.getFullApiKey(apiKey.id)
|
const response = await adminApi.getFullApiKey(apiKey.id)
|
||||||
await copyToClipboard(response.key)
|
const copied = await copyToClipboard(response.key, false)
|
||||||
|
if (copied) {
|
||||||
|
success('完整密钥已复制到剪贴板')
|
||||||
|
} else {
|
||||||
|
error('复制失败,请手动复制')
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('复制密钥失败:', err)
|
log.error('复制密钥失败:', err)
|
||||||
error('复制失败,请重试')
|
error('复制失败,请重试')
|
||||||
|
|||||||
Reference in New Issue
Block a user