@zhin.js/adapter-github 6.0.0 → 6.0.1

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 (47) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +18 -2
  3. package/adapters/github.js +0 -7
  4. package/adapters/github.ts +0 -7
  5. package/agent/prompt-sections/platform.ts +16 -0
  6. package/agent/tools/bind.ts +4 -3
  7. package/agent/tools/create_pr.ts +3 -2
  8. package/agent/tools/install.ts +4 -3
  9. package/agent/tools/patch_file.ts +3 -2
  10. package/agent/tools/prepare_workspace.ts +3 -2
  11. package/agent/tools/push_branch.ts +3 -2
  12. package/agent/tools/star.ts +3 -2
  13. package/agent/tools/subscribe.ts +3 -3
  14. package/agent/tools/subscriptions.ts +4 -4
  15. package/agent/tools/unbind.ts +4 -3
  16. package/agent/tools/unsubscribe.ts +3 -3
  17. package/agent/tools/whoami.ts +4 -3
  18. package/lib/client.d.ts +36 -0
  19. package/lib/client.js +47 -0
  20. package/lib/endpoint.d.ts +10 -24
  21. package/lib/endpoint.js +35 -65
  22. package/lib/github-bot-handlers.d.ts +5 -4
  23. package/lib/github-bot-handlers.js +12 -13
  24. package/lib/github-endpoint-commands.d.ts +1 -1
  25. package/lib/github-tool-handlers.d.ts +9 -8
  26. package/lib/github-tool-handlers.js +26 -33
  27. package/lib/index.d.ts +1 -1
  28. package/lib/index.js +1 -1
  29. package/lib/protocol.d.ts +1 -1
  30. package/lib/protocol.js +1 -1
  31. package/lib/webhook.d.ts +1 -0
  32. package/lib/webhook.js +1 -0
  33. package/package.json +19 -13
  34. package/plugin.js +0 -17
  35. package/src/client.ts +65 -0
  36. package/src/endpoint.ts +36 -75
  37. package/src/github-bot-handlers.ts +13 -9
  38. package/src/github-tool-handlers.ts +27 -32
  39. package/src/index.ts +1 -9
  40. package/src/protocol.ts +1 -1
  41. package/src/webhook.ts +5 -0
  42. package/lib/agent-prompt.d.ts +0 -2
  43. package/lib/agent-prompt.js +0 -84
  44. package/lib/github-agent-deps.d.ts +0 -47
  45. package/lib/github-agent-deps.js +0 -43
  46. package/src/agent-prompt.ts +0 -101
  47. package/src/github-agent-deps.ts +0 -82
package/src/client.ts ADDED
@@ -0,0 +1,65 @@
1
+ import path from 'node:path';
2
+ import type { PluginDatabaseHost } from 'zhin.js';
3
+ import { defineEndpointClient } from 'zhin.js/adapter';
4
+ import { GhClient } from './gh-client.js';
5
+ import { lookupGithubOauthAccessToken } from './oauth-users.js';
6
+ import type { ResolvedGithubConfig } from './protocol.js';
7
+ import { WorkspaceManager } from './workspace-manager.js';
8
+
9
+ /** GitHub SDK surface exposed to plugin handlers and Agent tools. */
10
+ export class GithubClient {
11
+ #workspaceManager?: WorkspaceManager;
12
+
13
+ constructor(
14
+ readonly name: string,
15
+ readonly api: GhClient,
16
+ readonly config: ResolvedGithubConfig,
17
+ readonly database?: PluginDatabaseHost,
18
+ ) {}
19
+
20
+ async getUserOrDefaultApi(platform?: string, platformUid?: string): Promise<GhClient> {
21
+ if (platform && platformUid) {
22
+ const token = await lookupGithubOauthAccessToken(this.database, platform, platformUid);
23
+ if (token) return this.api.withToken(token);
24
+ }
25
+ return this.api;
26
+ }
27
+
28
+ get clientId(): string | null {
29
+ return this.api.clientId || null;
30
+ }
31
+
32
+ get host(): string | undefined {
33
+ return this.config.host;
34
+ }
35
+
36
+ get appSlug(): string | null {
37
+ return this.api.appSlug || null;
38
+ }
39
+
40
+ get installations() {
41
+ return this.api.installations || [];
42
+ }
43
+
44
+ get workspaceManager(): WorkspaceManager {
45
+ if (!this.#workspaceManager) {
46
+ const root = this.config.workspaceRoot
47
+ ?? path.join(process.cwd(), 'data', 'github-workspaces');
48
+ this.#workspaceManager = new WorkspaceManager(this.api, root);
49
+ }
50
+ return this.#workspaceManager;
51
+ }
52
+ }
53
+
54
+ export type GithubClientEventMap = Record<string, unknown>;
55
+
56
+ declare module '@zhin.js/feature-kit' {
57
+ interface AdapterClientRegistry {
58
+ readonly github: {
59
+ readonly client: GithubClient;
60
+ readonly events: GithubClientEventMap;
61
+ };
62
+ }
63
+ }
64
+
65
+ export const githubClient = defineEndpointClient<GithubClient, GithubClientEventMap>('github');
package/src/endpoint.ts CHANGED
@@ -1,15 +1,12 @@
1
+ import { Endpoint } from 'zhin.js/adapter';
1
2
  /**
2
3
  * GithubEndpoint — lifecycle, outbound send, inbound admit.
3
4
  */
