@zhin.js/adapter-github 3.0.1 → 3.0.2

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 (102) hide show
  1. package/README.md +23 -12
  2. package/{skills/github/SKILL.md → agent/skills/github.md} +22 -5
  3. package/agent/tools/bind.ts +12 -0
  4. package/agent/tools/create_pr.ts +19 -0
  5. package/agent/tools/install.ts +12 -0
  6. package/agent/tools/patch_file.ts +18 -0
  7. package/agent/tools/prepare_workspace.ts +14 -0
  8. package/agent/tools/push_branch.ts +17 -0
  9. package/agent/tools/star.ts +15 -0
  10. package/agent/tools/subscribe.ts +16 -0
  11. package/agent/tools/subscriptions.ts +13 -0
  12. package/agent/tools/unbind.ts +12 -0
  13. package/agent/tools/unsubscribe.ts +15 -0
  14. package/agent/tools/whoami.ts +12 -0
  15. package/lib/agent/tools/bind.js +12 -0
  16. package/lib/agent/tools/bind.js.map +1 -0
  17. package/lib/agent/tools/create_pr.js +19 -0
  18. package/lib/agent/tools/create_pr.js.map +1 -0
  19. package/lib/agent/tools/install.js +12 -0
  20. package/lib/agent/tools/install.js.map +1 -0
  21. package/lib/agent/tools/patch_file.js +18 -0
  22. package/lib/agent/tools/patch_file.js.map +1 -0
  23. package/lib/agent/tools/prepare_workspace.js +14 -0
  24. package/lib/agent/tools/prepare_workspace.js.map +1 -0
  25. package/lib/agent/tools/push_branch.js +17 -0
  26. package/lib/agent/tools/push_branch.js.map +1 -0
  27. package/lib/agent/tools/star.js +15 -0
  28. package/lib/agent/tools/star.js.map +1 -0
  29. package/lib/agent/tools/subscribe.js +16 -0
  30. package/lib/agent/tools/subscribe.js.map +1 -0
  31. package/lib/agent/tools/subscriptions.js +13 -0
  32. package/lib/agent/tools/subscriptions.js.map +1 -0
  33. package/lib/agent/tools/unbind.js +12 -0
  34. package/lib/agent/tools/unbind.js.map +1 -0
  35. package/lib/agent/tools/unsubscribe.js +15 -0
  36. package/lib/agent/tools/unsubscribe.js.map +1 -0
  37. package/lib/agent/tools/whoami.js +12 -0
  38. package/lib/agent/tools/whoami.js.map +1 -0
  39. package/lib/{adapter.js → src/adapter.js} +11 -6
  40. package/lib/src/adapter.js.map +1 -0
  41. package/lib/{agent-prompt.js → src/agent-prompt.js} +10 -8
  42. package/lib/src/agent-prompt.js.map +1 -0
  43. package/lib/{endpoint.js → src/endpoint.js} +27 -3
  44. package/lib/src/endpoint.js.map +1 -0
  45. package/lib/{gh-client.js → src/gh-client.js} +99 -0
  46. package/lib/src/gh-client.js.map +1 -0
  47. package/lib/src/github-agent-deps.js +13 -0
  48. package/lib/src/github-agent-deps.js.map +1 -0
  49. package/lib/src/github-bot-handlers.js +78 -0
  50. package/lib/src/github-bot-handlers.js.map +1 -0
  51. package/lib/src/github-channel-context.js +32 -0
  52. package/lib/src/github-channel-context.js.map +1 -0
  53. package/lib/src/github-tool-handlers.js +215 -0
  54. package/lib/src/github-tool-handlers.js.map +1 -0
  55. package/lib/src/index.js +104 -0
  56. package/lib/src/index.js.map +1 -0
  57. package/lib/{register-github-mcp.js → src/register-github-mcp.js} +5 -0
  58. package/lib/src/register-github-mcp.js.map +1 -0
  59. package/lib/src/segment-mapper.js.map +1 -0
  60. package/lib/src/types.js.map +1 -0
  61. package/lib/src/workspace-manager.js +109 -0
  62. package/lib/src/workspace-manager.js.map +1 -0
  63. package/package.json +16 -6
  64. package/src/adapter.ts +12 -8
  65. package/src/agent-prompt.ts +11 -10
  66. package/src/endpoint.ts +38 -10
  67. package/src/gh-client.ts +131 -0
  68. package/src/github-agent-deps.ts +27 -0
  69. package/src/github-bot-handlers.ts +109 -0
  70. package/src/github-channel-context.ts +46 -0
  71. package/src/github-tool-handlers.ts +244 -0
  72. package/src/index.ts +32 -326
  73. package/src/register-github-mcp.ts +5 -0
  74. package/src/types.ts +6 -0
  75. package/src/workspace-manager.ts +114 -0
  76. package/lib/adapter.d.ts +0 -66
  77. package/lib/adapter.d.ts.map +0 -1
  78. package/lib/adapter.js.map +0 -1
  79. package/lib/agent-prompt.d.ts +0 -3
  80. package/lib/agent-prompt.d.ts.map +0 -1
  81. package/lib/agent-prompt.js.map +0 -1
  82. package/lib/endpoint.d.ts +0 -26
  83. package/lib/endpoint.d.ts.map +0 -1
  84. package/lib/endpoint.js.map +0 -1
  85. package/lib/gh-client.d.ts +0 -228
  86. package/lib/gh-client.d.ts.map +0 -1
  87. package/lib/gh-client.js.map +0 -1
  88. package/lib/index.d.ts +0 -34
  89. package/lib/index.d.ts.map +0 -1
  90. package/lib/index.js +0 -388
  91. package/lib/index.js.map +0 -1
  92. package/lib/register-github-mcp.d.ts +0 -7
  93. package/lib/register-github-mcp.d.ts.map +0 -1
  94. package/lib/register-github-mcp.js.map +0 -1
  95. package/lib/segment-mapper.d.ts +0 -2
  96. package/lib/segment-mapper.d.ts.map +0 -1
  97. package/lib/segment-mapper.js.map +0 -1
  98. package/lib/types.d.ts +0 -144
  99. package/lib/types.d.ts.map +0 -1
  100. package/lib/types.js.map +0 -1
  101. /package/lib/{segment-mapper.js → src/segment-mapper.js} +0 -0
  102. /package/lib/{types.js → src/types.js} +0 -0
