mirror of
https://github.com/jeffusion/gitea-ai-assistant.git
synced 2026-03-27 10:05:50 +00:00
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:
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user