mirror of
https://github.com/jeffusion/gitea-ai-assistant.git
synced 2026-06-06 07:26:49 +00:00
feat(notification): replace feishu-only flow with pluggable providers
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { Hono } from 'hono';
|
||||
import { configManager } from '../config/config-manager';
|
||||
import { CONFIG_FIELDS, CONFIG_GROUPS, type ConfigFieldMeta } from '../config/config-schema';
|
||||
import { getNotificationManager } from '../services/notification-manager';
|
||||
import type { NotificationProvider } from '../services/notification/types';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
// ── Constants ────────────────────────────────────────────────────────────────
|
||||
@@ -19,6 +21,36 @@ const INTEGER_FIELDS = new Set([
|
||||
|
||||
/** Fast lookup from envKey → field metadata. */
|
||||
const FIELDS_MAP = new Map<string, ConfigFieldMeta>(CONFIG_FIELDS.map((f) => [f.envKey, f]));
|
||||
const TESTABLE_PROVIDERS = new Set<NotificationProvider>(['feishu', 'wecom']);
|
||||
const NOTIFICATION_TEST_HISTORY_LIMIT = 30;
|
||||
|
||||
type NotificationTestRecord = {
|
||||
id: string;
|
||||
provider: string;
|
||||
status: 'success' | 'error';
|
||||
message: string;
|
||||
timestamp: string;
|
||||
};
|
||||
|
||||
const notificationTestHistory: NotificationTestRecord[] = [];
|
||||
|
||||
function appendNotificationTestRecord(
|
||||
provider: string,
|
||||
status: 'success' | 'error',
|
||||
message: string
|
||||
): void {
|
||||
notificationTestHistory.unshift({
|
||||
id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
provider,
|
||||
status,
|
||||
message,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
if (notificationTestHistory.length > NOTIFICATION_TEST_HISTORY_LIMIT) {
|
||||
notificationTestHistory.splice(NOTIFICATION_TEST_HISTORY_LIMIT);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -203,3 +235,56 @@ configRouter.post('/reset', async (c) => {
|
||||
return c.json({ message: '保存配置失败', error: errMsg }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
configRouter.post('/notification/test', async (c) => {
|
||||
try {
|
||||
let body: Record<string, unknown>;
|
||||
try {
|
||||
const parsed = await c.req.json();
|
||||
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
||||
appendNotificationTestRecord('unknown', 'error', '请求体必须是 JSON 对象');
|
||||
return c.json({ message: '发送测试通知失败', error: '请求体必须是 JSON 对象' }, 400);
|
||||
}
|
||||
body = parsed;
|
||||
} catch {
|
||||
appendNotificationTestRecord('unknown', 'error', '请求体必须是 JSON 对象');
|
||||
return c.json({ message: '发送测试通知失败', error: '请求体必须是 JSON 对象' }, 400);
|
||||
}
|
||||
|
||||
const provider = typeof body.provider === 'string' ? body.provider : '';
|
||||
|
||||
if (!TESTABLE_PROVIDERS.has(provider as NotificationProvider)) {
|
||||
appendNotificationTestRecord(
|
||||
provider || 'unknown',
|
||||
'error',
|
||||
'provider 必须是 feishu 或 wecom'
|
||||
);
|
||||
return c.json({ message: '发送测试通知失败', error: 'provider 必须是 feishu 或 wecom' }, 400);
|
||||
}
|
||||
|
||||
const notificationManager = getNotificationManager();
|
||||
const providerName = provider as NotificationProvider;
|
||||
|
||||
if (!notificationManager.hasService(providerName)) {
|
||||
appendNotificationTestRecord(providerName, 'error', `${providerName} 未启用或未配置`);
|
||||
return c.json({ message: '发送测试通知失败', error: `${providerName} 未启用或未配置` }, 400);
|
||||
}
|
||||
|
||||
await notificationManager.sendTestMessage(providerName);
|
||||
appendNotificationTestRecord(providerName, 'success', `${providerName} 测试通知已发送`);
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
message: `${providerName} 测试通知已发送`,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
appendNotificationTestRecord('unknown', 'error', errMsg);
|
||||
logger.error('发送测试通知失败:', error);
|
||||
return c.json({ message: '发送测试通知失败', error: errMsg }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
configRouter.get('/notification/test/history', (c) => {
|
||||
return c.json({ data: notificationTestHistory });
|
||||
});
|
||||
|
||||
@@ -6,7 +6,8 @@ import { codexEngine } from '../review/codex/codex-engine';
|
||||
import { LocalRepoManager } from '../review/context/local-repo-manager';
|
||||
import { SandboxExec } from '../review/context/sandbox-exec';
|
||||
import { reviewEngine } from '../review/engine';
|
||||
import { feishuService } from '../services/feishu';
|
||||
import { getNotificationManager } from '../services/notification-manager';
|
||||
import type { NotificationContext } from '../services/notification/types';
|
||||
import { PullRequestDetails, giteaService } from '../services/gitea';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
@@ -117,42 +118,47 @@ async function handlePullRequestEvent(c: Context, body: any): Promise<Response>
|
||||
|
||||
logger.info('收到PR事件', { owner, repo: repoName, prNumber, action: body.action });
|
||||
|
||||
// 处理PR审阅者通知(仅在飞书启用时)
|
||||
if (feishuService.isEnabled()) {
|
||||
try {
|
||||
// 获取PR的审阅者列表
|
||||
const reviewerUsernames = map(
|
||||
pullRequest.requested_reviewers,
|
||||
(reviewer) => reviewer.full_name || reviewer.login
|
||||
);
|
||||
// 处理PR审阅者通知(支持多平台)
|
||||
try {
|
||||
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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('处理PR审阅者通知失败:', error);
|
||||
// 继续执行代码审查流程,不因通知失败而中断
|
||||
if (reviewerUsernames.length > 0) {
|
||||
logger.info('PR有指定审阅者', {
|
||||
prNumber,
|
||||
reviewers: reviewerUsernames.join(','),
|
||||
});
|
||||
}
|
||||
|
||||
const notificationManager = getNotificationManager();
|
||||
const context: NotificationContext = {
|
||||
prTitle,
|
||||
prUrl,
|
||||
prNumber,
|
||||
reviewers: reviewerUsernames,
|
||||
repository: repoName,
|
||||
owner,
|
||||
actor: body.sender?.login,
|
||||
};
|
||||
|
||||
// 处理PR创建事件
|
||||
if (body.action === 'opened' && reviewerUsernames.length > 0) {
|
||||
await notificationManager.notifyPrCreated(context);
|
||||
}
|
||||
|
||||
// 处理审阅者指派事件
|
||||
if (body.action === 'review_requested' && body.requested_reviewer) {
|
||||
const newReviewerUsername =
|
||||
body.requested_reviewer.full_name || body.requested_reviewer.login;
|
||||
if (newReviewerUsername) {
|
||||
context.assignees = [newReviewerUsername];
|
||||
await notificationManager.notifyPrReviewerAssigned(context);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('处理PR审阅者通知失败:', error);
|
||||
}
|
||||
|
||||
// Fork PR策略:始终clone base repo(保证有baseSha),headCloneUrl作为额外remote(保证有headSha)
|
||||
@@ -365,26 +371,28 @@ async function handleIssueEvent(c: Context, body: any): Promise<Response> {
|
||||
assigneeUsernames: assigneeUsernames.join(','),
|
||||
});
|
||||
|
||||
if (!feishuService.isEnabled()) {
|
||||
return c.json({ status: 'ignored', message: '飞书通知未启用,工单事件已跳过' }, 200);
|
||||
}
|
||||
|
||||
try {
|
||||
// 处理工单创建事件
|
||||
const notificationManager = getNotificationManager();
|
||||
const context: NotificationContext = {
|
||||
issueTitle,
|
||||
issueUrl,
|
||||
issueNumber: issue.number,
|
||||
creator: creatorUsername,
|
||||
assignees: assigneeUsernames,
|
||||
repository: repository.name,
|
||||
owner: repository.owner?.login,
|
||||
actor: body.sender?.login,
|
||||
};
|
||||
|
||||
if (action === 'opened' && assigneeUsernames.length > 0) {
|
||||
await feishuService.sendIssueCreatedNotification(issueTitle, issueUrl, assigneeUsernames);
|
||||
}
|
||||
// 处理工单关闭事件
|
||||
else if (action === 'closed' && creatorUsername) {
|
||||
await feishuService.sendIssueClosedNotification(issueTitle, issueUrl, creatorUsername);
|
||||
}
|
||||
// 处理工单指派事件
|
||||
else if (action === 'assigned' && assigneeUsernames.length > 0) {
|
||||
await feishuService.sendIssueAssignedNotification(issueTitle, issueUrl, assigneeUsernames);
|
||||
await notificationManager.notifyIssueCreated(context);
|
||||
} else if (action === 'closed') {
|
||||
await notificationManager.notifyIssueClosed(context);
|
||||
} else if (action === 'assigned' && assigneeUsernames.length > 0) {
|
||||
await notificationManager.notifyIssueAssigned(context);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('处理工单事件失败:', error);
|
||||
return c.json({ error: '处理工单事件失败' }, 500);
|
||||
logger.error('新通知系统处理工单事件失败:', error);
|
||||
}
|
||||
|
||||
return c.json({ status: 'success', message: '工单事件处理完成' }, 200);
|
||||
|
||||
Reference in New Issue
Block a user