From e01e53910859b456790f01d4b96180224f5e3e80 Mon Sep 17 00:00:00 2001 From: weidian Date: Wed, 13 Aug 2025 17:52:46 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E5=BC=82=E5=B8=B8=E7=8A=B6=E6=80=81=20Webhook=20?= =?UTF-8?q?=E9=80=9A=E7=9F=A5=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 功能概述 - 新增账号禁用/异常状态的 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 --- .env.example | 8 +- config/config.example.js | 8 ++ src/app.js | 2 + src/routes/webhook.js | 120 ++++++++++++++++ src/services/claudeAccountService.js | 30 ++++ src/services/claudeConsoleAccountService.js | 21 +++ src/services/geminiAccountService.js | 15 ++ src/utils/webhookNotifier.js | 144 ++++++++++++++++++++ 8 files changed, 347 insertions(+), 1 deletion(-) create mode 100644 src/routes/webhook.js create mode 100644 src/utils/webhookNotifier.js diff --git a/.env.example b/.env.example index a796b496..e2949998 100644 --- a/.env.example +++ b/.env.example @@ -58,4 +58,10 @@ ENABLE_CORS=true TRUST_PROXY=true # 🔒 客户端限制(可选) -# ALLOW_CUSTOM_CLIENTS=false \ No newline at end of file +# 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 \ No newline at end of file diff --git a/config/config.example.js b/config/config.example.js index b3342c08..3bfb0fbb 100644 --- a/config/config.example.js +++ b/config/config.example.js @@ -120,6 +120,14 @@ const config = { 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: { debug: process.env.DEBUG === 'true', diff --git a/src/app.js b/src/app.js index 9af1a91e..0a0caba4 100644 --- a/src/app.js +++ b/src/app.js @@ -20,6 +20,7 @@ const geminiRoutes = require('./routes/geminiRoutes') const openaiGeminiRoutes = require('./routes/openaiGeminiRoutes') const openaiClaudeRoutes = require('./routes/openaiClaudeRoutes') const openaiRoutes = require('./routes/openaiRoutes') +const webhookRoutes = require('./routes/webhook') // Import middleware const { @@ -236,6 +237,7 @@ class Application { this.app.use('/openai/gemini', openaiGeminiRoutes) this.app.use('/openai/claude', openaiClaudeRoutes) this.app.use('/openai', openaiRoutes) + this.app.use('/admin/webhook', webhookRoutes) // 🏠 根路径重定向到新版管理界面 this.app.get('/', (req, res) => { diff --git a/src/routes/webhook.js b/src/routes/webhook.js new file mode 100644 index 00000000..df670eb4 --- /dev/null +++ b/src/routes/webhook.js @@ -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: url + }) + } else { + logger.warn(`❌ Webhook test failed for: ${url} - ${result.error}`) + res.status(400).json({ + success: false, + message: 'Webhook test failed', + url: 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 diff --git a/src/services/claudeAccountService.js b/src/services/claudeAccountService.js index 2029957b..da512f97 100644 --- a/src/services/claudeAccountService.js +++ b/src/services/claudeAccountService.js @@ -228,6 +228,21 @@ class ClaudeAccountService { accountData.status = 'error' accountData.errorMessage = error.message 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) @@ -1223,6 +1238,21 @@ class ClaudeAccountService { `⚠️ 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 } } catch (error) { logger.error(`❌ Failed to mark account ${accountId} as unauthorized:`, error) diff --git a/src/services/claudeConsoleAccountService.js b/src/services/claudeConsoleAccountService.js index e7385bdd..5b23d6d1 100644 --- a/src/services/claudeConsoleAccountService.js +++ b/src/services/claudeConsoleAccountService.js @@ -395,6 +395,9 @@ class ClaudeConsoleAccountService { try { const client = redis.getClientSafe() + // 获取账户信息用于webhook通知 + const accountData = await client.hgetall(`${this.ACCOUNT_KEY_PREFIX}${accountId}`) + const updates = { status: 'blocked', errorMessage: reason, @@ -404,6 +407,24 @@ class ClaudeConsoleAccountService { await client.hset(`${this.ACCOUNT_KEY_PREFIX}${accountId}`, updates) 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: reason + }) + } catch (webhookError) { + logger.error('Failed to send webhook notification:', webhookError) + } + } + return { success: true } } catch (error) { logger.error(`❌ Failed to block Claude Console account: ${accountId}`, error) diff --git a/src/services/geminiAccountService.js b/src/services/geminiAccountService.js index 498ab4fe..179116b3 100644 --- a/src/services/geminiAccountService.js +++ b/src/services/geminiAccountService.js @@ -764,6 +764,21 @@ async function refreshAccountToken(accountId) { status: 'error', 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) { logger.error('Failed to update account status after refresh error:', updateError) } diff --git a/src/utils/webhookNotifier.js b/src/utils/webhookNotifier.js new file mode 100644 index 00000000..648debca --- /dev/null +++ b/src/utils/webhookNotifier.js @@ -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() From d2bcb8ef5e580e0dafa03d75d11f2008b6bb5662 Mon Sep 17 00:00:00 2001 From: iRubbish Date: Thu, 14 Aug 2025 09:54:15 +0800 Subject: [PATCH 2/4] =?UTF-8?q?style:=20=E4=BD=BF=E7=94=A8=20prettier=20?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 格式化 config/config.example.js 文件 - 确保所有代码符合项目代码风格规范 - 解决自动构建中的格式检查问题 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- config/config.example.js | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/config/config.example.js b/config/config.example.js index 3bfb0fbb..e3584e48 100644 --- a/config/config.example.js +++ b/config/config.example.js @@ -1,5 +1,5 @@ -const path = require('path'); -require('dotenv').config(); +const path = require('path') +require('dotenv').config() const config = { // 🌐 服务器配置 @@ -29,14 +29,16 @@ const config = { retryDelayOnFailover: 100, maxRetriesPerRequest: 3, lazyConnect: true, - enableTLS: process.env.REDIS_ENABLE_TLS === 'true', + enableTLS: process.env.REDIS_ENABLE_TLS === 'true' }, // 🎯 Claude API配置 claude: { apiUrl: process.env.CLAUDE_API_URL || 'https://api.anthropic.com/v1/messages', 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配置 @@ -45,7 +47,8 @@ const config = { defaultRegion: process.env.AWS_REGION || 'us-east-1', smallFastModelRegion: process.env.ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION, 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, maxThinkingTokens: parseInt(process.env.MAX_THINKING_TOKENS) || 1024, enablePromptCaching: process.env.DISABLE_PROMPT_CACHING !== '1' @@ -82,7 +85,9 @@ const config = { // 🎨 Web界面配置 web: { 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', enableCors: process.env.ENABLE_CORS === 'true', sessionSecret: process.env.WEB_SESSION_SECRET || 'CHANGE-THIS-SESSION-SECRET' @@ -123,7 +128,9 @@ const config = { // 📢 Webhook通知配置 webhook: { enabled: process.env.WEBHOOK_ENABLED !== 'false', // 默认启用 - urls: process.env.WEBHOOK_URLS ? process.env.WEBHOOK_URLS.split(',').map(url => url.trim()) : [], + 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次 }, @@ -133,6 +140,6 @@ const config = { debug: process.env.DEBUG === 'true', hotReload: process.env.HOT_RELOAD === 'true' } -}; +} -module.exports = config; \ No newline at end of file +module.exports = config From 2b77fdc06c570c12eef3dcd7c99ccce1f580b377 Mon Sep 17 00:00:00 2001 From: iRubbish Date: Thu, 14 Aug 2025 14:14:01 +0800 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20ESLint=20?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E8=B4=A8=E9=87=8F=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复正则表达式中不必要的转义字符 - 使用对象解构优化代码风格 - 修复未使用变量的命名规范 - 确保所有修改文件通过 ESLint 检查 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- config/config.example.js | 4 ++-- src/app.js | 4 ++-- src/routes/webhook.js | 4 ++-- src/services/claudeConsoleAccountService.js | 2 +- src/utils/webhookNotifier.js | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/config/config.example.js b/config/config.example.js index e3584e48..1ab101cc 100644 --- a/config/config.example.js +++ b/config/config.example.js @@ -103,7 +103,7 @@ const config = { description: 'Official Claude Code CLI', // 匹配 Claude CLI 的 User-Agent // 示例: claude-cli/1.0.58 (external, cli) - userAgentPattern: /^claude-cli\/[\d\.]+\s+\(/i + userAgentPattern: /^claude-cli\/[\d.]+\s+\(/i }, { id: 'gemini_cli', @@ -111,7 +111,7 @@ const config = { description: 'Gemini Command Line Interface', // 匹配 GeminiCLI 的 User-Agent // 示例: GeminiCLI/v18.20.8 (darwin; arm64) - userAgentPattern: /^GeminiCLI\/v?[\d\.]+\s+\(/i + userAgentPattern: /^GeminiCLI\/v?[\d.]+\s+\(/i } // 添加自定义客户端示例: // { diff --git a/src/app.js b/src/app.js index 0a0caba4..e7dfd7e7 100644 --- a/src/app.js +++ b/src/app.js @@ -271,8 +271,8 @@ class Application { } if (!version) { try { - const packageJson = require('../package.json') - version = packageJson.version + const { version: pkgVersion } = require('../package.json') + version = pkgVersion } catch (error) { version = '1.0.0' } diff --git a/src/routes/webhook.js b/src/routes/webhook.js index df670eb4..5c3adcef 100644 --- a/src/routes/webhook.js +++ b/src/routes/webhook.js @@ -35,14 +35,14 @@ router.post('/test', authenticateAdmin, async (req, res) => { res.json({ success: true, message: 'Webhook test successful', - url: url + url }) } else { logger.warn(`❌ Webhook test failed for: ${url} - ${result.error}`) res.status(400).json({ success: false, message: 'Webhook test failed', - url: url, + url, error: result.error }) } diff --git a/src/services/claudeConsoleAccountService.js b/src/services/claudeConsoleAccountService.js index 5b23d6d1..91df2600 100644 --- a/src/services/claudeConsoleAccountService.js +++ b/src/services/claudeConsoleAccountService.js @@ -418,7 +418,7 @@ class ClaudeConsoleAccountService { platform: 'claude-console', status: 'blocked', errorCode: 'CLAUDE_CONSOLE_BLOCKED', - reason: reason + reason }) } catch (webhookError) { logger.error('Failed to send webhook notification:', webhookError) diff --git a/src/utils/webhookNotifier.js b/src/utils/webhookNotifier.js index 648debca..1015f581 100644 --- a/src/utils/webhookNotifier.js +++ b/src/utils/webhookNotifier.js @@ -119,9 +119,9 @@ class WebhookNotifier { * 获取错误代码映射 * @param {string} platform - 平台类型 * @param {string} status - 状态 - * @param {string} reason - 原因 + * @param {string} _reason - 原因 (未使用) */ - _getErrorCode(platform, status, reason) { + _getErrorCode(platform, status, _reason) { const errorCodes = { 'claude-oauth': { unauthorized: 'CLAUDE_OAUTH_UNAUTHORIZED', From b33c6594917afddb0ccc1cc55ffba7f000d81c27 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 14 Aug 2025 06:31:28 +0000 Subject: [PATCH 4/4] chore: sync VERSION file with release v1.1.108 [skip ci] --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 8c43a3a9..c69d5ced 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.107 +1.1.108