mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat: droid的apikey模式适配多种更新方式
This commit is contained in:
@@ -932,7 +932,26 @@ class DroidAccountService {
|
||||
: ''
|
||||
)
|
||||
const newApiKeysInput = Array.isArray(updates.apiKeys) ? updates.apiKeys : []
|
||||
const removeApiKeysInput = Array.isArray(updates.removeApiKeys) ? updates.removeApiKeys : []
|
||||
const wantsClearApiKeys = Boolean(updates.clearApiKeys)
|
||||
const rawApiKeyMode =
|
||||
typeof updates.apiKeyUpdateMode === 'string'
|
||||
? updates.apiKeyUpdateMode.trim().toLowerCase()
|
||||
: ''
|
||||
|
||||
let apiKeyUpdateMode = ['append', 'replace', 'delete'].includes(rawApiKeyMode)
|
||||
? rawApiKeyMode
|
||||
: ''
|
||||
|
||||
if (!apiKeyUpdateMode) {
|
||||
if (wantsClearApiKeys) {
|
||||
apiKeyUpdateMode = 'replace'
|
||||
} else if (removeApiKeysInput.length > 0) {
|
||||
apiKeyUpdateMode = 'delete'
|
||||
} else {
|
||||
apiKeyUpdateMode = 'append'
|
||||
}
|
||||
}
|
||||
|
||||
if (sanitizedUpdates.apiKeys !== undefined) {
|
||||
delete sanitizedUpdates.apiKeys
|
||||
@@ -940,33 +959,94 @@ class DroidAccountService {
|
||||
if (sanitizedUpdates.clearApiKeys !== undefined) {
|
||||
delete sanitizedUpdates.clearApiKeys
|
||||
}
|
||||
if (sanitizedUpdates.apiKeyUpdateMode !== undefined) {
|
||||
delete sanitizedUpdates.apiKeyUpdateMode
|
||||
}
|
||||
if (sanitizedUpdates.removeApiKeys !== undefined) {
|
||||
delete sanitizedUpdates.removeApiKeys
|
||||
}
|
||||
|
||||
if (wantsClearApiKeys || newApiKeysInput.length > 0) {
|
||||
const mergedApiKeys = this._buildApiKeyEntries(
|
||||
let mergedApiKeys = existingApiKeyEntries
|
||||
let apiKeysUpdated = false
|
||||
let addedCount = 0
|
||||
let removedCount = 0
|
||||
|
||||
if (apiKeyUpdateMode === 'delete') {
|
||||
const removalHashes = new Set()
|
||||
|
||||
for (const candidate of removeApiKeysInput) {
|
||||
if (typeof candidate !== 'string') {
|
||||
continue
|
||||
}
|
||||
const trimmed = candidate.trim()
|
||||
if (!trimmed) {
|
||||
continue
|
||||
}
|
||||
const hash = crypto.createHash('sha256').update(trimmed).digest('hex')
|
||||
removalHashes.add(hash)
|
||||
}
|
||||
|
||||
if (removalHashes.size > 0) {
|
||||
mergedApiKeys = existingApiKeyEntries.filter(
|
||||
(entry) => entry && entry.hash && !removalHashes.has(entry.hash)
|
||||
)
|
||||
removedCount = existingApiKeyEntries.length - mergedApiKeys.length
|
||||
apiKeysUpdated = removedCount > 0
|
||||
|
||||
if (!apiKeysUpdated) {
|
||||
logger.warn(
|
||||
`⚠️ 删除模式未匹配任何 Droid API Key: ${accountId} (提供 ${removalHashes.size} 条)`
|
||||
)
|
||||
}
|
||||
} else if (removeApiKeysInput.length > 0) {
|
||||
logger.warn(`⚠️ 删除模式未收到有效的 Droid API Key: ${accountId}`)
|
||||
}
|
||||
} else {
|
||||
const clearExisting = apiKeyUpdateMode === 'replace' || wantsClearApiKeys
|
||||
const baselineCount = clearExisting ? 0 : existingApiKeyEntries.length
|
||||
|
||||
mergedApiKeys = this._buildApiKeyEntries(
|
||||
newApiKeysInput,
|
||||
existingApiKeyEntries,
|
||||
wantsClearApiKeys
|
||||
clearExisting
|
||||
)
|
||||
|
||||
const baselineCount = wantsClearApiKeys ? 0 : existingApiKeyEntries.length
|
||||
const addedCount = Math.max(mergedApiKeys.length - baselineCount, 0)
|
||||
addedCount = Math.max(mergedApiKeys.length - baselineCount, 0)
|
||||
apiKeysUpdated = clearExisting || addedCount > 0
|
||||
}
|
||||
|
||||
if (apiKeysUpdated) {
|
||||
sanitizedUpdates.apiKeys = mergedApiKeys.length ? JSON.stringify(mergedApiKeys) : ''
|
||||
sanitizedUpdates.apiKeyCount = String(mergedApiKeys.length)
|
||||
|
||||
if (apiKeyUpdateMode === 'delete') {
|
||||
logger.info(
|
||||
`🔑 删除模式更新 Droid API keys for ${accountId}: 已移除 ${removedCount} 条,剩余 ${mergedApiKeys.length}`
|
||||
)
|
||||
} else if (apiKeyUpdateMode === 'replace' || wantsClearApiKeys) {
|
||||
logger.info(
|
||||
`🔑 覆盖模式更新 Droid API keys for ${accountId}: 当前总数 ${mergedApiKeys.length},新增 ${addedCount}`
|
||||
)
|
||||
} else {
|
||||
logger.info(
|
||||
`🔑 追加模式更新 Droid API keys for ${accountId}: 当前总数 ${mergedApiKeys.length},新增 ${addedCount}`
|
||||
)
|
||||
}
|
||||
|
||||
if (mergedApiKeys.length > 0) {
|
||||
sanitizedUpdates.authenticationMethod = 'api_key'
|
||||
sanitizedUpdates.status = sanitizedUpdates.status || 'active'
|
||||
logger.info(
|
||||
`🔑 Updated Droid API keys for ${accountId}: total ${mergedApiKeys.length} (added ${addedCount})`
|
||||
)
|
||||
} else {
|
||||
logger.info(`🔑 Cleared all API keys for Droid account ${accountId}`)
|
||||
// 如果完全移除 API Key,可根据是否仍有 token 来确定认证方式
|
||||
if (!sanitizedUpdates.accessToken && !account.accessToken) {
|
||||
sanitizedUpdates.authenticationMethod =
|
||||
account.authenticationMethod === 'api_key' ? '' : account.authenticationMethod
|
||||
}
|
||||
} else if (!sanitizedUpdates.accessToken && !account.accessToken) {
|
||||
const shouldPreserveApiKeyMode =
|
||||
account.authenticationMethod &&
|
||||
account.authenticationMethod.toLowerCase().trim() === 'api_key' &&
|
||||
(apiKeyUpdateMode === 'replace' || apiKeyUpdateMode === 'delete')
|
||||
|
||||
sanitizedUpdates.authenticationMethod = shouldPreserveApiKeyMode
|
||||
? 'api_key'
|
||||
: account.authenticationMethod === 'api_key'
|
||||
? ''
|
||||
: account.authenticationMethod
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2928,10 +2928,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 +2945,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 +2953,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 +2995,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>
|
||||
@@ -3283,7 +3310,7 @@ const form = ref({
|
||||
accessToken: '',
|
||||
refreshToken: '',
|
||||
apiKeysInput: '',
|
||||
clearExistingApiKeys: false,
|
||||
apiKeyUpdateMode: 'append',
|
||||
proxy: initProxyConfig(),
|
||||
// Claude Console 特定字段
|
||||
apiUrl: props.account?.apiUrl || '',
|
||||
@@ -3397,6 +3424,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: '',
|
||||
@@ -4313,19 +4381,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) {
|
||||
@@ -4674,10 +4763,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 = ''
|
||||
@@ -4686,6 +4776,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,
|
||||
@@ -4694,7 +4798,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 = ''
|
||||
}
|
||||
}
|
||||
@@ -4830,6 +4949,16 @@ watch(
|
||||
initModelMappings()
|
||||
// 重新初始化代理配置
|
||||
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 = ''
|
||||
@@ -4858,7 +4987,7 @@ watch(
|
||||
|
||||
form.value = {
|
||||
platform: newAccount.platform,
|
||||
addType: 'oauth',
|
||||
addType: derivedAddType,
|
||||
name: newAccount.name,
|
||||
description: newAccount.description || '',
|
||||
accountType: newAccount.accountType || 'shared',
|
||||
@@ -4872,6 +5001,9 @@ watch(
|
||||
projectId: newAccount.projectId || '',
|
||||
accessToken: '',
|
||||
refreshToken: '',
|
||||
authenticationMethod: newAccount.authenticationMethod || '',
|
||||
apiKeysInput: '',
|
||||
apiKeyUpdateMode: 'append',
|
||||
proxy: proxyConfig,
|
||||
// Claude Console 特定字段
|
||||
apiUrl: newAccount.apiUrl || '',
|
||||
|
||||
Reference in New Issue
Block a user