mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 09:38:02 +00:00
Merge branch 'main' of https://github.com/Wei-Shaw/claude-relay-service
This commit is contained in:
@@ -59,3 +59,9 @@ TRUST_PROXY=true
|
|||||||
|
|
||||||
# 🔒 客户端限制(可选)
|
# 🔒 客户端限制(可选)
|
||||||
# ALLOW_CUSTOM_CLIENTS=false
|
# ALLOW_CUSTOM_CLIENTS=false
|
||||||
|
|
||||||
|
# 📢 Webhook 通知配置
|
||||||
|
WEBHOOK_ENABLED=true
|
||||||
|
WEBHOOK_URLS=https://your-webhook-url.com/notify,https://backup-webhook.com/notify
|
||||||
|
WEBHOOK_TIMEOUT=10000
|
||||||
|
WEBHOOK_RETRIES=3
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
const path = require('path');
|
const path = require('path')
|
||||||
require('dotenv').config();
|
require('dotenv').config()
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
// 🌐 服务器配置
|
// 🌐 服务器配置
|
||||||
@@ -29,14 +29,16 @@ const config = {
|
|||||||
retryDelayOnFailover: 100,
|
retryDelayOnFailover: 100,
|
||||||
maxRetriesPerRequest: 3,
|
maxRetriesPerRequest: 3,
|
||||||
lazyConnect: true,
|
lazyConnect: true,
|
||||||
enableTLS: process.env.REDIS_ENABLE_TLS === 'true',
|
enableTLS: process.env.REDIS_ENABLE_TLS === 'true'
|
||||||
},
|
},
|
||||||
|
|
||||||
// 🎯 Claude API配置
|
// 🎯 Claude API配置
|
||||||
claude: {
|
claude: {
|
||||||
apiUrl: process.env.CLAUDE_API_URL || 'https://api.anthropic.com/v1/messages',
|
apiUrl: process.env.CLAUDE_API_URL || 'https://api.anthropic.com/v1/messages',
|
||||||
apiVersion: process.env.CLAUDE_API_VERSION || '2023-06-01',
|
apiVersion: process.env.CLAUDE_API_VERSION || '2023-06-01',
|
||||||
betaHeader: process.env.CLAUDE_BETA_HEADER || 'claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14'
|
betaHeader:
|
||||||
|
process.env.CLAUDE_BETA_HEADER ||
|
||||||
|
'claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,fine-grained-tool-streaming-2025-05-14'
|
||||||
},
|
},
|
||||||
|
|
||||||
// ☁️ Bedrock API配置
|
// ☁️ Bedrock API配置
|
||||||
@@ -45,7 +47,8 @@ const config = {
|
|||||||
defaultRegion: process.env.AWS_REGION || 'us-east-1',
|
defaultRegion: process.env.AWS_REGION || 'us-east-1',
|
||||||
smallFastModelRegion: process.env.ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION,
|
smallFastModelRegion: process.env.ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION,
|
||||||
defaultModel: process.env.ANTHROPIC_MODEL || 'us.anthropic.claude-sonnet-4-20250514-v1:0',
|
defaultModel: process.env.ANTHROPIC_MODEL || 'us.anthropic.claude-sonnet-4-20250514-v1:0',
|
||||||
smallFastModel: process.env.ANTHROPIC_SMALL_FAST_MODEL || 'us.anthropic.claude-3-5-haiku-20241022-v1:0',
|
smallFastModel:
|
||||||
|
process.env.ANTHROPIC_SMALL_FAST_MODEL || 'us.anthropic.claude-3-5-haiku-20241022-v1:0',
|
||||||
maxOutputTokens: parseInt(process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS) || 4096,
|
maxOutputTokens: parseInt(process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS) || 4096,
|
||||||
maxThinkingTokens: parseInt(process.env.MAX_THINKING_TOKENS) || 1024,
|
maxThinkingTokens: parseInt(process.env.MAX_THINKING_TOKENS) || 1024,
|
||||||
enablePromptCaching: process.env.DISABLE_PROMPT_CACHING !== '1'
|
enablePromptCaching: process.env.DISABLE_PROMPT_CACHING !== '1'
|
||||||
@@ -82,7 +85,9 @@ const config = {
|
|||||||
// 🎨 Web界面配置
|
// 🎨 Web界面配置
|
||||||
web: {
|
web: {
|
||||||
title: process.env.WEB_TITLE || 'Claude Relay Service',
|
title: process.env.WEB_TITLE || 'Claude Relay Service',
|
||||||
description: process.env.WEB_DESCRIPTION || 'Multi-account Claude API relay service with beautiful management interface',
|
description:
|
||||||
|
process.env.WEB_DESCRIPTION ||
|
||||||
|
'Multi-account Claude API relay service with beautiful management interface',
|
||||||
logoUrl: process.env.WEB_LOGO_URL || '/assets/logo.png',
|
logoUrl: process.env.WEB_LOGO_URL || '/assets/logo.png',
|
||||||
enableCors: process.env.ENABLE_CORS === 'true',
|
enableCors: process.env.ENABLE_CORS === 'true',
|
||||||
sessionSecret: process.env.WEB_SESSION_SECRET || 'CHANGE-THIS-SESSION-SECRET'
|
sessionSecret: process.env.WEB_SESSION_SECRET || 'CHANGE-THIS-SESSION-SECRET'
|
||||||
@@ -98,7 +103,7 @@ const config = {
|
|||||||
description: 'Official Claude Code CLI',
|
description: 'Official Claude Code CLI',
|
||||||
// 匹配 Claude CLI 的 User-Agent
|
// 匹配 Claude CLI 的 User-Agent
|
||||||
// 示例: claude-cli/1.0.58 (external, cli)
|
// 示例: claude-cli/1.0.58 (external, cli)
|
||||||
userAgentPattern: /^claude-cli\/[\d\.]+\s+\(/i
|
userAgentPattern: /^claude-cli\/[\d.]+\s+\(/i
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'gemini_cli',
|
id: 'gemini_cli',
|
||||||
@@ -106,7 +111,7 @@ const config = {
|
|||||||
description: 'Gemini Command Line Interface',
|
description: 'Gemini Command Line Interface',
|
||||||
// 匹配 GeminiCLI 的 User-Agent
|
// 匹配 GeminiCLI 的 User-Agent
|
||||||
// 示例: GeminiCLI/v18.20.8 (darwin; arm64)
|
// 示例: GeminiCLI/v18.20.8 (darwin; arm64)
|
||||||
userAgentPattern: /^GeminiCLI\/v?[\d\.]+\s+\(/i
|
userAgentPattern: /^GeminiCLI\/v?[\d.]+\s+\(/i
|
||||||
}
|
}
|
||||||
// 添加自定义客户端示例:
|
// 添加自定义客户端示例:
|
||||||
// {
|
// {
|
||||||
@@ -120,11 +125,21 @@ const config = {
|
|||||||
allowCustomClients: process.env.ALLOW_CUSTOM_CLIENTS === 'true'
|
allowCustomClients: process.env.ALLOW_CUSTOM_CLIENTS === 'true'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 📢 Webhook通知配置
|
||||||
|
webhook: {
|
||||||
|
enabled: process.env.WEBHOOK_ENABLED !== 'false', // 默认启用
|
||||||
|
urls: process.env.WEBHOOK_URLS
|
||||||
|
? process.env.WEBHOOK_URLS.split(',').map((url) => url.trim())
|
||||||
|
: [],
|
||||||
|
timeout: parseInt(process.env.WEBHOOK_TIMEOUT) || 10000, // 10秒超时
|
||||||
|
retries: parseInt(process.env.WEBHOOK_RETRIES) || 3 // 重试3次
|
||||||
|
},
|
||||||
|
|
||||||
// 🛠️ 开发配置
|
// 🛠️ 开发配置
|
||||||
development: {
|
development: {
|
||||||
debug: process.env.DEBUG === 'true',
|
debug: process.env.DEBUG === 'true',
|
||||||
hotReload: process.env.HOT_RELOAD === 'true'
|
hotReload: process.env.HOT_RELOAD === 'true'
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const geminiRoutes = require('./routes/geminiRoutes')
|
|||||||
const openaiGeminiRoutes = require('./routes/openaiGeminiRoutes')
|
const openaiGeminiRoutes = require('./routes/openaiGeminiRoutes')
|
||||||
const openaiClaudeRoutes = require('./routes/openaiClaudeRoutes')
|
const openaiClaudeRoutes = require('./routes/openaiClaudeRoutes')
|
||||||
const openaiRoutes = require('./routes/openaiRoutes')
|
const openaiRoutes = require('./routes/openaiRoutes')
|
||||||
|
const webhookRoutes = require('./routes/webhook')
|
||||||
|
|
||||||
// Import middleware
|
// Import middleware
|
||||||
const {
|
const {
|
||||||
@@ -236,6 +237,7 @@ class Application {
|
|||||||
this.app.use('/openai/gemini', openaiGeminiRoutes)
|
this.app.use('/openai/gemini', openaiGeminiRoutes)
|
||||||
this.app.use('/openai/claude', openaiClaudeRoutes)
|
this.app.use('/openai/claude', openaiClaudeRoutes)
|
||||||
this.app.use('/openai', openaiRoutes)
|
this.app.use('/openai', openaiRoutes)
|
||||||
|
this.app.use('/admin/webhook', webhookRoutes)
|
||||||
|
|
||||||
// 🏠 根路径重定向到新版管理界面
|
// 🏠 根路径重定向到新版管理界面
|
||||||
this.app.get('/', (req, res) => {
|
this.app.get('/', (req, res) => {
|
||||||
|
|||||||
120
src/routes/webhook.js
Normal file
120
src/routes/webhook.js
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
const express = require('express')
|
||||||
|
const router = express.Router()
|
||||||
|
const logger = require('../utils/logger')
|
||||||
|
const webhookNotifier = require('../utils/webhookNotifier')
|
||||||
|
const { authenticateAdmin } = require('../middleware/auth')
|
||||||
|
|
||||||
|
// 测试Webhook连通性
|
||||||
|
router.post('/test', authenticateAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { url } = req.body
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Missing webhook URL',
|
||||||
|
message: 'Please provide a webhook URL to test'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证URL格式
|
||||||
|
try {
|
||||||
|
new URL(url)
|
||||||
|
} catch (urlError) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Invalid URL format',
|
||||||
|
message: 'Please provide a valid webhook URL'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`🧪 Testing webhook URL: ${url}`)
|
||||||
|
|
||||||
|
const result = await webhookNotifier.testWebhook(url)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
logger.info(`✅ Webhook test successful for: ${url}`)
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Webhook test successful',
|
||||||
|
url
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
logger.warn(`❌ Webhook test failed for: ${url} - ${result.error}`)
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Webhook test failed',
|
||||||
|
url,
|
||||||
|
error: result.error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('❌ Webhook test error:', error)
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Internal server error',
|
||||||
|
message: 'Failed to test webhook'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 手动触发账号异常通知(用于测试)
|
||||||
|
router.post('/test-notification', authenticateAdmin, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
accountId = 'test-account-id',
|
||||||
|
accountName = 'Test Account',
|
||||||
|
platform = 'claude-oauth',
|
||||||
|
status = 'error',
|
||||||
|
errorCode = 'TEST_ERROR',
|
||||||
|
reason = 'Manual test notification'
|
||||||
|
} = req.body
|
||||||
|
|
||||||
|
logger.info(`🧪 Sending test notification for account: ${accountName}`)
|
||||||
|
|
||||||
|
await webhookNotifier.sendAccountAnomalyNotification({
|
||||||
|
accountId,
|
||||||
|
accountName,
|
||||||
|
platform,
|
||||||
|
status,
|
||||||
|
errorCode,
|
||||||
|
reason
|
||||||
|
})
|
||||||
|
|
||||||
|
logger.info(`✅ Test notification sent successfully`)
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Test notification sent successfully',
|
||||||
|
data: {
|
||||||
|
accountId,
|
||||||
|
accountName,
|
||||||
|
platform,
|
||||||
|
status,
|
||||||
|
errorCode,
|
||||||
|
reason
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('❌ Failed to send test notification:', error)
|
||||||
|
res.status(500).json({
|
||||||
|
error: 'Internal server error',
|
||||||
|
message: 'Failed to send test notification'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取Webhook配置信息
|
||||||
|
router.get('/config', authenticateAdmin, (req, res) => {
|
||||||
|
const config = require('../../config/config')
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
config: {
|
||||||
|
enabled: config.webhook?.enabled !== false,
|
||||||
|
urls: config.webhook?.urls || [],
|
||||||
|
timeout: config.webhook?.timeout || 10000,
|
||||||
|
retries: config.webhook?.retries || 3,
|
||||||
|
urlCount: (config.webhook?.urls || []).length
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = router
|
||||||
@@ -302,6 +302,21 @@ class ClaudeAccountService {
|
|||||||
accountData.status = 'error'
|
accountData.status = 'error'
|
||||||
accountData.errorMessage = error.message
|
accountData.errorMessage = error.message
|
||||||
await redis.setClaudeAccount(accountId, accountData)
|
await redis.setClaudeAccount(accountId, accountData)
|
||||||
|
|
||||||
|
// 发送Webhook通知
|
||||||
|
try {
|
||||||
|
const webhookNotifier = require('../utils/webhookNotifier')
|
||||||
|
await webhookNotifier.sendAccountAnomalyNotification({
|
||||||
|
accountId,
|
||||||
|
accountName: accountData.name,
|
||||||
|
platform: 'claude-oauth',
|
||||||
|
status: 'error',
|
||||||
|
errorCode: 'CLAUDE_OAUTH_ERROR',
|
||||||
|
reason: `Token refresh failed: ${error.message}`
|
||||||
|
})
|
||||||
|
} catch (webhookError) {
|
||||||
|
logger.error('Failed to send webhook notification:', webhookError)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.error(`❌ Failed to refresh token for account ${accountId}:`, error)
|
logger.error(`❌ Failed to refresh token for account ${accountId}:`, error)
|
||||||
@@ -1554,6 +1569,21 @@ class ClaudeAccountService {
|
|||||||
`⚠️ Account ${accountData.name} (${accountId}) marked as unauthorized and disabled for scheduling`
|
`⚠️ Account ${accountData.name} (${accountId}) marked as unauthorized and disabled for scheduling`
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 发送Webhook通知
|
||||||
|
try {
|
||||||
|
const webhookNotifier = require('../utils/webhookNotifier')
|
||||||
|
await webhookNotifier.sendAccountAnomalyNotification({
|
||||||
|
accountId,
|
||||||
|
accountName: accountData.name,
|
||||||
|
platform: 'claude-oauth',
|
||||||
|
status: 'unauthorized',
|
||||||
|
errorCode: 'CLAUDE_OAUTH_UNAUTHORIZED',
|
||||||
|
reason: 'Account unauthorized (401 errors detected)'
|
||||||
|
})
|
||||||
|
} catch (webhookError) {
|
||||||
|
logger.error('Failed to send webhook notification:', webhookError)
|
||||||
|
}
|
||||||
|
|
||||||
return { success: true }
|
return { success: true }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`❌ Failed to mark account ${accountId} as unauthorized:`, error)
|
logger.error(`❌ Failed to mark account ${accountId} as unauthorized:`, error)
|
||||||
|
|||||||
@@ -403,6 +403,9 @@ class ClaudeConsoleAccountService {
|
|||||||
try {
|
try {
|
||||||
const client = redis.getClientSafe()
|
const client = redis.getClientSafe()
|
||||||
|
|
||||||
|
// 获取账户信息用于webhook通知
|
||||||
|
const accountData = await client.hgetall(`${this.ACCOUNT_KEY_PREFIX}${accountId}`)
|
||||||
|
|
||||||
const updates = {
|
const updates = {
|
||||||
status: 'blocked',
|
status: 'blocked',
|
||||||
errorMessage: reason,
|
errorMessage: reason,
|
||||||
@@ -412,6 +415,24 @@ class ClaudeConsoleAccountService {
|
|||||||
await client.hset(`${this.ACCOUNT_KEY_PREFIX}${accountId}`, updates)
|
await client.hset(`${this.ACCOUNT_KEY_PREFIX}${accountId}`, updates)
|
||||||
|
|
||||||
logger.warn(`🚫 Claude Console account blocked: ${accountId} - ${reason}`)
|
logger.warn(`🚫 Claude Console account blocked: ${accountId} - ${reason}`)
|
||||||
|
|
||||||
|
// 发送Webhook通知
|
||||||
|
if (accountData && Object.keys(accountData).length > 0) {
|
||||||
|
try {
|
||||||
|
const webhookNotifier = require('../utils/webhookNotifier')
|
||||||
|
await webhookNotifier.sendAccountAnomalyNotification({
|
||||||
|
accountId,
|
||||||
|
accountName: accountData.name || 'Unknown Account',
|
||||||
|
platform: 'claude-console',
|
||||||
|
status: 'blocked',
|
||||||
|
errorCode: 'CLAUDE_CONSOLE_BLOCKED',
|
||||||
|
reason
|
||||||
|
})
|
||||||
|
} catch (webhookError) {
|
||||||
|
logger.error('Failed to send webhook notification:', webhookError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { success: true }
|
return { success: true }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`❌ Failed to block Claude Console account: ${accountId}`, error)
|
logger.error(`❌ Failed to block Claude Console account: ${accountId}`, error)
|
||||||
|
|||||||
@@ -764,6 +764,21 @@ async function refreshAccountToken(accountId) {
|
|||||||
status: 'error',
|
status: 'error',
|
||||||
errorMessage: error.message
|
errorMessage: error.message
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 发送Webhook通知
|
||||||
|
try {
|
||||||
|
const webhookNotifier = require('../utils/webhookNotifier')
|
||||||
|
await webhookNotifier.sendAccountAnomalyNotification({
|
||||||
|
accountId,
|
||||||
|
accountName: account.name,
|
||||||
|
platform: 'gemini',
|
||||||
|
status: 'error',
|
||||||
|
errorCode: 'GEMINI_ERROR',
|
||||||
|
reason: `Token refresh failed: ${error.message}`
|
||||||
|
})
|
||||||
|
} catch (webhookError) {
|
||||||
|
logger.error('Failed to send webhook notification:', webhookError)
|
||||||
|
}
|
||||||
} catch (updateError) {
|
} catch (updateError) {
|
||||||
logger.error('Failed to update account status after refresh error:', updateError)
|
logger.error('Failed to update account status after refresh error:', updateError)
|
||||||
}
|
}
|
||||||
|
|||||||
144
src/utils/webhookNotifier.js
Normal file
144
src/utils/webhookNotifier.js
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
const axios = require('axios')
|
||||||
|
const logger = require('./logger')
|
||||||
|
const config = require('../../config/config')
|
||||||
|
|
||||||
|
class WebhookNotifier {
|
||||||
|
constructor() {
|
||||||
|
this.webhookUrls = config.webhook?.urls || []
|
||||||
|
this.timeout = config.webhook?.timeout || 10000
|
||||||
|
this.retries = config.webhook?.retries || 3
|
||||||
|
this.enabled = config.webhook?.enabled !== false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送账号异常通知
|
||||||
|
* @param {Object} notification - 通知内容
|
||||||
|
* @param {string} notification.accountId - 账号ID
|
||||||
|
* @param {string} notification.accountName - 账号名称
|
||||||
|
* @param {string} notification.platform - 平台类型 (claude-oauth, claude-console, gemini)
|
||||||
|
* @param {string} notification.status - 异常状态 (unauthorized, blocked, error)
|
||||||
|
* @param {string} notification.errorCode - 异常代码
|
||||||
|
* @param {string} notification.reason - 异常原因
|
||||||
|
* @param {string} notification.timestamp - 时间戳
|
||||||
|
*/
|
||||||
|
async sendAccountAnomalyNotification(notification) {
|
||||||
|
if (!this.enabled || this.webhookUrls.length === 0) {
|
||||||
|
logger.debug('Webhook notification disabled or no URLs configured')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
type: 'account_anomaly',
|
||||||
|
data: {
|
||||||
|
accountId: notification.accountId,
|
||||||
|
accountName: notification.accountName,
|
||||||
|
platform: notification.platform,
|
||||||
|
status: notification.status,
|
||||||
|
errorCode: notification.errorCode,
|
||||||
|
reason: notification.reason,
|
||||||
|
timestamp: notification.timestamp || new Date().toISOString(),
|
||||||
|
service: 'claude-relay-service'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`📢 Sending account anomaly webhook notification: ${notification.accountName} (${notification.accountId}) - ${notification.status}`
|
||||||
|
)
|
||||||
|
|
||||||
|
const promises = this.webhookUrls.map((url) => this._sendWebhook(url, payload))
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.allSettled(promises)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to send webhook notifications:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送Webhook请求
|
||||||
|
* @param {string} url - Webhook URL
|
||||||
|
* @param {Object} payload - 请求载荷
|
||||||
|
*/
|
||||||
|
async _sendWebhook(url, payload, attempt = 1) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(url, payload, {
|
||||||
|
timeout: this.timeout,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'User-Agent': 'claude-relay-service/webhook-notifier'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.status >= 200 && response.status < 300) {
|
||||||
|
logger.info(`✅ Webhook sent successfully to ${url}`)
|
||||||
|
} else {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(
|
||||||
|
`❌ Failed to send webhook to ${url} (attempt ${attempt}/${this.retries}):`,
|
||||||
|
error.message
|
||||||
|
)
|
||||||
|
|
||||||
|
// 重试机制
|
||||||
|
if (attempt < this.retries) {
|
||||||
|
const delay = Math.pow(2, attempt - 1) * 1000 // 指数退避
|
||||||
|
logger.info(`🔄 Retrying webhook to ${url} in ${delay}ms...`)
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delay))
|
||||||
|
return this._sendWebhook(url, payload, attempt + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(`💥 All ${this.retries} webhook attempts failed for ${url}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试Webhook连通性
|
||||||
|
* @param {string} url - Webhook URL
|
||||||
|
*/
|
||||||
|
async testWebhook(url) {
|
||||||
|
const testPayload = {
|
||||||
|
type: 'test',
|
||||||
|
data: {
|
||||||
|
message: 'Claude Relay Service webhook test',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
service: 'claude-relay-service'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this._sendWebhook(url, testPayload)
|
||||||
|
return { success: true }
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取错误代码映射
|
||||||
|
* @param {string} platform - 平台类型
|
||||||
|
* @param {string} status - 状态
|
||||||
|
* @param {string} _reason - 原因 (未使用)
|
||||||
|
*/
|
||||||
|
_getErrorCode(platform, status, _reason) {
|
||||||
|
const errorCodes = {
|
||||||
|
'claude-oauth': {
|
||||||
|
unauthorized: 'CLAUDE_OAUTH_UNAUTHORIZED',
|
||||||
|
error: 'CLAUDE_OAUTH_ERROR'
|
||||||
|
},
|
||||||
|
'claude-console': {
|
||||||
|
blocked: 'CLAUDE_CONSOLE_BLOCKED',
|
||||||
|
error: 'CLAUDE_CONSOLE_ERROR'
|
||||||
|
},
|
||||||
|
gemini: {
|
||||||
|
error: 'GEMINI_ERROR',
|
||||||
|
unauthorized: 'GEMINI_UNAUTHORIZED'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errorCodes[platform]?.[status] || 'UNKNOWN_ERROR'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = new WebhookNotifier()
|
||||||
Reference in New Issue
Block a user