mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 00:53:33 +00:00
Merge upstream/main into feature/account-expiry-management
解决与 upstream/main 的代码冲突: - 保留账户到期时间 (expiresAt) 功能 - 采用 buildProxyPayload() 函数重构代理配置 - 同步最新的 Droid 平台功能和修复 主要改动: - AccountForm.vue: 整合到期时间字段和新的 proxy 处理方式 - 合并 upstream 的 Droid 多 API Key 支持等新特性 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -561,7 +561,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
|
||||
@@ -2413,6 +2422,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()
|
||||
|
||||
@@ -2551,24 +2568,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
|
||||
@@ -2974,6 +3053,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) => {
|
||||
// 如果有订阅信息
|
||||
|
||||
@@ -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密钥",',
|
||||
|
||||
Reference in New Issue
Block a user