@zhin.js/adapter-github 0.1.34 → 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/index.js CHANGED
@@ -3,9 +3,11 @@
3
3
  */
4
4
  import { usePlugin } from 'zhin.js';
5
5
  import { GitHubAdapter } from './adapter.js';
6
+ import { GhClient } from './gh-client.js';
6
7
  export * from './types.js';
7
8
  export { GitHubBot, parseMarkdown, toMarkdown } from './bot.js';
8
9
  export { GitHubAdapter } from './adapter.js';
10
+ export { GhClient } from './gh-client.js';
9
11
  const plugin = usePlugin();
10
12
  const { provide, defineModel, useContext, logger } = plugin;
11
13
  defineModel('github_subscriptions', {
@@ -28,15 +30,12 @@ defineModel('github_oauth_users', {
28
30
  platform: { type: 'text', nullable: false },
29
31
  platform_uid: { type: 'text', nullable: false },
30
32
  github_login: { type: 'text', nullable: false },
31
- github_id: { type: 'integer', nullable: false },
32
33
  access_token: { type: 'text', nullable: false },
33
- scope: { type: 'text', default: '' },
34
- created_at: { type: 'date', nullable: false },
35
- updated_at: { type: 'date', nullable: false },
34
+ created_at: { type: 'integer', default: 0 },
36
35
  });
37
36
  provide({
38
37
  name: 'github',
39
- description: 'GitHub Adapter — Issues/PRs as chat channels, full repo management via GitHub App',
38
+ description: 'GitHub Adapter — Issues/PRs as chat channels, full repo management via gh CLI',
40
39
  mounted: async (p) => {
41
40
  const adapter = new GitHubAdapter(p);
42
41
  await adapter.start();
@@ -46,9 +45,29 @@ provide({
46
45
  await adapter.stop();
47
46
  },
48
47
  });
49
- useContext('router', 'github', (router, adapter) => {
50
- adapter.setupWebhook(router);
51
- adapter.setupOAuth(router);
48
+ // 混合模式:有 router + webhook_secret → Webhook 实时;否则 → 轮询降级
49
+ useContext('github', (adapter) => {
50
+ if (adapter.hasWebhookConfig) {
51
+ // 尝试注册 Webhook(需要 router Context)
52
+ const router = plugin.inject('router');
53
+ if (router) {
54
+ adapter.setupWebhook(router);
55
+ logger.info('GitHub 事件源: Webhook (实时)');
56
+ }
57
+ else {
58
+ // router 还没就绪,等它挂载后再注册
59
+ plugin.useContext('router', (r) => {
60
+ adapter.setupWebhook(r);
61
+ logger.info('GitHub 事件源: Webhook (实时, 延迟注册)');
62
+ });
63
+ }
64
+ }
65
+ // Webhook 未配置或未激活时,总是启动轮询作为兜底
66
+ if (!adapter.webhookActive) {
67
+ adapter.startPolling();
68
+ logger.info('GitHub 事件源: 轮询');
69
+ }
70
+ return () => adapter.stopPolling();
52
71
  });
53
72
  // ── Tool 工具注册 ─────────────────────────────────────────────────────────
54
73
  useContext('tool', 'github', (toolService, adapter) => {
@@ -75,8 +94,8 @@ useContext('tool', 'github', (toolService, adapter) => {
75
94
  },
76
95
  platforms: ['github'],
77
96
  tags: ['github'],
78
- execute: async (args) => {
79
- const api = adapter.getAPI();
97
+ execute: async (args, context) => {
98
+ const api = await adapter.getUserOrDefaultAPI(context?.platform, context?.senderId);
80
99
  if (!api)
81
100
  return '❌ 没有可用的 GitHub bot';
82
101
  const { action, repo, number: num, title, body, head, base, state, approve, method } = args;
@@ -163,8 +182,8 @@ useContext('tool', 'github', (toolService, adapter) => {
163
182
  },
164
183
  platforms: ['github'],
165
184
  tags: ['github'],
166
- execute: async (args) => {
167
- const api = adapter.getAPI();
185
+ execute: async (args, context) => {
186
+ const api = await adapter.getUserOrDefaultAPI(context?.platform, context?.senderId);
168
187
  if (!api)
169
188
  return '❌ 没有可用的 GitHub bot';
170
189
  const { action, repo, number: num, title, body, labels, state } = args;
@@ -236,8 +255,8 @@ useContext('tool', 'github', (toolService, adapter) => {
236
255
  },
237
256
  platforms: ['github'],
238
257
  tags: ['github'],
239
- execute: async (args) => {
240
- const api = adapter.getAPI();
258
+ execute: async (args, context) => {
259
+ const api = await adapter.getUserOrDefaultAPI(context?.platform, context?.senderId);
241
260
  if (!api)
242
261
  return '❌ 没有可用的 GitHub bot';
243
262
  const { action, repo, limit: lim } = args;
@@ -311,8 +330,8 @@ useContext('tool', 'github', (toolService, adapter) => {
311
330
  },
312
331
  platforms: ['github'],
313
332
  tags: ['github'],
314
- execute: async (args) => {
315
- const api = adapter.getAPI();
333
+ execute: async (args, context) => {
334
+ const api = await adapter.getUserOrDefaultAPI(context?.platform, context?.senderId);
316
335
  if (!api)
317
336
  return '❌ 没有可用的 GitHub bot';
318
337
  const { action, query: q, limit: lim } = args;
@@ -365,8 +384,8 @@ useContext('tool', 'github', (toolService, adapter) => {
365
384
  },
366
385
  platforms: ['github'],
367
386
  tags: ['github'],
368
- execute: async (args) => {
369
- const api = adapter.getAPI();
387
+ execute: async (args, context) => {
388
+ const api = await adapter.getUserOrDefaultAPI(context?.platform, context?.senderId);
370
389
  if (!api)
371
390
  return '❌ 没有可用的 GitHub bot';
372
391
  const { action, repo, number: num, labels } = args;
@@ -422,8 +441,8 @@ useContext('tool', 'github', (toolService, adapter) => {
422
441
  },
423
442
  platforms: ['github'],
424
443
  tags: ['github'],
425
- execute: async (args) => {
426
- const api = adapter.getAPI();
444
+ execute: async (args, context) => {
445
+ const api = await adapter.getUserOrDefaultAPI(context?.platform, context?.senderId);
427
446
  if (!api)
428
447
  return '❌ 没有可用的 GitHub bot';
429
448
  const { action, repo, number: num, assignees } = args;
@@ -453,8 +472,8 @@ useContext('tool', 'github', (toolService, adapter) => {
453
472
  },
454
473
  platforms: ['github'],
455
474
  tags: ['github'],
456
- execute: async (args) => {
457
- const api = adapter.getAPI();
475
+ execute: async (args, context) => {
476
+ const api = await adapter.getUserOrDefaultAPI(context?.platform, context?.senderId);
458
477
  if (!api)
459
478
  return '❌ 没有可用的 GitHub bot';
460
479
  const r = await api.getFileContent(args.repo, args.path, args.ref);
@@ -492,8 +511,8 @@ useContext('tool', 'github', (toolService, adapter) => {
492
511
  },
493
512
  platforms: ['github'],
494
513
  tags: ['github'],
495
- execute: async (args) => {
496
- const api = adapter.getAPI();
514
+ execute: async (args, context) => {
515
+ const api = await adapter.getUserOrDefaultAPI(context?.platform, context?.senderId);
497
516
  if (!api)
498
517
  return '❌ 没有可用的 GitHub bot';
499
518
  const { action, repo, sha, path, base, head, limit: lim } = args;
@@ -539,8 +558,8 @@ useContext('tool', 'github', (toolService, adapter) => {
539
558
  },
540
559
  platforms: ['github'],
541
560
  tags: ['github'],
542
- execute: async (args) => {
543
- const api = adapter.getAPI();
561
+ execute: async (args, context) => {
562
+ const api = await adapter.getUserOrDefaultAPI(context?.platform, context?.senderId);
544
563
  if (!api)
545
564
  return '❌ 没有可用的 GitHub bot';
546
565
  const { type: itemType, repo, number: num, title, body, state } = args;
@@ -564,7 +583,7 @@ useContext('tool', 'github', (toolService, adapter) => {
564
583
  // --- Star ---
565
584
  {
566
585
  name: 'github_star',
567
- description: 'Star 或取消 Star 一个 GitHub 仓库(需要先 github_bind 绑定账号)',
586
+ description: 'Star 或取消 Star 一个 GitHub 仓库(使用你绑定的 GitHub 账号,未绑定则用 Bot 默认账号)',
568
587
  parameters: {
569
588
  type: 'object',
570
589
  properties: {
@@ -576,24 +595,22 @@ useContext('tool', 'github', (toolService, adapter) => {
576
595
  platforms: ['github'],
577
596
  tags: ['github'],
578
597
  execute: async (args, context) => {
579
- if (!context?.platform || !context?.senderId)
580
- return '❌ 无法获取用户信息';
581
- const client = await adapter.getOAuthClient(context.platform, context.senderId);
582
- if (!client)
583
- return '❌ 你还没有绑定 GitHub 账号,请先使用 github_bind';
598
+ const gh = await adapter.getUserOrDefaultAPI(context?.platform, context?.senderId);
599
+ if (!gh)
600
+ return '❌ 没有可用的 GitHub bot';
584
601
  const { action, repo } = args;
585
602
  switch (action) {
586
603
  case 'star': {
587
- const r = await client.starRepo(repo);
588
- return r.ok || r.status === 204 ? `⭐ 已 Star ${repo}` : `❌ ${r.data?.message || JSON.stringify(r.data)}`;
604
+ const r = await gh.starRepo(repo);
605
+ return r.ok ? `⭐ 已 Star ${repo}` : `❌ ${r.data?.message || JSON.stringify(r.data)}`;
589
606
  }
590
607
  case 'unstar': {
591
- const r = await client.unstarRepo(repo);
592
- return r.ok || r.status === 204 ? `💔 已取消 Star ${repo}` : `❌ ${r.data?.message || JSON.stringify(r.data)}`;
608
+ const r = await gh.unstarRepo(repo);
609
+ return r.ok ? `💔 已取消 Star ${repo}` : `❌ ${r.data?.message || JSON.stringify(r.data)}`;
593
610
  }
594
611
  case 'check': {
595
- const starred = await client.isStarred(repo);
596
- return starred ? `⭐ 你已 Star ${repo}` : `☆ 你尚未 Star ${repo}`;
612
+ const starred = await gh.isStarred(repo);
613
+ return starred ? `⭐ Star ${repo}` : `☆ 尚未 Star ${repo}`;
597
614
  }
598
615
  default: return `❌ 未知操作: ${action}`;
599
616
  }
@@ -602,7 +619,7 @@ useContext('tool', 'github', (toolService, adapter) => {
602
619
  // --- Fork ---
603
620
  {
604
621
  name: 'github_fork',
605
- description: 'Fork 一个 GitHub 仓库到自己的账号下(需要先 github_bind 绑定账号)',
622
+ description: 'Fork 一个 GitHub 仓库(使用你绑定的 GitHub 账号,未绑定则用 Bot 默认账号)',
606
623
  parameters: {
607
624
  type: 'object',
608
625
  properties: {
@@ -613,17 +630,166 @@ useContext('tool', 'github', (toolService, adapter) => {
613
630
  platforms: ['github'],
614
631
  tags: ['github'],
615
632
  execute: async (args, context) => {
616
- if (!context?.platform || !context?.senderId)
617
- return '❌ 无法获取用户信息';
618
- const client = await adapter.getOAuthClient(context.platform, context.senderId);
619
- if (!client)
620
- return '❌ 你还没有绑定 GitHub 账号,请先使用 github_bind';
621
- const r = await client.forkRepo(args.repo);
633
+ const gh = await adapter.getUserOrDefaultAPI(context?.platform, context?.senderId);
634
+ if (!gh)
635
+ return '❌ 没有可用的 GitHub bot';
636
+ const r = await gh.forkRepo(args.repo);
622
637
  if (!r.ok)
623
638
  return `❌ ${r.data?.message || JSON.stringify(r.data)}`;
624
639
  return `🍴 已 Fork ${args.repo} → ${r.data.full_name}\n🔗 ${r.data.html_url}`;
625
640
  },
626
641
  },
642
+ // --- Bind (Device Flow) ---
643
+ {
644
+ name: 'github_bind',
645
+ description: '绑定你的 GitHub 账号 — 使用 Device Flow 授权,无需输入密码',
646
+ parameters: {
647
+ type: 'object',
648
+ properties: {},
649
+ },
650
+ tags: ['github'],
651
+ execute: async (_args, context) => {
652
+ if (!context?.platform || !context?.senderId) {
653
+ return '❌ 无法获取当前用户信息';
654
+ }
655
+ const clientId = adapter.getClientId();
656
+ if (!clientId)
657
+ return '❌ Bot 未配置 GitHub App 或 App 无 client_id,无法进行账号绑定';
658
+ const db = plugin.root?.inject('database');
659
+ const model = db?.models?.get('github_oauth_users');
660
+ if (!model)
661
+ return '❌ 数据库未就绪';
662
+ // 检查是否已绑定
663
+ const [existing] = await model.select().where({ platform: context.platform, platform_uid: context.senderId });
664
+ if (existing) {
665
+ return `⚠️ 你已绑定 GitHub 账号: ${existing.github_login}\n如需重新绑定,请先执行 github_unbind`;
666
+ }
667
+ try {
668
+ const host = adapter.getHost();
669
+ const codeResp = await GhClient.deviceFlowRequestCode(clientId, host);
670
+ // 异步轮询 token(最多等 codeResp.expires_in 秒)
671
+ const tokenPromise = GhClient.deviceFlowPollToken(clientId, codeResp.device_code, codeResp.interval, codeResp.expires_in, host);
672
+ // 先回复用户授权链接
673
+ const replyMsg = [
674
+ `🔗 请在浏览器中打开以下链接进行授权:`,
675
+ ` ${codeResp.verification_uri}`,
676
+ ``,
677
+ `📋 输入验证码: **${codeResp.user_code}**`,
678
+ ``,
679
+ `⏳ 等待授权中…(${Math.floor(codeResp.expires_in / 60)} 分钟内有效)`,
680
+ ].join('\n');
681
+ // 在后台等待用户授权完成
682
+ tokenPromise.then(async (tokenData) => {
683
+ if (!tokenData) {
684
+ // 授权超时或被拒绝 — 由于 execute 已经返回了,这里只能通过日志记录
685
+ logger.warn(`GitHub Device Flow 超时/拒绝: ${context.platform}:${context.senderId}`);
686
+ return;
687
+ }
688
+ // 使用 token 获取 GitHub 用户名
689
+ const userGh = new GhClient({ host, token: tokenData.access_token });
690
+ const authResult = await userGh.verifyAuth();
691
+ const login = authResult.ok ? authResult.user : 'unknown';
692
+ await model.insert({
693
+ id: Date.now(),
694
+ platform: context.platform,
695
+ platform_uid: context.senderId,
696
+ github_login: login,
697
+ access_token: tokenData.access_token,
698
+ created_at: Date.now(),
699
+ });
700
+ logger.info(`GitHub 账号绑定成功: ${context.platform}:${context.senderId} → ${login}`);
701
+ // 尝试回复绑定成功消息
702
+ if (context.message?.$reply) {
703
+ await context.message.$reply(`✅ GitHub 账号绑定成功!\n👤 ${login}`);
704
+ }
705
+ }).catch(err => {
706
+ logger.error('GitHub Device Flow 错误:', err);
707
+ });
708
+ return replyMsg;
709
+ }
710
+ catch (e) {
711
+ return `❌ Device Flow 启动失败: ${e.message}`;
712
+ }
713
+ },
714
+ },
715
+ // --- Unbind ---
716
+ {
717
+ name: 'github_unbind',
718
+ description: '解除你绑定的 GitHub 账号',
719
+ parameters: {
720
+ type: 'object',
721
+ properties: {},
722
+ },
723
+ tags: ['github'],
724
+ execute: async (_args, context) => {
725
+ if (!context?.platform || !context?.senderId) {
726
+ return '❌ 无法获取当前用户信息';
727
+ }
728
+ const db = plugin.root?.inject('database');
729
+ const model = db?.models?.get('github_oauth_users');
730
+ if (!model)
731
+ return '❌ 数据库未就绪';
732
+ const [existing] = await model.select().where({ platform: context.platform, platform_uid: context.senderId });
733
+ if (!existing)
734
+ return '📭 你尚未绑定 GitHub 账号';
735
+ await model.delete().where({ id: existing.id });
736
+ return `✅ 已解除 GitHub 账号绑定: ${existing.github_login}`;
737
+ },
738
+ },
739
+ // --- Whoami ---
740
+ {
741
+ name: 'github_whoami',
742
+ description: '查看你绑定的 GitHub 账号信息',
743
+ parameters: {
744
+ type: 'object',
745
+ properties: {},
746
+ },
747
+ tags: ['github'],
748
+ execute: async (_args, context) => {
749
+ if (!context?.platform || !context?.senderId) {
750
+ return '❌ 无法获取当前用户信息';
751
+ }
752
+ const db = plugin.root?.inject('database');
753
+ const model = db?.models?.get('github_oauth_users');
754
+ if (!model)
755
+ return '❌ 数据库未就绪';
756
+ const [existing] = await model.select().where({ platform: context.platform, platform_uid: context.senderId });
757
+ if (!existing)
758
+ return '📭 你尚未绑定 GitHub 账号\n🔗 使用 github_bind 绑定你的账号';
759
+ // 验证 token 是否仍然有效
760
+ const userGh = new GhClient({ host: adapter.getHost(), token: existing.access_token });
761
+ const auth = await userGh.verifyAuth();
762
+ if (auth.ok) {
763
+ return `👤 已绑定 GitHub 账号: ${auth.user}\n📅 绑定时间: ${new Date(existing.created_at).toLocaleString('zh-CN')}`;
764
+ }
765
+ return `⚠️ 已绑定账号 ${existing.github_login},但 Token 已失效\n🔗 请执行 github_unbind 后重新 github_bind`;
766
+ },
767
+ },
768
+ // --- Install App ---
769
+ {
770
+ name: 'github_install',
771
+ description: '获取安装 GitHub App 的链接 — 安装后 Bot 可以访问你的仓库,你也可以使用更多功能',
772
+ parameters: {
773
+ type: 'object',
774
+ properties: {},
775
+ },
776
+ tags: ['github'],
777
+ execute: async () => {
778
+ const slug = adapter.getAppSlug();
779
+ if (!slug)
780
+ return '❌ Bot 未配置 GitHub App';
781
+ const host = adapter.getHost() || 'github.com';
782
+ const installations = adapter.getInstallations();
783
+ let msg = `🔗 请点击以下链接安装 GitHub App 到你的仓库:\n https://${host}/apps/${slug}/installations/new`;
784
+ if (installations.length) {
785
+ msg += `\n\n📋 当前已安装 (${installations.length}):`;
786
+ for (const inst of installations) {
787
+ msg += `\n • ${inst.account.login} (${inst.account.type})`;
788
+ }
789
+ }
790
+ return msg;
791
+ },
792
+ },
627
793
  // --- Subscribe ---
628
794
  {
629
795
  name: 'github_subscribe',
@@ -739,90 +905,10 @@ useContext('tool', 'github', (toolService, adapter) => {
739
905
  }).join('\n\n');
740
906
  },
741
907
  },
742
- // --- Bind ---
743
- {
744
- name: 'github_bind',
745
- description: '绑定 GitHub 账号,生成 OAuth 授权链接',
746
- parameters: {
747
- type: 'object',
748
- properties: {},
749
- },
750
- platforms: ['github'],
751
- tags: ['github'],
752
- execute: async (_args, context) => {
753
- if (!context?.platform || !context?.senderId)
754
- return '❌ 无法获取用户信息';
755
- // 检查是否已绑定
756
- const db = plugin.root?.inject('database');
757
- const model = db?.models?.get('github_oauth_users');
758
- if (model) {
759
- const [existing] = await model.select().where({ platform: context.platform, platform_uid: context.senderId });
760
- if (existing)
761
- return `✅ 你已绑定 GitHub 账号: ${existing.github_login}\n如需重新绑定,请先使用 github_unbind 解绑`;
762
- }
763
- const url = adapter.createOAuthState(context.platform, context.senderId);
764
- if (!url)
765
- return '❌ GitHub App 未配置 OAuth (缺少 client_id)';
766
- return `🔗 请点击以下链接绑定你的 GitHub 账号:\n${url}\n\n链接有效期 5 分钟`;
767
- },
768
- },
769
- // --- Unbind ---
770
- {
771
- name: 'github_unbind',
772
- description: '解绑 GitHub 账号',
773
- parameters: {
774
- type: 'object',
775
- properties: {},
776
- },
777
- platforms: ['github'],
778
- tags: ['github'],
779
- execute: async (_args, context) => {
780
- if (!context?.platform || !context?.senderId)
781
- return '❌ 无法获取用户信息';
782
- const db = plugin.root?.inject('database');
783
- const model = db?.models?.get('github_oauth_users');
784
- if (!model)
785
- return '❌ 数据库未就绪';
786
- const [existing] = await model.select().where({ platform: context.platform, platform_uid: context.senderId });
787
- if (!existing)
788
- return '📭 你还没有绑定 GitHub 账号';
789
- await model.delete().where({ id: existing.id });
790
- return `✅ 已解绑 GitHub 账号: ${existing.github_login}`;
791
- },
792
- },
793
- // --- Whoami ---
794
- {
795
- name: 'github_whoami',
796
- description: '查看当前绑定的 GitHub 账号信息',
797
- parameters: {
798
- type: 'object',
799
- properties: {},
800
- },
801
- platforms: ['github'],
802
- tags: ['github'],
803
- execute: async (_args, context) => {
804
- if (!context?.platform || !context?.senderId)
805
- return '❌ 无法获取用户信息';
806
- const db = plugin.root?.inject('database');
807
- const model = db?.models?.get('github_oauth_users');
808
- if (!model)
809
- return '❌ 数据库未就绪';
810
- const [row] = await model.select().where({ platform: context.platform, platform_uid: context.senderId });
811
- if (!row)
812
- return '📭 你还没有绑定 GitHub 账号,使用 github_bind 进行绑定';
813
- return [
814
- `🐙 GitHub 账号信息`,
815
- `👤 用户名: ${row.github_login}`,
816
- `🆔 GitHub ID: ${row.github_id}`,
817
- `📅 绑定时间: ${row.created_at instanceof Date ? row.created_at.toLocaleDateString() : String(row.created_at).split('T')[0]}`,
818
- `🔑 权限: ${row.scope || '(默认)'}`,
819
- ].join('\n');
820
- },
821
- },
822
908
  ];
823
- const disposers = tools.map(t => toolService.addTool(t, 'github'));
909
+ const disposers = tools.map(t => toolService.addTool(t, plugin.name));
824
910
  logger.debug(`GitHub 工具已注册: ${tools.map(t => t.name).join(', ')}`);
825
911
  return () => disposers.forEach(d => d());
826
912
  });
827
- logger.debug('GitHub 适配器已加载 (GitHub App 认证)');
913
+ logger.debug('GitHub 适配器已加载 (gh CLI 认证)');
828
914
  //# sourceMappingURL=index.js.map