@zhin.js/adapter-github 3.0.1 → 3.0.3

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 (84) hide show
  1. package/CHANGELOG.md +624 -0
  2. package/README.md +38 -190
  3. package/adapters/github.ts +51 -0
  4. package/{skills/github/SKILL.md → agent/skills/github.md} +22 -5
  5. package/agent/tools/bind.ts +12 -0
  6. package/agent/tools/create_pr.ts +19 -0
  7. package/agent/tools/install.ts +12 -0
  8. package/agent/tools/patch_file.ts +18 -0
  9. package/agent/tools/prepare_workspace.ts +14 -0
  10. package/agent/tools/push_branch.ts +17 -0
  11. package/agent/tools/star.ts +15 -0
  12. package/agent/tools/subscribe.ts +16 -0
  13. package/agent/tools/subscriptions.ts +13 -0
  14. package/agent/tools/unbind.ts +12 -0
  15. package/agent/tools/unsubscribe.ts +15 -0
  16. package/agent/tools/whoami.ts +12 -0
  17. package/lib/agent-prompt.d.ts +0 -1
  18. package/lib/agent-prompt.js +10 -9
  19. package/lib/endpoint.d.ts +46 -24
  20. package/lib/endpoint.js +144 -144
  21. package/lib/gh-client.d.ts +73 -1
  22. package/lib/gh-client.js +99 -1
  23. package/lib/github-agent-deps.d.ts +47 -0
  24. package/lib/github-agent-deps.js +43 -0
  25. package/lib/github-bot-handlers.d.ts +26 -0
  26. package/lib/github-bot-handlers.js +77 -0
  27. package/lib/github-channel-context.d.ts +16 -0
  28. package/lib/github-channel-context.js +31 -0
  29. package/lib/github-tool-handlers.d.ts +17 -0
  30. package/lib/github-tool-handlers.js +221 -0
  31. package/lib/index.d.ts +7 -32
  32. package/lib/index.js +7 -386
  33. package/lib/oauth-users.d.ts +33 -0
  34. package/lib/oauth-users.js +38 -0
  35. package/lib/protocol.d.ts +87 -0
  36. package/lib/protocol.js +241 -0
  37. package/lib/types.d.ts +6 -1
  38. package/lib/types.js +0 -1
  39. package/lib/webhook.d.ts +13 -0
  40. package/lib/webhook.js +87 -0
  41. package/lib/workspace-manager.d.ts +21 -0
  42. package/lib/workspace-manager.js +145 -0
  43. package/package.json +60 -23
  44. package/plugin.ts +52 -0
  45. package/schema.json +65 -0
  46. package/src/agent-prompt.ts +11 -10
  47. package/src/endpoint.ts +169 -150
  48. package/src/gh-client.ts +131 -0
  49. package/src/github-agent-deps.ts +82 -0
  50. package/src/github-bot-handlers.ts +109 -0
  51. package/src/github-channel-context.ts +46 -0
  52. package/src/github-tool-handlers.ts +257 -0
  53. package/src/index.ts +55 -431
  54. package/src/oauth-users.ts +47 -0
  55. package/src/protocol.ts +367 -0
  56. package/src/types.ts +6 -0
  57. package/src/webhook.ts +125 -0
  58. package/src/workspace-manager.ts +158 -0
  59. package/lib/adapter.d.ts +0 -66
  60. package/lib/adapter.d.ts.map +0 -1
  61. package/lib/adapter.js +0 -418
  62. package/lib/adapter.js.map +0 -1
  63. package/lib/agent-prompt.d.ts.map +0 -1
  64. package/lib/agent-prompt.js.map +0 -1
  65. package/lib/endpoint.d.ts.map +0 -1
  66. package/lib/endpoint.js.map +0 -1
  67. package/lib/gh-client.d.ts.map +0 -1
  68. package/lib/gh-client.js.map +0 -1
  69. package/lib/index.d.ts.map +0 -1
  70. package/lib/index.js.map +0 -1
  71. package/lib/register-github-mcp.d.ts +0 -7
  72. package/lib/register-github-mcp.d.ts.map +0 -1
  73. package/lib/register-github-mcp.js +0 -36
  74. package/lib/register-github-mcp.js.map +0 -1
  75. package/lib/segment-mapper.d.ts +0 -2
  76. package/lib/segment-mapper.d.ts.map +0 -1
  77. package/lib/segment-mapper.js +0 -2
  78. package/lib/segment-mapper.js.map +0 -1
  79. package/lib/types.d.ts.map +0 -1
  80. package/lib/types.js.map +0 -1
  81. package/plugin.yml +0 -3
  82. package/src/adapter.ts +0 -450
  83. package/src/register-github-mcp.ts +0 -62
  84. package/src/segment-mapper.ts +0 -1
