@zhin.js/adapter-github 3.0.1 → 3.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +624 -0
- package/README.md +38 -190
- package/adapters/github.ts +51 -0
- package/{skills/github/SKILL.md → agent/skills/github.md} +22 -5
- package/agent/tools/bind.ts +12 -0
- package/agent/tools/create_pr.ts +19 -0
- package/agent/tools/install.ts +12 -0
- package/agent/tools/patch_file.ts +18 -0
- package/agent/tools/prepare_workspace.ts +14 -0
- package/agent/tools/push_branch.ts +17 -0
- package/agent/tools/star.ts +15 -0
- package/agent/tools/subscribe.ts +16 -0
- package/agent/tools/subscriptions.ts +13 -0
- package/agent/tools/unbind.ts +12 -0
- package/agent/tools/unsubscribe.ts +15 -0
- package/agent/tools/whoami.ts +12 -0
- package/lib/agent-prompt.d.ts +0 -1
- package/lib/agent-prompt.js +10 -9
- package/lib/endpoint.d.ts +46 -24
- package/lib/endpoint.js +144 -144
- package/lib/gh-client.d.ts +73 -1
- package/lib/gh-client.js +99 -1
- package/lib/github-agent-deps.d.ts +47 -0
- package/lib/github-agent-deps.js +43 -0
- package/lib/github-bot-handlers.d.ts +26 -0
- package/lib/github-bot-handlers.js +77 -0
- package/lib/github-channel-context.d.ts +16 -0
- package/lib/github-channel-context.js +31 -0
- package/lib/github-tool-handlers.d.ts +17 -0
- package/lib/github-tool-handlers.js +221 -0
- package/lib/index.d.ts +7 -32
- package/lib/index.js +7 -386
- package/lib/oauth-users.d.ts +33 -0
- package/lib/oauth-users.js +38 -0
- package/lib/protocol.d.ts +87 -0
- package/lib/protocol.js +241 -0
- package/lib/types.d.ts +6 -1
- package/lib/types.js +0 -1
- package/lib/webhook.d.ts +13 -0
- package/lib/webhook.js +87 -0
- package/lib/workspace-manager.d.ts +21 -0
- package/lib/workspace-manager.js +145 -0
- package/package.json +60 -23
- package/plugin.ts +52 -0
- package/schema.json +65 -0
- package/src/agent-prompt.ts +11 -10
- package/src/endpoint.ts +169 -150
- package/src/gh-client.ts +131 -0
- package/src/github-agent-deps.ts +82 -0
- package/src/github-bot-handlers.ts +109 -0
- package/src/github-channel-context.ts +46 -0
- package/src/github-tool-handlers.ts +257 -0
- package/src/index.ts +55 -431
- package/src/oauth-users.ts +47 -0
- package/src/protocol.ts +367 -0
- package/src/types.ts +6 -0
- package/src/webhook.ts +125 -0
- package/src/workspace-manager.ts +158 -0
- package/lib/adapter.d.ts +0 -66
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -418
- package/lib/adapter.js.map +0 -1
- package/lib/agent-prompt.d.ts.map +0 -1
- package/lib/agent-prompt.js.map +0 -1
- package/lib/endpoint.d.ts.map +0 -1
- package/lib/endpoint.js.map +0 -1
- package/lib/gh-client.d.ts.map +0 -1
- package/lib/gh-client.js.map +0 -1
- package/lib/index.d.ts.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/register-github-mcp.d.ts +0 -7
- package/lib/register-github-mcp.d.ts.map +0 -1
- package/lib/register-github-mcp.js +0 -36
- package/lib/register-github-mcp.js.map +0 -1
- package/lib/segment-mapper.d.ts +0 -2
- package/lib/segment-mapper.d.ts.map +0 -1
- package/lib/segment-mapper.js +0 -2
- package/lib/segment-mapper.js.map +0 -1
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js.map +0 -1
- package/plugin.yml +0 -3
- package/src/adapter.ts +0 -450
- package/src/register-github-mcp.ts +0 -62
- package/src/segment-mapper.ts +0 -1
package/plugin.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { definePlugin, databaseHostToken } from '@zhin.js/plugin-runtime';
|
|
2
|
+
import { defineGithubOauthUsersTable } from './src/oauth-users.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* github_subscriptions — repo event subscriptions per chat channel
|
|
6
|
+
* (used by github_subscriptions agent tool; schema matches legacy defineModel).
|
|
7
|
+
*/
|
|
8
|
+
const GITHUB_SUBSCRIPTIONS_SCHEMA = {
|
|
9
|
+
id: { type: 'integer', primary: true },
|
|
10
|
+
repo: { type: 'text', nullable: false },
|
|
11
|
+
events: { type: 'json', default: [] },
|
|
12
|
+
target_id: { type: 'text', nullable: false },
|
|
13
|
+
target_type: { type: 'text', nullable: false },
|
|
14
|
+
adapter: { type: 'text', nullable: false },
|
|
15
|
+
endpoint: { type: 'text', nullable: false },
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Plugin Runtime GitHub adapter.
|
|
20
|
+
* - Endpoint: `adapters/github.ts`
|
|
21
|
+
* - OAuth user tokens: define `github_oauth_users` when DatabaseHost is present
|
|
22
|
+
*/
|
|
23
|
+
export default definePlugin({
|
|
24
|
+
name: 'github',
|
|
25
|
+
metadata: {
|
|
26
|
+
displayName: 'GitHub Adapter',
|
|
27
|
+
},
|
|
28
|
+
setup(context) {
|
|
29
|
+
if (context.resources.has(databaseHostToken)) {
|
|
30
|
+
const host = context.resources.use(databaseHostToken);
|
|
31
|
+
defineGithubOauthUsersTable(host);
|
|
32
|
+
host.define('github_subscriptions', { ...GITHUB_SUBSCRIPTIONS_SCHEMA });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Agent prompt contributor (orchestrator/deferred-worker GitHub guidance).
|
|
36
|
+
// `zhin.js/agent` is an optional peer — skip silently on IM-only installs.
|
|
37
|
+
let cancelled = false;
|
|
38
|
+
let unregister: (() => void) | undefined;
|
|
39
|
+
void Promise.all([
|
|
40
|
+
import('zhin.js/agent'),
|
|
41
|
+
import('./src/agent-prompt.js'),
|
|
42
|
+
]).then(([agent, prompt]) => {
|
|
43
|
+
if (cancelled) return;
|
|
44
|
+
agent.registerAgentPromptContributor(prompt.createGithubAgentPromptContributor());
|
|
45
|
+
unregister = () => agent.unregisterAgentPromptContributor('github');
|
|
46
|
+
}).catch(() => { /* optional peer not installed */ });
|
|
47
|
+
return () => {
|
|
48
|
+
cancelled = true;
|
|
49
|
+
unregister?.();
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
});
|
package/schema.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"additionalProperties": false,
|
|
5
|
+
"properties": {
|
|
6
|
+
"name": {
|
|
7
|
+
"type": "string",
|
|
8
|
+
"default": "github-bot"
|
|
9
|
+
},
|
|
10
|
+
"host": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"description": "GitHub Enterprise hostname (default github.com)"
|
|
13
|
+
},
|
|
14
|
+
"app_id": {
|
|
15
|
+
"type": ["string", "number"],
|
|
16
|
+
"description": "GitHub App ID"
|
|
17
|
+
},
|
|
18
|
+
"appId": {
|
|
19
|
+
"type": ["string", "number"],
|
|
20
|
+
"description": "GitHub App ID (camelCase alias)"
|
|
21
|
+
},
|
|
22
|
+
"private_key": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"description": "GitHub App private key (PEM content or file path)"
|
|
25
|
+
},
|
|
26
|
+
"privateKey": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"description": "GitHub App private key (camelCase alias)"
|
|
29
|
+
},
|
|
30
|
+
"webhook_secret": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "Webhook HMAC secret; enables httpHostToken POST route"
|
|
33
|
+
},
|
|
34
|
+
"webhookSecret": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"description": "Webhook HMAC secret (camelCase alias)"
|
|
37
|
+
},
|
|
38
|
+
"webhook_path": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"default": "/github/webhook"
|
|
41
|
+
},
|
|
42
|
+
"webhookPath": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"default": "/github/webhook"
|
|
45
|
+
},
|
|
46
|
+
"poll_interval": {
|
|
47
|
+
"type": "number",
|
|
48
|
+
"default": 60,
|
|
49
|
+
"description": "Deferred: polling fallback was removed in the Plugin Runtime migration; currently parsed but unused"
|
|
50
|
+
},
|
|
51
|
+
"auto_reply_repos": {
|
|
52
|
+
"type": "array",
|
|
53
|
+
"items": { "type": "string" },
|
|
54
|
+
"description": "Repos whose Issue/PR comments auto-trigger AI without @bot"
|
|
55
|
+
},
|
|
56
|
+
"bot_login": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"description": "Override App bot login (default {slug}[bot])"
|
|
59
|
+
},
|
|
60
|
+
"workspace_root": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"description": "Managed git workspace root"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
package/src/agent-prompt.ts
CHANGED
|
@@ -4,8 +4,7 @@ import type {
|
|
|
4
4
|
AgentPromptSection,
|
|
5
5
|
DeferredToolCatalogItem,
|
|
6
6
|
} from 'zhin.js';
|
|
7
|
-
import type
|
|
8
|
-
import { filterTools } from 'zhin.js/ai';
|
|
7
|
+
import { filterTools, type AgentTool } from 'zhin.js/ai';
|
|
9
8
|
|
|
10
9
|
function selectGithubDeferredTools(
|
|
11
10
|
query: string,
|
|
@@ -21,8 +20,8 @@ function selectGithubDeferredTools(
|
|
|
21
20
|
if (bash) pinned.push(bash);
|
|
22
21
|
|
|
23
22
|
const preferNames = [
|
|
24
|
-
...pool.filter(t => t.name.startsWith('mcp_github_')).map(t => t.name),
|
|
25
23
|
...pool.filter(t => t.name.startsWith('github_')).map(t => t.name),
|
|
24
|
+
...pool.filter(t => t.name.startsWith('mcp_github_')).map(t => t.name),
|
|
26
25
|
];
|
|
27
26
|
for (const name of preferNames) {
|
|
28
27
|
if (pinned.length >= maxTools) break;
|
|
@@ -47,16 +46,18 @@ function isGithubDelegatedTask(query: string, goal: string): boolean {
|
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
const ORCHESTRATOR_GITHUB = [
|
|
50
|
-
'On GitHub: use run_deferred_task with tool_query "github_"
|
|
51
|
-
'Discuss issues/PRs in chat context; do not call github_*
|
|
52
|
-
'
|
|
49
|
+
'On GitHub: use run_deferred_task with tool_query "github_".',
|
|
50
|
+
'Discuss issues/PRs in chat context; do not call github_* tools on this orchestrator.',
|
|
51
|
+
'Bot write operations use github_* tools (Installation Token), not mcp_github_*.',
|
|
53
52
|
].join('\n');
|
|
54
53
|
|
|
55
54
|
const WORKER_GITHUB = [
|
|
56
|
-
'
|
|
57
|
-
'
|
|
58
|
-
'
|
|
59
|
-
'
|
|
55
|
+
'Use github_prepare_workspace before multi-file edits in a repo.',
|
|
56
|
+
'Small single-file change: github_patch_file (Contents API).',
|
|
57
|
+
'Multi-file / tests: workspace + bash, then github_push_branch (requires approval) and github_create_pr for Issues.',
|
|
58
|
+
'Issue thread: new branch + new PR. PR thread: push to existing PR head branch.',
|
|
59
|
+
'Do NOT use mcp_github_* for writes — PAT acts as human, not Bot.',
|
|
60
|
+
'Summarize outcomes (PR link, branch) for the orchestrator.',
|
|
60
61
|
].map(line => `- ${line}`).join('\n');
|
|
61
62
|
|
|
62
63
|
export function createGithubAgentPromptContributor(): AgentPromptContributor {
|
package/src/endpoint.ts
CHANGED
|
@@ -1,182 +1,201 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* GithubEndpoint — lifecycle, outbound send, inbound admit.
|
|
3
3
|
*/
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
} from 'zhin.js';
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
IssueCommentPayload,
|
|
11
|
-
PRReviewCommentPayload,
|
|
12
|
-
PRReviewPayload,
|
|
13
|
-
} from './types.js';
|
|
14
|
-
import { buildChannelId, parseChannelId } from './types.js';
|
|
15
|
-
import type { GitHubAdapter } from './adapter.js';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import type { EndpointInstance } from '@zhin.js/adapter';
|
|
6
|
+
import type { MessageGateway } from '@zhin.js/core/runtime';
|
|
7
|
+
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
8
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
9
|
+
import type { CapabilityId, DatabaseHost } from '@zhin.js/plugin-runtime';
|
|
16
10
|
import { GhClient } from './gh-client.js';
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
11
|
+
import { registerGithubAgentEndpoint } from './github-agent-deps.js';
|
|
12
|
+
import { lookupGithubOauthAccessToken } from './oauth-users.js';
|
|
13
|
+
import {
|
|
14
|
+
enrichInboundContent,
|
|
15
|
+
formatInboundContent,
|
|
16
|
+
formatOutboundBody,
|
|
17
|
+
parseChannelId,
|
|
18
|
+
type GithubInboundComment,
|
|
19
|
+
type ResolvedGithubConfig,
|
|
20
|
+
} from './protocol.js';
|
|
21
|
+
import { registerGithubWebhookRoutes } from './webhook.js';
|
|
22
|
+
import { WorkspaceManager } from './workspace-manager.js';
|
|
23
|
+
|
|
24
|
+
const logger = getLogger('github');
|
|
25
|
+
|
|
26
|
+
export interface GithubEndpointOptions {
|
|
27
|
+
readonly id: CapabilityId;
|
|
28
|
+
readonly gateway: MessageGateway;
|
|
29
|
+
readonly http?: HttpHost;
|
|
30
|
+
readonly database?: DatabaseHost;
|
|
31
|
+
readonly config: ResolvedGithubConfig;
|
|
32
|
+
readonly createClient?: (config: ResolvedGithubConfig) => GhClient;
|
|
31
33
|
}
|
|
32
34
|
|
|
33
|
-
export
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
export class GithubEndpoint implements EndpointInstance {
|
|
36
|
+
readonly #options: GithubEndpointOptions;
|
|
37
|
+
readonly gh: GhClient;
|
|
38
|
+
readonly config: ResolvedGithubConfig;
|
|
39
|
+
readonly name: string;
|
|
40
|
+
#workspaceManager: WorkspaceManager | null = null;
|
|
41
|
+
#routeReleases: HttpRouteRegistration[] = [];
|
|
42
|
+
#open = false;
|
|
43
|
+
#started = false;
|
|
44
|
+
#unregisterAgent?: () => void;
|
|
45
|
+
|
|
46
|
+
constructor(options: GithubEndpointOptions) {
|
|
47
|
+
this.#options = options;
|
|
48
|
+
this.config = options.config;
|
|
49
|
+
this.name = options.config.name;
|
|
50
|
+
this.gh = options.createClient?.(options.config) ?? defaultCreateClient(options.config);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getAPI(): GhClient {
|
|
54
|
+
return this.gh;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async getUserOrDefaultAPI(platform?: string, platformUid?: string): Promise<GhClient | null> {
|
|
58
|
+
if (platform && platformUid) {
|
|
59
|
+
const token = await lookupGithubOauthAccessToken(
|
|
60
|
+
this.#options.database,
|
|
61
|
+
platform,
|
|
62
|
+
platformUid,
|
|
63
|
+
);
|
|
64
|
+
if (token) return this.gh.withToken(token);
|
|
44
65
|
}
|
|
45
|
-
|
|
46
|
-
}
|
|
66
|
+
return this.gh;
|
|
67
|
+
}
|
|
47
68
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
69
|
+
getClientId(): string | null {
|
|
70
|
+
return this.gh.clientId || null;
|
|
71
|
+
}
|
|
51
72
|
|
|
52
|
-
|
|
73
|
+
getHost(): string | undefined {
|
|
74
|
+
return this.config.host;
|
|
75
|
+
}
|
|
53
76
|
|
|
54
|
-
|
|
55
|
-
return this.
|
|
77
|
+
getAppSlug(): string | null {
|
|
78
|
+
return this.gh.appSlug || null;
|
|
56
79
|
}
|
|
57
80
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const appAuth = app_id && private_key
|
|
61
|
-
? { appId: app_id, privateKey: private_key }
|
|
62
|
-
: undefined;
|
|
63
|
-
this.gh = new GhClient({ host, appAuth });
|
|
81
|
+
getInstallations() {
|
|
82
|
+
return this.gh.installations || [];
|
|
64
83
|
}
|
|
65
84
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
this
|
|
85
|
+
getWorkspaceManager(): WorkspaceManager {
|
|
86
|
+
if (this.#workspaceManager) return this.#workspaceManager;
|
|
87
|
+
const workspaceRoot = this.config.workspaceRoot
|
|
88
|
+
?? path.join(process.cwd(), 'data', 'github-workspaces');
|
|
89
|
+
this.#workspaceManager = new WorkspaceManager(this.gh, workspaceRoot);
|
|
90
|
+
return this.#workspaceManager;
|
|
71
91
|
}
|
|
72
92
|
|
|
73
|
-
|
|
74
|
-
this
|
|
75
|
-
this.logger.debug(formatCompact({ endpoint: this.$id, disconnect: true }));
|
|
93
|
+
getDatabase(): DatabaseHost | undefined {
|
|
94
|
+
return this.#options.database;
|
|
76
95
|
}
|
|
77
96
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
97
|
+
async start(): Promise<void> {
|
|
98
|
+
if (this.#started) return;
|
|
99
|
+
this.#started = true;
|
|
100
|
+
try {
|
|
101
|
+
const result = await this.gh.verifyAuth();
|
|
102
|
+
if (!result.ok) throw new Error(`GitHub 认证失败: ${result.message}`);
|
|
103
|
+
this.#unregisterAgent = registerGithubAgentEndpoint(this.name, this);
|
|
104
|
+
if (this.config.webhookSecret) {
|
|
105
|
+
if (!this.#options.http) {
|
|
106
|
+
throw new TypeError('GitHub webhook_secret requires httpHostToken');
|
|
107
|
+
}
|
|
108
|
+
this.#routeReleases.push(...registerGithubWebhookRoutes(this.#options.http, this));
|
|
109
|
+
logger.debug(formatCompact({
|
|
110
|
+
endpoint: this.name,
|
|
111
|
+
op: 'webhook',
|
|
112
|
+
path: this.config.webhookPath,
|
|
113
|
+
}));
|
|
114
|
+
} else {
|
|
115
|
+
logger.debug(formatCompact({
|
|
116
|
+
endpoint: this.name,
|
|
117
|
+
op: 'connect',
|
|
118
|
+
mode: 'api-only',
|
|
119
|
+
bot: this.gh.authenticatedUser,
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
await this.stop();
|
|
124
|
+
logger.error('Failed to connect GitHub endpoint:', error);
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
103
127
|
}
|
|
104
128
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const number = payload.pull_request.number;
|
|
108
|
-
const channelId = buildChannelId(repo, 'pr', number);
|
|
109
|
-
const gh = this.gh;
|
|
110
|
-
|
|
111
|
-
const body = payload.comment.path
|
|
112
|
-
? `**${payload.comment.path}**\n${payload.comment.diff_hunk ? '```diff\n' + payload.comment.diff_hunk + '\n```\n' : ''}${payload.comment.body}`
|
|
113
|
-
: payload.comment.body;
|
|
114
|
-
|
|
115
|
-
return Message.from(payload, {
|
|
116
|
-
$id: payload.comment.id.toString(),
|
|
117
|
-
$adapter: 'github',
|
|
118
|
-
$endpoint: this.$config.name,
|
|
119
|
-
$sender: { id: payload.sender.login, name: payload.sender.login },
|
|
120
|
-
$channel: { id: channelId, type: 'group' },
|
|
121
|
-
$content: toCanonicalSegments(parseMarkdown(body)),
|
|
122
|
-
$raw: body,
|
|
123
|
-
$timestamp: new Date(payload.comment.created_at).getTime(),
|
|
124
|
-
$recall: async () => { await gh.deletePRReviewComment(repo, payload.comment.id); },
|
|
125
|
-
$reply: async (content: SendContent): Promise<string> => {
|
|
126
|
-
const r = await gh.createPRComment(repo, number, toMarkdown(content));
|
|
127
|
-
return r.ok ? r.data.id.toString() : '';
|
|
128
|
-
},
|
|
129
|
-
});
|
|
129
|
+
open(): void {
|
|
130
|
+
this.#open = true;
|
|
130
131
|
}
|
|
131
132
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const repo = payload.repository.full_name;
|
|
135
|
-
const number = payload.pull_request.number;
|
|
136
|
-
const channelId = buildChannelId(repo, 'pr', number);
|
|
137
|
-
const gh = this.gh;
|
|
138
|
-
|
|
139
|
-
const stateLabel: Record<string, string> = {
|
|
140
|
-
approved: '✅ APPROVED', changes_requested: '🔄 CHANGES REQUESTED',
|
|
141
|
-
commented: '💬 COMMENTED', dismissed: '❌ DISMISSED',
|
|
142
|
-
};
|
|
143
|
-
const body = `**[${stateLabel[payload.review.state] || payload.review.state}]**\n${payload.review.body}`;
|
|
144
|
-
|
|
145
|
-
return Message.from(payload, {
|
|
146
|
-
$id: payload.review.id.toString(),
|
|
147
|
-
$adapter: 'github',
|
|
148
|
-
$endpoint: this.$config.name,
|
|
149
|
-
$sender: { id: payload.sender.login, name: payload.sender.login },
|
|
150
|
-
$channel: { id: channelId, type: 'group' },
|
|
151
|
-
$content: toCanonicalSegments(parseMarkdown(body)),
|
|
152
|
-
$raw: body,
|
|
153
|
-
$timestamp: new Date(payload.review.submitted_at).getTime(),
|
|
154
|
-
$recall: async () => {},
|
|
155
|
-
$reply: async (content: SendContent): Promise<string> => {
|
|
156
|
-
const r = await gh.createPRComment(repo, number, toMarkdown(content));
|
|
157
|
-
return r.ok ? r.data.id.toString() : '';
|
|
158
|
-
},
|
|
159
|
-
});
|
|
133
|
+
close(): void {
|
|
134
|
+
this.#open = false;
|
|
160
135
|
}
|
|
161
136
|
|
|
162
|
-
async
|
|
163
|
-
|
|
164
|
-
|
|
137
|
+
async stop(): Promise<void> {
|
|
138
|
+
this.#open = false;
|
|
139
|
+
for (const release of this.#routeReleases.splice(0)) release();
|
|
140
|
+
this.#unregisterAgent?.();
|
|
141
|
+
this.#unregisterAgent = undefined;
|
|
142
|
+
this.#started = false;
|
|
143
|
+
logger.debug(formatCompact({ op: 'disconnect', endpoint: this.name }));
|
|
144
|
+
}
|
|
165
145
|
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
const text =
|
|
146
|
+
async send({ target, payload }: { readonly target: string; readonly payload: unknown }): Promise<string> {
|
|
147
|
+
const parsed = parseChannelId(target);
|
|
148
|
+
if (!parsed) throw new Error(`无效的 GitHub channel ID: ${target}`);
|
|
149
|
+
const text = formatOutboundBody(payload);
|
|
170
150
|
const r = parsed.type === 'issue'
|
|
171
151
|
? await this.gh.createIssueComment(parsed.repo, parsed.number, text)
|
|
172
152
|
: await this.gh.createPRComment(parsed.repo, parsed.number, text);
|
|
173
153
|
if (!r.ok) throw new Error(`发送失败: ${JSON.stringify(r.data)}`);
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
154
|
+
logger.debug(formatCompact({
|
|
155
|
+
op: 'github_send',
|
|
156
|
+
endpoint: this.name,
|
|
157
|
+
target,
|
|
158
|
+
messageId: r.data.id,
|
|
159
|
+
}));
|
|
160
|
+
return String(r.data.id);
|
|
177
161
|
}
|
|
178
162
|
|
|
179
|
-
|
|
180
|
-
|
|
163
|
+
/** Test / internal: admit a parsed comment when open. */
|
|
164
|
+
admit(comment: GithubInboundComment): void {
|
|
165
|
+
if (!this.#open) return;
|
|
166
|
+
const botUser = this.config.botLogin || this.gh.getBotLogin() || this.gh.authenticatedUser;
|
|
167
|
+
if (botUser && comment.sender === botUser) return;
|
|
168
|
+
const content = enrichInboundContent(
|
|
169
|
+
formatInboundContent(comment.content),
|
|
170
|
+
this.config,
|
|
171
|
+
botUser ?? undefined,
|
|
172
|
+
comment.repo,
|
|
173
|
+
);
|
|
174
|
+
void this.#options.gateway.receive({
|
|
175
|
+
adapter: this.#options.id,
|
|
176
|
+
target: comment.channelId,
|
|
177
|
+
content,
|
|
178
|
+
sender: comment.sender,
|
|
179
|
+
id: comment.id,
|
|
180
|
+
metadata: Object.freeze({
|
|
181
|
+
endpoint: this.name,
|
|
182
|
+
repo: comment.repo,
|
|
183
|
+
kind: comment.kind,
|
|
184
|
+
createdAt: comment.createdAt,
|
|
185
|
+
}),
|
|
186
|
+
}).catch((err) => {
|
|
187
|
+
logger.warn(formatCompact({
|
|
188
|
+
op: 'github_gateway_receive_failed',
|
|
189
|
+
target: comment.channelId,
|
|
190
|
+
error: err instanceof Error ? err.message : String(err),
|
|
191
|
+
}));
|
|
192
|
+
});
|
|
181
193
|
}
|
|
182
194
|
}
|
|
195
|
+
|
|
196
|
+
export function defaultCreateClient(config: ResolvedGithubConfig): GhClient {
|
|
197
|
+
const appAuth = config.appId && config.privateKey
|
|
198
|
+
? { appId: config.appId, privateKey: config.privateKey }
|
|
199
|
+
: undefined;
|
|
200
|
+
return new GhClient({ host: config.host, appAuth });
|
|
201
|
+
}
|