mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
feat(frontend): unify API key import dialog
Route API key imports through a shared CC Switch dialog with a GitHub link. Simplify key creation actions and narrow the action menu. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -80,7 +80,7 @@ INSTALL_DIR="$HOME/services/codex-proxy-rs" bash install.sh
|
||||
|
||||
部署完成后,打开 `http://127.0.0.1:8080`,使用 `admin@cpr.local` 和管理员密码登录
|
||||
|
||||
API Key 持有者可在登录页切换身份,查看自己的用量与额度,并通过「密钥配置」复制 Codex 配置或导入 CCSwitch
|
||||
API Key 持有者可在登录页切换身份,查看自己的用量与额度,并通过「密钥配置」复制 Codex 配置或导入 CC Switch
|
||||
|
||||
默认地址只能在服务器本机访问,从其他设备使用时,需要配置
|
||||
[HTTPS 反向代理](deploy/README.md#公网访问)
|
||||
@@ -93,7 +93,7 @@ API Key 持有者可在登录页切换身份,查看自己的用量与额度,
|
||||
|
||||
## 客户端接入
|
||||
|
||||
**Codex CLI / 桌面端**:在「使用密钥」中按操作系统复制配置,或通过 CCSwitch 导入,合并配置后重启 Codex
|
||||
**Codex CLI / 桌面端**:在「使用密钥」中按操作系统复制配置,合并配置后重启 Codex;也可通过「导入到」把服务连接与密钥添加到 CC Switch
|
||||
|
||||
完整步骤、生图配置与排障见[客户端配置](deploy/README.md#客户端配置)
|
||||
|
||||
|
||||
@@ -647,8 +647,8 @@ KeyUsageService 从 AuthService 的服务端身份或 Core 的入口认证结果
|
||||
ObservabilityStore 的范围查询;数据面额度查询不执行推理准入或开启窗口,入口认证和适用的请求中间件仍会执行。
|
||||
API 只输出各入口所需的字段白名单,不复用管理员的宽响应。
|
||||
客户端配置通过 ClientKeyStore 显式读取当前会话绑定 Key 的明文,不进入用量响应。
|
||||
前端 `/key-usage` 独立于管理布局,不挂载管理员菜单或请求管理接口;配置弹窗和 Codex / CCSwitch
|
||||
配置生成逻辑与管理端共用,明文仅在打开弹窗时获取,关闭后清除,不持久化到浏览器
|
||||
前端 `/key-usage` 独立于管理布局,不挂载管理员菜单或请求管理接口;配置弹窗、Codex 配置及 CC Switch
|
||||
导入链接生成逻辑与管理端共用,明文仅在打开弹窗时获取,关闭后清除,不持久化到浏览器
|
||||
|
||||
## 6. 路由、账号范围与 continuation
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Apple, Copy, Monitor, Upload } from '@lucide/vue'
|
||||
import { computed, shallowRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { buildCodexCcSwitchImportDeeplink, buildCodexConfig, CODEX_WEBSOCKET_ENABLED_BY_DEFAULT } from '@/utils/client'
|
||||
import ClientImportModal from './ClientImportModal.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
title?: string
|
||||
@@ -18,11 +19,13 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
copy: [text: string]
|
||||
afterLeave: []
|
||||
}>()
|
||||
|
||||
const { t } = useI18n({ useScope: 'global' })
|
||||
|
||||
const open = defineModel<boolean>({ default: false })
|
||||
const showImport = shallowRef(false)
|
||||
|
||||
const activePlatform = shallowRef('unix')
|
||||
const websocketEnabled = shallowRef(CODEX_WEBSOCKET_ENABLED_BY_DEFAULT)
|
||||
@@ -44,7 +47,7 @@ const codexConfig = computed(() => buildCodexConfig({
|
||||
websocketEnabled: websocketEnabled.value,
|
||||
}))
|
||||
|
||||
function importToCcs() {
|
||||
function importToClient() {
|
||||
if (!keyValue.value)
|
||||
return
|
||||
window.location.href = buildCodexCcSwitchImportDeeplink({
|
||||
@@ -61,6 +64,7 @@ function importToCcs() {
|
||||
:title="title ?? t('keys.configuration.title')"
|
||||
:description="t('keys.configuration.description')"
|
||||
width="48rem"
|
||||
@closed="showImport = false; emit('afterLeave')"
|
||||
>
|
||||
<div class="flex flex-col gap-5">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
@@ -103,13 +107,14 @@ function importToCcs() {
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<ZButton :disabled="!keyValue" @click="importToCcs">
|
||||
<ZButton :disabled="!keyValue" @click="showImport = true">
|
||||
<Upload class="size-4" />
|
||||
{{ t('keys.configuration.importCcSwitch') }}
|
||||
{{ t('keys.configuration.importTo') }}
|
||||
</ZButton>
|
||||
<ZButton type="primary" @click="open = false">
|
||||
{{ t('common.actions.close') }}
|
||||
</ZButton>
|
||||
</template>
|
||||
</ZDialog>
|
||||
<ClientImportModal v-if="open" v-model="showImport" @select="importToClient" />
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
import { Github } from '@boxicons/vue'
|
||||
import { ZDialog, ZMenuItem } from '@codex-proxy/ui'
|
||||
import { AppWindow } from '@lucide/vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: []
|
||||
afterLeave: []
|
||||
}>()
|
||||
const open = defineModel<boolean>({ default: false })
|
||||
const { t } = useI18n({ useScope: 'global' })
|
||||
|
||||
function select() {
|
||||
emit('select')
|
||||
open.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ZDialog
|
||||
v-model="open"
|
||||
:title="t('keys.configuration.importTo')"
|
||||
:description="t('keys.configuration.selectImportTool')"
|
||||
width="24rem"
|
||||
@closed="emit('afterLeave')"
|
||||
>
|
||||
<div class="grid gap-2">
|
||||
<div
|
||||
class="flex items-center gap-1 rounded-cp-sm pr-1 transition-colors hover:bg-cp-bg-text-hover has-[:focus-visible]:bg-cp-bg-text-hover motion-reduce:transition-none"
|
||||
>
|
||||
<ZMenuItem class="min-h-18 min-w-0 py-3 hover:bg-transparent!" @click="select">
|
||||
<template #icon>
|
||||
<span class="grid size-9 place-items-center rounded-cp bg-cp-fill-quaternary text-cp-text-secondary">
|
||||
<AppWindow class="size-5" aria-hidden="true" />
|
||||
</span>
|
||||
</template>
|
||||
<span class="grid gap-1 whitespace-normal">
|
||||
<span>CC Switch</span>
|
||||
<span class="text-xs leading-5 font-normal text-cp-text-secondary">{{ t('keys.configuration.ccSwitchDescription') }}</span>
|
||||
</span>
|
||||
</ZMenuItem>
|
||||
<a
|
||||
href="https://github.com/farion1231/cc-switch"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
:aria-label="t('keys.configuration.viewOnGitHub', { name: 'CC Switch' })"
|
||||
:title="t('keys.configuration.viewOnGitHub', { name: 'CC Switch' })"
|
||||
class="inline-grid size-9 shrink-0 place-items-center rounded-cp-sm text-cp-text-tertiary outline-none transition-colors hover:text-cp-text focus-visible:ring-2 focus-visible:ring-cp-control-outline focus-visible:ring-inset motion-reduce:transition-none"
|
||||
>
|
||||
<Github class="size-6" aria-hidden="true" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</ZDialog>
|
||||
</template>
|
||||
@@ -4,7 +4,10 @@
|
||||
"description": "Save or merge the configuration below and then restart Codex",
|
||||
"platform": "Platform",
|
||||
"toggleWebSocket": "Switch WebSocket configuration",
|
||||
"importCcSwitch": "Add to CC Switch",
|
||||
"importTo": "Import to",
|
||||
"selectImportTool": "Choose a tool to import into",
|
||||
"ccSwitchDescription": "Manage AI client configurations",
|
||||
"viewOnGitHub": "View {name} on GitHub",
|
||||
"copiedToClipboard": "Copied to clipboard",
|
||||
"fullApiKeyNotAvailable": "Full API key not available",
|
||||
"title": "Use API key"
|
||||
@@ -40,9 +43,8 @@
|
||||
},
|
||||
"created": {
|
||||
"apiKeyCreated": "API key created",
|
||||
"description": "Copy the API key or import it into CC Switch",
|
||||
"description": "Copy the API key or import it into a client",
|
||||
"accessNotice": "This key grants gateway access, share it only with trusted callers",
|
||||
"copyKey": "Copy key",
|
||||
"iHaveSaved": "I have saved"
|
||||
},
|
||||
"filters": {
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
"description": "保存或合并下方配置后重启 Codex",
|
||||
"platform": "配置平台",
|
||||
"toggleWebSocket": "切换 WebSocket 配置",
|
||||
"importCcSwitch": "添加到 CC Switch",
|
||||
"importTo": "导入到",
|
||||
"selectImportTool": "选择要导入的工具",
|
||||
"ccSwitchDescription": "管理和切换 AI 客户端配置",
|
||||
"viewOnGitHub": "在 GitHub 查看 {name}",
|
||||
"copiedToClipboard": "已复制到剪贴板",
|
||||
"fullApiKeyNotAvailable": "完整 API Key 不可用",
|
||||
"title": "使用密钥"
|
||||
@@ -40,9 +43,8 @@
|
||||
},
|
||||
"created": {
|
||||
"apiKeyCreated": "API Key 已创建",
|
||||
"description": "复制密钥,或直接导入 CCSwitch",
|
||||
"description": "复制密钥,或导入到客户端",
|
||||
"accessNotice": "该密钥具有网关访问权限,请仅发送给可信调用方",
|
||||
"copyKey": "复制密钥",
|
||||
"iHaveSaved": "我已保存"
|
||||
},
|
||||
"filters": {
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
"description": "保存或合併下方配置後重啓 Codex",
|
||||
"platform": "配置平臺",
|
||||
"toggleWebSocket": "切換 WebSocket 配置",
|
||||
"importCcSwitch": "添加到 CC Switch",
|
||||
"importTo": "導入到",
|
||||
"selectImportTool": "選擇要導入的工具",
|
||||
"ccSwitchDescription": "管理和切換 AI 客戶端配置",
|
||||
"viewOnGitHub": "在 GitHub 查看 {name}",
|
||||
"copiedToClipboard": "已複製到剪貼板",
|
||||
"fullApiKeyNotAvailable": "完整 API Key 不可用",
|
||||
"title": "使用密鑰"
|
||||
@@ -40,9 +43,8 @@
|
||||
},
|
||||
"created": {
|
||||
"apiKeyCreated": "API Key 已創建",
|
||||
"description": "複製密鑰,或直接導入 CCSwitch",
|
||||
"description": "複製密鑰,或導入到客戶端",
|
||||
"accessNotice": "該密鑰具有網關訪問權限,請僅發送給可信調用方",
|
||||
"copyKey": "複製密鑰",
|
||||
"iHaveSaved": "我已保存"
|
||||
},
|
||||
"filters": {
|
||||
|
||||
@@ -16,7 +16,7 @@ defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
use: [apiKey: ApiKey]
|
||||
importCcs: [apiKey: ApiKey]
|
||||
importClient: [apiKey: ApiKey]
|
||||
toggle: [apiKey: ApiKey]
|
||||
delete: [apiKey: ApiKey]
|
||||
edit: [apiKey: ApiKey]
|
||||
@@ -52,18 +52,18 @@ const { t } = useI18n({ useScope: 'global' })
|
||||
</ZIconButton>
|
||||
</template>
|
||||
<template #default="{ close }">
|
||||
<div class="w-44 p-1.5">
|
||||
<div class="w-36 p-1.5">
|
||||
<ZMenuItem @click.stop="(close(), emit('resetBudget', apiKey))">
|
||||
<template #icon>
|
||||
<RotateCcw class="size-3.5 text-cp-text-quaternary" />
|
||||
</template>
|
||||
{{ t('keys.actions.resetUsedCredit') }}
|
||||
</ZMenuItem>
|
||||
<ZMenuItem :disabled="revealing" @click.stop="(close(), emit('importCcs', apiKey))">
|
||||
<ZMenuItem :disabled="revealing" @click.stop="(close(), emit('importClient', apiKey))">
|
||||
<template #icon>
|
||||
<Upload class="size-3.5 text-cp-text-quaternary" />
|
||||
</template>
|
||||
{{ t('keys.configuration.importCcSwitch') }}
|
||||
{{ t('keys.configuration.importTo') }}
|
||||
</ZMenuItem>
|
||||
<ZMenuItem :loading="updatingStatus" @click.stop="(close(), emit('toggle', apiKey))">
|
||||
<template #icon>
|
||||
|
||||
@@ -2,19 +2,22 @@
|
||||
import { ZButton, ZDialog, ZIconButton } from '@codex-proxy/ui'
|
||||
|
||||
import { Copy, Upload } from '@lucide/vue'
|
||||
import { shallowRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import ClientImportModal from '@/components/ClientImportModal.vue'
|
||||
|
||||
defineProps<{ createdKey: string }>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
copy: [text: string]
|
||||
importCcs: []
|
||||
importClient: []
|
||||
afterLeave: []
|
||||
}>()
|
||||
|
||||
const { t } = useI18n({ useScope: 'global' })
|
||||
|
||||
const open = defineModel<boolean>({ default: false })
|
||||
const showImport = shallowRef(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -24,10 +27,10 @@ const open = defineModel<boolean>({ default: false })
|
||||
:description="t('keys.created.description')"
|
||||
type="success"
|
||||
width="36rem"
|
||||
@closed="emit('afterLeave')"
|
||||
@closed="showImport = false; emit('afterLeave')"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="rounded-cp border border-cp-warning-border bg-cp-warning-container px-4 py-3">
|
||||
<div class="rounded-cp bg-cp-warning-container px-4 py-3">
|
||||
<p class="m-0 text-cp font-semibold text-cp-warning-on-container">
|
||||
{{ t('keys.created.accessNotice') }}
|
||||
</p>
|
||||
@@ -48,21 +51,16 @@ const open = defineModel<boolean>({ default: false })
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<ZButton @click="emit('copy', createdKey)">
|
||||
<template #icon>
|
||||
<Copy class="size-4" />
|
||||
</template>
|
||||
{{ t('keys.created.copyKey') }}
|
||||
</ZButton>
|
||||
<ZButton @click="emit('importCcs')">
|
||||
<ZButton :disabled="!createdKey" @click="showImport = true">
|
||||
<template #icon>
|
||||
<Upload class="size-4" />
|
||||
</template>
|
||||
{{ t('keys.configuration.importCcSwitch') }}
|
||||
{{ t('keys.configuration.importTo') }}
|
||||
</ZButton>
|
||||
<ZButton type="primary" @click="open = false">
|
||||
{{ t('keys.created.iHaveSaved') }}
|
||||
</ZButton>
|
||||
</template>
|
||||
</ZDialog>
|
||||
<ClientImportModal v-if="open" v-model="showImport" @select="emit('importClient')" />
|
||||
</template>
|
||||
|
||||
@@ -18,6 +18,8 @@ export function useApiKeyUse() {
|
||||
const showCreatedKeyModal = shallowRef(false)
|
||||
const createdKey = shallowRef('')
|
||||
const createdKeyName = shallowRef('')
|
||||
const showImportKeyModal = shallowRef(false)
|
||||
const selectedImportKey = shallowRef<ApiKey | null>(null)
|
||||
|
||||
function showCreatedKey(key: string, name: string) {
|
||||
createdKey.value = key
|
||||
@@ -27,7 +29,7 @@ export function useApiKeyUse() {
|
||||
|
||||
const openAiBaseUrl = `${resolveServiceRootUrl()}/v1`
|
||||
|
||||
function importCreatedKeyToCcs() {
|
||||
function importCreatedKey() {
|
||||
if (!createdKey.value)
|
||||
return
|
||||
|
||||
@@ -45,7 +47,16 @@ export function useApiKeyUse() {
|
||||
})
|
||||
}
|
||||
|
||||
function importToCcs(apiKey: ApiKey) {
|
||||
function openImportKeyModal(apiKey: ApiKey) {
|
||||
revealRequest.invalidate()
|
||||
selectedImportKey.value = apiKey
|
||||
showImportKeyModal.value = true
|
||||
}
|
||||
|
||||
function importSelectedKey() {
|
||||
const apiKey = selectedImportKey.value
|
||||
if (!apiKey)
|
||||
return
|
||||
return withPlaintextKey(apiKey, (key) => {
|
||||
window.location.href = buildCodexCcSwitchImportDeeplink({
|
||||
apiKey: key,
|
||||
@@ -93,6 +104,7 @@ export function useApiKeyUse() {
|
||||
|
||||
onScopeDispose(() => {
|
||||
selectedUseKey.value = null
|
||||
selectedImportKey.value = null
|
||||
clearCreatedKey()
|
||||
})
|
||||
|
||||
@@ -101,14 +113,17 @@ export function useApiKeyUse() {
|
||||
selectedUseKey,
|
||||
showCreatedKeyModal,
|
||||
createdKey,
|
||||
showImportKeyModal,
|
||||
selectedImportKey,
|
||||
revealingKeyIds: revealingKeys.ids,
|
||||
openAiBaseUrl,
|
||||
showCreatedKey,
|
||||
clearCreatedKey,
|
||||
copyToClipboard,
|
||||
copyApiKey,
|
||||
importCreatedKeyToCcs,
|
||||
importCreatedKey,
|
||||
openUseKeyModal,
|
||||
importToCcs,
|
||||
openImportKeyModal,
|
||||
importSelectedKey,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ref, shallowRef, watch } from 'vue'
|
||||
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import ApiKeyConfigModal from '@/components/ApiKeyConfigModal.vue'
|
||||
import ClientImportModal from '@/components/ClientImportModal.vue'
|
||||
import LastUsedAtCell from '@/components/LastUsedAtCell.vue'
|
||||
import { useAccountGroupCatalog } from '@/composables/useAccountGroupCatalog'
|
||||
import { usePageSelection } from '@/composables/usePageSelection'
|
||||
@@ -58,15 +59,18 @@ const {
|
||||
selectedUseKey,
|
||||
showCreatedKeyModal,
|
||||
createdKey,
|
||||
showImportKeyModal,
|
||||
selectedImportKey,
|
||||
revealingKeyIds,
|
||||
openAiBaseUrl,
|
||||
showCreatedKey,
|
||||
clearCreatedKey,
|
||||
copyToClipboard,
|
||||
copyApiKey,
|
||||
importCreatedKeyToCcs,
|
||||
importCreatedKey,
|
||||
openUseKeyModal,
|
||||
importToCcs,
|
||||
openImportKeyModal,
|
||||
importSelectedKey,
|
||||
} = useApiKeyUse()
|
||||
|
||||
const {
|
||||
@@ -202,7 +206,7 @@ watch(
|
||||
@edit="openEdit"
|
||||
@reset-budget="openBudgetReset"
|
||||
@delete="requestDeleteKey"
|
||||
@import-ccs="importToCcs"
|
||||
@import-client="openImportKeyModal"
|
||||
@toggle="handleToggleStatus"
|
||||
@use="openUseKeyModal"
|
||||
/>
|
||||
@@ -233,7 +237,7 @@ watch(
|
||||
v-model="showCreatedKeyModal"
|
||||
:created-key="createdKey"
|
||||
@copy="copyToClipboard"
|
||||
@import-ccs="importCreatedKeyToCcs"
|
||||
@import-client="importCreatedKey"
|
||||
@after-leave="clearCreatedKey"
|
||||
/>
|
||||
|
||||
@@ -251,6 +255,12 @@ watch(
|
||||
@after-leave="selectedUseKey = null"
|
||||
/>
|
||||
|
||||
<ClientImportModal
|
||||
v-model="showImportKeyModal"
|
||||
@select="importSelectedKey"
|
||||
@after-leave="selectedImportKey = null"
|
||||
/>
|
||||
|
||||
<ZConfirmDialog
|
||||
v-model="showAllAccountsConfirm"
|
||||
:title="t('keys.scope.allAccounts.title')"
|
||||
|
||||
Reference in New Issue
Block a user