package/src/gh-client.ts CHANGED
@@ -21,6 +21,19 @@ export interface AppAuth {
21
21
  privateKey: string;
22
22
  }
23
23
 
24
+ export interface GitHubBotIdentity {
25
+ login: string;
26
+ email: string;
27
+ slug: string;
28
+ userId: number;
29
+ }
30
+
31
+ export interface GitHubCommitAuthor {
32
+ name: string;
33
+ email: string;
34
+ date?: string;
35
+ }
36
+
24
37
  export interface GhClientOptions {
25
38
  /** GitHub Enterprise 主机名(默认 github.com) */
26
39
  host?: string;
@@ -201,6 +214,7 @@ export class GhClient {
201
214
  this._user = name;
202
215
  this._appSlug = appData.slug || null;
203
216
  if (appData.client_id) this._clientId = appData.client_id;
217
+ await this.resolveBotUserId();
204
218
  const repos = this._repoToInstallation.size;
205
219
  return { ok: true, user: name, message: `GitHub App: ${name} (${this._allInstallations.length} 安装, ${repos} 仓库)` };
206
220
  }
@@ -313,6 +327,114 @@ export class GhClient {
313
327
  return this.del(`/repos/${repo}/pulls/comments/${commentId}`);
314
328
  }
315
329
 
330
+ // ── Contents / Pulls(Bot 写操作)──────────────────────────────────
331
+
332
+ async getFileContent(repo: string, path: string, ref?: string) {
333
+ let apiPath = `/repos/${repo}/contents/${path.split('/').map(encodeURIComponent).join('/')}`;
334
+ if (ref) apiPath += `?ref=${encodeURIComponent(ref)}`;
335
+ return this.get<{ content: string; sha: string; encoding: string }>(apiPath);
336
+ }
337
+
338
+ async createOrUpdateFile(
339
+ repo: string,
340
+ path: string,
341
+ content: string,
342
+ message: string,
343
+ options?: { branch?: string; sha?: string },
344
+ ) {
345
+ const body: Record<string, unknown> = {
346
+ message,
347
+ content: Buffer.from(content, 'utf-8').toString('base64'),
348
+ };
349
+ if (options?.branch) body.branch = options.branch;
350
+ if (options?.sha) body.sha = options.sha;
351
+ const apiPath = `/repos/${repo}/contents/${path.split('/').map(encodeURIComponent).join('/')}`;
352
+ return this.put<{ commit: { sha: string; html_url?: string }; content: { html_url?: string } }>(
353
+ apiPath,
354
+ body,
355
+ );
356
+ }
357
+
358
+ async getRef(repo: string, ref: string) {
359
+ return this.get<{ ref: string; object: { sha: string; type: string } }>(
360
+ `/repos/${repo}/git/ref/${encodeURIComponent(ref)}`,
361
+ );
362
+ }
363
+
364
+ async createRef(repo: string, ref: string, sha: string) {
365
+ return this.post(`/repos/${repo}/git/refs`, { ref, sha });
366
+ }
367
+
368
+ async createPullRequest(
369
+ repo: string,
370
+ title: string,
371
+ head: string,
372
+ base: string,
373
+ body?: string,
374
+ ) {
375
+ return this.post<{ number: number; html_url: string; head: { ref: string } }>(
376
+ `/repos/${repo}/pulls`,
377
+ { title, head, base, body: body ?? '' },
378
+ );
379
+ }
380
+
381
+ getBotLogin(): string | null {
382
+ if (this._user) return this._user;
383
+ if (this._appSlug) return `${this._appSlug}[bot]`;
384
+ return null;
385
+ }
386
+
387
+ getCommitAuthor(): GitHubCommitAuthor | null {
388
+ const identity = this.getBotIdentitySync();
389
+ if (!identity) return null;
390
+ return { name: identity.login, email: identity.email, date: new Date().toISOString() };
391
+ }
392
+
393
+ getBotIdentitySync(): GitHubBotIdentity | null {
394
+ const slug = this._appSlug;
395
+ const login = this.getBotLogin();
396
+ if (!slug || !login || this._botUserId == null) return null;
397
+ return {
398
+ login,
399
+ slug,
400
+ userId: this._botUserId,
401
+ email: `${this._botUserId}+${login}@users.noreply.github.com`,
402
+ };
403
+ }
404
+
405
+ async getBotIdentity(): Promise<GitHubBotIdentity | null> {
406
+ if (!this._appAuth) return null;
407
+ if (this._botUserId == null) await this.resolveBotUserId();
408
+ return this.getBotIdentitySync();
409
+ }
410
+
411
+ buildCloneUrl(repo: string, token: string): string {
412
+ const host = this.host || 'github.com';
413
+ return `https://x-access-token:${encodeURIComponent(token)}@${host}/${repo}.git`;
414
+ }
415
+
416
+ async resolveBotUserId(): Promise<number | null> {
417
+ if (this._botUserId != null) return this._botUserId;
418
+ const slug = this._appSlug;
419
+ if (!slug) return null;
420
+ const login = `${slug}[bot]`;
421
+ const baseUrl = this.host ? `https://${this.host}/api/v3` : 'https://api.github.com';
422
+ try {
423
+ const res = await fetch(`${baseUrl}/users/${encodeURIComponent(login)}`, {
424
+ headers: { Accept: 'application/vnd.github+json' },
425
+ });
426
+ if (!res.ok) return null;
427
+ const data = (await res.json()) as { id?: number };
428
+ if (typeof data.id === 'number') {
429
+ this._botUserId = data.id;
430
+ return data.id;
431
+ }
432
+ } catch {
433
+ return null;
434
+ }
435
+ return null;
436
+ }
437
+
316
438
  // ── Star ─────────────────────────────────────────────────────────
317
439
 
318
440
  async starRepo(repo: string) {
@@ -445,6 +567,15 @@ export class GhClient {
445
567
 
446
568
  /** App 的 client_id(verifyAuth 后从 /app 获取,用于 Device Flow) */
447
569
  private _clientId: string | null = null;
570
+ /** Bot 机器用户 numeric id(/users/{slug}[bot]) */
571
+ private _botUserId: number | null = null;
572
+
573
+ /** 确保 App 认证的 Installation Token 对指定 repo 有效,并返回 token */
574
+ async ensureInstallationTokenForRepo(repo: string): Promise<string | undefined> {
575
+ if (!this._appAuth) return this.token;
576
+ await this.ensureTokenForRepo(repo);
577
+ return this.token;
578
+ }
448
579
  get clientId(): string | null { return this._clientId; }
449
580
 
450
581
  // ── 事件轮询 ─────────────────────────────────────────────────────
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Agent tool deps for github.
3
+ * Endpoints register themselves on start; tools look up by config name / endpoint id.
4
+ */
5
+
6
+ import type { GhClient } from './gh-client.js';
7
+ import type { ResolvedGithubConfig } from './protocol.js';
8
+ import type { WorkspaceManager } from './workspace-manager.js';
9
+
10
+ export interface GithubAgentEndpoint {
11
+ readonly name: string;
12
+ readonly gh: GhClient;
13
+ readonly config: ResolvedGithubConfig;
14
+ getAPI(): GhClient;
15
+ getUserOrDefaultAPI(platform?: string, platformUid?: string): Promise<GhClient | null>;
16
+ getClientId(): string | null;
17
+ getHost(): string | undefined;
18
+ getAppSlug(): string | null;
19
+ getInstallations(): Array<{ id: number; account: { login: string; type: string }; target_type: string }>;
20
+ getWorkspaceManager(): WorkspaceManager;
21
+ /** DatabaseHost wired at endpoint creation (optional). */
22
+ getDatabase?(): unknown;
23
+ }
24
+
25
+ export interface GithubAgentDeps {
26
+ getEndpoint: (endpointId?: string) => GithubAgentEndpoint;
27
+ /** Alias kept for existing agent handlers that call getAdapter(). */
28
+ getAdapter: () => GithubAgentEndpoint;
29
+ getWorkspaceManager: () => WorkspaceManager;
30
+ getDatabase?: () => { models?: Map<string, unknown> } | null | undefined;
31
+ logger?: {
32
+ debug: (...args: unknown[]) => void;
33
+ warn: (...args: unknown[]) => void;
34
+ error: (...args: unknown[]) => void;
35
+ };
36
+ }
37
+
38
+ const endpoints = new Map<string, GithubAgentEndpoint>();
39
+ let override: GithubAgentDeps | null = null;
40
+
41
+ export function registerGithubAgentEndpoint(
42
+ endpointId: string,
43
+ endpoint: GithubAgentEndpoint,
44
+ ): () => void {
45
+ endpoints.set(endpointId, endpoint);
46
+ return () => {
47
+ if (endpoints.get(endpointId) === endpoint) {
48
+ endpoints.delete(endpointId);
49
+ }
50
+ };
51
+ }
52
+
53
+ /** Optional override used by tests / transitional callers. Pass `null` to clear. */
54
+ export function setGithubAgentDeps(deps: GithubAgentDeps | null): void {
55
+ override = deps;
56
+ }
57
+
58
+ function lookup(endpointId?: string): GithubAgentEndpoint {
59
+ if (endpointId) {
60
+ const registered = endpoints.get(endpointId);
61
+ if (!registered) throw new Error(`Endpoint ${endpointId} 不存在`);
62
+ return registered;
63
+ }
64
+ const first = endpoints.values().next().value;
65
+ if (!first) throw new Error('github agent deps not initialized');
66
+ return first;
67
+ }
68
+
69
+ export function getGithubAgentDeps(): GithubAgentDeps {
70
+ if (override) return override;
71
+ return {
72
+ getEndpoint: lookup,
73
+ getAdapter: () => lookup(),
74
+ getWorkspaceManager: () => lookup().getWorkspaceManager(),
75
+ getDatabase: () => lookup().getDatabase?.() as
76
+ { models?: Map<string, unknown> } | null | undefined,
77
+ };
78
+ }
79
+
80
+ export function getAdapter(): GithubAgentEndpoint {
81
+ return getGithubAgentDeps().getAdapter();
82
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * GitHub App Bot workflow handlers (Installation Token identity).
3
+ */
4
+ import type { Message } from 'zhin.js';
5
+ import { getCurrentCommMessage } from '@zhin.js/agent/security';
6
+ import { getAdapter, getGithubAgentDeps } from './github-agent-deps.js';
7
+ import {
8
+ parseMessageChannel,
9
+ resolveWorkspaceBranch,
10
+ formatChannelContext,
11
+ } from './github-channel-context.js';
12
+
13
+ function requireBotGh() {
14
+ const gh = getAdapter().getAPI();
15
+ if (!gh?.isAppAuth) {
16
+ throw new Error('需要 GitHub App 认证(app_id + private_key),Bot 写操作不可用');
17
+ }
18
+ return gh;
19
+ }
20
+
21
+ function resolveCommMessage(commMessage?: Message): Message {
22
+ const msg = commMessage ?? getCurrentCommMessage();
23
+ if (!msg) throw new Error('无 IM 上下文,请在 Issue/PR 评论线程内调用');
24
+ return msg;
25
+ }
26
+
27
+ function resolveChannel(msg: Message, repo?: string) {
28
+ const ctx = parseMessageChannel(msg);
29
+ if (ctx) return ctx;
30
+ if (repo) {
31
+ throw new Error(`请从 GitHub Issue/PR 频道调用,或提供完整 channel(当前仅 repo=${repo})`);
32
+ }
33
+ throw new Error('无法解析 GitHub 频道(需要 Issue/PR 评论上下文)');
34
+ }
35
+
36
+ export async function executeGithubPrepareWorkspace(
37
+ args: { repo?: string },
38
+ commMessage?: Message,
39
+ ) {
40
+ const msg = resolveCommMessage(commMessage);
41
+ const ctx = resolveChannel(msg, args.repo);
42
+ const gh = requireBotGh();
43
+ const wm = getGithubAgentDeps().getWorkspaceManager();
44
+ const { branch, base } = await resolveWorkspaceBranch(gh, ctx);
45
+ const repoPath = await wm.checkoutBranch(ctx.repo, branch, base);
46
+ return [
47
+ `✅ 工作区就绪`,
48
+ `📁 ${repoPath}`,
49
+ `🌿 分支 \`${branch}\`(base: \`${base}\`)`,
50
+ `📍 ${formatChannelContext(ctx)}`,
51
+ ].join('\n');
52
+ }
53
+
54
+ export async function executeGithubPatchFile(
55
+ args: { repo?: string; path: string; content: string; message: string; branch?: string },
56
+ commMessage?: Message,
57
+ ) {
58
+ const msg = resolveCommMessage(commMessage);
59
+ const ctx = resolveChannel(msg, args.repo);
60
+ const gh = requireBotGh();
61
+ const { branch } = args.branch
62
+ ? { branch: args.branch }
63
+ : await resolveWorkspaceBranch(gh, ctx);
64
+
65
+ const existing = await gh.getFileContent(ctx.repo, args.path, branch);
66
+ const sha = existing.ok ? existing.data.sha : undefined;
67
+ const r = await gh.createOrUpdateFile(
68
+ ctx.repo,
69
+ args.path,
70
+ args.content,
71
+ args.message,
72
+ { branch, sha },
73
+ );
74
+ if (!r.ok) {
75
+ return `❌ 更新文件失败: ${JSON.stringify(r.data)}`;
76
+ }
77
+ const url = r.data.content?.html_url ?? r.data.commit?.html_url ?? '';
78
+ return `✅ 已更新 \`${args.path}\` @ \`${branch}\`${url ? `\n🔗 ${url}` : ''}`;
79
+ }
80
+
81
+ export async function executeGithubPushBranch(
82
+ args: { repo?: string; branch?: string; message: string },
83
+ commMessage?: Message,
84
+ ) {
85
+ const msg = resolveCommMessage(commMessage);
86
+ const ctx = resolveChannel(msg, args.repo);
87
+ const gh = requireBotGh();
88
+ const wm = getGithubAgentDeps().getWorkspaceManager();
89
+ const branch = args.branch ?? (await resolveWorkspaceBranch(gh, ctx)).branch;
90
+ const result = await wm.commitAndPush(ctx.repo, branch, args.message);
91
+ return `✅ ${result}\n📍 ${ctx.repo} @ \`${branch}\``;
92
+ }
93
+
94
+ export async function executeGithubCreatePr(
95
+ args: { repo?: string; title: string; body?: string; head?: string; base?: string },
96
+ commMessage?: Message,
97
+ ) {
98
+ const msg = resolveCommMessage(commMessage);
99
+ const ctx = resolveChannel(msg, args.repo);
100
+ const gh = requireBotGh();
101
+ const resolved = await resolveWorkspaceBranch(gh, ctx);
102
+ const head = args.head ?? resolved.branch;
103
+ const base = args.base ?? resolved.base;
104
+ const r = await gh.createPullRequest(ctx.repo, args.title, head, base, args.body);
105
+ if (!r.ok) {
106
+ return `❌ 创建 PR 失败: ${JSON.stringify(r.data)}`;
107
+ }
108
+ return `✅ PR #${r.data.number} 已创建\n🔗 ${r.data.html_url}`;
109
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Parse GitHub Issue/PR channel context from IM messages.
3
+ */
4
+ import type { Message } from 'zhin.js';
5
+ import { buildChannelId, parseChannelId, type ParsedChannel } from './types.js';
6
+ import type { GhClient } from './gh-client.js';
7
+
8
+ export interface GitHubChannelContext extends ParsedChannel {
9
+ channelId: string;
10
+ }
11
+
12
+ export function parseMessageChannel(message: Message): GitHubChannelContext | null {
13
+ const channelId = message.$channel?.id;
14
+ if (!channelId) return null;
15
+ const parsed = parseChannelId(channelId);
16
+ if (!parsed) return null;
17
+ return { ...parsed, channelId };
18
+ }
19
+
20
+ export function issueBranchName(number: number): string {
21
+ return `zhin/bot/issue-${number}`;
22
+ }
23
+
24
+ export async function resolveWorkspaceBranch(
25
+ gh: GhClient,
26
+ ctx: GitHubChannelContext,
27
+ ): Promise<{ branch: string; base: string }> {
28
+ if (ctx.type === 'pr') {
29
+ const pr = await gh.getPR(ctx.repo, ctx.number);
30
+ if (pr.ok && pr.data?.head?.ref) {
31
+ const base = pr.data.base?.ref ?? 'main';
32
+ return { branch: pr.data.head.ref, base };
33
+ }
34
+ throw new Error(`无法读取 PR #${ctx.number} 的 head 分支`);
35
+ }
36
+
37
+ const repoInfo = await gh.getRepo(ctx.repo);
38
+ const base = repoInfo.ok && repoInfo.data?.default_branch
39
+ ? String(repoInfo.data.default_branch)
40
+ : 'main';
41
+ return { branch: issueBranchName(ctx.number), base };
42
+ }
43
+
44
+ export function formatChannelContext(ctx: GitHubChannelContext): string {
45
+ return buildChannelId(ctx.repo, ctx.type, ctx.number);
46
+ }
@@ -0,0 +1,257 @@
1
+ import { formatCompact } from '@zhin.js/logger';
2
+ import type { Message } from 'zhin.js';
3
+ import { getCurrentCommMessage } from '@zhin.js/agent/security';
4
+ import { GhClient } from './gh-client.js';
5
+ import type { EventType } from './types.js';
6
+ import { getAdapter, getGithubAgentDeps } from './github-agent-deps.js';
7
+
8
+ function oauthModel() {
9
+ const db = getGithubAgentDeps().getDatabase?.() as {
10
+ models?: Map<string, unknown>;
11
+ } | null | undefined;
12
+ return db?.models?.get('github_oauth_users') as {
13
+ select: () => { where: (q: object) => Promise<any[]> };
14
+ insert: (row: object) => Promise<void>;
15
+ delete: () => { where: (q: object) => Promise<void> };
16
+ } | undefined;
17
+ }
18
+
19
+ function subscriptionsModel() {
20
+ const db = getGithubAgentDeps().getDatabase?.() as {
21
+ models?: Map<string, unknown>;
22
+ } | null | undefined;
23
+ return db?.models?.get('github_subscriptions') as {
24
+ select: () => { where: (q: object) => Promise<any[]> };
25
+ insert: (row: object) => Promise<void>;
26
+ update: (row: object) => { where: (q: object) => Promise<void> };
27
+ delete: () => { where: (q: object) => Promise<void> };
28
+ } | undefined;
29
+ }
30
+
31
+ function depsLogger() {
32
+ return getGithubAgentDeps().logger ?? {
33
+ debug: (...args: unknown[]) => console.debug(...args),
34
+ warn: (...args: unknown[]) => console.warn(...args),
35
+ error: (...args: unknown[]) => console.error(...args),
36
+ };
37
+ }
38
+
39
+ export async function executeGithubStar(args: { action: 'star' | 'unstar' | 'check'; repo: string }, commMessage?: Message) {
40
+ const adapter = getAdapter();
41
+ const msg = commMessage ?? getCurrentCommMessage();
42
+ const gh = await adapter.getUserOrDefaultAPI(msg?.$adapter, msg?.$sender.id);
43
+ if (!gh) return '❌ 没有可用的 GitHub bot';
44
+ switch (args.action) {
45
+ case 'star': {
46
+ const r = await gh.starRepo(args.repo);
47
+ return r.ok ? `⭐ 已 Star ${args.repo}` : `❌ ${r.data?.message || JSON.stringify(r.data)}`;
48
+ }
49
+ case 'unstar': {
50
+ const r = await gh.unstarRepo(args.repo);
51
+ return r.ok ? `💔 已取消 Star ${args.repo}` : `❌ ${r.data?.message || JSON.stringify(r.data)}`;
52
+ }
53
+ case 'check': {
54
+ const starred = await gh.isStarred(args.repo);
55
+ return starred ? `⭐ 已 Star ${args.repo}` : `☆ 尚未 Star ${args.repo}`;
56
+ }
57
+ default:
58
+ return `❌ 未知操作: ${args.action}`;
59
+ }
60
+ }
61
+
62
+ export async function executeGithubBind(_args: Record<string, never>, commMessage?: Message) {
63
+ const adapter = getAdapter();
64
+ const log = depsLogger();
65
+ const msg = commMessage ?? getCurrentCommMessage();
66
+ if (!msg?.$adapter || !msg?.$sender?.id) return '❌ 无法获取当前用户信息';
67
+
68
+ const clientId = adapter.getClientId();
69
+ if (!clientId) return '❌ Endpoint 未配置 GitHub App 或 App 无 client_id,无法进行账号绑定';
70
+
71
+ const model = oauthModel();
72
+ if (!model) return '❌ 数据库未就绪';
73
+
74
+ const [existing] = await model.select().where({ platform: msg.$adapter, platform_uid: msg.$sender.id });
75
+ if (existing) {
76
+ return `⚠️ 你已绑定 GitHub 账号: ${existing.github_login}\n如需重新绑定,请先执行 github_unbind`;
77
+ }
78
+
79
+ try {
80
+ const host = adapter.getHost();
81
+ const codeResp = await GhClient.deviceFlowRequestCode(clientId, host);
82
+ const tokenPromise = GhClient.deviceFlowPollToken(
83
+ clientId, codeResp.device_code, codeResp.interval, codeResp.expires_in, host,
84
+ );
85
+
86
+ const replyMsg = [
87
+ '🔗 请在浏览器中打开以下链接进行授权:',
88
+ ` ${codeResp.verification_uri}`,
89
+ '',
90
+ `📋 输入验证码: **${codeResp.user_code}**`,
91
+ '',
92
+ `⏳ 等待授权中…(${Math.floor(codeResp.expires_in / 60)} 分钟内有效)`,
93
+ ].join('\n');
94
+
95
+ tokenPromise.then(async (tokenData) => {
96
+ if (!tokenData) {
97
+ log.warn(formatCompact({ op: 'device_flow', ok: false, platform: msg.$adapter, sender: msg.$sender.id }));
98
+ return;
99
+ }
100
+
101
+ const userGh = new GhClient({ host, token: tokenData.access_token });
102
+ const authResult = await userGh.verifyAuth();
103
+ const login = authResult.ok ? authResult.user : 'unknown';
104
+
105
+ await model.insert({
106
+ id: Date.now(),
107
+ platform: msg.$adapter,
108
+ platform_uid: msg.$sender.id,
109
+ github_login: login,
110
+ access_token: tokenData.access_token,
111
+ created_at: Date.now(),
112
+ });
113
+ log.debug(formatCompact({ op: 'bind', platform: msg.$adapter, sender: msg.$sender.id, login }));
114
+
115
+ if (msg?.$reply) {
116
+ await msg.$reply(`✅ GitHub 账号绑定成功!\n👤 ${login}`);
117
+ }
118
+ }).catch((err) => {
119
+ log.error('GitHub Device Flow 错误:', err);
120
+ });
121
+
122
+ return replyMsg;
123
+ } catch (e: unknown) {
124
+ return `❌ Device Flow 启动失败: ${e instanceof Error ? e.message : String(e)}`;
125
+ }
126
+ }
127
+
128
+ export async function executeGithubUnbind(_args: Record<string, never>, commMessage?: Message) {
129
+ const msg = commMessage ?? getCurrentCommMessage();
130
+ if (!msg?.$adapter || !msg?.$sender?.id) return '❌ 无法获取当前用户信息';
131
+
132
+ const model = oauthModel();
133
+ if (!model) return '❌ 数据库未就绪';
134
+
135
+ const [existing] = await model.select().where({ platform: msg.$adapter, platform_uid: msg.$sender.id });
136
+ if (!existing) return '📭 你尚未绑定 GitHub 账号';
137
+
138
+ await model.delete().where({ id: existing.id });
139
+ return `✅ 已解除 GitHub 账号绑定: ${existing.github_login}`;
140
+ }
141
+
142
+ export async function executeGithubWhoami(_args: Record<string, never>, commMessage?: Message) {
143
+ const adapter = getAdapter();
144
+ const msg = commMessage ?? getCurrentCommMessage();
145
+ if (!msg?.$adapter || !msg?.$sender?.id) return '❌ 无法获取当前用户信息';
146
+
147
+ const model = oauthModel();
148
+ if (!model) return '❌ 数据库未就绪';
149
+
150
+ const [existing] = await model.select().where({ platform: msg.$adapter, platform_uid: msg.$sender.id });
151
+ if (!existing) return '📭 你尚未绑定 GitHub 账号\n🔗 使用 github_bind 绑定你的账号';
152
+
153
+ const userGh = new GhClient({ host: adapter.getHost(), token: existing.access_token });
154
+ const auth = await userGh.verifyAuth();
155
+ if (auth.ok) {
156
+ return `👤 已绑定 GitHub 账号: ${auth.user}\n📅 绑定时间: ${new Date(existing.created_at).toLocaleString('zh-CN')}`;
157
+ }
158
+ return `⚠️ 已绑定账号 ${existing.github_login},但 Token 已失效\n🔗 请执行 github_unbind 后重新 github_bind`;
159
+ }
160
+
161
+ export async function executeGithubInstall() {
162
+ const adapter = getAdapter();
163
+ const slug = adapter.getAppSlug();
164
+ if (!slug) return '❌ Endpoint 未配置 GitHub App';
165
+ const host = adapter.getHost() || 'github.com';
166
+ const installations = adapter.getInstallations();
167
+ let msg = `🔗 请点击以下链接安装 GitHub App 到你的仓库:\n https://${host}/apps/${slug}/installations/new`;
168
+ if (installations.length) {
169
+ msg += `\n\n📋 当前已安装 (${installations.length}):`;
170
+ for (const inst of installations) {
171
+ msg += `\n • ${inst.account.login} (${inst.account.type})`;
172
+ }
173
+ }
174
+ return msg;
175
+ }
176
+
177
+ export async function executeGithubSubscribe(args: { repo: string; events?: string }, commMessage?: Message) {
178
+ const msg = commMessage ?? getCurrentCommMessage();
179
+ if (!msg?.$adapter || !msg?.$sender.id || !msg?.$channel?.id || !msg?.$endpoint) {
180
+ return '❌ 无法获取当前聊天通道信息';
181
+ }
182
+
183
+ const model = subscriptionsModel();
184
+ if (!model) return '❌ 数据库未就绪';
185
+
186
+ const validEvents: EventType[] = ['push', 'issue', 'star', 'fork', 'unstar', 'pull_request'];
187
+ const events: EventType[] = args.events
188
+ ? args.events.split(',').map((s) => s.trim()).filter((e): e is EventType => validEvents.includes(e as EventType))
189
+ : validEvents;
190
+ if (!events.length) return `❌ 无效的事件类型,可选: ${validEvents.join(', ')}`;
191
+
192
+ const [existing] = await model.select().where({
193
+ repo: args.repo,
194
+ target_id: msg.$channel?.id,
195
+ adapter: msg.$adapter,
196
+ endpoint: msg.$endpoint,
197
+ });
198
+ if (existing) {
199
+ await model.update({ events, target_type: msg.$channel?.type || 'private' }).where({ id: existing.id });
200
+ return `✅ 已更新订阅 ${args.repo}\n📡 事件: ${events.join(', ')}`;
201
+ }
202
+
203
+ await model.insert({
204
+ id: Date.now(),
205
+ repo: args.repo,
206
+ events,
207
+ target_id: msg.$channel?.id,
208
+ target_type: msg.$channel?.type || 'private',
209
+ adapter: msg.$adapter,
210
+ endpoint: msg.$endpoint,
211
+ });
212
+ return `✅ 已订阅 ${args.repo}\n📡 事件: ${events.join(', ')}\n📌 通知将推送到当前通道`;
213
+ }
214
+
215
+ export async function executeGithubUnsubscribe(args: { repo: string }, commMessage?: Message) {
216
+ const msg = commMessage ?? getCurrentCommMessage();
217
+ if (!msg?.$adapter || !msg?.$channel?.id || !msg?.$endpoint) {
218
+ return '❌ 无法获取当前聊天通道信息';
219
+ }
220
+
221
+ const model = subscriptionsModel();
222
+ if (!model) return '❌ 数据库未就绪';
223
+
224
+ const [existing] = await model.select().where({
225
+ repo: args.repo,
226
+ target_id: msg.$channel?.id,
227
+ adapter: msg.$adapter,
228
+ endpoint: msg.$endpoint,
229
+ });
230
+ if (!existing) return `📭 当前通道未订阅 ${args.repo}`;
231
+
232
+ await model.delete().where({ id: existing.id });
233
+ return `✅ 已取消订阅 ${args.repo}`;
234
+ }
235
+
236
+ export async function executeGithubSubscriptions(_args: Record<string, never>, commMessage?: Message) {
237
+ const msg = commMessage ?? getCurrentCommMessage();
238
+ if (!msg?.$adapter || !msg?.$channel?.id || !msg?.$endpoint) {
239
+ return '❌ 无法获取当前聊天通道信息';
240
+ }
241
+
242
+ const model = subscriptionsModel();
243
+ if (!model) return '❌ 数据库未就绪';
244
+
245
+ const subs = await model.select().where({
246
+ target_id: msg.$channel?.id,
247
+ adapter: msg.$adapter,
248
+ endpoint: msg.$endpoint,
249
+ });
250
+ if (!subs?.length) return '📭 当前通道没有任何 GitHub 订阅';
251
+
252
+ return `📋 当前通道订阅 (${subs.length}):\n\n` +
253
+ subs.map((s: any) => {
254
+ const events = Array.isArray(s.events) ? s.events : [];
255
+ return ` 📦 ${s.repo}\n 📡 ${events.join(', ') || '(无事件)'}`;
256
+ }).join('\n\n');
257
+ }