@zhin.js/adapter-github 1.1.18 → 1.1.19

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/lib/endpoint.js CHANGED
@@ -1,112 +1,137 @@
1
- import { Endpoint } from 'zhin.js/adapter';
1
+ import { createEndpointSpecification, endpointSpec } from 'zhin.js/adapter';
2
2
  import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
3
3
  import { GhClient } from './gh-client.js';
4
4
  import { enrichInboundContent, formatInboundContent, formatOutboundBody, githubInboundConversation, parseChannelId, } from './protocol.js';
5
5
  import { registerGithubWebhookRoutes } from './webhook.js';
6
- import { GithubClient } from './client.js';
6
+ import { WorkspaceManager } from './workspace-manager.js';
7
+ import { lookupGithubOauthAccessToken } from './oauth-users.js';
7
8
  /**
8
9
  * GitHub 是代码协作面(issue/PR),无好友/群/频道等 IM 社交概念,
9
10
  * 不适用 EndpointManagement 语义端口;本 endpoint 不暴露该端口。
10
11
  */
11
- export class GithubEndpoint extends Endpoint {
12
+ export class GithubEndpoint extends GhClient {
13
+ [endpointSpec] = createEndpointSpecification(this, {
14
+ start: signal => this.#startFramework(signal), open: () => this.#openFramework(), close: () => this.#closeFramework(),
15
+ stop: () => this.#stopFramework(), send: request => this.#sendFramework(request),
16
+ });
17
+ #workspaceManager;
18
+ get database() { return this.#options.database; }
19
+ get host() { return this.#options.config.host; }
20
+ async getUserOrDefaultApi(platform, platformUid) {
21
+ if (platform && platformUid) {
22
+ const token = await lookupGithubOauthAccessToken(this.database, platform, platformUid);
23
+ if (token)
24
+ return this.withToken(token);
25
+ }
26
+ return this;
27
+ }
28
+ get workspaceManager() { return this.#workspaceManager ??= new WorkspaceManager(this, this.config.workspaceRoot); }
12
29
  #logger;
13
30
  #options;
14
- client;
15
31
  #routeReleases = [];
16
32
  #open = false;
17
33
  #started = false;
18
34
  constructor(options) {
19
- super();
35
+ super({ host: options.config.host, appAuth: { appId: options.config.appId, privateKey: options.config.privateKey } });
20
36
  this.#logger = getAdapterLogger('github', options.config.id);
21
37
  this.#options = options;
22
- const api = options.createClient?.(options.config) ?? defaultCreateClient(options.config);
23
- this.client = new GithubClient(options.config.id, api, options.config, options.database);
24
38
  }
25
39
  get config() {
26
- return this.client.config;
40
+ return this.#options.config;
27
41
  }
28
- async start() {
42
+ async #startFramework(signal) {
43
+ this.bindExecutionSignal(signal);
44
+ signal.throwIfAborted();
29
45
  if (this.#started)
30
46
  return;
31
47
  this.#started = true;
32
48
  try {
33
- const result = await this.client.api.verifyAuth();
49
+ const result = await this.verifyAuth();
50
+ signal.throwIfAborted();
34
51
  if (!result.ok)
35
52
  throw new Error(`GitHub 认证失败: ${result.message}`);
36
- if (this.client.config.webhookSecret) {
53
+ if (this.#options.config.webhookSecret) {
37
54
  if (!this.#options.http) {
38
55
  throw new TypeError('GitHub webhook_secret requires httpHostToken');
39
56
  }
40
57
  this.#routeReleases.push(...registerGithubWebhookRoutes(this.#options.http, this));
41
58
  this.#logger.debug(formatCompact({
42
- endpoint: this.client.name,
59
+ endpoint: this.#options.config.id,
43
60
  op: 'webhook',
44
- path: this.client.config.webhookPath,
61
+ path: this.#options.config.webhookPath,
45
62
  }));
46
63
  }
47
64
  else {
48
65
  this.#logger.debug(formatCompact({
49
- endpoint: this.client.name,
66
+ endpoint: this.#options.config.id,
50
67
  op: 'connect',
51
68
  mode: 'api-only',
52
- bot: this.client.api.authenticatedUser,
69
+ bot: this.authenticatedUser,
53
70
  }));
54
71
  }
55
72
  }
56
73
  catch (error) {
57
- await this.stop();
74
+ await this.#stopFramework();
58
75
  this.#logger.error('Failed to connect GitHub endpoint:', error);
59
76
  throw error;
60
77
  }
61
78
  }
62
- open() {
79
+ #openFramework() {
63
80
  this.#open = true;
64
81
  }
65
- close() {
82
+ #closeFramework() {
66
83
  this.#open = false;
67
84
  }
68
- async stop() {
85
+ async #stopFramework() {
69
86
  this.#open = false;
70
87
  for (const release of this.#routeReleases.splice(0))
71
88
  release();
72
89
  this.#started = false;
73
90
  this.#logger.debug(formatCompact({ op: 'disconnect' }));
74
91
  }
75
- async send({ conversation, payload }) {
92
+ async #sendFramework({ conversation, payload, signal }) {
76
93
  const parsed = parseChannelId(conversation.id);
77
94
  if (!parsed)
78
95
  throw new Error(`无效的 GitHub conversation ID: ${conversation.id}`);
79
96
  const text = formatOutboundBody(payload);
80
97
  const r = parsed.type === 'issue'
81
- ? await this.client.api.createIssueComment(parsed.repo, parsed.number, text)
82
- : await this.client.api.createPRComment(parsed.repo, parsed.number, text);
98
+ ? await this.createIssueComment({ repo: parsed.repo, number: parsed.number, body: text, signal })
99
+ : await this.createPRComment({ repo: parsed.repo, number: parsed.number, body: text, signal });
83
100
  if (!r.ok)
84
101
  throw new Error(`发送失败: ${JSON.stringify(r.data)}`);
85
102
  this.#logger.debug(formatCompact({
86
103
  op: 'github_send',
87
- endpoint: this.client.name,
104
+ endpoint: this.#options.config.id,
88
105
  target: `${conversation.kind}:${conversation.id}`,
89
106
  messageId: r.data.id,
90
107
  }));
