From d84a0ed95614fc954fdf7b7f6725ede4e32d76f3 Mon Sep 17 00:00:00 2001 From: jeffusion Date: Wed, 4 Mar 2026 13:33:43 +0800 Subject: [PATCH] fix: make FEISHU_WEBHOOK_URL optional to prevent startup crash Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) --- src/config/__tests__/config-manager.test.ts | 13 ++-- src/config/config-manager.ts | 11 ++-- src/controllers/review.ts | 68 +++++++++++---------- src/services/feishu.ts | 19 ++++-- 4 files changed, 65 insertions(+), 46 deletions(-) diff --git a/src/config/__tests__/config-manager.test.ts b/src/config/__tests__/config-manager.test.ts index 5a8ae97..c9f1eca 100644 --- a/src/config/__tests__/config-manager.test.ts +++ b/src/config/__tests__/config-manager.test.ts @@ -89,8 +89,7 @@ describe('ConfigManager', () => { // Per-test temp overrides file process.env.CONFIG_OVERRIDES_PATH = tmpPath; - // FEISHU_WEBHOOK_URL has no Zod default → must be a valid URL for schema to pass. - process.env.FEISHU_WEBHOOK_URL = 'https://hooks.example.com/test'; + // FEISHU_WEBHOOK_URL is now optional → no need to set it for schema to pass. }); afterEach(async () => { @@ -192,20 +191,20 @@ describe('ConfigManager', () => { // ─── 5. Dev fallback ───────────────────────────────────────────────── describe('dev fallback', () => { - test('FEISHU_WEBHOOK_URL missing + NODE_ENV=development → feishu.webhookUrl ""', async () => { - process.env.FEISHU_WEBHOOK_URL = ''; // invalid → safeParse fails + test('FEISHU_WEBHOOK_URL missing + NODE_ENV=development → feishu.webhookUrl undefined', async () => { + process.env.FEISHU_WEBHOOK_URL = ''; // empty → preprocess converts to undefined process.env.NODE_ENV = 'development'; const cm = await importFresh(); const cfg: AppConfig = cm.getCurrent(); - expect(cfg.feishu.webhookUrl).toBe(''); + expect(cfg.feishu.webhookUrl).toBeUndefined(); }); - test('FEISHU_WEBHOOK_URL missing + NODE_ENV unset → feishu.webhookUrl ""', async () => { + test('FEISHU_WEBHOOK_URL missing + NODE_ENV unset → feishu.webhookUrl undefined', async () => { process.env.FEISHU_WEBHOOK_URL = ''; process.env.NODE_ENV = ''; // falsy → same branch as undefined const cm = await importFresh(); const cfg: AppConfig = cm.getCurrent(); - expect(cfg.feishu.webhookUrl).toBe(''); + expect(cfg.feishu.webhookUrl).toBeUndefined(); }); }); }); diff --git a/src/config/config-manager.ts b/src/config/config-manager.ts index ea5c89e..09b005e 100644 --- a/src/config/config-manager.ts +++ b/src/config/config-manager.ts @@ -48,7 +48,10 @@ const envSchema = z.object({ CUSTOM_LINE_COMMENT_PROMPT: z.string().optional(), // Feishu - FEISHU_WEBHOOK_URL: z.string().url(), + FEISHU_WEBHOOK_URL: z.preprocess( + (val) => (typeof val === 'string' && val.trim() === '' ? undefined : val), + z.string().url().optional() + ), FEISHU_WEBHOOK_SECRET: z.string().optional(), // App @@ -117,7 +120,7 @@ export interface AppConfig { customLineCommentPrompt: string | undefined; }; feishu: { - webhookUrl: string; + webhookUrl: string | undefined; webhookSecret: string | undefined; }; app: { @@ -169,8 +172,8 @@ const DEV_FALLBACK_CONFIG: AppConfig = { customLineCommentPrompt: undefined, }, feishu: { - webhookUrl: '', - webhookSecret: '', + webhookUrl: undefined, + webhookSecret: undefined, }, app: { port: 5174, diff --git a/src/controllers/review.ts b/src/controllers/review.ts index 8a1bb34..aa256a3 100644 --- a/src/controllers/review.ts +++ b/src/controllers/review.ts @@ -119,40 +119,42 @@ async function handlePullRequestEvent(c: Context, body: any): Promise logger.info('收到PR事件', { owner, repo: repoName, prNumber, action: body.action }); - // 处理PR审阅者通知 - try { - // 获取PR的审阅者列表 - const reviewerUsernames = map( - pullRequest.requested_reviewers, - (reviewer) => reviewer.full_name || reviewer.login - ); + // 处理PR审阅者通知(仅在飞书启用时) + if (feishuService.isEnabled()) { + try { + // 获取PR的审阅者列表 + const reviewerUsernames = map( + pullRequest.requested_reviewers, + (reviewer) => reviewer.full_name || reviewer.login + ); - // 记录审阅者信息 - if (reviewerUsernames.length > 0) { - logger.info('PR有指定审阅者', { - prNumber, - reviewers: reviewerUsernames.join(','), - }); - } - - // 处理PR创建事件,如果有审阅者,则通知 - if (body.action === 'opened' && reviewerUsernames.length > 0) { - await feishuService.sendPrCreatedNotification(prTitle, prUrl, reviewerUsernames); - } - - // 处理审阅者指派事件 - if (body.action === 'review_requested' && body.requested_reviewer) { - const newReviewerUsername = - body.requested_reviewer.full_name || body.requested_reviewer.login; - if (newReviewerUsername) { - await feishuService.sendPrReviewerAssignedNotification(prTitle, prUrl, [ - newReviewerUsername, - ]); + // 记录审阅者信息 + if (reviewerUsernames.length > 0) { + logger.info('PR有指定审阅者', { + prNumber, + reviewers: reviewerUsernames.join(','), + }); } + + // 处理PR创建事件,如果有审阅者,则通知 + if (body.action === 'opened' && reviewerUsernames.length > 0) { + await feishuService.sendPrCreatedNotification(prTitle, prUrl, reviewerUsernames); + } + + // 处理审阅者指派事件 + if (body.action === 'review_requested' && body.requested_reviewer) { + const newReviewerUsername = + body.requested_reviewer.full_name || body.requested_reviewer.login; + if (newReviewerUsername) { + await feishuService.sendPrReviewerAssignedNotification(prTitle, prUrl, [ + newReviewerUsername, + ]); + } + } + } catch (error) { + logger.error('处理PR审阅者通知失败:', error); + // 继续执行代码审查流程,不因通知失败而中断 } - } catch (error) { - logger.error('处理PR审阅者通知失败:', error); - // 继续执行代码审查流程,不因通知失败而中断 } if (config.review.engine === 'agent') { @@ -340,6 +342,10 @@ async function handleIssueEvent(c: Context, body: any): Promise { assigneeUsernames: assigneeUsernames.join(','), }); + if (!feishuService.isEnabled()) { + return c.json({ status: 'ignored', message: '飞书通知未启用,工单事件已跳过' }, 200); + } + try { // 处理工单创建事件 if (action === 'opened' && assigneeUsernames.length > 0) { diff --git a/src/services/feishu.ts b/src/services/feishu.ts index e5a2958..246aac1 100644 --- a/src/services/feishu.ts +++ b/src/services/feishu.ts @@ -3,7 +3,7 @@ import config from '../config'; import { logger } from '../utils/logger'; export class FeishuService { - private webhookUrl: string; + private webhookUrl?: string; private webhookSecret?: string; constructor() { @@ -11,15 +11,21 @@ export class FeishuService { this.webhookSecret = config.feishu.webhookSecret; if (!this.webhookUrl) { - logger.error('飞书webhook URL未配置'); - throw new Error('飞书webhook URL未配置'); + logger.info('飞书webhook URL未配置,飞书通知已禁用'); } - if (!this.webhookSecret) { + if (this.webhookUrl && !this.webhookSecret) { logger.warn('飞书webhook密钥未配置,签名验证将被禁用'); } } + /** + * 判断飞书通知是否已启用 + */ + isEnabled(): boolean { + return !!this.webhookUrl; + } + /** * 生成飞书消息签名 * @param timestamp 时间戳 @@ -37,6 +43,11 @@ export class FeishuService { * @param usernames 需要@的用户名列表 */ async sendMessage(content: string, usernames: string[] = []): Promise { + if (!this.webhookUrl) { + logger.debug('飞书通知已跳过: webhook URL未配置'); + return; + } + try { const timestamp = Math.floor(Date.now() / 1000).toString(); const message: any = {