@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/CHANGELOG.md +38 -0
- package/README.md +5 -7
- package/adapters/github/index.js +3 -36
- package/adapters/github/index.ts +3 -39
- package/agents/github/skills/github-account/handlers.ts +6 -6
- package/agents/github/skills/github-repository/handlers.js +1 -1
- package/agents/github/skills/github-repository/handlers.ts +7 -7
- package/agents/github/skills/github-subscriptions/handlers.ts +6 -6
- package/commands/github/endpoint/definition.js +0 -2
- package/commands/github/endpoint/definition.ts +0 -2
- package/lib/adapter.d.ts +19 -0
- package/lib/adapter.js +30 -0
- package/lib/client.d.ts +0 -35
- package/lib/client.js +1 -44
- package/lib/console.d.ts +1 -1
- package/lib/console.js +2 -5
- package/lib/endpoint.d.ts +9 -12
- package/lib/endpoint.js +56 -31
- package/lib/gh-client.d.ts +27 -6
- package/lib/gh-client.js +74 -49
- package/lib/index.d.ts +2 -1
- package/lib/index.js +1 -1
- package/lib/webhook.d.ts +1 -1
- package/lib/webhook.js +3 -3
- package/package.json +21 -21
- package/plugin.js +0 -3
- package/src/adapter.ts +32 -0
- package/src/client.ts +0 -61
- package/src/console.ts +2 -5
- package/src/endpoint.ts +52 -33
- package/src/gh-client.ts +90 -57
- package/src/index.ts +2 -1
- package/src/webhook.ts +4 -4
- package/lib/github-runtime-state.d.ts +0 -1
- package/lib/github-runtime-state.js +0 -6
- package/src/github-runtime-state.ts +0 -7
package/lib/endpoint.js
CHANGED
|
@@ -1,112 +1,137 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
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
|
|
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.
|
|
40
|
+
return this.#options.config;
|
|
27
41
|
}
|
|
28
|
-
async
|
|
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.
|
|
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.
|
|
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.
|
|
59
|
+
endpoint: this.#options.config.id,
|
|
43
60
|
op: 'webhook',
|
|
44
|
-
path: this.
|
|
61
|
+
path: this.#options.config.webhookPath,
|
|
45
62
|
}));
|
|
46
63
|
}
|
|
47
64
|
else {
|
|
48
65
|
this.#logger.debug(formatCompact({
|
|
49
|
-
endpoint: this.
|
|
66
|
+
endpoint: this.#options.config.id,
|
|
50
67
|
op: 'connect',
|
|
51
68
|
mode: 'api-only',
|
|
52
|
-
bot: this.
|
|
69
|
+
bot: this.authenticatedUser,
|
|
53
70
|
}));
|
|
54
71
|
}
|
|
55
72
|
}
|
|
56
73
|
catch (error) {
|
|
57
|
-
await this
|
|
74
|
+
await this.#stopFramework();
|
|
58
75
|
this.#logger.error('Failed to connect GitHub endpoint:', error);
|
|
59
76
|
throw error;
|
|
60
77
|
}
|
|
61
78
|
}
|
|
62
|
-
|
|
79
|
+
#openFramework() {
|
|
63
80
|
this.#open = true;
|
|
64
81
|
}
|
|
65
|
-
|
|
82
|
+
#closeFramework() {
|
|
66
83
|
this.#open = false;
|
|
67
84
|
}
|
|
68
|
-
async
|
|
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
|
|
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.
|
|
82
|
-
: await this.
|
|
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.
|
|
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.
|
|
98
|
-
|| this.
|
|
99
|
-
|| this.
|
|
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.
|
|
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.
|
|
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.
|
|
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,
|
package/lib/gh-client.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
174
|
-
const appRes = await
|
|
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,
|
|
258
|
-
return this.
|
|
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(
|
|
264
|
-
return this.createIssueComment(
|
|
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.
|
|
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.
|
|
358
|
+
const baseUrl = this.apiHost ? `https://${this.apiHost}/api/v3` : 'https://api.github.com';
|
|
341
359
|
try {
|
|
342
|
-
const res = await
|
|
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.
|
|
409
|
-
? `https://${this.
|
|
427
|
+
const baseUrl = this.apiHost
|
|
428
|
+
? `https://${this.apiHost}/api/v3`
|
|
410
429
|
: 'https://api.github.com';
|
|
411
|
-
const res = await
|
|
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.
|
|
436
|
-
const listRes = await
|
|
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
|
|
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 {
|
|
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
|
}
|