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:
litongtongxue
2025-10-12 00:55:25 +08:00
16 changed files with 1338 additions and 228 deletions

View File

@@ -1745,31 +1745,54 @@ router.delete('/account-groups/:groupId', authenticateAdmin, async (req, res) =>
router.get('/account-groups/:groupId/members', authenticateAdmin, async (req, res) => {
try {
const { groupId } = req.params
const group = await accountGroupService.getGroup(groupId)
if (!group) {
return res.status(404).json({ error: '分组不存在' })
}
const memberIds = await accountGroupService.getGroupMembers(groupId)
// 获取成员详细信息
const members = []
for (const memberId of memberIds) {
// 尝试从不同的服务获取账户信息
// 根据分组平台优先查找对应账户
let account = null
switch (group.platform) {
case 'droid':
account = await droidAccountService.getAccount(memberId)
break
case 'gemini':
account = await geminiAccountService.getAccount(memberId)
break
case 'openai':
account = await openaiAccountService.getAccount(memberId)
break
case 'claude':
default:
account = await claudeAccountService.getAccount(memberId)
if (!account) {
account = await claudeConsoleAccountService.getAccount(memberId)
}
break
}
// 先尝试Claude OAuth账户
account = await claudeAccountService.getAccount(memberId)
// 如果找不到尝试Claude Console账户
// 兼容旧数据:若按平台未找到,则继续尝试其他平台
if (!account) {
account = await claudeAccountService.getAccount(memberId)
}
if (!account) {
account = await claudeConsoleAccountService.getAccount(memberId)
}
// 如果还找不到尝试Gemini账户
if (!account) {
account = await geminiAccountService.getAccount(memberId)
}
// 如果还找不到尝试OpenAI账户
if (!account) {
account = await openaiAccountService.getAccount(memberId)
}
if (!account && group.platform !== 'droid') {
account = await droidAccountService.getAccount(memberId)
}
if (account) {
members.push(account)
@@ -8676,7 +8699,52 @@ router.get('/droid-accounts', authenticateAdmin, async (req, res) => {
// 创建 Droid 账户
router.post('/droid-accounts', authenticateAdmin, async (req, res) => {
try {
const account = await droidAccountService.createAccount(req.body)
const { accountType: rawAccountType = 'shared', groupId, groupIds } = req.body
const normalizedAccountType = rawAccountType || 'shared'
if (!['shared', 'dedicated', 'group'].includes(normalizedAccountType)) {
return res.status(400).json({ error: '账户类型必须是 shared、dedicated 或 group' })
}
const normalizedGroupIds = Array.isArray(groupIds)
? groupIds.filter((id) => typeof id === 'string' && id.trim())
: []
if (
normalizedAccountType === 'group' &&
normalizedGroupIds.length === 0 &&
(!groupId || typeof groupId !== 'string' || !groupId.trim())
) {
return res.status(400).json({ error: '分组调度账户必须至少选择一个分组' })
}
const accountPayload = {
...req.body,
accountType: normalizedAccountType
}
delete accountPayload.groupId
delete accountPayload.groupIds
const account = await droidAccountService.createAccount(accountPayload)
if (normalizedAccountType === 'group') {
try {
if (normalizedGroupIds.length > 0) {
await accountGroupService.setAccountGroups(account.id, normalizedGroupIds, 'droid')
} else if (typeof groupId === 'string' && groupId.trim()) {
await accountGroupService.addAccountToGroup(account.id, groupId, 'droid')
}
} catch (groupError) {
logger.error(`Failed to attach Droid account ${account.id} to groups:`, groupError)
return res.status(500).json({
error: 'Failed to bind Droid account to groups',
message: groupError.message
})
}
}
logger.success(`Created Droid account: ${account.name} (${account.id})`)
return res.json({ success: true, data: account })
} catch (error) {
@@ -8689,7 +8757,72 @@ router.post('/droid-accounts', authenticateAdmin, async (req, res) => {
router.put('/droid-accounts/:id', authenticateAdmin, async (req, res) => {
try {
const { id } = req.params
const account = await droidAccountService.updateAccount(id, req.body)
const updates = { ...req.body }
const { accountType: rawAccountType, groupId, groupIds } = updates
if (rawAccountType && !['shared', 'dedicated', 'group'].includes(rawAccountType)) {
return res.status(400).json({ error: '账户类型必须是 shared、dedicated 或 group' })
}
if (
rawAccountType === 'group' &&
(!groupId || typeof groupId !== 'string' || !groupId.trim()) &&
(!Array.isArray(groupIds) || groupIds.length === 0)
) {
return res.status(400).json({ error: '分组调度账户必须至少选择一个分组' })
}
const currentAccount = await droidAccountService.getAccount(id)
if (!currentAccount) {
return res.status(404).json({ error: 'Droid account not found' })
}
const normalizedGroupIds = Array.isArray(groupIds)
? groupIds.filter((gid) => typeof gid === 'string' && gid.trim())
: []
const hasGroupIdsField = Object.prototype.hasOwnProperty.call(updates, 'groupIds')
const hasGroupIdField = Object.prototype.hasOwnProperty.call(updates, 'groupId')
const targetAccountType = rawAccountType || currentAccount.accountType || 'shared'
delete updates.groupId
delete updates.groupIds
if (rawAccountType) {
updates.accountType = targetAccountType
}
const account = await droidAccountService.updateAccount(id, updates)
try {
if (currentAccount.accountType === 'group' && targetAccountType !== 'group') {
await accountGroupService.removeAccountFromAllGroups(id)
} else if (targetAccountType === 'group') {
if (hasGroupIdsField) {
if (normalizedGroupIds.length > 0) {
await accountGroupService.setAccountGroups(id, normalizedGroupIds, 'droid')
} else {
await accountGroupService.removeAccountFromAllGroups(id)
}
} else if (hasGroupIdField && typeof groupId === 'string' && groupId.trim()) {
await accountGroupService.setAccountGroups(id, [groupId], 'droid')
}
}
} catch (groupError) {
logger.error(`Failed to update Droid account ${id} groups:`, groupError)
return res.status(500).json({
error: 'Failed to update Droid account groups',
message: groupError.message
})
}
if (targetAccountType === 'group') {
try {
account.groupInfos = await accountGroupService.getAccountGroups(id)
} catch (groupFetchError) {
logger.debug(`Failed to fetch group infos for Droid account ${id}:`, groupFetchError)
}
}
return res.json({ success: true, data: account })
} catch (error) {
logger.error(`Failed to update Droid account ${req.params.id}:`, error)
@@ -8697,6 +8830,53 @@ router.put('/droid-accounts/:id', authenticateAdmin, async (req, res) => {
}
})
// 切换 Droid 账户调度状态
router.put('/droid-accounts/:id/toggle-schedulable', authenticateAdmin, async (req, res) => {
try {
const { id } = req.params
const account = await droidAccountService.getAccount(id)
if (!account) {
return res.status(404).json({ error: 'Droid account not found' })
}
const currentSchedulable = account.schedulable === true || account.schedulable === 'true'
const newSchedulable = !currentSchedulable
await droidAccountService.updateAccount(id, { schedulable: newSchedulable ? 'true' : 'false' })
const updatedAccount = await droidAccountService.getAccount(id)
const actualSchedulable = updatedAccount
? updatedAccount.schedulable === true || updatedAccount.schedulable === 'true'
: newSchedulable
if (!actualSchedulable) {
await webhookNotifier.sendAccountAnomalyNotification({
accountId: account.id,
accountName: account.name || 'Droid Account',
platform: 'droid',
status: 'disabled',
errorCode: 'DROID_MANUALLY_DISABLED',
reason: '账号已被管理员手动禁用调度',
timestamp: new Date().toISOString()
})
}
logger.success(
`🔄 Admin toggled Droid account schedulable status: ${id} -> ${
actualSchedulable ? 'schedulable' : 'not schedulable'
}`
)
return res.json({ success: true, schedulable: actualSchedulable })
} catch (error) {
logger.error('❌ Failed to toggle Droid account schedulable status:', error)
return res
.status(500)
.json({ error: 'Failed to toggle schedulable status', message: error.message })
}
})
// 删除 Droid 账户
router.delete('/droid-accounts/:id', authenticateAdmin, async (req, res) => {
try {