@zhin.js/adapter-github 3.0.0 → 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.
- package/README.md +23 -12
- package/{skills/github/SKILL.md → agent/skills/github.md} +22 -5
- package/agent/tools/bind.ts +12 -0
- package/agent/tools/create_pr.ts +19 -0
- package/agent/tools/install.ts +12 -0
- package/agent/tools/patch_file.ts +18 -0
- package/agent/tools/prepare_workspace.ts +14 -0
- package/agent/tools/push_branch.ts +17 -0
- package/agent/tools/star.ts +15 -0
- package/agent/tools/subscribe.ts +16 -0
- package/agent/tools/subscriptions.ts +13 -0
- package/agent/tools/unbind.ts +12 -0
- package/agent/tools/unsubscribe.ts +15 -0
- package/agent/tools/whoami.ts +12 -0
- package/lib/agent/tools/bind.js +12 -0
- package/lib/agent/tools/bind.js.map +1 -0
- package/lib/agent/tools/create_pr.js +19 -0
- package/lib/agent/tools/create_pr.js.map +1 -0
- package/lib/agent/tools/install.js +12 -0
- package/lib/agent/tools/install.js.map +1 -0
- package/lib/agent/tools/patch_file.js +18 -0
- package/lib/agent/tools/patch_file.js.map +1 -0
- package/lib/agent/tools/prepare_workspace.js +14 -0
- package/lib/agent/tools/prepare_workspace.js.map +1 -0
- package/lib/agent/tools/push_branch.js +17 -0
- package/lib/agent/tools/push_branch.js.map +1 -0
- package/lib/agent/tools/star.js +15 -0
- package/lib/agent/tools/star.js.map +1 -0
- package/lib/agent/tools/subscribe.js +16 -0
- package/lib/agent/tools/subscribe.js.map +1 -0
- package/lib/agent/tools/subscriptions.js +13 -0
- package/lib/agent/tools/subscriptions.js.map +1 -0
- package/lib/agent/tools/unbind.js +12 -0
- package/lib/agent/tools/unbind.js.map +1 -0
- package/lib/agent/tools/unsubscribe.js +15 -0
- package/lib/agent/tools/unsubscribe.js.map +1 -0
- package/lib/agent/tools/whoami.js +12 -0
- package/lib/agent/tools/whoami.js.map +1 -0
- package/lib/{adapter.js → src/adapter.js} +12 -6
- package/lib/src/adapter.js.map +1 -0
- package/lib/{agent-prompt.js → src/agent-prompt.js} +10 -8
- package/lib/src/agent-prompt.js.map +1 -0
- package/lib/{endpoint.js → src/endpoint.js} +37 -8
- package/lib/src/endpoint.js.map +1 -0
- package/lib/{gh-client.js → src/gh-client.js} +99 -0
- package/lib/src/gh-client.js.map +1 -0
- package/lib/src/github-agent-deps.js +13 -0
- package/lib/src/github-agent-deps.js.map +1 -0
- package/lib/src/github-bot-handlers.js +78 -0
- package/lib/src/github-bot-handlers.js.map +1 -0
- package/lib/src/github-channel-context.js +32 -0
- package/lib/src/github-channel-context.js.map +1 -0
- package/lib/src/github-tool-handlers.js +215 -0
- package/lib/src/github-tool-handlers.js.map +1 -0
- package/lib/src/index.js +104 -0
- package/lib/src/index.js.map +1 -0
- package/lib/{register-github-mcp.js → src/register-github-mcp.js} +5 -0
- package/lib/src/register-github-mcp.js.map +1 -0
- package/lib/src/segment-mapper.js +2 -0
- package/lib/src/segment-mapper.js.map +1 -0
- package/lib/src/types.js.map +1 -0
- package/lib/src/workspace-manager.js +109 -0
- package/lib/src/workspace-manager.js.map +1 -0
- package/package.json +16 -6
- package/src/adapter.ts +13 -8
- package/src/agent-prompt.ts +11 -10
- package/src/endpoint.ts +48 -14
- package/src/gh-client.ts +131 -0
- package/src/github-agent-deps.ts +27 -0
- package/src/github-bot-handlers.ts +109 -0
- package/src/github-channel-context.ts +46 -0
- package/src/github-tool-handlers.ts +244 -0
- package/src/index.ts +32 -326
- package/src/register-github-mcp.ts +5 -0
- package/src/segment-mapper.ts +1 -0
- package/src/types.ts +6 -0
- package/src/workspace-manager.ts +114 -0
- package/lib/adapter.d.ts +0 -65
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js.map +0 -1
- package/lib/agent-prompt.d.ts +0 -3
- package/lib/agent-prompt.d.ts.map +0 -1
- package/lib/agent-prompt.js.map +0 -1
- package/lib/endpoint.d.ts +0 -26
- package/lib/endpoint.d.ts.map +0 -1
- package/lib/endpoint.js.map +0 -1
- package/lib/gh-client.d.ts +0 -228
- package/lib/gh-client.d.ts.map +0 -1
- package/lib/gh-client.js.map +0 -1
- package/lib/index.d.ts +0 -34
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js +0 -388
- package/lib/index.js.map +0 -1
- package/lib/register-github-mcp.d.ts +0 -7
- package/lib/register-github-mcp.d.ts.map +0 -1
- package/lib/register-github-mcp.js.map +0 -1
- package/lib/types.d.ts +0 -144
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js.map +0 -1
- /package/lib/{types.js → src/types.js} +0 -0
package/src/agent-prompt.ts
CHANGED
|
@@ -4,8 +4,7 @@ import type {
|
|
|
4
4
|
AgentPromptSection,
|
|
5
5
|
DeferredToolCatalogItem,
|
|
6
6
|
} from 'zhin.js';
|
|
7
|
-
import type
|
|
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_"
|
|
51
|
-
'Discuss issues/PRs in chat context; do not call github_*
|
|
52
|
-
'
|
|
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
|
-
'
|
|
57
|
-
'
|
|
58
|
-
'
|
|
59
|
-
'
|
|
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
|
@@ -3,23 +3,22 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { formatCompact, Endpoint, Message, segment, SendContent, SendOptions, type MessageSegment,
|
|
5
5
|
coerceQrcodeSegmentsToText,
|
|
6
|
+
expandInteractiveSegmentsInContent,
|
|
6
7
|
} from 'zhin.js';
|
|
7
|
-
import type
|
|
8
|
-
|
|
9
|
-
IssueCommentPayload,
|
|
10
|
-
PRReviewCommentPayload,
|
|
11
|
-
PRReviewPayload,
|
|
12
|
-
} from './types.js';
|
|
13
|
-
import { buildChannelId, parseChannelId } from './types.js';
|
|
8
|
+
import { buildChannelId, parseChannelId, type GitHubEndpointConfig, type IssueCommentPayload, type PRReviewCommentPayload, type PRReviewPayload } from './types.js';
|
|
9
|
+
|
|
14
10
|
import type { GitHubAdapter } from './adapter.js';
|
|
15
11
|
import { GhClient } from './gh-client.js';
|
|
12
|
+
import { fromCanonicalSegments, toCanonicalSegments } from './segment-mapper.js';
|
|
13
|
+
|
|
14
|
+
/** GitHub @mention:支持 zhin-ai[bot] 等 App bot 用户名 */
|
|
15
|
+
const GITHUB_MENTION_RE = /@([^\s@]+(?:\[bot\])?)/g;
|
|
16
16
|
|
|
17
17
|
export function parseMarkdown(md: string): MessageSegment[] {
|
|
18
18
|
const segments: MessageSegment[] = [];
|
|
19
|
-
const mentionRe = /@(\w[-\w]*)/g;
|
|
20
19
|
let lastIdx = 0;
|
|
21
20
|
let match: RegExpExecArray | null;
|
|
22
|
-
while ((match =
|
|
21
|
+
while ((match = GITHUB_MENTION_RE.exec(md)) !== null) {
|
|
23
22
|
if (match.index > lastIdx) segments.push({ type: 'text', data: { text: md.slice(lastIdx, match.index) } });
|
|
24
23
|
segments.push({ type: 'at', data: { id: match[1], name: match[1], text: match[0] } });
|
|
25
24
|
lastIdx = match.index + match[0].length;
|
|
@@ -28,12 +27,41 @@ export function parseMarkdown(md: string): MessageSegment[] {
|
|
|
28
27
|
return segments.length ? segments : [{ type: 'text', data: { text: md } }];
|
|
29
28
|
}
|
|
30
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
|
+
|
|
31
58
|
export function toMarkdown(content: SendContent): string {
|
|
32
59
|
if (!Array.isArray(content)) content = [content];
|
|
33
60
|
return content.map(seg => {
|
|
34
61
|
if (typeof seg === 'string') return seg;
|
|
35
62
|
switch (seg.type) {
|
|
36
63
|
case 'text': return seg.data.text || '';
|
|
64
|
+
case 'mention': return `@${seg.data.name || seg.data.target}`;
|
|
37
65
|
case 'at': return `@${seg.data.name || seg.data.id}`;
|
|
38
66
|
case 'image': return seg.data.url ? `` : '[image]';
|
|
39
67
|
case 'link': return `[${seg.data.text || seg.data.url}](${seg.data.url})`;
|
|
@@ -45,6 +73,8 @@ export function toMarkdown(content: SendContent): string {
|
|
|
45
73
|
export class GitHubEndpoint implements Endpoint<GitHubEndpointConfig, IssueCommentPayload> {
|
|
46
74
|
$connected = false;
|
|
47
75
|
gh: GhClient;
|
|
76
|
+
/** App bot 登录名,供 @ 触发匹配 */
|
|
77
|
+
$platformUserId?: string;
|
|
48
78
|
|
|
49
79
|
get $id() { return this.$config.name; }
|
|
50
80
|
|
|
@@ -63,8 +93,9 @@ export class GitHubEndpoint implements Endpoint<GitHubEndpointConfig, IssueComme
|
|
|
63
93
|
async $connect(): Promise<void> {
|
|
64
94
|
const result = await this.gh.verifyAuth();
|
|
65
95
|
if (!result.ok) throw new Error(`GitHub 认证失败: ${result.message}`);
|
|
96
|
+
this.$platformUserId = this.$config.bot_login || this.gh.getBotLogin() || undefined;
|
|
66
97
|
this.$connected = true;
|
|
67
|
-
this.logger.
|
|
98
|
+
this.logger.debug(formatCompact({ endpoint: this.$id, bot: this.$platformUserId }));
|
|
68
99
|
}
|
|
69
100
|
|
|
70
101
|
async $disconnect(): Promise<void> {
|
|
@@ -85,7 +116,7 @@ export class GitHubEndpoint implements Endpoint<GitHubEndpointConfig, IssueComme
|
|
|
85
116
|
$endpoint: this.$config.name,
|
|
86
117
|
$sender: { id: payload.sender.login, name: payload.sender.login },
|
|
87
118
|
$channel: { id: channelId, type: 'group' },
|
|
88
|
-
$content: parseMarkdown(payload.comment.body),
|
|
119
|
+
$content: toCanonicalSegments(parseMarkdown(payload.comment.body)),
|
|
89
120
|
$raw: payload.comment.body,
|
|
90
121
|
$timestamp: new Date(payload.comment.created_at).getTime(),
|
|
91
122
|
$recall: async () => { await gh.deleteIssueComment(repo, payload.comment.id); },
|
|
@@ -115,7 +146,7 @@ export class GitHubEndpoint implements Endpoint<GitHubEndpointConfig, IssueComme
|
|
|
115
146
|
$endpoint: this.$config.name,
|
|
116
147
|
$sender: { id: payload.sender.login, name: payload.sender.login },
|
|
117
148
|
$channel: { id: channelId, type: 'group' },
|
|
118
|
-
$content: parseMarkdown(body),
|
|
149
|
+
$content: toCanonicalSegments(parseMarkdown(body)),
|
|
119
150
|
$raw: body,
|
|
120
151
|
$timestamp: new Date(payload.comment.created_at).getTime(),
|
|
121
152
|
$recall: async () => { await gh.deletePRReviewComment(repo, payload.comment.id); },
|
|
@@ -145,7 +176,7 @@ export class GitHubEndpoint implements Endpoint<GitHubEndpointConfig, IssueComme
|
|
|
145
176
|
$endpoint: this.$config.name,
|
|
146
177
|
$sender: { id: payload.sender.login, name: payload.sender.login },
|
|
147
178
|
$channel: { id: channelId, type: 'group' },
|
|
148
|
-
$content: parseMarkdown(body),
|
|
179
|
+
$content: toCanonicalSegments(parseMarkdown(body)),
|
|
149
180
|
$raw: body,
|
|
150
181
|
$timestamp: new Date(payload.review.submitted_at).getTime(),
|
|
151
182
|
$recall: async () => {},
|
|
@@ -160,7 +191,10 @@ export class GitHubEndpoint implements Endpoint<GitHubEndpointConfig, IssueComme
|
|
|
160
191
|
const parsed = parseChannelId(options.id);
|
|
161
192
|
if (!parsed) throw new Error(`无效的 GitHub channel ID: ${options.id}`);
|
|
162
193
|
|
|
163
|
-
const
|
|
194
|
+
const expanded = expandInteractiveSegmentsInContent(coerceQrcodeSegmentsToText(options.content ?? ''));
|
|
195
|
+
const arr = Array.isArray(expanded) ? expanded : [expanded];
|
|
196
|
+
const canonical = arr.map((s) => (typeof s === 'string' ? { type: 'text' as const, data: { text: s } } : s));
|
|
197
|
+
const text = toMarkdown(fromCanonicalSegments(canonical));
|
|
164
198
|
const r = parsed.type === 'issue'
|
|
165
199
|
? await this.gh.createIssueComment(parsed.repo, parsed.number, text)
|
|
166
200
|
: await this.gh.createPRComment(parsed.repo, parsed.number, text);
|
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
|
+
}
|