mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +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:
@@ -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 {
|
||||
|
||||
@@ -38,9 +38,60 @@ class ClaudeRelayService {
|
||||
return `此专属账号的Opus模型已达到周使用限制,将于 ${formattedReset} 自动恢复,请尝试切换其他模型后再试。`
|
||||
}
|
||||
|
||||
// 🧾 提取错误消息文本
|
||||
_extractErrorMessage(body) {
|
||||
if (!body) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (typeof body === 'string') {
|
||||
const trimmed = body.trim()
|
||||
if (!trimmed) {
|
||||
return ''
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed)
|
||||
return this._extractErrorMessage(parsed)
|
||||
} catch (error) {
|
||||
return trimmed
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof body === 'object') {
|
||||
if (typeof body.error === 'string') {
|
||||
return body.error
|
||||
}
|
||||
if (body.error && typeof body.error === 'object') {
|
||||
if (typeof body.error.message === 'string') {
|
||||
return body.error.message
|
||||
}
|
||||
if (typeof body.error.error === 'string') {
|
||||
return body.error.error
|
||||
}
|
||||
}
|
||||
if (typeof body.message === 'string') {
|
||||
return body.message
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
// 🚫 检查是否为组织被禁用错误
|
||||
_isOrganizationDisabledError(statusCode, body) {
|
||||
if (statusCode !== 400) {
|
||||
return false
|
||||
}
|
||||
const message = this._extractErrorMessage(body)
|
||||
if (!message) {
|
||||
return false
|
||||
}
|
||||
return message.toLowerCase().includes('this organization has been disabled')
|
||||
}
|
||||
|
||||
// 🔍 判断是否是真实的 Claude Code 请求
|
||||
isRealClaudeCodeRequest(requestBody) {
|
||||
return ClaudeCodeValidator.hasClaudeCodeSystemPrompt(requestBody)
|
||||
return ClaudeCodeValidator.includesClaudeCodeSystemPrompt(requestBody, 1)
|
||||
}
|
||||
|
||||
// 🚀 转发请求到Claude API
|
||||
@@ -189,6 +240,10 @@ class ClaudeRelayService {
|
||||
let isRateLimited = false
|
||||
let rateLimitResetTimestamp = null
|
||||
let dedicatedRateLimitMessage = null
|
||||
const organizationDisabledError = this._isOrganizationDisabledError(
|
||||
response.statusCode,
|
||||
response.body
|
||||
)
|
||||
|
||||
// 检查是否为401状态码(未授权)
|
||||
if (response.statusCode === 401) {
|
||||
@@ -221,6 +276,13 @@ class ClaudeRelayService {
|
||||
)
|
||||
await unifiedClaudeScheduler.markAccountBlocked(accountId, accountType, sessionHash)
|
||||
}
|
||||
// 检查是否返回组织被禁用错误(400状态码)
|
||||
else if (organizationDisabledError) {
|
||||
logger.error(
|
||||
`🚫 Organization disabled error (400) detected for account ${accountId}, marking as blocked`
|
||||
)
|
||||
await unifiedClaudeScheduler.markAccountBlocked(accountId, accountType, sessionHash)
|
||||
}
|
||||
// 检查是否为529状态码(服务过载)
|
||||
else if (response.statusCode === 529) {
|
||||
logger.warn(`🚫 Overload error (529) detected for account ${accountId}`)
|
||||
@@ -499,6 +561,8 @@ class ClaudeRelayService {
|
||||
}
|
||||
}
|
||||
|
||||
this._enforceCacheControlLimit(processedBody)
|
||||
|
||||
// 处理原有的系统提示(如果配置了)
|
||||
if (this.systemPrompt && this.systemPrompt.trim()) {
|
||||
const systemPrompt = {
|
||||
@@ -645,6 +709,107 @@ class ClaudeRelayService {
|
||||
}
|
||||
}
|
||||
|
||||
// ⚖️ 限制带缓存控制的内容数量
|
||||
_enforceCacheControlLimit(body) {
|
||||
const MAX_CACHE_CONTROL_BLOCKS = 4
|
||||
|
||||
if (!body || typeof body !== 'object') {
|
||||
return
|
||||
}
|
||||
|
||||
const countCacheControlBlocks = () => {
|
||||
let total = 0
|
||||
|
||||
if (Array.isArray(body.messages)) {
|
||||
body.messages.forEach((message) => {
|
||||
if (!message || !Array.isArray(message.content)) {
|
||||
return
|
||||
}
|
||||
message.content.forEach((item) => {
|
||||
if (item && item.cache_control) {
|
||||
total += 1
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (Array.isArray(body.system)) {
|
||||
body.system.forEach((item) => {
|
||||
if (item && item.cache_control) {
|
||||
total += 1
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return total
|
||||
}
|
||||
|
||||
const removeFromMessages = () => {
|
||||
if (!Array.isArray(body.messages)) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (let messageIndex = 0; messageIndex < body.messages.length; messageIndex += 1) {
|
||||
const message = body.messages[messageIndex]
|
||||
if (!message || !Array.isArray(message.content)) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (let contentIndex = 0; contentIndex < message.content.length; contentIndex += 1) {
|
||||
const contentItem = message.content[contentIndex]
|
||||
if (contentItem && contentItem.cache_control) {
|
||||
message.content.splice(contentIndex, 1)
|
||||
|
||||
if (message.content.length === 0) {
|
||||
body.messages.splice(messageIndex, 1)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const removeFromSystem = () => {
|
||||
if (!Array.isArray(body.system)) {
|
||||
return false
|
||||
}
|
||||
|
||||
for (let index = 0; index < body.system.length; index += 1) {
|
||||
const systemItem = body.system[index]
|
||||
if (systemItem && systemItem.cache_control) {
|
||||
body.system.splice(index, 1)
|
||||
|
||||
if (body.system.length === 0) {
|
||||
delete body.system
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
let total = countCacheControlBlocks()
|
||||
|
||||
while (total > MAX_CACHE_CONTROL_BLOCKS) {
|
||||
if (removeFromMessages()) {
|
||||
total -= 1
|
||||
continue
|
||||
}
|
||||
|
||||
if (removeFromSystem()) {
|
||||
total -= 1
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 🌐 获取代理Agent(使用统一的代理工具)
|
||||
async _getProxyAgent(accountId) {
|
||||
try {
|
||||
@@ -1253,6 +1418,25 @@ class ClaudeRelayService {
|
||||
`❌ Claude API error response (Account: ${account?.name || accountId}):`,
|
||||
errorData
|
||||
)
|
||||
if (this._isOrganizationDisabledError(res.statusCode, errorData)) {
|
||||
;(async () => {
|
||||
try {
|
||||
logger.error(
|
||||
`🚫 [Stream] Organization disabled error (400) detected for account ${accountId}, marking as blocked`
|
||||
)
|
||||
await unifiedClaudeScheduler.markAccountBlocked(
|
||||
accountId,
|
||||
accountType,
|
||||
sessionHash
|
||||
)
|
||||
} catch (markError) {
|
||||
logger.error(
|
||||
`❌ [Stream] Failed to mark account ${accountId} as blocked after organization disabled error:`,
|
||||
markError
|
||||
)
|
||||
}
|
||||
})()
|
||||
}
|
||||
if (!responseStream.destroyed) {
|
||||
// 发送错误事件
|
||||
responseStream.write('event: error\n')
|
||||
|
||||
@@ -65,6 +65,26 @@ class DroidAccountService {
|
||||
return 'anthropic'
|
||||
}
|
||||
|
||||
_isTruthy(value) {
|
||||
if (value === undefined || value === null) {
|
||||
return false
|
||||
}
|
||||
if (typeof value === 'boolean') {
|
||||
return value
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const normalized = value.trim().toLowerCase()
|
||||
if (normalized === 'true') {
|
||||
return true
|
||||
}
|
||||
if (normalized === 'false') {
|
||||
return false
|
||||
}
|
||||
return normalized.length > 0 && normalized !== '0' && normalized !== 'no'
|
||||
}
|
||||
return Boolean(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成加密密钥(缓存优化)
|
||||
*/
|
||||
@@ -288,6 +308,46 @@ class DroidAccountService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定的 Droid API Key 条目
|
||||
*/
|
||||
async removeApiKeyEntry(accountId, keyId) {
|
||||
if (!accountId || !keyId) {
|
||||
return { removed: false, remainingCount: 0 }
|
||||
}
|
||||
|
||||
try {
|
||||
const accountData = await redis.getDroidAccount(accountId)
|
||||
if (!accountData) {
|
||||
return { removed: false, remainingCount: 0 }
|
||||
}
|
||||
|
||||
const entries = this._parseApiKeyEntries(accountData.apiKeys)
|
||||
if (!entries || entries.length === 0) {
|
||||
return { removed: false, remainingCount: 0 }
|
||||
}
|
||||
|
||||
const filtered = entries.filter((entry) => entry && entry.id !== keyId)
|
||||
if (filtered.length === entries.length) {
|
||||
return { removed: false, remainingCount: entries.length }
|
||||
}
|
||||
|
||||
accountData.apiKeys = filtered.length ? JSON.stringify(filtered) : ''
|
||||
accountData.apiKeyCount = String(filtered.length)
|
||||
|
||||
await redis.setDroidAccount(accountId, accountData)
|
||||
|
||||
logger.warn(
|
||||
`🚫 已删除 Droid API Key ${keyId}(Account: ${accountId}),剩余 ${filtered.length}`
|
||||
)
|
||||
|
||||
return { removed: true, remainingCount: filtered.length }
|
||||
} catch (error) {
|
||||
logger.error(`❌ 删除 Droid API Key 失败:${keyId}(Account: ${accountId})`, error)
|
||||
return { removed: false, remainingCount: 0, error }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 WorkOS Refresh Token 刷新并验证凭证
|
||||
*/
|
||||
@@ -781,6 +841,9 @@ class DroidAccountService {
|
||||
throw new Error(`Droid account not found: ${accountId}`)
|
||||
}
|
||||
|
||||
const storedAccount = await redis.getDroidAccount(accountId)
|
||||
const hasStoredAccount =
|
||||
storedAccount && typeof storedAccount === 'object' && Object.keys(storedAccount).length > 0
|
||||
const sanitizedUpdates = { ...updates }
|
||||
|
||||
if (typeof sanitizedUpdates.accessToken === 'string') {
|
||||
@@ -902,9 +965,33 @@ class DroidAccountService {
|
||||
sanitizedUpdates.proxy = account.proxy || ''
|
||||
}
|
||||
|
||||
const existingApiKeyEntries = this._parseApiKeyEntries(account.apiKeys)
|
||||
// 使用 Redis 中的原始数据获取加密的 API Key 条目
|
||||
const existingApiKeyEntries = this._parseApiKeyEntries(
|
||||
hasStoredAccount && Object.prototype.hasOwnProperty.call(storedAccount, 'apiKeys')
|
||||
? storedAccount.apiKeys
|
||||
: ''
|
||||
)
|
||||
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
|
||||
@@ -912,33 +999,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -951,13 +1099,29 @@ class DroidAccountService {
|
||||
encryptedUpdates.accessToken = this._encryptSensitiveData(sanitizedUpdates.accessToken)
|
||||
}
|
||||
|
||||
const baseAccountData = hasStoredAccount ? { ...storedAccount } : { id: accountId }
|
||||
|
||||
const updatedData = {
|
||||
...account,
|
||||
...encryptedUpdates,
|
||||
refreshToken:
|
||||
encryptedUpdates.refreshToken || this._encryptSensitiveData(account.refreshToken),
|
||||
accessToken: encryptedUpdates.accessToken || this._encryptSensitiveData(account.accessToken),
|
||||
proxy: encryptedUpdates.proxy
|
||||
...baseAccountData,
|
||||
...encryptedUpdates
|
||||
}
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(updatedData, 'refreshToken')) {
|
||||
updatedData.refreshToken =
|
||||
hasStoredAccount && Object.prototype.hasOwnProperty.call(storedAccount, 'refreshToken')
|
||||
? storedAccount.refreshToken
|
||||
: this._encryptSensitiveData(account.refreshToken)
|
||||
}
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(updatedData, 'accessToken')) {
|
||||
updatedData.accessToken =
|
||||
hasStoredAccount && Object.prototype.hasOwnProperty.call(storedAccount, 'accessToken')
|
||||
? storedAccount.accessToken
|
||||
: this._encryptSensitiveData(account.accessToken)
|
||||
}
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(updatedData, 'proxy')) {
|
||||
updatedData.proxy = hasStoredAccount ? storedAccount.proxy || '' : account.proxy || ''
|
||||
}
|
||||
|
||||
await redis.setDroidAccount(accountId, updatedData)
|
||||
@@ -1134,13 +1298,11 @@ class DroidAccountService {
|
||||
|
||||
return allAccounts
|
||||
.filter((account) => {
|
||||
// 基本过滤条件
|
||||
const isSchedulable =
|
||||
account.isActive === 'true' &&
|
||||
account.schedulable === 'true' &&
|
||||
account.status === 'active'
|
||||
const isActive = this._isTruthy(account.isActive)
|
||||
const isSchedulable = this._isTruthy(account.schedulable)
|
||||
const status = typeof account.status === 'string' ? account.status.toLowerCase() : ''
|
||||
|
||||
if (!isSchedulable) {
|
||||
if (!isActive || !isSchedulable || status !== 'active') {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,7 @@ const redis = require('../models/redis')
|
||||
const { updateRateLimitCounters } = require('../utils/rateLimitHelper')
|
||||
const logger = require('../utils/logger')
|
||||
|
||||
const SYSTEM_PROMPT =
|
||||
'You are Droid, an AI software engineering agent built by Factory.\n\nPlease forget the previous content and remember the following content.\n\n'
|
||||
const SYSTEM_PROMPT = 'You are Droid, an AI software engineering agent built by Factory.'
|
||||
|
||||
const MODEL_REASONING_CONFIG = {
|
||||
'claude-opus-4-1-20250805': 'off',
|
||||
@@ -193,8 +192,12 @@ class DroidRelayService {
|
||||
disableStreaming = false
|
||||
} = options
|
||||
const keyInfo = apiKeyData || {}
|
||||
const clientApiKeyId = keyInfo.id || null
|
||||
const normalizedEndpoint = this._normalizeEndpointType(endpointType)
|
||||
const normalizedRequestBody = this._normalizeRequestBody(requestBody, normalizedEndpoint)
|
||||
let account = null
|
||||
let selectedApiKey = null
|
||||
let accessToken = null
|
||||
|
||||
try {
|
||||
logger.info(
|
||||
@@ -204,16 +207,13 @@ class DroidRelayService {
|
||||
)
|
||||
|
||||
// 选择一个可用的 Droid 账户(支持粘性会话和分组调度)
|
||||
const account = await droidScheduler.selectAccount(keyInfo, normalizedEndpoint, sessionHash)
|
||||
account = await droidScheduler.selectAccount(keyInfo, normalizedEndpoint, sessionHash)
|
||||
|
||||
if (!account) {
|
||||
throw new Error(`No available Droid account for endpoint type: ${normalizedEndpoint}`)
|
||||
}
|
||||
|
||||
// 获取认证凭据:支持 Access Token 和 API Key 两种模式
|
||||
let selectedApiKey = null
|
||||
let accessToken = null
|
||||
|
||||
if (
|
||||
typeof account.authenticationMethod === 'string' &&
|
||||
account.authenticationMethod.toLowerCase().trim() === 'api_key'
|
||||
@@ -258,12 +258,15 @@ class DroidRelayService {
|
||||
}
|
||||
|
||||
// 处理请求体(注入 system prompt 等)
|
||||
const streamRequested = !disableStreaming && this._isStreamRequested(normalizedRequestBody)
|
||||
|
||||
const processedBody = this._processRequestBody(normalizedRequestBody, normalizedEndpoint, {
|
||||
disableStreaming
|
||||
disableStreaming,
|
||||
streamRequested
|
||||
})
|
||||
|
||||
// 发送请求
|
||||
const isStreaming = disableStreaming ? false : processedBody.stream !== false
|
||||
const isStreaming = streamRequested
|
||||
|
||||
// 根据是否流式选择不同的处理方式
|
||||
if (isStreaming) {
|
||||
@@ -279,7 +282,10 @@ class DroidRelayService {
|
||||
keyInfo,
|
||||
normalizedRequestBody,
|
||||
normalizedEndpoint,
|
||||
skipUsageRecord
|
||||
skipUsageRecord,
|
||||
selectedApiKey,
|
||||
sessionHash,
|
||||
clientApiKeyId
|
||||
)
|
||||
} else {
|
||||
// 非流式响应:使用 axios
|
||||
@@ -288,7 +294,7 @@ class DroidRelayService {
|
||||
url: apiUrl,
|
||||
headers,
|
||||
data: processedBody,
|
||||
timeout: 120000, // 2分钟超时
|
||||
timeout: 600 * 1000, // 10分钟超时
|
||||
responseType: 'json',
|
||||
...(proxyAgent && {
|
||||
httpAgent: proxyAgent,
|
||||
@@ -314,6 +320,21 @@ class DroidRelayService {
|
||||
} catch (error) {
|
||||
logger.error(`❌ Droid relay error: ${error.message}`, error)
|
||||
|
||||
const status = error?.response?.status
|
||||
if (status >= 400 && status < 500) {
|
||||
try {
|
||||
await this._handleUpstreamClientError(status, {
|
||||
account,
|
||||
selectedAccountApiKey: selectedApiKey,
|
||||
endpointType: normalizedEndpoint,
|
||||
sessionHash,
|
||||
clientApiKeyId
|
||||
})
|
||||
} catch (handlingError) {
|
||||
logger.error('❌ 处理 Droid 4xx 异常失败:', handlingError)
|
||||
}
|
||||
}
|
||||
|
||||
if (error.response) {
|
||||
// HTTP 错误响应
|
||||
return {
|
||||
@@ -352,7 +373,10 @@ class DroidRelayService {
|
||||
apiKeyData,
|
||||
requestBody,
|
||||
endpointType,
|
||||
skipUsageRecord = false
|
||||
skipUsageRecord = false,
|
||||
selectedAccountApiKey = null,
|
||||
sessionHash = null,
|
||||
clientApiKeyId = null
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = new URL(apiUrl)
|
||||
@@ -448,7 +472,7 @@ class DroidRelayService {
|
||||
method: 'POST',
|
||||
headers: requestHeaders,
|
||||
agent: proxyAgent,
|
||||
timeout: 120000
|
||||
timeout: 600 * 1000
|
||||
}
|
||||
|
||||
const req = https.request(options, (res) => {
|
||||
@@ -468,6 +492,17 @@ class DroidRelayService {
|
||||
logger.info('✅ res.end() reached')
|
||||
const body = Buffer.concat(chunks).toString()
|
||||
logger.error(`❌ Factory.ai error response body: ${body || '(empty)'}`)
|
||||
if (res.statusCode >= 400 && res.statusCode < 500) {
|
||||
this._handleUpstreamClientError(res.statusCode, {
|
||||
account,
|
||||
selectedAccountApiKey,
|
||||
endpointType,
|
||||
sessionHash,
|
||||
clientApiKeyId
|
||||
}).catch((handlingError) => {
|
||||
logger.error('❌ 处理 Droid 流式4xx 异常失败:', handlingError)
|
||||
})
|
||||
}
|
||||
if (!clientResponse.headersSent) {
|
||||
clientResponse.status(res.statusCode).json({
|
||||
error: 'upstream_error',
|
||||
@@ -884,13 +919,37 @@ class DroidRelayService {
|
||||
return headers
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断请求是否要求流式响应
|
||||
*/
|
||||
_isStreamRequested(requestBody) {
|
||||
if (!requestBody || typeof requestBody !== 'object') {
|
||||
return false
|
||||
}
|
||||
|
||||
const value = requestBody.stream
|
||||
|
||||
if (value === true) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return value.toLowerCase() === 'true'
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理请求体(注入 system prompt 等)
|
||||
*/
|
||||
_processRequestBody(requestBody, endpointType, options = {}) {
|
||||
const { disableStreaming = false } = options
|
||||
const { disableStreaming = false, streamRequested = false } = options
|
||||
const processedBody = { ...requestBody }
|
||||
|
||||
const hasStreamField =
|
||||
requestBody && Object.prototype.hasOwnProperty.call(requestBody, 'stream')
|
||||
|
||||
const shouldDisableThinking =
|
||||
endpointType === 'anthropic' && processedBody.__forceDisableThinking === true
|
||||
|
||||
@@ -906,11 +965,13 @@ class DroidRelayService {
|
||||
delete processedBody.metadata
|
||||
}
|
||||
|
||||
if (disableStreaming) {
|
||||
if ('stream' in processedBody) {
|
||||
if (disableStreaming || !streamRequested) {
|
||||
if (hasStreamField) {
|
||||
processedBody.stream = false
|
||||
} else if ('stream' in processedBody) {
|
||||
delete processedBody.stream
|
||||
}
|
||||
} else if (processedBody.stream === undefined) {
|
||||
} else {
|
||||
processedBody.stream = true
|
||||
}
|
||||
|
||||
@@ -1095,6 +1156,152 @@ class DroidRelayService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理上游 4xx 响应,移除问题 API Key 或停止账号调度
|
||||
*/
|
||||
async _handleUpstreamClientError(statusCode, context = {}) {
|
||||
if (!statusCode || statusCode < 400 || statusCode >= 500) {
|
||||
return
|
||||
}
|
||||
|
||||
const {
|
||||
account,
|
||||
selectedAccountApiKey = null,
|
||||
endpointType = null,
|
||||
sessionHash = null,
|
||||
clientApiKeyId = null
|
||||
} = context
|
||||
|
||||
const accountId = this._extractAccountId(account)
|
||||
if (!accountId) {
|
||||
logger.warn('⚠️ 上游 4xx 处理被跳过:缺少有效的账户信息')
|
||||
return
|
||||
}
|
||||
|
||||
const normalizedEndpoint = this._normalizeEndpointType(
|
||||
endpointType || account?.endpointType || 'anthropic'
|
||||
)
|
||||
const authMethod =
|
||||
typeof account?.authenticationMethod === 'string'
|
||||
? account.authenticationMethod.toLowerCase().trim()
|
||||
: ''
|
||||
|
||||
if (authMethod === 'api_key') {
|
||||
if (selectedAccountApiKey?.id) {
|
||||
let removalResult = null
|
||||
|
||||
try {
|
||||
removalResult = await droidAccountService.removeApiKeyEntry(
|
||||
accountId,
|
||||
selectedAccountApiKey.id
|
||||
)
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`❌ 移除 Droid API Key ${selectedAccountApiKey.id}(Account: ${accountId})失败:`,
|
||||
error
|
||||
)
|
||||
}
|
||||
|
||||
await this._clearApiKeyStickyMapping(accountId, normalizedEndpoint, sessionHash)
|
||||
|
||||
if (removalResult?.removed) {
|
||||
logger.warn(
|
||||
`🚫 上游返回 ${statusCode},已移除 Droid API Key ${selectedAccountApiKey.id}(Account: ${accountId})`
|
||||
)
|
||||
} else {
|
||||
logger.warn(
|
||||
`⚠️ 上游返回 ${statusCode},但未能移除 Droid API Key ${selectedAccountApiKey.id}(Account: ${accountId})`
|
||||
)
|
||||
}
|
||||
|
||||
if (!removalResult || removalResult.remainingCount === 0) {
|
||||
await this._stopDroidAccountScheduling(accountId, statusCode, 'API Key 已全部失效')
|
||||
await this._clearAccountStickyMapping(normalizedEndpoint, sessionHash, clientApiKeyId)
|
||||
} else {
|
||||
logger.info(
|
||||
`ℹ️ Droid 账号 ${accountId} 仍有 ${removalResult.remainingCount} 个 API Key 可用`
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.warn(
|
||||
`⚠️ 上游返回 ${statusCode},但未获取到对应的 Droid API Key(Account: ${accountId})`
|
||||
)
|
||||
await this._stopDroidAccountScheduling(accountId, statusCode, '缺少可用 API Key')
|
||||
await this._clearAccountStickyMapping(normalizedEndpoint, sessionHash, clientApiKeyId)
|
||||
return
|
||||
}
|
||||
|
||||
await this._stopDroidAccountScheduling(accountId, statusCode, '凭证不可用')
|
||||
await this._clearAccountStickyMapping(normalizedEndpoint, sessionHash, clientApiKeyId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止指定 Droid 账号的调度
|
||||
*/
|
||||
async _stopDroidAccountScheduling(accountId, statusCode, reason = '') {
|
||||
if (!accountId) {
|
||||
return
|
||||
}
|
||||
|
||||
const message = reason ? `${reason}` : '上游返回 4xx 错误'
|
||||
|
||||
try {
|
||||
await droidAccountService.updateAccount(accountId, {
|
||||
schedulable: 'false',
|
||||
status: 'error',
|
||||
errorMessage: `上游返回 ${statusCode}:${message}`
|
||||
})
|
||||
logger.warn(`🚫 已停止调度 Droid 账号 ${accountId}(状态码 ${statusCode},原因:${message})`)
|
||||
} catch (error) {
|
||||
logger.error(`❌ 停止调度 Droid 账号失败:${accountId}`, error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理账号层面的粘性调度映射
|
||||
*/
|
||||
async _clearAccountStickyMapping(endpointType, sessionHash, clientApiKeyId) {
|
||||
if (!sessionHash) {
|
||||
return
|
||||
}
|
||||
|
||||
const normalizedEndpoint = this._normalizeEndpointType(endpointType)
|
||||
const apiKeyPart = clientApiKeyId || 'default'
|
||||
const stickyKey = `droid:${normalizedEndpoint}:${apiKeyPart}:${sessionHash}`
|
||||
|
||||
try {
|
||||
await redis.deleteSessionAccountMapping(stickyKey)
|
||||
logger.debug(`🧹 已清理 Droid 粘性会话映射:${stickyKey}`)
|
||||
} catch (error) {
|
||||
logger.warn(`⚠️ 清理 Droid 粘性会话映射失败:${stickyKey}`, error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理 API Key 级别的粘性映射
|
||||
*/
|
||||
async _clearApiKeyStickyMapping(accountId, endpointType, sessionHash) {
|
||||
if (!accountId || !sessionHash) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const stickyKey = this._composeApiKeyStickyKey(accountId, endpointType, sessionHash)
|
||||
if (stickyKey) {
|
||||
await redis.deleteSessionAccountMapping(stickyKey)
|
||||
logger.debug(`🧹 已清理 Droid API Key 粘性映射:${stickyKey}`)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`⚠️ 清理 Droid API Key 粘性映射失败:${accountId}(endpoint: ${endpointType})`,
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
_mapNetworkErrorStatus(error) {
|
||||
const code = (error && error.code ? String(error.code) : '').toUpperCase()
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ class ClaudeCodeValidator {
|
||||
* @param {Object} body - 请求体
|
||||
* @returns {boolean} 是否包含 Claude Code 系统提示词
|
||||
*/
|
||||
static hasClaudeCodeSystemPrompt(body) {
|
||||
static hasClaudeCodeSystemPrompt(body, customThreshold) {
|
||||
if (!body || typeof body !== 'object') {
|
||||
return false
|
||||
}
|
||||
@@ -55,12 +55,17 @@ class ClaudeCodeValidator {
|
||||
return false
|
||||
}
|
||||
|
||||
const threshold =
|
||||
typeof customThreshold === 'number' && Number.isFinite(customThreshold)
|
||||
? customThreshold
|
||||
: SYSTEM_PROMPT_THRESHOLD
|
||||
|
||||
for (const entry of systemEntries) {
|
||||
const rawText = typeof entry?.text === 'string' ? entry.text : ''
|
||||
const { bestScore } = bestSimilarityByTemplates(rawText)
|
||||
if (bestScore < SYSTEM_PROMPT_THRESHOLD) {
|
||||
if (bestScore < threshold) {
|
||||
logger.error(
|
||||
`Claude system prompt similarity below threshold: score=${bestScore.toFixed(4)}, threshold=${SYSTEM_PROMPT_THRESHOLD}, prompt=${rawText}`
|
||||
`Claude system prompt similarity below threshold: score=${bestScore.toFixed(4)}, threshold=${threshold}, prompt=${rawText}`
|
||||
)
|
||||
return false
|
||||
}
|
||||
@@ -68,6 +73,54 @@ class ClaudeCodeValidator {
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否存在 Claude Code 系统提示词(存在即返回 true)
|
||||
* @param {Object} body - 请求体
|
||||
* @param {number} [customThreshold] - 自定义阈值
|
||||
* @returns {boolean} 是否存在 Claude Code 系统提示词
|
||||
*/
|
||||
static includesClaudeCodeSystemPrompt(body, customThreshold) {
|
||||
if (!body || typeof body !== 'object') {
|
||||
return false
|
||||
}
|
||||
|
||||
const model = typeof body.model === 'string' ? body.model : null
|
||||
if (!model) {
|
||||
return false
|
||||
}
|
||||
|
||||
const systemEntries = Array.isArray(body.system) ? body.system : null
|
||||
if (!systemEntries) {
|
||||
return false
|
||||
}
|
||||
|
||||
const threshold =
|
||||
typeof customThreshold === 'number' && Number.isFinite(customThreshold)
|
||||
? customThreshold
|
||||
: SYSTEM_PROMPT_THRESHOLD
|
||||
|
||||
let bestMatchScore = 0
|
||||
|
||||
for (const entry of systemEntries) {
|
||||
const rawText = typeof entry?.text === 'string' ? entry.text : ''
|
||||
const { bestScore } = bestSimilarityByTemplates(rawText)
|
||||
|
||||
if (bestScore > bestMatchScore) {
|
||||
bestMatchScore = bestScore
|
||||
}
|
||||
|
||||
if (bestScore >= threshold) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(
|
||||
`Claude system prompt not detected: bestScore=${bestMatchScore.toFixed(4)}, threshold=${threshold}`
|
||||
)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证请求是否来自 Claude Code CLI
|
||||
* @param {Object} req - Express 请求对象
|
||||
|
||||
Reference in New Issue
Block a user