From fcc8387c24ff16d873a960ad99c20726f4d4f8b1 Mon Sep 17 00:00:00 2001 From: Hg Date: Tue, 26 Aug 2025 17:40:02 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0Bark=E4=BD=9C?= =?UTF-8?q?=E4=B8=BAwebhook=E6=B8=A0=E9=81=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/webhookConfigService.js | 99 ++++++++++++++++++++++++- src/services/webhookService.js | 105 ++++++++++++++++++++++++++- 2 files changed, 200 insertions(+), 4 deletions(-) diff --git a/src/services/webhookConfigService.js b/src/services/webhookConfigService.js index 18a460f6..59955cd1 100644 --- a/src/services/webhookConfigService.js +++ b/src/services/webhookConfigService.js @@ -56,15 +56,26 @@ class WebhookConfigService { // 验证平台配置 if (config.platforms) { - const validPlatforms = ['wechat_work', 'dingtalk', 'feishu', 'slack', 'discord', 'custom'] + const validPlatforms = [ + 'wechat_work', + 'dingtalk', + 'feishu', + 'slack', + 'discord', + 'custom', + 'bark' + ] for (const platform of config.platforms) { if (!validPlatforms.includes(platform.type)) { throw new Error(`不支持的平台类型: ${platform.type}`) } - if (!platform.url || !this.isValidUrl(platform.url)) { - throw new Error(`无效的webhook URL: ${platform.url}`) + // Bark平台使用deviceKey而不是url + if (platform.type !== 'bark') { + if (!platform.url || !this.isValidUrl(platform.url)) { + throw new Error(`无效的webhook URL: ${platform.url}`) + } } // 验证平台特定的配置 @@ -108,6 +119,88 @@ class WebhookConfigService { case 'custom': // 自定义webhook,用户自行负责格式 break + case 'bark': + // 验证设备密钥 + if (!platform.deviceKey) { + throw new Error('Bark平台必须提供设备密钥') + } + + // 验证设备密钥格式(通常是22-24位字符) + if (platform.deviceKey.length < 20 || platform.deviceKey.length > 30) { + logger.warn('⚠️ Bark设备密钥长度可能不正确,请检查是否完整复制') + } + + // 验证服务器URL(如果提供) + if (platform.serverUrl) { + if (!this.isValidUrl(platform.serverUrl)) { + throw new Error('Bark服务器URL格式无效') + } + if (!platform.serverUrl.includes('/push')) { + logger.warn('⚠️ Bark服务器URL应该以/push结尾') + } + } + + // 验证声音参数(如果提供) + if (platform.sound) { + const validSounds = [ + 'default', + 'alarm', + 'anticipate', + 'bell', + 'birdsong', + 'bloom', + 'calypso', + 'chime', + 'choo', + 'descent', + 'electronic', + 'fanfare', + 'glass', + 'gotosleep', + 'healthnotification', + 'horn', + 'ladder', + 'mailsent', + 'minuet', + 'multiwayinvitation', + 'newmail', + 'newsflash', + 'noir', + 'paymentsuccess', + 'shake', + 'sherwoodforest', + 'silence', + 'spell', + 'suspense', + 'telegraph', + 'tiptoes', + 'typewriters', + 'update', + 'alert' + ] + if (!validSounds.includes(platform.sound)) { + logger.warn(`⚠️ 未知的Bark声音: ${platform.sound}`) + } + } + + // 验证级别参数 + if (platform.level) { + const validLevels = ['active', 'timeSensitive', 'passive', 'critical'] + if (!validLevels.includes(platform.level)) { + throw new Error(`无效的Bark通知级别: ${platform.level}`) + } + } + + // 验证图标URL(如果提供) + if (platform.icon && !this.isValidUrl(platform.icon)) { + logger.warn('⚠️ Bark图标URL格式可能不正确') + } + + // 验证点击跳转URL(如果提供) + if (platform.clickUrl && !this.isValidUrl(platform.clickUrl)) { + logger.warn('⚠️ Bark点击跳转URL格式可能不正确') + } + break } } diff --git a/src/services/webhookService.js b/src/services/webhookService.js index ad2778ff..cffa3b03 100644 --- a/src/services/webhookService.js +++ b/src/services/webhookService.js @@ -11,7 +11,8 @@ class WebhookService { feishu: this.sendToFeishu.bind(this), slack: this.sendToSlack.bind(this), discord: this.sendToDiscord.bind(this), - custom: this.sendToCustom.bind(this) + custom: this.sendToCustom.bind(this), + bark: this.sendToBark.bind(this) } } @@ -212,6 +213,33 @@ class WebhookService { await this.sendHttpRequest(platform.url, payload, platform.timeout || 10000) } + /** + * Bark webhook + */ + async sendToBark(platform, type, data) { + const payload = { + device_key: platform.deviceKey, + title: this.getNotificationTitle(type), + body: this.formatMessageForBark(type, data), + level: platform.level || this.getBarkLevel(type), + sound: platform.sound || this.getBarkSound(type), + group: platform.group || 'claude-relay', + badge: 1 + } + + // 添加可选参数 + if (platform.icon) { + payload.icon = platform.icon + } + + if (platform.clickUrl) { + payload.url = platform.clickUrl + } + + const url = platform.serverUrl || 'https://api.day.app/push' + await this.sendHttpRequest(url, payload, platform.timeout || 10000) + } + /** * 发送HTTP请求 */ @@ -351,6 +379,81 @@ class WebhookService { return titles[type] || '📢 系统通知' } + /** + * 获取Bark通知级别 + */ + getBarkLevel(type) { + const levels = { + accountAnomaly: 'timeSensitive', + quotaWarning: 'active', + systemError: 'critical', + securityAlert: 'critical', + test: 'passive' + } + + return levels[type] || 'active' + } + + /** + * 获取Bark声音 + */ + getBarkSound(type) { + const sounds = { + accountAnomaly: 'alarm', + quotaWarning: 'bell', + systemError: 'alert', + securityAlert: 'alarm', + test: 'default' + } + + return sounds[type] || 'default' + } + + /** + * 格式化Bark消息 + */ + formatMessageForBark(type, data) { + const lines = [] + + if (data.accountName) { + lines.push(`账号: ${data.accountName}`) + } + + if (data.platform) { + lines.push(`平台: ${data.platform}`) + } + + if (data.status) { + lines.push(`状态: ${data.status}`) + } + + if (data.errorCode) { + lines.push(`错误: ${data.errorCode}`) + } + + if (data.reason) { + lines.push(`原因: ${data.reason}`) + } + + if (data.message) { + lines.push(`消息: ${data.message}`) + } + + if (data.quota) { + lines.push(`剩余配额: ${data.quota.remaining}/${data.quota.total}`) + } + + if (data.usage) { + lines.push(`使用率: ${data.usage}%`) + } + + // 添加服务标识和时间戳 + lines.push(`\n服务: Claude Relay Service`) + lines.push(`时间: ${new Date().toLocaleString('zh-CN')}`) + + return lines.join('\n') + } + /** * 格式化通知详情 */ From a7009e686408760e08b012fa397031b437cebbca Mon Sep 17 00:00:00 2001 From: zjpyb Date: Thu, 28 Aug 2025 00:03:12 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20Gemini=E5=8E=9F=E7=94=9F=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E6=B2=A1=E8=8E=B7=E5=8F=96=E5=88=B0modelName=20#295?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/routes/geminiRoutes.js | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/routes/geminiRoutes.js b/src/routes/geminiRoutes.js index c5d706a3..28b95a8f 100644 --- a/src/routes/geminiRoutes.js +++ b/src/routes/geminiRoutes.js @@ -50,7 +50,7 @@ router.post('/messages', authenticateApiKey, async (req, res) => { // 提取请求参数 const { messages, - model = 'gemini-2.0-flash-exp', + model = 'gemini-2.5-flash', temperature = 0.7, max_tokens = 4096, stream = false @@ -217,7 +217,7 @@ router.get('/models', authenticateApiKey, async (req, res) => { object: 'list', data: [ { - id: 'gemini-2.0-flash-exp', + id: 'gemini-2.5-flash', object: 'model', created: Date.now() / 1000, owned_by: 'google' @@ -311,8 +311,8 @@ async function handleLoadCodeAssist(req, res) { try { const sessionHash = sessionHelper.generateSessionHash(req.body) - // 使用统一调度选择账号(传递请求的模型) - const requestedModel = req.body.model + // 从路径参数或请求体中获取模型名 + const requestedModel = req.body.model || req.params.modelName || 'gemini-2.5-flash' const { accountId } = await unifiedGeminiScheduler.selectAccountForApiKey( req.apiKey, sessionHash, @@ -368,8 +368,8 @@ async function handleOnboardUser(req, res) { const { tierId, cloudaicompanionProject, metadata } = req.body const sessionHash = sessionHelper.generateSessionHash(req.body) - // 使用统一调度选择账号(传递请求的模型) - const requestedModel = req.body.model + // 从路径参数或请求体中获取模型名 + const requestedModel = req.body.model || req.params.modelName || 'gemini-2.5-flash' const { accountId } = await unifiedGeminiScheduler.selectAccountForApiKey( req.apiKey, sessionHash, @@ -439,7 +439,9 @@ async function handleCountTokens(req, res) { try { // 处理请求体结构,支持直接 contents 或 request.contents const requestData = req.body.request || req.body - const { contents, model = 'gemini-2.0-flash-exp' } = requestData + const { contents } = requestData + // 从路径参数或请求体中获取模型名 + const model = requestData.model || req.params.modelName || 'gemini-2.5-flash' const sessionHash = sessionHelper.generateSessionHash(req.body) // 验证必需参数 @@ -487,7 +489,9 @@ async function handleCountTokens(req, res) { // 共用的 generateContent 处理函数 async function handleGenerateContent(req, res) { try { - const { model, project, user_prompt_id, request: requestData } = req.body + const { project, user_prompt_id, request: requestData } = req.body + // 从路径参数或请求体中获取模型名 + const model = req.body.model || req.params.modelName || 'gemini-2.5-flash' const sessionHash = sessionHelper.generateSessionHash(req.body) // 处理不同格式的请求 @@ -610,7 +614,9 @@ async function handleStreamGenerateContent(req, res) { let abortController = null try { - const { model, project, user_prompt_id, request: requestData } = req.body + const { project, user_prompt_id, request: requestData } = req.body + // 从路径参数或请求体中获取模型名 + const model = req.body.model || req.params.modelName || 'gemini-2.5-flash' const sessionHash = sessionHelper.generateSessionHash(req.body) // 处理不同格式的请求 From fb57cfd2937c476f08b4a2776c69a04e17b7b87a Mon Sep 17 00:00:00 2001 From: zjpyb Date: Thu, 28 Aug 2025 02:38:01 +0800 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DGemini=20v1beta?= =?UTF-8?q?=E6=B5=81=E5=BC=8F=E5=93=8D=E5=BA=94=E4=B8=AD=E6=96=AD=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 优化SSE流式响应处理逻辑,修复客户端接收第一条消息后断开连接的问题 - 统一流处理缓冲区,正确处理不完整的SSE行 - v1beta版本返回response字段内容,v1internal保持原始转发 - 移除调试日志输出,提升生产环境稳定性 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/routes/geminiRoutes.js | 88 +++++++++++++++++++++++++++++--------- 1 file changed, 68 insertions(+), 20 deletions(-) diff --git a/src/routes/geminiRoutes.js b/src/routes/geminiRoutes.js index 28b95a8f..9881371a 100644 --- a/src/routes/geminiRoutes.js +++ b/src/routes/geminiRoutes.js @@ -708,8 +708,28 @@ async function handleStreamGenerateContent(req, res) { res.setHeader('Connection', 'keep-alive') res.setHeader('X-Accel-Buffering', 'no') + // SSE 解析函数 + const parseSSELine = (line) => { + if (!line.startsWith('data: ')) { + return { type: 'other', line, data: null } + } + + const jsonStr = line.substring(6).trim() + + if (!jsonStr || jsonStr === '[DONE]') { + return { type: 'control', line, data: null, jsonStr } + } + + try { + const data = JSON.parse(jsonStr) + return { type: 'data', line, data, jsonStr } + } catch (e) { + return { type: 'invalid', line, data: null, jsonStr, error: e } + } + } + // 处理流式响应并捕获usage数据 - let buffer = '' + let streamBuffer = '' // 统一的流处理缓冲区 let totalUsage = { promptTokenCount: 0, candidatesTokenCount: 0, @@ -721,32 +741,60 @@ async function handleStreamGenerateContent(req, res) { try { const chunkStr = chunk.toString() - // 直接转发数据到客户端 - if (!res.destroyed) { - res.write(chunkStr) + if (!chunkStr.trim()) { + return } - // 同时解析数据以捕获usage信息 - buffer += chunkStr - const lines = buffer.split('\n') - buffer = lines.pop() || '' + // 使用统一缓冲区处理不完整的行 + streamBuffer += chunkStr + const lines = streamBuffer.split('\n') + streamBuffer = lines.pop() || '' // 保留最后一个不完整的行 + + const processedLines = [] for (const line of lines) { - if (line.startsWith('data: ') && line.length > 6) { - try { - const jsonStr = line.slice(6) - if (jsonStr && jsonStr !== '[DONE]') { - const data = JSON.parse(jsonStr) + if (!line.trim()) { + continue // 跳过空行,不添加到处理队列 + } - // 从响应中提取usage数据 - if (data.response?.usageMetadata) { - totalUsage = data.response.usageMetadata - logger.debug('📊 Captured Gemini usage data:', totalUsage) - } + // 解析 SSE 行 + const parsed = parseSSELine(line) + + // 提取 usage 数据(适用于所有版本) + if (parsed.type === 'data' && parsed.data.response?.usageMetadata) { + totalUsage = parsed.data.response.usageMetadata + logger.debug('📊 Captured Gemini usage data:', totalUsage) + } + + // 根据版本处理输出 + if (version === 'v1beta') { + if (parsed.type === 'data') { + if (parsed.data.response) { + // 有 response 字段,只返回 response 的内容 + processedLines.push(`data: ${JSON.stringify(parsed.data.response)}`) + } else { + // 没有 response 字段,返回整个数据对象 + processedLines.push(`data: ${JSON.stringify(parsed.data)}`) } - } catch (e) { - // 忽略解析错误 + } else if (parsed.type === 'control') { + // 控制消息(如 [DONE])保持原样 + processedLines.push(line) } + // 跳过其他类型的行('other', 'invalid') + } + } + + // 发送数据到客户端 + if (version === 'v1beta') { + for (const line of processedLines) { + if (!res.destroyed) { + res.write(`${line}\n\n`) + } + } + } else { + // v1internal 直接转发原始数据 + if (!res.destroyed) { + res.write(chunkStr) } } } catch (error) { From 79c7d1d116f35d4703eff69365d39855136d0fa2 Mon Sep 17 00:00:00 2001 From: zjpyb Date: Thu, 28 Aug 2025 03:04:52 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DGemini=20v1beta?= =?UTF-8?q?=E9=9D=9E=E6=B5=81=E5=BC=8F=E5=93=8D=E5=BA=94=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E7=BB=93=E6=9E=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/routes/geminiRoutes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/geminiRoutes.js b/src/routes/geminiRoutes.js index 9881371a..2a1525f2 100644 --- a/src/routes/geminiRoutes.js +++ b/src/routes/geminiRoutes.js @@ -586,7 +586,7 @@ async function handleGenerateContent(req, res) { } } - res.json(response) + res.json(version === 'v1beta' ? response.response : response) } catch (error) { const version = req.path.includes('v1beta') ? 'v1beta' : 'v1internal' // 打印详细的错误信息 From 71b33747615e9c68539caba31e7f2c0696e7c2bd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 28 Aug 2025 00:41:58 +0000 Subject: [PATCH 5/5] chore: sync VERSION file with release v1.1.121 [skip ci] --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 872610a2..cc868806 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.120 +1.1.121