@zhin.js/adapter-github 3.0.2 → 4.0.0
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/CHANGELOG.md +646 -0
- package/README.md +55 -203
- package/adapters/github.ts +51 -0
- package/agent/skills/github.md +2 -2
- package/agent/tools/bind.ts +2 -2
- package/agent/tools/create_pr.ts +2 -2
- package/agent/tools/install.ts +2 -2
- package/agent/tools/patch_file.ts +2 -2
- package/agent/tools/prepare_workspace.ts +2 -2
- package/agent/tools/push_branch.ts +2 -2
- package/agent/tools/star.ts +2 -2
- package/agent/tools/subscribe.ts +2 -2
- package/agent/tools/subscriptions.ts +2 -2
- package/agent/tools/unbind.ts +2 -2
- package/agent/tools/unsubscribe.ts +2 -2
- package/agent/tools/whoami.ts +2 -2
- package/lib/agent-prompt.d.ts +2 -0
- package/lib/{src/agent-prompt.js → agent-prompt.js} +0 -1
- package/lib/endpoint.d.ts +48 -0
- package/lib/endpoint.js +165 -0
- package/lib/gh-client.d.ts +300 -0
- package/lib/{src/gh-client.js → gh-client.js} +1 -2
- package/lib/github-agent-deps.d.ts +47 -0
- package/lib/github-agent-deps.js +43 -0
- package/lib/github-bot-handlers.d.ts +26 -0
- package/lib/{src/github-bot-handlers.js → github-bot-handlers.js} +0 -1
- package/lib/github-channel-context.d.ts +16 -0
- package/lib/{src/github-channel-context.js → github-channel-context.js} +0 -1
- package/lib/github-tool-handlers.d.ts +17 -0
- package/lib/{src/github-tool-handlers.js → github-tool-handlers.js} +14 -8
- package/lib/index.d.ts +9 -0
- package/lib/index.js +9 -0
- package/lib/oauth-users.d.ts +33 -0
- package/lib/oauth-users.js +38 -0
- package/lib/protocol.d.ts +87 -0
- package/lib/protocol.js +241 -0
- package/lib/types.d.ts +149 -0
- package/lib/{src/types.js → types.js} +0 -1
- package/lib/webhook.d.ts +13 -0
- package/lib/webhook.js +87 -0
- package/lib/workspace-manager.d.ts +21 -0
- package/lib/{src/workspace-manager.js → workspace-manager.js} +46 -1
- package/package.json +53 -26
- package/plugin.ts +52 -0
- package/schema.json +94 -0
- package/src/endpoint.ts +166 -175
- package/src/gh-client.ts +1 -1
- package/src/github-agent-deps.ts +66 -11
- package/src/github-tool-handlers.ts +20 -7
- package/src/index.ts +55 -137
- package/src/oauth-users.ts +47 -0
- package/src/protocol.ts +367 -0
- package/src/webhook.ts +125 -0
- package/src/workspace-manager.ts +54 -0
- package/lib/agent/tools/bind.js +0 -12
- package/lib/agent/tools/bind.js.map +0 -1
- package/lib/agent/tools/create_pr.js +0 -19
- package/lib/agent/tools/create_pr.js.map +0 -1
- package/lib/agent/tools/install.js +0 -12
- package/lib/agent/tools/install.js.map +0 -1
- package/lib/agent/tools/patch_file.js +0 -18
- package/lib/agent/tools/patch_file.js.map +0 -1
- package/lib/agent/tools/prepare_workspace.js +0 -14
- package/lib/agent/tools/prepare_workspace.js.map +0 -1
- package/lib/agent/tools/push_branch.js +0 -17
- package/lib/agent/tools/push_branch.js.map +0 -1
- package/lib/agent/tools/star.js +0 -15
- package/lib/agent/tools/star.js.map +0 -1
- package/lib/agent/tools/subscribe.js +0 -16
- package/lib/agent/tools/subscribe.js.map +0 -1
- package/lib/agent/tools/subscriptions.js +0 -13
- package/lib/agent/tools/subscriptions.js.map +0 -1
- package/lib/agent/tools/unbind.js +0 -12
- package/lib/agent/tools/unbind.js.map +0 -1
- package/lib/agent/tools/unsubscribe.js +0 -15
- package/lib/agent/tools/unsubscribe.js.map +0 -1
- package/lib/agent/tools/whoami.js +0 -12
- package/lib/agent/tools/whoami.js.map +0 -1
- package/lib/src/adapter.js +0 -423
- package/lib/src/adapter.js.map +0 -1
- package/lib/src/agent-prompt.js.map +0 -1
- package/lib/src/endpoint.js +0 -189
- package/lib/src/endpoint.js.map +0 -1
- package/lib/src/gh-client.js.map +0 -1
- package/lib/src/github-agent-deps.js +0 -13
- package/lib/src/github-agent-deps.js.map +0 -1
- package/lib/src/github-bot-handlers.js.map +0 -1
- package/lib/src/github-channel-context.js.map +0 -1
- package/lib/src/github-tool-handlers.js.map +0 -1
- package/lib/src/index.js +0 -104
- package/lib/src/index.js.map +0 -1
- package/lib/src/register-github-mcp.js +0 -41
- package/lib/src/register-github-mcp.js.map +0 -1
- package/lib/src/segment-mapper.js +0 -2
- package/lib/src/segment-mapper.js.map +0 -1
- package/lib/src/types.js.map +0 -1
- package/lib/src/workspace-manager.js.map +0 -1
- package/plugin.yml +0 -3
- package/src/adapter.ts +0 -454
- package/src/register-github-mcp.ts +0 -67
- package/src/segment-mapper.ts +0 -1
package/src/adapter.ts
DELETED
|
@@ -1,454 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GitHub 适配器(基于 gh CLI + App 认证 + Webhook/轮询混合)
|
|
3
|
-
*/
|
|
4
|
-
import { formatCompact, Adapter, Message, Plugin, OUTBOUND_RICH_SEGMENT_POLICY_TEXT_ONLY } from 'zhin.js';
|
|
5
|
-
import crypto from 'node:crypto';
|
|
6
|
-
import { GitHubEndpoint, enrichGithubInboundMessage } from './endpoint.js';
|
|
7
|
-
import type { Router } from '@zhin.js/host-router';
|
|
8
|
-
import { type GitHubEndpointConfig, type EventType, type GenericWebhookPayload, type Subscription, type IssueCommentPayload, type PRReviewCommentPayload, type PRReviewPayload } from './types.js';
|
|
9
|
-
import type { GhClient } from './gh-client.js';
|
|
10
|
-
|
|
11
|
-
const VALID_EVENTS: EventType[] = ['push', 'issue', 'star', 'fork', 'unstar', 'pull_request'];
|
|
12
|
-
|
|
13
|
-
function safeParseEvents(raw: any): EventType[] {
|
|
14
|
-
if (Array.isArray(raw)) return raw;
|
|
15
|
-
if (typeof raw === 'string') {
|
|
16
|
-
try { const parsed = JSON.parse(raw); if (Array.isArray(parsed)) return parsed; } catch {}
|
|
17
|
-
}
|
|
18
|
-
return [];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function formatNotification(event: string, p: GenericWebhookPayload): string {
|
|
22
|
-
const repo = p.repository.full_name;
|
|
23
|
-
const sender = p.sender.login;
|
|
24
|
-
const repoUrl = p.repository.html_url;
|
|
25
|
-
switch (event) {
|
|
26
|
-
case 'push': {
|
|
27
|
-
const branch = p.ref?.replace('refs/heads/', '') || '?';
|
|
28
|
-
const commits = p.commits || [];
|
|
29
|
-
const compareUrl = commits.length >= 2
|
|
30
|
-
? `${repoUrl}/compare/${commits[0].id.substring(0, 12)}...${commits[commits.length - 1].id.substring(0, 12)}`
|
|
31
|
-
: commits.length === 1 ? `${repoUrl}/commit/${commits[0].id}` : '';
|
|
32
|
-
let msg = `📦 ${repo}\n🌿 ${sender} pushed ${commits.length} commit(s) to \`${branch}\`\n`;
|
|
33
|
-
if (commits.length) {
|
|
34
|
-
msg += '\n';
|
|
35
|
-
msg += commits.slice(0, 5).map(c =>
|
|
36
|
-
` • [\`${c.id.substring(0, 7)}\`](${repoUrl}/commit/${c.id}) ${c.message.split('\n')[0]}`
|
|
37
|
-
).join('\n');
|
|
38
|
-
if (commits.length > 5) msg += `\n ... +${commits.length - 5} more`;
|
|
39
|
-
}
|
|
40
|
-
if (compareUrl) msg += `\n\n🔗 ${compareUrl}`;
|
|
41
|
-
return msg;
|
|
42
|
-
}
|
|
43
|
-
case 'issues': {
|
|
44
|
-
const i = p.issue!;
|
|
45
|
-
const act = p.action === 'opened' ? '📝 opened' : p.action === 'closed' ? '✅ closed' : `🔄 ${p.action || 'updated'}`;
|
|
46
|
-
let msg = `🐛 ${repo}\n👤 ${sender} ${act} issue #${i.number}\n📌 ${i.title}`;
|
|
47
|
-
msg += `\n🔗 ${i.html_url}`;
|
|
48
|
-
return msg;
|
|
49
|
-
}
|
|
50
|
-
case 'star': {
|
|
51
|
-
const starred = p.action !== 'deleted';
|
|
52
|
-
return `${starred ? '⭐' : '💔'} ${repo}\n👤 ${sender} ${starred ? 'starred' : 'unstarred'}\n🔗 ${repoUrl}`;
|
|
53
|
-
}
|
|
54
|
-
case 'fork':
|
|
55
|
-
return `🍴 ${repo}\n👤 ${sender} forked → ${p.forkee!.full_name}\n🔗 ${p.forkee!.html_url}`;
|
|
56
|
-
case 'pull_request': {
|
|
57
|
-
const pr = p.pull_request!;
|
|
58
|
-
const act = p.action === 'opened' ? '📝 opened'
|
|
59
|
-
: p.action === 'closed' ? (pr.state === 'closed' ? '❌ closed' : '✅ merged')
|
|
60
|
-
: `🔄 ${p.action || 'updated'}`;
|
|
61
|
-
let msg = `🔀 ${repo}\n👤 ${sender} ${act} PR #${pr.number}\n📌 ${pr.title}`;
|
|
62
|
-
msg += `\n🌿 ${pr.head.ref} → ${pr.base.ref}`;
|
|
63
|
-
msg += `\n🔗 ${pr.html_url}`;
|
|
64
|
-
return msg;
|
|
65
|
-
}
|
|
66
|
-
default:
|
|
67
|
-
return `📬 ${repo}\n📡 ${event}${p.action ? ` (${p.action})` : ''} by ${sender}\n🔗 ${repoUrl}`;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export class GitHubAdapter extends Adapter<GitHubEndpoint> {
|
|
72
|
-
static override readonly capabilities = ['inbound', 'outbound'] as const;
|
|
73
|
-
static override outboundRichSegmentPolicy = OUTBOUND_RICH_SEGMENT_POLICY_TEXT_ONLY;
|
|
74
|
-
static override interactivePolicy = 'text' as const;
|
|
75
|
-
|
|
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;
|
|
84
|
-
|
|
85
|
-
constructor(plugin: Plugin) {
|
|
86
|
-
super(plugin, 'github', []);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
createEndpoint(config: GitHubEndpointConfig): GitHubEndpoint {
|
|
90
|
-
return new GitHubEndpoint(this, config);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async start(): Promise<void> {
|
|
94
|
-
await super.start();
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async stop(): Promise<void> {
|
|
98
|
-
this.stopPolling();
|
|
99
|
-
await super.stop();
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** 获取第一个可用 Endpoint 的 GhClient (工具用) */
|
|
103
|
-
getAPI(): GhClient | null {
|
|
104
|
-
const endpoint = this.endpoints.values().next().value as GitHubEndpoint | undefined;
|
|
105
|
-
return endpoint?.gh || null;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** 获取指定用户绑定的 GhClient;未绑定则返回 null */
|
|
109
|
-
async getUserAPI(platform: string, platformUid: string): Promise<GhClient | null> {
|
|
110
|
-
const db = this.plugin.root?.inject('database');
|
|
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,若未绑定则降级为 Endpoint 默认的 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
|
-
/** 获取第一个 Endpoint 的 client_id(App 认证时从 /app 自动获取) */
|
|
130
|
-
getClientId(): string | null {
|
|
131
|
-
const endpoint = this.endpoints.values().next().value as GitHubEndpoint | undefined;
|
|
132
|
-
return endpoint?.gh.clientId || null;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** 获取第一个 Endpoint 的 host 配置 */
|
|
136
|
-
getHost(): string | undefined {
|
|
137
|
-
const endpoint = this.endpoints.values().next().value as GitHubEndpoint | undefined;
|
|
138
|
-
return endpoint?.$config.host;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/** 获取 App slug(用于生成安装链接) */
|
|
142
|
-
getAppSlug(): string | null {
|
|
143
|
-
const endpoint = this.endpoints.values().next().value as GitHubEndpoint | undefined;
|
|
144
|
-
return endpoint?.gh.appSlug || null;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** 获取所有已发现的安装信息 */
|
|
148
|
-
getInstallations() {
|
|
149
|
-
const endpoint = this.endpoints.values().next().value as GitHubEndpoint | undefined;
|
|
150
|
-
return endpoint?.gh.installations || [];
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/** 第一个 Endpoint 是否配置了 Webhook */
|
|
154
|
-
get hasWebhookConfig(): boolean {
|
|
155
|
-
const endpoint = this.endpoints.values().next().value as GitHubEndpoint | undefined;
|
|
156
|
-
return !!endpoint?.$config.webhook_secret;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/** Webhook 是否已激活 */
|
|
160
|
-
get webhookActive(): boolean {
|
|
161
|
-
return this._webhookActive;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// ── Webhook ──────────────────────────────────────────────────────
|
|
165
|
-
|
|
166
|
-
/** 在 router 上挂载 Webhook 路由(生产环境推荐) */
|
|
167
|
-
setupWebhook(router: Router): void {
|
|
168
|
-
const endpoint = this.endpoints.values().next().value as GitHubEndpoint | undefined;
|
|
169
|
-
if (!endpoint?.$config.webhook_secret) {
|
|
170
|
-
this.plugin.logger.warn(formatCompact( { op: 'webhook', ok: false, error: 'missing webhook_secret' }));
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
const secret = endpoint.$config.webhook_secret;
|
|
174
|
-
const path = endpoint.$config.webhook_path || '/github/webhook';
|
|
175
|
-
|
|
176
|
-
router.post(path, async (ctx) => {
|
|
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;
|
|
180
|
-
|
|
181
|
-
if (!signature || !event) {
|
|
182
|
-
ctx.status = 400;
|
|
183
|
-
ctx.body = { error: 'Missing signature or event header' };
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
|
|
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(formatCompact( { op: 'webhook', ok: false, error: 'invalid signature', delivery: deliveryId }));
|
|
192
|
-
ctx.status = 401;
|
|
193
|
-
ctx.body = { error: 'Invalid signature' };
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
|
|
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
|
-
);
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
this._webhookActive = true;
|
|
208
|
-
this.plugin.logger.debug(formatCompact( { op: 'webhook', path }));
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/** 处理 Webhook 推送的事件 */
|
|
212
|
-
async handleWebhookPayload(event: string, payload: any): Promise<void> {
|
|
213
|
-
const endpoint = this.endpoints.values().next().value as GitHubEndpoint | 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');
|
|
221
|
-
const eventsModel = db?.models?.get('github_events');
|
|
222
|
-
if (eventsModel) {
|
|
223
|
-
await eventsModel.insert({ id: Date.now(), repo, event_type: event, payload }).catch(() => {});
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// 处理消息类事件(Issue/PR 评论)
|
|
228
|
-
if (endpoint && event === 'issue_comment' && payload.action === 'created' && payload.comment) {
|
|
229
|
-
let message = endpoint.$formatMessage(payload as IssueCommentPayload);
|
|
230
|
-
message = enrichGithubInboundMessage(message, endpoint.$config, endpoint.gh, repo);
|
|
231
|
-
const botUser = endpoint.gh.authenticatedUser;
|
|
232
|
-
if (!(botUser && message.$sender.id === botUser)) {
|
|
233
|
-
this.emit('message.receive', message);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (endpoint && event === 'pull_request_review_comment' && payload.action === 'created' && payload.comment) {
|
|
238
|
-
let message = endpoint.formatPRReviewComment(payload as PRReviewCommentPayload);
|
|
239
|
-
message = enrichGithubInboundMessage(message, endpoint.$config, endpoint.gh, repo);
|
|
240
|
-
const botUser = endpoint.gh.authenticatedUser;
|
|
241
|
-
if (!(botUser && message.$sender.id === botUser)) {
|
|
242
|
-
this.emit('message.receive', message);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (endpoint && event === 'pull_request_review' && payload.action === 'submitted') {
|
|
247
|
-
const formatted = endpoint.formatPRReview(payload as PRReviewPayload);
|
|
248
|
-
if (formatted) {
|
|
249
|
-
let message = formatted;
|
|
250
|
-
message = enrichGithubInboundMessage(message, endpoint.$config, endpoint.gh, repo);
|
|
251
|
-
const botUser = endpoint.gh.authenticatedUser;
|
|
252
|
-
if (!(botUser && message.$sender.id === botUser)) {
|
|
253
|
-
this.emit('message.receive', message);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// 通知订阅者
|
|
259
|
-
if (repo) {
|
|
260
|
-
const genericPayload: GenericWebhookPayload = {
|
|
261
|
-
action: payload.action,
|
|
262
|
-
repository: payload.repository,
|
|
263
|
-
sender: payload.sender,
|
|
264
|
-
ref: payload.ref,
|
|
265
|
-
commits: payload.commits,
|
|
266
|
-
issue: payload.issue,
|
|
267
|
-
pull_request: payload.pull_request,
|
|
268
|
-
forkee: payload.forkee,
|
|
269
|
-
};
|
|
270
|
-
await this.dispatchNotification(event, genericPayload);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// ── 事件轮询 ────────────────────────────────────────────────────
|
|
275
|
-
|
|
276
|
-
/** 启动事件轮询 */
|
|
277
|
-
startPolling(): void {
|
|
278
|
-
if (this._pollTimer) return;
|
|
279
|
-
const endpoint = this.endpoints.values().next().value as GitHubEndpoint | undefined;
|
|
280
|
-
const interval = (endpoint?.$config.poll_interval || 60) * 1000;
|
|
281
|
-
this.plugin.logger.debug(formatCompact( { op: 'poll', interval_s: interval / 1000 }));
|
|
282
|
-
|
|
283
|
-
// 立即执行一次
|
|
284
|
-
this.pollAllRepos().catch(e => this.plugin.logger.error('轮询失败:', e));
|
|
285
|
-
|
|
286
|
-
this._pollTimer = setInterval(() => {
|
|
287
|
-
this.pollAllRepos().catch(e => this.plugin.logger.error('轮询失败:', e));
|
|
288
|
-
}, interval);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/** 停止事件轮询 */
|
|
292
|
-
stopPolling(): void {
|
|
293
|
-
if (this._pollTimer) {
|
|
294
|
-
clearInterval(this._pollTimer);
|
|
295
|
-
this._pollTimer = null;
|
|
296
|
-
this.plugin.logger.debug('GitHub 事件轮询已停止');
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/** 轮询所有已订阅仓库的事件 */
|
|
301
|
-
private async pollAllRepos(): Promise<void> {
|
|
302
|
-
const db = this.plugin.root?.inject('database');
|
|
303
|
-
const model = db?.models?.get('github_subscriptions');
|
|
304
|
-
if (!model) return;
|
|
305
|
-
|
|
306
|
-
// 获取所有不重复的订阅仓库
|
|
307
|
-
const allSubs = await model.select();
|
|
308
|
-
const repos = [...new Set((allSubs || []).map((s: Subscription) => s.repo))] as string[];
|
|
309
|
-
if (!repos.length) return;
|
|
310
|
-
|
|
311
|
-
const gh = this.getAPI();
|
|
312
|
-
if (!gh) return;
|
|
313
|
-
|
|
314
|
-
for (const repo of repos) {
|
|
315
|
-
try {
|
|
316
|
-
await this.pollRepoEvents(repo, gh);
|
|
317
|
-
} catch (e) {
|
|
318
|
-
this.plugin.logger.warn(formatCompact( { op: 'poll', repo, ok: false, error: String(e) }));
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/** 轮询单个仓库的事件 */
|
|
324
|
-
private async pollRepoEvents(repo: string, gh: GhClient): Promise<void> {
|
|
325
|
-
const etag = this._etags.get(repo);
|
|
326
|
-
const { events, etag: newEtag } = await gh.listRepoEvents(repo, etag);
|
|
327
|
-
if (newEtag) this._etags.set(repo, newEtag);
|
|
328
|
-
if (!events.length) return;
|
|
329
|
-
|
|
330
|
-
const lastId = this._lastEventIds.get(repo);
|
|
331
|
-
const newEvents: any[] = [];
|
|
332
|
-
for (const ev of events) {
|
|
333
|
-
if (ev.id === lastId) break;
|
|
334
|
-
newEvents.push(ev);
|
|
335
|
-
}
|
|
336
|
-
if (!newEvents.length) return;
|
|
337
|
-
|
|
338
|
-
// 记录最新事件 ID
|
|
339
|
-
this._lastEventIds.set(repo, events[0].id);
|
|
340
|
-
|
|
341
|
-
// 首次轮询只记录位置,不触发通知(避免启动时大量回溯)
|
|
342
|
-
if (!lastId) {
|
|
343
|
-
this.plugin.logger.debug(`${repo}: 首次轮询,记录位置 (${events[0].id}),跳过 ${events.length} 条历史事件`);
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
this.plugin.logger.debug(`${repo}: ${newEvents.length} 条新事件`);
|
|
348
|
-
|
|
349
|
-
const endpoint = this.endpoints.values().next().value as GitHubEndpoint | undefined;
|
|
350
|
-
|
|
351
|
-
// 按时间正序处理(API 返回倒序)
|
|
352
|
-
for (const ev of newEvents.reverse()) {
|
|
353
|
-
const eventName = this.mapEventType(ev.type);
|
|
354
|
-
if (!eventName) continue;
|
|
355
|
-
|
|
356
|
-
// 构造与 webhook payload 兼容的结构
|
|
357
|
-
const payload: GenericWebhookPayload = {
|
|
358
|
-
action: ev.payload?.action,
|
|
359
|
-
repository: ev.repo ? { full_name: ev.repo.name, html_url: `https://github.com/${ev.repo.name}`, description: '' } : ev.payload?.repository,
|
|
360
|
-
sender: { login: ev.actor?.login || '?', id: ev.actor?.id || 0, html_url: `https://github.com/${ev.actor?.login}` },
|
|
361
|
-
ref: ev.payload?.ref,
|
|
362
|
-
commits: ev.payload?.commits,
|
|
363
|
-
issue: ev.payload?.issue,
|
|
364
|
-
pull_request: ev.payload?.pull_request,
|
|
365
|
-
forkee: ev.payload?.forkee,
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
// 记录事件到数据库
|
|
369
|
-
const db = this.plugin.root?.inject('database');
|
|
370
|
-
const eventsModel = db?.models?.get('github_events');
|
|
371
|
-
if (eventsModel) {
|
|
372
|
-
await eventsModel.insert({ id: Date.now(), repo, event_type: eventName, payload: ev.payload }).catch(() => {});
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// 处理消息类事件(Issue/PR 评论)
|
|
376
|
-
if (endpoint && eventName === 'issue_comment' && ev.payload?.action === 'created' && ev.payload?.comment) {
|
|
377
|
-
let message = endpoint.$formatMessage(ev.payload as IssueCommentPayload);
|
|
378
|
-
message = enrichGithubInboundMessage(message, endpoint.$config, endpoint.gh, repo);
|
|
379
|
-
const botUser = endpoint.gh.authenticatedUser;
|
|
380
|
-
if (!(botUser && message.$sender.id === botUser)) {
|
|
381
|
-
this.emit('message.receive', message);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// 通知订阅者
|
|
386
|
-
await this.dispatchNotification(eventName, payload);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/** Events API type → webhook event name */
|
|
391
|
-
private mapEventType(type: string): string | null {
|
|
392
|
-
const map: Record<string, string> = {
|
|
393
|
-
PushEvent: 'push',
|
|
394
|
-
IssuesEvent: 'issues',
|
|
395
|
-
WatchEvent: 'star',
|
|
396
|
-
ForkEvent: 'fork',
|
|
397
|
-
PullRequestEvent: 'pull_request',
|
|
398
|
-
IssueCommentEvent: 'issue_comment',
|
|
399
|
-
PullRequestReviewEvent: 'pull_request_review',
|
|
400
|
-
PullRequestReviewCommentEvent: 'pull_request_review_comment',
|
|
401
|
-
};
|
|
402
|
-
return map[type] || null;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// ── 通知推送 ───────────────────────────────────────────────────────
|
|
406
|
-
|
|
407
|
-
async dispatchNotification(eventName: string, payload: GenericWebhookPayload): Promise<void> {
|
|
408
|
-
let eventType: EventType | null = null;
|
|
409
|
-
switch (eventName) {
|
|
410
|
-
case 'push': eventType = 'push'; break;
|
|
411
|
-
case 'issues': eventType = 'issue'; break;
|
|
412
|
-
case 'star': eventType = payload.action === 'deleted' ? 'unstar' : 'star'; break;
|
|
413
|
-
case 'fork': eventType = 'fork'; break;
|
|
414
|
-
case 'pull_request': eventType = 'pull_request'; break;
|
|
415
|
-
}
|
|
416
|
-
if (!eventType) {
|
|
417
|
-
this.plugin.logger.debug(`dispatchNotification: 未知事件 ${eventName},跳过`);
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
const repo = payload.repository.full_name;
|
|
422
|
-
const db = this.plugin.root?.inject('database');
|
|
423
|
-
const model = db?.models?.get('github_subscriptions');
|
|
424
|
-
if (!model) {
|
|
425
|
-
this.plugin.logger.warn(formatCompact( { op: 'notify', ok: false, error: 'subscriptions model not ready' }));
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
const subs = await model.select().where({ repo });
|
|
430
|
-
this.plugin.logger.debug(`dispatchNotification: ${repo} ${eventName}(${eventType}) — 找到 ${subs?.length || 0} 条订阅`);
|
|
431
|
-
if (!subs?.length) return;
|
|
432
|
-
|
|
433
|
-
const text = formatNotification(eventName, payload);
|
|
434
|
-
for (const sub of subs) {
|
|
435
|
-
const s = sub as Subscription;
|
|
436
|
-
const events = safeParseEvents(s.events);
|
|
437
|
-
if (!events.includes(eventType)) {
|
|
438
|
-
this.plugin.logger.debug(`dispatchNotification: ${s.adapter}:${s.target_id} 未订阅 ${eventType},跳过`);
|
|
439
|
-
continue;
|
|
440
|
-
}
|
|
441
|
-
try {
|
|
442
|
-
const targetAdapter = this.plugin.root?.inject(s.adapter as keyof Plugin.Contexts);
|
|
443
|
-
if (!(targetAdapter instanceof Adapter)) {
|
|
444
|
-
this.plugin.logger.warn(formatCompact( { op: 'notify', ok: false, adapter: s.adapter, error: 'no sendMessage' }));
|
|
445
|
-
continue;
|
|
446
|
-
}
|
|
447
|
-
this.plugin.logger.debug(formatCompact( { op: 'notify', event: eventType, adapter: s.adapter, endpoint: s.endpoint, target: s.target_id }));
|
|
448
|
-
await targetAdapter.sendMessage({ context: s.adapter, endpoint: s.endpoint, id: s.target_id, type: s.target_type, content: text });
|
|
449
|
-
} catch (e) {
|
|
450
|
-
this.plugin.logger.error(`通知推送失败 → ${s.adapter}:${s.target_id}`, e);
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Register @modelcontextprotocol/server-github when a PAT is available.
|
|
3
|
-
*/
|
|
4
|
-
import type { Plugin } from 'zhin.js';
|
|
5
|
-
import type { AIConfig } from 'zhin.js/ai';
|
|
6
|
-
import 'zhin.js/agent';
|
|
7
|
-
|
|
8
|
-
interface GithubMcpServerEntry {
|
|
9
|
-
name: string;
|
|
10
|
-
transport: 'stdio';
|
|
11
|
-
command: string;
|
|
12
|
-
args: string[];
|
|
13
|
-
env: Record<string, string>;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface AgentOrchestratorLike {
|
|
17
|
-
mcps: { has(name: string): boolean };
|
|
18
|
-
addMcp(config: GithubMcpServerEntry, scope?: object, source?: string): () => void;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function resolveGithubMcpToken(ai?: AIConfig): string | undefined {
|
|
22
|
-
const fromConfig = ai?.githubMcp?.token?.trim();
|
|
23
|
-
if (fromConfig) return fromConfig;
|
|
24
|
-
return (
|
|
25
|
-
process.env.GITHUB_PERSONAL_ACCESS_TOKEN?.trim() ||
|
|
26
|
-
process.env.GITHUB_TOKEN?.trim() ||
|
|
27
|
-
undefined
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function registerGithubMcp(plugin: Plugin): void {
|
|
32
|
-
const { useContext, root, logger } = plugin;
|
|
33
|
-
|
|
34
|
-
useContext('ai', (ai) => {
|
|
35
|
-
if (!ai?.isReady?.()) return;
|
|
36
|
-
|
|
37
|
-
const orchestrator = root.inject('agent') as AgentOrchestratorLike | undefined;
|
|
38
|
-
if (!orchestrator) return;
|
|
39
|
-
|
|
40
|
-
if (orchestrator.mcps.has('github')) return;
|
|
41
|
-
|
|
42
|
-
const configService = root.inject('config');
|
|
43
|
-
const appConfig = configService?.getPrimary<{ ai?: AIConfig }>() || {};
|
|
44
|
-
const githubMcp = appConfig.ai?.githubMcp as { token?: string; enabled?: boolean } | undefined;
|
|
45
|
-
if (githubMcp?.enabled !== true) {
|
|
46
|
-
logger.debug('[MCP] server-github skipped: set ai.githubMcp.enabled=true to opt in (PAT / human identity)');
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
const token = resolveGithubMcpToken(appConfig.ai);
|
|
50
|
-
if (!token) {
|
|
51
|
-
logger.debug('[MCP] server-github skipped: set GITHUB_PERSONAL_ACCESS_TOKEN or ai.githubMcp.token');
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return orchestrator.addMcp(
|
|
56
|
-
{
|
|
57
|
-
name: 'github',
|
|
58
|
-
transport: 'stdio',
|
|
59
|
-
command: 'npx',
|
|
60
|
-
args: ['-y', '@modelcontextprotocol/server-github'],
|
|
61
|
-
env: { GITHUB_PERSONAL_ACCESS_TOKEN: token },
|
|
62
|
-
},
|
|
63
|
-
{},
|
|
64
|
-
'adapter-github',
|
|
65
|
-
);
|
|
66
|
-
});
|
|
67
|
-
}
|
package/src/segment-mapper.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { toCanonicalSegments, fromCanonicalSegments } from 'zhin.js';
|