@zhin.js/adapter-github 3.0.2 → 3.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +624 -0
- package/README.md +38 -201
- package/adapters/github.ts +51 -0
- package/agent/skills/github.md +2 -2
- package/agent/tools/bind.ts +2 -2
- package/agent/tools/create_pr.ts +2 -2
- package/agent/tools/install.ts +2 -2
- package/agent/tools/patch_file.ts +2 -2
- package/agent/tools/prepare_workspace.ts +2 -2
- package/agent/tools/push_branch.ts +2 -2
- package/agent/tools/star.ts +2 -2
- package/agent/tools/subscribe.ts +2 -2
- package/agent/tools/subscriptions.ts +2 -2
- package/agent/tools/unbind.ts +2 -2
- package/agent/tools/unsubscribe.ts +2 -2
- package/agent/tools/whoami.ts +2 -2
- package/lib/agent-prompt.d.ts +2 -0
- package/lib/{src/agent-prompt.js → agent-prompt.js} +0 -1
- package/lib/endpoint.d.ts +48 -0
- package/lib/endpoint.js +165 -0
- package/lib/gh-client.d.ts +300 -0
- package/lib/{src/gh-client.js → gh-client.js} +1 -2
- 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/{src/github-bot-handlers.js → github-bot-handlers.js} +0 -1
- package/lib/github-channel-context.d.ts +16 -0
- package/lib/{src/github-channel-context.js → github-channel-context.js} +0 -1
- package/lib/github-tool-handlers.d.ts +17 -0
- package/lib/{src/github-tool-handlers.js → github-tool-handlers.js} +14 -8
- package/lib/index.d.ts +9 -0
- package/lib/index.js +9 -0
- 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 +149 -0
- package/lib/{src/types.js → 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/{src/workspace-manager.js → workspace-manager.js} +37 -1
- package/package.json +53 -26
- package/plugin.ts +52 -0
- package/schema.json +65 -0
- package/src/endpoint.ts +166 -175
- package/src/gh-client.ts +1 -1
- package/src/github-agent-deps.ts +66 -11
- package/src/github-tool-handlers.ts +20 -7
- package/src/index.ts +55 -137
- package/src/oauth-users.ts +47 -0
- package/src/protocol.ts +367 -0
- package/src/webhook.ts +125 -0
- package/src/workspace-manager.ts +44 -0
- package/lib/agent/tools/bind.js +0 -12
- package/lib/agent/tools/bind.js.map +0 -1
- package/lib/agent/tools/create_pr.js +0 -19
- package/lib/agent/tools/create_pr.js.map +0 -1
- package/lib/agent/tools/install.js +0 -12
- package/lib/agent/tools/install.js.map +0 -1
- package/lib/agent/tools/patch_file.js +0 -18
- package/lib/agent/tools/patch_file.js.map +0 -1
- package/lib/agent/tools/prepare_workspace.js +0 -14
- package/lib/agent/tools/prepare_workspace.js.map +0 -1
- package/lib/agent/tools/push_branch.js +0 -17
- package/lib/agent/tools/push_branch.js.map +0 -1
- package/lib/agent/tools/star.js +0 -15
- package/lib/agent/tools/star.js.map +0 -1
- package/lib/agent/tools/subscribe.js +0 -16
- package/lib/agent/tools/subscribe.js.map +0 -1
- package/lib/agent/tools/subscriptions.js +0 -13
- package/lib/agent/tools/subscriptions.js.map +0 -1
- package/lib/agent/tools/unbind.js +0 -12
- package/lib/agent/tools/unbind.js.map +0 -1
- package/lib/agent/tools/unsubscribe.js +0 -15
- package/lib/agent/tools/unsubscribe.js.map +0 -1
- package/lib/agent/tools/whoami.js +0 -12
- package/lib/agent/tools/whoami.js.map +0 -1
- package/lib/src/adapter.js +0 -423
- package/lib/src/adapter.js.map +0 -1
- package/lib/src/agent-prompt.js.map +0 -1
- package/lib/src/endpoint.js +0 -189
- package/lib/src/endpoint.js.map +0 -1
- package/lib/src/gh-client.js.map +0 -1
- package/lib/src/github-agent-deps.js +0 -13
- package/lib/src/github-agent-deps.js.map +0 -1
- package/lib/src/github-bot-handlers.js.map +0 -1
- package/lib/src/github-channel-context.js.map +0 -1
- package/lib/src/github-tool-handlers.js.map +0 -1
- package/lib/src/index.js +0 -104
- package/lib/src/index.js.map +0 -1
- package/lib/src/register-github-mcp.js +0 -41
- package/lib/src/register-github-mcp.js.map +0 -1
- package/lib/src/segment-mapper.js +0 -2
- package/lib/src/segment-mapper.js.map +0 -1
- package/lib/src/types.js.map +0 -1
- package/lib/src/workspace-manager.js.map +0 -1
- package/plugin.yml +0 -3
- package/src/adapter.ts +0 -454
- package/src/register-github-mcp.ts +0 -67
- package/src/segment-mapper.ts +0 -1
package/src/index.ts
CHANGED
|
@@ -1,141 +1,59 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
router: import('@zhin.js/host-router').Router;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
interface Models {
|
|
24
|
-
github_subscriptions: {
|
|
25
|
-
id: number;
|
|
26
|
-
repo: string;
|
|
27
|
-
events: import('./types.js').EventType[];
|
|
28
|
-
target_id: string;
|
|
29
|
-
target_type: 'private' | 'group' | 'channel';
|
|
30
|
-
adapter: string;
|
|
31
|
-
endpoint: string;
|
|
32
|
-
};
|
|
33
|
-
github_events: {
|
|
34
|
-
id: number;
|
|
35
|
-
repo: string;
|
|
36
|
-
event_type: string;
|
|
37
|
-
payload: any;
|
|
38
|
-
};
|
|
39
|
-
github_oauth_users: import('./types.js').GitHubOAuthUser;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
buildChannelId,
|
|
3
|
+
enrichInboundContent,
|
|
4
|
+
formatInboundContent,
|
|
5
|
+
formatNotification,
|
|
6
|
+
formatOutboundBody,
|
|
7
|
+
normalizeWebhookPath,
|
|
8
|
+
parseChannelId,
|
|
9
|
+
parseIssueCommentInbound,
|
|
10
|
+
parsePrReviewCommentInbound,
|
|
11
|
+
parsePrReviewInbound,
|
|
12
|
+
resolveGithubConfig,
|
|
13
|
+
shouldAutoReplyRepo,
|
|
14
|
+
verifyWebhookSignature,
|
|
15
|
+
type GithubAdapterConfig,
|
|
16
|
+
type GithubInboundComment,
|
|
17
|
+
type GithubWireSegment,
|
|
18
|
+
type ResolvedGithubConfig,
|
|
19
|
+
} from './protocol.js';
|
|
42
20
|
|
|
43
21
|
export * from './types.js';
|
|
44
|
-
export { GitHubEndpoint, parseMarkdown, toMarkdown, enrichGithubInboundMessage, shouldAutoReplyRepo } from './endpoint.js';
|
|
45
|
-
export { GitHubAdapter } from './adapter.js';
|
|
46
|
-
export { GhClient } from './gh-client.js';
|
|
47
|
-
export { WorkspaceManager } from './workspace-manager.js';
|
|
48
|
-
export { parseMessageChannel, issueBranchName, resolveWorkspaceBranch } from './github-channel-context.js';
|
|
49
|
-
|
|
50
|
-
const plugin = usePlugin();
|
|
51
|
-
const { provide, defineModel, useContext, logger } = plugin;
|
|
52
|
-
|
|
53
|
-
registerGithubMcp(plugin);
|
|
54
|
-
|
|
55
|
-
defineModel('github_subscriptions', {
|
|
56
|
-
id: { type: 'integer', primary: true },
|
|
57
|
-
repo: { type: 'text', nullable: false },
|
|
58
|
-
events: { type: 'json', default: [] },
|
|
59
|
-
target_id: { type: 'text', nullable: false },
|
|
60
|
-
target_type: { type: 'text', nullable: false },
|
|
61
|
-
adapter: { type: 'text', nullable: false },
|
|
62
|
-
endpoint: { type: 'text', nullable: false },
|
|
63
|
-
});
|
|
64
22
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
23
|
+
export {
|
|
24
|
+
getAdapter,
|
|
25
|
+
getGithubAgentDeps,
|
|
26
|
+
registerGithubAgentEndpoint,
|
|
27
|
+
setGithubAgentDeps,
|
|
28
|
+
type GithubAgentDeps,
|
|
29
|
+
type GithubAgentEndpoint,
|
|
30
|
+
} from './github-agent-deps.js';
|
|
71
31
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (router) {
|
|
101
|
-
adapter.setupWebhook(router);
|
|
102
|
-
logger.debug(formatCompact({ op: 'events', source: 'webhook' }));
|
|
103
|
-
} else {
|
|
104
|
-
plugin.useContext('router', (r) => {
|
|
105
|
-
adapter.setupWebhook(r);
|
|
106
|
-
logger.debug(formatCompact({ op: 'events', source: 'webhook', deferred: true }));
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
if (!adapter.webhookActive) {
|
|
111
|
-
adapter.startPolling();
|
|
112
|
-
logger.debug(formatCompact({ op: 'events', source: 'poll' }));
|
|
113
|
-
}
|
|
114
|
-
return () => adapter.stopPolling();
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
useContext('tool', 'github', (_toolService, adapter: GitHubAdapter) => {
|
|
118
|
-
// Endpoint 由 App 从 zhin.config endpoints 注入,可能晚于本回调;懒解析 GhClient。
|
|
119
|
-
let workspaceManager: WorkspaceManager | null = null;
|
|
120
|
-
setGithubAgentDeps({
|
|
121
|
-
getAdapter: () => adapter,
|
|
122
|
-
getWorkspaceManager: () => {
|
|
123
|
-
if (workspaceManager) return workspaceManager;
|
|
124
|
-
const gh = adapter.getAPI();
|
|
125
|
-
if (!gh) {
|
|
126
|
-
throw new Error('GitHub Endpoint 尚未就绪(检查 endpoints 中 github 配置是否已连接)');
|
|
127
|
-
}
|
|
128
|
-
const endpoint = adapter.endpoints.values().next().value;
|
|
129
|
-
const cfg = endpoint?.$config as GitHubEndpointConfig | undefined;
|
|
130
|
-
const workspaceRoot = cfg?.workspace_root
|
|
131
|
-
?? path.join(process.cwd(), 'data', 'github-workspaces');
|
|
132
|
-
workspaceManager = new WorkspaceManager(gh, workspaceRoot);
|
|
133
|
-
return workspaceManager;
|
|
134
|
-
},
|
|
135
|
-
plugin,
|
|
136
|
-
});
|
|
137
|
-
logger.debug('GitHub agent deps initialized (lazy workspace)');
|
|
138
|
-
return () => {};
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
logger.debug('GitHub 适配器已加载 (gh CLI 认证)');
|
|
32
|
+
export { GhClient } from './gh-client.js';
|
|
33
|
+
export { WorkspaceManager } from './workspace-manager.js';
|
|
34
|
+
export {
|
|
35
|
+
parseMessageChannel,
|
|
36
|
+
issueBranchName,
|
|
37
|
+
resolveWorkspaceBranch,
|
|
38
|
+
formatChannelContext,
|
|
39
|
+
} from './github-channel-context.js';
|
|
40
|
+
|
|
41
|
+
export {
|
|
42
|
+
GithubEndpoint,
|
|
43
|
+
defaultCreateClient,
|
|
44
|
+
type GithubEndpointOptions,
|
|
45
|
+
} from './endpoint.js';
|
|
46
|
+
|
|
47
|
+
export {
|
|
48
|
+
GITHUB_OAUTH_USERS_TABLE,
|
|
49
|
+
GITHUB_OAUTH_USERS_SCHEMA,
|
|
50
|
+
defineGithubOauthUsersTable,
|
|
51
|
+
lookupGithubOauthAccessToken,
|
|
52
|
+
} from './oauth-users.js';
|
|
53
|
+
|
|
54
|
+
export {
|
|
55
|
+
registerGithubWebhookRoutes,
|
|
56
|
+
handleGithubWebhookRequest,
|
|
57
|
+
dispatchGithubWebhookPayload,
|
|
58
|
+
type GithubWebhookHandler,
|
|
59
|
+
} from './webhook.js';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* github_oauth_users — SSOT for Runtime user-token binding (gh bind / Endpoint lookup).
|
|
3
|
+
*/
|
|
4
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
5
|
+
import type { DatabaseHost } from '@zhin.js/plugin-runtime';
|
|
6
|
+
|
|
7
|
+
const logger = getLogger('github');
|
|
8
|
+
|
|
9
|
+
export const GITHUB_OAUTH_USERS_TABLE = 'github_oauth_users';
|
|
10
|
+
|
|
11
|
+
export const GITHUB_OAUTH_USERS_SCHEMA = {
|
|
12
|
+
id: { type: 'integer', primary: true },
|
|
13
|
+
platform: { type: 'text', nullable: false },
|
|
14
|
+
platform_uid: { type: 'text', nullable: false },
|
|
15
|
+
github_login: { type: 'text', nullable: false },
|
|
16
|
+
access_token: { type: 'text', nullable: false },
|
|
17
|
+
created_at: { type: 'integer', nullable: false },
|
|
18
|
+
} as const;
|
|
19
|
+
|
|
20
|
+
export function defineGithubOauthUsersTable(
|
|
21
|
+
host: { define(name: string, definition: Record<string, unknown>): void },
|
|
22
|
+
): void {
|
|
23
|
+
host.define(GITHUB_OAUTH_USERS_TABLE, { ...GITHUB_OAUTH_USERS_SCHEMA });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Resolve a stored PAT/device-flow token; null when DB missing or no row. */
|
|
27
|
+
export async function lookupGithubOauthAccessToken(
|
|
28
|
+
database: DatabaseHost | undefined,
|
|
29
|
+
platform: string,
|
|
30
|
+
platformUid: string,
|
|
31
|
+
): Promise<string | null> {
|
|
32
|
+
if (!database?.started) return null;
|
|
33
|
+
const model = database.models.get(GITHUB_OAUTH_USERS_TABLE);
|
|
34
|
+
if (!model) return null;
|
|
35
|
+
try {
|
|
36
|
+
const rows = await model.select().where({ platform, platform_uid: platformUid });
|
|
37
|
+
const token = rows[0]?.access_token;
|
|
38
|
+
return typeof token === 'string' && token.trim() ? token : null;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
logger.debug(formatCompact({
|
|
41
|
+
op: 'oauth_lookup_fail',
|
|
42
|
+
platform,
|
|
43
|
+
error: error instanceof Error ? error.message : String(error),
|
|
44
|
+
}));
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub webhook / comment helpers — no legacy Adapter/Endpoint / segment-mapper.
|
|
3
|
+
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
7
|
+
import type { IncomingMessage } from 'node:http';
|
|
8
|
+
import { pickCredential } from '@zhin.js/adapter';
|
|
9
|
+
import {
|
|
10
|
+
buildChannelId,
|
|
11
|
+
type GenericWebhookPayload,
|
|
12
|
+
type IssueCommentPayload,
|
|
13
|
+
type PRReviewCommentPayload,
|
|
14
|
+
type PRReviewPayload,
|
|
15
|
+
} from './types.js';
|
|
16
|
+
|
|
17
|
+
export type {
|
|
18
|
+
EventType,
|
|
19
|
+
GenericWebhookPayload,
|
|
20
|
+
GitHubComment,
|
|
21
|
+
GitHubEndpointConfig,
|
|
22
|
+
GitHubIssue,
|
|
23
|
+
GitHubOAuthUser,
|
|
24
|
+
GitHubPR,
|
|
25
|
+
GitHubRepo,
|
|
26
|
+
GitHubUser,
|
|
27
|
+
IssueCommentPayload,
|
|
28
|
+
ParsedChannel,
|
|
29
|
+
PRReviewCommentPayload,
|
|
30
|
+
PRReviewPayload,
|
|
31
|
+
Subscription,
|
|
32
|
+
} from './types.js';
|
|
33
|
+
|
|
34
|
+
export { buildChannelId, parseChannelId } from './types.js';
|
|
35
|
+
|
|
36
|
+
/** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
|
|
37
|
+
export interface GithubAdapterConfig {
|
|
38
|
+
readonly name?: string;
|
|
39
|
+
readonly host?: string;
|
|
40
|
+
readonly app_id?: string | number;
|
|
41
|
+
readonly appId?: string | number;
|
|
42
|
+
readonly private_key?: string;
|
|
43
|
+
readonly privateKey?: string;
|
|
44
|
+
readonly webhook_secret?: string;
|
|
45
|
+
readonly webhookSecret?: string;
|
|
46
|
+
readonly webhook_path?: string;
|
|
47
|
+
readonly webhookPath?: string;
|
|
48
|
+
readonly poll_interval?: number;
|
|
49
|
+
readonly pollInterval?: number;
|
|
50
|
+
readonly auto_reply_repos?: readonly string[];
|
|
51
|
+
readonly autoReplyRepos?: readonly string[];
|
|
52
|
+
readonly bot_login?: string;
|
|
53
|
+
readonly botLogin?: string;
|
|
54
|
+
readonly workspace_root?: string;
|
|
55
|
+
readonly workspaceRoot?: string;
|
|
56
|
+
/** Transitional: legacy root `endpoints[]` with `context: github`. */
|
|
57
|
+
readonly endpoints?: ReadonlyArray<Partial<ResolvedGithubConfig> & {
|
|
58
|
+
readonly context?: string;
|
|
59
|
+
readonly app_id?: string | number;
|
|
60
|
+
readonly private_key?: string;
|
|
61
|
+
readonly webhook_secret?: string;
|
|
62
|
+
readonly webhook_path?: string;
|
|
63
|
+
readonly poll_interval?: number;
|
|
64
|
+
readonly auto_reply_repos?: readonly string[];
|
|
65
|
+
readonly bot_login?: string;
|
|
66
|
+
readonly workspace_root?: string;
|
|
67
|
+
}>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ResolvedGithubConfig {
|
|
71
|
+
readonly context: 'github';
|
|
72
|
+
readonly name: string;
|
|
73
|
+
readonly host?: string;
|
|
74
|
+
readonly appId?: string | number;
|
|
75
|
+
readonly privateKey?: string;
|
|
76
|
+
readonly webhookSecret?: string;
|
|
77
|
+
readonly webhookPath: string;
|
|
78
|
+
readonly pollInterval: number;
|
|
79
|
+
readonly autoReplyRepos: readonly string[];
|
|
80
|
+
readonly botLogin?: string;
|
|
81
|
+
readonly workspaceRoot?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface GithubWireSegment {
|
|
85
|
+
readonly type: string;
|
|
86
|
+
readonly data?: Record<string, unknown>;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface GithubInboundComment {
|
|
90
|
+
readonly id: string;
|
|
91
|
+
readonly channelId: string;
|
|
92
|
+
readonly sender: string;
|
|
93
|
+
readonly content: string;
|
|
94
|
+
readonly repo: string;
|
|
95
|
+
readonly kind: 'issue_comment' | 'pr_review_comment' | 'pr_review';
|
|
96
|
+
readonly createdAt: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function resolveGithubConfig(config: GithubAdapterConfig = {}): ResolvedGithubConfig {
|
|
100
|
+
const entry = config.endpoints?.find((item) => item.context === 'github');
|
|
101
|
+
const appIdRaw = pickCredential(
|
|
102
|
+
config.appId != null ? String(config.appId) : undefined,
|
|
103
|
+
config.app_id != null ? String(config.app_id) : undefined,
|
|
104
|
+
entry?.appId != null ? String(entry.appId) : undefined,
|
|
105
|
+
entry?.app_id != null ? String(entry.app_id) : undefined,
|
|
106
|
+
process.env.GITHUB_APP_ID,
|
|
107
|
+
);
|
|
108
|
+
const appId = appIdRaw ? (Number(appIdRaw) || appIdRaw) : undefined;
|
|
109
|
+
const privateKey = pickCredential(
|
|
110
|
+
config.privateKey,
|
|
111
|
+
config.private_key,
|
|
112
|
+
entry?.privateKey,
|
|
113
|
+
entry?.private_key,
|
|
114
|
+
) || undefined;
|
|
115
|
+
if (!appId || !privateKey) {
|
|
116
|
+
throw new TypeError(
|
|
117
|
+
'GitHub adapter requires app_id + private_key (plugins.<key> or GITHUB_APP_ID)',
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
const name = (typeof config.name === 'string' && config.name)
|
|
121
|
+
|| (typeof entry?.name === 'string' && entry.name)
|
|
122
|
+
|| process.env.GITHUB_BOT_NAME
|
|
123
|
+
|| 'github-bot';
|
|
124
|
+
const webhookSecret = config.webhookSecret ?? config.webhook_secret
|
|
125
|
+
?? entry?.webhookSecret ?? entry?.webhook_secret
|
|
126
|
+
?? process.env.GITHUB_WEBHOOK_SECRET
|
|
127
|
+
?? undefined;
|
|
128
|
+
const host = config.host ?? entry?.host;
|
|
129
|
+
const webhookPath = normalizeWebhookPath(
|
|
130
|
+
config.webhookPath ?? config.webhook_path ?? entry?.webhookPath ?? entry?.webhook_path
|
|
131
|
+
?? '/github/webhook',
|
|
132
|
+
);
|
|
133
|
+
const pollInterval = Number(
|
|
134
|
+
config.pollInterval ?? config.poll_interval ?? entry?.pollInterval ?? entry?.poll_interval ?? 60,
|
|
135
|
+
) || 60;
|
|
136
|
+
const autoReplyRepos = [
|
|
137
|
+
...(config.autoReplyRepos ?? config.auto_reply_repos
|
|
138
|
+
?? entry?.autoReplyRepos ?? entry?.auto_reply_repos ?? []),
|
|
139
|
+
];
|
|
140
|
+
const botLogin = config.botLogin ?? config.bot_login ?? entry?.botLogin ?? entry?.bot_login;
|
|
141
|
+
const workspaceRoot = config.workspaceRoot ?? config.workspace_root
|
|
142
|
+
?? entry?.workspaceRoot ?? entry?.workspace_root;
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
context: 'github',
|
|
146
|
+
name,
|
|
147
|
+
...(host ? { host } : {}),
|
|
148
|
+
...(appId != null ? { appId } : {}),
|
|
149
|
+
...(privateKey ? { privateKey } : {}),
|
|
150
|
+
...(webhookSecret ? { webhookSecret } : {}),
|
|
151
|
+
webhookPath,
|
|
152
|
+
pollInterval,
|
|
153
|
+
autoReplyRepos,
|
|
154
|
+
...(botLogin ? { botLogin } : {}),
|
|
155
|
+
...(workspaceRoot ? { workspaceRoot } : {}),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function normalizeWebhookPath(path: string): string {
|
|
160
|
+
const trimmed = path.trim() || '/github/webhook';
|
|
161
|
+
return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function shouldAutoReplyRepo(
|
|
165
|
+
config: Pick<ResolvedGithubConfig, 'autoReplyRepos'>,
|
|
166
|
+
repo: string,
|
|
167
|
+
): boolean {
|
|
168
|
+
const list = config.autoReplyRepos;
|
|
169
|
+
if (!list.length) return false;
|
|
170
|
+
const key = repo.toLowerCase();
|
|
171
|
+
return list.some((r) => r.toLowerCase() === key);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Prepend synthetic @bot for auto_reply repos (gateway text path). */
|
|
175
|
+
export function enrichInboundContent(
|
|
176
|
+
content: string,
|
|
177
|
+
config: Pick<ResolvedGithubConfig, 'autoReplyRepos' | 'botLogin'>,
|
|
178
|
+
botLogin: string | undefined,
|
|
179
|
+
repo: string,
|
|
180
|
+
): string {
|
|
181
|
+
const login = config.botLogin || botLogin;
|
|
182
|
+
if (!login || !shouldAutoReplyRepo(config, repo)) return content;
|
|
183
|
+
if (content.includes(`@${login}`)) return content;
|
|
184
|
+
return `@${login} ${content}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Build inbound text for MessageGateway.receive from markdown/comment body. */
|
|
188
|
+
export function formatInboundContent(body: string): string {
|
|
189
|
+
return body;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Build markdown body for Issue/PR comment send. */
|
|
193
|
+
export function formatOutboundBody(payload: unknown): string {
|
|
194
|
+
if (typeof payload === 'string') return payload;
|
|
195
|
+
if (payload == null) return '';
|
|
196
|
+
if (!Array.isArray(payload)) {
|
|
197
|
+
if (typeof payload === 'object' && payload !== null && 'text' in payload) {
|
|
198
|
+
return String((payload as { text?: unknown }).text ?? '');
|
|
199
|
+
}
|
|
200
|
+
return String(payload);
|
|
201
|
+
}
|
|
202
|
+
return payload.map((seg) => {
|
|
203
|
+
if (typeof seg === 'string') return seg;
|
|
204
|
+
const item = seg as GithubWireSegment;
|
|
205
|
+
switch (item.type) {
|
|
206
|
+
case 'text':
|
|
207
|
+
return String(item.data?.text ?? '');
|
|
208
|
+
case 'mention':
|
|
209
|
+
case 'at':
|
|
210
|
+
return `@${item.data?.name || item.data?.id || item.data?.target || ''}`;
|
|
211
|
+
case 'image':
|
|
212
|
+
return item.data?.url ? `` : '[image]';
|
|
213
|
+
case 'link':
|
|
214
|
+
return `[${item.data?.text || item.data?.url || ''}](${item.data?.url || ''})`;
|
|
215
|
+
default:
|
|
216
|
+
return String(item.data?.text ?? `[${item.type}]`);
|
|
217
|
+
}
|
|
218
|
+
}).join('');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function verifyWebhookSignature(
|
|
222
|
+
secret: string,
|
|
223
|
+
rawBody: string,
|
|
224
|
+
signatureHeader: string | undefined,
|
|
225
|
+
): boolean {
|
|
226
|
+
if (!signatureHeader?.startsWith('sha256=')) return false;
|
|
227
|
+
const expected = `sha256=${createHmac('sha256', secret).update(rawBody).digest('hex')}`;
|
|
228
|
+
try {
|
|
229
|
+
const a = Buffer.from(signatureHeader);
|
|
230
|
+
const b = Buffer.from(expected);
|
|
231
|
+
if (a.length !== b.length) return false;
|
|
232
|
+
return timingSafeEqual(a, b);
|
|
233
|
+
} catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function headerValue(
|
|
239
|
+
headers: IncomingMessage['headers'],
|
|
240
|
+
name: string,
|
|
241
|
+
): string {
|
|
242
|
+
const raw = headers[name] ?? headers[name.toLowerCase()];
|
|
243
|
+
if (Array.isArray(raw)) return raw[0] ?? '';
|
|
244
|
+
return raw ?? '';
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function readTextBody(
|
|
248
|
+
request: IncomingMessage,
|
|
249
|
+
options: { readonly limit?: number } = {},
|
|
250
|
+
): Promise<string> {
|
|
251
|
+
const limit = options.limit ?? 1_048_576;
|
|
252
|
+
const chunks: Buffer[] = [];
|
|
253
|
+
let size = 0;
|
|
254
|
+
for await (const chunk of request) {
|
|
255
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
256
|
+
size += buffer.length;
|
|
257
|
+
if (size > limit) {
|
|
258
|
+
request.destroy();
|
|
259
|
+
throw new Error(`Request body exceeds ${limit} bytes`);
|
|
260
|
+
}
|
|
261
|
+
chunks.push(buffer);
|
|
262
|
+
}
|
|
263
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function parseIssueCommentInbound(payload: IssueCommentPayload): GithubInboundComment | null {
|
|
267
|
+
if (payload.action !== 'created' || !payload.comment) return null;
|
|
268
|
+
const repo = payload.repository.full_name;
|
|
269
|
+
const isPR = 'pull_request' in (payload.issue as object);
|
|
270
|
+
return {
|
|
271
|
+
id: String(payload.comment.id),
|
|
272
|
+
channelId: buildChannelId(repo, isPR ? 'pr' : 'issue', payload.issue.number),
|
|
273
|
+
sender: payload.sender.login,
|
|
274
|
+
content: payload.comment.body,
|
|
275
|
+
repo,
|
|
276
|
+
kind: 'issue_comment',
|
|
277
|
+
createdAt: new Date(payload.comment.created_at).getTime(),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function parsePrReviewCommentInbound(payload: PRReviewCommentPayload): GithubInboundComment | null {
|
|
282
|
+
if (payload.action !== 'created' || !payload.comment) return null;
|
|
283
|
+
const repo = payload.repository.full_name;
|
|
284
|
+
const body = payload.comment.path
|
|
285
|
+
? `**${payload.comment.path}**\n${payload.comment.diff_hunk ? `\`\`\`diff\n${payload.comment.diff_hunk}\n\`\`\`\n` : ''}${payload.comment.body}`
|
|
286
|
+
: payload.comment.body;
|
|
287
|
+
return {
|
|
288
|
+
id: String(payload.comment.id),
|
|
289
|
+
channelId: buildChannelId(repo, 'pr', payload.pull_request.number),
|
|
290
|
+
sender: payload.sender.login,
|
|
291
|
+
content: body,
|
|
292
|
+
repo,
|
|
293
|
+
kind: 'pr_review_comment',
|
|
294
|
+
createdAt: new Date(payload.comment.created_at).getTime(),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function parsePrReviewInbound(payload: PRReviewPayload): GithubInboundComment | null {
|
|
299
|
+
if (payload.action !== 'submitted' || !payload.review.body) return null;
|
|
300
|
+
const repo = payload.repository.full_name;
|
|
301
|
+
const stateLabel: Record<string, string> = {
|
|
302
|
+
approved: '✅ APPROVED',
|
|
303
|
+
changes_requested: '🔄 CHANGES REQUESTED',
|
|
304
|
+
commented: '💬 COMMENTED',
|
|
305
|
+
dismissed: '❌ DISMISSED',
|
|
306
|
+
};
|
|
307
|
+
const body = `**[${stateLabel[payload.review.state] || payload.review.state}]**\n${payload.review.body}`;
|
|
308
|
+
return {
|
|
309
|
+
id: String(payload.review.id),
|
|
310
|
+
channelId: buildChannelId(repo, 'pr', payload.pull_request.number),
|
|
311
|
+
sender: payload.sender.login,
|
|
312
|
+
content: body,
|
|
313
|
+
repo,
|
|
314
|
+
kind: 'pr_review',
|
|
315
|
+
createdAt: new Date(payload.review.submitted_at).getTime(),
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function formatNotification(event: string, p: GenericWebhookPayload): string {
|
|
320
|
+
const repo = p.repository.full_name;
|
|
321
|
+
const sender = p.sender.login;
|
|
322
|
+
const repoUrl = p.repository.html_url;
|
|
323
|
+
switch (event) {
|
|
324
|
+
case 'push': {
|
|
325
|
+
const branch = p.ref?.replace('refs/heads/', '') || '?';
|
|
326
|
+
const commits = p.commits || [];
|
|
327
|
+
const compareUrl = commits.length >= 2
|
|
328
|
+
? `${repoUrl}/compare/${commits[0].id.substring(0, 12)}...${commits[commits.length - 1].id.substring(0, 12)}`
|
|
329
|
+
: commits.length === 1 ? `${repoUrl}/commit/${commits[0].id}` : '';
|
|
330
|
+
let msg = `📦 ${repo}\n🌿 ${sender} pushed ${commits.length} commit(s) to \`${branch}\`\n`;
|
|
331
|
+
if (commits.length) {
|
|
332
|
+
msg += '\n';
|
|
333
|
+
msg += commits.slice(0, 5).map((c) =>
|
|
334
|
+
` • [\`${c.id.substring(0, 7)}\`](${repoUrl}/commit/${c.id}) ${c.message.split('\n')[0]}`,
|
|
335
|
+
).join('\n');
|
|
336
|
+
if (commits.length > 5) msg += `\n ... +${commits.length - 5} more`;
|
|
337
|
+
}
|
|
338
|
+
if (compareUrl) msg += `\n\n🔗 ${compareUrl}`;
|
|
339
|
+
return msg;
|
|
340
|
+
}
|
|
341
|
+
case 'issues': {
|
|
342
|
+
const i = p.issue!;
|
|
343
|
+
const act = p.action === 'opened' ? '📝 opened' : p.action === 'closed' ? '✅ closed' : `🔄 ${p.action || 'updated'}`;
|
|
344
|
+
return `🐛 ${repo}\n👤 ${sender} ${act} issue #${i.number}\n📌 ${i.title}\n🔗 ${i.html_url}`;
|
|
345
|
+
}
|
|
346
|
+
case 'star': {
|
|
347
|
+
const starred = p.action !== 'deleted';
|
|
348
|
+
return `${starred ? '⭐' : '💔'} ${repo}\n👤 ${sender} ${starred ? 'starred' : 'unstarred'}\n🔗 ${repoUrl}`;
|
|
349
|
+
}
|
|
350
|
+
case 'fork':
|
|
351
|
+
return `🍴 ${repo}\n👤 ${sender} forked → ${p.forkee!.full_name}\n🔗 ${p.forkee!.html_url}`;
|
|
352
|
+
case 'pull_request': {
|
|
353
|
+
const pr = p.pull_request!;
|
|
354
|
+
const act = p.action === 'opened' ? '📝 opened'
|
|
355
|
+
: p.action === 'closed' ? (pr.state === 'closed' ? '❌ closed' : '✅ merged')
|
|
356
|
+
: `🔄 ${p.action || 'updated'}`;
|
|
357
|
+
return `🔀 ${repo}\n👤 ${sender} ${act} PR #${pr.number}\n📌 ${pr.title}\n🌿 ${pr.head.ref} → ${pr.base.ref}\n🔗 ${pr.html_url}`;
|
|
358
|
+
}
|
|
359
|
+
default:
|
|
360
|
+
return `📬 ${repo}\n📡 ${event}${p.action ? ` (${p.action})` : ''} by ${sender}\n🔗 ${repoUrl}`;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Parse @mentions in markdown into a flat display string (gateway text path). */
|
|
365
|
+
export function parseMarkdownMentions(md: string): string {
|
|
366
|
+
return md;
|
|
367
|
+
}
|