@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/index.ts
CHANGED
|
@@ -3,17 +3,18 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { usePlugin, type Plugin, type Context, type ToolFeature, type Tool, type ToolContext } from 'zhin.js';
|
|
5
5
|
import { GitHubAdapter } from './adapter.js';
|
|
6
|
+
import { GhClient } from './gh-client.js';
|
|
6
7
|
import type { EventType } from './types.js';
|
|
7
8
|
|
|
8
9
|
declare module 'zhin.js' {
|
|
10
|
+
interface Adapters {
|
|
11
|
+
github: GitHubAdapter;
|
|
12
|
+
}
|
|
9
13
|
namespace Plugin {
|
|
10
14
|
interface Contexts {
|
|
11
15
|
router: import('@zhin.js/http').Router;
|
|
12
16
|
}
|
|
13
17
|
}
|
|
14
|
-
interface Adapters {
|
|
15
|
-
github: GitHubAdapter;
|
|
16
|
-
}
|
|
17
18
|
interface Models {
|
|
18
19
|
github_subscriptions: {
|
|
19
20
|
id: number;
|
|
@@ -37,6 +38,7 @@ declare module 'zhin.js' {
|
|
|
37
38
|
export * from './types.js';
|
|
38
39
|
export { GitHubBot, parseMarkdown, toMarkdown } from './bot.js';
|
|
39
40
|
export { GitHubAdapter } from './adapter.js';
|
|
41
|
+
export { GhClient } from './gh-client.js';
|
|
40
42
|
|
|
41
43
|
const plugin = usePlugin();
|
|
42
44
|
const { provide, defineModel, useContext, logger } = plugin;
|
|
@@ -63,16 +65,13 @@ defineModel('github_oauth_users', {
|
|
|
63
65
|
platform: { type: 'text', nullable: false },
|
|
64
66
|
platform_uid: { type: 'text', nullable: false },
|
|
65
67
|
github_login: { type: 'text', nullable: false },
|
|
66
|
-
github_id: { type: 'integer', nullable: false },
|
|
67
68
|
access_token: { type: 'text', nullable: false },
|
|
68
|
-
|
|
69
|
-
created_at: { type: 'date', nullable: false },
|
|
70
|
-
updated_at: { type: 'date', nullable: false },
|
|
69
|
+
created_at: { type: 'integer', default: 0 },
|
|
71
70
|
});
|
|
72
71
|
|
|
73
72
|
provide({
|
|
74
73
|
name: 'github',
|
|
75
|
-
description: 'GitHub Adapter — Issues/PRs as chat channels, full repo management via
|
|
74
|
+
description: 'GitHub Adapter — Issues/PRs as chat channels, full repo management via gh CLI',
|
|
76
75
|
mounted: async (p: Plugin) => {
|
|
77
76
|
const adapter = new GitHubAdapter(p);
|
|
78
77
|
await adapter.start();
|
|
@@ -83,9 +82,28 @@ provide({
|
|
|
83
82
|
},
|
|
84
83
|
} as Context<'github'>);
|
|
85
84
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
adapter.
|
|
85
|
+
// 混合模式:有 router + webhook_secret → Webhook 实时;否则 → 轮询降级
|
|
86
|
+
useContext('github', (adapter) => {
|
|
87
|
+
if (adapter.hasWebhookConfig) {
|
|
88
|
+
// 尝试注册 Webhook(需要 router Context)
|
|
89
|
+
const router = plugin.inject('router' as any) as any;
|
|
90
|
+
if (router) {
|
|
91
|
+
adapter.setupWebhook(router);
|
|
92
|
+
logger.info('GitHub 事件源: Webhook (实时)');
|
|
93
|
+
} else {
|
|
94
|
+
// router 还没就绪,等它挂载后再注册
|
|
95
|
+
plugin.useContext('router' as any, (r: any) => {
|
|
96
|
+
adapter.setupWebhook(r);
|
|
97
|
+
logger.info('GitHub 事件源: Webhook (实时, 延迟注册)');
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Webhook 未配置或未激活时,总是启动轮询作为兜底
|
|
102
|
+
if (!adapter.webhookActive) {
|
|
103
|
+
adapter.startPolling();
|
|
104
|
+
logger.info('GitHub 事件源: 轮询');
|
|
105
|
+
}
|
|
106
|
+
return () => adapter.stopPolling();
|
|
89
107
|
});
|
|
90
108
|
|
|
91
109
|
// ── Tool 工具注册 ─────────────────────────────────────────────────────────
|
|
@@ -113,8 +131,8 @@ useContext('tool', 'github', (toolService: ToolFeature, adapter: GitHubAdapter)
|
|
|
113
131
|
},
|
|
114
132
|
platforms: ['github'],
|
|
115
133
|
tags: ['github'],
|
|
116
|
-
execute: async (args: Record<string, any
|
|
117
|
-
const api = adapter.
|
|
134
|
+
execute: async (args: Record<string, any>, context?: ToolContext) => {
|
|
135
|
+
const api = await adapter.getUserOrDefaultAPI(context?.platform, context?.senderId);
|
|
118
136
|
if (!api) return '❌ 没有可用的 GitHub bot';
|
|
119
137
|
const { action, repo, number: num, title, body, head, base, state, approve, method } = args;
|
|
120
138
|
switch (action) {
|
|
@@ -191,8 +209,8 @@ useContext('tool', 'github', (toolService: ToolFeature, adapter: GitHubAdapter)
|
|
|
191
209
|
},
|
|
192
210
|
platforms: ['github'],
|
|
193
211
|
tags: ['github'],
|
|
194
|
-
execute: async (args: Record<string, any
|
|
195
|
-
const api = adapter.
|
|
212
|
+
execute: async (args: Record<string, any>, context?: ToolContext) => {
|
|
213
|
+
const api = await adapter.getUserOrDefaultAPI(context?.platform, context?.senderId);
|
|
196
214
|
if (!api) return '❌ 没有可用的 GitHub bot';
|
|
197
215
|
const { action, repo, number: num, title, body, labels, state } = args;
|
|
198
216
|
switch (action) {
|
|
@@ -255,8 +273,8 @@ useContext('tool', 'github', (toolService: ToolFeature, adapter: GitHubAdapter)
|
|
|
255
273
|
},
|
|
256
274
|
platforms: ['github'],
|
|
257
275
|
tags: ['github'],
|
|
258
|
-
execute: async (args: Record<string, any
|
|
259
|
-
const api = adapter.
|
|
276
|
+
execute: async (args: Record<string, any>, context?: ToolContext) => {
|
|
277
|
+
const api = await adapter.getUserOrDefaultAPI(context?.platform, context?.senderId);
|
|
260
278
|
if (!api) return '❌ 没有可用的 GitHub bot';
|
|
261
279
|
const { action, repo, limit: lim } = args;
|
|
262
280
|
const limit = lim || 10;
|
|
@@ -324,8 +342,8 @@ useContext('tool', 'github', (toolService: ToolFeature, adapter: GitHubAdapter)
|
|
|
324
342
|
},
|
|
325
343
|
platforms: ['github'],
|
|
326
344
|
tags: ['github'],
|
|
327
|
-
execute: async (args: Record<string, any
|
|
328
|
-
const api = adapter.
|
|
345
|
+
execute: async (args: Record<string, any>, context?: ToolContext) => {
|
|
346
|
+
const api = await adapter.getUserOrDefaultAPI(context?.platform, context?.senderId);
|
|
329
347
|
if (!api) return '❌ 没有可用的 GitHub bot';
|
|
330
348
|
const { action, query: q, limit: lim } = args;
|
|
331
349
|
const limit = lim || 10;
|
|
@@ -377,8 +395,8 @@ useContext('tool', 'github', (toolService: ToolFeature, adapter: GitHubAdapter)
|
|
|
377
395
|
},
|
|
378
396
|
platforms: ['github'],
|
|
379
397
|
tags: ['github'],
|
|
380
|
-
execute: async (args: Record<string, any
|
|
381
|
-
const api = adapter.
|
|
398
|
+
execute: async (args: Record<string, any>, context?: ToolContext) => {
|
|
399
|
+
const api = await adapter.getUserOrDefaultAPI(context?.platform, context?.senderId);
|
|
382
400
|
if (!api) return '❌ 没有可用的 GitHub bot';
|
|
383
401
|
const { action, repo, number: num, labels } = args;
|
|
384
402
|
switch (action) {
|
|
@@ -427,8 +445,8 @@ useContext('tool', 'github', (toolService: ToolFeature, adapter: GitHubAdapter)
|
|
|
427
445
|
},
|
|
428
446
|
platforms: ['github'],
|
|
429
447
|
tags: ['github'],
|
|
430
|
-
execute: async (args: Record<string, any
|
|
431
|
-
const api = adapter.
|
|
448
|
+
execute: async (args: Record<string, any>, context?: ToolContext) => {
|
|
449
|
+
const api = await adapter.getUserOrDefaultAPI(context?.platform, context?.senderId);
|
|
432
450
|
if (!api) return '❌ 没有可用的 GitHub bot';
|
|
433
451
|
const { action, repo, number: num, assignees } = args;
|
|
434
452
|
const assigneeArr = assignees.split(',').map((s: string) => s.trim());
|
|
@@ -456,8 +474,8 @@ useContext('tool', 'github', (toolService: ToolFeature, adapter: GitHubAdapter)
|
|
|
456
474
|
},
|
|
457
475
|
platforms: ['github'],
|
|
458
476
|
tags: ['github'],
|
|
459
|
-
execute: async (args: Record<string, any
|
|
460
|
-
const api = adapter.
|
|
477
|
+
execute: async (args: Record<string, any>, context?: ToolContext) => {
|
|
478
|
+
const api = await adapter.getUserOrDefaultAPI(context?.platform, context?.senderId);
|
|
461
479
|
if (!api) return '❌ 没有可用的 GitHub bot';
|
|
462
480
|
const r = await api.getFileContent(args.repo, args.path, args.ref);
|
|
463
481
|
if (!r.ok) return `❌ ${r.data?.message || JSON.stringify(r.data)}`;
|
|
@@ -493,8 +511,8 @@ useContext('tool', 'github', (toolService: ToolFeature, adapter: GitHubAdapter)
|
|
|
493
511
|
},
|
|
494
512
|
platforms: ['github'],
|
|
495
513
|
tags: ['github'],
|
|
496
|
-
execute: async (args: Record<string, any
|
|
497
|
-
const api = adapter.
|
|
514
|
+
execute: async (args: Record<string, any>, context?: ToolContext) => {
|
|
515
|
+
const api = await adapter.getUserOrDefaultAPI(context?.platform, context?.senderId);
|
|
498
516
|
if (!api) return '❌ 没有可用的 GitHub bot';
|
|
499
517
|
const { action, repo, sha, path, base, head, limit: lim } = args;
|
|
500
518
|
if (action === 'list') {
|
|
@@ -538,8 +556,8 @@ useContext('tool', 'github', (toolService: ToolFeature, adapter: GitHubAdapter)
|
|
|
538
556
|
},
|
|
539
557
|
platforms: ['github'],
|
|
540
558
|
tags: ['github'],
|
|
541
|
-
execute: async (args: Record<string, any
|
|
542
|
-
const api = adapter.
|
|
559
|
+
execute: async (args: Record<string, any>, context?: ToolContext) => {
|
|
560
|
+
const api = await adapter.getUserOrDefaultAPI(context?.platform, context?.senderId);
|
|
543
561
|
if (!api) return '❌ 没有可用的 GitHub bot';
|
|
544
562
|
const { type: itemType, repo, number: num, title, body, state } = args;
|
|
545
563
|
const data: any = {};
|
|
@@ -557,7 +575,7 @@ useContext('tool', 'github', (toolService: ToolFeature, adapter: GitHubAdapter)
|
|
|
557
575
|
// --- Star ---
|
|
558
576
|
{
|
|
559
577
|
name: 'github_star',
|
|
560
|
-
description: 'Star 或取消 Star 一个 GitHub
|
|
578
|
+
description: 'Star 或取消 Star 一个 GitHub 仓库(使用你绑定的 GitHub 账号,未绑定则用 Bot 默认账号)',
|
|
561
579
|
parameters: {
|
|
562
580
|
type: 'object' as const,
|
|
563
581
|
properties: {
|
|
@@ -569,22 +587,21 @@ useContext('tool', 'github', (toolService: ToolFeature, adapter: GitHubAdapter)
|
|
|
569
587
|
platforms: ['github'],
|
|
570
588
|
tags: ['github'],
|
|
571
589
|
execute: async (args: Record<string, any>, context?: ToolContext) => {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
if (!client) return '❌ 你还没有绑定 GitHub 账号,请先使用 github_bind';
|
|
590
|
+
const gh = await adapter.getUserOrDefaultAPI(context?.platform, context?.senderId);
|
|
591
|
+
if (!gh) return '❌ 没有可用的 GitHub bot';
|
|
575
592
|
const { action, repo } = args;
|
|
576
593
|
switch (action) {
|
|
577
594
|
case 'star': {
|
|
578
|
-
const r = await
|
|
579
|
-
return r.ok
|
|
595
|
+
const r = await gh.starRepo(repo);
|
|
596
|
+
return r.ok ? `⭐ 已 Star ${repo}` : `❌ ${r.data?.message || JSON.stringify(r.data)}`;
|
|
580
597
|
}
|
|
581
598
|
case 'unstar': {
|
|
582
|
-
const r = await
|
|
583
|
-
return r.ok
|
|
599
|
+
const r = await gh.unstarRepo(repo);
|
|
600
|
+
return r.ok ? `💔 已取消 Star ${repo}` : `❌ ${r.data?.message || JSON.stringify(r.data)}`;
|
|
584
601
|
}
|
|
585
602
|
case 'check': {
|
|
586
|
-
const starred = await
|
|
587
|
-
return starred ? `⭐
|
|
603
|
+
const starred = await gh.isStarred(repo);
|
|
604
|
+
return starred ? `⭐ 已 Star ${repo}` : `☆ 尚未 Star ${repo}`;
|
|
588
605
|
}
|
|
589
606
|
default: return `❌ 未知操作: ${action}`;
|
|
590
607
|
}
|
|
@@ -593,7 +610,7 @@ useContext('tool', 'github', (toolService: ToolFeature, adapter: GitHubAdapter)
|
|
|
593
610
|
// --- Fork ---
|
|
594
611
|
{
|
|
595
612
|
name: 'github_fork',
|
|
596
|
-
description: 'Fork 一个 GitHub
|
|
613
|
+
description: 'Fork 一个 GitHub 仓库(使用你绑定的 GitHub 账号,未绑定则用 Bot 默认账号)',
|
|
597
614
|
parameters: {
|
|
598
615
|
type: 'object' as const,
|
|
599
616
|
properties: {
|
|
@@ -604,14 +621,171 @@ useContext('tool', 'github', (toolService: ToolFeature, adapter: GitHubAdapter)
|
|
|
604
621
|
platforms: ['github'],
|
|
605
622
|
tags: ['github'],
|
|
606
623
|
execute: async (args: Record<string, any>, context?: ToolContext) => {
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
const r = await client.forkRepo(args.repo);
|
|
624
|
+
const gh = await adapter.getUserOrDefaultAPI(context?.platform, context?.senderId);
|
|
625
|
+
if (!gh) return '❌ 没有可用的 GitHub bot';
|
|
626
|
+
const r = await gh.forkRepo(args.repo);
|
|
611
627
|
if (!r.ok) return `❌ ${r.data?.message || JSON.stringify(r.data)}`;
|
|
612
628
|
return `🍴 已 Fork ${args.repo} → ${r.data.full_name}\n🔗 ${r.data.html_url}`;
|
|
613
629
|
},
|
|
614
630
|
},
|
|
631
|
+
// --- Bind (Device Flow) ---
|
|
632
|
+
{
|
|
633
|
+
name: 'github_bind',
|
|
634
|
+
description: '绑定你的 GitHub 账号 — 使用 Device Flow 授权,无需输入密码',
|
|
635
|
+
parameters: {
|
|
636
|
+
type: 'object' as const,
|
|
637
|
+
properties: {},
|
|
638
|
+
},
|
|
639
|
+
tags: ['github'],
|
|
640
|
+
execute: async (_args: Record<string, any>, context?: ToolContext) => {
|
|
641
|
+
if (!context?.platform || !context?.senderId) {
|
|
642
|
+
return '❌ 无法获取当前用户信息';
|
|
643
|
+
}
|
|
644
|
+
const clientId = adapter.getClientId();
|
|
645
|
+
if (!clientId) return '❌ Bot 未配置 GitHub App 或 App 无 client_id,无法进行账号绑定';
|
|
646
|
+
|
|
647
|
+
const db = plugin.root?.inject('database') as any;
|
|
648
|
+
const model = db?.models?.get('github_oauth_users');
|
|
649
|
+
if (!model) return '❌ 数据库未就绪';
|
|
650
|
+
|
|
651
|
+
// 检查是否已绑定
|
|
652
|
+
const [existing] = await model.select().where({ platform: context.platform, platform_uid: context.senderId });
|
|
653
|
+
if (existing) {
|
|
654
|
+
return `⚠️ 你已绑定 GitHub 账号: ${existing.github_login}\n如需重新绑定,请先执行 github_unbind`;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
try {
|
|
658
|
+
const host = adapter.getHost();
|
|
659
|
+
const codeResp = await GhClient.deviceFlowRequestCode(clientId, host);
|
|
660
|
+
// 异步轮询 token(最多等 codeResp.expires_in 秒)
|
|
661
|
+
const tokenPromise = GhClient.deviceFlowPollToken(
|
|
662
|
+
clientId, codeResp.device_code, codeResp.interval, codeResp.expires_in, host,
|
|
663
|
+
);
|
|
664
|
+
|
|
665
|
+
// 先回复用户授权链接
|
|
666
|
+
const replyMsg = [
|
|
667
|
+
`🔗 请在浏览器中打开以下链接进行授权:`,
|
|
668
|
+
` ${codeResp.verification_uri}`,
|
|
669
|
+
``,
|
|
670
|
+
`📋 输入验证码: **${codeResp.user_code}**`,
|
|
671
|
+
``,
|
|
672
|
+
`⏳ 等待授权中…(${Math.floor(codeResp.expires_in / 60)} 分钟内有效)`,
|
|
673
|
+
].join('\n');
|
|
674
|
+
|
|
675
|
+
// 在后台等待用户授权完成
|
|
676
|
+
tokenPromise.then(async (tokenData) => {
|
|
677
|
+
if (!tokenData) {
|
|
678
|
+
// 授权超时或被拒绝 — 由于 execute 已经返回了,这里只能通过日志记录
|
|
679
|
+
logger.warn(`GitHub Device Flow 超时/拒绝: ${context.platform}:${context.senderId}`);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// 使用 token 获取 GitHub 用户名
|
|
684
|
+
const userGh = new GhClient({ host, token: tokenData.access_token });
|
|
685
|
+
const authResult = await userGh.verifyAuth();
|
|
686
|
+
const login = authResult.ok ? authResult.user : 'unknown';
|
|
687
|
+
|
|
688
|
+
await model.insert({
|
|
689
|
+
id: Date.now(),
|
|
690
|
+
platform: context.platform,
|
|
691
|
+
platform_uid: context.senderId,
|
|
692
|
+
github_login: login,
|
|
693
|
+
access_token: tokenData.access_token,
|
|
694
|
+
created_at: Date.now(),
|
|
695
|
+
});
|
|
696
|
+
logger.info(`GitHub 账号绑定成功: ${context.platform}:${context.senderId} → ${login}`);
|
|
697
|
+
|
|
698
|
+
// 尝试回复绑定成功消息
|
|
699
|
+
if (context.message?.$reply) {
|
|
700
|
+
await context.message.$reply(`✅ GitHub 账号绑定成功!\n👤 ${login}`);
|
|
701
|
+
}
|
|
702
|
+
}).catch(err => {
|
|
703
|
+
logger.error('GitHub Device Flow 错误:', err);
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
return replyMsg;
|
|
707
|
+
} catch (e: any) {
|
|
708
|
+
return `❌ Device Flow 启动失败: ${e.message}`;
|
|
709
|
+
}
|
|
710
|
+
},
|
|
711
|
+
},
|
|
712
|
+
// --- Unbind ---
|
|
713
|
+
{
|
|
714
|
+
name: 'github_unbind',
|
|
715
|
+
description: '解除你绑定的 GitHub 账号',
|
|
716
|
+
parameters: {
|
|
717
|
+
type: 'object' as const,
|
|
718
|
+
properties: {},
|
|
719
|
+
},
|
|
720
|
+
tags: ['github'],
|
|
721
|
+
execute: async (_args: Record<string, any>, context?: ToolContext) => {
|
|
722
|
+
if (!context?.platform || !context?.senderId) {
|
|
723
|
+
return '❌ 无法获取当前用户信息';
|
|
724
|
+
}
|
|
725
|
+
const db = plugin.root?.inject('database') as any;
|
|
726
|
+
const model = db?.models?.get('github_oauth_users');
|
|
727
|
+
if (!model) return '❌ 数据库未就绪';
|
|
728
|
+
|
|
729
|
+
const [existing] = await model.select().where({ platform: context.platform, platform_uid: context.senderId });
|
|
730
|
+
if (!existing) return '📭 你尚未绑定 GitHub 账号';
|
|
731
|
+
|
|
732
|
+
await model.delete().where({ id: existing.id });
|
|
733
|
+
return `✅ 已解除 GitHub 账号绑定: ${existing.github_login}`;
|
|
734
|
+
},
|
|
735
|
+
},
|
|
736
|
+
// --- Whoami ---
|
|
737
|
+
{
|
|
738
|
+
name: 'github_whoami',
|
|
739
|
+
description: '查看你绑定的 GitHub 账号信息',
|
|
740
|
+
parameters: {
|
|
741
|
+
type: 'object' as const,
|
|
742
|
+
properties: {},
|
|
743
|
+
},
|
|
744
|
+
tags: ['github'],
|
|
745
|
+
execute: async (_args: Record<string, any>, context?: ToolContext) => {
|
|
746
|
+
if (!context?.platform || !context?.senderId) {
|
|
747
|
+
return '❌ 无法获取当前用户信息';
|
|
748
|
+
}
|
|
749
|
+
const db = plugin.root?.inject('database') as any;
|
|
750
|
+
const model = db?.models?.get('github_oauth_users');
|
|
751
|
+
if (!model) return '❌ 数据库未就绪';
|
|
752
|
+
|
|
753
|
+
const [existing] = await model.select().where({ platform: context.platform, platform_uid: context.senderId });
|
|
754
|
+
if (!existing) return '📭 你尚未绑定 GitHub 账号\n🔗 使用 github_bind 绑定你的账号';
|
|
755
|
+
|
|
756
|
+
// 验证 token 是否仍然有效
|
|
757
|
+
const userGh = new GhClient({ host: adapter.getHost(), token: existing.access_token });
|
|
758
|
+
const auth = await userGh.verifyAuth();
|
|
759
|
+
if (auth.ok) {
|
|
760
|
+
return `👤 已绑定 GitHub 账号: ${auth.user}\n📅 绑定时间: ${new Date(existing.created_at).toLocaleString('zh-CN')}`;
|
|
761
|
+
}
|
|
762
|
+
return `⚠️ 已绑定账号 ${existing.github_login},但 Token 已失效\n🔗 请执行 github_unbind 后重新 github_bind`;
|
|
763
|
+
},
|
|
764
|
+
},
|
|
765
|
+
// --- Install App ---
|
|
766
|
+
{
|
|
767
|
+
name: 'github_install',
|
|
768
|
+
description: '获取安装 GitHub App 的链接 — 安装后 Bot 可以访问你的仓库,你也可以使用更多功能',
|
|
769
|
+
parameters: {
|
|
770
|
+
type: 'object' as const,
|
|
771
|
+
properties: {},
|
|
772
|
+
},
|
|
773
|
+
tags: ['github'],
|
|
774
|
+
execute: async () => {
|
|
775
|
+
const slug = adapter.getAppSlug();
|
|
776
|
+
if (!slug) return '❌ Bot 未配置 GitHub App';
|
|
777
|
+
const host = adapter.getHost() || 'github.com';
|
|
778
|
+
const installations = adapter.getInstallations();
|
|
779
|
+
let msg = `🔗 请点击以下链接安装 GitHub App 到你的仓库:\n https://${host}/apps/${slug}/installations/new`;
|
|
780
|
+
if (installations.length) {
|
|
781
|
+
msg += `\n\n📋 当前已安装 (${installations.length}):`;
|
|
782
|
+
for (const inst of installations) {
|
|
783
|
+
msg += `\n • ${inst.account.login} (${inst.account.type})`;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return msg;
|
|
787
|
+
},
|
|
788
|
+
},
|
|
615
789
|
// --- Subscribe ---
|
|
616
790
|
{
|
|
617
791
|
name: 'github_subscribe',
|
|
@@ -728,84 +902,6 @@ useContext('tool', 'github', (toolService: ToolFeature, adapter: GitHubAdapter)
|
|
|
728
902
|
}).join('\n\n');
|
|
729
903
|
},
|
|
730
904
|
},
|
|
731
|
-
// --- Bind ---
|
|
732
|
-
{
|
|
733
|
-
name: 'github_bind',
|
|
734
|
-
description: '绑定 GitHub 账号,生成 OAuth 授权链接',
|
|
735
|
-
parameters: {
|
|
736
|
-
type: 'object' as const,
|
|
737
|
-
properties: {},
|
|
738
|
-
},
|
|
739
|
-
platforms: ['github'],
|
|
740
|
-
tags: ['github'],
|
|
741
|
-
execute: async (_args: Record<string, any>, context?: ToolContext) => {
|
|
742
|
-
if (!context?.platform || !context?.senderId) return '❌ 无法获取用户信息';
|
|
743
|
-
|
|
744
|
-
// 检查是否已绑定
|
|
745
|
-
const db = plugin.root?.inject('database') as any;
|
|
746
|
-
const model = db?.models?.get('github_oauth_users');
|
|
747
|
-
if (model) {
|
|
748
|
-
const [existing] = await model.select().where({ platform: context.platform, platform_uid: context.senderId });
|
|
749
|
-
if (existing) return `✅ 你已绑定 GitHub 账号: ${existing.github_login}\n如需重新绑定,请先使用 github_unbind 解绑`;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
const url = adapter.createOAuthState(context.platform, context.senderId);
|
|
753
|
-
if (!url) return '❌ GitHub App 未配置 OAuth (缺少 client_id)';
|
|
754
|
-
|
|
755
|
-
return `🔗 请点击以下链接绑定你的 GitHub 账号:\n${url}\n\n链接有效期 5 分钟`;
|
|
756
|
-
},
|
|
757
|
-
},
|
|
758
|
-
// --- Unbind ---
|
|
759
|
-
{
|
|
760
|
-
name: 'github_unbind',
|
|
761
|
-
description: '解绑 GitHub 账号',
|
|
762
|
-
parameters: {
|
|
763
|
-
type: 'object' as const,
|
|
764
|
-
properties: {},
|
|
765
|
-
},
|
|
766
|
-
platforms: ['github'],
|
|
767
|
-
tags: ['github'],
|
|
768
|
-
execute: async (_args: Record<string, any>, context?: ToolContext) => {
|
|
769
|
-
if (!context?.platform || !context?.senderId) return '❌ 无法获取用户信息';
|
|
770
|
-
const db = plugin.root?.inject('database') as any;
|
|
771
|
-
const model = db?.models?.get('github_oauth_users');
|
|
772
|
-
if (!model) return '❌ 数据库未就绪';
|
|
773
|
-
|
|
774
|
-
const [existing] = await model.select().where({ platform: context.platform, platform_uid: context.senderId });
|
|
775
|
-
if (!existing) return '📭 你还没有绑定 GitHub 账号';
|
|
776
|
-
|
|
777
|
-
await model.delete().where({ id: existing.id });
|
|
778
|
-
return `✅ 已解绑 GitHub 账号: ${existing.github_login}`;
|
|
779
|
-
},
|
|
780
|
-
},
|
|
781
|
-
// --- Whoami ---
|
|
782
|
-
{
|
|
783
|
-
name: 'github_whoami',
|
|
784
|
-
description: '查看当前绑定的 GitHub 账号信息',
|
|
785
|
-
parameters: {
|
|
786
|
-
type: 'object' as const,
|
|
787
|
-
properties: {},
|
|
788
|
-
},
|
|
789
|
-
platforms: ['github'],
|
|
790
|
-
tags: ['github'],
|
|
791
|
-
execute: async (_args: Record<string, any>, context?: ToolContext) => {
|
|
792
|
-
if (!context?.platform || !context?.senderId) return '❌ 无法获取用户信息';
|
|
793
|
-
const db = plugin.root?.inject('database') as any;
|
|
794
|
-
const model = db?.models?.get('github_oauth_users');
|
|
795
|
-
if (!model) return '❌ 数据库未就绪';
|
|
796
|
-
|
|
797
|
-
const [row] = await model.select().where({ platform: context.platform, platform_uid: context.senderId });
|
|
798
|
-
if (!row) return '📭 你还没有绑定 GitHub 账号,使用 github_bind 进行绑定';
|
|
799
|
-
|
|
800
|
-
return [
|
|
801
|
-
`🐙 GitHub 账号信息`,
|
|
802
|
-
`👤 用户名: ${row.github_login}`,
|
|
803
|
-
`🆔 GitHub ID: ${row.github_id}`,
|
|
804
|
-
`📅 绑定时间: ${row.created_at instanceof Date ? row.created_at.toLocaleDateString() : String(row.created_at).split('T')[0]}`,
|
|
805
|
-
`🔑 权限: ${row.scope || '(默认)'}`,
|
|
806
|
-
].join('\n');
|
|
807
|
-
},
|
|
808
|
-
},
|
|
809
905
|
];
|
|
810
906
|
|
|
811
907
|
const disposers = tools.map(t => toolService.addTool(t, plugin.name));
|
|
@@ -814,4 +910,4 @@ useContext('tool', 'github', (toolService: ToolFeature, adapter: GitHubAdapter)
|
|
|
814
910
|
return () => disposers.forEach(d => d());
|
|
815
911
|
});
|
|
816
912
|
|
|
817
|
-
logger.debug('GitHub 适配器已加载 (
|
|
913
|
+
logger.debug('GitHub 适配器已加载 (gh CLI 认证)');
|
package/src/types.ts
CHANGED
|
@@ -1,45 +1,23 @@
|
|
|
1
1
|
// ── Bot 配置 ─────────────────────────────────────────────────────────
|
|
2
|
-
//
|
|
3
|
-
//
|
|
2
|
+
// 基于 gh CLI 认证:需要系统已安装并认证 gh CLI
|
|
3
|
+
// 认证方式:gh auth login
|
|
4
4
|
|
|
5
5
|
export interface GitHubBotConfig {
|
|
6
6
|
context: 'github';
|
|
7
7
|
/** Bot 标识名称 */
|
|
8
8
|
name: string;
|
|
9
|
+
/** GitHub Enterprise 主机名(默认 github.com) */
|
|
10
|
+
host?: string;
|
|
9
11
|
/** GitHub App ID */
|
|
10
|
-
app_id
|
|
11
|
-
/** GitHub App
|
|
12
|
-
private_key
|
|
13
|
-
/**
|
|
14
|
-
installation_id?: number;
|
|
15
|
-
/** Webhook 签名密钥 */
|
|
12
|
+
app_id?: string | number;
|
|
13
|
+
/** GitHub App 私钥(PEM 内容或文件路径) */
|
|
14
|
+
private_key?: string;
|
|
15
|
+
/** Webhook Secret(配置后启用 Webhook 接收事件,不配置则使用轮询) */
|
|
16
16
|
webhook_secret?: string;
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
-
/** 服务公开访问地址,用于 OAuth 回调和绑定链接(如 https://bot.example.com) */
|
|
22
|
-
public_url?: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// ── OAuth 用户绑定 ───────────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
export interface GitHubOAuthUser {
|
|
28
|
-
id: number;
|
|
29
|
-
/** 聊天平台名称 (icqq / kook / discord ...) */
|
|
30
|
-
platform: string;
|
|
31
|
-
/** 聊天平台用户 ID */
|
|
32
|
-
platform_uid: string;
|
|
33
|
-
/** GitHub 用户名 */
|
|
34
|
-
github_login: string;
|
|
35
|
-
/** GitHub 用户 ID */
|
|
36
|
-
github_id: number;
|
|
37
|
-
/** OAuth access_token */
|
|
38
|
-
access_token: string;
|
|
39
|
-
/** 授权范围 */
|
|
40
|
-
scope: string;
|
|
41
|
-
created_at: Date;
|
|
42
|
-
updated_at: Date;
|
|
17
|
+
/** Webhook 路由路径(默认 /github/webhook) */
|
|
18
|
+
webhook_path?: string;
|
|
19
|
+
/** 事件轮询间隔(秒,默认 60,Webhook 模式下作为降级备选) */
|
|
20
|
+
poll_interval?: number;
|
|
43
21
|
}
|
|
44
22
|
|
|
45
23
|
// ── Channel ID ───────────────────────────────────────────────────────
|
|
@@ -165,3 +143,20 @@ export interface Subscription {
|
|
|
165
143
|
export type PrAction = 'list' | 'view' | 'diff' | 'merge' | 'create' | 'review' | 'close';
|
|
166
144
|
export type IssueAction = 'list' | 'view' | 'create' | 'close' | 'comment';
|
|
167
145
|
export type RepoAction = 'info' | 'branches' | 'releases' | 'runs' | 'stars';
|
|
146
|
+
|
|
147
|
+
// ── OAuth 用户绑定 ───────────────────────────────────────────────────
|
|
148
|
+
// 存储各平台用户绑定的 GitHub OAuth Token(Device Flow)
|
|
149
|
+
|
|
150
|
+
export interface GitHubOAuthUser {
|
|
151
|
+
id: number;
|
|
152
|
+
/** 来源平台 (icqq, kook, discord …) */
|
|
153
|
+
platform: string;
|
|
154
|
+
/** 该平台上的用户 ID */
|
|
155
|
+
platform_uid: string;
|
|
156
|
+
/** GitHub 用户名 */
|
|
157
|
+
github_login: string;
|
|
158
|
+
/** OAuth Access Token */
|
|
159
|
+
access_token: string;
|
|
160
|
+
/** 绑定时间 (ms) */
|
|
161
|
+
created_at: number;
|
|
162
|
+
}
|
package/lib/api.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../src/api.ts"],"names":[],"mappings":"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"}
|