@zhin.js/adapter-github 3.0.1 → 3.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +624 -0
- package/README.md +38 -190
- package/adapters/github.ts +51 -0
- 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-prompt.d.ts +0 -1
- package/lib/agent-prompt.js +10 -9
- package/lib/endpoint.d.ts +46 -24
- package/lib/endpoint.js +144 -144
- package/lib/gh-client.d.ts +73 -1
- package/lib/gh-client.js +99 -1
- package/lib/github-agent-deps.d.ts +47 -0
- package/lib/github-agent-deps.js +43 -0
- package/lib/github-bot-handlers.d.ts +26 -0
- package/lib/github-bot-handlers.js +77 -0
- package/lib/github-channel-context.d.ts +16 -0
- package/lib/github-channel-context.js +31 -0
- package/lib/github-tool-handlers.d.ts +17 -0
- package/lib/github-tool-handlers.js +221 -0
- package/lib/index.d.ts +7 -32
- package/lib/index.js +7 -386
- package/lib/oauth-users.d.ts +33 -0
- package/lib/oauth-users.js +38 -0
- package/lib/protocol.d.ts +87 -0
- package/lib/protocol.js +241 -0
- package/lib/types.d.ts +6 -1
- package/lib/types.js +0 -1
- package/lib/webhook.d.ts +13 -0
- package/lib/webhook.js +87 -0
- package/lib/workspace-manager.d.ts +21 -0
- package/lib/workspace-manager.js +145 -0
- package/package.json +60 -23
- package/plugin.ts +52 -0
- package/schema.json +65 -0
- package/src/agent-prompt.ts +11 -10
- package/src/endpoint.ts +169 -150
- package/src/gh-client.ts +131 -0
- package/src/github-agent-deps.ts +82 -0
- package/src/github-bot-handlers.ts +109 -0
- package/src/github-channel-context.ts +46 -0
- package/src/github-tool-handlers.ts +257 -0
- package/src/index.ts +55 -431
- package/src/oauth-users.ts +47 -0
- package/src/protocol.ts +367 -0
- package/src/types.ts +6 -0
- package/src/webhook.ts +125 -0
- package/src/workspace-manager.ts +158 -0
- package/lib/adapter.d.ts +0 -66
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -418
- package/lib/adapter.js.map +0 -1
- package/lib/agent-prompt.d.ts.map +0 -1
- 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 -7
- package/lib/register-github-mcp.d.ts.map +0 -1
- package/lib/register-github-mcp.js +0 -36
- package/lib/register-github-mcp.js.map +0 -1
- package/lib/segment-mapper.d.ts +0 -2
- package/lib/segment-mapper.d.ts.map +0 -1
- package/lib/segment-mapper.js +0 -2
- package/lib/segment-mapper.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 -450
- package/src/register-github-mcp.ts +0 -62
- package/src/segment-mapper.ts +0 -1
package/lib/endpoint.d.ts
CHANGED
|
@@ -1,26 +1,48 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import type { GitHubEndpointConfig, IssueCommentPayload, PRReviewCommentPayload, PRReviewPayload } from './types.js';
|
|
6
|
-
import type { GitHubAdapter } from './adapter.js';
|
|
1
|
+
import type { EndpointInstance } from '@zhin.js/adapter';
|
|
2
|
+
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
3
|
+
import type { HttpHost } from '@zhin.js/host-http';
|
|
4
|
+
import type { CapabilityId, DatabaseHost } from '@zhin.js/plugin-runtime';
|
|
7
5
|
import { GhClient } from './gh-client.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
constructor(adapter: GitHubAdapter, $config: GitHubEndpointConfig);
|
|
18
|
-
$connect(): Promise<void>;
|
|
19
|
-
$disconnect(): Promise<void>;
|
|
20
|
-
$formatMessage(payload: IssueCommentPayload): Message<IssueCommentPayload>;
|
|
21
|
-
formatPRReviewComment(payload: PRReviewCommentPayload): Message<PRReviewCommentPayload>;
|
|
22
|
-
formatPRReview(payload: PRReviewPayload): Message<PRReviewPayload> | null;
|
|
23
|
-
$sendMessage(options: SendOptions): Promise<string>;
|
|
24
|
-
$recallMessage(id: string): Promise<void>;
|
|
6
|
+
import { type GithubInboundComment, type ResolvedGithubConfig } from './protocol.js';
|
|
7
|
+
import { WorkspaceManager } from './workspace-manager.js';
|
|
8
|
+
export interface GithubEndpointOptions {
|
|
9
|
+
readonly id: CapabilityId;
|
|
10
|
+
readonly gateway: MessageGateway;
|
|
11
|
+
readonly http?: HttpHost;
|
|
12
|
+
readonly database?: DatabaseHost;
|
|
13
|
+
readonly config: ResolvedGithubConfig;
|
|
14
|
+
readonly createClient?: (config: ResolvedGithubConfig) => GhClient;
|
|
25
15
|
}
|
|
26
|
-
|
|
16
|
+
export declare class GithubEndpoint implements EndpointInstance {
|
|
17
|
+
#private;
|
|
18
|
+
readonly gh: GhClient;
|
|
19
|
+
readonly config: ResolvedGithubConfig;
|
|
20
|
+
readonly name: string;
|
|
21
|
+
constructor(options: GithubEndpointOptions);
|
|
22
|
+
getAPI(): GhClient;
|
|
23
|
+
getUserOrDefaultAPI(platform?: string, platformUid?: string): Promise<GhClient | null>;
|
|
24
|
+
getClientId(): string | null;
|
|
25
|
+
getHost(): string | undefined;
|
|
26
|
+
getAppSlug(): string | null;
|
|
27
|
+
getInstallations(): {
|
|
28
|
+
id: number;
|
|
29
|
+
account: {
|
|
30
|
+
login: string;
|
|
31
|
+
type: string;
|
|
32
|
+
};
|
|
33
|
+
target_type: string;
|
|
34
|
+
}[];
|
|
35
|
+
getWorkspaceManager(): WorkspaceManager;
|
|
36
|
+
getDatabase(): DatabaseHost | undefined;
|
|
37
|
+
start(): Promise<void>;
|
|
38
|
+
open(): void;
|
|
39
|
+
close(): void;
|
|
40
|
+
stop(): Promise<void>;
|
|
41
|
+
send({ target, payload }: {
|
|
42
|
+
readonly target: string;
|
|
43
|
+
readonly payload: unknown;
|
|
44
|
+
}): Promise<string>;
|
|
45
|
+
/** Test / internal: admit a parsed comment when open. */
|
|
46
|
+
admit(comment: GithubInboundComment): void;
|
|
47
|
+
}
|
|
48
|
+
export declare function defaultCreateClient(config: ResolvedGithubConfig): GhClient;
|
package/lib/endpoint.js
CHANGED
|
@@ -1,165 +1,165 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* GithubEndpoint — lifecycle, outbound send, inbound admit.
|
|
3
3
|
*/
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
6
6
|
import { GhClient } from './gh-client.js';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
7
|
+
import { registerGithubAgentEndpoint } from './github-agent-deps.js';
|
|
8
|
+
import { lookupGithubOauthAccessToken } from './oauth-users.js';
|
|
9
|
+
import { enrichInboundContent, formatInboundContent, formatOutboundBody, parseChannelId, } from './protocol.js';
|
|
10
|
+
import { registerGithubWebhookRoutes } from './webhook.js';
|
|
11
|
+
import { WorkspaceManager } from './workspace-manager.js';
|
|
12
|
+
const logger = getLogger('github');
|
|
13
|
+
export class GithubEndpoint {
|
|
14
|
+
#options;
|
|
15
|
+
gh;
|
|
16
|
+
config;
|
|
17
|
+
name;
|
|
18
|
+
#workspaceManager = null;
|
|
19
|
+
#routeReleases = [];
|
|
20
|
+
#open = false;
|
|
21
|
+
#started = false;
|
|
22
|
+
#unregisterAgent;
|
|
23
|
+
constructor(options) {
|
|
24
|
+
this.#options = options;
|
|
25
|
+
this.config = options.config;
|
|
26
|
+
this.name = options.config.name;
|
|
27
|
+
this.gh = options.createClient?.(options.config) ?? defaultCreateClient(options.config);
|
|
18
28
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (typeof seg === 'string')
|
|
28
|
-
return seg;
|
|
29
|
-
switch (seg.type) {
|
|
30
|
-
case 'text': return seg.data.text || '';
|
|
31
|
-
case 'mention': return `@${seg.data.name || seg.data.target}`;
|
|
32
|
-
case 'at': return `@${seg.data.name || seg.data.id}`;
|
|
33
|
-
case 'image': return seg.data.url ? `` : '[image]';
|
|
34
|
-
case 'link': return `[${seg.data.text || seg.data.url}](${seg.data.url})`;
|
|
35
|
-
default: return seg.data?.text || `[${seg.type}]`;
|
|
29
|
+
getAPI() {
|
|
30
|
+
return this.gh;
|
|
31
|
+
}
|
|
32
|
+
async getUserOrDefaultAPI(platform, platformUid) {
|
|
33
|
+
if (platform && platformUid) {
|
|
34
|
+
const token = await lookupGithubOauthAccessToken(this.#options.database, platform, platformUid);
|
|
35
|
+
if (token)
|
|
36
|
+
return this.gh.withToken(token);
|
|
36
37
|
}
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
export class GitHubEndpoint {
|
|
40
|
-
adapter;
|
|
41
|
-
$config;
|
|
42
|
-
$connected = false;
|
|
43
|
-
gh;
|
|
44
|
-
get $id() { return this.$config.name; }
|
|
45
|
-
get logger() {
|
|
46
|
-
return this.adapter.plugin.logger;
|
|
38
|
+
return this.gh;
|
|
47
39
|
}
|
|
48
|
-
|
|
49
|
-
this.
|
|
50
|
-
this.$config = $config;
|
|
51
|
-
const { host, app_id, private_key } = $config;
|
|
52
|
-
const appAuth = app_id && private_key
|
|
53
|
-
? { appId: app_id, privateKey: private_key }
|
|
54
|
-
: undefined;
|
|
55
|
-
this.gh = new GhClient({ host, appAuth });
|
|
40
|
+
getClientId() {
|
|
41
|
+
return this.gh.clientId || null;
|
|
56
42
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (!result.ok)
|
|
60
|
-
throw new Error(`GitHub 认证失败: ${result.message}`);
|
|
61
|
-
this.$connected = true;
|
|
62
|
-
this.logger.info(formatCompact({ endpoint: this.$id }));
|
|
43
|
+
getHost() {
|
|
44
|
+
return this.config.host;
|
|
63
45
|
}
|
|
64
|
-
|
|
65
|
-
this
|
|
66
|
-
this.logger.debug(formatCompact({ endpoint: this.$id, disconnect: true }));
|
|
46
|
+
getAppSlug() {
|
|
47
|
+
return this.gh.appSlug || null;
|
|
67
48
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const number = payload.issue.number;
|
|
71
|
-
const isPR = 'pull_request' in payload.issue;
|
|
72
|
-
const channelId = buildChannelId(repo, isPR ? 'pr' : 'issue', number);
|
|
73
|
-
const gh = this.gh;
|
|
74
|
-
const result = Message.from(payload, {
|
|
75
|
-
$id: payload.comment.id.toString(),
|
|
76
|
-
$adapter: 'github',
|
|
77
|
-
$endpoint: this.$config.name,
|
|
78
|
-
$sender: { id: payload.sender.login, name: payload.sender.login },
|
|
79
|
-
$channel: { id: channelId, type: 'group' },
|
|
80
|
-
$content: toCanonicalSegments(parseMarkdown(payload.comment.body)),
|
|
81
|
-
$raw: payload.comment.body,
|
|
82
|
-
$timestamp: new Date(payload.comment.created_at).getTime(),
|
|
83
|
-
$recall: async () => { await gh.deleteIssueComment(repo, payload.comment.id); },
|
|
84
|
-
$reply: async (content, quote) => {
|
|
85
|
-
const text = toMarkdown(content);
|
|
86
|
-
const finalBody = quote ? `> ${payload.comment.body.split('\n')[0]}\n\n${text}` : text;
|
|
87
|
-
const r = await gh.createIssueComment(repo, number, finalBody);
|
|
88
|
-
return r.ok ? r.data.id.toString() : '';
|
|
89
|
-
},
|
|
90
|
-
});
|
|
91
|
-
return result;
|
|
49
|
+
getInstallations() {
|
|
50
|
+
return this.gh.installations || [];
|
|
92
51
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
: payload.comment.body;
|
|
101
|
-
return Message.from(payload, {
|
|
102
|
-
$id: payload.comment.id.toString(),
|
|
103
|
-
$adapter: 'github',
|
|
104
|
-
$endpoint: this.$config.name,
|
|
105
|
-
$sender: { id: payload.sender.login, name: payload.sender.login },
|
|
106
|
-
$channel: { id: channelId, type: 'group' },
|
|
107
|
-
$content: toCanonicalSegments(parseMarkdown(body)),
|
|
108
|
-
$raw: body,
|
|
109
|
-
$timestamp: new Date(payload.comment.created_at).getTime(),
|
|
110
|
-
$recall: async () => { await gh.deletePRReviewComment(repo, payload.comment.id); },
|
|
111
|
-
$reply: async (content) => {
|
|
112
|
-
const r = await gh.createPRComment(repo, number, toMarkdown(content));
|
|
113
|
-
return r.ok ? r.data.id.toString() : '';
|
|
114
|
-
},
|
|
115
|
-
});
|
|
52
|
+
getWorkspaceManager() {
|
|
53
|
+
if (this.#workspaceManager)
|
|
54
|
+
return this.#workspaceManager;
|
|
55
|
+
const workspaceRoot = this.config.workspaceRoot
|
|
56
|
+
?? path.join(process.cwd(), 'data', 'github-workspaces');
|
|
57
|
+
this.#workspaceManager = new WorkspaceManager(this.gh, workspaceRoot);
|
|
58
|
+
return this.#workspaceManager;
|
|
116
59
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return null;
|
|
120
|
-
const repo = payload.repository.full_name;
|
|
121
|
-
const number = payload.pull_request.number;
|
|
122
|
-
const channelId = buildChannelId(repo, 'pr', number);
|
|
123
|
-
const gh = this.gh;
|
|
124
|
-
const stateLabel = {
|
|
125
|
-
approved: '✅ APPROVED', changes_requested: '🔄 CHANGES REQUESTED',
|
|
126
|
-
commented: '💬 COMMENTED', dismissed: '❌ DISMISSED',
|
|
127
|
-
};
|
|
128
|
-
const body = `**[${stateLabel[payload.review.state] || payload.review.state}]**\n${payload.review.body}`;
|
|
129
|
-
return Message.from(payload, {
|
|
130
|
-
$id: payload.review.id.toString(),
|
|
131
|
-
$adapter: 'github',
|
|
132
|
-
$endpoint: this.$config.name,
|
|
133
|
-
$sender: { id: payload.sender.login, name: payload.sender.login },
|
|
134
|
-
$channel: { id: channelId, type: 'group' },
|
|
135
|
-
$content: toCanonicalSegments(parseMarkdown(body)),
|
|
136
|
-
$raw: body,
|
|
137
|
-
$timestamp: new Date(payload.review.submitted_at).getTime(),
|
|
138
|
-
$recall: async () => { },
|
|
139
|
-
$reply: async (content) => {
|
|
140
|
-
const r = await gh.createPRComment(repo, number, toMarkdown(content));
|
|
141
|
-
return r.ok ? r.data.id.toString() : '';
|
|
142
|
-
},
|
|
143
|
-
});
|
|
60
|
+
getDatabase() {
|
|
61
|
+
return this.#options.database;
|
|
144
62
|
}
|
|
145
|
-
async
|
|
146
|
-
|
|
63
|
+
async start() {
|
|
64
|
+
if (this.#started)
|
|
65
|
+
return;
|
|
66
|
+
this.#started = true;
|
|
67
|
+
try {
|
|
68
|
+
const result = await this.gh.verifyAuth();
|
|
69
|
+
if (!result.ok)
|
|
70
|
+
throw new Error(`GitHub 认证失败: ${result.message}`);
|
|
71
|
+
this.#unregisterAgent = registerGithubAgentEndpoint(this.name, this);
|
|
72
|
+
if (this.config.webhookSecret) {
|
|
73
|
+
if (!this.#options.http) {
|
|
74
|
+
throw new TypeError('GitHub webhook_secret requires httpHostToken');
|
|
75
|
+
}
|
|
76
|
+
this.#routeReleases.push(...registerGithubWebhookRoutes(this.#options.http, this));
|
|
77
|
+
logger.debug(formatCompact({
|
|
78
|
+
endpoint: this.name,
|
|
79
|
+
op: 'webhook',
|
|
80
|
+
path: this.config.webhookPath,
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
logger.debug(formatCompact({
|
|
85
|
+
endpoint: this.name,
|
|
86
|
+
op: 'connect',
|
|
87
|
+
mode: 'api-only',
|
|
88
|
+
bot: this.gh.authenticatedUser,
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
await this.stop();
|
|
94
|
+
logger.error('Failed to connect GitHub endpoint:', error);
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
open() {
|
|
99
|
+
this.#open = true;
|
|
100
|
+
}
|
|
101
|
+
close() {
|
|
102
|
+
this.#open = false;
|
|
103
|
+
}
|
|
104
|
+
async stop() {
|
|
105
|
+
this.#open = false;
|
|
106
|
+
for (const release of this.#routeReleases.splice(0))
|
|
107
|
+
release();
|
|
108
|
+
this.#unregisterAgent?.();
|
|
109
|
+
this.#unregisterAgent = undefined;
|
|
110
|
+
this.#started = false;
|
|
111
|
+
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.name }));
|
|
112
|
+
}
|
|
113
|
+
async send({ target, payload }) {
|
|
114
|
+
const parsed = parseChannelId(target);
|
|
147
115
|
if (!parsed)
|
|
148
|
-
throw new Error(`无效的 GitHub channel ID: ${
|
|
149
|
-
const
|
|
150
|
-
const arr = Array.isArray(expanded) ? expanded : [expanded];
|
|
151
|
-
const canonical = arr.map((s) => (typeof s === 'string' ? { type: 'text', data: { text: s } } : s));
|
|
152
|
-
const text = toMarkdown(fromCanonicalSegments(canonical));
|
|
116
|
+
throw new Error(`无效的 GitHub channel ID: ${target}`);
|
|
117
|
+
const text = formatOutboundBody(payload);
|
|
153
118
|
const r = parsed.type === 'issue'
|
|
154
119
|
? await this.gh.createIssueComment(parsed.repo, parsed.number, text)
|
|
155
120
|
: await this.gh.createPRComment(parsed.repo, parsed.number, text);
|
|
156
121
|
if (!r.ok)
|
|
157
122
|
throw new Error(`发送失败: ${JSON.stringify(r.data)}`);
|
|
158
|
-
|
|
159
|
-
|
|
123
|
+
logger.debug(formatCompact({
|
|
124
|
+
op: 'github_send',
|
|
125
|
+
endpoint: this.name,
|
|
126
|
+
target,
|
|
127
|
+
messageId: r.data.id,
|
|
128
|
+
}));
|
|
129
|
+
return String(r.data.id);
|
|
160
130
|
}
|
|
161
|
-
|
|
162
|
-
|
|
131
|
+
/** Test / internal: admit a parsed comment when open. */
|
|
132
|
+
admit(comment) {
|
|
133
|
+
if (!this.#open)
|
|
134
|
+
return;
|
|
135
|
+
const botUser = this.config.botLogin || this.gh.getBotLogin() || this.gh.authenticatedUser;
|
|
136
|
+
if (botUser && comment.sender === botUser)
|
|
137
|
+
return;
|
|
138
|
+
const content = enrichInboundContent(formatInboundContent(comment.content), this.config, botUser ?? undefined, comment.repo);
|
|
139
|
+
void this.#options.gateway.receive({
|
|
140
|
+
adapter: this.#options.id,
|
|
141
|
+
target: comment.channelId,
|
|
142
|
+
content,
|
|
143
|
+
sender: comment.sender,
|
|
144
|
+
id: comment.id,
|
|
145
|
+
metadata: Object.freeze({
|
|
146
|
+
endpoint: this.name,
|
|
147
|
+
repo: comment.repo,
|
|
148
|
+
kind: comment.kind,
|
|
149
|
+
createdAt: comment.createdAt,
|
|
150
|
+
}),
|
|
151
|
+
}).catch((err) => {
|
|
152
|
+
logger.warn(formatCompact({
|
|
153
|
+
op: 'github_gateway_receive_failed',
|
|
154
|
+
target: comment.channelId,
|
|
155
|
+
error: err instanceof Error ? err.message : String(err),
|
|
156
|
+
}));
|
|
157
|
+
});
|
|
163
158
|
}
|
|
164
159
|
}
|
|
165
|
-
|
|
160
|
+
export function defaultCreateClient(config) {
|
|
161
|
+
const appAuth = config.appId && config.privateKey
|
|
162
|
+
? { appId: config.appId, privateKey: config.privateKey }
|
|
163
|
+
: undefined;
|
|
164
|
+
return new GhClient({ host: config.host, appAuth });
|
|
165
|
+
}
|
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,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent tool deps for github.
|
|
3
|
+
* Endpoints register themselves on start; tools look up by config name / endpoint id.
|
|
4
|
+
*/
|
|
5
|
+
import type { GhClient } from './gh-client.js';
|
|
6
|
+
import type { ResolvedGithubConfig } from './protocol.js';
|
|
7
|
+
import type { WorkspaceManager } from './workspace-manager.js';
|
|
8
|
+
export interface GithubAgentEndpoint {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly gh: GhClient;
|
|
11
|
+
readonly config: ResolvedGithubConfig;
|
|
12
|
+
getAPI(): GhClient;
|
|
13
|
+
getUserOrDefaultAPI(platform?: string, platformUid?: string): Promise<GhClient | null>;
|
|
14
|
+
getClientId(): string | null;
|
|
15
|
+
getHost(): string | undefined;
|
|
16
|
+
getAppSlug(): string | null;
|
|
17
|
+
getInstallations(): Array<{
|
|
18
|
+
id: number;
|
|
19
|
+
account: {
|
|
20
|
+
login: string;
|
|
21
|
+
type: string;
|
|
22
|
+
};
|
|
23
|
+
target_type: string;
|
|
24
|
+
}>;
|
|
25
|
+
getWorkspaceManager(): WorkspaceManager;
|
|
26
|
+
/** DatabaseHost wired at endpoint creation (optional). */
|
|
27
|
+
getDatabase?(): unknown;
|
|
28
|
+
}
|
|
29
|
+
export interface GithubAgentDeps {
|
|
30
|
+
getEndpoint: (endpointId?: string) => GithubAgentEndpoint;
|
|
31
|
+
/** Alias kept for existing agent handlers that call getAdapter(). */
|
|
32
|
+
getAdapter: () => GithubAgentEndpoint;
|
|
33
|
+
getWorkspaceManager: () => WorkspaceManager;
|
|
34
|
+
getDatabase?: () => {
|
|
35
|
+
models?: Map<string, unknown>;
|
|
36
|
+
} | null | undefined;
|
|
37
|
+
logger?: {
|
|
38
|
+
debug: (...args: unknown[]) => void;
|
|
39
|
+
warn: (...args: unknown[]) => void;
|
|
40
|
+
error: (...args: unknown[]) => void;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export declare function registerGithubAgentEndpoint(endpointId: string, endpoint: GithubAgentEndpoint): () => void;
|
|
44
|
+
/** Optional override used by tests / transitional callers. Pass `null` to clear. */
|
|
45
|
+
export declare function setGithubAgentDeps(deps: GithubAgentDeps | null): void;
|
|
46
|
+
export declare function getGithubAgentDeps(): GithubAgentDeps;
|
|
47
|
+
export declare function getAdapter(): GithubAgentEndpoint;
|