@zhin.js/adapter-github 0.1.35 → 0.1.37
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/lib/adapter.d.ts +53 -9
- package/lib/adapter.d.ts.map +1 -1
- package/lib/adapter.js +254 -189
- package/lib/adapter.js.map +1 -1
- package/lib/bot.d.ts +5 -2
- package/lib/bot.d.ts.map +1 -1
- package/lib/bot.js +19 -26
- package/lib/bot.js.map +1 -1
- package/lib/{api.d.ts → gh-client.d.ts} +113 -42
- package/lib/gh-client.d.ts.map +1 -0
- package/lib/gh-client.js +555 -0
- package/lib/gh-client.js.map +1 -0
- package/lib/index.d.ts +4 -3
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +212 -126
- package/lib/index.js.map +1 -1
- package/lib/types.d.ts +23 -29
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js +2 -2
- package/lib/types.js.map +1 -1
- package/package.json +5 -5
- package/plugin.yml +1 -1
- package/skills/github/SKILL.md +17 -8
- package/src/adapter.ts +266 -188
- package/src/bot.ts +19 -25
- package/src/gh-client.ts +640 -0
- package/src/index.ts +219 -123
- package/src/types.ts +29 -34
- package/lib/api.d.ts.map +0 -1
- package/lib/api.js +0 -300
- package/lib/api.js.map +0 -1
- package/src/api.ts +0 -353
package/src/adapter.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
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
|
+
import type { Router } from '@zhin.js/http';
|
|
11
12
|
import type { GitHubBotConfig, EventType, GenericWebhookPayload, Subscription } from './types.js';
|
|
12
|
-
import {
|
|
13
|
+
import type { GhClient } from './gh-client.js';
|
|
13
14
|
import type { IssueCommentPayload, PRReviewCommentPayload, PRReviewPayload } from './types.js';
|
|
14
15
|
|
|
15
16
|
const VALID_EVENTS: EventType[] = ['push', 'issue', 'star', 'fork', 'unstar', 'pull_request'];
|
|
@@ -22,9 +23,6 @@ function safeParseEvents(raw: any): EventType[] {
|
|
|
22
23
|
return [];
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
const oauthStates = new Map<string, { platform: string; platformUid: string; expires: number }>();
|
|
26
|
-
const OAUTH_STATE_TTL = 5 * 60 * 1000;
|
|
27
|
-
|
|
28
26
|
function formatNotification(event: string, p: GenericWebhookPayload): string {
|
|
29
27
|
const repo = p.repository.full_name;
|
|
30
28
|
const sender = p.sender.login;
|
|
@@ -76,10 +74,14 @@ function formatNotification(event: string, p: GenericWebhookPayload): string {
|
|
|
76
74
|
}
|
|
77
75
|
|
|
78
76
|
export class GitHubAdapter extends Adapter<GitHubBot> {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
/** 轮询定时器 */
|
|
78
|
+
private _pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
79
|
+
/** 每个 repo 的 ETag 缓存 */
|
|
80
|
+
private _etags = new Map<string, string>();
|
|
81
|
+
/** 每个 repo 最后处理的事件 ID(防重复) */
|
|
82
|
+
private _lastEventIds = new Map<string, string>();
|
|
83
|
+
/** Webhook 是否已激活 */
|
|
84
|
+
private _webhookActive = false;
|
|
83
85
|
|
|
84
86
|
constructor(plugin: Plugin) {
|
|
85
87
|
super(plugin, 'github', []);
|
|
@@ -93,232 +95,308 @@ export class GitHubAdapter extends Adapter<GitHubBot> {
|
|
|
93
95
|
await super.start();
|
|
94
96
|
}
|
|
95
97
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
async stop(): Promise<void> {
|
|
99
|
+
this.stopPolling();
|
|
100
|
+
await super.stop();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 获取第一个可用 bot 的 GhClient (工具用) */
|
|
104
|
+
getAPI(): GhClient | null {
|
|
98
105
|
const bot = this.bots.values().next().value as GitHubBot | undefined;
|
|
99
|
-
return bot?.
|
|
106
|
+
return bot?.gh || null;
|
|
100
107
|
}
|
|
101
108
|
|
|
102
|
-
/**
|
|
103
|
-
|
|
109
|
+
/** 获取指定用户绑定的 GhClient;未绑定则返回 null */
|
|
110
|
+
async getUserAPI(platform: string, platformUid: string): Promise<GhClient | null> {
|
|
111
|
+
const db = this.plugin.root?.inject('database') as any;
|
|
112
|
+
const model = db?.models?.get('github_oauth_users');
|
|
113
|
+
if (!model) return null;
|
|
114
|
+
const [record] = await model.select().where({ platform, platform_uid: platformUid });
|
|
115
|
+
if (!record?.access_token) return null;
|
|
116
|
+
const base = this.getAPI();
|
|
117
|
+
if (!base) return null;
|
|
118
|
+
return base.withToken(record.access_token);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** 获取用户 API,若未绑定则降级为 bot 默认的 API */
|
|
122
|
+
async getUserOrDefaultAPI(platform?: string, platformUid?: string): Promise<GhClient | null> {
|
|
123
|
+
if (platform && platformUid) {
|
|
124
|
+
const userApi = await this.getUserAPI(platform, platformUid);
|
|
125
|
+
if (userApi) return userApi;
|
|
126
|
+
}
|
|
127
|
+
return this.getAPI();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** 获取第一个 bot 的 client_id(App 认证时从 /app 自动获取) */
|
|
131
|
+
getClientId(): string | null {
|
|
104
132
|
const bot = this.bots.values().next().value as GitHubBot | undefined;
|
|
105
|
-
|
|
106
|
-
|
|
133
|
+
return bot?.gh.clientId || null;
|
|
134
|
+
}
|
|
107
135
|
|
|
108
|
-
|
|
109
|
-
|
|
136
|
+
/** 获取第一个 bot 的 host 配置 */
|
|
137
|
+
getHost(): string | undefined {
|
|
138
|
+
const bot = this.bots.values().next().value as GitHubBot | undefined;
|
|
139
|
+
return bot?.$config.host;
|
|
140
|
+
}
|
|
110
141
|
|
|
111
|
-
|
|
112
|
-
|
|
142
|
+
/** 获取 App slug(用于生成安装链接) */
|
|
143
|
+
getAppSlug(): string | null {
|
|
144
|
+
const bot = this.bots.values().next().value as GitHubBot | undefined;
|
|
145
|
+
return bot?.gh.appSlug || null;
|
|
113
146
|
}
|
|
114
147
|
|
|
115
|
-
|
|
148
|
+
/** 获取所有已发现的安装信息 */
|
|
149
|
+
getInstallations() {
|
|
150
|
+
const bot = this.bots.values().next().value as GitHubBot | undefined;
|
|
151
|
+
return bot?.gh.installations || [];
|
|
152
|
+
}
|
|
116
153
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
154
|
+
/** 第一个 bot 是否配置了 Webhook */
|
|
155
|
+
get hasWebhookConfig(): boolean {
|
|
156
|
+
const bot = this.bots.values().next().value as GitHubBot | undefined;
|
|
157
|
+
return !!bot?.$config.webhook_secret;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Webhook 是否已激活 */
|
|
161
|
+
get webhookActive(): boolean {
|
|
162
|
+
return this._webhookActive;
|
|
124
163
|
}
|
|
125
164
|
|
|
126
|
-
// ──
|
|
165
|
+
// ── Webhook ──────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
/** 在 router 上挂载 Webhook 路由(生产环境推荐) */
|
|
168
|
+
setupWebhook(router: Router): void {
|
|
169
|
+
const bot = this.bots.values().next().value as GitHubBot | undefined;
|
|
170
|
+
if (!bot?.$config.webhook_secret) {
|
|
171
|
+
this.plugin.logger.warn('Webhook 配置缺失 webhook_secret,跳过注册');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const secret = bot.$config.webhook_secret;
|
|
175
|
+
const path = bot.$config.webhook_path || '/github/webhook';
|
|
127
176
|
|
|
128
|
-
|
|
129
|
-
|
|
177
|
+
router.post(path, async (ctx) => {
|
|
178
|
+
const signature = ctx.get('x-hub-signature-256') as string;
|
|
179
|
+
const event = ctx.get('x-github-event') as string;
|
|
180
|
+
const deliveryId = ctx.get('x-github-delivery') as string;
|
|
130
181
|
|
|
131
|
-
|
|
132
|
-
const state = ctx.query.state as string;
|
|
133
|
-
if (!state || !oauthStates.has(state)) {
|
|
182
|
+
if (!signature || !event) {
|
|
134
183
|
ctx.status = 400;
|
|
135
|
-
ctx.body =
|
|
184
|
+
ctx.body = { error: 'Missing signature or event header' };
|
|
136
185
|
return;
|
|
137
186
|
}
|
|
138
187
|
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
188
|
+
// HMAC-SHA256 签名验证
|
|
189
|
+
const body = (ctx.request as any).rawBody || JSON.stringify((ctx.request as any).body);
|
|
190
|
+
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(body).digest('hex');
|
|
191
|
+
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
|
|
192
|
+
this.plugin.logger.warn(`Webhook 签名验证失败 (delivery: ${deliveryId})`);
|
|
193
|
+
ctx.status = 401;
|
|
194
|
+
ctx.body = { error: 'Invalid signature' };
|
|
144
195
|
return;
|
|
145
196
|
}
|
|
146
197
|
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
198
|
+
const payload = (ctx.request as any).body;
|
|
199
|
+
ctx.status = 200;
|
|
200
|
+
ctx.body = { ok: true };
|
|
201
|
+
|
|
202
|
+
// 异步处理事件,不阻塞响应
|
|
203
|
+
this.handleWebhookPayload(event, payload).catch(e =>
|
|
204
|
+
this.plugin.logger.error(`Webhook 事件处理失败 (${event}):`, e)
|
|
205
|
+
);
|
|
151
206
|
});
|
|
152
207
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
208
|
+
this._webhookActive = true;
|
|
209
|
+
this.plugin.logger.info(`GitHub Webhook 已注册: POST ${path}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** 处理 Webhook 推送的事件 */
|
|
213
|
+
async handleWebhookPayload(event: string, payload: any): Promise<void> {
|
|
214
|
+
const bot = this.bots.values().next().value as GitHubBot | undefined;
|
|
215
|
+
const repo = payload.repository?.full_name;
|
|
216
|
+
|
|
217
|
+
this.plugin.logger.debug(`Webhook: ${event}${payload.action ? `.${payload.action}` : ''} ${repo || ''}`);
|
|
218
|
+
|
|
219
|
+
// 记录事件到数据库
|
|
220
|
+
if (repo) {
|
|
221
|
+
const db = this.plugin.root?.inject('database') as any;
|
|
222
|
+
const eventsModel = db?.models?.get('github_events');
|
|
223
|
+
if (eventsModel) {
|
|
224
|
+
await eventsModel.insert({ id: Date.now(), repo, event_type: event, payload }).catch(() => {});
|
|
159
225
|
}
|
|
226
|
+
}
|
|
160
227
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
228
|
+
// 处理消息类事件(Issue/PR 评论)
|
|
229
|
+
if (bot && event === 'issue_comment' && payload.action === 'created' && payload.comment) {
|
|
230
|
+
const message = bot.$formatMessage(payload as IssueCommentPayload);
|
|
231
|
+
const botUser = bot.gh.authenticatedUser;
|
|
232
|
+
if (!(botUser && message.$sender.id === botUser)) {
|
|
233
|
+
this.emit('message.receive', message);
|
|
167
234
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
if (!
|
|
174
|
-
|
|
175
|
-
ctx.body = 'OAuth not configured.';
|
|
176
|
-
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (bot && event === 'pull_request_review_comment' && payload.action === 'created' && payload.comment) {
|
|
238
|
+
const message = bot.formatPRReviewComment(payload as PRReviewCommentPayload);
|
|
239
|
+
const botUser = bot.gh.authenticatedUser;
|
|
240
|
+
if (!(botUser && message.$sender.id === botUser)) {
|
|
241
|
+
this.emit('message.receive', message);
|
|
177
242
|
}
|
|
243
|
+
}
|
|
178
244
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
if (!
|
|
184
|
-
|
|
185
|
-
ctx.body = 'Failed to fetch GitHub user info.';
|
|
186
|
-
return;
|
|
245
|
+
if (bot && event === 'pull_request_review' && payload.action === 'submitted') {
|
|
246
|
+
const message = bot.formatPRReview(payload as PRReviewPayload);
|
|
247
|
+
if (message) {
|
|
248
|
+
const botUser = bot.gh.authenticatedUser;
|
|
249
|
+
if (!(botUser && message.$sender.id === botUser)) {
|
|
250
|
+
this.emit('message.receive', message);
|
|
187
251
|
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
188
254
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
255
|
+
// 通知订阅者
|
|
256
|
+
if (repo) {
|
|
257
|
+
const genericPayload: GenericWebhookPayload = {
|
|
258
|
+
action: payload.action,
|
|
259
|
+
repository: payload.repository,
|
|
260
|
+
sender: payload.sender,
|
|
261
|
+
ref: payload.ref,
|
|
262
|
+
commits: payload.commits,
|
|
263
|
+
issue: payload.issue,
|
|
264
|
+
pull_request: payload.pull_request,
|
|
265
|
+
forkee: payload.forkee,
|
|
266
|
+
};
|
|
267
|
+
await this.dispatchNotification(event, genericPayload);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
197
270
|
|
|
198
|
-
|
|
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
|
-
}
|
|
271
|
+
// ── 事件轮询 ────────────────────────────────────────────────────
|
|
220
272
|
|
|
221
|
-
|
|
273
|
+
/** 启动事件轮询 */
|
|
274
|
+
startPolling(): void {
|
|
275
|
+
if (this._pollTimer) return;
|
|
276
|
+
const bot = this.bots.values().next().value as GitHubBot | undefined;
|
|
277
|
+
const interval = (bot?.$config.poll_interval || 60) * 1000;
|
|
278
|
+
this.plugin.logger.info(`GitHub 事件轮询已启动 (间隔 ${interval / 1000}s)`);
|
|
222
279
|
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
});
|
|
280
|
+
// 立即执行一次
|
|
281
|
+
this.pollAllRepos().catch(e => this.plugin.logger.error('轮询失败:', e));
|
|
231
282
|
|
|
232
|
-
this.
|
|
283
|
+
this._pollTimer = setInterval(() => {
|
|
284
|
+
this.pollAllRepos().catch(e => this.plugin.logger.error('轮询失败:', e));
|
|
285
|
+
}, interval);
|
|
233
286
|
}
|
|
234
287
|
|
|
235
|
-
|
|
288
|
+
/** 停止事件轮询 */
|
|
289
|
+
stopPolling(): void {
|
|
290
|
+
if (this._pollTimer) {
|
|
291
|
+
clearInterval(this._pollTimer);
|
|
292
|
+
this._pollTimer = null;
|
|
293
|
+
this.plugin.logger.debug('GitHub 事件轮询已停止');
|
|
294
|
+
}
|
|
295
|
+
}
|
|
236
296
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const payload = ctx.request.body;
|
|
297
|
+
/** 轮询所有已订阅仓库的事件 */
|
|
298
|
+
private async pollAllRepos(): Promise<void> {
|
|
299
|
+
const db = this.plugin.root?.inject('database') as any;
|
|
300
|
+
const model = db?.models?.get('github_subscriptions');
|
|
301
|
+
if (!model) return;
|
|
243
302
|
|
|
244
|
-
|
|
303
|
+
// 获取所有不重复的订阅仓库
|
|
304
|
+
const allSubs = await model.select();
|
|
305
|
+
const repos = [...new Set((allSubs || []).map((s: Subscription) => s.repo))] as string[];
|
|
306
|
+
if (!repos.length) return;
|
|
245
307
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
ctx.status = 200;
|
|
249
|
-
ctx.body = { message: 'pong' };
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
308
|
+
const gh = this.getAPI();
|
|
309
|
+
if (!gh) return;
|
|
252
310
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
}
|
|
311
|
+
for (const repo of repos) {
|
|
312
|
+
try {
|
|
313
|
+
await this.pollRepoEvents(repo, gh);
|
|
314
|
+
} catch (e) {
|
|
315
|
+
this.plugin.logger.warn(`轮询 ${repo} 事件失败:`, e);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
272
319
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
320
|
+
/** 轮询单个仓库的事件 */
|
|
321
|
+
private async pollRepoEvents(repo: string, gh: GhClient): Promise<void> {
|
|
322
|
+
const etag = this._etags.get(repo);
|
|
323
|
+
const { events, etag: newEtag } = await gh.listRepoEvents(repo, etag);
|
|
324
|
+
if (newEtag) this._etags.set(repo, newEtag);
|
|
325
|
+
if (!events.length) return;
|
|
326
|
+
|
|
327
|
+
const lastId = this._lastEventIds.get(repo);
|
|
328
|
+
const newEvents: any[] = [];
|
|
329
|
+
for (const ev of events) {
|
|
330
|
+
if (ev.id === lastId) break;
|
|
331
|
+
newEvents.push(ev);
|
|
332
|
+
}
|
|
333
|
+
if (!newEvents.length) return;
|
|
278
334
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (eventsModel) {
|
|
282
|
-
await eventsModel.insert({ id: Date.now(), repo: payload.repository.full_name, event_type: eventName, payload });
|
|
283
|
-
}
|
|
335
|
+
// 记录最新事件 ID
|
|
336
|
+
this._lastEventIds.set(repo, events[0].id);
|
|
284
337
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
}
|
|
338
|
+
// 首次轮询只记录位置,不触发通知(避免启动时大量回溯)
|
|
339
|
+
if (!lastId) {
|
|
340
|
+
this.plugin.logger.debug(`${repo}: 首次轮询,记录位置 (${events[0].id}),跳过 ${events.length} 条历史事件`);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
307
343
|
|
|
308
|
-
|
|
344
|
+
this.plugin.logger.debug(`${repo}: ${newEvents.length} 条新事件`);
|
|
309
345
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
346
|
+
const bot = this.bots.values().next().value as GitHubBot | undefined;
|
|
347
|
+
|
|
348
|
+
// 按时间正序处理(API 返回倒序)
|
|
349
|
+
for (const ev of newEvents.reverse()) {
|
|
350
|
+
const eventName = this.mapEventType(ev.type);
|
|
351
|
+
if (!eventName) continue;
|
|
352
|
+
|
|
353
|
+
// 构造与 webhook payload 兼容的结构
|
|
354
|
+
const payload: GenericWebhookPayload = {
|
|
355
|
+
action: ev.payload?.action,
|
|
356
|
+
repository: ev.repo ? { full_name: ev.repo.name, html_url: `https://github.com/${ev.repo.name}`, description: '' } : ev.payload?.repository,
|
|
357
|
+
sender: { login: ev.actor?.login || '?', id: ev.actor?.id || 0, html_url: `https://github.com/${ev.actor?.login}` },
|
|
358
|
+
ref: ev.payload?.ref,
|
|
359
|
+
commits: ev.payload?.commits,
|
|
360
|
+
issue: ev.payload?.issue,
|
|
361
|
+
pull_request: ev.payload?.pull_request,
|
|
362
|
+
forkee: ev.payload?.forkee,
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
// 记录事件到数据库
|
|
366
|
+
const db = this.plugin.root?.inject('database') as any;
|
|
367
|
+
const eventsModel = db?.models?.get('github_events');
|
|
368
|
+
if (eventsModel) {
|
|
369
|
+
await eventsModel.insert({ id: Date.now(), repo, event_type: eventName, payload: ev.payload }).catch(() => {});
|
|
316
370
|
}
|
|
317
|
-
});
|
|
318
371
|
|
|
319
|
-
|
|
372
|
+
// 处理消息类事件(Issue/PR 评论)
|
|
373
|
+
if (bot && eventName === 'issue_comment' && ev.payload?.action === 'created' && ev.payload?.comment) {
|
|
374
|
+
const message = bot.$formatMessage(ev.payload as IssueCommentPayload);
|
|
375
|
+
const botUser = bot.gh.authenticatedUser;
|
|
376
|
+
if (!(botUser && message.$sender.id === botUser)) {
|
|
377
|
+
this.emit('message.receive', message);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// 通知订阅者
|
|
382
|
+
await this.dispatchNotification(eventName, payload);
|
|
383
|
+
}
|
|
320
384
|
}
|
|
321
385
|
|
|
386
|
+
/** Events API type → webhook event name */
|
|
387
|
+
private mapEventType(type: string): string | null {
|
|
388
|
+
const map: Record<string, string> = {
|
|
389
|
+
PushEvent: 'push',
|
|
390
|
+
IssuesEvent: 'issues',
|
|
391
|
+
WatchEvent: 'star',
|
|
392
|
+
ForkEvent: 'fork',
|
|
393
|
+
PullRequestEvent: 'pull_request',
|
|
394
|
+
IssueCommentEvent: 'issue_comment',
|
|
395
|
+
PullRequestReviewEvent: 'pull_request_review',
|
|
396
|
+
PullRequestReviewCommentEvent: 'pull_request_review_comment',
|
|
397
|
+
};
|
|
398
|
+
return map[type] || null;
|
|
399
|
+
}
|
|
322
400
|
|
|
323
401
|
// ── 通知推送 ───────────────────────────────────────────────────────
|
|
324
402
|
|