@@ -4,8 +4,7 @@ import type {
4
4
  AgentPromptSection,
5
5
  DeferredToolCatalogItem,
6
6
  } from 'zhin.js';
7
- import type { AgentTool } from 'zhin.js/ai';
8
- import { filterTools } from 'zhin.js/ai';
7
+ import { filterTools, type AgentTool } from 'zhin.js/ai';
9
8
 
10
9
  function selectGithubDeferredTools(
11
10
  query: string,
@@ -21,8 +20,8 @@ function selectGithubDeferredTools(
21
20
  if (bash) pinned.push(bash);
22
21
 
23
22
  const preferNames = [
24
- ...pool.filter(t => t.name.startsWith('mcp_github_')).map(t => t.name),
25
23
  ...pool.filter(t => t.name.startsWith('github_')).map(t => t.name),
24
+ ...pool.filter(t => t.name.startsWith('mcp_github_')).map(t => t.name),
26
25
  ];
27
26
  for (const name of preferNames) {
28
27
  if (pinned.length >= maxTools) break;
@@ -47,16 +46,18 @@ function isGithubDelegatedTask(query: string, goal: string): boolean {
47
46
  }
48
47
 
49
48
  const ORCHESTRATOR_GITHUB = [
50
- 'On GitHub: use run_deferred_task with tool_query "github_" or "mcp_github_" or "gh issue"/"gh pr".',
51
- 'Discuss issues/PRs in chat context; do not call github_* or mcp_github_* tools on this orchestrator.',
52
- 'Skip tool_search when the user clearly names a repo, issue number, or PR.',
49
+ 'On GitHub: use run_deferred_task with tool_query "github_".',
50
+ 'Discuss issues/PRs in chat context; do not call github_* tools on this orchestrator.',
51
+ 'Bot write operations use github_* tools (Installation Token), not mcp_github_*.',
53
52
  ].join('\n');
54
53
 
55
54
  const WORKER_GITHUB = [
56
- 'Prefer `gh` via bash for repo operations when bash is available.',
57
- 'Use mcp_github_* or github_* plugin tools for structured API actions.',
58
- 'Do not use mcp_filesystem_* or unrelated MCP servers to "discover" GitHub.',
59
- 'Summarize outcomes (issue link, PR state) for the orchestrator.',
55
+ 'Use github_prepare_workspace before multi-file edits in a repo.',
56
+ 'Small single-file change: github_patch_file (Contents API).',
57
+ 'Multi-file / tests: workspace + bash, then github_push_branch (requires approval) and github_create_pr for Issues.',
58
+ 'Issue thread: new branch + new PR. PR thread: push to existing PR head branch.',
59
+ 'Do NOT use mcp_github_* for writes — PAT acts as human, not Bot.',
60
+ 'Summarize outcomes (PR link, branch) for the orchestrator.',
60
61
  ].map(line => `- ${line}`).join('\n');
61
62
 
62
63
  export function createGithubAgentPromptContributor(): AgentPromptContributor {
package/src/endpoint.ts CHANGED
@@ -5,23 +5,20 @@ import { formatCompact, Endpoint, Message, segment, SendContent, SendOptions, ty
5
5
  coerceQrcodeSegmentsToText,
6
6
  expandInteractiveSegmentsInContent,
7
7
  } from 'zhin.js';
8
- import type {
9
- GitHubEndpointConfig,
10
- IssueCommentPayload,
11
- PRReviewCommentPayload,
12
- PRReviewPayload,
13
- } from './types.js';
14
- import { buildChannelId, parseChannelId } from './types.js';
8
+ import { buildChannelId, parseChannelId, type GitHubEndpointConfig, type IssueCommentPayload, type PRReviewCommentPayload, type PRReviewPayload } from './types.js';
9
+
15
10
  import type { GitHubAdapter } from './adapter.js';
16
11
  import { GhClient } from './gh-client.js';
17
12
  import { fromCanonicalSegments, toCanonicalSegments } from './segment-mapper.js';
18
13
 
14
+ /** GitHub @mention:支持 zhin-ai[bot] 等 App bot 用户名 */
15
+ const GITHUB_MENTION_RE = /@([^\s@]+(?:\[bot\])?)/g;
16
+
19
17
  export function parseMarkdown(md: string): MessageSegment[] {
20
18
  const segments: MessageSegment[] = [];
21
- const mentionRe = /@(\w[-\w]*)/g;
22
19
  let lastIdx = 0;
23
20
  let match: RegExpExecArray | null;
24
- while ((match = mentionRe.exec(md)) !== null) {
21
+ while ((match = GITHUB_MENTION_RE.exec(md)) !== null) {
25
22
  if (match.index > lastIdx) segments.push({ type: 'text', data: { text: md.slice(lastIdx, match.index) } });
26
23
  segments.push({ type: 'at', data: { id: match[1], name: match[1], text: match[0] } });
27
24
  lastIdx = match.index + match[0].length;
@@ -30,6 +27,34 @@ export function parseMarkdown(md: string): MessageSegment[] {
30
27
  return segments.length ? segments : [{ type: 'text', data: { text: md } }];
31
28
  }
32
29
 
30
+ export function shouldAutoReplyRepo(config: GitHubEndpointConfig, repo: string): boolean {
31
+ const list = config.auto_reply_repos;
32
+ if (!list?.length) return false;
33
+ const key = repo.toLowerCase();
34
+ return list.some((r) => r.toLowerCase() === key);
35
+ }
36
+
37
+ export function enrichGithubInboundMessage<T extends object>(
38
+ message: Message<T>,
39
+ config: GitHubEndpointConfig,
40
+ gh: GhClient,
41
+ repo: string,
42
+ ): Message<T> {
43
+ const botLogin = config.bot_login || gh.getBotLogin();
44
+ if (botLogin && shouldAutoReplyRepo(config, repo)) {
45
+ const hasBotAt = message.$content.some(
46
+ (seg) => (seg.type === 'at' || seg.type === 'mention') && seg.data?.id === botLogin,
47
+ );
48
+ if (!hasBotAt) {
49
+ message.$content = [
50
+ { type: 'at', data: { id: botLogin, name: botLogin, text: `@${botLogin}` } },
51
+ ...message.$content,
52
+ ];
53
+ }
54
+ }
55
+ return message;
56
+ }
57
+
33
58
  export function toMarkdown(content: SendContent): string {
34
59
  if (!Array.isArray(content)) content = [content];
35
60
  return content.map(seg => {
@@ -48,6 +73,8 @@ export function toMarkdown(content: SendContent): string {
48
73
  export class GitHubEndpoint implements Endpoint<GitHubEndpointConfig, IssueCommentPayload> {
49
74
  $connected = false;
50
75
  gh: GhClient;
76
+ /** App bot 登录名,供 @ 触发匹配 */
77
+ $platformUserId?: string;
51
78
 
52
79
  get $id() { return this.$config.name; }
53
80
 
@@ -66,8 +93,9 @@ export class GitHubEndpoint implements Endpoint<GitHubEndpointConfig, IssueComme
66
93
  async $connect(): Promise<void> {
67
94
  const result = await this.gh.verifyAuth();
68
95
  if (!result.ok) throw new Error(`GitHub 认证失败: ${result.message}`);
96
+ this.$platformUserId = this.$config.bot_login || this.gh.getBotLogin() || undefined;
69
97
  this.$connected = true;
70
- this.logger.info(formatCompact({ endpoint: this.$id }));
98
+ this.logger.debug(formatCompact({ endpoint: this.$id, bot: this.$platformUserId }));
71
99
  }
72
100
 
73
101
  async $disconnect(): Promise<void> {
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:${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,27 @@
1
+ /**
2
+ * Shared runtime deps for github agent/ authoring tools.
3
+ */
4
+ import type { Plugin } from 'zhin.js';
5
+ import type { GitHubAdapter } from './adapter.js';
6
+ import type { WorkspaceManager } from './workspace-manager.js';
7
+
8
+ export interface GithubAgentDeps {
9
+ getAdapter: () => GitHubAdapter;
10
+ getWorkspaceManager: () => WorkspaceManager;
11
+ plugin: Plugin;
12
+ }
13
+
14
+ let _deps: GithubAgentDeps | null = null;
15
+
16
+ export function setGithubAgentDeps(deps: GithubAgentDeps): void {
17
+ _deps = deps;
18
+ }
19
+
20
+ export function getGithubAgentDeps(): GithubAgentDeps {
21
+ if (!_deps) throw new Error('github agent deps not initialized');
22
+ return _deps;
23
+ }
24
+
25
+ export function getAdapter(): GitHubAdapter {
26
+ return getGithubAgentDeps().getAdapter();
27
+ }
@@ -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
+ }