91
108
  return String(r.data.id);
92
109
  }
93
110
  /** Test / internal: admit a parsed comment when open. */
94
- admit(comment) {
111
+ admit(comment, nativeEvent = comment) {
112
+ if (!this.#open)
113
+ return;
114
+ void this[endpointSpec].events.run(() => this.#admitComment(comment, nativeEvent)).catch(error => {
115
+ this.#logger.warn(formatCompact({ op: 'github_gateway_receive_failed', error: error instanceof Error ? error.message : String(error) }));
116
+ });
117
+ }
118
+ #admitComment(comment, nativeEvent = comment) {
95
119
  if (!this.#open)
96
120
  return;
97
- const botUser = this.client.config.botLogin
98
- || this.client.api.getBotLogin()
99
- || this.client.api.authenticatedUser;
121
+ const botUser = this.#options.config.botLogin
122
+ || this.getBotLogin()
123
+ || this.authenticatedUser;
100
124
  if (botUser && comment.sender === botUser)
101
125
  return;
102
126
  const conversation = githubInboundConversation(String(this.#options.id), comment);
103
- const content = enrichInboundContent(formatInboundContent(comment.content), this.client.config, botUser ?? undefined, comment.repo);
104
- void this.emit('message.receive', {
127
+ const content = enrichInboundContent(formatInboundContent(comment.content), this.#options.config, botUser ?? undefined, comment.repo);
128
+ void this[endpointSpec].events.emit('message.receive', {
129
+ event: nativeEvent,
105
130
  conversation,
106
131
  message: { conversation, id: comment.id },
107
132
  content,
108
133
  sender: { id: comment.sender, name: comment.sender },
109
- endpointId: this.client.name,
134
+ endpointId: this.#options.config.id,
110
135
  metadata: Object.freeze({
111
136
  repo: comment.repo,
112
137
  kind: comment.kind,
@@ -123,7 +148,7 @@ export class GithubEndpoint extends Endpoint {
123
148
  admitPlatform(name, event) {
124
149
  if (!this.#open)
125
150
  return;
126
- void this.emitPlatform(name, event).catch((error) => {
151
+ void this[endpointSpec].events.run(() => this[endpointSpec].events.platform(name, event)).catch((error) => {
127
152
  this.#logger.warn(formatCompact({
128
153
  op: 'github_platform_event_failed',
129
154
  event: name,
@@ -25,6 +25,7 @@ export interface GitHubCommitAuthor {
25
25
  date?: string;
26
26
  }
27
27
  export interface GhClientOptions {
28
+ readonly signal?: AbortSignal;
28
29
  /** GitHub Enterprise 主机名(默认 github.com) */
29
30
  host?: string;
30
31
  /** 覆盖 gh CLI 默认认证的 Personal Access Token / OAuth Token */
@@ -32,8 +33,27 @@ export interface GhClientOptions {
32
33
  /** GitHub App 认证 — 自动管理 Installation Token */
33
34
  appAuth?: AppAuth;
34
35
  }
36
+ /** Per-request cancellation is independent of this account's execution lifetime. */
37
+ export interface GhRequestLifetime {
38
+ readonly signal?: AbortSignal;
39
+ }
40
+ /** An API action and its transport inputs, retained together through authentication and subprocess IO. */
41
+ export interface GhApiRequest extends GhRequestLifetime {
42
+ readonly method: string;
43
+ readonly path: string;
44
+ readonly body?: unknown;
45
+ readonly headers?: Readonly<Record<string, string>>;
46
+ }
47
+ /** One comment publication, shared by issue and PR conversation sends. */
48
+ export interface GhCommentRequest extends GhRequestLifetime {
49
+ readonly repo: string;
50
+ readonly number: number;
51
+ readonly body: string;
52
+ }
35
53
  export declare class GhClient {
36
- private host?;
54
+ protected apiHost?: string;
55
+ protected executionSignal?: AbortSignal;
56
+ protected bindExecutionSignal(signal: AbortSignal): void;
37
57
  private token?;
38
58
  private _appAuth?;
39
59
  private _resolvedKey?;
@@ -47,6 +67,7 @@ export declare class GhClient {
47
67
  /** 上次 syncInstallations 时间 (ms) */
48
68
  private _lastSyncTime;
49
69
  constructor(options?: GhClientOptions);
70
+ protected fetchNative(input: string, init?: RequestInit): Promise<Response>;
50
71
  /** 创建使用指定 token 的副本(共享 host 配置,不继承 App 认证) */
51
72
  withToken(token: string): GhClient;
52
73
  get authenticatedUser(): string | null;
@@ -56,7 +77,7 @@ export declare class GhClient {
56
77
  * @param repo 仓库全名(owner/repo),用于选择正确的 Installation
57
78
  */
58
79
  private ensureTokenForRepo;
59
- request<T = any>(method: string, path: string, body?: any, headers?: Record<string, string>): Promise<{
80
+ request<T = any>({ method, path, body, headers, signal }: GhApiRequest): Promise<{
60
81
  ok: boolean;
61
82
  status: number;
62
83
  data: T;
@@ -136,7 +157,7 @@ export declare class GhClient {
136
157
  status: number;
137
158
  data: any;
138
159
  }>;
139
- createIssueComment(repo: string, issueNumber: number, body: string): Promise<{
160
+ createIssueComment({ repo, number, body, signal }: GhCommentRequest): Promise<{
140
161
  ok: boolean;
141
162
  status: number;
142
163
  data: {
@@ -150,7 +171,7 @@ export declare class GhClient {
150
171
  status: number;
151
172
  data: any;
152
173
  }>;
153
- createPRComment(repo: string, prNumber: number, body: string): Promise<{
174
+ createPRComment(request: GhCommentRequest): Promise<{
154
175
  ok: boolean;
155
176
  status: number;
156
177
  data: {
@@ -243,9 +264,9 @@ export declare class GhClient {
243
264
  /** 生成 GitHub App JWT(有效期 10 分钟) */
244
265
  static createJWT(appId: string | number, privateKey: string): string;
245
266
  /** 使用 JWT 获取指定 Installation 的 Access Token(有效期 1 小时) */
246
- refreshInstallationToken(installationId: number): Promise<void>;
267
+ refreshInstallationToken(installationId: number, { signal }?: GhRequestLifetime): Promise<void>;
247
268
  /** 发现所有 App 安装及其可访问的仓库 */
248
- syncInstallations(): Promise<void>;
269
+ syncInstallations({ signal }?: GhRequestLifetime): Promise<void>;
249
270
  /** 当前是否使用 App 认证 */
250
271
  get isAppAuth(): boolean;
251
272
  /** 所有已发现的安装 */
package/lib/gh-client.js CHANGED
@@ -9,10 +9,14 @@
9
9
  * 零外部依赖,使用 Node.js 原生模块。
10
10
  */
11
11
  import { spawn } from 'node:child_process';
12
+ import { createEndpointOperation } from 'zhin.js/adapter';
13
+ import { EndpointDeliveryError } from '@zhin.js/im-contract';
12
14
  import crypto from 'node:crypto';
13
15
  import fs from 'node:fs';
14
16
  export class GhClient {
15
- host;
17
+ apiHost;
18
+ executionSignal;
19
+ bindExecutionSignal(signal) { this.executionSignal = signal; }
16
20
  token;
17
21
  _appAuth;
18
22
  _resolvedKey;
@@ -26,23 +30,30 @@ export class GhClient {
26
30
  /** 上次 syncInstallations 时间 (ms) */
27
31
  _lastSyncTime = 0;
28
32
  constructor(options = {}) {
29
- this.host = options.host;
33
+ this.apiHost = options.host;
34
+ this.executionSignal = options.signal;
30
35
  this.token = options.token;
31
36
  this._appAuth = options.appAuth;
32
37
  }
38
+ fetchNative(input, init) {
39
+ this.executionSignal?.throwIfAborted();
40
+ init?.signal?.throwIfAborted();
41
+ return fetch(input, { ...init, ...(this.executionSignal ? { signal: init?.signal ? AbortSignal.any([this.executionSignal, init.signal]) : this.executionSignal } : {}) });
42
+ }
33
43
  /** 创建使用指定 token 的副本(共享 host 配置,不继承 App 认证) */
34
44
  withToken(token) {
35
- return new GhClient({ host: this.host, token });
45
+ return new GhClient({ host: this.apiHost, token, signal: this.executionSignal });
36
46
  }
37
47
  get authenticatedUser() { return this._user; }
38
48
  // ── 底层执行 ──────────────────────────────────────────────────────
39
- exec(args, stdin) {
40
- const fullArgs = this.host ? [...args, '--hostname', this.host] : args;
41
- const env = this.token
42
- ? { ...process.env, GH_TOKEN: this.token }
49
+ exec({ args, stdin, token, signal = this.executionSignal }) {
50
+ signal?.throwIfAborted();
51
+ const fullArgs = this.apiHost ? [...args, '--hostname', this.apiHost] : args;
52
+ const env = token
53
+ ? { ...process.env, GH_TOKEN: token }
43
54
  : undefined;
44
55
  return new Promise((resolve, reject) => {
45
- const proc = spawn('gh', fullArgs, { stdio: ['pipe', 'pipe', 'pipe'], env });
56
+ const proc = spawn('gh', fullArgs, { stdio: ['pipe', 'pipe', 'pipe'], env, signal });
46
57
  let stdout = '';
47
58
  let stderr = '';
48
59
  proc.stdout.on('data', (d) => (stdout += d.toString()));
@@ -73,9 +84,9 @@ export class GhClient {
73
84
  * 确保 App 认证的 Installation Token 有效(自动按 repo 查找对应安装)
74
85
  * @param repo 仓库全名(owner/repo),用于选择正确的 Installation
75
86
  */
76
- async ensureTokenForRepo(repo) {
87
+ async ensureTokenForRepo(repo, signal) {
77
88
  if (!this._appAuth)
78
- return;
89
+ return this.token;
79
90
  let installId;
80
91
  if (repo) {
81
92
  const key = repo.toLowerCase();
@@ -92,7 +103,7 @@ export class GhClient {
92
103
  }
93
104
  // 懒加载重新发现(5 分钟冷却)
94
105
  if (!installId && Date.now() - this._lastSyncTime > 5 * 60_000) {
95
- await this.syncInstallations();
106
+ await this.syncInstallations({ signal });
96
107
  installId = this._repoToInstallation.get(key);
97
108
  if (!installId) {
98
109
  const owner = key.split('/')[0];
@@ -110,18 +121,20 @@ export class GhClient {
110
121
  installId = this._allInstallations[0].id;
111
122
  }
112
123
  if (!installId)
113
- return;
124
+ return this.token;
114
125
  const cached = this._installationTokens.get(installId);
115
126
  if (cached && cached.expiresAt > Date.now() + 60_000) {
116
- this.token = cached.token;
117
- return;
127
+ return cached.token;
118
128
  }
119
- await this.refreshInstallationToken(installId);
129
+ await this.refreshInstallationToken(installId, { signal });
130
+ return this._installationTokens.get(installId)?.token;
120
131
  }
121
- async request(method, path, body, headers) {
132
+ async request({ method, path, body, headers, signal }) {
133
+ const operationSignal = this.executionSignal && signal ? AbortSignal.any([this.executionSignal, signal]) : this.executionSignal ?? signal;
134
+ const operation = createEndpointOperation({ signal: operationSignal });
122
135
  // 从路径自动检测 repo 以选择正确的 Installation Token
123
136
  const repoMatch = path.match(/^\/repos\/([^/?]+\/[^/?]+)/);
124
- await this.ensureTokenForRepo(repoMatch?.[1]);
137
+ const token = await operation.prepare(() => this.ensureTokenForRepo(repoMatch?.[1], operationSignal));
125
138
  const args = ['api', path, '--method', method];
126
139
  if (body)
127
140
  args.push('--input', '-');
@@ -131,7 +144,8 @@ export class GhClient {
131
144
  }
132
145
  }
133
146
  try {
134
- const raw = await this.exec(args, body ? JSON.stringify(body) : undefined);
147
+ const execute = () => this.exec({ args, stdin: body ? JSON.stringify(body) : undefined, token, signal: operationSignal });
148
+ const raw = await operation.run(execute);
135
149
  let data;
136
150
  try {
137
151
  data = raw ? JSON.parse(raw) : null;
@@ -142,6 +156,8 @@ export class GhClient {
142
156
  return { ok: true, status: 200, data };
143
157
  }
144
158
  catch (err) {
159
+ if (err instanceof EndpointDeliveryError)
160
+ throw err;
145
161
  let data;
146
162
  const errObj = err instanceof Error ? err : new Error(String(err));
147
163
  try {
@@ -153,11 +169,11 @@ export class GhClient {
153
169
  return { ok: false, status: err.exitCode || 0, data };
154
170
  }
155
171
  }
156
- get(path) { return this.request('GET', path); }
157
- post(path, body) { return this.request('POST', path, body); }
158
- patch(path, body) { return this.request('PATCH', path, body); }
159
- put(path, body) { return this.request('PUT', path, body); }
160
- del(path, body) { return this.request('DELETE', path, body); }
172
+ get(path) { return this.request({ method: 'GET', path }); }
173
+ post(path, body) { return this.request({ method: 'POST', path, body }); }
174
+ patch(path, body) { return this.request({ method: 'PATCH', path, body }); }
175
+ put(path, body) { return this.request({ method: 'PUT', path, body }); }
176
+ del(path, body) { return this.request({ method: 'DELETE', path, body }); }
161
177
  // ── 认证验证 ──────────────────────────────────────────────────────
162
178
  async verifyAuth() {
163
179
  // App 认证模式:自动发现所有安装
@@ -170,8 +186,8 @@ export class GhClient {
170
186
  // 用 JWT 查询 App 信息(/app 端点只接受 JWT 认证,不能用 Installation Token)
171
187
  const key = this.resolvePrivateKey();
172
188
  const jwt = GhClient.createJWT(this._appAuth.appId, key);
173
- const baseUrl = this.host ? `https://${this.host}/api/v3` : 'https://api.github.com';
174
- const appRes = await fetch(`${baseUrl}/app`, {
189
+ const baseUrl = this.apiHost ? `https://${this.apiHost}/api/v3` : 'https://api.github.com';
190
+ const appRes = await this.fetchNative(`${baseUrl}/app`, {
175
191
  headers: { Authorization: `Bearer ${jwt}`, Accept: 'application/vnd.github+json' },
176
192
  });
177
193
  if (appRes.ok) {
@@ -203,8 +219,8 @@ export class GhClient {
203
219
  }
204
220
  return { ok: false, user: '', message: 'Token 无效' };
205
221
  }
206
- await this.exec(['auth', 'status']);
207
- const raw = await this.exec(['api', '/user', '--jq', '.login']);
222
+ await this.exec({ args: ['auth', 'status'], token: this.token });
223
+ const raw = await this.exec({ args: ['api', '/user', '--jq', '.login'], token: this.token });
208
224
  const login = raw.trim();
209
225
  this._user = login;
210
226
  return { ok: true, user: login, message: `gh CLI: ${login}` };
@@ -254,14 +270,16 @@ export class GhClient {
254
270
  async closePR(repo, number) {
255
271
  return this.patch(`/repos/${repo}/pulls/${number}`, { state: 'closed' });
256
272
  }
257
- async createIssueComment(repo, issueNumber, body) {
258
- return this.post(`/repos/${repo}/issues/${issueNumber}/comments`, { body });
273
+ async createIssueComment({ repo, number, body, signal }) {
274
+ return this.request({
275
+ method: 'POST', path: `/repos/${repo}/issues/${number}/comments`, body: { body }, signal,
276
+ });
259
277
  }
260
278
  async deleteIssueComment(repo, commentId) {
261
279
  return this.del(`/repos/${repo}/issues/comments/${commentId}`);
262
280
  }
263
- async createPRComment(repo, prNumber, body) {
264
- return this.createIssueComment(repo, prNumber, body);
281
+ async createPRComment(request) {
282
+ return this.createIssueComment(request);
265
283
  }
266
284
  async deletePRReviewComment(repo, commentId) {
267
285
  return this.del(`/repos/${repo}/pulls/comments/${commentId}`);
@@ -327,7 +345,7 @@ export class GhClient {
327
345
  return this.getBotIdentitySync();
328
346
  }
329
347
  buildCloneUrl(repo, token) {
330
- const host = this.host || 'github.com';
348
+ const host = this.apiHost || 'github.com';
331
349
  return `https://x-access-token:${encodeURIComponent(token)}@${host}/${repo}.git`;
332
350
  }
333
351
  async resolveBotUserId() {
@@ -337,9 +355,9 @@ export class GhClient {
337
355
  if (!slug)
338
356
  return null;
339
357
  const login = `${slug}[bot]`;
340
- const baseUrl = this.host ? `https://${this.host}/api/v3` : 'https://api.github.com';
358
+ const baseUrl = this.apiHost ? `https://${this.apiHost}/api/v3` : 'https://api.github.com';
341
359
  try {
342
- const res = await fetch(`${baseUrl}/users/${encodeURIComponent(login)}`, {
360
+ const res = await this.fetchNative(`${baseUrl}/users/${encodeURIComponent(login)}`, {
343
361
  headers: { Accept: 'application/vnd.github+json' },
344
362
  });
345
363
  if (!res.ok)
@@ -363,7 +381,7 @@ export class GhClient {
363
381
  return this.del(`/user/starred/${repo}`);
364
382
  }
365
383
  async isStarred(repo) {
366
- const r = await this.request('GET', `/user/starred/${repo}`);
384
+ const r = await this.request({ method: 'GET', path: `/user/starred/${repo}` });
367
385
  return r.ok;
368
386
  }
369
387
  // ── Fork ─────────────────────────────────────────────────────────
@@ -400,16 +418,17 @@ export class GhClient {
400
418
  return `${header}.${payload}.${signature}`;
401
419
  }
402
420
  /** 使用 JWT 获取指定 Installation 的 Access Token(有效期 1 小时) */
403
- async refreshInstallationToken(installationId) {
421
+ async refreshInstallationToken(installationId, { signal } = {}) {
422
+ signal?.throwIfAborted();
404
423
  if (!this._appAuth)
405
424
  throw new Error('未配置 App 认证');
406
425
  const key = this.resolvePrivateKey();
407
426
  const jwt = GhClient.createJWT(this._appAuth.appId, key);
408
- const baseUrl = this.host
409
- ? `https://${this.host}/api/v3`
427
+ const baseUrl = this.apiHost
428
+ ? `https://${this.apiHost}/api/v3`
410
429
  : 'https://api.github.com';
411
- const res = await fetch(`${baseUrl}/app/installations/${installationId}/access_tokens`, {
412
- method: 'POST',
430
+ const res = await this.fetchNative(`${baseUrl}/app/installations/${installationId}/access_tokens`, {
431
+ method: 'POST', signal,
413
432
  headers: {
414
433
  Authorization: `Bearer ${jwt}`,
415
434
  Accept: 'application/vnd.github+json',
@@ -420,6 +439,7 @@ export class GhClient {
420
439
  throw new Error(`Installation Token 获取失败 (${res.status}): ${body}`);
421
440
  }
422
441
  const data = (await res.json());
442
+ signal?.throwIfAborted();
423
443
  this.token = data.token;
424
444
  this._installationTokens.set(installationId, {
425
445
  token: data.token,
@@ -427,39 +447,44 @@ export class GhClient {
427
447
  });
428
448
  }
429
449
  /** 发现所有 App 安装及其可访问的仓库 */
430
- async syncInstallations() {
450
+ async syncInstallations({ signal } = {}) {
451
+ signal?.throwIfAborted();
431
452
  if (!this._appAuth)
432
453
  return;
433
454
  const key = this.resolvePrivateKey();
434
455
  const jwt = GhClient.createJWT(this._appAuth.appId, key);
435
- const baseUrl = this.host ? `https://${this.host}/api/v3` : 'https://api.github.com';
436
- const listRes = await fetch(`${baseUrl}/app/installations`, {
437
- headers: { Authorization: `Bearer ${jwt}`, Accept: 'application/vnd.github+json' },
456
+ const baseUrl = this.apiHost ? `https://${this.apiHost}/api/v3` : 'https://api.github.com';
457
+ const listRes = await this.fetchNative(`${baseUrl}/app/installations`, {
458
+ headers: { Authorization: `Bearer ${jwt}`, Accept: 'application/vnd.github+json' }, signal,
438
459
  });
439
460
  if (!listRes.ok) {
440
461
  const errBody = await listRes.text();
441
462
  throw new Error(`列出安装失败: ${listRes.status} - ${errBody}`);
442
463
  }
443
464
  const installations = (await listRes.json());
465
+ signal?.throwIfAborted();
444
466
  this._allInstallations = installations;
445
467
  this._repoToInstallation.clear();
446
468
  for (const inst of installations) {
447
469
  try {
448
- await this.refreshInstallationToken(inst.id);
470
+ await this.refreshInstallationToken(inst.id, { signal });
449
471
  const cached = this._installationTokens.get(inst.id);
450
472
  if (!cached)
451
473
  continue;
452
- const repoRes = await fetch(`${baseUrl}/installation/repositories?per_page=100`, {
453
- headers: { Authorization: `Bearer ${cached.token}`, Accept: 'application/vnd.github+json' },
474
+ const repoRes = await this.fetchNative(`${baseUrl}/installation/repositories?per_page=100`, {
475
+ headers: { Authorization: `Bearer ${cached.token}`, Accept: 'application/vnd.github+json' }, signal,
454
476
  });
455
477
  if (repoRes.ok) {
456
478
  const data = (await repoRes.json());
479
+ signal?.throwIfAborted();
457
480
  for (const repo of data.repositories) {
458
481
  this._repoToInstallation.set(repo.full_name.toLowerCase(), inst.id);
459
482
  }
460
483
  }
461
484
  }
462
485
  catch {
486
+ signal?.throwIfAborted();
487
+ this.executionSignal?.throwIfAborted();
463
488
  // 单个安装失败不影响其他
464
489
  }
465
490
  }
@@ -490,12 +515,12 @@ export class GhClient {
490
515
  * @returns events 数组 + 新 etag;若 304 未修改则 events 为空
491
516
  */
492
517
  async listRepoEvents(repo, etag) {
493
- await this.ensureTokenForRepo(repo);
518
+ const token = await this.ensureTokenForRepo(repo);
494
519
  const args = ['api', `/repos/${repo}/events?per_page=30`, '--method', 'GET', '--include'];
495
520
  if (etag)
496
521
  args.push('-H', `If-None-Match: ${etag}`);
497
522
  try {
498
- const raw = await this.exec(args);
523
+ const raw = await this.exec({ args, token });
499
524
  // --include 输出包含 HTTP 头 + 空行 + body
500
525
  const sepIdx = raw.indexOf('\r\n\r\n');
501
526
  const headerPart = sepIdx >= 0 ? raw.slice(0, sepIdx) : '';
package/lib/index.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  export { buildChannelId, enrichInboundContent, formatInboundContent, formatNotification, formatOutboundBody, normalizeWebhookPath, parseChannelId, parseIssueCommentInbound, parsePrReviewCommentInbound, parsePrReviewInbound, resolveGithubConfig, shouldAutoReplyRepo, verifyWebhookSignature, type GithubEndpointConfig, type GithubInboundComment, type GithubWireSegment, type ResolvedGithubConfig, } from './protocol.js';
2
2
  export * from './types.js';
3
3
  export { GhClient } from './gh-client.js';
4
- export { GithubClient, githubClient, type GithubClientEventMap } from './client.js';
4
+ export type { GithubClientEventMap } from './client.js';
5
5
  export { WorkspaceManager } from './workspace-manager.js';
6
6
  export { parseMessageChannel, issueBranchName, resolveWorkspaceBranch, formatChannelContext, } from './github-channel-context.js';
7
7
  export { GithubEndpoint, defaultCreateClient, type GithubEndpointOptions, } from './endpoint.js';
8
8
  export { GITHUB_OAUTH_USERS_TABLE, GITHUB_OAUTH_USERS_SCHEMA, defineGithubOauthUsersTable, lookupGithubOauthAccessToken, } from './oauth-users.js';
9
9
  export { registerGithubWebhookRoutes, handleGithubWebhookRequest, dispatchGithubWebhookPayload, type GithubWebhookHandler, } from './webhook.js';
10
+ export { GithubAdapter } from './adapter.js';
package/lib/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  export { buildChannelId, enrichInboundContent, formatInboundContent, formatNotification, formatOutboundBody, normalizeWebhookPath, parseChannelId, parseIssueCommentInbound, parsePrReviewCommentInbound, parsePrReviewInbound, resolveGithubConfig, shouldAutoReplyRepo, verifyWebhookSignature, } from './protocol.js';
2
2
  export * from './types.js';
3
3
  export { GhClient } from './gh-client.js';
4
- export { GithubClient, githubClient } from './client.js';
5
4
  export { WorkspaceManager } from './workspace-manager.js';
6
5
  export { parseMessageChannel, issueBranchName, resolveWorkspaceBranch, formatChannelContext, } from './github-channel-context.js';
7
6
  export { GithubEndpoint, defaultCreateClient, } from './endpoint.js';
8
7
  export { GITHUB_OAUTH_USERS_TABLE, GITHUB_OAUTH_USERS_SCHEMA, defineGithubOauthUsersTable, lookupGithubOauthAccessToken, } from './oauth-users.js';
9
8
  export { registerGithubWebhookRoutes, handleGithubWebhookRequest, dispatchGithubWebhookPayload, } from './webhook.js';
9
+ export { GithubAdapter } from './adapter.js';
package/lib/webhook.d.ts CHANGED
@@ -7,7 +7,7 @@ import { type GithubInboundComment, type ResolvedGithubConfig } from './protocol
7
7
  export interface GithubWebhookHandler {
8
8
  readonly config: ResolvedGithubConfig;
9
9
  admitPlatform(name: string, event: unknown): void;
10
- admit(comment: GithubInboundComment): void;
10
+ admit(comment: GithubInboundComment, nativeEvent?: unknown): void;
11
11
  }
12
12
  export declare function registerGithubWebhookRoutes(http: HttpHost, handler: GithubWebhookHandler): HttpRouteRegistration[];
13
13
  export declare function handleGithubWebhookRequest(request: IncomingMessage, response: ServerResponse, handler: GithubWebhookHandler): Promise<void>;
package/lib/webhook.js CHANGED
@@ -70,19 +70,19 @@ export async function dispatchGithubWebhookPayload(event, payload, handler) {
70
70
  if (event === 'issue_comment') {
71
71
  const inbound = parseIssueCommentInbound(payload);
72
72
  if (inbound)
73
- handler.admit(inbound);
73
+ handler.admit(inbound, payload);
74
74
  return;
75
75
  }
76
76
  if (event === 'pull_request_review_comment') {
77
77
  const inbound = parsePrReviewCommentInbound(payload);
78
78
  if (inbound)
79
- handler.admit(inbound);
79
+ handler.admit(inbound, payload);
80
80
  return;
81
81
  }
82
82
  if (event === 'pull_request_review') {
83
83
  const inbound = parsePrReviewInbound(payload);
84
84
  if (inbound)
85
- handler.admit(inbound);
85
+ handler.admit(inbound, payload);
86
86
  }
87
87
  // Cross-adapter subscription fan-out deferred (needs multi-adapter send in Runtime).
88
88
  }