@zhin.js/adapter-github 3.0.2 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/CHANGELOG.md +646 -0
  2. package/README.md +55 -203
  3. package/adapters/github.ts +51 -0
  4. package/agent/skills/github.md +2 -2
  5. package/agent/tools/bind.ts +2 -2
  6. package/agent/tools/create_pr.ts +2 -2
  7. package/agent/tools/install.ts +2 -2
  8. package/agent/tools/patch_file.ts +2 -2
  9. package/agent/tools/prepare_workspace.ts +2 -2
  10. package/agent/tools/push_branch.ts +2 -2
  11. package/agent/tools/star.ts +2 -2
  12. package/agent/tools/subscribe.ts +2 -2
  13. package/agent/tools/subscriptions.ts +2 -2
  14. package/agent/tools/unbind.ts +2 -2
  15. package/agent/tools/unsubscribe.ts +2 -2
  16. package/agent/tools/whoami.ts +2 -2
  17. package/lib/agent-prompt.d.ts +2 -0
  18. package/lib/{src/agent-prompt.js → agent-prompt.js} +0 -1
  19. package/lib/endpoint.d.ts +48 -0
  20. package/lib/endpoint.js +165 -0
  21. package/lib/gh-client.d.ts +300 -0
  22. package/lib/{src/gh-client.js → gh-client.js} +1 -2
  23. package/lib/github-agent-deps.d.ts +47 -0
  24. package/lib/github-agent-deps.js +43 -0
  25. package/lib/github-bot-handlers.d.ts +26 -0
  26. package/lib/{src/github-bot-handlers.js → github-bot-handlers.js} +0 -1
  27. package/lib/github-channel-context.d.ts +16 -0
  28. package/lib/{src/github-channel-context.js → github-channel-context.js} +0 -1
  29. package/lib/github-tool-handlers.d.ts +17 -0
  30. package/lib/{src/github-tool-handlers.js → github-tool-handlers.js} +14 -8
  31. package/lib/index.d.ts +9 -0
  32. package/lib/index.js +9 -0
  33. package/lib/oauth-users.d.ts +33 -0
  34. package/lib/oauth-users.js +38 -0
  35. package/lib/protocol.d.ts +87 -0
  36. package/lib/protocol.js +241 -0
  37. package/lib/types.d.ts +149 -0
  38. package/lib/{src/types.js → types.js} +0 -1
  39. package/lib/webhook.d.ts +13 -0
  40. package/lib/webhook.js +87 -0
  41. package/lib/workspace-manager.d.ts +21 -0
  42. package/lib/{src/workspace-manager.js → workspace-manager.js} +46 -1
  43. package/package.json +53 -26
  44. package/plugin.ts +52 -0
  45. package/schema.json +94 -0
  46. package/src/endpoint.ts +166 -175
  47. package/src/gh-client.ts +1 -1
  48. package/src/github-agent-deps.ts +66 -11
  49. package/src/github-tool-handlers.ts +20 -7
  50. package/src/index.ts +55 -137
  51. package/src/oauth-users.ts +47 -0
  52. package/src/protocol.ts +367 -0
  53. package/src/webhook.ts +125 -0
  54. package/src/workspace-manager.ts +54 -0
  55. package/lib/agent/tools/bind.js +0 -12
  56. package/lib/agent/tools/bind.js.map +0 -1
  57. package/lib/agent/tools/create_pr.js +0 -19
  58. package/lib/agent/tools/create_pr.js.map +0 -1
  59. package/lib/agent/tools/install.js +0 -12
  60. package/lib/agent/tools/install.js.map +0 -1
  61. package/lib/agent/tools/patch_file.js +0 -18
  62. package/lib/agent/tools/patch_file.js.map +0 -1
  63. package/lib/agent/tools/prepare_workspace.js +0 -14
  64. package/lib/agent/tools/prepare_workspace.js.map +0 -1
  65. package/lib/agent/tools/push_branch.js +0 -17
  66. package/lib/agent/tools/push_branch.js.map +0 -1
  67. package/lib/agent/tools/star.js +0 -15
  68. package/lib/agent/tools/star.js.map +0 -1
  69. package/lib/agent/tools/subscribe.js +0 -16
  70. package/lib/agent/tools/subscribe.js.map +0 -1
  71. package/lib/agent/tools/subscriptions.js +0 -13
  72. package/lib/agent/tools/subscriptions.js.map +0 -1
  73. package/lib/agent/tools/unbind.js +0 -12
  74. package/lib/agent/tools/unbind.js.map +0 -1
  75. package/lib/agent/tools/unsubscribe.js +0 -15
  76. package/lib/agent/tools/unsubscribe.js.map +0 -1
  77. package/lib/agent/tools/whoami.js +0 -12
  78. package/lib/agent/tools/whoami.js.map +0 -1
  79. package/lib/src/adapter.js +0 -423
  80. package/lib/src/adapter.js.map +0 -1
  81. package/lib/src/agent-prompt.js.map +0 -1
  82. package/lib/src/endpoint.js +0 -189
  83. package/lib/src/endpoint.js.map +0 -1
  84. package/lib/src/gh-client.js.map +0 -1
  85. package/lib/src/github-agent-deps.js +0 -13
  86. package/lib/src/github-agent-deps.js.map +0 -1
  87. package/lib/src/github-bot-handlers.js.map +0 -1
  88. package/lib/src/github-channel-context.js.map +0 -1
  89. package/lib/src/github-tool-handlers.js.map +0 -1
  90. package/lib/src/index.js +0 -104
  91. package/lib/src/index.js.map +0 -1
  92. package/lib/src/register-github-mcp.js +0 -41
  93. package/lib/src/register-github-mcp.js.map +0 -1
  94. package/lib/src/segment-mapper.js +0 -2
  95. package/lib/src/segment-mapper.js.map +0 -1
  96. package/lib/src/types.js.map +0 -1
  97. package/lib/src/workspace-manager.js.map +0 -1
  98. package/plugin.yml +0 -3
  99. package/src/adapter.ts +0 -454
  100. package/src/register-github-mcp.ts +0 -67
  101. package/src/segment-mapper.ts +0 -1
