feat: Codex账号管理优化与API Key激活机制

 新功能
- 支持通过refreshToken新增Codex账号,创建时立即验证token有效性
- API Key新增首次使用自动激活机制,支持activation模式设置有效期
- 前端账号表单增加token验证功能,确保账号创建成功

🐛 修复
- 修复Codex token刷新失败问题,增加分布式锁防止并发刷新
- 优化token刷新错误处理,提供更详细的错误信息和建议
- 修复OpenAI账号token过期检测和自动刷新逻辑

📝 文档更新
- 更新README中Codex使用说明,改为config.toml配置方式
- 优化Cherry Studio等第三方工具接入文档
- 添加详细的配置示例和账号类型说明

🎨 界面优化
- 改进账号创建表单UI,支持手动和OAuth两种模式
- 优化API Key过期时间编辑弹窗,支持激活操作
- 调整教程页面布局,提升移动端响应式体验

💡 代码改进
- 重构token刷新服务,增强错误处理和重试机制
- 优化代理配置处理,确保OAuth请求正确使用代理
- 改进webhook通知,增加token刷新失败告警
This commit is contained in:
shaw
2025-09-06 17:39:05 +08:00
parent 0e746b1056
commit d2f3f6866c
19 changed files with 1231 additions and 463 deletions

View File

