@zhin.js/adapter-github 0.1.35 → 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/lib/adapter.d.ts +52 -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 +2 -2
- package/plugin.yml +1 -1
- package/skills/github/SKILL.md +17 -8
- package/src/adapter.ts +265 -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,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 {
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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?.
|
|
105
|
+
return bot?.gh || null;
|
|
100
106
|
}
|
|
101
107
|
|
|
102
|
-
/**
|
|
103
|
-
|
|
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
|
-
|
|
106
|
-
|
|
132
|
+
return bot?.gh.clientId || null;
|
|
133
|
+
}
|
|
107
134
|
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
147
|
+
/** 获取所有已发现的安装信息 */
|
|
148
|
+
getInstallations() {
|
|
149
|
+
const bot = this.bots.values().next().value as GitHubBot | undefined;
|
|
150
|
+
return bot?.gh.installations || [];
|
|
151
|
+
}
|
|
116
152
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
// ──
|
|
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
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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 =
|
|
183
|
+
ctx.body = { error: 'Missing signature or event header' };
|
|
136
184
|
return;
|
|
137
185
|
}
|
|
138
186
|
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
if (!
|
|
174
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
if (!
|
|
184
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
});
|
|
279
|
+
// 立即执行一次
|
|
280
|
+
this.pollAllRepos().catch(e => this.plugin.logger.error('轮询失败:', e));
|
|
231
281
|
|
|
232
|
-
this.
|
|
282
|
+
this._pollTimer = setInterval(() => {
|
|
283
|
+
this.pollAllRepos().catch(e => this.plugin.logger.error('轮询失败:', e));
|
|
284
|
+
}, interval);
|
|
233
285
|
}
|
|
234
286
|
|
|
235
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
ctx.status = 200;
|
|
249
|
-
ctx.body = { message: 'pong' };
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
307
|
+
const gh = this.getAPI();
|
|
308
|
+
if (!gh) return;
|
|
252
309
|
|
|
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
|
-
}
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
280
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
337
|
+
// 首次轮询只记录位置,不触发通知(避免启动时大量回溯)
|
|
338
|
+
if (!lastId) {
|
|
339
|
+
this.plugin.logger.debug(`${repo}: 首次轮询,记录位置 (${events[0].id}),跳过 ${events.length} 条历史事件`);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
307
342
|
|
|
308
|
-
|
|
343
|
+
this.plugin.logger.debug(`${repo}: ${newEvents.length} 条新事件`);
|
|
309
344
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
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
|
|