@@ -1,15 +1,22 @@
1
- import { formatCompact } from 'zhin.js';
1
+ import { formatCompact } from '@zhin.js/logger';
2
2
  import { getCurrentCommMessage } from '@zhin.js/agent/security';
3
3
  import { GhClient } from './gh-client.js';
4
4
  import { getAdapter, getGithubAgentDeps } from './github-agent-deps.js';
5
5
  function oauthModel() {
6
- const db = getGithubAgentDeps().plugin.root?.inject('database');
6
+ const db = getGithubAgentDeps().getDatabase?.();
7
7
  return db?.models?.get('github_oauth_users');
8
8
  }
9
9
  function subscriptionsModel() {
10
- const db = getGithubAgentDeps().plugin.root?.inject('database');
10
+ const db = getGithubAgentDeps().getDatabase?.();
11
11
  return db?.models?.get('github_subscriptions');
12
12
  }
13
+ function depsLogger() {
14
+ return getGithubAgentDeps().logger ?? {
15
+ debug: (...args) => console.debug(...args),
16
+ warn: (...args) => console.warn(...args),
17
+ error: (...args) => console.error(...args),
18
+ };
19
+ }
13
20
  export async function executeGithubStar(args, commMessage) {
14
21
  const adapter = getAdapter();
15
22
  const msg = commMessage ?? getCurrentCommMessage();
@@ -35,7 +42,7 @@ export async function executeGithubStar(args, commMessage) {
35
42
  }
36
43
  export async function executeGithubBind(_args, commMessage) {
37
44
  const adapter = getAdapter();
38
- const { plugin } = getGithubAgentDeps();
45
+ const log = depsLogger();
39
46
  const msg = commMessage ?? getCurrentCommMessage();
40
47
  if (!msg?.$adapter || !msg?.$sender?.id)
41
48
  return '❌ 无法获取当前用户信息';
@@ -63,7 +70,7 @@ export async function executeGithubBind(_args, commMessage) {
63
70
  ].join('\n');
64
71
  tokenPromise.then(async (tokenData) => {
65
72
  if (!tokenData) {
66
- plugin.logger.warn(formatCompact({ op: 'device_flow', ok: false, platform: msg.$adapter, sender: msg.$sender.id }));
73
+ log.warn(formatCompact({ op: 'device_flow', ok: false, platform: msg.$adapter, sender: msg.$sender.id }));
67
74
  return;
68
75
  }
69
76
  const userGh = new GhClient({ host, token: tokenData.access_token });
@@ -77,12 +84,12 @@ export async function executeGithubBind(_args, commMessage) {
77
84
  access_token: tokenData.access_token,
78
85
  created_at: Date.now(),
79
86
  });
80
- plugin.logger.debug(formatCompact({ op: 'bind', platform: msg.$adapter, sender: msg.$sender.id, login }));
87
+ log.debug(formatCompact({ op: 'bind', platform: msg.$adapter, sender: msg.$sender.id, login }));
81
88
  if (msg?.$reply) {
82
89
  await msg.$reply(`✅ GitHub 账号绑定成功!\n👤 ${login}`);
83
90
  }
84
91
  }).catch((err) => {
85
- plugin.logger.error('GitHub Device Flow 错误:', err);
92
+ log.error('GitHub Device Flow 错误:', err);
86
93
  });
87
94
  return replyMsg;
88
95
  }
@@ -212,4 +219,3 @@ export async function executeGithubSubscriptions(_args, commMessage) {
212
219
  return ` 📦 ${s.repo}\n 📡 ${events.join(', ') || '(无事件)'}`;
213
220
  }).join('\n\n');
214
221
  }
