Merge branch 'Wei-Shaw:main' into main

This commit is contained in:
jft0m
2025-10-12 09:28:12 +08:00
committed by GitHub
16 changed files with 1338 additions and 228 deletions

View File

@@ -1773,6 +1773,9 @@
<ul class="mt-1 list-disc space-y-1 pl-4">
<li>新会话将随机命中一个 Key并在会话有效期内保持粘性。</li>
<li>若某 Key 失效,会自动切换到剩余可用 Key最大化成功率。</li>
<li>
若上游返回 4xx 错误码,该 Key 会被自动移除;全部 Key 清空后账号将暂停调度。
</li>
</ul>
</div>
</div>
@@ -2928,10 +2931,10 @@
</h5>
<p class="mb-1 text-sm text-purple-800 dark:text-purple-200">
当前已保存 <strong>{{ existingApiKeyCount }}</strong> 条 API Key。您可以追加新的
Key 或使用下方选项清空后重新填写
Key,或通过下方模式快速覆盖、删除指定 Key
</p>
<p class="text-xs text-purple-700 dark:text-purple-300">
留空表示保留现有 Key 不变;填写内容后将覆盖或追加(视清空选项而定)
留空表示保留现有 Key 不变;根据所选模式决定是追加、覆盖还是删除输入的 Key
</p>
</div>
</div>
@@ -2945,7 +2948,7 @@
v-model="form.apiKeysInput"
class="form-input w-full resize-none border-gray-300 font-mono text-xs dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:placeholder-gray-400"
:class="{ 'border-red-500': errors.apiKeys }"
placeholder="留空表示不更新;每行一个 API Key"
placeholder="根据模式填写;每行一个 API Key"
rows="6"
/>
<p v-if="errors.apiKeys" class="mt-1 text-xs text-red-500">
@@ -2953,16 +2956,41 @@
</p>
</div>
<label
class="flex cursor-pointer items-center gap-2 rounded-md border border-purple-200 bg-white/80 px-3 py-2 text-sm text-purple-800 transition-colors hover:border-purple-300 dark:border-purple-700 dark:bg-purple-800/20 dark:text-purple-100"
>
<input
v-model="form.clearExistingApiKeys"
class="rounded border-purple-300 text-purple-600 focus:ring-purple-500 dark:border-purple-500 dark:bg-purple-900"
type="checkbox"
/>
<span>清空已有 API Key 后再应用上方的 Key 列表</span>
</label>
<div class="space-y-2">
<div class="flex items-center justify-between">
<span class="text-sm font-semibold text-purple-800 dark:text-purple-100"
>API Key 更新模式</span
>
<span class="text-xs text-purple-600 dark:text-purple-300">
{{ currentApiKeyModeLabel }}
</span>
</div>
<div
class="relative grid h-11 grid-cols-3 overflow-hidden rounded-2xl border border-purple-200/80 bg-gradient-to-r from-purple-50/80 via-white to-purple-50/80 shadow-inner dark:border-purple-700/70 dark:from-purple-900/40 dark:via-purple-900/20 dark:to-purple-900/40"
>
<span
class="pointer-events-none absolute inset-y-0 rounded-2xl bg-gradient-to-r from-purple-500/90 via-purple-600 to-indigo-500/90 shadow-lg ring-1 ring-purple-100/80 transition-all duration-300 ease-out dark:from-purple-500/70 dark:via-purple-600/70 dark:to-indigo-500/70 dark:ring-purple-400/30"
:style="apiKeyModeSliderStyle"
/>
<button
v-for="option in apiKeyModeOptions"
:key="option.value"
class="relative z-10 flex items-center justify-center rounded-2xl px-2 text-xs font-semibold transition-all duration-200 ease-out focus:outline-none focus-visible:ring-2 focus-visible:ring-purple-500/60 dark:focus-visible:ring-purple-400/60"
:class="
form.apiKeyUpdateMode === option.value
? 'text-white drop-shadow-sm'
: 'text-purple-500/80 hover:text-purple-700 dark:text-purple-200/70 dark:hover:text-purple-100'
"
type="button"
@click="form.apiKeyUpdateMode = option.value"
>
{{ option.label }}
</button>
</div>
<p class="text-xs text-purple-700 dark:text-purple-300">
{{ currentApiKeyModeDescription }}
</p>
</div>
<div
class="rounded-lg border border-purple-200 bg-white/70 p-3 text-xs text-purple-800 dark:border-purple-700 dark:bg-purple-800/20 dark:text-purple-100"
@@ -2970,7 +2998,9 @@
<p class="font-medium"><i class="fas fa-lightbulb mr-1" />小提示</p>
<ul class="mt-1 list-disc space-y-1 pl-4">
<li>系统会为新的 Key 自动建立粘性映射,保持同一会话命中同一个 Key。</li>
<li>勾选“清空”后保存即彻底移除旧 Key可用于紧急轮换或封禁处理。</li>
<li>追加模式会保留现有 Key 并在末尾追加新的 Key。</li>
<li>覆盖模式会先清空旧 Key 再写入上方的新列表。</li>
<li>删除模式会根据输入精准移除指定 Key适合快速处理失效或被封禁的 Key。</li>
</ul>
</div>
</div>
@@ -3137,26 +3167,126 @@ const determinePlatformGroup = (platform) => {
return ''
}
// 初始化代理配置
const initProxyConfig = () => {
if (props.account?.proxy && props.account.proxy.host && props.account.proxy.port) {
return {
enabled: true,
type: props.account.proxy.type || 'socks5',
host: props.account.proxy.host,
port: props.account.proxy.port,
username: props.account.proxy.username || '',
password: props.account.proxy.password || ''
const createDefaultProxyState = () => ({
enabled: false,
type: 'socks5',
host: '',
port: '',
username: '',
password: ''
})
const parseProxyResponse = (rawProxy) => {
if (!rawProxy) {
return null
}
let proxyObject = rawProxy
if (typeof rawProxy === 'string') {
try {
proxyObject = JSON.parse(rawProxy)
} catch (error) {
return null
}
}
return {
enabled: false,
type: 'socks5',
host: '',
port: '',
username: '',
password: ''
if (
proxyObject &&
typeof proxyObject === 'object' &&
proxyObject.proxy &&
typeof proxyObject.proxy === 'object'
) {
proxyObject = proxyObject.proxy
}
if (!proxyObject || typeof proxyObject !== 'object') {
return null
}
const host =
typeof proxyObject.host === 'string'
? proxyObject.host.trim()
: proxyObject.host !== undefined && proxyObject.host !== null
? String(proxyObject.host).trim()
: ''
const port =
proxyObject.port !== undefined && proxyObject.port !== null
? String(proxyObject.port).trim()
: ''
const type =
typeof proxyObject.type === 'string' && proxyObject.type.trim()
? proxyObject.type.trim()
: 'socks5'
const username =
typeof proxyObject.username === 'string'
? proxyObject.username
: proxyObject.username !== undefined && proxyObject.username !== null
? String(proxyObject.username)
: ''
const password =
typeof proxyObject.password === 'string'
? proxyObject.password
: proxyObject.password !== undefined && proxyObject.password !== null
? String(proxyObject.password)
: ''
return {
type,
host,
port,
username,
password
}
}
const normalizeProxyFormState = (rawProxy) => {
const parsed = parseProxyResponse(rawProxy)
if (parsed && parsed.host && parsed.port) {
return {
enabled: true,
type: parsed.type || 'socks5',
host: parsed.host,
port: parsed.port,
username: parsed.username || '',
password: parsed.password || ''
}
}
return createDefaultProxyState()
}
const buildProxyPayload = (proxyState) => {
if (!proxyState || !proxyState.enabled) {
return null
}
const host = (proxyState.host || '').trim()
const portNumber = Number.parseInt(proxyState.port, 10)
if (!host || Number.isNaN(portNumber) || portNumber <= 0) {
return null
}
const username = proxyState.username ? proxyState.username.trim() : ''
const password = proxyState.password ? proxyState.password.trim() : ''
return {
type: proxyState.type || 'socks5',
host,
port: portNumber,
username: username || null,
password: password || null
}
}
// 初始化代理配置
const initProxyConfig = () => {
return normalizeProxyFormState(props.account?.proxy)
}
// 表单数据
@@ -3183,7 +3313,7 @@ const form = ref({
accessToken: '',
refreshToken: '',
apiKeysInput: '',
clearExistingApiKeys: false,
apiKeyUpdateMode: 'append',
proxy: initProxyConfig(),
// Claude Console 特定字段
apiUrl: props.account?.apiUrl || '',
@@ -3297,6 +3427,47 @@ const parseApiKeysInput = (input) => {
return uniqueKeys
}
const apiKeyModeOptions = [
{
value: 'append',
label: '追加模式',
description: '保留现有 Key并在末尾追加新 Key 列表。'
},
{
value: 'replace',
label: '覆盖模式',
description: '先清空旧 Key再写入上方的新 Key 列表。'
},
{
value: 'delete',
label: '删除模式',
description: '输入要移除的 Key可精准删除失效或被封禁的 Key。'
}
]
const apiKeyModeSliderStyle = computed(() => {
const index = Math.max(
apiKeyModeOptions.findIndex((option) => option.value === form.value.apiKeyUpdateMode),
0
)
const widthPercent = 100 / apiKeyModeOptions.length
return {
width: `${widthPercent}%`,
left: `${index * widthPercent}%`
}
})
const currentApiKeyModeLabel = computed(() => {
const option = apiKeyModeOptions.find((item) => item.value === form.value.apiKeyUpdateMode)
return option ? option.label : apiKeyModeOptions[0].label
})
const currentApiKeyModeDescription = computed(() => {
const option = apiKeyModeOptions.find((item) => item.value === form.value.apiKeyUpdateMode)
return option ? option.description : apiKeyModeOptions[0].description
})
// 表单验证错误
const errors = ref({
name: '',
@@ -3488,17 +3659,8 @@ const nextStep = async () => {
const generateSetupTokenAuthUrl = async () => {
setupTokenLoading.value = true
try {
const proxyConfig = form.value.proxy?.enabled
? {
proxy: {
type: form.value.proxy.type,
host: form.value.proxy.host,
port: parseInt(form.value.proxy.port),
username: form.value.proxy.username || null,
password: form.value.proxy.password || null
}
}
: {}
const proxyPayload = buildProxyPayload(form.value.proxy)
const proxyConfig = proxyPayload ? { proxy: proxyPayload } : {}
const result = await accountsStore.generateClaudeSetupTokenUrl(proxyConfig)
setupTokenAuthUrl.value = result.authUrl
@@ -3567,14 +3729,9 @@ const exchangeSetupTokenCode = async () => {
}
// 添加代理配置(如果启用)
if (form.value.proxy?.enabled) {
data.proxy = {
type: form.value.proxy.type,
host: form.value.proxy.host,
port: parseInt(form.value.proxy.port),
username: form.value.proxy.username || null,
password: form.value.proxy.password || null
}
const proxyPayload = buildProxyPayload(form.value.proxy)
if (proxyPayload) {
data.proxy = proxyPayload
}
const tokenInfo = await accountsStore.exchangeClaudeSetupTokenCode(data)
@@ -3606,21 +3763,15 @@ const handleOAuthSuccess = async (tokenInfo) => {
form.value.unifiedClientId = generateClientId()
}
const proxyPayload = buildProxyPayload(form.value.proxy)
const data = {
name: form.value.name,
description: form.value.description,
accountType: form.value.accountType,
groupId: form.value.accountType === 'group' ? form.value.groupId : undefined,
groupIds: form.value.accountType === 'group' ? form.value.groupIds : undefined,
proxy: form.value.proxy.enabled
? {
type: form.value.proxy.type,
host: form.value.proxy.host,
port: parseInt(form.value.proxy.port),
username: form.value.proxy.username || null,
password: form.value.proxy.password || null
}
: null
proxy: proxyPayload
}
const currentPlatform = form.value.platform
@@ -3903,21 +4054,15 @@ const createAccount = async () => {
loading.value = true
try {
const proxyPayload = buildProxyPayload(form.value.proxy)
const data = {
name: form.value.name,
description: form.value.description,
accountType: form.value.accountType,
groupId: form.value.accountType === 'group' ? form.value.groupId : undefined,
groupIds: form.value.accountType === 'group' ? form.value.groupIds : undefined,
proxy: form.value.proxy.enabled
? {
type: form.value.proxy.type,
host: form.value.proxy.host,
port: parseInt(form.value.proxy.port),
username: form.value.proxy.username || null,
password: form.value.proxy.password || null
}
: null
proxy: proxyPayload
}
if (form.value.platform === 'claude') {
@@ -4168,21 +4313,15 @@ const updateAccount = async () => {
loading.value = true
try {
const proxyPayload = buildProxyPayload(form.value.proxy)
const data = {
name: form.value.name,
description: form.value.description,
accountType: form.value.accountType,
groupId: form.value.accountType === 'group' ? form.value.groupId : undefined,
groupIds: form.value.accountType === 'group' ? form.value.groupIds : undefined,
proxy: form.value.proxy.enabled
? {
type: form.value.proxy.type,
host: form.value.proxy.host,
port: parseInt(form.value.proxy.port),
username: form.value.proxy.username || null,
password: form.value.proxy.password || null
}
: null
proxy: proxyPayload
}
// 只有非空时才更新token
@@ -4245,19 +4384,40 @@ const updateAccount = async () => {
if (props.account.platform === 'droid') {
const trimmedApiKeysInput = form.value.apiKeysInput?.trim() || ''
const apiKeyUpdateMode = form.value.apiKeyUpdateMode || 'append'
if (trimmedApiKeysInput) {
const apiKeys = parseApiKeysInput(trimmedApiKeysInput)
if (apiKeys.length === 0) {
errors.value.apiKeys = '请至少填写一个 API Key'
if (apiKeyUpdateMode === 'delete') {
if (!trimmedApiKeysInput) {
errors.value.apiKeys = '请填写需要删除的 API Key'
loading.value = false
return
}
data.apiKeys = apiKeys
}
if (form.value.clearExistingApiKeys) {
data.clearApiKeys = true
const removeApiKeys = parseApiKeysInput(trimmedApiKeysInput)
if (removeApiKeys.length === 0) {
errors.value.apiKeys = '请填写需要删除的 API Key'
loading.value = false
return
}
data.removeApiKeys = removeApiKeys
data.apiKeyUpdateMode = 'delete'
} else {
if (trimmedApiKeysInput) {
const apiKeys = parseApiKeysInput(trimmedApiKeysInput)
if (apiKeys.length === 0) {
errors.value.apiKeys = '请至少填写一个 API Key'
loading.value = false
return
}
data.apiKeys = apiKeys
} else if (apiKeyUpdateMode === 'replace') {
data.apiKeys = []
}
if (apiKeyUpdateMode !== 'append' || trimmedApiKeysInput) {
data.apiKeyUpdateMode = apiKeyUpdateMode
}
}
if (isEditingDroidApiKey.value) {
@@ -4606,10 +4766,11 @@ watch(
errors.value.accessToken = ''
errors.value.refreshToken = ''
form.value.authenticationMethod = 'api_key'
form.value.apiKeyUpdateMode = 'append'
} else if (oldType === 'apikey') {
// 切换离开 API Key 模式时重置 API Key 输入
form.value.apiKeysInput = ''
form.value.clearExistingApiKeys = false
form.value.apiKeyUpdateMode = 'append'
errors.value.apiKeys = ''
if (!isEdit.value) {
form.value.authenticationMethod = ''
@@ -4618,6 +4779,20 @@ watch(
}
)
// 监听 API Key 更新模式切换,自动清理提示
watch(
() => form.value.apiKeyUpdateMode,
(newMode, oldMode) => {
if (newMode === oldMode) {
return
}
if (errors.value.apiKeys) {
errors.value.apiKeys = ''
}
}
)
// 监听 API Key 输入,自动清理错误提示
watch(
() => form.value.apiKeysInput,
@@ -4626,7 +4801,22 @@ watch(
return
}
if (parseApiKeysInput(newValue).length > 0) {
const parsed = parseApiKeysInput(newValue)
const mode = form.value.apiKeyUpdateMode
if (mode === 'append' && parsed.length > 0) {
errors.value.apiKeys = ''
return
}
if (mode === 'replace') {
if (parsed.length > 0 || !newValue || newValue.trim() === '') {
errors.value.apiKeys = ''
}
return
}
if (mode === 'delete' && parsed.length > 0) {
errors.value.apiKeys = ''
}
}
@@ -4761,24 +4951,17 @@ watch(
if (newAccount) {
initModelMappings()
// 重新初始化代理配置
const proxyConfig =
newAccount.proxy && newAccount.proxy.host && newAccount.proxy.port
? {
enabled: true,
type: newAccount.proxy.type || 'socks5',
host: newAccount.proxy.host,
port: newAccount.proxy.port,
username: newAccount.proxy.username || '',
password: newAccount.proxy.password || ''
}
: {
enabled: false,
type: 'socks5',
host: '',
port: '',
username: '',
password: ''
}
const proxyConfig = normalizeProxyFormState(newAccount.proxy)
const normalizedAuthMethod =
typeof newAccount.authenticationMethod === 'string'
? newAccount.authenticationMethod.trim().toLowerCase()
: ''
const derivedAddType =
normalizedAuthMethod === 'api_key'
? 'apikey'
: normalizedAuthMethod === 'manual'
? 'manual'
: 'oauth'
// 获取分组ID - 可能来自 groupId 字段或 groupInfo 对象
let groupId = ''
@@ -4807,7 +4990,7 @@ watch(
form.value = {
platform: newAccount.platform,
addType: 'oauth',
addType: derivedAddType,
name: newAccount.name,
description: newAccount.description || '',
accountType: newAccount.accountType || 'shared',
@@ -4821,6 +5004,9 @@ watch(
projectId: newAccount.projectId || '',
accessToken: '',
refreshToken: '',
authenticationMethod: newAccount.authenticationMethod || '',
apiKeysInput: '',
apiKeyUpdateMode: 'append',
proxy: proxyConfig,
// Claude Console 特定字段
apiUrl: newAccount.apiUrl || '',

View File

@@ -546,7 +546,16 @@
>Droid</span
>
<span class="mx-1 h-4 w-px bg-cyan-300 dark:bg-cyan-600" />
<span class="text-xs font-medium text-cyan-700 dark:text-cyan-300">OAuth</span>
<span class="text-xs font-medium text-cyan-700 dark:text-cyan-300">
{{ getDroidAuthType(account) }}
</span>
<span
v-if="isDroidApiKeyMode(account)"
:class="getDroidApiKeyBadgeClasses(account)"
>
<i class="fas fa-key text-[9px]" />
<span>x{{ getDroidApiKeyCount(account) }}</span>
</span>
</div>
<div
v-else
@@ -2351,6 +2360,14 @@ const loadAccounts = async (forceReload = false) => {
}
}
filteredAccounts = filteredAccounts.map((account) => {
const proxyConfig = normalizeProxyData(account.proxyConfig || account.proxy)
return {
...account,
proxyConfig: proxyConfig || null
}
})
accounts.value = filteredAccounts
cleanupSelectedAccounts()
@@ -2489,24 +2506,86 @@ const filterByGroup = () => {
loadAccounts()
}
// 规范化代理配置,支持字符串与对象
function normalizeProxyData(proxy) {
if (!proxy) {
return null
}
let proxyObject = proxy
if (typeof proxy === 'string') {
try {
proxyObject = JSON.parse(proxy)
} catch (error) {
return null
}
}
if (!proxyObject || typeof proxyObject !== 'object') {
return null
}
const candidate =
proxyObject.proxy && typeof proxyObject.proxy === 'object' ? proxyObject.proxy : proxyObject
const host =
typeof candidate.host === 'string'
? candidate.host.trim()
: candidate.host !== undefined && candidate.host !== null
? String(candidate.host).trim()
: ''
const port =
candidate.port !== undefined && candidate.port !== null ? String(candidate.port).trim() : ''
if (!host || !port) {
return null
}
const type =
typeof candidate.type === 'string' && candidate.type.trim() ? candidate.type.trim() : 'socks5'
const username =
typeof candidate.username === 'string'
? candidate.username
: candidate.username !== undefined && candidate.username !== null
? String(candidate.username)
: ''
const password =
typeof candidate.password === 'string'
? candidate.password
: candidate.password !== undefined && candidate.password !== null
? String(candidate.password)
: ''
return {
type,
host,
port,
username,
password
}
}
// 格式化代理信息显示
const formatProxyDisplay = (proxy) => {
if (!proxy || !proxy.host || !proxy.port) return null
const parsed = normalizeProxyData(proxy)
if (!parsed) {
return null
}
// 缩短类型名称
const typeShort = proxy.type === 'socks5' ? 'S5' : proxy.type.toUpperCase()
const typeShort = parsed.type.toLowerCase() === 'socks5' ? 'S5' : parsed.type.toUpperCase()
// 缩短主机名(如果太长)
let host = proxy.host
let host = parsed.host
if (host.length > 15) {
host = host.substring(0, 12) + '...'
}
let display = `${typeShort}://${host}:${proxy.port}`
let display = `${typeShort}://${host}:${parsed.port}`
// 如果有用户名密码,添加认证信息(部分隐藏)
if (proxy.username) {
display = `${typeShort}://***@${host}:${proxy.port}`
if (parsed.username) {
display = `${typeShort}://***@${host}:${parsed.port}`
}
return display
@@ -2960,6 +3039,112 @@ const getOpenAIAuthType = () => {
return 'OAuth'
}
// 获取 Droid 账号的认证方式
const getDroidAuthType = (account) => {
if (!account || typeof account !== 'object') {
return 'OAuth'
}
const apiKeyModeFlag =
account.isApiKeyMode ?? account.is_api_key_mode ?? account.apiKeyMode ?? account.api_key_mode
if (
apiKeyModeFlag === true ||
apiKeyModeFlag === 'true' ||
apiKeyModeFlag === 1 ||
apiKeyModeFlag === '1'
) {
return 'API Key'
}
const methodCandidate =
account.authenticationMethod ||
account.authMethod ||
account.authentication_mode ||
account.authenticationMode ||
account.authentication_method ||
account.auth_type ||
account.authType ||
account.authentication_type ||
account.authenticationType ||
account.droidAuthType ||
account.droidAuthenticationMethod ||
account.method ||
account.auth ||
''
if (typeof methodCandidate === 'string') {
const normalized = methodCandidate.trim().toLowerCase()
const compacted = normalized.replace(/[\s_-]/g, '')
if (compacted === 'apikey') {
return 'API Key'
}
}
return 'OAuth'
}
// 判断是否为 API Key 模式的 Droid 账号
const isDroidApiKeyMode = (account) => getDroidAuthType(account) === 'API Key'
// 获取 Droid 账号的 API Key 数量
const getDroidApiKeyCount = (account) => {
if (!account || typeof account !== 'object') {
return 0
}
const candidates = [
account.apiKeyCount,
account.api_key_count,
account.apiKeysCount,
account.api_keys_count
]
for (const candidate of candidates) {
const value = Number(candidate)
if (Number.isFinite(value) && value >= 0) {
return value
}
}
if (Array.isArray(account.apiKeys)) {
return account.apiKeys.length
}
if (typeof account.apiKeys === 'string' && account.apiKeys.trim()) {
try {
const parsed = JSON.parse(account.apiKeys)
if (Array.isArray(parsed)) {
return parsed.length
}
} catch (error) {
// 忽略解析错误,维持默认值
}
}
return 0
}
// 根据数量返回徽标样式
const getDroidApiKeyBadgeClasses = (account) => {
const count = getDroidApiKeyCount(account)
const baseClass =
'ml-1 inline-flex items-center gap-1 rounded-md border px-1.5 py-[1px] text-[10px] font-medium shadow-sm backdrop-blur-sm'
if (count > 0) {
return [
baseClass,
'border-cyan-200 bg-cyan-50/90 text-cyan-700 dark:border-cyan-500/40 dark:bg-cyan-900/40 dark:text-cyan-200'
]
}
return [
baseClass,
'border-rose-200 bg-rose-50/90 text-rose-600 dark:border-rose-500/40 dark:bg-rose-900/40 dark:text-rose-200'
]
}
// 获取 Claude 账号类型显示
const getClaudeAccountType = (account) => {
// 如果有订阅信息

View File

@@ -2309,7 +2309,7 @@ const droidCliConfigLines = computed(() => [
'{',
' "custom_models": [',
' {',
' "model_display_name": "Sonnet 4.5 [Custom]",',
' "model_display_name": "Sonnet 4.5 [crs]",',
' "model": "claude-sonnet-4-5-20250929",',
` "base_url": "${droidClaudeBaseUrl.value}",`,
' "api_key": "你的API密钥",',
@@ -2317,7 +2317,7 @@ const droidCliConfigLines = computed(() => [
' "max_tokens": 8192',
' },',
' {',
' "model_display_name": "GPT5-Codex [Custom]",',
' "model_display_name": "GPT5-Codex [crs]",',
' "model": "gpt-5-codex",',
` "base_url": "${droidOpenaiBaseUrl.value}",`,
' "api_key": "你的API密钥",',