@@ -570,7 +570,16 @@
</td>
<td class="whitespace-nowrap px-3 py-4 text-sm">
<div class="inline-flex items-center gap-1.5">
<span v-if="key.expiresAt">
<!-- 未激活状态 -->
<span
v-if="key.expirationMode === 'activation' && !key.isActivated"
class="inline-flex items-center text-blue-600 dark:text-blue-400"
>
<i class="fas fa-pause-circle mr-1" />
未激活 ({{ key.activationDays || 30 }}天)
</span>
<!-- 已设置过期时间 -->
<span v-else-if="key.expiresAt">
<span
v-if="isApiKeyExpired(key.expiresAt)"
class="inline-flex items-center text-red-600"
@@ -589,6 +598,7 @@
{{ formatExpireDate(key.expiresAt) }}
</span>
</span>
<!-- 永不过期 -->
<span
v-else
class="inline-flex items-center text-gray-400 dark:text-gray-500"
@@ -2650,18 +2660,29 @@ const closeExpiryEdit = () => {
}
// 保存过期时间
const handleSaveExpiry = async ({ keyId, expiresAt }) => {
const handleSaveExpiry = async ({ keyId, expiresAt, activateNow }) => {
try {
const data = await apiClient.put(`/admin/api-keys/${keyId}`, {
expiresAt: expiresAt || null
// 使用新的PATCH端点来修改过期时间
const data = await apiClient.patch(`/admin/api-keys/${keyId}/expiration`, {
expiresAt: expiresAt || null,
activateNow: activateNow || false
})
if (data.success) {
showToast('过期时间已更新', 'success')
showToast(activateNow ? 'API Key已激活' : '过期时间已更新', 'success')
// 更新本地数据
const key = apiKeys.value.find((k) => k.id === keyId)
if (key) {
key.expiresAt = expiresAt || null
if (activateNow && data.updates) {
key.isActivated = true
key.activatedAt = data.updates.activatedAt
key.expiresAt = data.updates.expiresAt
} else {
key.expiresAt = expiresAt || null
if (expiresAt && !key.isActivated) {
key.isActivated = true
}
}
}
closeExpiryEdit()
} else {

View File

@@ -420,74 +420,42 @@
</p>
<div class="space-y-4">
<div class="rounded-lg border border-indigo-200 bg-white p-3 sm:p-4">
<h6 class="mb-2 text-sm font-medium text-gray-800 dark:text-gray-600 sm:text-base">
PowerShell 设置方法
</h6>
<p class="mb-3 text-sm text-gray-600"> PowerShell 中运行以下命令</p>
<div class="rounded-lg border border-yellow-200 bg-yellow-50 p-3 sm:p-4">
<h6 class="mb-2 font-medium text-yellow-800">Codex 配置文件</h6>
<p class="mb-3 text-sm text-yellow-700">
<code class="rounded bg-yellow-100 px-1">~/.codex/config.toml</code>
文件中添加以下配置
</p>
<div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="whitespace-nowrap text-gray-300">
$env:OPENAI_BASE_URL = "{{ openaiBaseUrl }}"
</div>
<div class="whitespace-nowrap text-gray-300">
$env:OPENAI_API_KEY = "你的API密钥"
</div>
<div class="whitespace-nowrap text-gray-300">model_provider = "crs"</div>
<div class="whitespace-nowrap text-gray-300">model = "gpt-5"</div>
<div class="whitespace-nowrap text-gray-300">model_reasoning_effort = "high"</div>
<div class="whitespace-nowrap text-gray-300">disable_response_storage = true</div>
<div class="mt-2"></div>
<div class="whitespace-nowrap text-gray-300">[model_providers.crs]</div>
<div class="whitespace-nowrap text-gray-300">name = "crs"</div>
<div class="whitespace-nowrap text-gray-300">base_url = "{{ openaiBaseUrl }}"</div>
<div class="whitespace-nowrap text-gray-300">wire_api = "responses"</div>
</div>
<p class="mt-3 text-sm text-yellow-700">
<code class="rounded bg-yellow-100 px-1">~/.codex/auth.json</code>
文件中配置API密钥
</p>
<div
class="mt-2 overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="whitespace-nowrap text-gray-300">{</div>
<div class="whitespace-nowrap text-gray-300">"OPENAI_API_KEY": "你的API密钥"</div>
<div class="whitespace-nowrap text-gray-300">}</div>
</div>
<p class="mt-2 text-xs text-yellow-700">
💡 使用与 Claude Code 相同的 API 密钥即可格式如 cr_xxxxxxxxxx
</p>
</div>
<div class="rounded-lg border border-indigo-200 bg-white p-3 sm:p-4">
<h6 class="mb-2 text-sm font-medium text-gray-800 dark:text-gray-600 sm:text-base">
PowerShell 永久设置用户级
</h6>
<p class="mb-3 text-sm text-gray-600"> PowerShell 中运行以下命令</p>
<div
class="mb-3 overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="mb-2"># 设置用户级环境变量永久生效</div>
<div class="whitespace-nowrap text-gray-300">
[System.Environment]::SetEnvironmentVariable("OPENAI_BASE_URL", "{{
openaiBaseUrl
}}", [System.EnvironmentVariableTarget]::User)
</div>
<div class="whitespace-nowrap text-gray-300">
[System.Environment]::SetEnvironmentVariable("OPENAI_API_KEY", "你的API密钥",
[System.EnvironmentVariableTarget]::User)
</div>
</div>
<p class="mt-2 text-xs text-blue-700">
💡 设置后需要重新打开 PowerShell 窗口才能生效
</p>
</div>
<div class="rounded-lg border border-indigo-200 bg-indigo-50 p-3 sm:p-4">
<h6 class="mb-2 font-medium text-indigo-800">验证 Codex 环境变量</h6>
<p class="mb-3 text-sm text-indigo-700"> PowerShell 中验证</p>
<div
class="space-y-1 overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="whitespace-nowrap text-gray-300">echo $env:OPENAI_BASE_URL</div>
<div class="whitespace-nowrap text-gray-300">echo $env:OPENAI_API_KEY</div>
</div>
</div>
<div class="rounded-lg border border-yellow-200 bg-yellow-50 p-3 sm:p-4">
<h6 class="mb-2 font-medium text-yellow-800">Codex 额外配置</h6>
<p class="mb-3 text-sm text-yellow-700">
需要在
<code class="rounded bg-yellow-100 px-1">~/.codex/config.toml</code>
文件中添加以下配置来禁用响应存储
</p>
<div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="whitespace-nowrap text-gray-300">disable_response_storage = true</div>
</div>
</div>
</div>
</div>
</div>
@@ -938,81 +906,42 @@
</p>
<div class="space-y-4">
<div class="rounded-lg border border-indigo-200 bg-white p-3 sm:p-4">
<h6 class="mb-2 text-sm font-medium text-gray-800 dark:text-gray-600 sm:text-base">
Terminal 设置方法
</h6>
<p class="mb-3 text-sm text-gray-600"> Terminal 中运行以下命令</p>
<div class="rounded-lg border border-yellow-200 bg-yellow-50 p-3 sm:p-4">
<h6 class="mb-2 font-medium text-yellow-800">Codex 配置文件</h6>
<p class="mb-3 text-sm text-yellow-700">
<code class="rounded bg-yellow-100 px-1">~/.codex/config.toml</code>
文件中添加以下配置
</p>
<div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="whitespace-nowrap text-gray-300">
export OPENAI_BASE_URL="{{ openaiBaseUrl }}"
</div>
<div class="whitespace-nowrap text-gray-300">
export OPENAI_API_KEY="你的API密钥"
</div>
<div class="whitespace-nowrap text-gray-300">model_provider = "crs"</div>
<div class="whitespace-nowrap text-gray-300">model = "gpt-5"</div>
<div class="whitespace-nowrap text-gray-300">model_reasoning_effort = "high"</div>
<div class="whitespace-nowrap text-gray-300">disable_response_storage = true</div>
<div class="mt-2"></div>
<div class="whitespace-nowrap text-gray-300">[model_providers.crs]</div>
<div class="whitespace-nowrap text-gray-300">name = "crs"</div>
<div class="whitespace-nowrap text-gray-300">base_url = "{{ openaiBaseUrl }}"</div>
<div class="whitespace-nowrap text-gray-300">wire_api = "responses"</div>
</div>
<p class="mt-3 text-sm text-yellow-700">
<code class="rounded bg-yellow-100 px-1">~/.codex/auth.json</code>
文件中配置API密钥
</p>
<div
class="mt-2 overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="whitespace-nowrap text-gray-300">{</div>
<div class="whitespace-nowrap text-gray-300">"OPENAI_API_KEY": "你的API密钥"</div>
<div class="whitespace-nowrap text-gray-300">}</div>
</div>
<p class="mt-2 text-xs text-yellow-700">
💡 使用与 Claude Code 相同的 API 密钥即可格式如 cr_xxxxxxxxxx
</p>
</div>
<div class="rounded-lg border border-indigo-200 bg-white p-3 sm:p-4">
<h6 class="mb-2 text-sm font-medium text-gray-800 dark:text-gray-600 sm:text-base">
永久设置方法
</h6>
<p class="mb-3 text-sm text-gray-600">添加到你的 shell 配置文件</p>
<div
class="mb-3 overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="mb-2"># 对于 zsh (默认)</div>
<div class="whitespace-nowrap text-gray-300">
echo 'export OPENAI_BASE_URL="{{ openaiBaseUrl }}"' >> ~/.zshrc
</div>
<div class="whitespace-nowrap text-gray-300">
echo 'export OPENAI_API_KEY="你的API密钥"' >> ~/.zshrc
</div>
<div class="whitespace-nowrap text-gray-300">source ~/.zshrc</div>
</div>
<div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="mb-2"># 对于 bash</div>
<div class="whitespace-nowrap text-gray-300">
echo 'export OPENAI_BASE_URL="{{ openaiBaseUrl }}"' >> ~/.bash_profile
</div>
<div class="whitespace-nowrap text-gray-300">
echo 'export OPENAI_API_KEY="你的API密钥"' >> ~/.bash_profile
</div>
<div class="whitespace-nowrap text-gray-300">source ~/.bash_profile</div>
</div>
</div>
<div class="rounded-lg border border-indigo-200 bg-indigo-50 p-3 sm:p-4">
<h6 class="mb-2 font-medium text-indigo-800">验证 Codex 环境变量</h6>
<p class="mb-3 text-sm text-indigo-700"> Terminal 中验证</p>
<div
class="space-y-1 overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="whitespace-nowrap text-gray-300">echo $OPENAI_BASE_URL</div>
<div class="whitespace-nowrap text-gray-300">echo $OPENAI_API_KEY</div>
</div>
</div>
<div class="rounded-lg border border-yellow-200 bg-yellow-50 p-3 sm:p-4">
<h6 class="mb-2 font-medium text-yellow-800">Codex 额外配置</h6>
<p class="mb-3 text-sm text-yellow-700">
需要在
<code class="rounded bg-yellow-100 px-1">~/.codex/config.toml</code>
文件中添加以下配置来禁用响应存储
</p>
<div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="whitespace-nowrap text-gray-300">disable_response_storage = true</div>
</div>
</div>
</div>
</div>
</div>
@@ -1454,81 +1383,42 @@
</p>
<div class="space-y-4">
<div class="rounded-lg border border-indigo-200 bg-white p-3 sm:p-4">
<h6 class="mb-2 text-sm font-medium text-gray-800 dark:text-gray-600 sm:text-base">
终端设置方法
</h6>
<p class="mb-3 text-sm text-gray-600">在终端中运行以下命令</p>
<div class="rounded-lg border border-yellow-200 bg-yellow-50 p-3 sm:p-4">
<h6 class="mb-2 font-medium text-yellow-800">Codex 配置文件</h6>
<p class="mb-3 text-sm text-yellow-700">
<code class="rounded bg-yellow-100 px-1">~/.codex/config.toml</code>
文件中添加以下配置
</p>
<div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="whitespace-nowrap text-gray-300">
export OPENAI_BASE_URL="{{ openaiBaseUrl }}"
</div>
<div class="whitespace-nowrap text-gray-300">
export OPENAI_API_KEY="你的API密钥"
</div>
<div class="whitespace-nowrap text-gray-300">model_provider = "crs"</div>
<div class="whitespace-nowrap text-gray-300">model = "gpt-5"</div>
<div class="whitespace-nowrap text-gray-300">model_reasoning_effort = "high"</div>
<div class="whitespace-nowrap text-gray-300">disable_response_storage = true</div>
<div class="mt-2"></div>
<div class="whitespace-nowrap text-gray-300">[model_providers.crs]</div>
<div class="whitespace-nowrap text-gray-300">name = "crs"</div>
<div class="whitespace-nowrap text-gray-300">base_url = "{{ openaiBaseUrl }}"</div>
<div class="whitespace-nowrap text-gray-300">wire_api = "responses"</div>
</div>
<p class="mt-3 text-sm text-yellow-700">
<code class="rounded bg-yellow-100 px-1">~/.codex/auth.json</code>
文件中配置API密钥
</p>
<div
class="mt-2 overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="whitespace-nowrap text-gray-300">{</div>
<div class="whitespace-nowrap text-gray-300">"OPENAI_API_KEY": "你的API密钥"</div>
<div class="whitespace-nowrap text-gray-300">}</div>
</div>
<p class="mt-2 text-xs text-yellow-700">
💡 使用与 Claude Code 相同的 API 密钥即可格式如 cr_xxxxxxxxxx
</p>
</div>
<div class="rounded-lg border border-indigo-200 bg-white p-3 sm:p-4">
<h6 class="mb-2 text-sm font-medium text-gray-800 dark:text-gray-600 sm:text-base">
永久设置方法
</h6>
<p class="mb-3 text-sm text-gray-600">添加到你的 shell 配置文件</p>
<div
class="mb-3 overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="mb-2"># 对于 bash (默认)</div>
<div class="whitespace-nowrap text-gray-300">
echo 'export OPENAI_BASE_URL="{{ openaiBaseUrl }}"' >> ~/.bashrc
</div>
<div class="whitespace-nowrap text-gray-300">
echo 'export OPENAI_API_KEY="你的API密钥"' >> ~/.bashrc
</div>
<div class="whitespace-nowrap text-gray-300">source ~/.bashrc</div>
</div>
<div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="mb-2"># 对于 zsh</div>
<div class="whitespace-nowrap text-gray-300">
echo 'export OPENAI_BASE_URL="{{ openaiBaseUrl }}"' >> ~/.zshrc
</div>
<div class="whitespace-nowrap text-gray-300">
echo 'export OPENAI_API_KEY="你的API密钥"' >> ~/.zshrc
</div>
<div class="whitespace-nowrap text-gray-300">source ~/.zshrc</div>
</div>
</div>
<div class="rounded-lg border border-indigo-200 bg-indigo-50 p-3 sm:p-4">
<h6 class="mb-2 font-medium text-indigo-800">验证 Codex 环境变量</h6>
<p class="mb-3 text-sm text-indigo-700">在终端中验证</p>
<div
class="space-y-1 overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="whitespace-nowrap text-gray-300">echo $OPENAI_BASE_URL</div>
<div class="whitespace-nowrap text-gray-300">echo $OPENAI_API_KEY</div>
</div>
</div>
<div class="rounded-lg border border-yellow-200 bg-yellow-50 p-3 sm:p-4">
<h6 class="mb-2 font-medium text-yellow-800">Codex 额外配置</h6>
<p class="mb-3 text-sm text-yellow-700">
需要在
<code class="rounded bg-yellow-100 px-1">~/.codex/config.toml</code>
文件中添加以下配置来禁用响应存储
</p>
<div
class="overflow-x-auto rounded bg-gray-900 p-2 font-mono text-xs text-green-400 sm:p-3 sm:text-sm"
>
<div class="whitespace-nowrap text-gray-300">disable_response_storage = true</div>
</div>
</div>
</div>
</div>
</div>