215
- //# sourceMappingURL=github-tool-handlers.js.map
package/lib/index.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ export { buildChannelId, enrichInboundContent, formatInboundContent, formatNotification, formatOutboundBody, normalizeWebhookPath, parseChannelId, parseIssueCommentInbound, parsePrReviewCommentInbound, parsePrReviewInbound, resolveGithubConfig, shouldAutoReplyRepo, verifyWebhookSignature, type GithubAdapterConfig, type GithubInboundComment, type GithubWireSegment, type ResolvedGithubConfig, } from './protocol.js';
2
+ export * from './types.js';
3
+ export { getAdapter, getGithubAgentDeps, registerGithubAgentEndpoint, setGithubAgentDeps, type GithubAgentDeps, type GithubAgentEndpoint, } from './github-agent-deps.js';
4
+ export { GhClient } from './gh-client.js';
5
+ export { WorkspaceManager } from './workspace-manager.js';
6
+ export { parseMessageChannel, issueBranchName, resolveWorkspaceBranch, formatChannelContext, } from './github-channel-context.js';
7
+ export { GithubEndpoint, defaultCreateClient, type GithubEndpointOptions, } from './endpoint.js';
8
+ export { GITHUB_OAUTH_USERS_TABLE, GITHUB_OAUTH_USERS_SCHEMA, defineGithubOauthUsersTable, lookupGithubOauthAccessToken, } from './oauth-users.js';
9
+ export { registerGithubWebhookRoutes, handleGithubWebhookRequest, dispatchGithubWebhookPayload, type GithubWebhookHandler, } from './webhook.js';
package/lib/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export { buildChannelId, enrichInboundContent, formatInboundContent, formatNotification, formatOutboundBody, normalizeWebhookPath, parseChannelId, parseIssueCommentInbound, parsePrReviewCommentInbound, parsePrReviewInbound, resolveGithubConfig, shouldAutoReplyRepo, verifyWebhookSignature, } from './protocol.js';
2
+ export * from './types.js';
3
+ export { getAdapter, getGithubAgentDeps, registerGithubAgentEndpoint, setGithubAgentDeps, } from './github-agent-deps.js';
4
+ export { GhClient } from './gh-client.js';
5
+ export { WorkspaceManager } from './workspace-manager.js';
6
+ export { parseMessageChannel, issueBranchName, resolveWorkspaceBranch, formatChannelContext, } from './github-channel-context.js';
7
+ export { GithubEndpoint, defaultCreateClient, } from './endpoint.js';
8
+ export { GITHUB_OAUTH_USERS_TABLE, GITHUB_OAUTH_USERS_SCHEMA, defineGithubOauthUsersTable, lookupGithubOauthAccessToken, } from './oauth-users.js';
9
+ export { registerGithubWebhookRoutes, handleGithubWebhookRequest, dispatchGithubWebhookPayload, } from './webhook.js';
@@ -0,0 +1,33 @@
1
+ import type { DatabaseHost } from '@zhin.js/plugin-runtime';
2
+ export declare const GITHUB_OAUTH_USERS_TABLE = "github_oauth_users";
3
+ export declare const GITHUB_OAUTH_USERS_SCHEMA: {
4
+ readonly id: {
5
+ readonly type: "integer";
6
+ readonly primary: true;
7
+ };
8
+ readonly platform: {
9
+ readonly type: "text";
10
+ readonly nullable: false;
11
+ };
12
+ readonly platform_uid: {
13
+ readonly type: "text";
14
+ readonly nullable: false;
15
+ };
16
+ readonly github_login: {
17
+ readonly type: "text";
18
+ readonly nullable: false;
19
+ };
20
+ readonly access_token: {
21
+ readonly type: "text";
22
+ readonly nullable: false;
23
+ };
24
+ readonly created_at: {
25
+ readonly type: "integer";
26
+ readonly nullable: false;
27
+ };
28
+ };
29
+ export declare function defineGithubOauthUsersTable(host: {
30
+ define(name: string, definition: Record<string, unknown>): void;
31
+ }): void;
32
+ /** Resolve a stored PAT/device-flow token; null when DB missing or no row. */
33
+ export declare function lookupGithubOauthAccessToken(database: DatabaseHost | undefined, platform: string, platformUid: string): Promise<string | null>;
@@ -0,0 +1,38 @@
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
+ const logger = getLogger('github');
6
+ export const GITHUB_OAUTH_USERS_TABLE = 'github_oauth_users';
7
+ export const GITHUB_OAUTH_USERS_SCHEMA = {
8
+ id: { type: 'integer', primary: true },
9
+ platform: { type: 'text', nullable: false },
10
+ platform_uid: { type: 'text', nullable: false },
11
+ github_login: { type: 'text', nullable: false },
12
+ access_token: { type: 'text', nullable: false },
13
+ created_at: { type: 'integer', nullable: false },
14
+ };
15
+ export function defineGithubOauthUsersTable(host) {
16
+ host.define(GITHUB_OAUTH_USERS_TABLE, { ...GITHUB_OAUTH_USERS_SCHEMA });
17
+ }
18
+ /** Resolve a stored PAT/device-flow token; null when DB missing or no row. */
19
+ export async function lookupGithubOauthAccessToken(database, platform, platformUid) {
20
+ if (!database?.started)
21
+ return null;
22
+ const model = database.models.get(GITHUB_OAUTH_USERS_TABLE);
23
+ if (!model)
24
+ return null;
25
+ try {
26
+ const rows = await model.select().where({ platform, platform_uid: platformUid });
27
+ const token = rows[0]?.access_token;
28
+ return typeof token === 'string' && token.trim() ? token : null;
29
+ }
30
+ catch (error) {
31
+ logger.debug(formatCompact({
32
+ op: 'oauth_lookup_fail',
33
+ platform,
34
+ error: error instanceof Error ? error.message : String(error),
35
+ }));
36
+ return null;
37
+ }
38
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * GitHub webhook / comment helpers — no legacy Adapter/Endpoint / segment-mapper.
3
+ * Canonicalization is owned by gateway/core before endpoint.send.
4
+ */
5
+ import type { IncomingMessage } from 'node:http';
6
+ import { type GenericWebhookPayload, type IssueCommentPayload, type PRReviewCommentPayload, type PRReviewPayload } from './types.js';
7
+ export type { EventType, GenericWebhookPayload, GitHubComment, GitHubEndpointConfig, GitHubIssue, GitHubOAuthUser, GitHubPR, GitHubRepo, GitHubUser, IssueCommentPayload, ParsedChannel, PRReviewCommentPayload, PRReviewPayload, Subscription, } from './types.js';
8
+ export { buildChannelId, parseChannelId } from './types.js';
9
+ /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
10
+ export interface GithubAdapterConfig {
11
+ readonly name?: string;
12
+ readonly host?: string;
13
+ readonly app_id?: string | number;
14
+ readonly appId?: string | number;
15
+ readonly private_key?: string;
16
+ readonly privateKey?: string;
17
+ readonly webhook_secret?: string;
18
+ readonly webhookSecret?: string;
19
+ readonly webhook_path?: string;
20
+ readonly webhookPath?: string;
21
+ readonly poll_interval?: number;
22
+ readonly pollInterval?: number;
23
+ readonly auto_reply_repos?: readonly string[];
24
+ readonly autoReplyRepos?: readonly string[];
25
+ readonly bot_login?: string;
26
+ readonly botLogin?: string;
27
+ readonly workspace_root?: string;
28
+ readonly workspaceRoot?: string;
29
+ /** Transitional: legacy root `endpoints[]` with `context: github`. */
30
+ readonly endpoints?: ReadonlyArray<Partial<ResolvedGithubConfig> & {
31
+ readonly context?: string;
32
+ readonly app_id?: string | number;
33
+ readonly private_key?: string;
34
+ readonly webhook_secret?: string;
35
+ readonly webhook_path?: string;
36
+ readonly poll_interval?: number;
37
+ readonly auto_reply_repos?: readonly string[];
38
+ readonly bot_login?: string;
39
+ readonly workspace_root?: string;
40
+ }>;
41
+ }
42
+ export interface ResolvedGithubConfig {
43
+ readonly context: 'github';
44
+ readonly name: string;
45
+ readonly host?: string;
46
+ readonly appId?: string | number;
47
+ readonly privateKey?: string;
48
+ readonly webhookSecret?: string;
49
+ readonly webhookPath: string;
50
+ readonly pollInterval: number;
51
+ readonly autoReplyRepos: readonly string[];
52
+ readonly botLogin?: string;
53
+ readonly workspaceRoot?: string;
54
+ }
55
+ export interface GithubWireSegment {
56
+ readonly type: string;
57
+ readonly data?: Record<string, unknown>;
58
+ }
59
+ export interface GithubInboundComment {
60
+ readonly id: string;
61
+ readonly channelId: string;
62
+ readonly sender: string;
63
+ readonly content: string;
64
+ readonly repo: string;
65
+ readonly kind: 'issue_comment' | 'pr_review_comment' | 'pr_review';
66
+ readonly createdAt: number;
67
+ }
68
+ export declare function resolveGithubConfig(config?: GithubAdapterConfig): ResolvedGithubConfig;
69
+ export declare function normalizeWebhookPath(path: string): string;
70
+ export declare function shouldAutoReplyRepo(config: Pick<ResolvedGithubConfig, 'autoReplyRepos'>, repo: string): boolean;
71
+ /** Prepend synthetic @bot for auto_reply repos (gateway text path). */
72
+ export declare function enrichInboundContent(content: string, config: Pick<ResolvedGithubConfig, 'autoReplyRepos' | 'botLogin'>, botLogin: string | undefined, repo: string): string;
73
+ /** Build inbound text for MessageGateway.receive from markdown/comment body. */
74
+ export declare function formatInboundContent(body: string): string;
75
+ /** Build markdown body for Issue/PR comment send. */
76
+ export declare function formatOutboundBody(payload: unknown): string;
77
+ export declare function verifyWebhookSignature(secret: string, rawBody: string, signatureHeader: string | undefined): boolean;
78
+ export declare function headerValue(headers: IncomingMessage['headers'], name: string): string;
79
+ export declare function readTextBody(request: IncomingMessage, options?: {
80
+ readonly limit?: number;
81
+ }): Promise<string>;
82
+ export declare function parseIssueCommentInbound(payload: IssueCommentPayload): GithubInboundComment | null;
83
+ export declare function parsePrReviewCommentInbound(payload: PRReviewCommentPayload): GithubInboundComment | null;
84
+ export declare function parsePrReviewInbound(payload: PRReviewPayload): GithubInboundComment | null;
85
+ export declare function formatNotification(event: string, p: GenericWebhookPayload): string;
86
+ /** Parse @mentions in markdown into a flat display string (gateway text path). */
87
+ export declare function parseMarkdownMentions(md: string): string;
@@ -0,0 +1,241 @@
1
+ /**
2
+ * GitHub webhook / comment helpers — no legacy Adapter/Endpoint / segment-mapper.
3
+ * Canonicalization is owned by gateway/core before endpoint.send.
4
+ */
5
+ import { createHmac, timingSafeEqual } from 'node:crypto';
6
+ import { pickCredential } from '@zhin.js/adapter';
7
+ import { buildChannelId, } from './types.js';
8
+ export { buildChannelId, parseChannelId } from './types.js';
9
+ export function resolveGithubConfig(config = {}) {
10
+ const entry = config.endpoints?.find((item) => item.context === 'github');
11
+ const appIdRaw = pickCredential(config.appId != null ? String(config.appId) : undefined, config.app_id != null ? String(config.app_id) : undefined, entry?.appId != null ? String(entry.appId) : undefined, entry?.app_id != null ? String(entry.app_id) : undefined, process.env.GITHUB_APP_ID);
12
+ const appId = appIdRaw ? (Number(appIdRaw) || appIdRaw) : undefined;
13
+ const privateKey = pickCredential(config.privateKey, config.private_key, entry?.privateKey, entry?.private_key) || undefined;
14
+ if (!appId || !privateKey) {
15
+ throw new TypeError('GitHub adapter requires app_id + private_key (plugins.<key> or GITHUB_APP_ID)');
16
+ }
17
+ const name = (typeof config.name === 'string' && config.name)
18
+ || (typeof entry?.name === 'string' && entry.name)
19
+ || process.env.GITHUB_BOT_NAME
20
+ || 'github-bot';
21
+ const webhookSecret = config.webhookSecret ?? config.webhook_secret
22
+ ?? entry?.webhookSecret ?? entry?.webhook_secret
23
+ ?? process.env.GITHUB_WEBHOOK_SECRET
24
+ ?? undefined;
25
+ const host = config.host ?? entry?.host;
26
+ const webhookPath = normalizeWebhookPath(config.webhookPath ?? config.webhook_path ?? entry?.webhookPath ?? entry?.webhook_path
27
+ ?? '/github/webhook');
28
+ const pollInterval = Number(config.pollInterval ?? config.poll_interval ?? entry?.pollInterval ?? entry?.poll_interval ?? 60) || 60;
29
+ const autoReplyRepos = [
30
+ ...(config.autoReplyRepos ?? config.auto_reply_repos
31
+ ?? entry?.autoReplyRepos ?? entry?.auto_reply_repos ?? []),
32
+ ];
33
+ const botLogin = config.botLogin ?? config.bot_login ?? entry?.botLogin ?? entry?.bot_login;
34
+ const workspaceRoot = config.workspaceRoot ?? config.workspace_root
35
+ ?? entry?.workspaceRoot ?? entry?.workspace_root;
36
+ return {
37
+ context: 'github',
38
+ name,
39
+ ...(host ? { host } : {}),
40
+ ...(appId != null ? { appId } : {}),
41
+ ...(privateKey ? { privateKey } : {}),
42
+ ...(webhookSecret ? { webhookSecret } : {}),
43
+ webhookPath,
44
+ pollInterval,
45
+ autoReplyRepos,
46
+ ...(botLogin ? { botLogin } : {}),
47
+ ...(workspaceRoot ? { workspaceRoot } : {}),
48
+ };
49
+ }
50
+ export function normalizeWebhookPath(path) {
51
+ const trimmed = path.trim() || '/github/webhook';
52
+ return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
53
+ }
54
+ export function shouldAutoReplyRepo(config, repo) {
55
+ const list = config.autoReplyRepos;
56
+ if (!list.length)
57
+ return false;
58
+ const key = repo.toLowerCase();
59
+ return list.some((r) => r.toLowerCase() === key);
60
+ }
61
+ /** Prepend synthetic @bot for auto_reply repos (gateway text path). */
62
+ export function enrichInboundContent(content, config, botLogin, repo) {
63
+ const login = config.botLogin || botLogin;
64
+ if (!login || !shouldAutoReplyRepo(config, repo))
65
+ return content;
66
+ if (content.includes(`@${login}`))
67
+ return content;
68
+ return `@${login} ${content}`;
69
+ }
70
+ /** Build inbound text for MessageGateway.receive from markdown/comment body. */
71
+ export function formatInboundContent(body) {
72
+ return body;
73
+ }
74
+ /** Build markdown body for Issue/PR comment send. */
75
+ export function formatOutboundBody(payload) {
76
+ if (typeof payload === 'string')
77
+ return payload;
78
+ if (payload == null)
79
+ return '';
80
+ if (!Array.isArray(payload)) {
81
+ if (typeof payload === 'object' && payload !== null && 'text' in payload) {
82
+ return String(payload.text ?? '');
83
+ }
84
+ return String(payload);
85
+ }
86
+ return payload.map((seg) => {
87
+ if (typeof seg === 'string')
88
+ return seg;
89
+ const item = seg;
90
+ switch (item.type) {
91
+ case 'text':
92
+ return String(item.data?.text ?? '');
93
+ case 'mention':
94
+ case 'at':
95
+ return `@${item.data?.name || item.data?.id || item.data?.target || ''}`;
96
+ case 'image':
97
+ return item.data?.url ? `![image](${item.data.url})` : '[image]';
98
+ case 'link':
99
+ return `[${item.data?.text || item.data?.url || ''}](${item.data?.url || ''})`;
100
+ default:
101
+ return String(item.data?.text ?? `[${item.type}]`);
102
+ }
103
+ }).join('');
104
+ }
105
+ export function verifyWebhookSignature(secret, rawBody, signatureHeader) {
106
+ if (!signatureHeader?.startsWith('sha256='))
107
+ return false;
108
+ const expected = `sha256=${createHmac('sha256', secret).update(rawBody).digest('hex')}`;
109
+ try {
110
+ const a = Buffer.from(signatureHeader);
111
+ const b = Buffer.from(expected);
112
+ if (a.length !== b.length)
113
+ return false;
114
+ return timingSafeEqual(a, b);
115
+ }
116
+ catch {
117
+ return false;
118
+ }
119
+ }
120
+ export function headerValue(headers, name) {
121
+ const raw = headers[name] ?? headers[name.toLowerCase()];
122
+ if (Array.isArray(raw))
123
+ return raw[0] ?? '';
124
+ return raw ?? '';
125
+ }
126
+ export async function readTextBody(request, options = {}) {
127
+ const limit = options.limit ?? 1_048_576;
128
+ const chunks = [];
129
+ let size = 0;
130
+ for await (const chunk of request) {
131
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
132
+ size += buffer.length;
133
+ if (size > limit) {
134
+ request.destroy();
135
+ throw new Error(`Request body exceeds ${limit} bytes`);
136
+ }
137
+ chunks.push(buffer);
138
+ }
139
+ return Buffer.concat(chunks).toString('utf8');
140
+ }
141
+ export function parseIssueCommentInbound(payload) {
142
+ if (payload.action !== 'created' || !payload.comment)
143
+ return null;
144
+ const repo = payload.repository.full_name;
145
+ const isPR = 'pull_request' in payload.issue;
146
+ return {
147
+ id: String(payload.comment.id),
148
+ channelId: buildChannelId(repo, isPR ? 'pr' : 'issue', payload.issue.number),
149
+ sender: payload.sender.login,
150
+ content: payload.comment.body,
151
+ repo,
152
+ kind: 'issue_comment',
153
+ createdAt: new Date(payload.comment.created_at).getTime(),
154
+ };
155
+ }
156
+ export function parsePrReviewCommentInbound(payload) {
157
+ if (payload.action !== 'created' || !payload.comment)
158
+ return null;
159
+ const repo = payload.repository.full_name;
160
+ const body = payload.comment.path
161
+ ? `**${payload.comment.path}**\n${payload.comment.diff_hunk ? `\`\`\`diff\n${payload.comment.diff_hunk}\n\`\`\`\n` : ''}${payload.comment.body}`
162
+ : payload.comment.body;
163
+ return {
164
+ id: String(payload.comment.id),
165
+ channelId: buildChannelId(repo, 'pr', payload.pull_request.number),
166
+ sender: payload.sender.login,
167
+ content: body,
168
+ repo,
169
+ kind: 'pr_review_comment',
170
+ createdAt: new Date(payload.comment.created_at).getTime(),
171
+ };
172
+ }
173
+ export function parsePrReviewInbound(payload) {
174
+ if (payload.action !== 'submitted' || !payload.review.body)
175
+ return null;
176
+ const repo = payload.repository.full_name;
177
+ const stateLabel = {
178
+ approved: '✅ APPROVED',
179
+ changes_requested: '🔄 CHANGES REQUESTED',
180
+ commented: '💬 COMMENTED',
181
+ dismissed: '❌ DISMISSED',
182
+ };
183
+ const body = `**[${stateLabel[payload.review.state] || payload.review.state}]**\n${payload.review.body}`;
184
+ return {
185
+ id: String(payload.review.id),
186
+ channelId: buildChannelId(repo, 'pr', payload.pull_request.number),
187
+ sender: payload.sender.login,
188
+ content: body,
189
+ repo,
190
+ kind: 'pr_review',
191
+ createdAt: new Date(payload.review.submitted_at).getTime(),
192
+ };
193
+ }
194
+ export function formatNotification(event, p) {
195
+ const repo = p.repository.full_name;
196
+ const sender = p.sender.login;
197
+ const repoUrl = p.repository.html_url;
198
+ switch (event) {
199
+ case 'push': {
200
+ const branch = p.ref?.replace('refs/heads/', '') || '?';
201
+ const commits = p.commits || [];
202
+ const compareUrl = commits.length >= 2
203
+ ? `${repoUrl}/compare/${commits[0].id.substring(0, 12)}...${commits[commits.length - 1].id.substring(0, 12)}`
204
+ : commits.length === 1 ? `${repoUrl}/commit/${commits[0].id}` : '';
205
+ let msg = `📦 ${repo}\n🌿 ${sender} pushed ${commits.length} commit(s) to \`${branch}\`\n`;
206
+ if (commits.length) {
207
+ msg += '\n';
208
+ msg += commits.slice(0, 5).map((c) => ` • [\`${c.id.substring(0, 7)}\`](${repoUrl}/commit/${c.id}) ${c.message.split('\n')[0]}`).join('\n');
209
+ if (commits.length > 5)
210
+ msg += `\n ... +${commits.length - 5} more`;
211
+ }
212
+ if (compareUrl)
213
+ msg += `\n\n🔗 ${compareUrl}`;
214
+ return msg;
215
+ }
216
+ case 'issues': {
217
+ const i = p.issue;
218
+ const act = p.action === 'opened' ? '📝 opened' : p.action === 'closed' ? '✅ closed' : `🔄 ${p.action || 'updated'}`;
219
+ return `🐛 ${repo}\n👤 ${sender} ${act} issue #${i.number}\n📌 ${i.title}\n🔗 ${i.html_url}`;
220
+ }
221
+ case 'star': {
222
+ const starred = p.action !== 'deleted';
223
+ return `${starred ? '⭐' : '💔'} ${repo}\n👤 ${sender} ${starred ? 'starred' : 'unstarred'}\n🔗 ${repoUrl}`;
224
+ }
225
+ case 'fork':
226
+ return `🍴 ${repo}\n👤 ${sender} forked → ${p.forkee.full_name}\n🔗 ${p.forkee.html_url}`;
227
+ case 'pull_request': {
228
+ const pr = p.pull_request;
229
+ const act = p.action === 'opened' ? '📝 opened'
230
+ : p.action === 'closed' ? (pr.state === 'closed' ? '❌ closed' : '✅ merged')
231
+ : `🔄 ${p.action || 'updated'}`;
232
+ return `🔀 ${repo}\n👤 ${sender} ${act} PR #${pr.number}\n📌 ${pr.title}\n🌿 ${pr.head.ref} → ${pr.base.ref}\n🔗 ${pr.html_url}`;
233
+ }
234
+ default:
235
+ return `📬 ${repo}\n📡 ${event}${p.action ? ` (${p.action})` : ''} by ${sender}\n🔗 ${repoUrl}`;
236
+ }
237
+ }
238
+ /** Parse @mentions in markdown into a flat display string (gateway text path). */
239
+ export function parseMarkdownMentions(md) {
240
+ return md;
241
+ }
package/lib/types.d.ts ADDED
@@ -0,0 +1,149 @@
1
+ export interface GitHubEndpointConfig {
2
+ context: 'github';
3
+ /** Endpoint 标识名称 */
4
+ name: string;
5
+ /** GitHub Enterprise 主机名(默认 github.com) */
6
+ host?: string;
7
+ /** GitHub App ID */
8
+ app_id?: string | number;
9
+ /** GitHub App 私钥(PEM 内容或文件路径) */
10
+ private_key?: string;
11
+ /** Webhook Secret(配置后启用 Webhook 接收事件,不配置则使用轮询) */
12
+ webhook_secret?: string;
13
+ /** Webhook 路由路径(默认 /github/webhook) */
14
+ webhook_path?: string;
15
+ /** 事件轮询间隔(秒,默认 60,Webhook 模式下作为降级备选) */
16
+ poll_interval?: number;
17
+ /** 这些仓库的人类 Issue/PR 评论自动触发 AI(无需 @bot) */
18
+ auto_reply_repos?: string[];
19
+ /** 覆盖 App bot 登录名(默认 {slug}[bot]) */
20
+ bot_login?: string;
21
+ /** 托管 git 工作区根目录(默认 {cwd}/data/github-workspaces) */
22
+ workspace_root?: string;
23
+ }
24
+ export interface ParsedChannel {
25
+ repo: string;
26
+ type: 'issue' | 'pr';
27
+ number: number;
28
+ }
29
+ export declare function parseChannelId(channelId: string): ParsedChannel | null;
30
+ export declare function buildChannelId(repo: string, type: 'issue' | 'pr', number: number): string;
31
+ export interface GitHubUser {
32
+ login: string;
33
+ id: number;
34
+ html_url: string;
35
+ }
36
+ export interface GitHubComment {
37
+ id: number;
38
+ body: string;
39
+ user: GitHubUser;
40
+ created_at: string;
41
+ updated_at: string;
42
+ html_url: string;
43
+ }
44
+ export interface GitHubRepo {
45
+ full_name: string;
46
+ html_url: string;
47
+ description?: string;
48
+ }
49
+ export interface GitHubIssue {
50
+ number: number;
51
+ title: string;
52
+ html_url: string;
53
+ state: string;
54
+ user: GitHubUser;
55
+ }
56
+ export interface GitHubPR {
57
+ number: number;
58
+ title: string;
59
+ html_url: string;
60
+ state: string;
61
+ user: GitHubUser;
62
+ head: {
63
+ ref: string;
64
+ };
65
+ base: {
66
+ ref: string;
67
+ };
68
+ }
69
+ export interface IssueCommentPayload {
70
+ action: 'created' | 'edited' | 'deleted';
71
+ comment: GitHubComment;
72
+ issue: GitHubIssue;
73
+ repository: GitHubRepo;
74
+ sender: GitHubUser;
75
+ }
76
+ export interface PRReviewCommentPayload {
77
+ action: 'created' | 'edited' | 'deleted';
78
+ comment: GitHubComment & {
79
+ diff_hunk?: string;
80
+ path?: string;
81
+ };
82
+ pull_request: GitHubPR;
83
+ repository: GitHubRepo;
84
+ sender: GitHubUser;
85
+ }
86
+ export interface PRReviewPayload {
87
+ action: 'submitted' | 'edited' | 'dismissed';
88
+ review: {
89
+ id: number;
90
+ body: string | null;
91
+ state: 'approved' | 'changes_requested' | 'commented' | 'dismissed';
92
+ user: GitHubUser;
93
+ html_url: string;
94
+ submitted_at: string;
95
+ };
96
+ pull_request: GitHubPR;
97
+ repository: GitHubRepo;
98
+ sender: GitHubUser;
99
+ }
100
+ export interface GenericWebhookPayload {
101
+ action?: string;
102
+ repository: GitHubRepo;
103
+ sender: GitHubUser;
104
+ ref?: string;
105
+ commits?: Array<{
106
+ id: string;
107
+ message: string;
108
+ author: {
109
+ name: string;
110
+ username?: string;
111
+ };
112
+ url: string;
113
+ }>;
114
+ issue?: GitHubIssue;
115
+ pull_request?: GitHubPR;
116
+ forkee?: {
117
+ full_name: string;
118
+ html_url: string;
119
+ owner: {
120
+ login: string;
121
+ };
122
+ };
123
+ }
124
+ export type EventType = 'push' | 'issue' | 'star' | 'fork' | 'unstar' | 'pull_request';
125
+ export interface Subscription {
126
+ id: number;
127
+ repo: string;
128
+ events: EventType[];
129
+ target_id: string;
130
+ target_type: 'private' | 'group' | 'channel';
131
+ adapter: string;
132
+ endpoint: string;
133
+ }
134
+ export type PrAction = 'list' | 'view' | 'diff' | 'merge' | 'create' | 'review' | 'close';
135
+ export type IssueAction = 'list' | 'view' | 'create' | 'close' | 'comment';
136
+ export type RepoAction = 'info' | 'branches' | 'releases' | 'runs' | 'stars';
137
+ export interface GitHubOAuthUser {
138
+ id: number;
139
+ /** 来源平台 (icqq, kook, discord …) */
140
+ platform: string;
141
+ /** 该平台上的用户 ID */
142
+ platform_uid: string;
143
+ /** GitHub 用户名 */
144
+ github_login: string;
145
+ /** OAuth Access Token */
146
+ access_token: string;
147
+ /** 绑定时间 (ms) */
148
+ created_at: number;
149
+ }
@@ -13,4 +13,3 @@ export function parseChannelId(channelId) {
13
13
  export function buildChannelId(repo, type, number) {
14
14
  return type === 'issue' ? `${repo}/issues/${number}` : `${repo}/pull/${number}`;
15
15
  }
16
- //# sourceMappingURL=types.js.map
@@ -0,0 +1,13 @@
1
+ /**
2
+ * GitHub webhook HTTP: verify → parse → admit inbound comments.
3
+ */
4
+ import type { IncomingMessage, ServerResponse } from 'node:http';
5
+ import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
6
+ import { type GithubInboundComment, type ResolvedGithubConfig } from './protocol.js';
7
+ export interface GithubWebhookHandler {
8
+ readonly config: ResolvedGithubConfig;
9
+ admit(comment: GithubInboundComment): void;
10
+ }
11
+ export declare function registerGithubWebhookRoutes(http: HttpHost, handler: GithubWebhookHandler): HttpRouteRegistration[];
12
+ export declare function handleGithubWebhookRequest(request: IncomingMessage, response: ServerResponse, handler: GithubWebhookHandler): Promise<void>;
13
+ export declare function dispatchGithubWebhookPayload(event: string, payload: unknown, handler: GithubWebhookHandler): Promise<void>;