@zhin.js/adapter-github 1.0.1 → 1.1.0
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/CHANGELOG.md +1118 -0
- package/README.md +70 -191
- package/adapters/github.js +51 -0
- package/adapters/github.ts +59 -0
- package/agent/prompt-sections/platform.ts +16 -0
- package/{skills/github/SKILL.md → agent/skills/github.md} +22 -5
- package/agent/tools/bind.ts +13 -0
- package/agent/tools/create_pr.ts +20 -0
- package/agent/tools/install.ts +13 -0
- package/agent/tools/patch_file.ts +19 -0
- package/agent/tools/prepare_workspace.ts +15 -0
- package/agent/tools/push_branch.ts +18 -0
- package/agent/tools/star.ts +16 -0
- package/agent/tools/subscribe.ts +16 -0
- package/agent/tools/subscriptions.ts +13 -0
- package/agent/tools/unbind.ts +13 -0
- package/agent/tools/unsubscribe.ts +15 -0
- package/agent/tools/whoami.ts +13 -0
- package/commands/endpoint/add/[id].js +3 -0
- package/commands/endpoint/add/[id].ts +3 -0
- package/commands/endpoint/list.js +3 -0
- package/commands/endpoint/list.ts +3 -0
- package/commands/endpoint/remove/[id].js +3 -0
- package/commands/endpoint/remove/[id].ts +3 -0
- package/lib/client.d.ts +36 -0
- package/lib/client.js +47 -0
- package/lib/endpoint.d.ts +32 -22
- package/lib/endpoint.js +125 -145
- package/lib/gh-client.d.ts +73 -1
- package/lib/gh-client.js +99 -1
- package/lib/github-bot-handlers.d.ts +27 -0
- package/lib/github-bot-handlers.js +76 -0
- package/lib/github-channel-context.d.ts +16 -0
- package/lib/github-channel-context.js +31 -0
- package/lib/github-endpoint-commands.d.ts +1 -0
- package/lib/github-endpoint-commands.js +22 -0
- package/lib/github-runtime-state.d.ts +1 -0
- package/lib/github-runtime-state.js +6 -0
- package/lib/github-tool-handlers.d.ts +18 -0
- package/lib/github-tool-handlers.js +214 -0
- package/lib/index.d.ts +7 -32
- package/lib/index.js +7 -385
- package/lib/oauth-users.d.ts +33 -0
- package/lib/oauth-users.js +38 -0
- package/lib/protocol.d.ts +94 -0
- package/lib/protocol.js +292 -0
- package/lib/types.d.ts +6 -1
- package/lib/types.js +0 -1
- package/lib/webhook.d.ts +14 -0
- package/lib/webhook.js +88 -0
- package/lib/workspace-manager.d.ts +21 -0
- package/lib/workspace-manager.js +154 -0
- package/package.json +77 -23
- package/plugin.js +38 -0
- package/schema.json +130 -0
- package/src/client.ts +65 -0
- package/src/endpoint.ts +145 -150
- package/src/gh-client.ts +131 -0
- package/src/github-bot-handlers.ts +113 -0
- package/src/github-channel-context.ts +46 -0
- package/src/github-endpoint-commands.ts +23 -0
- package/src/github-runtime-state.ts +7 -0
- package/src/github-tool-handlers.ts +252 -0
- package/src/index.ts +48 -431
- package/src/oauth-users.ts +47 -0
- package/src/protocol.ts +425 -0
- package/src/types.ts +6 -0
- package/src/webhook.ts +130 -0
- package/src/workspace-manager.ts +168 -0
- package/lib/adapter.d.ts +0 -64
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -416
- 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 +0 -82
- package/lib/agent-prompt.js.map +0 -1
- package/lib/endpoint.d.ts.map +0 -1
- package/lib/endpoint.js.map +0 -1
- package/lib/gh-client.d.ts.map +0 -1
- package/lib/gh-client.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/register-github-mcp.d.ts +0 -6
- package/lib/register-github-mcp.d.ts.map +0 -1
- package/lib/register-github-mcp.js +0 -35
- package/lib/register-github-mcp.js.map +0 -1
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js.map +0 -1
- package/plugin.yml +0 -3
- package/src/adapter.ts +0 -448
- package/src/agent-prompt.ts +0 -99
- package/src/register-github-mcp.ts +0 -60
package/lib/gh-client.js
CHANGED
|
@@ -181,6 +181,7 @@ export class GhClient {
|
|
|
181
181
|
this._appSlug = appData.slug || null;
|
|
182
182
|
if (appData.client_id)
|
|
183
183
|
this._clientId = appData.client_id;
|
|
184
|
+
await this.resolveBotUserId();
|
|
184
185
|
const repos = this._repoToInstallation.size;
|
|
185
186
|
return { ok: true, user: name, message: `GitHub App: ${name} (${this._allInstallations.length} 安装, ${repos} 仓库)` };
|
|
186
187
|
}
|
|
@@ -265,6 +266,95 @@ export class GhClient {
|
|
|
265
266
|
async deletePRReviewComment(repo, commentId) {
|
|
266
267
|
return this.del(`/repos/${repo}/pulls/comments/${commentId}`);
|
|
267
268
|
}
|
|
269
|
+
// ── Contents / Pulls(Bot 写操作)──────────────────────────────────
|
|
270
|
+
async getFileContent(repo, path, ref) {
|
|
271
|
+
let apiPath = `/repos/${repo}/contents/${path.split('/').map(encodeURIComponent).join('/')}`;
|
|
272
|
+
if (ref)
|
|
273
|
+
apiPath += `?ref=${encodeURIComponent(ref)}`;
|
|
274
|
+
return this.get(apiPath);
|
|
275
|
+
}
|
|
276
|
+
async createOrUpdateFile(repo, path, content, message, options) {
|
|
277
|
+
const body = {
|
|
278
|
+
message,
|
|
279
|
+
content: Buffer.from(content, 'utf-8').toString('base64'),
|
|
280
|
+
};
|
|
281
|
+
if (options?.branch)
|
|
282
|
+
body.branch = options.branch;
|
|
283
|
+
if (options?.sha)
|
|
284
|
+
body.sha = options.sha;
|
|
285
|
+
const apiPath = `/repos/${repo}/contents/${path.split('/').map(encodeURIComponent).join('/')}`;
|
|
286
|
+
return this.put(apiPath, body);
|
|
287
|
+
}
|
|
288
|
+
async getRef(repo, ref) {
|
|
289
|
+
return this.get(`/repos/${repo}/git/ref/${encodeURIComponent(ref)}`);
|
|
290
|
+
}
|
|
291
|
+
async createRef(repo, ref, sha) {
|
|
292
|
+
return this.post(`/repos/${repo}/git/refs`, { ref, sha });
|
|
293
|
+
}
|
|
294
|
+
async createPullRequest(repo, title, head, base, body) {
|
|
295
|
+
return this.post(`/repos/${repo}/pulls`, { title, head, base, body: body ?? '' });
|
|
296
|
+
}
|
|
297
|
+
getBotLogin() {
|
|
298
|
+
if (this._user)
|
|
299
|
+
return this._user;
|
|
300
|
+
if (this._appSlug)
|
|
301
|
+
return `${this._appSlug}[bot]`;
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
getCommitAuthor() {
|
|
305
|
+
const identity = this.getBotIdentitySync();
|
|
306
|
+
if (!identity)
|
|
307
|
+
return null;
|
|
308
|
+
return { name: identity.login, email: identity.email, date: new Date().toISOString() };
|
|
309
|
+
}
|
|
310
|
+
getBotIdentitySync() {
|
|
311
|
+
const slug = this._appSlug;
|
|
312
|
+
const login = this.getBotLogin();
|
|
313
|
+
if (!slug || !login || this._botUserId == null)
|
|
314
|
+
return null;
|
|
315
|
+
return {
|
|
316
|
+
login,
|
|
317
|
+
slug,
|
|
318
|
+
userId: this._botUserId,
|
|
319
|
+
email: `${this._botUserId}+${login}@users.noreply.github.com`,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
async getBotIdentity() {
|
|
323
|
+
if (!this._appAuth)
|
|
324
|
+
return null;
|
|
325
|
+
if (this._botUserId == null)
|
|
326
|
+
await this.resolveBotUserId();
|
|
327
|
+
return this.getBotIdentitySync();
|
|
328
|
+
}
|
|
329
|
+
buildCloneUrl(repo, token) {
|
|
330
|
+
const host = this.host || 'github.com';
|
|
331
|
+
return `https://x-access-token:${encodeURIComponent(token)}@${host}/${repo}.git`;
|
|
332
|
+
}
|
|
333
|
+
async resolveBotUserId() {
|
|
334
|
+
if (this._botUserId != null)
|
|
335
|
+
return this._botUserId;
|
|
336
|
+
const slug = this._appSlug;
|
|
337
|
+
if (!slug)
|
|
338
|
+
return null;
|
|
339
|
+
const login = `${slug}[bot]`;
|
|
340
|
+
const baseUrl = this.host ? `https://${this.host}/api/v3` : 'https://api.github.com';
|
|
341
|
+
try {
|
|
342
|
+
const res = await fetch(`${baseUrl}/users/${encodeURIComponent(login)}`, {
|
|
343
|
+
headers: { Accept: 'application/vnd.github+json' },
|
|
344
|
+
});
|
|
345
|
+
if (!res.ok)
|
|
346
|
+
return null;
|
|
347
|
+
const data = (await res.json());
|
|
348
|
+
if (typeof data.id === 'number') {
|
|
349
|
+
this._botUserId = data.id;
|
|
350
|
+
return data.id;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
268
358
|
// ── Star ─────────────────────────────────────────────────────────
|
|
269
359
|
async starRepo(repo) {
|
|
270
360
|
return this.put(`/user/starred/${repo}`);
|
|
@@ -384,6 +474,15 @@ export class GhClient {
|
|
|
384
474
|
get appSlug() { return this._appSlug; }
|
|
385
475
|
/** App 的 client_id(verifyAuth 后从 /app 获取,用于 Device Flow) */
|
|
386
476
|
_clientId = null;
|
|
477
|
+
/** Bot 机器用户 numeric id(/users/{slug}[bot]) */
|
|
478
|
+
_botUserId = null;
|
|
479
|
+
/** 确保 App 认证的 Installation Token 对指定 repo 有效,并返回 token */
|
|
480
|
+
async ensureInstallationTokenForRepo(repo) {
|
|
481
|
+
if (!this._appAuth)
|
|
482
|
+
return this.token;
|
|
483
|
+
await this.ensureTokenForRepo(repo);
|
|
484
|
+
return this.token;
|
|
485
|
+
}
|
|
387
486
|
get clientId() { return this._clientId; }
|
|
388
487
|
// ── 事件轮询 ─────────────────────────────────────────────────────
|
|
389
488
|
/**
|
|
@@ -495,4 +594,3 @@ export class GhClient {
|
|
|
495
594
|
return null;
|
|
496
595
|
}
|
|
497
596
|
}
|
|
498
|
-
//# sourceMappingURL=gh-client.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub App Bot workflow handlers (Installation Token identity).
|
|
3
|
+
*/
|
|
4
|
+
import type { Message } from 'zhin.js';
|
|
5
|
+
import type { GithubClient } from './client.js';
|
|
6
|
+
export declare function executeGithubPrepareWorkspace(args: {
|
|
7
|
+
repo?: string;
|
|
8
|
+
}, client: GithubClient, commMessage?: Message): Promise<string>;
|
|
9
|
+
export declare function executeGithubPatchFile(args: {
|
|
10
|
+
repo?: string;
|
|
11
|
+
path: string;
|
|
12
|
+
content: string;
|
|
13
|
+
message: string;
|
|
14
|
+
branch?: string;
|
|
15
|
+
}, client: GithubClient, commMessage?: Message): Promise<string>;
|
|
16
|
+
export declare function executeGithubPushBranch(args: {
|
|
17
|
+
repo?: string;
|
|
18
|
+
branch?: string;
|
|
19
|
+
message: string;
|
|
20
|
+
}, client: GithubClient, commMessage?: Message): Promise<string>;
|
|
21
|
+
export declare function executeGithubCreatePr(args: {
|
|
22
|
+
repo?: string;
|
|
23
|
+
title: string;
|
|
24
|
+
body?: string;
|
|
25
|
+
head?: string;
|
|
26
|
+
base?: string;
|
|
27
|
+
}, client: GithubClient, commMessage?: Message): Promise<string>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { getCurrentCommMessage } from '@zhin.js/agent/security';
|
|
2
|
+
import { parseMessageChannel, resolveWorkspaceBranch, formatChannelContext, } from './github-channel-context.js';
|
|
3
|
+
function requireBotGh(client) {
|
|
4
|
+
const gh = client.api;
|
|
5
|
+
if (!gh?.isAppAuth) {
|
|
6
|
+
throw new Error('需要 GitHub App 认证(app_id + private_key),Bot 写操作不可用');
|
|
7
|
+
}
|
|
8
|
+
return gh;
|
|
9
|
+
}
|
|
10
|
+
function resolveCommMessage(commMessage) {
|
|
11
|
+
const msg = commMessage ?? getCurrentCommMessage();
|
|
12
|
+
if (!msg)
|
|
13
|
+
throw new Error('无 IM 上下文,请在 Issue/PR 评论线程内调用');
|
|
14
|
+
return msg;
|
|
15
|
+
}
|
|
16
|
+
function resolveChannel(msg, repo) {
|
|
17
|
+
const ctx = parseMessageChannel(msg);
|
|
18
|
+
if (ctx)
|
|
19
|
+
return ctx;
|
|
20
|
+
if (repo) {
|
|
21
|
+
throw new Error(`请从 GitHub Issue/PR 频道调用,或提供完整 channel(当前仅 repo=${repo})`);
|
|
22
|
+
}
|
|
23
|
+
throw new Error('无法解析 GitHub 频道(需要 Issue/PR 评论上下文)');
|
|
24
|
+
}
|
|
25
|
+
export async function executeGithubPrepareWorkspace(args, client, commMessage) {
|
|
26
|
+
const msg = resolveCommMessage(commMessage);
|
|
27
|
+
const ctx = resolveChannel(msg, args.repo);
|
|
28
|
+
const gh = requireBotGh(client);
|
|
29
|
+
const wm = client.workspaceManager;
|
|
30
|
+
const { branch, base } = await resolveWorkspaceBranch(gh, ctx);
|
|
31
|
+
const repoPath = await wm.checkoutBranch(ctx.repo, branch, base);
|
|
32
|
+
return [
|
|
33
|
+
`✅ 工作区就绪`,
|
|
34
|
+
`📁 ${repoPath}`,
|
|
35
|
+
`🌿 分支 \`${branch}\`(base: \`${base}\`)`,
|
|
36
|
+
`📍 ${formatChannelContext(ctx)}`,
|
|
37
|
+
].join('\n');
|
|
38
|
+
}
|
|
39
|
+
export async function executeGithubPatchFile(args, client, commMessage) {
|
|
40
|
+
const msg = resolveCommMessage(commMessage);
|
|
41
|
+
const ctx = resolveChannel(msg, args.repo);
|
|
42
|
+
const gh = requireBotGh(client);
|
|
43
|
+
const { branch } = args.branch
|
|
44
|
+
? { branch: args.branch }
|
|
45
|
+
: await resolveWorkspaceBranch(gh, ctx);
|
|
46
|
+
const existing = await gh.getFileContent(ctx.repo, args.path, branch);
|
|
47
|
+
const sha = existing.ok ? existing.data.sha : undefined;
|
|
48
|
+
const r = await gh.createOrUpdateFile(ctx.repo, args.path, args.content, args.message, { branch, sha });
|
|
49
|
+
if (!r.ok) {
|
|
50
|
+
return `❌ 更新文件失败: ${JSON.stringify(r.data)}`;
|
|
51
|
+
}
|
|
52
|
+
const url = r.data.content?.html_url ?? r.data.commit?.html_url ?? '';
|
|
53
|
+
return `✅ 已更新 \`${args.path}\` @ \`${branch}\`${url ? `\n🔗 ${url}` : ''}`;
|
|
54
|
+
}
|
|
55
|
+
export async function executeGithubPushBranch(args, client, commMessage) {
|
|
56
|
+
const msg = resolveCommMessage(commMessage);
|
|
57
|
+
const ctx = resolveChannel(msg, args.repo);
|
|
58
|
+
const gh = requireBotGh(client);
|
|
59
|
+
const wm = client.workspaceManager;
|
|
60
|
+
const branch = args.branch ?? (await resolveWorkspaceBranch(gh, ctx)).branch;
|
|
61
|
+
const result = await wm.commitAndPush(ctx.repo, branch, args.message);
|
|
62
|
+
return `✅ ${result}\n📍 ${ctx.repo} @ \`${branch}\``;
|
|
63
|
+
}
|
|
64
|
+
export async function executeGithubCreatePr(args, client, commMessage) {
|
|
65
|
+
const msg = resolveCommMessage(commMessage);
|
|
66
|
+
const ctx = resolveChannel(msg, args.repo);
|
|
67
|
+
const gh = requireBotGh(client);
|
|
68
|
+
const resolved = await resolveWorkspaceBranch(gh, ctx);
|
|
69
|
+
const head = args.head ?? resolved.branch;
|
|
70
|
+
const base = args.base ?? resolved.base;
|
|
71
|
+
const r = await gh.createPullRequest(ctx.repo, args.title, head, base, args.body);
|
|
72
|
+
if (!r.ok) {
|
|
73
|
+
return `❌ 创建 PR 失败: ${JSON.stringify(r.data)}`;
|
|
74
|
+
}
|
|
75
|
+
return `✅ PR #${r.data.number} 已创建\n🔗 ${r.data.html_url}`;
|
|
76
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse GitHub Issue/PR channel context from IM messages.
|
|
3
|
+
*/
|
|
4
|
+
import type { Message } from 'zhin.js';
|
|
5
|
+
import { type ParsedChannel } from './types.js';
|
|
6
|
+
import type { GhClient } from './gh-client.js';
|
|
7
|
+
export interface GitHubChannelContext extends ParsedChannel {
|
|
8
|
+
channelId: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function parseMessageChannel(message: Message): GitHubChannelContext | null;
|
|
11
|
+
export declare function issueBranchName(number: number): string;
|
|
12
|
+
export declare function resolveWorkspaceBranch(gh: GhClient, ctx: GitHubChannelContext): Promise<{
|
|
13
|
+
branch: string;
|
|
14
|
+
base: string;
|
|
15
|
+
}>;
|
|
16
|
+
export declare function formatChannelContext(ctx: GitHubChannelContext): string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { buildChannelId, parseChannelId } from './types.js';
|
|
2
|
+
export function parseMessageChannel(message) {
|
|
3
|
+
const channelId = message.$channel?.id;
|
|
4
|
+
if (!channelId)
|
|
5
|
+
return null;
|
|
6
|
+
const parsed = parseChannelId(channelId);
|
|
7
|
+
if (!parsed)
|
|
8
|
+
return null;
|
|
9
|
+
return { ...parsed, channelId };
|
|
10
|
+
}
|
|
11
|
+
export function issueBranchName(number) {
|
|
12
|
+
return `zhin/bot/issue-${number}`;
|
|
13
|
+
}
|
|
14
|
+
export async function resolveWorkspaceBranch(gh, ctx) {
|
|
15
|
+
if (ctx.type === 'pr') {
|
|
16
|
+
const pr = await gh.getPR(ctx.repo, ctx.number);
|
|
17
|
+
if (pr.ok && pr.data?.head?.ref) {
|
|
18
|
+
const base = pr.data.base?.ref ?? 'main';
|
|
19
|
+
return { branch: pr.data.head.ref, base };
|
|
20
|
+
}
|
|
21
|
+
throw new Error(`无法读取 PR #${ctx.number} 的 head 分支`);
|
|
22
|
+
}
|
|
23
|
+
const repoInfo = await gh.getRepo(ctx.repo);
|
|
24
|
+
const base = repoInfo.ok && repoInfo.data?.default_branch
|
|
25
|
+
? String(repoInfo.data.default_branch)
|
|
26
|
+
: 'main';
|
|
27
|
+
return { branch: issueBranchName(ctx.number), base };
|
|
28
|
+
}
|
|
29
|
+
export function formatChannelContext(ctx) {
|
|
30
|
+
return buildChannelId(ctx.repo, ctx.type, ctx.number);
|
|
31
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const githubEndpointCommands: import("@zhin.js/adapter").EndpointCommands<Readonly<import("@zhin.js/command").CommandDefinition<unknown, unknown, import("@zhin.js/command").CommandMessage, string | undefined>>>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `github.endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
|
|
3
|
+
* commands/endpoint/ 下的 list / add / remove 直接默认导出这三项。
|
|
4
|
+
*
|
|
5
|
+
* 字段说明:private_key 内容长且含换行,不适合 kv 直传——运行时(gh-client
|
|
6
|
+
* resolvePrivateKey)本就支持 PEM 内容或文件路径,故 add 采用**内联路径**形式:
|
|
7
|
+
* `github.endpoint add mybot app_id=123456 private_key=./data/mybot.pem`。
|
|
8
|
+
*/
|
|
9
|
+
import { createEndpointCommands } from 'zhin.js/adapter';
|
|
10
|
+
import { defineCommand } from 'zhin.js/command';
|
|
11
|
+
import { githubRuntimeStateToken } from './github-runtime-state.js';
|
|
12
|
+
export const githubEndpointCommands = createEndpointCommands({
|
|
13
|
+
adapterKey: 'github',
|
|
14
|
+
adapterDisplayName: 'GitHub',
|
|
15
|
+
fields: [
|
|
16
|
+
{ key: 'app_id', required: true, env: true, description: 'GitHub App ID' },
|
|
17
|
+
{ key: 'private_key', required: true, description: 'PEM 文件路径(推荐,如 ./data/xxx.pem)或 PEM 内容' },
|
|
18
|
+
{ key: 'webhook_secret', env: true, description: 'webhook 签名密钥(不配则 API-only)' },
|
|
19
|
+
],
|
|
20
|
+
running: (use) => use(githubRuntimeStateToken).endpoints.values(),
|
|
21
|
+
describeEntry: (entry) => `app_id: ${String(entry.app_id ?? entry.appId)}`,
|
|
22
|
+
}, defineCommand);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const githubRuntimeStateToken: import("zhin.js").Token<import("@zhin.js/adapter").EndpointRuntimeState>;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
|
|
3
|
+
* 由 plugin.ts setup() provide,adapter create 与 `github.endpoint` 命令共享(同一 owner generation)。
|
|
4
|
+
*/
|
|
5
|
+
import { defineEndpointRuntimeStateToken } from 'zhin.js/adapter';
|
|
6
|
+
export const githubRuntimeStateToken = defineEndpointRuntimeStateToken('github');
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Message } from 'zhin.js';
|
|
2
|
+
import type { GithubClient } from './client.js';
|
|
3
|
+
export declare function executeGithubStar(args: {
|
|
4
|
+
action: 'star' | 'unstar' | 'check';
|
|
5
|
+
repo: string;
|
|
6
|
+
}, client: GithubClient, commMessage?: Message): Promise<string>;
|
|
7
|
+
export declare function executeGithubBind(_args: Record<string, never>, client: GithubClient, commMessage?: Message): Promise<string>;
|
|
8
|
+
export declare function executeGithubUnbind(_args: Record<string, never>, client: GithubClient, commMessage?: Message): Promise<string>;
|
|
9
|
+
export declare function executeGithubWhoami(_args: Record<string, never>, client: GithubClient, commMessage?: Message): Promise<string>;
|
|
10
|
+
export declare function executeGithubInstall(client: GithubClient): Promise<string>;
|
|
11
|
+
export declare function executeGithubSubscribe(args: {
|
|
12
|
+
repo: string;
|
|
13
|
+
events?: string;
|
|
14
|
+
}, client: GithubClient, commMessage?: Message): Promise<string>;
|
|
15
|
+
export declare function executeGithubUnsubscribe(args: {
|
|
16
|
+
repo: string;
|
|
17
|
+
}, client: GithubClient, commMessage?: Message): Promise<string>;
|
|
18
|
+
export declare function executeGithubSubscriptions(_args: Record<string, never>, client: GithubClient, commMessage?: Message): Promise<string>;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { formatCompact } from '@zhin.js/logger';
|
|
2
|
+
import { getCurrentCommMessage } from '@zhin.js/agent/security';
|
|
3
|
+
import { GhClient } from './gh-client.js';
|
|
4
|
+
function oauthModel(client) {
|
|
5
|
+
const db = client.database;
|
|
6
|
+
return db?.models?.get('github_oauth_users');
|
|
7
|
+
}
|
|
8
|
+
function subscriptionsModel(client) {
|
|
9
|
+
const db = client.database;
|
|
10
|
+
return db?.models?.get('github_subscriptions');
|
|
11
|
+
}
|
|
12
|
+
function depsLogger() {
|
|
13
|
+
return {
|
|
14
|
+
debug: (...args) => console.debug(...args),
|
|
15
|
+
warn: (...args) => console.warn(...args),
|
|
16
|
+
error: (...args) => console.error(...args),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export async function executeGithubStar(args, client, commMessage) {
|
|
20
|
+
const msg = commMessage ?? getCurrentCommMessage();
|
|
21
|
+
const gh = await client.getUserOrDefaultApi(msg?.$adapter, msg?.$sender.id);
|
|
22
|
+
switch (args.action) {
|
|
23
|
+
case 'star': {
|
|
24
|
+
const r = await gh.starRepo(args.repo);
|
|
25
|
+
return r.ok ? `⭐ 已 Star ${args.repo}` : `❌ ${r.data?.message || JSON.stringify(r.data)}`;
|
|
26
|
+
}
|
|
27
|
+
case 'unstar': {
|
|
28
|
+
const r = await gh.unstarRepo(args.repo);
|
|
29
|
+
return r.ok ? `💔 已取消 Star ${args.repo}` : `❌ ${r.data?.message || JSON.stringify(r.data)}`;
|
|
30
|
+
}
|
|
31
|
+
case 'check': {
|
|
32
|
+
const starred = await gh.isStarred(args.repo);
|
|
33
|
+
return starred ? `⭐ 已 Star ${args.repo}` : `☆ 尚未 Star ${args.repo}`;
|
|
34
|
+
}
|
|
35
|
+
default:
|
|
36
|
+
return `❌ 未知操作: ${args.action}`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export async function executeGithubBind(_args, client, commMessage) {
|
|
40
|
+
const log = depsLogger();
|
|
41
|
+
const msg = commMessage ?? getCurrentCommMessage();
|
|
42
|
+
if (!msg?.$adapter || !msg?.$sender?.id)
|
|
43
|
+
return '❌ 无法获取当前用户信息';
|
|
44
|
+
const clientId = client.clientId;
|
|
45
|
+
if (!clientId)
|
|
46
|
+
return '❌ Endpoint 未配置 GitHub App 或 App 无 client_id,无法进行账号绑定';
|
|
47
|
+
const model = oauthModel(client);
|
|
48
|
+
if (!model)
|
|
49
|
+
return '❌ 数据库未就绪';
|
|
50
|
+
const [existing] = await model.select().where({ platform: msg.$adapter, platform_uid: msg.$sender.id });
|
|
51
|
+
if (existing) {
|
|
52
|
+
return `⚠️ 你已绑定 GitHub 账号: ${existing.github_login}\n如需重新绑定,请先执行 github_unbind`;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const host = client.host;
|
|
56
|
+
const codeResp = await GhClient.deviceFlowRequestCode(clientId, host);
|
|
57
|
+
const tokenPromise = GhClient.deviceFlowPollToken(clientId, codeResp.device_code, codeResp.interval, codeResp.expires_in, host);
|
|
58
|
+
const replyMsg = [
|
|
59
|
+
'🔗 请在浏览器中打开以下链接进行授权:',
|
|
60
|
+
` ${codeResp.verification_uri}`,
|
|
61
|
+
'',
|
|
62
|
+
`📋 输入验证码: **${codeResp.user_code}**`,
|
|
63
|
+
'',
|
|
64
|
+
`⏳ 等待授权中…(${Math.floor(codeResp.expires_in / 60)} 分钟内有效)`,
|
|
65
|
+
].join('\n');
|
|
66
|
+
tokenPromise.then(async (tokenData) => {
|
|
67
|
+
if (!tokenData) {
|
|
68
|
+
log.warn(formatCompact({ op: 'device_flow', ok: false, platform: msg.$adapter, sender: msg.$sender.id }));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const userGh = new GhClient({ host, token: tokenData.access_token });
|
|
72
|
+
const authResult = await userGh.verifyAuth();
|
|
73
|
+
const login = authResult.ok ? authResult.user : 'unknown';
|
|
74
|
+
await model.insert({
|
|
75
|
+
id: Date.now(),
|
|
76
|
+
platform: msg.$adapter,
|
|
77
|
+
platform_uid: msg.$sender.id,
|
|
78
|
+
github_login: login,
|
|
79
|
+
access_token: tokenData.access_token,
|
|
80
|
+
created_at: Date.now(),
|
|
81
|
+
});
|
|
82
|
+
log.debug(formatCompact({ op: 'bind', platform: msg.$adapter, sender: msg.$sender.id, login }));
|
|
83
|
+
if (msg?.$reply) {
|
|
84
|
+
await msg.$reply(`✅ GitHub 账号绑定成功!\n👤 ${login}`);
|
|
85
|
+
}
|
|
86
|
+
}).catch((err) => {
|
|
87
|
+
log.error('GitHub Device Flow 错误:', err);
|
|
88
|
+
});
|
|
89
|
+
return replyMsg;
|
|
90
|
+
}
|
|
91
|
+
catch (e) {
|
|
92
|
+
return `❌ Device Flow 启动失败: ${e instanceof Error ? e.message : String(e)}`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export async function executeGithubUnbind(_args, client, commMessage) {
|
|
96
|
+
const msg = commMessage ?? getCurrentCommMessage();
|
|
97
|
+
if (!msg?.$adapter || !msg?.$sender?.id)
|
|
98
|
+
return '❌ 无法获取当前用户信息';
|
|
99
|
+
const model = oauthModel(client);
|
|
100
|
+
if (!model)
|
|
101
|
+
return '❌ 数据库未就绪';
|
|
102
|
+
const [existing] = await model.select().where({ platform: msg.$adapter, platform_uid: msg.$sender.id });
|
|
103
|
+
if (!existing)
|
|
104
|
+
return '📭 你尚未绑定 GitHub 账号';
|
|
105
|
+
await model.delete().where({ id: existing.id });
|
|
106
|
+
return `✅ 已解除 GitHub 账号绑定: ${existing.github_login}`;
|
|
107
|
+
}
|
|
108
|
+
export async function executeGithubWhoami(_args, client, commMessage) {
|
|
109
|
+
const msg = commMessage ?? getCurrentCommMessage();
|
|
110
|
+
if (!msg?.$adapter || !msg?.$sender?.id)
|
|
111
|
+
return '❌ 无法获取当前用户信息';
|
|
112
|
+
const model = oauthModel(client);
|
|
113
|
+
if (!model)
|
|
114
|
+
return '❌ 数据库未就绪';
|
|
115
|
+
const [existing] = await model.select().where({ platform: msg.$adapter, platform_uid: msg.$sender.id });
|
|
116
|
+
if (!existing)
|
|
117
|
+
return '📭 你尚未绑定 GitHub 账号\n🔗 使用 github_bind 绑定你的账号';
|
|
118
|
+
const userGh = new GhClient({ host: client.host, token: existing.access_token });
|
|
119
|
+
const auth = await userGh.verifyAuth();
|
|
120
|
+
if (auth.ok) {
|
|
121
|
+
return `👤 已绑定 GitHub 账号: ${auth.user}\n📅 绑定时间: ${new Date(existing.created_at).toLocaleString('zh-CN')}`;
|
|
122
|
+
}
|
|
123
|
+
return `⚠️ 已绑定账号 ${existing.github_login},但 Token 已失效\n🔗 请执行 github_unbind 后重新 github_bind`;
|
|
124
|
+
}
|
|
125
|
+
export async function executeGithubInstall(client) {
|
|
126
|
+
const slug = client.appSlug;
|
|
127
|
+
if (!slug)
|
|
128
|
+
return '❌ Endpoint 未配置 GitHub App';
|
|
129
|
+
const host = client.host || 'github.com';
|
|
130
|
+
const installations = client.installations;
|
|
131
|
+
let msg = `🔗 请点击以下链接安装 GitHub App 到你的仓库:\n https://${host}/apps/${slug}/installations/new`;
|
|
132
|
+
if (installations.length) {
|
|
133
|
+
msg += `\n\n📋 当前已安装 (${installations.length}):`;
|
|
134
|
+
for (const inst of installations) {
|
|
135
|
+
msg += `\n • ${inst.account.login} (${inst.account.type})`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return msg;
|
|
139
|
+
}
|
|
140
|
+
export async function executeGithubSubscribe(args, client, commMessage) {
|
|
141
|
+
const msg = commMessage ?? getCurrentCommMessage();
|
|
142
|
+
if (!msg?.$adapter || !msg?.$sender.id || !msg?.$channel?.id || !msg?.$endpoint) {
|
|
143
|
+
return '❌ 无法获取当前聊天通道信息';
|
|
144
|
+
}
|
|
145
|
+
const model = subscriptionsModel(client);
|
|
146
|
+
if (!model)
|
|
147
|
+
return '❌ 数据库未就绪';
|
|
148
|
+
const validEvents = ['push', 'issue', 'star', 'fork', 'unstar', 'pull_request'];
|
|
149
|
+
const events = args.events
|
|
150
|
+
? args.events.split(',').map((s) => s.trim()).filter((e) => validEvents.includes(e))
|
|
151
|
+
: validEvents;
|
|
152
|
+
if (!events.length)
|
|
153
|
+
return `❌ 无效的事件类型,可选: ${validEvents.join(', ')}`;
|
|
154
|
+
const [existing] = await model.select().where({
|
|
155
|
+
repo: args.repo,
|
|
156
|
+
target_id: msg.$channel?.id,
|
|
157
|
+
adapter: msg.$adapter,
|
|
158
|
+
endpoint: msg.$endpoint,
|
|
159
|
+
});
|
|
160
|
+
if (existing) {
|
|
161
|
+
await model.update({ events, target_type: msg.$channel?.type || 'private' }).where({ id: existing.id });
|
|
162
|
+
return `✅ 已更新订阅 ${args.repo}\n📡 事件: ${events.join(', ')}`;
|
|
163
|
+
}
|
|
164
|
+
await model.insert({
|
|
165
|
+
id: Date.now(),
|
|
166
|
+
repo: args.repo,
|
|
167
|
+
events,
|
|
168
|
+
target_id: msg.$channel?.id,
|
|
169
|
+
target_type: msg.$channel?.type || 'private',
|
|
170
|
+
adapter: msg.$adapter,
|
|
171
|
+
endpoint: msg.$endpoint,
|
|
172
|
+
});
|
|
173
|
+
return `✅ 已订阅 ${args.repo}\n📡 事件: ${events.join(', ')}\n📌 通知将推送到当前通道`;
|
|
174
|
+
}
|
|
175
|
+
export async function executeGithubUnsubscribe(args, client, commMessage) {
|
|
176
|
+
const msg = commMessage ?? getCurrentCommMessage();
|
|
177
|
+
if (!msg?.$adapter || !msg?.$channel?.id || !msg?.$endpoint) {
|
|
178
|
+
return '❌ 无法获取当前聊天通道信息';
|
|
179
|
+
}
|
|
180
|
+
const model = subscriptionsModel(client);
|
|
181
|
+
if (!model)
|
|
182
|
+
return '❌ 数据库未就绪';
|
|
183
|
+
const [existing] = await model.select().where({
|
|
184
|
+
repo: args.repo,
|
|
185
|
+
target_id: msg.$channel?.id,
|
|
186
|
+
adapter: msg.$adapter,
|
|
187
|
+
endpoint: msg.$endpoint,
|
|
188
|
+
});
|
|
189
|
+
if (!existing)
|
|
190
|
+
return `📭 当前通道未订阅 ${args.repo}`;
|
|
191
|
+
await model.delete().where({ id: existing.id });
|
|
192
|
+
return `✅ 已取消订阅 ${args.repo}`;
|
|
193
|
+
}
|
|
194
|
+
export async function executeGithubSubscriptions(_args, client, commMessage) {
|
|
195
|
+
const msg = commMessage ?? getCurrentCommMessage();
|
|
196
|
+
if (!msg?.$adapter || !msg?.$channel?.id || !msg?.$endpoint) {
|
|
197
|
+
return '❌ 无法获取当前聊天通道信息';
|
|
198
|
+
}
|
|
199
|
+
const model = subscriptionsModel(client);
|
|
200
|
+
if (!model)
|
|
201
|
+
return '❌ 数据库未就绪';
|
|
202
|
+
const subs = await model.select().where({
|
|
203
|
+
target_id: msg.$channel?.id,
|
|
204
|
+
adapter: msg.$adapter,
|
|
205
|
+
endpoint: msg.$endpoint,
|
|
206
|
+
});
|
|
207
|
+
if (!subs?.length)
|
|
208
|
+
return '📭 当前通道没有任何 GitHub 订阅';
|
|
209
|
+
return `📋 当前通道订阅 (${subs.length}):\n\n` +
|
|
210
|
+
subs.map((s) => {
|
|
211
|
+
const events = Array.isArray(s.events) ? s.events : [];
|
|
212
|
+
return ` 📦 ${s.repo}\n 📡 ${events.join(', ') || '(无事件)'}`;
|
|
213
|
+
}).join('\n\n');
|
|
214
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,34 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
declare module 'zhin.js' {
|
|
3
|
-
interface Adapters {
|
|
4
|
-
github: GitHubAdapter;
|
|
5
|
-
}
|
|
6
|
-
namespace Plugin {
|
|
7
|
-
interface Contexts {
|
|
8
|
-
router: import('@zhin.js/host-router').Router;
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
interface Models {
|
|
12
|
-
github_subscriptions: {
|
|
13
|
-
id: number;
|
|
14
|
-
repo: string;
|
|
15
|
-
events: import('./types.js').EventType[];
|
|
16
|
-
target_id: string;
|
|
17
|
-
target_type: 'private' | 'group' | 'channel';
|
|
18
|
-
adapter: string;
|
|
19
|
-
endpoint: string;
|
|
20
|
-
};
|
|
21
|
-
github_events: {
|
|
22
|
-
id: number;
|
|
23
|
-
repo: string;
|
|
24
|
-
event_type: string;
|
|
25
|
-
payload: any;
|
|
26
|
-
};
|
|
27
|
-
github_oauth_users: import('./types.js').GitHubOAuthUser;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
1
|
+
export { buildChannelId, enrichInboundContent, formatInboundContent, formatNotification, formatOutboundBody, normalizeWebhookPath, parseChannelId, parseIssueCommentInbound, parsePrReviewCommentInbound, parsePrReviewInbound, resolveGithubConfig, shouldAutoReplyRepo, verifyWebhookSignature, type GithubAdapterConfig, type GithubInboundComment, type GithubWireSegment, type ResolvedGithubConfig, } from './protocol.js';
|
|
30
2
|
export * from './types.js';
|
|
31
|
-
export { GitHubEndpoint, parseMarkdown, toMarkdown } from './endpoint.js';
|
|
32
|
-
export { GitHubAdapter } from './adapter.js';
|
|
33
3
|
export { GhClient } from './gh-client.js';
|
|
34
|
-
|
|
4
|
+
export { GithubClient, githubClient, type GithubClientEventMap } from './client.js';
|
|
5
|
+
export { WorkspaceManager } from './workspace-manager.js';
|
|
6
|
+
export { parseMessageChannel, issueBranchName, resolveWorkspaceBranch, formatChannelContext, } from './github-channel-context.js';
|
|
7
|
+
export { GithubEndpoint, defaultCreateClient, type GithubEndpointOptions, } from './endpoint.js';
|
|
8
|
+
export { GITHUB_OAUTH_USERS_TABLE, GITHUB_OAUTH_USERS_SCHEMA, defineGithubOauthUsersTable, lookupGithubOauthAccessToken, } from './oauth-users.js';
|
|
9
|
+
export { registerGithubWebhookRoutes, handleGithubWebhookRequest, dispatchGithubWebhookPayload, type GithubWebhookHandler, } from './webhook.js';
|