fix: make FEISHU_WEBHOOK_URL optional to prevent startup crash

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)
This commit is contained in:
jeffusion
2026-03-04 13:33:43 +08:00
committed by 路遥知码力
parent dd147a24b4
commit d84a0ed956
4 changed files with 65 additions and 46 deletions

View File

@@ -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();
});
});
});

View File

@@ -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,

View File

@@ -119,40 +119,42 @@ async function handlePullRequestEvent(c: Context, body: any): Promise<Response>
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<Response> {
assigneeUsernames: assigneeUsernames.join(','),
});
if (!feishuService.isEnabled()) {
return c.json({ status: 'ignored', message: '飞书通知未启用,工单事件已跳过' }, 200);
}
try {
// 处理工单创建事件
if (action === 'opened' && assigneeUsernames.length > 0) {

View File

@@ -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<void> {
if (!this.webhookUrl) {
logger.debug('飞书通知已跳过: webhook URL未配置');
return;
}
try {
const timestamp = Math.floor(Date.now() / 1000).toString();
const message: any = {