@zhin.js/adapter-github 3.0.2 → 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 -201
- package/adapters/github.ts +51 -0
- package/agent/skills/github.md +2 -2
- package/agent/tools/bind.ts +2 -2
- package/agent/tools/create_pr.ts +2 -2
- package/agent/tools/install.ts +2 -2
- package/agent/tools/patch_file.ts +2 -2
- package/agent/tools/prepare_workspace.ts +2 -2
- package/agent/tools/push_branch.ts +2 -2
- package/agent/tools/star.ts +2 -2
- package/agent/tools/subscribe.ts +2 -2
- package/agent/tools/subscriptions.ts +2 -2
- package/agent/tools/unbind.ts +2 -2
- package/agent/tools/unsubscribe.ts +2 -2
- package/agent/tools/whoami.ts +2 -2
- package/lib/agent-prompt.d.ts +2 -0
- package/lib/{src/agent-prompt.js → agent-prompt.js} +0 -1
- package/lib/endpoint.d.ts +48 -0
- package/lib/endpoint.js +165 -0
- package/lib/gh-client.d.ts +300 -0
- package/lib/{src/gh-client.js → gh-client.js} +1 -2
- 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/{src/github-bot-handlers.js → github-bot-handlers.js} +0 -1
- package/lib/github-channel-context.d.ts +16 -0
- package/lib/{src/github-channel-context.js → github-channel-context.js} +0 -1
- package/lib/github-tool-handlers.d.ts +17 -0
- package/lib/{src/github-tool-handlers.js → github-tool-handlers.js} +14 -8
- package/lib/index.d.ts +9 -0
- package/lib/index.js +9 -0
- 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 +149 -0
- package/lib/{src/types.js → 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/{src/workspace-manager.js → workspace-manager.js} +37 -1
- package/package.json +53 -26
- package/plugin.ts +52 -0
- package/schema.json +65 -0
- package/src/endpoint.ts +166 -175
- package/src/gh-client.ts +1 -1
- package/src/github-agent-deps.ts +66 -11
- package/src/github-tool-handlers.ts +20 -7
- package/src/index.ts +55 -137
- package/src/oauth-users.ts +47 -0
- package/src/protocol.ts +367 -0
- package/src/webhook.ts +125 -0
- package/src/workspace-manager.ts +44 -0
- package/lib/agent/tools/bind.js +0 -12
- package/lib/agent/tools/bind.js.map +0 -1
- package/lib/agent/tools/create_pr.js +0 -19
- package/lib/agent/tools/create_pr.js.map +0 -1
- package/lib/agent/tools/install.js +0 -12
- package/lib/agent/tools/install.js.map +0 -1
- package/lib/agent/tools/patch_file.js +0 -18
- package/lib/agent/tools/patch_file.js.map +0 -1
- package/lib/agent/tools/prepare_workspace.js +0 -14
- package/lib/agent/tools/prepare_workspace.js.map +0 -1
- package/lib/agent/tools/push_branch.js +0 -17
- package/lib/agent/tools/push_branch.js.map +0 -1
- package/lib/agent/tools/star.js +0 -15
- package/lib/agent/tools/star.js.map +0 -1
- package/lib/agent/tools/subscribe.js +0 -16
- package/lib/agent/tools/subscribe.js.map +0 -1
- package/lib/agent/tools/subscriptions.js +0 -13
- package/lib/agent/tools/subscriptions.js.map +0 -1
- package/lib/agent/tools/unbind.js +0 -12
- package/lib/agent/tools/unbind.js.map +0 -1
- package/lib/agent/tools/unsubscribe.js +0 -15
- package/lib/agent/tools/unsubscribe.js.map +0 -1
- package/lib/agent/tools/whoami.js +0 -12
- package/lib/agent/tools/whoami.js.map +0 -1
- package/lib/src/adapter.js +0 -423
- package/lib/src/adapter.js.map +0 -1
- package/lib/src/agent-prompt.js.map +0 -1
- package/lib/src/endpoint.js +0 -189
- package/lib/src/endpoint.js.map +0 -1
- package/lib/src/gh-client.js.map +0 -1
- package/lib/src/github-agent-deps.js +0 -13
- package/lib/src/github-agent-deps.js.map +0 -1
- package/lib/src/github-bot-handlers.js.map +0 -1
- package/lib/src/github-channel-context.js.map +0 -1
- package/lib/src/github-tool-handlers.js.map +0 -1
- package/lib/src/index.js +0 -104
- package/lib/src/index.js.map +0 -1
- package/lib/src/register-github-mcp.js +0 -41
- package/lib/src/register-github-mcp.js.map +0 -1
- package/lib/src/segment-mapper.js +0 -2
- package/lib/src/segment-mapper.js.map +0 -1
- package/lib/src/types.js.map +0 -1
- package/lib/src/workspace-manager.js.map +0 -1
- package/plugin.yml +0 -3
- package/src/adapter.ts +0 -454
- package/src/register-github-mcp.ts +0 -67
- package/src/segment-mapper.ts +0 -1
package/src/endpoint.ts
CHANGED
|
@@ -1,210 +1,201 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* GithubEndpoint — lifecycle, outbound send, inbound admit.
|
|
3
3
|
*/
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from 'zhin.js';
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
import type { GitHubAdapter } from './adapter.js';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import type { EndpointInstance } from '@zhin.js/adapter';
|
|
6
|
+
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
7
|
+
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
8
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
9
|
+
import type { CapabilityId, DatabaseHost } from '@zhin.js/plugin-runtime';
|
|
11
10
|
import { GhClient } from './gh-client.js';
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
11
|
+
import { registerGithubAgentEndpoint } from './github-agent-deps.js';
|
|
12
|
+
import { lookupGithubOauthAccessToken } from './oauth-users.js';
|
|
13
|
+
import {
|
|
14
|
+
enrichInboundContent,
|
|
15
|
+
formatInboundContent,
|
|
16
|
+
formatOutboundBody,
|
|
17
|
+
parseChannelId,
|
|
18
|
+
type GithubInboundComment,
|
|
19
|
+
type ResolvedGithubConfig,
|
|
20
|
+
} from './protocol.js';
|
|
21
|
+
import { registerGithubWebhookRoutes } from './webhook.js';
|
|
22
|
+
import { WorkspaceManager } from './workspace-manager.js';
|
|
23
|
+
|
|
24
|
+
const logger = getLogger('github');
|
|
25
|
+
|
|
26
|
+
export interface GithubEndpointOptions {
|
|
27
|
+
readonly id: CapabilityId;
|
|
28
|
+
readonly gateway: MessageGateway;
|
|
29
|
+
readonly http?: HttpHost;
|
|
30
|
+
readonly database?: DatabaseHost;
|
|
31
|
+
readonly config: ResolvedGithubConfig;
|
|
32
|
+
readonly createClient?: (config: ResolvedGithubConfig) => GhClient;
|
|
28
33
|
}
|
|
29
34
|
|
|
30
|
-
export
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
export class GithubEndpoint implements EndpointInstance {
|
|
36
|
+
readonly #options: GithubEndpointOptions;
|
|
37
|
+
readonly gh: GhClient;
|
|
38
|
+
readonly config: ResolvedGithubConfig;
|
|
39
|
+
readonly name: string;
|
|
40
|
+
#workspaceManager: WorkspaceManager | null = null;
|
|
41
|
+
#routeReleases: HttpRouteRegistration[] = [];
|
|
42
|
+
#open = false;
|
|
43
|
+
#started = false;
|
|
44
|
+
#unregisterAgent?: () => void;
|
|
45
|
+
|
|
46
|
+
constructor(options: GithubEndpointOptions) {
|
|
47
|
+
this.#options = options;
|
|
48
|
+
this.config = options.config;
|
|
49
|
+
this.name = options.config.name;
|
|
50
|
+
this.gh = options.createClient?.(options.config) ?? defaultCreateClient(options.config);
|
|
51
|
+
}
|
|
36
52
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
config: GitHubEndpointConfig,
|
|
40
|
-
gh: GhClient,
|
|
41
|
-
repo: string,
|
|
42
|
-
): Message<T> {
|
|
43
|
-
const botLogin = config.bot_login || gh.getBotLogin();
|
|
44
|
-
if (botLogin && shouldAutoReplyRepo(config, repo)) {
|
|
45
|
-
const hasBotAt = message.$content.some(
|
|
46
|
-
(seg) => (seg.type === 'at' || seg.type === 'mention') && seg.data?.id === botLogin,
|
|
47
|
-
);
|
|
48
|
-
if (!hasBotAt) {
|
|
49
|
-
message.$content = [
|
|
50
|
-
{ type: 'at', data: { id: botLogin, name: botLogin, text: `@${botLogin}` } },
|
|
51
|
-
...message.$content,
|
|
52
|
-
];
|
|
53
|
-
}
|
|
53
|
+
getAPI(): GhClient {
|
|
54
|
+
return this.gh;
|
|
54
55
|
}
|
|
55
|
-
return message;
|
|
56
|
-
}
|
|
57
56
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
case 'image': return seg.data.url ? `` : '[image]';
|
|
67
|
-
case 'link': return `[${seg.data.text || seg.data.url}](${seg.data.url})`;
|
|
68
|
-
default: return seg.data?.text || `[${seg.type}]`;
|
|
57
|
+
async getUserOrDefaultAPI(platform?: string, platformUid?: string): Promise<GhClient | null> {
|
|
58
|
+
if (platform && platformUid) {
|
|
59
|
+
const token = await lookupGithubOauthAccessToken(
|
|
60
|
+
this.#options.database,
|
|
61
|
+
platform,
|
|
62
|
+
platformUid,
|
|
63
|
+
);
|
|
64
|
+
if (token) return this.gh.withToken(token);
|
|
69
65
|
}
|
|
70
|
-
|
|
71
|
-
}
|
|
66
|
+
return this.gh;
|
|
67
|
+
}
|
|
72
68
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
/** App bot 登录名,供 @ 触发匹配 */
|
|
77
|
-
$platformUserId?: string;
|
|
69
|
+
getClientId(): string | null {
|
|
70
|
+
return this.gh.clientId || null;
|
|
71
|
+
}
|
|
78
72
|
|
|
79
|
-
|
|
73
|
+
getHost(): string | undefined {
|
|
74
|
+
return this.config.host;
|
|
75
|
+
}
|
|
80
76
|
|
|
81
|
-
|
|
82
|
-
return this.
|
|
77
|
+
getAppSlug(): string | null {
|
|
78
|
+
return this.gh.appSlug || null;
|
|
83
79
|
}
|
|
84
80
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const appAuth = app_id && private_key
|
|
88
|
-
? { appId: app_id, privateKey: private_key }
|
|
89
|
-
: undefined;
|
|
90
|
-
this.gh = new GhClient({ host, appAuth });
|
|
81
|
+
getInstallations() {
|
|
82
|
+
return this.gh.installations || [];
|
|
91
83
|
}
|
|
92
84
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
this
|
|
98
|
-
|
|
85
|
+
getWorkspaceManager(): WorkspaceManager {
|
|
86
|
+
if (this.#workspaceManager) return this.#workspaceManager;
|
|
87
|
+
const workspaceRoot = this.config.workspaceRoot
|
|
88
|
+
?? path.join(process.cwd(), 'data', 'github-workspaces');
|
|
89
|
+
this.#workspaceManager = new WorkspaceManager(this.gh, workspaceRoot);
|
|
90
|
+
return this.#workspaceManager;
|
|
99
91
|
}
|
|
100
92
|
|
|
101
|
-
|
|
102
|
-
this
|
|
103
|
-
this.logger.debug(formatCompact({ endpoint: this.$id, disconnect: true }));
|
|
93
|
+
getDatabase(): DatabaseHost | undefined {
|
|
94
|
+
return this.#options.database;
|
|
104
95
|
}
|
|
105
96
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
97
|
+
async start(): Promise<void> {
|
|
98
|
+
if (this.#started) return;
|
|
99
|
+
this.#started = true;
|
|
100
|
+
try {
|
|
101
|
+
const result = await this.gh.verifyAuth();
|
|
102
|
+
if (!result.ok) throw new Error(`GitHub 认证失败: ${result.message}`);
|
|
103
|
+
this.#unregisterAgent = registerGithubAgentEndpoint(this.name, this);
|
|
104
|
+
if (this.config.webhookSecret) {
|
|
105
|
+
if (!this.#options.http) {
|
|
106
|
+
throw new TypeError('GitHub webhook_secret requires httpHostToken');
|
|
107
|
+
}
|
|
108
|
+
this.#routeReleases.push(...registerGithubWebhookRoutes(this.#options.http, this));
|
|
109
|
+
logger.debug(formatCompact({
|
|
110
|
+
endpoint: this.name,
|
|
111
|
+
op: 'webhook',
|
|
112
|
+
path: this.config.webhookPath,
|
|
113
|
+
}));
|
|
114
|
+
} else {
|
|
115
|
+
logger.debug(formatCompact({
|
|
116
|
+
endpoint: this.name,
|
|
117
|
+
op: 'connect',
|
|
118
|
+
mode: 'api-only',
|
|
119
|
+
bot: this.gh.authenticatedUser,
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
await this.stop();
|
|
124
|
+
logger.error('Failed to connect GitHub endpoint:', error);
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
131
127
|
}
|
|
132
128
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const number = payload.pull_request.number;
|
|
136
|
-
const channelId = buildChannelId(repo, 'pr', number);
|
|
137
|
-
const gh = this.gh;
|
|
138
|
-
|
|
139
|
-
const body = payload.comment.path
|
|
140
|
-
? `**${payload.comment.path}**\n${payload.comment.diff_hunk ? '```diff\n' + payload.comment.diff_hunk + '\n```\n' : ''}${payload.comment.body}`
|
|
141
|
-
: payload.comment.body;
|
|
142
|
-
|
|
143
|
-
return Message.from(payload, {
|
|
144
|
-
$id: payload.comment.id.toString(),
|
|
145
|
-
$adapter: 'github',
|
|
146
|
-
$endpoint: this.$config.name,
|
|
147
|
-
$sender: { id: payload.sender.login, name: payload.sender.login },
|
|
148
|
-
$channel: { id: channelId, type: 'group' },
|
|
149
|
-
$content: toCanonicalSegments(parseMarkdown(body)),
|
|
150
|
-
$raw: body,
|
|
151
|
-
$timestamp: new Date(payload.comment.created_at).getTime(),
|
|
152
|
-
$recall: async () => { await gh.deletePRReviewComment(repo, payload.comment.id); },
|
|
153
|
-
$reply: async (content: SendContent): Promise<string> => {
|
|
154
|
-
const r = await gh.createPRComment(repo, number, toMarkdown(content));
|
|
155
|
-
return r.ok ? r.data.id.toString() : '';
|
|
156
|
-
},
|
|
157
|
-
});
|
|
129
|
+
open(): void {
|
|
130
|
+
this.#open = true;
|
|
158
131
|
}
|
|
159
132
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const repo = payload.repository.full_name;
|
|
163
|
-
const number = payload.pull_request.number;
|
|
164
|
-
const channelId = buildChannelId(repo, 'pr', number);
|
|
165
|
-
const gh = this.gh;
|
|
166
|
-
|
|
167
|
-
const stateLabel: Record<string, string> = {
|
|
168
|
-
approved: '✅ APPROVED', changes_requested: '🔄 CHANGES REQUESTED',
|
|
169
|
-
commented: '💬 COMMENTED', dismissed: '❌ DISMISSED',
|
|
170
|
-
};
|
|
171
|
-
const body = `**[${stateLabel[payload.review.state] || payload.review.state}]**\n${payload.review.body}`;
|
|
172
|
-
|
|
173
|
-
return Message.from(payload, {
|
|
174
|
-
$id: payload.review.id.toString(),
|
|
175
|
-
$adapter: 'github',
|
|
176
|
-
$endpoint: this.$config.name,
|
|
177
|
-
$sender: { id: payload.sender.login, name: payload.sender.login },
|
|
178
|
-
$channel: { id: channelId, type: 'group' },
|
|
179
|
-
$content: toCanonicalSegments(parseMarkdown(body)),
|
|
180
|
-
$raw: body,
|
|
181
|
-
$timestamp: new Date(payload.review.submitted_at).getTime(),
|
|
182
|
-
$recall: async () => {},
|
|
183
|
-
$reply: async (content: SendContent): Promise<string> => {
|
|
184
|
-
const r = await gh.createPRComment(repo, number, toMarkdown(content));
|
|
185
|
-
return r.ok ? r.data.id.toString() : '';
|
|
186
|
-
},
|
|
187
|
-
});
|
|
133
|
+
close(): void {
|
|
134
|
+
this.#open = false;
|
|
188
135
|
}
|
|
189
136
|
|
|
190
|
-
async
|
|
191
|
-
|
|
192
|
-
|
|
137
|
+
async stop(): Promise<void> {
|
|
138
|
+
this.#open = false;
|
|
139
|
+
for (const release of this.#routeReleases.splice(0)) release();
|
|
140
|
+
this.#unregisterAgent?.();
|
|
141
|
+
this.#unregisterAgent = undefined;
|
|
142
|
+
this.#started = false;
|
|
143
|
+
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.name }));
|
|
144
|
+
}
|
|
193
145
|
|
|
194
|
-
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
const text =
|
|
146
|
+
async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
|
|
147
|
+
const parsed = parseChannelId(target);
|
|
148
|
+
if (!parsed) throw new Error(`无效的 GitHub channel ID: ${target}`);
|
|
149
|
+
const text = formatOutboundBody(payload);
|
|
198
150
|
const r = parsed.type === 'issue'
|
|
199
151
|
? await this.gh.createIssueComment(parsed.repo, parsed.number, text)
|
|
200
152
|
: await this.gh.createPRComment(parsed.repo, parsed.number, text);
|
|
201
153
|
if (!r.ok) throw new Error(`发送失败: ${JSON.stringify(r.data)}`);
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
154
|
+
logger.debug(formatCompact({
|
|
155
|
+
op: 'github_send',
|
|
156
|
+
endpoint: this.name,
|
|
157
|
+
target,
|
|
158
|
+
messageId: r.data.id,
|
|
159
|
+
}));
|
|
160
|
+
return String(r.data.id);
|
|
205
161
|
}
|
|
206
162
|
|
|
207
|
-
|
|
208
|
-
|
|
163
|
+
/** Test / internal: admit a parsed comment when open. */
|
|
164
|
+
admit(comment: GithubInboundComment): void {
|
|
165
|
+
if (!this.#open) return;
|
|
166
|
+
const botUser = this.config.botLogin || this.gh.getBotLogin() || this.gh.authenticatedUser;
|
|
167
|
+
if (botUser && comment.sender === botUser) return;
|
|
168
|
+
const content = enrichInboundContent(
|
|
169
|
+
formatInboundContent(comment.content),
|
|
170
|
+
this.config,
|
|
171
|
+
botUser ?? undefined,
|
|
172
|
+
comment.repo,
|
|
173
|
+
);
|
|
174
|
+
void this.#options.gateway.receive({
|
|
175
|
+
adapter: this.#options.id,
|
|
176
|
+
target: comment.channelId,
|
|
177
|
+
content,
|
|
178
|
+
sender: comment.sender,
|
|
179
|
+
id: comment.id,
|
|
180
|
+
metadata: Object.freeze({
|
|
181
|
+
endpoint: this.name,
|
|
182
|
+
repo: comment.repo,
|
|
183
|
+
kind: comment.kind,
|
|
184
|
+
createdAt: comment.createdAt,
|
|
185
|
+
}),
|
|
186
|
+
}).catch((err) => {
|
|
187
|
+
logger.warn(formatCompact({
|
|
188
|
+
op: 'github_gateway_receive_failed',
|
|
189
|
+
target: comment.channelId,
|
|
190
|
+
error: err instanceof Error ? err.message : String(err),
|
|
191
|
+
}));
|
|
192
|
+
});
|
|
209
193
|
}
|
|
210
194
|
}
|
|
195
|
+
|
|
196
|
+
export function defaultCreateClient(config: ResolvedGithubConfig): GhClient {
|
|
197
|
+
const appAuth = config.appId && config.privateKey
|
|
198
|
+
? { appId: config.appId, privateKey: config.privateKey }
|
|
199
|
+
: undefined;
|
|
200
|
+
return new GhClient({ host: config.host, appAuth });
|
|
201
|
+
}
|
package/src/gh-client.ts
CHANGED
|
@@ -410,7 +410,7 @@ export class GhClient {
|
|
|
410
410
|
|
|
411
411
|
buildCloneUrl(repo: string, token: string): string {
|
|
412
412
|
const host = this.host || 'github.com';
|
|
413
|
-
return `https://x-access-token:${token}@${host}/${repo}.git`;
|
|
413
|
+
return `https://x-access-token:${encodeURIComponent(token)}@${host}/${repo}.git`;
|
|
414
414
|
}
|
|
415
415
|
|
|
416
416
|
async resolveBotUserId(): Promise<number | null> {
|
package/src/github-agent-deps.ts
CHANGED
|
@@ -1,27 +1,82 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Agent tool deps for github.
|
|
3
|
+
* Endpoints register themselves on start; tools look up by config name / endpoint id.
|
|
3
4
|
*/
|
|
4
|
-
|
|
5
|
-
import type {
|
|
5
|
+
|
|
6
|
+
import type { GhClient } from './gh-client.js';
|
|
7
|
+
import type { ResolvedGithubConfig } from './protocol.js';
|
|
6
8
|
import type { WorkspaceManager } from './workspace-manager.js';
|
|
7
9
|
|
|
10
|
+
export interface GithubAgentEndpoint {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly gh: GhClient;
|
|
13
|
+
readonly config: ResolvedGithubConfig;
|
|
14
|
+
getAPI(): GhClient;
|
|
15
|
+
getUserOrDefaultAPI(platform?: string, platformUid?: string): Promise<GhClient | null>;
|
|
16
|
+
getClientId(): string | null;
|
|
17
|
+
getHost(): string | undefined;
|
|
18
|
+
getAppSlug(): string | null;
|
|
19
|
+
getInstallations(): Array<{ id: number; account: { login: string; type: string }; target_type: string }>;
|
|
20
|
+
getWorkspaceManager(): WorkspaceManager;
|
|
21
|
+
/** DatabaseHost wired at endpoint creation (optional). */
|
|
22
|
+
getDatabase?(): unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
8
25
|
export interface GithubAgentDeps {
|
|
9
|
-
|
|
26
|
+
getEndpoint: (endpointId?: string) => GithubAgentEndpoint;
|
|
27
|
+
/** Alias kept for existing agent handlers that call getAdapter(). */
|
|
28
|
+
getAdapter: () => GithubAgentEndpoint;
|
|
10
29
|
getWorkspaceManager: () => WorkspaceManager;
|
|
11
|
-
|
|
30
|
+
getDatabase?: () => { models?: Map<string, unknown> } | null | undefined;
|
|
31
|
+
logger?: {
|
|
32
|
+
debug: (...args: unknown[]) => void;
|
|
33
|
+
warn: (...args: unknown[]) => void;
|
|
34
|
+
error: (...args: unknown[]) => void;
|
|
35
|
+
};
|
|
12
36
|
}
|
|
13
37
|
|
|
14
|
-
|
|
38
|
+
const endpoints = new Map<string, GithubAgentEndpoint>();
|
|
39
|
+
let override: GithubAgentDeps | null = null;
|
|
40
|
+
|
|
41
|
+
export function registerGithubAgentEndpoint(
|
|
42
|
+
endpointId: string,
|
|
43
|
+
endpoint: GithubAgentEndpoint,
|
|
44
|
+
): () => void {
|
|
45
|
+
endpoints.set(endpointId, endpoint);
|
|
46
|
+
return () => {
|
|
47
|
+
if (endpoints.get(endpointId) === endpoint) {
|
|
48
|
+
endpoints.delete(endpointId);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Optional override used by tests / transitional callers. Pass `null` to clear. */
|
|
54
|
+
export function setGithubAgentDeps(deps: GithubAgentDeps | null): void {
|
|
55
|
+
override = deps;
|
|
56
|
+
}
|
|
15
57
|
|
|
16
|
-
|
|
17
|
-
|
|
58
|
+
function lookup(endpointId?: string): GithubAgentEndpoint {
|
|
59
|
+
if (endpointId) {
|
|
60
|
+
const registered = endpoints.get(endpointId);
|
|
61
|
+
if (!registered) throw new Error(`Endpoint ${endpointId} 不存在`);
|
|
62
|
+
return registered;
|
|
63
|
+
}
|
|
64
|
+
const first = endpoints.values().next().value;
|
|
65
|
+
if (!first) throw new Error('github agent deps not initialized');
|
|
66
|
+
return first;
|
|
18
67
|
}
|
|
19
68
|
|
|
20
69
|
export function getGithubAgentDeps(): GithubAgentDeps {
|
|
21
|
-
if (
|
|
22
|
-
return
|
|
70
|
+
if (override) return override;
|
|
71
|
+
return {
|
|
72
|
+
getEndpoint: lookup,
|
|
73
|
+
getAdapter: () => lookup(),
|
|
74
|
+
getWorkspaceManager: () => lookup().getWorkspaceManager(),
|
|
75
|
+
getDatabase: () => lookup().getDatabase?.() as
|
|
76
|
+
{ models?: Map<string, unknown> } | null | undefined,
|
|
77
|
+
};
|
|
23
78
|
}
|
|
24
79
|
|
|
25
|
-
export function getAdapter():
|
|
80
|
+
export function getAdapter(): GithubAgentEndpoint {
|
|
26
81
|
return getGithubAgentDeps().getAdapter();
|
|
27
82
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { formatCompact
|
|
1
|
+
import { formatCompact } from '@zhin.js/logger';
|
|
2
|
+
import type { Message } from 'zhin.js';
|
|
2
3
|
import { getCurrentCommMessage } from '@zhin.js/agent/security';
|
|
3
4
|
import { GhClient } from './gh-client.js';
|
|
4
5
|
import type { EventType } from './types.js';
|
|
5
6
|
import { getAdapter, getGithubAgentDeps } from './github-agent-deps.js';
|
|
6
7
|
|
|
7
8
|
function oauthModel() {
|
|
8
|
-
const db = getGithubAgentDeps().
|
|
9
|
+
const db = getGithubAgentDeps().getDatabase?.() as {
|
|
10
|
+
models?: Map<string, unknown>;
|
|
11
|
+
} | null | undefined;
|
|
9
12
|
return db?.models?.get('github_oauth_users') as {
|
|
10
13
|
select: () => { where: (q: object) => Promise<any[]> };
|
|
11
14
|
insert: (row: object) => Promise<void>;
|
|
@@ -14,7 +17,9 @@ function oauthModel() {
|
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
function subscriptionsModel() {
|
|
17
|
-
const db = getGithubAgentDeps().
|
|
20
|
+
const db = getGithubAgentDeps().getDatabase?.() as {
|
|
21
|
+
models?: Map<string, unknown>;
|
|
22
|
+
} | null | undefined;
|
|
18
23
|
return db?.models?.get('github_subscriptions') as {
|
|
19
24
|
select: () => { where: (q: object) => Promise<any[]> };
|
|
20
25
|
insert: (row: object) => Promise<void>;
|
|
@@ -23,6 +28,14 @@ function subscriptionsModel() {
|
|
|
23
28
|
} | undefined;
|
|
24
29
|
}
|
|
25
30
|
|
|
31
|
+
function depsLogger() {
|
|
32
|
+
return getGithubAgentDeps().logger ?? {
|
|
33
|
+
debug: (...args: unknown[]) => console.debug(...args),
|
|
34
|
+
warn: (...args: unknown[]) => console.warn(...args),
|
|
35
|
+
error: (...args: unknown[]) => console.error(...args),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
26
39
|
export async function executeGithubStar(args: { action: 'star' | 'unstar' | 'check'; repo: string }, commMessage?: Message) {
|
|
27
40
|
const adapter = getAdapter();
|
|
28
41
|
const msg = commMessage ?? getCurrentCommMessage();
|
|
@@ -48,7 +61,7 @@ export async function executeGithubStar(args: { action: 'star' | 'unstar' | 'che
|
|
|
48
61
|
|
|
49
62
|
export async function executeGithubBind(_args: Record<string, never>, commMessage?: Message) {
|
|
50
63
|
const adapter = getAdapter();
|
|
51
|
-
const
|
|
64
|
+
const log = depsLogger();
|
|
52
65
|
const msg = commMessage ?? getCurrentCommMessage();
|
|
53
66
|
if (!msg?.$adapter || !msg?.$sender?.id) return '❌ 无法获取当前用户信息';
|
|
54
67
|
|
|
@@ -81,7 +94,7 @@ export async function executeGithubBind(_args: Record<string, never>, commMessag
|
|
|
81
94
|
|
|
82
95
|
tokenPromise.then(async (tokenData) => {
|
|
83
96
|
if (!tokenData) {
|
|
84
|
-
|
|
97
|
+
log.warn(formatCompact({ op: 'device_flow', ok: false, platform: msg.$adapter, sender: msg.$sender.id }));
|
|
85
98
|
return;
|
|
86
99
|
}
|
|
87
100
|
|
|
@@ -97,13 +110,13 @@ export async function executeGithubBind(_args: Record<string, never>, commMessag
|
|
|
97
110
|
access_token: tokenData.access_token,
|
|
98
111
|
created_at: Date.now(),
|
|
99
112
|
});
|
|
100
|
-
|
|
113
|
+
log.debug(formatCompact({ op: 'bind', platform: msg.$adapter, sender: msg.$sender.id, login }));
|
|
101
114
|
|
|
102
115
|
if (msg?.$reply) {
|
|
103
116
|
await msg.$reply(`✅ GitHub 账号绑定成功!\n👤 ${login}`);
|
|
104
117
|
}
|
|
105
118
|
}).catch((err) => {
|
|
106
|
-
|
|
119
|
+
log.error('GitHub Device Flow 错误:', err);
|
|
107
120
|
});
|
|
108
121
|
|
|
109
122
|
return replyMsg;
|