@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.
Files changed (101) hide show
  1. package/CHANGELOG.md +624 -0
  2. package/README.md +38 -201
  3. package/adapters/github.ts +51 -0
  4. package/agent/skills/github.md +2 -2
  5. package/agent/tools/bind.ts +2 -2
  6. package/agent/tools/create_pr.ts +2 -2
  7. package/agent/tools/install.ts +2 -2
  8. package/agent/tools/patch_file.ts +2 -2
  9. package/agent/tools/prepare_workspace.ts +2 -2
  10. package/agent/tools/push_branch.ts +2 -2
  11. package/agent/tools/star.ts +2 -2
  12. package/agent/tools/subscribe.ts +2 -2
  13. package/agent/tools/subscriptions.ts +2 -2
  14. package/agent/tools/unbind.ts +2 -2
  15. package/agent/tools/unsubscribe.ts +2 -2
  16. package/agent/tools/whoami.ts +2 -2
  17. package/lib/agent-prompt.d.ts +2 -0
  18. package/lib/{src/agent-prompt.js → agent-prompt.js} +0 -1
  19. package/lib/endpoint.d.ts +48 -0
  20. package/lib/endpoint.js +165 -0
  21. package/lib/gh-client.d.ts +300 -0
  22. package/lib/{src/gh-client.js → gh-client.js} +1 -2
  23. package/lib/github-agent-deps.d.ts +47 -0
  24. package/lib/github-agent-deps.js +43 -0
  25. package/lib/github-bot-handlers.d.ts +26 -0
  26. package/lib/{src/github-bot-handlers.js → github-bot-handlers.js} +0 -1
  27. package/lib/github-channel-context.d.ts +16 -0
  28. package/lib/{src/github-channel-context.js → github-channel-context.js} +0 -1
  29. package/lib/github-tool-handlers.d.ts +17 -0
  30. package/lib/{src/github-tool-handlers.js → github-tool-handlers.js} +14 -8
  31. package/lib/index.d.ts +9 -0
  32. package/lib/index.js +9 -0
  33. package/lib/oauth-users.d.ts +33 -0
  34. package/lib/oauth-users.js +38 -0
  35. package/lib/protocol.d.ts +87 -0
  36. package/lib/protocol.js +241 -0
  37. package/lib/types.d.ts +149 -0
  38. package/lib/{src/types.js → types.js} +0 -1
  39. package/lib/webhook.d.ts +13 -0
  40. package/lib/webhook.js +87 -0
  41. package/lib/workspace-manager.d.ts +21 -0
  42. package/lib/{src/workspace-manager.js → workspace-manager.js} +37 -1
  43. package/package.json +53 -26
  44. package/plugin.ts +52 -0
  45. package/schema.json +65 -0
  46. package/src/endpoint.ts +166 -175
  47. package/src/gh-client.ts +1 -1
  48. package/src/github-agent-deps.ts +66 -11
  49. package/src/github-tool-handlers.ts +20 -7
  50. package/src/index.ts +55 -137
  51. package/src/oauth-users.ts +47 -0
  52. package/src/protocol.ts +367 -0
  53. package/src/webhook.ts +125 -0
  54. package/src/workspace-manager.ts +44 -0
  55. package/lib/agent/tools/bind.js +0 -12
  56. package/lib/agent/tools/bind.js.map +0 -1
  57. package/lib/agent/tools/create_pr.js +0 -19
  58. package/lib/agent/tools/create_pr.js.map +0 -1
  59. package/lib/agent/tools/install.js +0 -12
  60. package/lib/agent/tools/install.js.map +0 -1
  61. package/lib/agent/tools/patch_file.js +0 -18
  62. package/lib/agent/tools/patch_file.js.map +0 -1
  63. package/lib/agent/tools/prepare_workspace.js +0 -14
  64. package/lib/agent/tools/prepare_workspace.js.map +0 -1
  65. package/lib/agent/tools/push_branch.js +0 -17
  66. package/lib/agent/tools/push_branch.js.map +0 -1
  67. package/lib/agent/tools/star.js +0 -15
  68. package/lib/agent/tools/star.js.map +0 -1
  69. package/lib/agent/tools/subscribe.js +0 -16
  70. package/lib/agent/tools/subscribe.js.map +0 -1
  71. package/lib/agent/tools/subscriptions.js +0 -13
  72. package/lib/agent/tools/subscriptions.js.map +0 -1
  73. package/lib/agent/tools/unbind.js +0 -12
  74. package/lib/agent/tools/unbind.js.map +0 -1
  75. package/lib/agent/tools/unsubscribe.js +0 -15
  76. package/lib/agent/tools/unsubscribe.js.map +0 -1
  77. package/lib/agent/tools/whoami.js +0 -12
  78. package/lib/agent/tools/whoami.js.map +0 -1
  79. package/lib/src/adapter.js +0 -423
  80. package/lib/src/adapter.js.map +0 -1
  81. package/lib/src/agent-prompt.js.map +0 -1
  82. package/lib/src/endpoint.js +0 -189
  83. package/lib/src/endpoint.js.map +0 -1
  84. package/lib/src/gh-client.js.map +0 -1
  85. package/lib/src/github-agent-deps.js +0 -13
  86. package/lib/src/github-agent-deps.js.map +0 -1
  87. package/lib/src/github-bot-handlers.js.map +0 -1
  88. package/lib/src/github-channel-context.js.map +0 -1
  89. package/lib/src/github-tool-handlers.js.map +0 -1
  90. package/lib/src/index.js +0 -104
  91. package/lib/src/index.js.map +0 -1
  92. package/lib/src/register-github-mcp.js +0 -41
  93. package/lib/src/register-github-mcp.js.map +0 -1
  94. package/lib/src/segment-mapper.js +0 -2
  95. package/lib/src/segment-mapper.js.map +0 -1
  96. package/lib/src/types.js.map +0 -1
  97. package/lib/src/workspace-manager.js.map +0 -1
  98. package/plugin.yml +0 -3
  99. package/src/adapter.ts +0 -454
  100. package/src/register-github-mcp.ts +0 -67
  101. package/src/segment-mapper.ts +0 -1
