mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
feat: 添加账号异常状态 Webhook 通知功能
## 功能概述
- 新增账号禁用/异常状态的 Webhook 实时通知机制
- 支持 Claude OAuth、Claude Console、Gemini 三种平台的账号监控
- 提供完整的 Webhook 管理 API 和配置选项
## 主要变更
### 新增文件
- `src/utils/webhookNotifier.js`: Webhook 通知核心服务
- `src/routes/webhook.js`: Webhook 管理 API 路由
### 功能集成
- Claude OAuth 账号:unauthorized 状态 + token 刷新错误通知
- Claude Console 账号:blocked 状态通知
- Gemini 账号:token 刷新错误通知
### 配置支持
- 新增环境变量:WEBHOOK_ENABLED, WEBHOOK_URLS, WEBHOOK_TIMEOUT, WEBHOOK_RETRIES
- 支持多个 Webhook URL 并发通知
- 自动重试机制(指数退避)+ 超时保护
### 管理端点
- POST /admin/webhook/test: 测试连通性
- POST /admin/webhook/test-notification: 发送测试通知
- GET /admin/webhook/config: 查看配置信息
## 通知格式
```json
{
"type": "account_anomaly",
"data": {
"accountId": "uuid",
"accountName": "账号名称",
"platform": "claude-oauth|claude-console|gemini",
"status": "unauthorized|blocked|error",
"errorCode": "CLAUDE_OAUTH_UNAUTHORIZED",
"reason": "具体异常原因",
"timestamp": "2025-01-13T10:30:00.000Z"
}
}
```
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
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