4
- import path from 'node:path';
5
- import type { EndpointInstance, EndpointSendRequest } from 'zhin.js/adapter';
6
- import type { MessageGateway, SideEventGateway } from '@zhin.js/core/runtime';
5
+ import { type EndpointSendRequest } from 'zhin.js/adapter';
7
6
  import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
8
7
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
9
8
  import type { CapabilityId, PluginDatabaseHost } from 'zhin.js';
10
9
  import { GhClient } from './gh-client.js';
11
- import { registerGithubAgentEndpoint } from './github-agent-deps.js';
12
- import { lookupGithubOauthAccessToken } from './oauth-users.js';
13
10
  import {
14
11
  enrichInboundContent,
15
12
  formatInboundContent,
@@ -20,12 +17,10 @@ import {
20
17
  type ResolvedGithubConfig,
21
18
  } from './protocol.js';
22
19
  import { registerGithubWebhookRoutes } from './webhook.js';
23
- import { WorkspaceManager } from './workspace-manager.js';
20
+ import { GithubClient } from './client.js';
24
21
 
25
22
  export interface GithubEndpointOptions {
26
23
  readonly id: CapabilityId;
27
- readonly gateway: MessageGateway;
28
- readonly sideEvents?: SideEventGateway;
29
24
  readonly http?: HttpHost;
30
25
  readonly database?: PluginDatabaseHost;
31
26
  readonly config: ResolvedGithubConfig;
@@ -36,94 +31,49 @@ export interface GithubEndpointOptions {
36
31
  * GitHub 是代码协作面(issue/PR),无好友/群/频道等 IM 社交概念,
37
32
  * 不适用 EndpointManagement 语义端口;本 endpoint 不暴露该端口。
38
33
  */
39
- export class GithubEndpoint implements EndpointInstance {
34
+ export class GithubEndpoint extends Endpoint<GithubClient> {
40
35
  readonly #logger!: ReturnType<typeof getAdapterLogger>;
41
36
 
42
37
  readonly #options: GithubEndpointOptions;
43
- readonly gh: GhClient;
44
- readonly config: ResolvedGithubConfig;
45
- readonly name: string;
46
- #workspaceManager: WorkspaceManager | null = null;
38
+ readonly client: GithubClient;
47
39
  #routeReleases: HttpRouteRegistration[] = [];
48
40
  #open = false;
49
41
  #started = false;
50
- #unregisterAgent?: () => void;
51
42
 
52
43
  constructor(options: GithubEndpointOptions) {
44
+ super();
53
45
  this.#logger = getAdapterLogger('github', options.config.id);
54
46
  this.#options = options;
55
- this.config = options.config;
56
- this.name = options.config.id;
57
- this.gh = options.createClient?.(options.config) ?? defaultCreateClient(options.config);
58
- }
59
-
60
- getAPI(): GhClient {
61
- return this.gh;
62
- }
63
-
64
- async getUserOrDefaultAPI(platform?: string, platformUid?: string): Promise<GhClient | null> {
65
- if (platform && platformUid) {
66
- const token = await lookupGithubOauthAccessToken(
67
- this.#options.database,
68
- platform,
69
- platformUid,
70
- );
71
- if (token) return this.gh.withToken(token);
72
- }
73
- return this.gh;
74
- }
75
-
76
- getClientId(): string | null {
77
- return this.gh.clientId || null;
78
- }
79
-
80
- getHost(): string | undefined {
81
- return this.config.host;
82
- }
83
-
84
- getAppSlug(): string | null {
85
- return this.gh.appSlug || null;
86
- }
87
-
88
- getInstallations() {
89
- return this.gh.installations || [];
47
+ const api = options.createClient?.(options.config) ?? defaultCreateClient(options.config);
48
+ this.client = new GithubClient(options.config.id, api, options.config, options.database);
90
49
  }
91
50
 
92
- getWorkspaceManager(): WorkspaceManager {
93
- if (this.#workspaceManager) return this.#workspaceManager;
94
- const workspaceRoot = this.config.workspaceRoot
95
- ?? path.join(process.cwd(), 'data', 'github-workspaces');
96
- this.#workspaceManager = new WorkspaceManager(this.gh, workspaceRoot);
97
- return this.#workspaceManager;
98
- }
99
-
100
- getDatabase(): PluginDatabaseHost | undefined {
101
- return this.#options.database;
51
+ get config(): ResolvedGithubConfig {
52
+ return this.client.config;
102
53
  }
103
54
 
104
55
  async start(): Promise<void> {
105
56
  if (this.#started) return;
106
57
  this.#started = true;
107
58
  try {
108
- const result = await this.gh.verifyAuth();
59
+ const result = await this.client.api.verifyAuth();
109
60
  if (!result.ok) throw new Error(`GitHub 认证失败: ${result.message}`);
110
- this.#unregisterAgent = registerGithubAgentEndpoint(this.name, this);
111
- if (this.config.webhookSecret) {
61
+ if (this.client.config.webhookSecret) {
112
62
  if (!this.#options.http) {
113
63
  throw new TypeError('GitHub webhook_secret requires httpHostToken');
114
64
  }
115
65
  this.#routeReleases.push(...registerGithubWebhookRoutes(this.#options.http, this));
116
66
  this.#logger.debug(formatCompact({
117
- endpoint: this.name,
67
+ endpoint: this.client.name,
118
68
  op: 'webhook',
119
- path: this.config.webhookPath,
69
+ path: this.client.config.webhookPath,
120
70
  }));
121
71
  } else {
122
72
  this.#logger.debug(formatCompact({
123
- endpoint: this.name,
73
+ endpoint: this.client.name,
124
74
  op: 'connect',
125
75
  mode: 'api-only',
126
- bot: this.gh.authenticatedUser,
76
+ bot: this.client.api.authenticatedUser,
127
77
  }));
128
78
  }
129
79
  } catch (error) {
@@ -144,8 +94,6 @@ export class GithubEndpoint implements EndpointInstance {
144
94
  async stop(): Promise<void> {
145
95
  this.#open = false;
146
96
  for (const release of this.#routeReleases.splice(0)) release();
147
- this.#unregisterAgent?.();
148
- this.#unregisterAgent = undefined;
149
97
  this.#started = false;
150
98
  this.#logger.debug(formatCompact({ op: 'disconnect' }));
151
99
  }
@@ -155,12 +103,12 @@ export class GithubEndpoint implements EndpointInstance {
155
103
  if (!parsed) throw new Error(`无效的 GitHub conversation ID: ${conversation.id}`);
156
104
  const text = formatOutboundBody(payload);
157
105
  const r = parsed.type === 'issue'
158
- ? await this.gh.createIssueComment(parsed.repo, parsed.number, text)
159
- : await this.gh.createPRComment(parsed.repo, parsed.number, text);
106
+ ? await this.client.api.createIssueComment(parsed.repo, parsed.number, text)
107
+ : await this.client.api.createPRComment(parsed.repo, parsed.number, text);
160
108
  if (!r.ok) throw new Error(`发送失败: ${JSON.stringify(r.data)}`);
161
109
  this.#logger.debug(formatCompact({
162
110
  op: 'github_send',
163
- endpoint: this.name,
111
+ endpoint: this.client.name,
164
112
  target: `${conversation.kind}:${conversation.id}`,
165
113
  messageId: r.data.id,
166
114
  }));
@@ -170,21 +118,23 @@ export class GithubEndpoint implements EndpointInstance {
170
118
  /** Test / internal: admit a parsed comment when open. */
171
119
  admit(comment: GithubInboundComment): void {
172
120
  if (!this.#open) return;
173
- const botUser = this.config.botLogin || this.gh.getBotLogin() || this.gh.authenticatedUser;
121
+ const botUser = this.client.config.botLogin
122
+ || this.client.api.getBotLogin()
123
+ || this.client.api.authenticatedUser;
174
124
  if (botUser && comment.sender === botUser) return;
175
125
  const conversation = githubInboundConversation(String(this.#options.id), comment);
176
126
  const content = enrichInboundContent(
177
127
  formatInboundContent(comment.content),
178
- this.config,
128
+ this.client.config,
179
129
  botUser ?? undefined,
180
130
  comment.repo,
181
131
  );
182
- void this.#options.gateway.receive({
132
+ void this.emit('message.receive', {
183
133
  conversation,
184
134
  message: { conversation, id: comment.id },
185
135
  content,
186
136
  sender: { id: comment.sender, name: comment.sender },
187
- endpointId: this.name,
137
+ endpointId: this.client.name,
188
138
  metadata: Object.freeze({
189
139
  repo: comment.repo,
190
140
  kind: comment.kind,
@@ -198,6 +148,17 @@ export class GithubEndpoint implements EndpointInstance {
198
148
  }));
199
149
  });
200
150
  }
151
+
152
+ admitPlatform(name: string, event: unknown): void {
153
+ if (!this.#open) return;
154
+ void this.emitPlatform(name, event).catch((error) => {
155
+ this.#logger.warn(formatCompact({
156
+ op: 'github_platform_event_failed',
157
+ event: name,
158
+ error: error instanceof Error ? error.message : String(error),
159
+ }));
160
+ });
161
+ }
201
162
  }
202
163
 
203
164
  export function defaultCreateClient(config: ResolvedGithubConfig): GhClient {
@@ -3,15 +3,15 @@
3
3
  */
4
4
  import type { Message } from 'zhin.js';
5
5
  import { getCurrentCommMessage } from '@zhin.js/agent/security';
6
- import { getAdapter, getGithubAgentDeps } from './github-agent-deps.js';
6
+ import type { GithubClient } from './client.js';
7
7
  import {
8
8
  parseMessageChannel,
9
9
  resolveWorkspaceBranch,
10
10
  formatChannelContext,
11
11
  } from './github-channel-context.js';
12
12
 
13
- function requireBotGh() {
14
- const gh = getAdapter().getAPI();
13
+ function requireBotGh(client: GithubClient) {
14
+ const gh = client.api;
15
15
  if (!gh?.isAppAuth) {
16
16
  throw new Error('需要 GitHub App 认证(app_id + private_key),Bot 写操作不可用');
17
17
  }
@@ -35,12 +35,13 @@ function resolveChannel(msg: Message, repo?: string) {
35
35
 
36
36
  export async function executeGithubPrepareWorkspace(
37
37
  args: { repo?: string },
38
+ client: GithubClient,
38
39
  commMessage?: Message,
39
40
  ) {
40
41
  const msg = resolveCommMessage(commMessage);
41
42
  const ctx = resolveChannel(msg, args.repo);
42
- const gh = requireBotGh();
43
- const wm = getGithubAgentDeps().getWorkspaceManager();
43
+ const gh = requireBotGh(client);
44
+ const wm = client.workspaceManager;
44
45
  const { branch, base } = await resolveWorkspaceBranch(gh, ctx);
45
46
  const repoPath = await wm.checkoutBranch(ctx.repo, branch, base);
46
47
  return [
@@ -53,11 +54,12 @@ export async function executeGithubPrepareWorkspace(
53
54
 
54
55
  export async function executeGithubPatchFile(
55
56
  args: { repo?: string; path: string; content: string; message: string; branch?: string },
57
+ client: GithubClient,
56
58
  commMessage?: Message,
57
59
  ) {
58
60
  const msg = resolveCommMessage(commMessage);
59
61
  const ctx = resolveChannel(msg, args.repo);
60
- const gh = requireBotGh();
62
+ const gh = requireBotGh(client);
61
63
  const { branch } = args.branch
62
64
  ? { branch: args.branch }
63
65
  : await resolveWorkspaceBranch(gh, ctx);
@@ -80,12 +82,13 @@ export async function executeGithubPatchFile(
80
82
 
81
83
  export async function executeGithubPushBranch(
82
84
  args: { repo?: string; branch?: string; message: string },
85
+ client: GithubClient,
83
86
  commMessage?: Message,
84
87
  ) {
85
88
  const msg = resolveCommMessage(commMessage);
86
89
  const ctx = resolveChannel(msg, args.repo);
87
- const gh = requireBotGh();
88
- const wm = getGithubAgentDeps().getWorkspaceManager();
90
+ const gh = requireBotGh(client);
91
+ const wm = client.workspaceManager;
89
92
  const branch = args.branch ?? (await resolveWorkspaceBranch(gh, ctx)).branch;
90
93
  const result = await wm.commitAndPush(ctx.repo, branch, args.message);
91
94
  return `✅ ${result}\n📍 ${ctx.repo} @ \`${branch}\``;
@@ -93,11 +96,12 @@ export async function executeGithubPushBranch(
93
96
 
94
97
  export async function executeGithubCreatePr(
95
98
  args: { repo?: string; title: string; body?: string; head?: string; base?: string },
99
+ client: GithubClient,
96
100
  commMessage?: Message,
97
101
  ) {
98
102
  const msg = resolveCommMessage(commMessage);
99
103
  const ctx = resolveChannel(msg, args.repo);
100
- const gh = requireBotGh();
104
+ const gh = requireBotGh(client);
101
105
  const resolved = await resolveWorkspaceBranch(gh, ctx);
102
106
  const head = args.head ?? resolved.branch;
103
107
  const base = args.base ?? resolved.base;
@@ -3,10 +3,10 @@ import type { Message } from 'zhin.js';
3
3
  import { getCurrentCommMessage } from '@zhin.js/agent/security';
4
4
  import { GhClient } from './gh-client.js';
5
5
  import type { EventType } from './types.js';
6
- import { getAdapter, getGithubAgentDeps } from './github-agent-deps.js';
6
+ import type { GithubClient } from './client.js';
7
7
 
8
- function oauthModel() {
9
- const db = getGithubAgentDeps().getDatabase?.() as {
8
+ function oauthModel(client: GithubClient) {
9
+ const db = client.database as {
10
10
  models?: Map<string, unknown>;
11
11
  } | null | undefined;
12
12
  return db?.models?.get('github_oauth_users') as {
@@ -16,8 +16,8 @@ function oauthModel() {
16
16
  } | undefined;
17
17
  }
18
18
 
19
- function subscriptionsModel() {
20
- const db = getGithubAgentDeps().getDatabase?.() as {
19
+ function subscriptionsModel(client: GithubClient) {
20
+ const db = client.database as {
21
21
  models?: Map<string, unknown>;
22
22
  } | null | undefined;
23
23
  return db?.models?.get('github_subscriptions') as {
@@ -29,18 +29,16 @@ function subscriptionsModel() {
29
29
  }
30
30
 
31
31
  function depsLogger() {
32
- return getGithubAgentDeps().logger ?? {
32
+ return {
33
33
  debug: (...args: unknown[]) => console.debug(...args),
34
34
  warn: (...args: unknown[]) => console.warn(...args),
35
35
  error: (...args: unknown[]) => console.error(...args),
36
36
  };
37
37
  }
38
38
 
39
- export async function executeGithubStar(args: { action: 'star' | 'unstar' | 'check'; repo: string }, commMessage?: Message) {
40
- const adapter = getAdapter();
39
+ export async function executeGithubStar(args: { action: 'star' | 'unstar' | 'check'; repo: string }, client: GithubClient, commMessage?: Message) {
41
40
  const msg = commMessage ?? getCurrentCommMessage();
42
- const gh = await adapter.getUserOrDefaultAPI(msg?.$adapter, msg?.$sender.id);
43
- if (!gh) return '❌ 没有可用的 GitHub bot';
41
+ const gh = await client.getUserOrDefaultApi(msg?.$adapter, msg?.$sender.id);
44
42
  switch (args.action) {
45
43
  case 'star': {
46
44
  const r = await gh.starRepo(args.repo);
@@ -59,16 +57,15 @@ export async function executeGithubStar(args: { action: 'star' | 'unstar' | 'che
59
57
  }
60
58
  }
61
59
 
62
- export async function executeGithubBind(_args: Record<string, never>, commMessage?: Message) {
63
- const adapter = getAdapter();
60
+ export async function executeGithubBind(_args: Record<string, never>, client: GithubClient, commMessage?: Message) {
64
61
  const log = depsLogger();
65
62
  const msg = commMessage ?? getCurrentCommMessage();
66
63
  if (!msg?.$adapter || !msg?.$sender?.id) return '❌ 无法获取当前用户信息';
67
64
 
68
- const clientId = adapter.getClientId();
65
+ const clientId = client.clientId;
69
66
  if (!clientId) return '❌ Endpoint 未配置 GitHub App 或 App 无 client_id,无法进行账号绑定';
70
67
 
71
- const model = oauthModel();
68
+ const model = oauthModel(client);
72
69
  if (!model) return '❌ 数据库未就绪';
73
70
 
74
71
  const [existing] = await model.select().where({ platform: msg.$adapter, platform_uid: msg.$sender.id });
@@ -77,7 +74,7 @@ export async function executeGithubBind(_args: Record<string, never>, commMessag
77
74
  }
78
75
 
79
76
  try {
80
- const host = adapter.getHost();
77
+ const host = client.host;
81
78
  const codeResp = await GhClient.deviceFlowRequestCode(clientId, host);
82
79
  const tokenPromise = GhClient.deviceFlowPollToken(
83
80
  clientId, codeResp.device_code, codeResp.interval, codeResp.expires_in, host,
@@ -125,11 +122,11 @@ export async function executeGithubBind(_args: Record<string, never>, commMessag
125
122
  }
126
123
  }
127
124
 
128
- export async function executeGithubUnbind(_args: Record<string, never>, commMessage?: Message) {
125
+ export async function executeGithubUnbind(_args: Record<string, never>, client: GithubClient, commMessage?: Message) {
129
126
  const msg = commMessage ?? getCurrentCommMessage();
130
127
  if (!msg?.$adapter || !msg?.$sender?.id) return '❌ 无法获取当前用户信息';
131
128
 
132
- const model = oauthModel();
129
+ const model = oauthModel(client);
133
130
  if (!model) return '❌ 数据库未就绪';
134
131
 
135
132
  const [existing] = await model.select().where({ platform: msg.$adapter, platform_uid: msg.$sender.id });
@@ -139,18 +136,17 @@ export async function executeGithubUnbind(_args: Record<string, never>, commMess
139
136
  return `✅ 已解除 GitHub 账号绑定: ${existing.github_login}`;
140
137
  }
141
138
 
142
- export async function executeGithubWhoami(_args: Record<string, never>, commMessage?: Message) {
143
- const adapter = getAdapter();
139
+ export async function executeGithubWhoami(_args: Record<string, never>, client: GithubClient, commMessage?: Message) {
144
140
  const msg = commMessage ?? getCurrentCommMessage();
145
141
  if (!msg?.$adapter || !msg?.$sender?.id) return '❌ 无法获取当前用户信息';
146
142
 
147
- const model = oauthModel();
143
+ const model = oauthModel(client);
148
144
  if (!model) return '❌ 数据库未就绪';
149
145
 
150
146
  const [existing] = await model.select().where({ platform: msg.$adapter, platform_uid: msg.$sender.id });
151
147
  if (!existing) return '📭 你尚未绑定 GitHub 账号\n🔗 使用 github_bind 绑定你的账号';
152
148
 
153
- const userGh = new GhClient({ host: adapter.getHost(), token: existing.access_token });
149
+ const userGh = new GhClient({ host: client.host, token: existing.access_token });
154
150
  const auth = await userGh.verifyAuth();
155
151
  if (auth.ok) {
156
152
  return `👤 已绑定 GitHub 账号: ${auth.user}\n📅 绑定时间: ${new Date(existing.created_at).toLocaleString('zh-CN')}`;
@@ -158,12 +154,11 @@ export async function executeGithubWhoami(_args: Record<string, never>, commMess
158
154
  return `⚠️ 已绑定账号 ${existing.github_login},但 Token 已失效\n🔗 请执行 github_unbind 后重新 github_bind`;
159
155
  }
160
156
 
161
- export async function executeGithubInstall() {
162
- const adapter = getAdapter();
163
- const slug = adapter.getAppSlug();
157
+ export async function executeGithubInstall(client: GithubClient) {
158
+ const slug = client.appSlug;
164
159
  if (!slug) return '❌ Endpoint 未配置 GitHub App';
165
- const host = adapter.getHost() || 'github.com';
166
- const installations = adapter.getInstallations();
160
+ const host = client.host || 'github.com';
161
+ const installations = client.installations;
167
162
  let msg = `🔗 请点击以下链接安装 GitHub App 到你的仓库:\n https://${host}/apps/${slug}/installations/new`;
168
163
  if (installations.length) {
169
164
  msg += `\n\n📋 当前已安装 (${installations.length}):`;
@@ -174,13 +169,13 @@ export async function executeGithubInstall() {
174
169
  return msg;
175
170
  }
176
171
 
177
- export async function executeGithubSubscribe(args: { repo: string; events?: string }, commMessage?: Message) {
172
+ export async function executeGithubSubscribe(args: { repo: string; events?: string }, client: GithubClient, commMessage?: Message) {
178
173
  const msg = commMessage ?? getCurrentCommMessage();
179
174
  if (!msg?.$adapter || !msg?.$sender.id || !msg?.$channel?.id || !msg?.$endpoint) {
180
175
  return '❌ 无法获取当前聊天通道信息';
181
176
  }
182
177
 
183
- const model = subscriptionsModel();
178
+ const model = subscriptionsModel(client);
184
179
  if (!model) return '❌ 数据库未就绪';
185
180
 
186
181
  const validEvents: EventType[] = ['push', 'issue', 'star', 'fork', 'unstar', 'pull_request'];
@@ -212,13 +207,13 @@ export async function executeGithubSubscribe(args: { repo: string; events?: stri
212
207
  return `✅ 已订阅 ${args.repo}\n📡 事件: ${events.join(', ')}\n📌 通知将推送到当前通道`;
213
208
  }
214
209
 
215
- export async function executeGithubUnsubscribe(args: { repo: string }, commMessage?: Message) {
210
+ export async function executeGithubUnsubscribe(args: { repo: string }, client: GithubClient, commMessage?: Message) {
216
211
  const msg = commMessage ?? getCurrentCommMessage();
217
212
  if (!msg?.$adapter || !msg?.$channel?.id || !msg?.$endpoint) {
218
213
  return '❌ 无法获取当前聊天通道信息';
219
214
  }
220
215
 
221
- const model = subscriptionsModel();
216
+ const model = subscriptionsModel(client);
222
217
  if (!model) return '❌ 数据库未就绪';
223
218
 
224
219
  const [existing] = await model.select().where({
@@ -233,13 +228,13 @@ export async function executeGithubUnsubscribe(args: { repo: string }, commMessa
233
228
  return `✅ 已取消订阅 ${args.repo}`;
234
229
  }
235
230
 
236
- export async function executeGithubSubscriptions(_args: Record<string, never>, commMessage?: Message) {
231
+ export async function executeGithubSubscriptions(_args: Record<string, never>, client: GithubClient, commMessage?: Message) {
237
232
  const msg = commMessage ?? getCurrentCommMessage();
238
233
  if (!msg?.$adapter || !msg?.$channel?.id || !msg?.$endpoint) {
239
234
  return '❌ 无法获取当前聊天通道信息';
240
235
  }
241
236
 
242
- const model = subscriptionsModel();
237
+ const model = subscriptionsModel(client);
243
238
  if (!model) return '❌ 数据库未就绪';
244
239
 
245
240
  const subs = await model.select().where({
package/src/index.ts CHANGED
@@ -20,16 +20,8 @@ export {
20
20
 
21
21
  export * from './types.js';
22
22
 
23
- export {
24
- getAdapter,
25
- getGithubAgentDeps,
26
- registerGithubAgentEndpoint,
27
- setGithubAgentDeps,
28
- type GithubAgentDeps,
29
- type GithubAgentEndpoint,
30
- } from './github-agent-deps.js';
31
-
32
23
  export { GhClient } from './gh-client.js';
24
+ export { GithubClient, githubClient, type GithubClientEventMap } from './client.js';
33
25
  export { WorkspaceManager } from './workspace-manager.js';
34
26
  export {
35
27
  parseMessageChannel,
package/src/protocol.ts CHANGED
@@ -206,7 +206,7 @@ export function enrichInboundContent(
206
206
  return `@${login} ${content}`;
207
207
  }
208
208
 
209
- /** Build inbound text for MessageGateway.receive from markdown/comment body. */
209
+ /** Build inbound text for OutboundMessageService.receive from markdown/comment body. */
210
210
  export function formatInboundContent(body: string): string {
211
211
  return body;
212
212
  }
package/src/webhook.ts CHANGED
@@ -22,6 +22,7 @@ const logger = getLogger('github');
22
22
 
23
23
  export interface GithubWebhookHandler {
24
24
  readonly config: ResolvedGithubConfig;
25
+ admitPlatform(name: string, event: unknown): void;
25
26
  admit(comment: GithubInboundComment): void;
26
27
  }
27
28
 
@@ -106,6 +107,10 @@ export async function dispatchGithubWebhookPayload(
106
107
  const body = payload as Record<string, unknown>;
107
108
  const repo = (body.repository as { full_name?: string } | undefined)?.full_name;
108
109
  logger.debug(`Webhook: ${event}${(body.action as string) ? `.${body.action}` : ''} ${repo || ''}`);
110
+ handler.admitPlatform(
111
+ typeof body.action === 'string' && body.action ? `${event}.${body.action}` : event,
112
+ payload,
113
+ );
109
114
 
110
115
  if (event === 'issue_comment') {
111
116
  const inbound = parseIssueCommentInbound(payload as IssueCommentPayload);
@@ -1,2 +0,0 @@
1
- import type { AgentPromptContributor } from 'zhin.js';
2
- export declare function createGithubAgentPromptContributor(): AgentPromptContributor;
@@ -1,84 +0,0 @@
1
- import { filterTools } from 'zhin.js/ai';
2
- function selectGithubDeferredTools(query, goal, deferredCatalog, maxTools) {
3
- const pool = deferredCatalog.filter(t => !t.name.startsWith('mcp_filesystem') && !t.name.startsWith('mcp_icqq_'));
4
- const pinned = [];
5
- const bash = pool.find(t => t.name === 'bash');
6
- if (bash)
7
- pinned.push(bash);
8
- const preferNames = [
9
- ...pool.filter(t => t.name.startsWith('github_')).map(t => t.name),
10
- ...pool.filter(t => t.name.startsWith('mcp_github_')).map(t => t.name),
11
- ];
12
- for (const name of preferNames) {
13
- if (pinned.length >= maxTools)
14
- break;
15
- const t = pool.find(x => x.name === name);
16
- if (t && !pinned.some(p => p.name === name))
17
- pinned.push(t);
18
- }
19
- const extra = filterTools(`${query} ${goal}`, pool, { maxTools, minScore: 0.08 })
20
- .map((t) => ({ name: t.name, description: t.description }));
21
- const merged = [...pinned];
22
- for (const t of extra) {
23
- if (merged.length >= maxTools)
24
- break;
25
- if (!merged.some(p => p.name === t.name))
26
- merged.push(t);
27
- }
28
- return merged.slice(0, maxTools);
29
- }
30
- function isGithubDelegatedTask(query, goal) {
31
- const text = `${query} ${goal}`;
32
- return /github|gh_|mcp_github|\bissue\b|pull\s*request|\bpr\b/i.test(text);
33
- }
34
- const ORCHESTRATOR_GITHUB = [
35
- 'On GitHub: use run_deferred_task with tool_query "github_".',
36
- 'Discuss issues/PRs in chat context; do not call github_* tools on this orchestrator.',
37
- 'Bot write operations use github_* tools (Installation Token), not mcp_github_*.',
38
- ].join('\n');
39
- const WORKER_GITHUB = [
40
- 'Use github_prepare_workspace before multi-file edits in a repo.',
41
- 'Small single-file change: github_patch_file (Contents API).',
42
- 'Multi-file / tests: workspace + bash, then github_push_branch (requires approval) and github_create_pr for Issues.',
43
- 'Issue thread: new branch + new PR. PR thread: push to existing PR head branch.',
44
- 'Do NOT use mcp_github_* for writes — PAT acts as human, not Bot.',
45
- 'Summarize outcomes (PR link, branch) for the orchestrator.',
46
- ].map(line => `- ${line}`).join('\n');
47
- export function createGithubAgentPromptContributor() {
48
- return {
49
- platform: 'github',
50
- async buildSections(ctx) {
51
- if (ctx.slot === 'orchestrator') {
52
- return [{
53
- id: 'platform.github.orchestrator',
54
- title: '## GitHub',
55
- body: ORCHESTRATOR_GITHUB,
56
- priority: 50,
57
- }];
58
- }
59
- if (ctx.slot === 'deferred_worker') {
60
- const query = ctx.deferred?.toolQuery ?? ctx.deferred?.goal ?? '';
61
- const goal = ctx.deferred?.goal ?? '';
62
- if (!isGithubDelegatedTask(query, goal))
63
- return null;
64
- return [{
65
- id: 'platform.github.deferred_worker',
66
- title: '## GitHub(本任务)',
67
- body: WORKER_GITHUB,
68
- priority: 50,
69
- }];
70
- }
71
- return null;
72
- },
73
- matchesDeferredTask(ctx) {
74
- const query = ctx.deferred?.toolQuery ?? ctx.deferred?.goal ?? ctx.userMessagePreview ?? '';
75
- const goal = ctx.deferred?.goal ?? ctx.userMessagePreview ?? '';
76
- return isGithubDelegatedTask(query, goal);
77
- },
78
- selectDeferredTools(query, goal, catalog, maxTools) {
79
- if (!isGithubDelegatedTask(query, goal))
80
- return null;
81
- return selectGithubDeferredTools(query, goal, catalog, maxTools);
82
- },
83
- };
84
- }