package/src/endpoint.ts CHANGED
@@ -1,210 +1,201 @@
1
1
  /**
2
- * GitHub Endpoint 实现(基于 gh CLI)
2
+ * GithubEndpoint lifecycle, outbound send, inbound admit.
3
3
  */
4
- import { formatCompact, Endpoint, Message, segment, SendContent, SendOptions, type MessageSegment,
5
- coerceQrcodeSegmentsToText,
6
- expandInteractiveSegmentsInContent,
7
- } from 'zhin.js';
8
- import { buildChannelId, parseChannelId, type GitHubEndpointConfig, type IssueCommentPayload, type PRReviewCommentPayload, type PRReviewPayload } from './types.js';
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 { fromCanonicalSegments, toCanonicalSegments } from './segment-mapper.js';
13
-
14
- /** GitHub @mention:支持 zhin-ai[bot] 等 App bot 用户名 */
15
- const GITHUB_MENTION_RE = /@([^\s@]+(?:\[bot\])?)/g;
16
-
17
- export function parseMarkdown(md: string): MessageSegment[] {
18
- const segments: MessageSegment[] = [];
19
- let lastIdx = 0;
20
- let match: RegExpExecArray | null;
21
- while ((match = GITHUB_MENTION_RE.exec(md)) !== null) {
22
- if (match.index > lastIdx) segments.push({ type: 'text', data: { text: md.slice(lastIdx, match.index) } });
23
- segments.push({ type: 'at', data: { id: match[1], name: match[1], text: match[0] } });
24
- lastIdx = match.index + match[0].length;
25
- }
26
- if (lastIdx < md.length) segments.push({ type: 'text', data: { text: md.slice(lastIdx) } });
27
- return segments.length ? segments : [{ type: 'text', data: { text: md } }];
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 function shouldAutoReplyRepo(config: GitHubEndpointConfig, repo: string): boolean {
31
- const list = config.auto_reply_repos;
32
- if (!list?.length) return false;
33
- const key = repo.toLowerCase();
34
- return list.some((r) => r.toLowerCase() === key);
35
- }
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
- export function enrichGithubInboundMessage<T extends object>(
38
- message: Message<T>,
39
- config: GitHubEndpointConfig,
40
- gh: GhClient,
41
- repo: string,
42
- ): Message<T> {
43
- const botLogin = config.bot_login || gh.getBotLogin();
44
- if (botLogin && shouldAutoReplyRepo(config, repo)) {
45
- const hasBotAt = message.$content.some(
46
- (seg) => (seg.type === 'at' || seg.type === 'mention') && seg.data?.id === botLogin,
47
- );
48
- if (!hasBotAt) {
49
- message.$content = [
50
- { type: 'at', data: { id: botLogin, name: botLogin, text: `@${botLogin}` } },
51
- ...message.$content,
52
- ];
53
- }
53
+ getAPI(): GhClient {
54
+ return this.gh;
54
55
  }
55
- return message;
56
- }
57
56
 
58
- export function toMarkdown(content: SendContent): string {
59
- if (!Array.isArray(content)) content = [content];
60
- return content.map(seg => {
61
- if (typeof seg === 'string') return seg;
62
- switch (seg.type) {
63
- case 'text': return seg.data.text || '';
64
- case 'mention': return `@${seg.data.name || seg.data.target}`;
65
- case 'at': return `@${seg.data.name || seg.data.id}`;
66
- case 'image': return seg.data.url ? `![image](${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
- }).join('');
71
- }
66
+ return this.gh;
67
+ }
72
68
 
73
- export class GitHubEndpoint implements Endpoint<GitHubEndpointConfig, IssueCommentPayload> {
74
- $connected = false;
75
- gh: GhClient;
76
- /** App bot 登录名,供 @ 触发匹配 */
77
- $platformUserId?: string;
69
+ getClientId(): string | null {
70
+ return this.gh.clientId || null;
71
+ }
78
72
 
79
- get $id() { return this.$config.name; }
73
+ getHost(): string | undefined {
74
+ return this.config.host;
75
+ }
80
76
 
81
- get logger() {
82
- return this.adapter.plugin.logger;
77
+ getAppSlug(): string | null {
78
+ return this.gh.appSlug || null;
83
79
  }
84
80
 
85
- constructor(public adapter: GitHubAdapter, public $config: GitHubEndpointConfig) {
86
- const { host, app_id, private_key } = $config;
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
- async $connect(): Promise<void> {
94
- const result = await this.gh.verifyAuth();
95
- if (!result.ok) throw new Error(`GitHub 认证失败: ${result.message}`);
96
- this.$platformUserId = this.$config.bot_login || this.gh.getBotLogin() || undefined;
97
- this.$connected = true;
98
- this.logger.debug(formatCompact({ endpoint: this.$id, bot: this.$platformUserId }));
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
- async $disconnect(): Promise<void> {
102
- this.$connected = false;
103
- this.logger.debug(formatCompact({ endpoint: this.$id, disconnect: true }));
93
+ getDatabase(): DatabaseHost | undefined {
94
+ return this.#options.database;
104
95
  }
105
96
 
106
- $formatMessage(payload: IssueCommentPayload): Message<IssueCommentPayload> {
107
- const repo = payload.repository.full_name;
108
- const number = payload.issue.number;
109
- const isPR = 'pull_request' in (payload.issue as any);
110
- const channelId = buildChannelId(repo, isPR ? 'pr' : 'issue', number);
111
- const gh = this.gh;
112
-
113
- const result = Message.from(payload, {
114
- $id: payload.comment.id.toString(),
115
- $adapter: 'github',
116
- $endpoint: this.$config.name,
117
- $sender: { id: payload.sender.login, name: payload.sender.login },
118
- $channel: { id: channelId, type: 'group' },
119
- $content: toCanonicalSegments(parseMarkdown(payload.comment.body)),
120
- $raw: payload.comment.body,
121
- $timestamp: new Date(payload.comment.created_at).getTime(),
122
- $recall: async () => { await gh.deleteIssueComment(repo, payload.comment.id); },
123
- $reply: async (content: SendContent, quote?: boolean | string): Promise<string> => {
124
- const text = toMarkdown(content);
125
- const finalBody = quote ? `> ${payload.comment.body.split('\n')[0]}\n\n${text}` : text;
126
- const r = await gh.createIssueComment(repo, number, finalBody);
127
- return r.ok ? r.data.id.toString() : '';
128
- },
129
- });
130
- return result;
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
- formatPRReviewComment(payload: PRReviewCommentPayload): Message<PRReviewCommentPayload> {
134
- const repo = payload.repository.full_name;
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
- formatPRReview(payload: PRReviewPayload): Message<PRReviewPayload> | null {
161
- if (!payload.review.body) return null;
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 $sendMessage(options: SendOptions): Promise<string> {
191
- const parsed = parseChannelId(options.id);
192
- if (!parsed) throw new Error(`无效的 GitHub channel ID: ${options.id}`);
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
- const expanded = expandInteractiveSegmentsInContent(coerceQrcodeSegmentsToText(options.content ?? ''));
195
- const arr = Array.isArray(expanded) ? expanded : [expanded];
196
- const canonical = arr.map((s) => (typeof s === 'string' ? { type: 'text' as const, data: { text: s } } : s));
197
- const text = toMarkdown(fromCanonicalSegments(canonical));
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
- this.logger.debug(`${this.$id} send → ${options.id}: ${text.slice(0, 80)}...`);
204
- return r.data.id.toString();
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
- async $recallMessage(id: string): Promise<void> {
208
- this.logger.warn(formatCompact( { op: 'recall', ok: false, error: 'use message.$recall()' }));
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> {
@@ -1,27 +1,82 @@
1
1
  /**
2
- * Shared runtime deps for github agent/ authoring tools.
2
+ * Agent tool deps for github.
3
+ * Endpoints register themselves on start; tools look up by config name / endpoint id.
3
4
  */
4
- import type { Plugin } from 'zhin.js';
5
- import type { GitHubAdapter } from './adapter.js';
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
- getAdapter: () => GitHubAdapter;
26
+ getEndpoint: (endpointId?: string) => GithubAgentEndpoint;
27
+ /** Alias kept for existing agent handlers that call getAdapter(). */
28
+ getAdapter: () => GithubAgentEndpoint;
10
29
  getWorkspaceManager: () => WorkspaceManager;
11
- plugin: Plugin;
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
- let _deps: GithubAgentDeps | null = null;
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
- export function setGithubAgentDeps(deps: GithubAgentDeps): void {
17
- _deps = deps;
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 (!_deps) throw new Error('github agent deps not initialized');
22
- return _deps;
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(): GitHubAdapter {
80
+ export function getAdapter(): GithubAgentEndpoint {
26
81
  return getGithubAgentDeps().getAdapter();
27
82
  }
@@ -1,11 +1,14 @@
1
- import { formatCompact, type Message } from 'zhin.js';
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().plugin.root?.inject('database') as any;
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().plugin.root?.inject('database') as any;
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 { plugin } = getGithubAgentDeps();
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
- plugin.logger.warn(formatCompact({ op: 'device_flow', ok: false, platform: msg.$adapter, sender: msg.$sender.id }));
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
- plugin.logger.debug(formatCompact({ op: 'bind', platform: msg.$adapter, sender: msg.$sender.id, login }));
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
- plugin.logger.error('GitHub Device Flow 错误:', err);
119
+ log.error('GitHub Device Flow 错误:', err);
107
120
  });
108
121
 
109
122
  return replyMsg;