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