@zhin.js/adapter-github 0.1.34 → 0.1.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/adapter.ts CHANGED
@@ -1,15 +1,15 @@
1
1
  /**
2
- * GitHub 适配器
2
+ * GitHub 适配器(基于 gh CLI + App 认证 + Webhook/轮询混合)
3
3
  */
4
- import crypto from 'node:crypto';
5
4
  import {
6
5
  Adapter,
7
6
  Plugin,
8
7
  Message,
9
8
  } from 'zhin.js';
9
+ import crypto from 'node:crypto';
10
10
  import { GitHubBot } from './bot.js';
11
11
  import type { GitHubBotConfig, EventType, GenericWebhookPayload, Subscription } from './types.js';
12
- import { GitHubAPI, GitHubOAuthClient, exchangeOAuthCode } from './api.js';
12
+ import type { GhClient } from './gh-client.js';
13
13
  import type { IssueCommentPayload, PRReviewCommentPayload, PRReviewPayload } from './types.js';
14
14
 
15
15
  const VALID_EVENTS: EventType[] = ['push', 'issue', 'star', 'fork', 'unstar', 'pull_request'];
@@ -22,9 +22,6 @@ function safeParseEvents(raw: any): EventType[] {
22
22
  return [];
23
23
  }
24
24
 
25
- const oauthStates = new Map<string, { platform: string; platformUid: string; expires: number }>();
26
- const OAUTH_STATE_TTL = 5 * 60 * 1000;
27
-
28
25
  function formatNotification(event: string, p: GenericWebhookPayload): string {
29
26
  const repo = p.repository.full_name;
30
27
  const sender = p.sender.login;
@@ -76,10 +73,14 @@ function formatNotification(event: string, p: GenericWebhookPayload): string {
76
73
  }
77
74
 
78
75
  export class GitHubAdapter extends Adapter<GitHubBot> {
79
- get publicUrl(): string | undefined {
80
- const bot = this.bots.values().next().value as GitHubBot | undefined;
81
- return bot?.$config.public_url?.replace(/\/+$/, '');
82
- }
76
+ /** 轮询定时器 */
77
+ private _pollTimer: ReturnType<typeof setInterval> | null = null;
78
+ /** 每个 repo 的 ETag 缓存 */
79
+ private _etags = new Map<string, string>();
80
+ /** 每个 repo 最后处理的事件 ID(防重复) */
81
+ private _lastEventIds = new Map<string, string>();
82
+ /** Webhook 是否已激活 */
83
+ private _webhookActive = false;
83
84
 
84
85
  constructor(plugin: Plugin) {
85
86
  super(plugin, 'github', []);
@@ -93,232 +94,308 @@ export class GitHubAdapter extends Adapter<GitHubBot> {
93
94
  await super.start();
94
95
  }
95
96
 
96
- /** 获取第一个可用 bot 的 API (工具用) */
97
- getAPI(): GitHubAPI | null {
97
+ async stop(): Promise<void> {
98
+ this.stopPolling();
99
+ await super.stop();
100
+ }
101
+
102
+ /** 获取第一个可用 bot 的 GhClient (工具用) */
103
+ getAPI(): GhClient | null {
98
104
  const bot = this.bots.values().next().value as GitHubBot | undefined;
99
- return bot?.api || null;
105
+ return bot?.gh || null;
100
106
  }
101
107
 
102
- /** 创建 OAuth 状态并返回授权 URL(供 github_bind 工具使用) */
103
- createOAuthState(platform: string, platformUid: string): string | null {
108
+ /** 获取指定用户绑定的 GhClient;未绑定则返回 null */
109
+ async getUserAPI(platform: string, platformUid: string): Promise<GhClient | null> {
110
+ const db = this.plugin.root?.inject('database') as any;
111
+ const model = db?.models?.get('github_oauth_users');
112
+ if (!model) return null;
113
+ const [record] = await model.select().where({ platform, platform_uid: platformUid });
114
+ if (!record?.access_token) return null;
115
+ const base = this.getAPI();
116
+ if (!base) return null;
117
+ return base.withToken(record.access_token);
118
+ }
119
+
120
+ /** 获取用户 API,若未绑定则降级为 bot 默认的 API */
121
+ async getUserOrDefaultAPI(platform?: string, platformUid?: string): Promise<GhClient | null> {
122
+ if (platform && platformUid) {
123
+ const userApi = await this.getUserAPI(platform, platformUid);
124
+ if (userApi) return userApi;
125
+ }
126
+ return this.getAPI();
127
+ }
128
+
129
+ /** 获取第一个 bot 的 client_id(App 认证时从 /app 自动获取) */
130
+ getClientId(): string | null {
104
131
  const bot = this.bots.values().next().value as GitHubBot | undefined;
105
- const clientId = bot?.$config.client_id;
106
- if (!clientId) return null;
132
+ return bot?.gh.clientId || null;
133
+ }
107
134
 
108
- const state = crypto.randomUUID();
109
- oauthStates.set(state, { platform, platformUid, expires: Date.now() + OAUTH_STATE_TTL });
135
+ /** 获取第一个 bot 的 host 配置 */
136
+ getHost(): string | undefined {
137
+ const bot = this.bots.values().next().value as GitHubBot | undefined;
138
+ return bot?.$config.host;
139
+ }
110
140
 
111
- const base = this.publicUrl || '';
112
- return `${base}/pub/github/oauth?state=${state}`;
141
+ /** 获取 App slug(用于生成安装链接) */
142
+ getAppSlug(): string | null {
143
+ const bot = this.bots.values().next().value as GitHubBot | undefined;
144
+ return bot?.gh.appSlug || null;
113
145
  }
114
146
 
115
- // ── OAuth 用户查询 ─────────────────────────────────────────────────
147
+ /** 获取所有已发现的安装信息 */
148
+ getInstallations() {
149
+ const bot = this.bots.values().next().value as GitHubBot | undefined;
150
+ return bot?.gh.installations || [];
151
+ }
116
152
 
117
- async getOAuthClient(platform: string, platformUid: string): Promise<GitHubOAuthClient | null> {
118
- const db = this.plugin.root?.inject('database') as any;
119
- const model = db?.models?.get('github_oauth_users');
120
- if (!model) return null;
121
- const [row] = await model.select().where({ platform, platform_uid: platformUid });
122
- if (!row) return null;
123
- return new GitHubOAuthClient(row.access_token);
153
+ /** 第一个 bot 是否配置了 Webhook */
154
+ get hasWebhookConfig(): boolean {
155
+ const bot = this.bots.values().next().value as GitHubBot | undefined;
156
+ return !!bot?.$config.webhook_secret;
157
+ }
158
+
159
+ /** Webhook 是否已激活 */
160
+ get webhookActive(): boolean {
161
+ return this._webhookActive;
124
162
  }
125
163
 
126
- // ── OAuth 路由 (由 useContext('router') 注入) ─────────────────────
164
+ // ── Webhook ──────────────────────────────────────────────────────
165
+
166
+ /** 在 router 上挂载 Webhook 路由(生产环境推荐) */
167
+ setupWebhook(router: any): void {
168
+ const bot = this.bots.values().next().value as GitHubBot | undefined;
169
+ if (!bot?.$config.webhook_secret) {
170
+ this.plugin.logger.warn('Webhook 配置缺失 webhook_secret,跳过注册');
171
+ return;
172
+ }
173
+ const secret = bot.$config.webhook_secret;
174
+ const path = bot.$config.webhook_path || '/github/webhook';
127
175
 
128
- setupOAuth(router: import('@zhin.js/http').Router): void {
129
- const OAUTH_SCOPES = 'repo,user';
176
+ router.post(path, async (ctx: any) => {
177
+ const signature = ctx.get('x-hub-signature-256') as string;
178
+ const event = ctx.get('x-github-event') as string;
179
+ const deliveryId = ctx.get('x-github-delivery') as string;
130
180
 
131
- router.get('/pub/github/oauth', async (ctx: any) => {
132
- const state = ctx.query.state as string;
133
- if (!state || !oauthStates.has(state)) {
181
+ if (!signature || !event) {
134
182
  ctx.status = 400;
135
- ctx.body = 'Invalid or expired state. Please use /github bind to generate a new link.';
183
+ ctx.body = { error: 'Missing signature or event header' };
136
184
  return;
137
185
  }
138
186
 
139
- const bot = this.bots.values().next().value as GitHubBot | undefined;
140
- const clientId = bot?.$config.client_id;
141
- if (!clientId) {
142
- ctx.status = 500;
143
- ctx.body = 'GitHub App OAuth not configured (missing client_id).';
187
+ // HMAC-SHA256 签名验证
188
+ const body = (ctx.request as any).rawBody || JSON.stringify((ctx.request as any).body);
189
+ const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(body).digest('hex');
190
+ if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
191
+ this.plugin.logger.warn(`Webhook 签名验证失败 (delivery: ${deliveryId})`);
192
+ ctx.status = 401;
193
+ ctx.body = { error: 'Invalid signature' };
144
194
  return;
145
195
  }
146
196
 
147
- const base = this.publicUrl || ctx.origin;
148
- const redirectUri = `${base}/pub/github/oauth/callback`;
149
- const url = `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${OAUTH_SCOPES}&state=${state}`;
150
- ctx.redirect(url);
197
+ const payload = (ctx.request as any).body;
198
+ ctx.status = 200;
199
+ ctx.body = { ok: true };
200
+
201
+ // 异步处理事件,不阻塞响应
202
+ this.handleWebhookPayload(event, payload).catch(e =>
203
+ this.plugin.logger.error(`Webhook 事件处理失败 (${event}):`, e)
204
+ );
151
205
  });
152
206
 
153
- router.get('/pub/github/oauth/callback', async (ctx: any) => {
154
- const { code, state } = ctx.query as { code?: string; state?: string };
155
- if (!code || !state) {
156
- ctx.status = 400;
157
- ctx.body = 'Missing code or state parameter.';
158
- return;
207
+ this._webhookActive = true;
208
+ this.plugin.logger.info(`GitHub Webhook 已注册: POST ${path}`);
209
+ }
210
+
211
+ /** 处理 Webhook 推送的事件 */
212
+ async handleWebhookPayload(event: string, payload: any): Promise<void> {
213
+ const bot = this.bots.values().next().value as GitHubBot | undefined;
214
+ const repo = payload.repository?.full_name;
215
+
216
+ this.plugin.logger.debug(`Webhook: ${event}${payload.action ? `.${payload.action}` : ''} ${repo || ''}`);
217
+
218
+ // 记录事件到数据库
219
+ if (repo) {
220
+ const db = this.plugin.root?.inject('database') as any;
221
+ const eventsModel = db?.models?.get('github_events');
222
+ if (eventsModel) {
223
+ await eventsModel.insert({ id: Date.now(), repo, event_type: event, payload }).catch(() => {});
159
224
  }
225
+ }
160
226
 
161
- const pending = oauthStates.get(state);
162
- if (!pending || Date.now() > pending.expires) {
163
- oauthStates.delete(state);
164
- ctx.status = 400;
165
- ctx.body = 'State expired. Please use /github bind to try again.';
166
- return;
227
+ // 处理消息类事件(Issue/PR 评论)
228
+ if (bot && event === 'issue_comment' && payload.action === 'created' && payload.comment) {
229
+ const message = bot.$formatMessage(payload as IssueCommentPayload);
230
+ const botUser = bot.gh.authenticatedUser;
231
+ if (!(botUser && message.$sender.id === botUser)) {
232
+ this.emit('message.receive', message);
167
233
  }
168
- oauthStates.delete(state);
169
-
170
- const bot = this.bots.values().next().value as GitHubBot | undefined;
171
- const clientId = bot?.$config.client_id;
172
- const clientSecret = bot?.$config.client_secret;
173
- if (!clientId || !clientSecret) {
174
- ctx.status = 500;
175
- ctx.body = 'OAuth not configured.';
176
- return;
234
+ }
235
+
236
+ if (bot && event === 'pull_request_review_comment' && payload.action === 'created' && payload.comment) {
237
+ const message = bot.formatPRReviewComment(payload as PRReviewCommentPayload);
238
+ const botUser = bot.gh.authenticatedUser;
239
+ if (!(botUser && message.$sender.id === botUser)) {
240
+ this.emit('message.receive', message);
177
241
  }
242
+ }
178
243
 
179
- try {
180
- const tokenData = await exchangeOAuthCode(clientId, clientSecret, code);
181
- const oauthClient = new GitHubOAuthClient(tokenData.access_token);
182
- const userRes = await oauthClient.getUser();
183
- if (!userRes.ok) {
184
- ctx.status = 500;
185
- ctx.body = 'Failed to fetch GitHub user info.';
186
- return;
244
+ if (bot && event === 'pull_request_review' && payload.action === 'submitted') {
245
+ const message = bot.formatPRReview(payload as PRReviewPayload);
246
+ if (message) {
247
+ const botUser = bot.gh.authenticatedUser;
248
+ if (!(botUser && message.$sender.id === botUser)) {
249
+ this.emit('message.receive', message);
187
250
  }
251
+ }
252
+ }
188
253
 
189
- const ghUser = userRes.data;
190
- const db = this.plugin.root?.inject('database') as any;
191
- const model = db?.models?.get('github_oauth_users');
192
- if (!model) {
193
- ctx.status = 500;
194
- ctx.body = 'Database not ready.';
195
- return;
196
- }
254
+ // 通知订阅者
255
+ if (repo) {
256
+ const genericPayload: GenericWebhookPayload = {
257
+ action: payload.action,
258
+ repository: payload.repository,
259
+ sender: payload.sender,
260
+ ref: payload.ref,
261
+ commits: payload.commits,
262
+ issue: payload.issue,
263
+ pull_request: payload.pull_request,
264
+ forkee: payload.forkee,
265
+ };
266
+ await this.dispatchNotification(event, genericPayload);
267
+ }
268
+ }
197
269
 
198
- const [existing] = await model.select().where({ platform: pending.platform, platform_uid: pending.platformUid });
199
- if (existing) {
200
- await model.update({
201
- github_login: ghUser.login,
202
- github_id: ghUser.id,
203
- access_token: tokenData.access_token,
204
- scope: tokenData.scope || '',
205
- updated_at: new Date(),
206
- }).where({ id: existing.id });
207
- } else {
208
- await model.insert({
209
- id: Date.now(),
210
- platform: pending.platform,
211
- platform_uid: pending.platformUid,
212
- github_login: ghUser.login,
213
- github_id: ghUser.id,
214
- access_token: tokenData.access_token,
215
- scope: tokenData.scope || '',
216
- created_at: new Date(),
217
- updated_at: new Date(),
218
- });
219
- }
270
+ // ── 事件轮询 ────────────────────────────────────────────────────
220
271
 
221
- this.plugin.logger.info(`OAuth 绑定成功: ${pending.platform}:${pending.platformUid} → ${ghUser.login}`);
272
+ /** 启动事件轮询 */
273
+ startPolling(): void {
274
+ if (this._pollTimer) return;
275
+ const bot = this.bots.values().next().value as GitHubBot | undefined;
276
+ const interval = (bot?.$config.poll_interval || 60) * 1000;
277
+ this.plugin.logger.info(`GitHub 事件轮询已启动 (间隔 ${interval / 1000}s)`);
222
278
 
223
- ctx.type = 'text/html';
224
- ctx.body = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>绑定成功</title><style>body{font-family:system-ui;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#f5f5f5}div{text-align:center;background:#fff;padding:3rem;border-radius:12px;box-shadow:0 2px 12px rgba(0,0,0,.1)}h1{color:#28a745;margin-bottom:.5rem}p{color:#666}</style></head><body><div><h1>GitHub 账号绑定成功</h1><p>已绑定 GitHub 用户: <strong>${ghUser.login}</strong></p><p>你现在可以关闭这个页面,回到聊天中使用 GitHub 功能了。</p></div></body></html>`;
225
- } catch (err: any) {
226
- this.plugin.logger.error('OAuth callback 失败:', err);
227
- ctx.status = 500;
228
- ctx.body = `OAuth failed: ${err.message}`;
229
- }
230
- });
279
+ // 立即执行一次
280
+ this.pollAllRepos().catch(e => this.plugin.logger.error('轮询失败:', e));
231
281
 
232
- this.plugin.logger.debug('GitHub OAuth: GET /pub/github/oauth, GET /pub/github/oauth/callback');
282
+ this._pollTimer = setInterval(() => {
283
+ this.pollAllRepos().catch(e => this.plugin.logger.error('轮询失败:', e));
284
+ }, interval);
233
285
  }
234
286
 
235
- // ── Webhook 路由 (由 useContext('router') 注入) ────────────────────
287
+ /** 停止事件轮询 */
288
+ stopPolling(): void {
289
+ if (this._pollTimer) {
290
+ clearInterval(this._pollTimer);
291
+ this._pollTimer = null;
292
+ this.plugin.logger.debug('GitHub 事件轮询已停止');
293
+ }
294
+ }
236
295
 
237
- setupWebhook(router: import('@zhin.js/http').Router): void {
238
- router.post('/pub/github/webhook', async (ctx: any) => {
239
- try {
240
- const eventName = ctx.request.headers['x-github-event'] as string;
241
- const signature = ctx.request.headers['x-hub-signature-256'] as string;
242
- const payload = ctx.request.body;
296
+ /** 轮询所有已订阅仓库的事件 */
297
+ private async pollAllRepos(): Promise<void> {
298
+ const db = this.plugin.root?.inject('database') as any;
299
+ const model = db?.models?.get('github_subscriptions');
300
+ if (!model) return;
243
301
 
244
- this.plugin.logger.info(`GitHub Webhook: ${eventName} - ${payload?.repository?.full_name || '(no repo)'}`);
302
+ // 获取所有不重复的订阅仓库
303
+ const allSubs = await model.select();
304
+ const repos = [...new Set((allSubs || []).map((s: Subscription) => s.repo))] as string[];
305
+ if (!repos.length) return;
245
306
 
246
- if (eventName === 'ping') {
247
- this.plugin.logger.info(`GitHub Webhook ping OK — hook_id: ${payload?.hook_id}, zen: ${payload?.zen}`);
248
- ctx.status = 200;
249
- ctx.body = { message: 'pong' };
250
- return;
251
- }
307
+ const gh = this.getAPI();
308
+ if (!gh) return;
252
309
 
253
- if (signature) {
254
- let verified = false;
255
- const rawBody = JSON.stringify(payload);
256
- for (const bot of this.bots.values()) {
257
- const secret = bot.$config.webhook_secret;
258
- if (!secret) continue;
259
- const expected = `sha256=${crypto.createHmac('sha256', secret).update(rawBody).digest('hex')}`;
260
- if (signature === expected) { verified = true; break; }
261
- }
262
- if (!verified) {
263
- const hasSecret = Array.from(this.bots.values()).some(b => b.$config.webhook_secret);
264
- if (hasSecret) {
265
- this.plugin.logger.warn('GitHub Webhook 签名验证失败');
266
- ctx.status = 401;
267
- ctx.body = { error: 'Invalid signature' };
268
- return;
269
- }
270
- }
271
- }
310
+ for (const repo of repos) {
311
+ try {
312
+ await this.pollRepoEvents(repo, gh);
313
+ } catch (e) {
314
+ this.plugin.logger.warn(`轮询 ${repo} 事件失败:`, e);
315
+ }
316
+ }
317
+ }
272
318
 
273
- if (!payload?.repository) {
274
- ctx.status = 400;
275
- ctx.body = { error: 'Invalid payload' };
276
- return;
277
- }
319
+ /** 轮询单个仓库的事件 */
320
+ private async pollRepoEvents(repo: string, gh: GhClient): Promise<void> {
321
+ const etag = this._etags.get(repo);
322
+ const { events, etag: newEtag } = await gh.listRepoEvents(repo, etag);
323
+ if (newEtag) this._etags.set(repo, newEtag);
324
+ if (!events.length) return;
325
+
326
+ const lastId = this._lastEventIds.get(repo);
327
+ const newEvents: any[] = [];
328
+ for (const ev of events) {
329
+ if (ev.id === lastId) break;
330
+ newEvents.push(ev);
331
+ }
332
+ if (!newEvents.length) return;
278
333
 
279
- const db = this.plugin.root?.inject('database') as any;
280
- const eventsModel = db?.models?.get('github_events');
281
- if (eventsModel) {
282
- await eventsModel.insert({ id: Date.now(), repo: payload.repository.full_name, event_type: eventName, payload });
283
- }
334
+ // 记录最新事件 ID
335
+ this._lastEventIds.set(repo, events[0].id);
284
336
 
285
- const bot = this.bots.values().next().value as GitHubBot | undefined;
286
-
287
- if (bot) {
288
- let message: Message | null = null;
289
-
290
- if (eventName === 'issue_comment' && payload.action === 'created') {
291
- message = bot.$formatMessage(payload as IssueCommentPayload);
292
- } else if (eventName === 'pull_request_review_comment' && payload.action === 'created') {
293
- message = bot.formatPRReviewComment(payload as PRReviewCommentPayload);
294
- } else if (eventName === 'pull_request_review' && payload.action === 'submitted') {
295
- message = bot.formatPRReview(payload as PRReviewPayload);
296
- }
297
-
298
- if (message) {
299
- const botUser = bot.api.authenticatedUser;
300
- if (botUser && message.$sender.id === botUser) {
301
- this.plugin.logger.debug(`忽略 bot 自身评论: ${message.$sender.id}`);
302
- } else {
303
- this.emit('message.receive', message);
304
- }
305
- }
306
- }
337
+ // 首次轮询只记录位置,不触发通知(避免启动时大量回溯)
338
+ if (!lastId) {
339
+ this.plugin.logger.debug(`${repo}: 首次轮询,记录位置 (${events[0].id}),跳过 ${events.length} 条历史事件`);
340
+ return;
341
+ }
307
342
 
308
- await this.dispatchNotification(eventName, payload);
343
+ this.plugin.logger.debug(`${repo}: ${newEvents.length} 条新事件`);
309
344
 
310
- ctx.status = 200;
311
- ctx.body = { message: 'OK' };
312
- } catch (error) {
313
- this.plugin.logger.error('Webhook 处理失败:', error);
314
- ctx.status = 500;
315
- ctx.body = { error: 'Internal server error' };
345
+ const bot = this.bots.values().next().value as GitHubBot | undefined;
346
+
347
+ // 按时间正序处理(API 返回倒序)
348
+ for (const ev of newEvents.reverse()) {
349
+ const eventName = this.mapEventType(ev.type);
350
+ if (!eventName) continue;
351
+
352
+ // 构造与 webhook payload 兼容的结构
353
+ const payload: GenericWebhookPayload = {
354
+ action: ev.payload?.action,
355
+ repository: ev.repo ? { full_name: ev.repo.name, html_url: `https://github.com/${ev.repo.name}`, description: '' } : ev.payload?.repository,
356
+ sender: { login: ev.actor?.login || '?', id: ev.actor?.id || 0, html_url: `https://github.com/${ev.actor?.login}` },
357
+ ref: ev.payload?.ref,
358
+ commits: ev.payload?.commits,
359
+ issue: ev.payload?.issue,
360
+ pull_request: ev.payload?.pull_request,
361
+ forkee: ev.payload?.forkee,
362
+ };
363
+
364
+ // 记录事件到数据库
365
+ const db = this.plugin.root?.inject('database') as any;
366
+ const eventsModel = db?.models?.get('github_events');
367
+ if (eventsModel) {
368
+ await eventsModel.insert({ id: Date.now(), repo, event_type: eventName, payload: ev.payload }).catch(() => {});
316
369
  }
317
- });
318
370
 
319
- this.plugin.logger.debug('GitHub Webhook: POST /pub/github/webhook');
371
+ // 处理消息类事件(Issue/PR 评论)
372
+ if (bot && eventName === 'issue_comment' && ev.payload?.action === 'created' && ev.payload?.comment) {
373
+ const message = bot.$formatMessage(ev.payload as IssueCommentPayload);
374
+ const botUser = bot.gh.authenticatedUser;
375
+ if (!(botUser && message.$sender.id === botUser)) {
376
+ this.emit('message.receive', message);
377
+ }
378
+ }
379
+
380
+ // 通知订阅者
381
+ await this.dispatchNotification(eventName, payload);
382
+ }
320
383
  }
321
384
 
385
+ /** Events API type → webhook event name */
386
+ private mapEventType(type: string): string | null {
387
+ const map: Record<string, string> = {
388
+ PushEvent: 'push',
389
+ IssuesEvent: 'issues',
390
+ WatchEvent: 'star',
391
+ ForkEvent: 'fork',
392
+ PullRequestEvent: 'pull_request',
393
+ IssueCommentEvent: 'issue_comment',
394
+ PullRequestReviewEvent: 'pull_request_review',
395
+ PullRequestReviewCommentEvent: 'pull_request_review_comment',
396
+ };
397
+ return map[type] || null;
398
+ }
322
399
 
323
400
  // ── 通知推送 ───────────────────────────────────────────────────────
324
401