mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
fix(ui): route Codex voice control through configured gateway
Include the realtime WebSocket base URL in generated Codex configuration and document voice setup. Simplify configuration comments. Track key reveal actions separately so opening client configuration and copying a key show loading only on the initiating button. Preserve duplicate request protection and stale request cancellation. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
+20
-4
@@ -335,7 +335,7 @@ SSE 注释保活用于防止传输链路空闲断开,不会重置 Codex 等待
|
||||
|
||||
管理端生成的 CC Switch 一键导入包含连接信息和当前 Key 的日/周额度查询,默认每 30 分钟刷新。
|
||||
查询地址和凭据随导入生成,在 CC Switch 中修改 Provider 的地址或 Key 后,需重新导入以同步用量查询。
|
||||
导入不包含下方模板的远程压缩、原生生图和 WebSocket 设置。需要这些设置时,使用下方配置直连 CPR,
|
||||
导入不包含下方模板的远程压缩、原生生图、WebSocket 和语音设置。需要这些设置时,使用下方配置直连 CPR,
|
||||
避免 CC Switch 切换或接管后重写 Provider 配置
|
||||
|
||||
Linux/macOS 默认目录为 `~/.codex/`,Windows 为 `%USERPROFILE%\.codex\`;
|
||||
@@ -352,6 +352,8 @@ model = "gpt-5.6-terra"
|
||||
review_model = "gpt-5.6-terra"
|
||||
model_reasoning_effort = "max"
|
||||
service_tier = "default"
|
||||
# 语音通话控制地址
|
||||
experimental_realtime_ws_base_url = "http://127.0.0.1:8080/v1"
|
||||
|
||||
[model_providers.OpenAI]
|
||||
name = "OpenAI"
|
||||
@@ -359,15 +361,15 @@ base_url = "http://127.0.0.1:8080/v1"
|
||||
wire_api = "responses"
|
||||
supports_websockets = false
|
||||
requires_openai_auth = false
|
||||
# 填写代理密钥,真实账号由服务端管理。
|
||||
# 代理密钥
|
||||
experimental_bearer_token = "<client-api-key>"
|
||||
|
||||
[model_providers.OpenAI.capabilities]
|
||||
# Codex 0.162.0+ 显式启用 V2 远程压缩,不依赖 Provider 名称识别
|
||||
# V2 远程压缩
|
||||
remote_compaction = "v2"
|
||||
|
||||
[model_providers.OpenAI.http_headers]
|
||||
# 供客户端识别服务端托管认证,本身不是密钥。
|
||||
# 服务端托管认证
|
||||
X-OpenAI-Actor-Authorization = "proxy-managed"
|
||||
|
||||
[features]
|
||||
@@ -401,6 +403,20 @@ V2 远程压缩通过 Responses 接口发送 `compaction_trigger`,网关和上
|
||||
Codex OAuth 账号的上游传输方式默认 WS;固定为 SSE 的账号不承接必须依赖 WS 的预热及连接内续接。
|
||||
仅使用这类账号时,客户端保持 `supports_websockets = false`,避免先尝试 WS 再回退
|
||||
|
||||
### 语音通话
|
||||
|
||||
Codex 0.164.0 的 `/voice` 通过 Provider 的 `base_url` 创建通话,控制连接另用顶层
|
||||
`experimental_realtime_ws_base_url`,不会自动继承 Provider 地址。两者应指向同一网关的 `/v1`,
|
||||
使用相同的代理密钥,并确保反向代理允许 WebSocket Upgrade。此设置独立于 Responses 的
|
||||
`supports_websockets` 开关
|
||||
|
||||
已有配置需把 `experimental_realtime_ws_base_url` 合并到第一个 TOML 表之前,修改后完全退出并重启
|
||||
Codex。创建通话成功后仍报 `realtime websocket handshake failed` 时,先检查客户端日志中的
|
||||
`connecting realtime websocket` 是否指向网关;若仍为 `api.openai.com`,说明控制连接未经过代理
|
||||
|
||||
语音使用具备上游权限的 OAuth 账号,音频流由客户端与上游直接传输,路由与限制见
|
||||
[Codex Live 语音通话](../docs/api.md#codex-live-语音通话)
|
||||
|
||||
### 时区与客户端环境信息
|
||||
|
||||
要覆盖模型请求中的位置和时区,在管理端启用全局自定义位置,或为账号关联的代理设置位置;
|
||||
|
||||
@@ -17,6 +17,8 @@ model = "${CODEX_DEFAULT_MODEL}"
|
||||
review_model = "${CODEX_DEFAULT_MODEL}"
|
||||
model_reasoning_effort = "max"
|
||||
service_tier = "default"
|
||||
# 语音通话控制地址
|
||||
experimental_realtime_ws_base_url = ${JSON.stringify(baseUrl)}
|
||||
|
||||
[model_providers.OpenAI]
|
||||
name = "OpenAI"
|
||||
@@ -25,15 +27,15 @@ model_catalog_url = ${JSON.stringify(`${baseUrl}/models`)}
|
||||
wire_api = "responses"
|
||||
supports_websockets = ${websocketEnabled}
|
||||
requires_openai_auth = false
|
||||
# 代理密钥仅用于网关鉴权,真实账号登录状态由服务端管理
|
||||
# 代理密钥
|
||||
experimental_bearer_token = ${JSON.stringify(input.apiKey)}
|
||||
|
||||
[model_providers.OpenAI.capabilities]
|
||||
# Codex 0.162.0+ 显式启用 V2 远程压缩,不依赖 Provider 名称识别
|
||||
# V2 远程压缩
|
||||
remote_compaction = "v2"
|
||||
|
||||
[model_providers.OpenAI.http_headers]
|
||||
# 声明服务端托管认证,让官方客户端启用原生生图,该标记不是密钥
|
||||
# 服务端托管认证
|
||||
X-OpenAI-Actor-Authorization = "proxy-managed"
|
||||
|
||||
[features]
|
||||
|
||||
@@ -12,6 +12,7 @@ defineProps<{
|
||||
deleting: boolean
|
||||
updatingStatus: boolean
|
||||
revealing: boolean
|
||||
configuring: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -38,7 +39,7 @@ const { t } = useI18n({ useScope: 'global' })
|
||||
<ZIconButton
|
||||
size="small"
|
||||
:aria-label="t('keys.actions.useKey')"
|
||||
:loading="revealing"
|
||||
:loading="configuring"
|
||||
:disabled="revealing"
|
||||
@click.stop="emit('use', apiKey)"
|
||||
>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useI18n } from 'vue-i18n'
|
||||
defineProps<{
|
||||
prefix: string
|
||||
revealing: boolean
|
||||
copying: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -25,7 +26,7 @@ const { t } = useI18n({ useScope: 'global' })
|
||||
<ZIconButton
|
||||
size="small"
|
||||
:aria-label="t('keys.fields.copyFullKey')"
|
||||
:loading="revealing"
|
||||
:loading="copying"
|
||||
:disabled="revealing"
|
||||
@click="emit('copy')"
|
||||
>
|
||||
|
||||
@@ -2,16 +2,17 @@ import type { ApiKey } from '@/api'
|
||||
import { onScopeDispose, shallowRef } from 'vue'
|
||||
import { revealApiKey } from '@/api'
|
||||
import { useCopyText } from '@/composables/useCopyText'
|
||||
import { useIdSet } from '@/composables/useIdSet'
|
||||
import { useRequestState } from '@/composables/useRequestState'
|
||||
import { t } from '@/i18n'
|
||||
import { buildCodexCcSwitchImportDeeplink, resolveServiceRootUrl } from '@/utils/client'
|
||||
import { notification as ZNotification } from '@/utils/notification'
|
||||
|
||||
type KeyRevealAction = 'copy' | 'use' | 'import'
|
||||
|
||||
// 明文读取、复制、展示与导入共用生命周期,列表数据不保存明文。
|
||||
export function useApiKeyUse() {
|
||||
const copyText = useCopyText()
|
||||
const revealingKeys = useIdSet<string>()
|
||||
const revealingKeys = shallowRef(new Map<string, KeyRevealAction>())
|
||||
const revealRequest = useRequestState()
|
||||
const showUseKeyModal = shallowRef(false)
|
||||
const selectedUseKey = shallowRef<(ApiKey & { key: string }) | null>(null)
|
||||
@@ -41,7 +42,7 @@ export function useApiKeyUse() {
|
||||
}
|
||||
|
||||
function openUseKeyModal(apiKey: ApiKey) {
|
||||
return withPlaintextKey(apiKey, (key) => {
|
||||
return withPlaintextKey(apiKey, 'use', (key) => {
|
||||
selectedUseKey.value = { ...apiKey, key }
|
||||
showUseKeyModal.value = true
|
||||
})
|
||||
@@ -57,7 +58,7 @@ export function useApiKeyUse() {
|
||||
const apiKey = selectedImportKey.value
|
||||
if (!apiKey)
|
||||
return
|
||||
return withPlaintextKey(apiKey, (key) => {
|
||||
return withPlaintextKey(apiKey, 'import', (key) => {
|
||||
window.location.href = buildCodexCcSwitchImportDeeplink({
|
||||
apiKey: key,
|
||||
baseUrl: openAiBaseUrl,
|
||||
@@ -73,14 +74,15 @@ export function useApiKeyUse() {
|
||||
})
|
||||
}
|
||||
|
||||
async function withPlaintextKey(apiKey: ApiKey, use: (key: string) => void | Promise<void>) {
|
||||
if (revealingKeys.has(apiKey.id))
|
||||
async function withPlaintextKey(apiKey: ApiKey, action: KeyRevealAction, use: (key: string) => void | Promise<void>) {
|
||||
if (revealingKeys.value.has(apiKey.id))
|
||||
return
|
||||
// 明文操作共用一个当前意图,跨行点击和页面离开都使旧结果失效
|
||||
const requestId = revealRequest.start()
|
||||
revealingKeys.value = new Map(revealingKeys.value).set(apiKey.id, action)
|
||||
try {
|
||||
const result = await revealingKeys.run(apiKey.id, () => revealApiKey({ id: apiKey.id }, { signal: revealRequest.signal }))
|
||||
if (!result || !revealRequest.isCurrent(requestId))
|
||||
const result = await revealApiKey({ id: apiKey.id }, { signal: revealRequest.signal })
|
||||
if (!revealRequest.isCurrent(requestId))
|
||||
return
|
||||
if (!result.plaintextKey) {
|
||||
ZNotification.error({
|
||||
@@ -94,12 +96,15 @@ export function useApiKeyUse() {
|
||||
revealRequest.fail(requestId, error)
|
||||
}
|
||||
finally {
|
||||
const pendingKeys = new Map(revealingKeys.value)
|
||||
pendingKeys.delete(apiKey.id)
|
||||
revealingKeys.value = pendingKeys
|
||||
revealRequest.finish(requestId)
|
||||
}
|
||||
}
|
||||
|
||||
function copyApiKey(apiKey: ApiKey) {
|
||||
return withPlaintextKey(apiKey, copyToClipboard)
|
||||
return withPlaintextKey(apiKey, 'copy', copyToClipboard)
|
||||
}
|
||||
|
||||
function clearCreatedKey() {
|
||||
@@ -120,7 +125,7 @@ export function useApiKeyUse() {
|
||||
createdKey,
|
||||
showImportKeyModal,
|
||||
selectedImportKey,
|
||||
revealingKeyIds: revealingKeys.ids,
|
||||
revealingKeys,
|
||||
openAiBaseUrl,
|
||||
showCreatedKey,
|
||||
clearCreatedKey,
|
||||
|
||||
@@ -61,7 +61,7 @@ const {
|
||||
createdKey,
|
||||
showImportKeyModal,
|
||||
selectedImportKey,
|
||||
revealingKeyIds,
|
||||
revealingKeys,
|
||||
openAiBaseUrl,
|
||||
showCreatedKey,
|
||||
clearCreatedKey,
|
||||
@@ -164,7 +164,8 @@ watch(
|
||||
<template #prefix="{ row }">
|
||||
<ApiKeyPrefixCell
|
||||
:prefix="row.prefix"
|
||||
:revealing="revealingKeyIds.has(row.id)"
|
||||
:revealing="revealingKeys.has(row.id)"
|
||||
:copying="revealingKeys.get(row.id) === 'copy'"
|
||||
@copy="copyApiKey(row)"
|
||||
/>
|
||||
</template>
|
||||
@@ -200,7 +201,8 @@ watch(
|
||||
<ApiKeyActions
|
||||
:api-key="row"
|
||||
:deleting="deletingKey"
|
||||
:revealing="revealingKeyIds.has(row.id)"
|
||||
:revealing="revealingKeys.has(row.id)"
|
||||
:configuring="revealingKeys.get(row.id) === 'use'"
|
||||
:updating-status="updatingStatusKeyIds.has(row.id)"
|
||||
@edit="openEdit"
|
||||
@reset-budget="openBudgetReset"
|
||||
|
||||
Reference in New Issue
Block a user