@zhin.js/adapter-github 1.0.1 → 1.1.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/CHANGELOG.md +1212 -0
- package/README.md +73 -195
- package/adapters/github/index.js +51 -0
- package/adapters/github/index.ts +59 -0
- package/agents/github/agent.json +20 -0
- package/agents/github/boundaries.md +3 -0
- package/agents/github/conventions.md +3 -0
- package/agents/github/skills/github-account/SKILL.md +20 -0
- package/agents/github/skills/github-account/handlers.js +116 -0
- package/agents/github/skills/github-account/handlers.ts +136 -0
- package/agents/github/skills/github-account/tools/bind/index.js +13 -0
- package/agents/github/skills/github-account/tools/bind/index.ts +13 -0
- package/agents/github/skills/github-account/tools/install/index.js +13 -0
- package/agents/github/skills/github-account/tools/install/index.ts +13 -0
- package/agents/github/skills/github-account/tools/unbind/index.js +13 -0
- package/agents/github/skills/github-account/tools/unbind/index.ts +13 -0
- package/agents/github/skills/github-account/tools/whoami/index.js +13 -0
- package/agents/github/skills/github-account/tools/whoami/index.ts +13 -0
- package/agents/github/skills/github-cli/SKILL.md +47 -0
- package/agents/github/skills/github-repository/SKILL.md +21 -0
- package/agents/github/skills/github-repository/handlers.js +77 -0
- package/agents/github/skills/github-repository/handlers.ts +113 -0
- package/agents/github/skills/github-repository/tools/create_pr/index.js +20 -0
- package/agents/github/skills/github-repository/tools/create_pr/index.ts +20 -0
- package/agents/github/skills/github-repository/tools/patch_file/index.js +19 -0
- package/agents/github/skills/github-repository/tools/patch_file/index.ts +19 -0
- package/agents/github/skills/github-repository/tools/prepare_workspace/index.js +15 -0
- package/agents/github/skills/github-repository/tools/prepare_workspace/index.ts +15 -0
- package/agents/github/skills/github-repository/tools/push_branch/index.js +18 -0
- package/agents/github/skills/github-repository/tools/push_branch/index.ts +18 -0
- package/agents/github/skills/github-subscriptions/SKILL.md +20 -0
- package/agents/github/skills/github-subscriptions/handlers.js +101 -0
- package/agents/github/skills/github-subscriptions/handlers.ts +119 -0
- package/agents/github/skills/github-subscriptions/tools/star/index.js +16 -0
- package/agents/github/skills/github-subscriptions/tools/star/index.ts +16 -0
- package/agents/github/skills/github-subscriptions/tools/subscribe/index.js +16 -0
- package/agents/github/skills/github-subscriptions/tools/subscribe/index.ts +16 -0
- package/agents/github/skills/github-subscriptions/tools/subscriptions/index.js +13 -0
- package/agents/github/skills/github-subscriptions/tools/subscriptions/index.ts +13 -0
- package/agents/github/skills/github-subscriptions/tools/unsubscribe/index.js +15 -0
- package/agents/github/skills/github-subscriptions/tools/unsubscribe/index.ts +15 -0
- package/agents/github/system.md +3 -0
- package/commands/github/endpoint/add/[id]/index.js +3 -0
- package/commands/github/endpoint/add/[id]/index.ts +3 -0
- package/commands/github/endpoint/definition.js +23 -0
- package/commands/github/endpoint/definition.ts +23 -0
- package/commands/github/endpoint/list/index.js +3 -0
- package/commands/github/endpoint/list/index.ts +3 -0
- package/commands/github/endpoint/remove/[id]/index.js +3 -0
- package/commands/github/endpoint/remove/[id]/index.ts +3 -0
- package/lib/client.d.ts +36 -0
- package/lib/client.js +44 -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-channel-context.d.ts +16 -0
- package/lib/github-channel-context.js +31 -0
- package/lib/github-runtime-state.d.ts +1 -0
- package/lib/github-runtime-state.js +6 -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 +72 -0
- package/lib/protocol.js +281 -0
- package/lib/types.d.ts +0 -18
- package/lib/types.js +2 -4
- 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 +93 -23
- package/plugin.js +38 -0
- package/prompt-sections/platform/index.js +16 -0
- package/prompt-sections/platform/index.ts +16 -0
- package/schema.json +137 -0
- package/src/client.ts +62 -0
- package/src/endpoint.ts +145 -150
- package/src/gh-client.ts +131 -0
- package/src/github-channel-context.ts +46 -0
- package/src/github-runtime-state.ts +7 -0
- package/src/index.ts +48 -431
- package/src/oauth-users.ts +47 -0
- package/src/protocol.ts +372 -0
- package/src/types.ts +0 -22
- 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/skills/github/SKILL.md +0 -266
- package/src/adapter.ts +0 -448
- package/src/agent-prompt.ts +0 -99
- package/src/register-github-mcp.ts +0 -60
package/lib/endpoint.js
CHANGED
|
@@ -1,160 +1,140 @@
|
|
|
1
|
+
import { Endpoint } from 'zhin.js/adapter';
|
|
2
|
+
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
3
|
+
import { GhClient } from './gh-client.js';
|
|
4
|
+
import { enrichInboundContent, formatInboundContent, formatOutboundBody, githubInboundConversation, parseChannelId, } from './protocol.js';
|
|
5
|
+
import { registerGithubWebhookRoutes } from './webhook.js';
|
|
6
|
+
import { GithubClient } from './client.js';
|
|
1
7
|
/**
|
|
2
|
-
* GitHub
|
|
8
|
+
* GitHub 是代码协作面(issue/PR),无好友/群/频道等 IM 社交概念,
|
|
9
|
+
* 不适用 EndpointManagement 语义端口;本 endpoint 不暴露该端口。
|
|
3
10
|
*/
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
if (lastIdx < md.length)
|
|
19
|
-
segments.push({ type: 'text', data: { text: md.slice(lastIdx) } });
|
|
20
|
-
return segments.length ? segments : [{ type: 'text', data: { text: md } }];
|
|
21
|
-
}
|
|
22
|
-
export function toMarkdown(content) {
|
|
23
|
-
if (!Array.isArray(content))
|
|
24
|
-
content = [content];
|
|
25
|
-
return content.map(seg => {
|
|
26
|
-
if (typeof seg === 'string')
|
|
27
|
-
return seg;
|
|
28
|
-
switch (seg.type) {
|
|
29
|
-
case 'text': return seg.data.text || '';
|
|
30
|
-
case 'at': return `@${seg.data.name || seg.data.id}`;
|
|
31
|
-
case 'image': return seg.data.url ? `` : '[image]';
|
|
32
|
-
case 'link': return `[${seg.data.text || seg.data.url}](${seg.data.url})`;
|
|
33
|
-
default: return seg.data?.text || `[${seg.type}]`;
|
|
34
|
-
}
|
|
35
|
-
}).join('');
|
|
36
|
-
}
|
|
37
|
-
export class GitHubEndpoint {
|
|
38
|
-
adapter;
|
|
39
|
-
$config;
|
|
40
|
-
$connected = false;
|
|
41
|
-
gh;
|
|
42
|
-
get $id() { return this.$config.name; }
|
|
43
|
-
get logger() {
|
|
44
|
-
return this.adapter.plugin.logger;
|
|
11
|
+
export class GithubEndpoint extends Endpoint {
|
|
12
|
+
#logger;
|
|
13
|
+
#options;
|
|
14
|
+
client;
|
|
15
|
+
#routeReleases = [];
|
|
16
|
+
#open = false;
|
|
17
|
+
#started = false;
|
|
18
|
+
constructor(options) {
|
|
19
|
+
super();
|
|
20
|
+
this.#logger = getAdapterLogger('github', options.config.id);
|
|
21
|
+
this.#options = options;
|
|
22
|
+
const api = options.createClient?.(options.config) ?? defaultCreateClient(options.config);
|
|
23
|
+
this.client = new GithubClient(options.config.id, api, options.config, options.database);
|
|
45
24
|
}
|
|
46
|
-
|
|
47
|
-
this.
|
|
48
|
-
this.$config = $config;
|
|
49
|
-
const { host, app_id, private_key } = $config;
|
|
50
|
-
const appAuth = app_id && private_key
|
|
51
|
-
? { appId: app_id, privateKey: private_key }
|
|
52
|
-
: undefined;
|
|
53
|
-
this.gh = new GhClient({ host, appAuth });
|
|
25
|
+
get config() {
|
|
26
|
+
return this.client.config;
|
|
54
27
|
}
|
|
55
|
-
async
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
28
|
+
async start() {
|
|
29
|
+
if (this.#started)
|
|
30
|
+
return;
|
|
31
|
+
this.#started = true;
|
|
32
|
+
try {
|
|
33
|
+
const result = await this.client.api.verifyAuth();
|
|
34
|
+
if (!result.ok)
|
|
35
|
+
throw new Error(`GitHub 认证失败: ${result.message}`);
|
|
36
|
+
if (this.client.config.webhookSecret) {
|
|
37
|
+
if (!this.#options.http) {
|
|
38
|
+
throw new TypeError('GitHub webhook_secret requires httpHostToken');
|
|
39
|
+
}
|
|
40
|
+
this.#routeReleases.push(...registerGithubWebhookRoutes(this.#options.http, this));
|
|
41
|
+
this.#logger.debug(formatCompact({
|
|
42
|
+
endpoint: this.client.name,
|
|
43
|
+
op: 'webhook',
|
|
44
|
+
path: this.client.config.webhookPath,
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
this.#logger.debug(formatCompact({
|
|
49
|
+
endpoint: this.client.name,
|
|
50
|
+
op: 'connect',
|
|
51
|
+
mode: 'api-only',
|
|
52
|
+
bot: this.client.api.authenticatedUser,
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
await this.stop();
|
|
58
|
+
this.#logger.error('Failed to connect GitHub endpoint:', error);
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
65
61
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const number = payload.issue.number;
|
|
69
|
-
const isPR = 'pull_request' in payload.issue;
|
|
70
|
-
const channelId = buildChannelId(repo, isPR ? 'pr' : 'issue', number);
|
|
71
|
-
const gh = this.gh;
|
|
72
|
-
const result = Message.from(payload, {
|
|
73
|
-
$id: payload.comment.id.toString(),
|
|
74
|
-
$adapter: 'github',
|
|
75
|
-
$endpoint: this.$config.name,
|
|
76
|
-
$sender: { id: payload.sender.login, name: payload.sender.login },
|
|
77
|
-
$channel: { id: channelId, type: 'group' },
|
|
78
|
-
$content: parseMarkdown(payload.comment.body),
|
|
79
|
-
$raw: payload.comment.body,
|
|
80
|
-
$timestamp: new Date(payload.comment.created_at).getTime(),
|
|
81
|
-
$recall: async () => { await gh.deleteIssueComment(repo, payload.comment.id); },
|
|
82
|
-
$reply: async (content, quote) => {
|
|
83
|
-
const text = toMarkdown(content);
|
|
84
|
-
const finalBody = quote ? `> ${payload.comment.body.split('\n')[0]}\n\n${text}` : text;
|
|
85
|
-
const r = await gh.createIssueComment(repo, number, finalBody);
|
|
86
|
-
return r.ok ? r.data.id.toString() : '';
|
|
87
|
-
},
|
|
88
|
-
});
|
|
89
|
-
return result;
|
|
62
|
+
open() {
|
|
63
|
+
this.#open = true;
|
|
90
64
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const number = payload.pull_request.number;
|
|
94
|
-
const channelId = buildChannelId(repo, 'pr', number);
|
|
95
|
-
const gh = this.gh;
|
|
96
|
-
const body = payload.comment.path
|
|
97
|
-
? `**${payload.comment.path}**\n${payload.comment.diff_hunk ? '```diff\n' + payload.comment.diff_hunk + '\n```\n' : ''}${payload.comment.body}`
|
|
98
|
-
: payload.comment.body;
|
|
99
|
-
return Message.from(payload, {
|
|
100
|
-
$id: payload.comment.id.toString(),
|
|
101
|
-
$adapter: 'github',
|
|
102
|
-
$endpoint: this.$config.name,
|
|
103
|
-
$sender: { id: payload.sender.login, name: payload.sender.login },
|
|
104
|
-
$channel: { id: channelId, type: 'group' },
|
|
105
|
-
$content: parseMarkdown(body),
|
|
106
|
-
$raw: body,
|
|
107
|
-
$timestamp: new Date(payload.comment.created_at).getTime(),
|
|
108
|
-
$recall: async () => { await gh.deletePRReviewComment(repo, payload.comment.id); },
|
|
109
|
-
$reply: async (content) => {
|
|
110
|
-
const r = await gh.createPRComment(repo, number, toMarkdown(content));
|
|
111
|
-
return r.ok ? r.data.id.toString() : '';
|
|
112
|
-
},
|
|
113
|
-
});
|
|
65
|
+
close() {
|
|
66
|
+
this.#open = false;
|
|
114
67
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const gh = this.gh;
|
|
122
|
-
const stateLabel = {
|
|
123
|
-
approved: '✅ APPROVED', changes_requested: '🔄 CHANGES REQUESTED',
|
|
124
|
-
commented: '💬 COMMENTED', dismissed: '❌ DISMISSED',
|
|
125
|
-
};
|
|
126
|
-
const body = `**[${stateLabel[payload.review.state] || payload.review.state}]**\n${payload.review.body}`;
|
|
127
|
-
return Message.from(payload, {
|
|
128
|
-
$id: payload.review.id.toString(),
|
|
129
|
-
$adapter: 'github',
|
|
130
|
-
$endpoint: this.$config.name,
|
|
131
|
-
$sender: { id: payload.sender.login, name: payload.sender.login },
|
|
132
|
-
$channel: { id: channelId, type: 'group' },
|
|
133
|
-
$content: parseMarkdown(body),
|
|
134
|
-
$raw: body,
|
|
135
|
-
$timestamp: new Date(payload.review.submitted_at).getTime(),
|
|
136
|
-
$recall: async () => { },
|
|
137
|
-
$reply: async (content) => {
|
|
138
|
-
const r = await gh.createPRComment(repo, number, toMarkdown(content));
|
|
139
|
-
return r.ok ? r.data.id.toString() : '';
|
|
140
|
-
},
|
|
141
|
-
});
|
|
68
|
+
async stop() {
|
|
69
|
+
this.#open = false;
|
|
70
|
+
for (const release of this.#routeReleases.splice(0))
|
|
71
|
+
release();
|
|
72
|
+
this.#started = false;
|
|
73
|
+
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
142
74
|
}
|
|
143
|
-
async
|
|
144
|
-
const parsed = parseChannelId(
|
|
75
|
+
async send({ conversation, payload }) {
|
|
76
|
+
const parsed = parseChannelId(conversation.id);
|
|
145
77
|
if (!parsed)
|
|
146
|
-
throw new Error(`无效的 GitHub
|
|
147
|
-
const text =
|
|
78
|
+
throw new Error(`无效的 GitHub conversation ID: ${conversation.id}`);
|
|
79
|
+
const text = formatOutboundBody(payload);
|
|
148
80
|
const r = parsed.type === 'issue'
|
|
149
|
-
? await this.
|
|
150
|
-
: await this.
|
|
81
|
+
? await this.client.api.createIssueComment(parsed.repo, parsed.number, text)
|
|
82
|
+
: await this.client.api.createPRComment(parsed.repo, parsed.number, text);
|
|
151
83
|
if (!r.ok)
|
|
152
84
|
throw new Error(`发送失败: ${JSON.stringify(r.data)}`);
|
|
153
|
-
this
|
|
154
|
-
|
|
85
|
+
this.#logger.debug(formatCompact({
|
|
86
|
+
op: 'github_send',
|
|
87
|
+
endpoint: this.client.name,
|
|
88
|
+
target: `${conversation.kind}:${conversation.id}`,
|
|
89
|
+
messageId: r.data.id,
|
|
90
|
+
}));
|
|
91
|
+
return String(r.data.id);
|
|
155
92
|
}
|
|
156
|
-
|
|
157
|
-
|
|
93
|
+
/** Test / internal: admit a parsed comment when open. */
|
|
94
|
+
admit(comment) {
|
|
95
|
+
if (!this.#open)
|
|
96
|
+
return;
|
|
97
|
+
const botUser = this.client.config.botLogin
|
|
98
|
+
|| this.client.api.getBotLogin()
|
|
99
|
+
|| this.client.api.authenticatedUser;
|
|
100
|
+
if (botUser && comment.sender === botUser)
|
|
101
|
+
return;
|
|
102
|
+
const conversation = githubInboundConversation(String(this.#options.id), comment);
|
|
103
|
+
const content = enrichInboundContent(formatInboundContent(comment.content), this.client.config, botUser ?? undefined, comment.repo);
|
|
104
|
+
void this.emit('message.receive', {
|
|
105
|
+
conversation,
|
|
106
|
+
message: { conversation, id: comment.id },
|
|
107
|
+
content,
|
|
108
|
+
sender: { id: comment.sender, name: comment.sender },
|
|
109
|
+
endpointId: this.client.name,
|
|
110
|
+
metadata: Object.freeze({
|
|
111
|
+
repo: comment.repo,
|
|
112
|
+
kind: comment.kind,
|
|
113
|
+
createdAt: comment.createdAt,
|
|
114
|
+
}),
|
|
115
|
+
}).catch((err) => {
|
|
116
|
+
this.#logger.warn(formatCompact({
|
|
117
|
+
op: 'github_gateway_receive_failed',
|
|
118
|
+
target: `${conversation.kind}:${conversation.id}`,
|
|
119
|
+
error: err instanceof Error ? err.message : String(err),
|
|
120
|
+
}));
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
admitPlatform(name, event) {
|
|
124
|
+
if (!this.#open)
|
|
125
|
+
return;
|
|
126
|
+
void this.emitPlatform(name, event).catch((error) => {
|
|
127
|
+
this.#logger.warn(formatCompact({
|
|
128
|
+
op: 'github_platform_event_failed',
|
|
129
|
+
event: name,
|
|
130
|
+
error: error instanceof Error ? error.message : String(error),
|
|
131
|
+
}));
|
|
132
|
+
});
|
|
158
133
|
}
|
|
159
134
|
}
|
|
160
|
-
|
|
135
|
+
export function defaultCreateClient(config) {
|
|
136
|
+
return new GhClient({
|
|
137
|
+
host: config.host,
|
|
138
|
+
appAuth: { appId: config.appId, privateKey: config.privateKey },
|
|
139
|
+
});
|
|
140
|
+
}
|
package/lib/gh-client.d.ts
CHANGED
|
@@ -13,6 +13,17 @@ export interface AppAuth {
|
|
|
13
13
|
/** PEM 格式私钥内容,或私钥文件路径 */
|
|
14
14
|
privateKey: string;
|
|
15
15
|
}
|
|
16
|
+
export interface GitHubBotIdentity {
|
|
17
|
+
login: string;
|
|
18
|
+
email: string;
|
|
19
|
+
slug: string;
|
|
20
|
+
userId: number;
|
|
21
|
+
}
|
|
22
|
+
export interface GitHubCommitAuthor {
|
|
23
|
+
name: string;
|
|
24
|
+
email: string;
|
|
25
|
+
date?: string;
|
|
26
|
+
}
|
|
16
27
|
export interface GhClientOptions {
|
|
17
28
|
/** GitHub Enterprise 主机名(默认 github.com) */
|
|
18
29
|
host?: string;
|
|
@@ -153,6 +164,64 @@ export declare class GhClient {
|
|
|
153
164
|
status: number;
|
|
154
165
|
data: any;
|
|
155
166
|
}>;
|
|
167
|
+
getFileContent(repo: string, path: string, ref?: string): Promise<{
|
|
168
|
+
ok: boolean;
|
|
169
|
+
status: number;
|
|
170
|
+
data: {
|
|
171
|
+
content: string;
|
|
172
|
+
sha: string;
|
|
173
|
+
encoding: string;
|
|
174
|
+
};
|
|
175
|
+
}>;
|
|
176
|
+
createOrUpdateFile(repo: string, path: string, content: string, message: string, options?: {
|
|
177
|
+
branch?: string;
|
|
178
|
+
sha?: string;
|
|
179
|
+
}): Promise<{
|
|
180
|
+
ok: boolean;
|
|
181
|
+
status: number;
|
|
182
|
+
data: {
|
|
183
|
+
commit: {
|
|
184
|
+
sha: string;
|
|
185
|
+
html_url?: string;
|
|
186
|
+
};
|
|
187
|
+
content: {
|
|
188
|
+
html_url?: string;
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
}>;
|
|
192
|
+
getRef(repo: string, ref: string): Promise<{
|
|
193
|
+
ok: boolean;
|
|
194
|
+
status: number;
|
|
195
|
+
data: {
|
|
196
|
+
ref: string;
|
|
197
|
+
object: {
|
|
198
|
+
sha: string;
|
|
199
|
+
type: string;
|
|
200
|
+
};
|
|
201
|
+
};
|
|
202
|
+
}>;
|
|
203
|
+
createRef(repo: string, ref: string, sha: string): Promise<{
|
|
204
|
+
ok: boolean;
|
|
205
|
+
status: number;
|
|
206
|
+
data: any;
|
|
207
|
+
}>;
|
|
208
|
+
createPullRequest(repo: string, title: string, head: string, base: string, body?: string): Promise<{
|
|
209
|
+
ok: boolean;
|
|
210
|
+
status: number;
|
|
211
|
+
data: {
|
|
212
|
+
number: number;
|
|
213
|
+
html_url: string;
|
|
214
|
+
head: {
|
|
215
|
+
ref: string;
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
}>;
|
|
219
|
+
getBotLogin(): string | null;
|
|
220
|
+
getCommitAuthor(): GitHubCommitAuthor | null;
|
|
221
|
+
getBotIdentitySync(): GitHubBotIdentity | null;
|
|
222
|
+
getBotIdentity(): Promise<GitHubBotIdentity | null>;
|
|
223
|
+
buildCloneUrl(repo: string, token: string): string;
|
|
224
|
+
resolveBotUserId(): Promise<number | null>;
|
|
156
225
|
starRepo(repo: string): Promise<{
|
|
157
226
|
ok: boolean;
|
|
158
227
|
status: number;
|
|
@@ -193,6 +262,10 @@ export declare class GhClient {
|
|
|
193
262
|
get appSlug(): string | null;
|
|
194
263
|
/** App 的 client_id(verifyAuth 后从 /app 获取,用于 Device Flow) */
|
|
195
264
|
private _clientId;
|
|
265
|
+
/** Bot 机器用户 numeric id(/users/{slug}[bot]) */
|
|
266
|
+
private _botUserId;
|
|
267
|
+
/** 确保 App 认证的 Installation Token 对指定 repo 有效,并返回 token */
|
|
268
|
+
ensureInstallationTokenForRepo(repo: string): Promise<string | undefined>;
|
|
196
269
|
get clientId(): string | null;
|
|
197
270
|
/**
|
|
198
271
|
* 获取仓库事件(支持 ETag 条件请求)
|
|
@@ -225,4 +298,3 @@ export declare class GhClient {
|
|
|
225
298
|
scope: string;
|
|
226
299
|
} | null>;
|
|
227
300
|
}
|
|
228
|
-
//# sourceMappingURL=gh-client.d.ts.map
|
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,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 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');
|
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 GithubEndpointConfig, 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';
|