@zhin.js/adapter-github 1.0.1 → 1.1.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.
- package/CHANGELOG.md +1118 -0
- package/README.md +70 -191
- package/adapters/github.js +51 -0
- package/adapters/github.ts +59 -0
- package/agent/prompt-sections/platform.ts +16 -0
- package/{skills/github/SKILL.md → agent/skills/github.md} +22 -5
- package/agent/tools/bind.ts +13 -0
- package/agent/tools/create_pr.ts +20 -0
- package/agent/tools/install.ts +13 -0
- package/agent/tools/patch_file.ts +19 -0
- package/agent/tools/prepare_workspace.ts +15 -0
- package/agent/tools/push_branch.ts +18 -0
- package/agent/tools/star.ts +16 -0
- package/agent/tools/subscribe.ts +16 -0
- package/agent/tools/subscriptions.ts +13 -0
- package/agent/tools/unbind.ts +13 -0
- package/agent/tools/unsubscribe.ts +15 -0
- package/agent/tools/whoami.ts +13 -0
- package/commands/endpoint/add/[id].js +3 -0
- package/commands/endpoint/add/[id].ts +3 -0
- package/commands/endpoint/list.js +3 -0
- package/commands/endpoint/list.ts +3 -0
- package/commands/endpoint/remove/[id].js +3 -0
- package/commands/endpoint/remove/[id].ts +3 -0
- package/lib/client.d.ts +36 -0
- package/lib/client.js +47 -0
- package/lib/endpoint.d.ts +32 -22
- package/lib/endpoint.js +125 -145
- package/lib/gh-client.d.ts +73 -1
- package/lib/gh-client.js +99 -1
- package/lib/github-bot-handlers.d.ts +27 -0
- package/lib/github-bot-handlers.js +76 -0
- package/lib/github-channel-context.d.ts +16 -0
- package/lib/github-channel-context.js +31 -0
- package/lib/github-endpoint-commands.d.ts +1 -0
- package/lib/github-endpoint-commands.js +22 -0
- package/lib/github-runtime-state.d.ts +1 -0
- package/lib/github-runtime-state.js +6 -0
- package/lib/github-tool-handlers.d.ts +18 -0
- package/lib/github-tool-handlers.js +214 -0
- package/lib/index.d.ts +7 -32
- package/lib/index.js +7 -385
- package/lib/oauth-users.d.ts +33 -0
- package/lib/oauth-users.js +38 -0
- package/lib/protocol.d.ts +94 -0
- package/lib/protocol.js +292 -0
- package/lib/types.d.ts +6 -1
- package/lib/types.js +0 -1
- package/lib/webhook.d.ts +14 -0
- package/lib/webhook.js +88 -0
- package/lib/workspace-manager.d.ts +21 -0
- package/lib/workspace-manager.js +154 -0
- package/package.json +77 -23
- package/plugin.js +38 -0
- package/schema.json +130 -0
- package/src/client.ts +65 -0
- package/src/endpoint.ts +145 -150
- package/src/gh-client.ts +131 -0
- package/src/github-bot-handlers.ts +113 -0
- package/src/github-channel-context.ts +46 -0
- package/src/github-endpoint-commands.ts +23 -0
- package/src/github-runtime-state.ts +7 -0
- package/src/github-tool-handlers.ts +252 -0
- package/src/index.ts +48 -431
- package/src/oauth-users.ts +47 -0
- package/src/protocol.ts +425 -0
- package/src/types.ts +6 -0
- package/src/webhook.ts +130 -0
- package/src/workspace-manager.ts +168 -0
- package/lib/adapter.d.ts +0 -64
- package/lib/adapter.d.ts.map +0 -1
- package/lib/adapter.js +0 -416
- package/lib/adapter.js.map +0 -1
- package/lib/agent-prompt.d.ts +0 -3
- package/lib/agent-prompt.d.ts.map +0 -1
- package/lib/agent-prompt.js +0 -82
- 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 -6
- package/lib/register-github-mcp.d.ts.map +0 -1
- package/lib/register-github-mcp.js +0 -35
- package/lib/register-github-mcp.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 -448
- package/src/agent-prompt.ts +0 -99
- package/src/register-github-mcp.ts +0 -60
package/src/index.ts
CHANGED
|
@@ -1,434 +1,51 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
interface Models {
|
|
21
|
-
github_subscriptions: {
|
|
22
|
-
id: number;
|
|
23
|
-
repo: string;
|
|
24
|
-
events: import('./types.js').EventType[];
|
|
25
|
-
target_id: string;
|
|
26
|
-
target_type: 'private' | 'group' | 'channel';
|
|
27
|
-
adapter: string;
|
|
28
|
-
endpoint: string;
|
|
29
|
-
};
|
|
30
|
-
github_events: {
|
|
31
|
-
id: number;
|
|
32
|
-
repo: string;
|
|
33
|
-
event_type: string;
|
|
34
|
-
payload: any;
|
|
35
|
-
};
|
|
36
|
-
github_oauth_users: import('./types.js').GitHubOAuthUser;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
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';
|
|
39
20
|
|
|
40
21
|
export * from './types.js';
|
|
41
|
-
export { GitHubEndpoint, parseMarkdown, toMarkdown } from './endpoint.js';
|
|
42
|
-
export { GitHubAdapter } from './adapter.js';
|
|
43
|
-
export { GhClient } from './gh-client.js';
|
|
44
|
-
|
|
45
|
-
const plugin = usePlugin();
|
|
46
|
-
const { provide, defineModel, useContext, logger } = plugin;
|
|
47
|
-
|
|
48
|
-
registerGithubMcp(plugin);
|
|
49
|
-
|
|
50
|
-
defineModel('github_subscriptions', {
|
|
51
|
-
id: { type: 'integer', primary: true },
|
|
52
|
-
repo: { type: 'text', nullable: false },
|
|
53
|
-
events: { type: 'json', default: [] },
|
|
54
|
-
target_id: { type: 'text', nullable: false },
|
|
55
|
-
target_type: { type: 'text', nullable: false },
|
|
56
|
-
adapter: { type: 'text', nullable: false },
|
|
57
|
-
endpoint: { type: 'text', nullable: false },
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
defineModel('github_events', {
|
|
61
|
-
id: { type: 'integer', primary: true },
|
|
62
|
-
repo: { type: 'text', nullable: false },
|
|
63
|
-
event_type: { type: 'text', nullable: false },
|
|
64
|
-
payload: { type: 'json', default: {} },
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
defineModel('github_oauth_users', {
|
|
68
|
-
id: { type: 'integer', primary: true },
|
|
69
|
-
platform: { type: 'text', nullable: false },
|
|
70
|
-
platform_uid: { type: 'text', nullable: false },
|
|
71
|
-
github_login: { type: 'text', nullable: false },
|
|
72
|
-
access_token: { type: 'text', nullable: false },
|
|
73
|
-
created_at: { type: 'integer', default: 0 },
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
provide({
|
|
77
|
-
name: 'github',
|
|
78
|
-
description: 'GitHub Adapter — Issues/PRs as chat channels, full repo management via gh CLI',
|
|
79
|
-
mounted: async (p: Plugin) => {
|
|
80
|
-
registerAgentPromptContributor(createGithubAgentPromptContributor());
|
|
81
|
-
const adapter = new GitHubAdapter(p);
|
|
82
|
-
await adapter.start();
|
|
83
|
-
return adapter;
|
|
84
|
-
},
|
|
85
|
-
dispose: async (adapter: GitHubAdapter) => {
|
|
86
|
-
unregisterAgentPromptContributor('github');
|
|
87
|
-
await adapter.stop();
|
|
88
|
-
},
|
|
89
|
-
} as Context<'github'>);
|
|
90
|
-
|
|
91
|
-
// 混合模式:有 router + webhook_secret → Webhook 实时;否则 → 轮询降级
|
|
92
|
-
useContext('github', (adapter) => {
|
|
93
|
-
if (adapter.hasWebhookConfig) {
|
|
94
|
-
// 尝试注册 Webhook(需要 router Context)
|
|
95
|
-
const router = plugin.inject('router');
|
|
96
|
-
if (router) {
|
|
97
|
-
adapter.setupWebhook(router);
|
|
98
|
-
logger.debug(formatCompact( { op: 'events', source: 'webhook' }));
|
|
99
|
-
} else {
|
|
100
|
-
// router 还没就绪,等它挂载后再注册
|
|
101
|
-
plugin.useContext('router', (r) => {
|
|
102
|
-
adapter.setupWebhook(r);
|
|
103
|
-
logger.debug(formatCompact( { op: 'events', source: 'webhook', deferred: true }));
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
// Webhook 未配置或未激活时,总是启动轮询作为兜底
|
|
108
|
-
if (!adapter.webhookActive) {
|
|
109
|
-
adapter.startPolling();
|
|
110
|
-
logger.debug(formatCompact( { op: 'events', source: 'poll' }));
|
|
111
|
-
}
|
|
112
|
-
return () => adapter.stopPolling();
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
// ── Tool 工具注册 ─────────────────────────────────────────────────────────
|
|
116
|
-
useContext('tool', 'github', (toolService: ToolFeature, adapter: GitHubAdapter) => {
|
|
117
|
-
const tools: Tool[] = [
|
|
118
|
-
// --- Star ---
|
|
119
|
-
{
|
|
120
|
-
name: 'github_star',
|
|
121
|
-
description: 'Star 或取消 Star 一个 GitHub 仓库(使用你绑定的 GitHub 账号,未绑定则用 Endpoint 默认账号)',
|
|
122
|
-
parameters: {
|
|
123
|
-
type: 'object' as const,
|
|
124
|
-
properties: {
|
|
125
|
-
action: { type: 'string' as const, description: 'star|unstar|check', enum: ['star', 'unstar', 'check'] },
|
|
126
|
-
repo: { type: 'string' as const, description: 'owner/repo (必填)' },
|
|
127
|
-
},
|
|
128
|
-
required: ['action', 'repo'],
|
|
129
|
-
},
|
|
130
|
-
tags: ['github'],
|
|
131
|
-
execute: async (args: Record<string, any>, commMessage?: Message<any>) => {
|
|
132
|
-
const gh = await adapter.getUserOrDefaultAPI(commMessage?.$adapter, commMessage?.$sender.id);
|
|
133
|
-
if (!gh) return '❌ 没有可用的 GitHub bot';
|
|
134
|
-
const { action, repo } = args;
|
|
135
|
-
switch (action) {
|
|
136
|
-
case 'star': {
|
|
137
|
-
const r = await gh.starRepo(repo);
|
|
138
|
-
return r.ok ? `⭐ 已 Star ${repo}` : `❌ ${r.data?.message || JSON.stringify(r.data)}`;
|
|
139
|
-
}
|
|
140
|
-
case 'unstar': {
|
|
141
|
-
const r = await gh.unstarRepo(repo);
|
|
142
|
-
return r.ok ? `💔 已取消 Star ${repo}` : `❌ ${r.data?.message || JSON.stringify(r.data)}`;
|
|
143
|
-
}
|
|
144
|
-
case 'check': {
|
|
145
|
-
const starred = await gh.isStarred(repo);
|
|
146
|
-
return starred ? `⭐ 已 Star ${repo}` : `☆ 尚未 Star ${repo}`;
|
|
147
|
-
}
|
|
148
|
-
default: return `❌ 未知操作: ${action}`;
|
|
149
|
-
}
|
|
150
|
-
},
|
|
151
|
-
},
|
|
152
|
-
// --- Bind (Device Flow) ---
|
|
153
|
-
{
|
|
154
|
-
name: 'github_bind',
|
|
155
|
-
description: '绑定你的 GitHub 账号 — 使用 Device Flow 授权,无需输入密码',
|
|
156
|
-
parameters: {
|
|
157
|
-
type: 'object' as const,
|
|
158
|
-
properties: {},
|
|
159
|
-
},
|
|
160
|
-
tags: ['github'],
|
|
161
|
-
execute: async (_args: Record<string, any>, commMessage?: Message<any>) => {
|
|
162
|
-
if (!commMessage?.$adapter || !commMessage?.$sender?.id) {
|
|
163
|
-
return '❌ 无法获取当前用户信息';
|
|
164
|
-
}
|
|
165
|
-
const clientId = adapter.getClientId();
|
|
166
|
-
if (!clientId) return '❌ Endpoint 未配置 GitHub App 或 App 无 client_id,无法进行账号绑定';
|
|
167
|
-
|
|
168
|
-
const db = plugin.root?.inject('database') as any;
|
|
169
|
-
const model = db?.models?.get('github_oauth_users');
|
|
170
|
-
if (!model) return '❌ 数据库未就绪';
|
|
171
|
-
|
|
172
|
-
// 检查是否已绑定
|
|
173
|
-
const [existing] = await model.select().where({ platform: commMessage.$adapter, platform_uid: commMessage.$sender.id });
|
|
174
|
-
if (existing) {
|
|
175
|
-
return `⚠️ 你已绑定 GitHub 账号: ${existing.github_login}\n如需重新绑定,请先执行 github_unbind`;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
try {
|
|
179
|
-
const host = adapter.getHost();
|
|
180
|
-
const codeResp = await GhClient.deviceFlowRequestCode(clientId, host);
|
|
181
|
-
// 异步轮询 token(最多等 codeResp.expires_in 秒)
|
|
182
|
-
const tokenPromise = GhClient.deviceFlowPollToken(
|
|
183
|
-
clientId, codeResp.device_code, codeResp.interval, codeResp.expires_in, host,
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
// 先回复用户授权链接
|
|
187
|
-
const replyMsg = [
|
|
188
|
-
`🔗 请在浏览器中打开以下链接进行授权:`,
|
|
189
|
-
` ${codeResp.verification_uri}`,
|
|
190
|
-
``,
|
|
191
|
-
`📋 输入验证码: **${codeResp.user_code}**`,
|
|
192
|
-
``,
|
|
193
|
-
`⏳ 等待授权中…(${Math.floor(codeResp.expires_in / 60)} 分钟内有效)`,
|
|
194
|
-
].join('\n');
|
|
195
|
-
|
|
196
|
-
// 在后台等待用户授权完成
|
|
197
|
-
tokenPromise.then(async (tokenData) => {
|
|
198
|
-
if (!tokenData) {
|
|
199
|
-
// 授权超时或被拒绝 — 由于 execute 已经返回了,这里只能通过日志记录
|
|
200
|
-
logger.warn(formatCompact( { op: 'device_flow', ok: false, platform: commMessage.$adapter, sender: commMessage.$sender.id }));
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// 使用 token 获取 GitHub 用户名
|
|
205
|
-
const userGh = new GhClient({ host, token: tokenData.access_token });
|
|
206
|
-
const authResult = await userGh.verifyAuth();
|
|
207
|
-
const login = authResult.ok ? authResult.user : 'unknown';
|
|
208
|
-
|
|
209
|
-
await model.insert({
|
|
210
|
-
id: Date.now(),
|
|
211
|
-
platform: commMessage.$adapter,
|
|
212
|
-
platform_uid: commMessage.$sender.id,
|
|
213
|
-
github_login: login,
|
|
214
|
-
access_token: tokenData.access_token,
|
|
215
|
-
created_at: Date.now(),
|
|
216
|
-
});
|
|
217
|
-
logger.info(formatCompact( { op: 'bind', platform: commMessage.$adapter, sender: commMessage.$sender.id, login }));
|
|
218
22
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (!model) return '❌ 数据库未就绪';
|
|
249
|
-
|
|
250
|
-
const [existing] = await model.select().where({ platform: commMessage.$adapter, platform_uid: commMessage.$sender.id });
|
|
251
|
-
if (!existing) return '📭 你尚未绑定 GitHub 账号';
|
|
252
|
-
|
|
253
|
-
await model.delete().where({ id: existing.id });
|
|
254
|
-
return `✅ 已解除 GitHub 账号绑定: ${existing.github_login}`;
|
|
255
|
-
},
|
|
256
|
-
},
|
|
257
|
-
// --- Whoami ---
|
|
258
|
-
{
|
|
259
|
-
name: 'github_whoami',
|
|
260
|
-
description: '查看你绑定的 GitHub 账号信息',
|
|
261
|
-
parameters: {
|
|
262
|
-
type: 'object' as const,
|
|
263
|
-
properties: {},
|
|
264
|
-
},
|
|
265
|
-
tags: ['github'],
|
|
266
|
-
execute: async (_args: Record<string, any>, commMessage?: Message<any>) => {
|
|
267
|
-
if (!commMessage?.$adapter || !commMessage?.$sender?.id) {
|
|
268
|
-
return '❌ 无法获取当前用户信息';
|
|
269
|
-
}
|
|
270
|
-
const db = plugin.root?.inject('database') as any;
|
|
271
|
-
const model = db?.models?.get('github_oauth_users');
|
|
272
|
-
if (!model) return '❌ 数据库未就绪';
|
|
273
|
-
|
|
274
|
-
const [existing] = await model.select().where({ platform: commMessage.$adapter, platform_uid: commMessage.$sender.id });
|
|
275
|
-
if (!existing) return '📭 你尚未绑定 GitHub 账号\n🔗 使用 github_bind 绑定你的账号';
|
|
276
|
-
|
|
277
|
-
// 验证 token 是否仍然有效
|
|
278
|
-
const userGh = new GhClient({ host: adapter.getHost(), token: existing.access_token });
|
|
279
|
-
const auth = await userGh.verifyAuth();
|
|
280
|
-
if (auth.ok) {
|
|
281
|
-
return `👤 已绑定 GitHub 账号: ${auth.user}\n📅 绑定时间: ${new Date(existing.created_at).toLocaleString('zh-CN')}`;
|
|
282
|
-
}
|
|
283
|
-
return `⚠️ 已绑定账号 ${existing.github_login},但 Token 已失效\n🔗 请执行 github_unbind 后重新 github_bind`;
|
|
284
|
-
},
|
|
285
|
-
},
|
|
286
|
-
// --- Install App ---
|
|
287
|
-
{
|
|
288
|
-
name: 'github_install',
|
|
289
|
-
description: '获取安装 GitHub App 的链接 — 安装后 Endpoint 可以访问你的仓库,你也可以使用更多功能',
|
|
290
|
-
parameters: {
|
|
291
|
-
type: 'object' as const,
|
|
292
|
-
properties: {},
|
|
293
|
-
},
|
|
294
|
-
tags: ['github'],
|
|
295
|
-
execute: async () => {
|
|
296
|
-
const slug = adapter.getAppSlug();
|
|
297
|
-
if (!slug) return '❌ Endpoint 未配置 GitHub App';
|
|
298
|
-
const host = adapter.getHost() || 'github.com';
|
|
299
|
-
const installations = adapter.getInstallations();
|
|
300
|
-
let msg = `🔗 请点击以下链接安装 GitHub App 到你的仓库:\n https://${host}/apps/${slug}/installations/new`;
|
|
301
|
-
if (installations.length) {
|
|
302
|
-
msg += `\n\n📋 当前已安装 (${installations.length}):`;
|
|
303
|
-
for (const inst of installations) {
|
|
304
|
-
msg += `\n • ${inst.account.login} (${inst.account.type})`;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
return msg;
|
|
308
|
-
},
|
|
309
|
-
},
|
|
310
|
-
// --- Subscribe ---
|
|
311
|
-
{
|
|
312
|
-
name: 'github_subscribe',
|
|
313
|
-
description: '订阅 GitHub 仓库的 Webhook 事件,事件将推送到当前聊天通道',
|
|
314
|
-
parameters: {
|
|
315
|
-
type: 'object' as const,
|
|
316
|
-
properties: {
|
|
317
|
-
repo: { type: 'string' as const, description: 'owner/repo (必填)' },
|
|
318
|
-
events: { type: 'string' as const, description: '要订阅的事件类型,逗号分隔 (push/issue/star/fork/unstar/pull_request),默认全部' },
|
|
319
|
-
},
|
|
320
|
-
required: ['repo'],
|
|
321
|
-
},
|
|
322
|
-
platforms: ['github'],
|
|
323
|
-
tags: ['github'],
|
|
324
|
-
execute: async (args: Record<string, any>, commMessage?: Message<any>) => {
|
|
325
|
-
if (!commMessage?.$adapter || !commMessage?.$sender.id || !commMessage?.$channel?.id || !commMessage?.$endpoint) {
|
|
326
|
-
return '❌ 无法获取当前聊天通道信息';
|
|
327
|
-
}
|
|
328
|
-
const db = plugin.root?.inject('database') as any;
|
|
329
|
-
const model = db?.models?.get('github_subscriptions');
|
|
330
|
-
if (!model) return '❌ 数据库未就绪';
|
|
331
|
-
|
|
332
|
-
const validEvents: EventType[] = ['push', 'issue', 'star', 'fork', 'unstar', 'pull_request'];
|
|
333
|
-
const events: EventType[] = args.events
|
|
334
|
-
? args.events.split(',').map((s: string) => s.trim()).filter((e: string) => validEvents.includes(e as EventType))
|
|
335
|
-
: validEvents;
|
|
336
|
-
if (!events.length) return `❌ 无效的事件类型,可选: ${validEvents.join(', ')}`;
|
|
337
|
-
|
|
338
|
-
const [existing] = await model.select().where({
|
|
339
|
-
repo: args.repo,
|
|
340
|
-
target_id: commMessage.$channel?.id,
|
|
341
|
-
adapter: commMessage.$adapter,
|
|
342
|
-
endpoint: commMessage.$endpoint,
|
|
343
|
-
});
|
|
344
|
-
if (existing) {
|
|
345
|
-
await model.update({ events, target_type: commMessage.$channel?.type || 'private' }).where({ id: existing.id });
|
|
346
|
-
return `✅ 已更新订阅 ${args.repo}\n📡 事件: ${events.join(', ')}`;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
await model.insert({
|
|
350
|
-
id: Date.now(),
|
|
351
|
-
repo: args.repo,
|
|
352
|
-
events,
|
|
353
|
-
target_id: commMessage.$channel?.id,
|
|
354
|
-
target_type: commMessage.$channel?.type || 'private',
|
|
355
|
-
adapter: commMessage.$adapter,
|
|
356
|
-
endpoint: commMessage.$endpoint,
|
|
357
|
-
});
|
|
358
|
-
return `✅ 已订阅 ${args.repo}\n📡 事件: ${events.join(', ')}\n📌 通知将推送到当前通道`;
|
|
359
|
-
},
|
|
360
|
-
},
|
|
361
|
-
// --- Unsubscribe ---
|
|
362
|
-
{
|
|
363
|
-
name: 'github_unsubscribe',
|
|
364
|
-
description: '取消订阅 GitHub 仓库的 Webhook 事件',
|
|
365
|
-
parameters: {
|
|
366
|
-
type: 'object' as const,
|
|
367
|
-
properties: {
|
|
368
|
-
repo: { type: 'string' as const, description: 'owner/repo (必填)' },
|
|
369
|
-
},
|
|
370
|
-
required: ['repo'],
|
|
371
|
-
},
|
|
372
|
-
platforms: ['github'],
|
|
373
|
-
tags: ['github'],
|
|
374
|
-
execute: async (args: Record<string, any>, commMessage?: Message<any>) => {
|
|
375
|
-
if (!commMessage?.$adapter || !commMessage?.$channel?.id || !commMessage?.$endpoint) {
|
|
376
|
-
return '❌ 无法获取当前聊天通道信息';
|
|
377
|
-
}
|
|
378
|
-
const db = plugin.root?.inject('database') as any;
|
|
379
|
-
const model = db?.models?.get('github_subscriptions');
|
|
380
|
-
if (!model) return '❌ 数据库未就绪';
|
|
381
|
-
|
|
382
|
-
const [existing] = await model.select().where({
|
|
383
|
-
repo: args.repo,
|
|
384
|
-
target_id: commMessage.$channel?.id,
|
|
385
|
-
adapter: commMessage.$adapter,
|
|
386
|
-
endpoint: commMessage.$endpoint,
|
|
387
|
-
});
|
|
388
|
-
if (!existing) return `📭 当前通道未订阅 ${args.repo}`;
|
|
389
|
-
|
|
390
|
-
await model.delete().where({ id: existing.id });
|
|
391
|
-
return `✅ 已取消订阅 ${args.repo}`;
|
|
392
|
-
},
|
|
393
|
-
},
|
|
394
|
-
// --- Subscriptions ---
|
|
395
|
-
{
|
|
396
|
-
name: 'github_subscriptions',
|
|
397
|
-
description: '查看当前聊天通道的 GitHub 仓库订阅列表',
|
|
398
|
-
parameters: {
|
|
399
|
-
type: 'object' as const,
|
|
400
|
-
properties: {},
|
|
401
|
-
},
|
|
402
|
-
platforms: ['github'],
|
|
403
|
-
tags: ['github'],
|
|
404
|
-
execute: async (_args: Record<string, any>, commMessage?: Message<any>) => {
|
|
405
|
-
if (!commMessage?.$adapter || !commMessage?.$channel?.id || !commMessage?.$endpoint) {
|
|
406
|
-
return '❌ 无法获取当前聊天通道信息';
|
|
407
|
-
}
|
|
408
|
-
const db = plugin.root?.inject('database') as any;
|
|
409
|
-
const model = db?.models?.get('github_subscriptions');
|
|
410
|
-
if (!model) return '❌ 数据库未就绪';
|
|
411
|
-
|
|
412
|
-
const subs = await model.select().where({
|
|
413
|
-
target_id: commMessage.$channel?.id,
|
|
414
|
-
adapter: commMessage.$adapter,
|
|
415
|
-
endpoint: commMessage.$endpoint,
|
|
416
|
-
});
|
|
417
|
-
if (!subs?.length) return '📭 当前通道没有任何 GitHub 订阅';
|
|
418
|
-
|
|
419
|
-
return `📋 当前通道订阅 (${subs.length}):\n\n` +
|
|
420
|
-
subs.map((s: any) => {
|
|
421
|
-
const events = Array.isArray(s.events) ? s.events : [];
|
|
422
|
-
return ` 📦 ${s.repo}\n 📡 ${events.join(', ') || '(无事件)'}`;
|
|
423
|
-
}).join('\n\n');
|
|
424
|
-
},
|
|
425
|
-
},
|
|
426
|
-
];
|
|
427
|
-
|
|
428
|
-
const disposers = tools.map(t => toolService.addTool(t, plugin.name));
|
|
429
|
-
logger.debug(`GitHub 工具已注册: ${tools.map(t => t.name).join(', ')}`);
|
|
430
|
-
|
|
431
|
-
return () => disposers.forEach(d => d());
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
logger.debug('GitHub 适配器已加载 (gh CLI 认证)');
|
|
23
|
+
export { GhClient } from './gh-client.js';
|
|
24
|
+
export { GithubClient, githubClient, type GithubClientEventMap } from './client.js';
|
|
25
|
+
export { WorkspaceManager } from './workspace-manager.js';
|
|
26
|
+
export {
|
|
27
|
+
parseMessageChannel,
|
|
28
|
+
issueBranchName,
|
|
29
|
+
resolveWorkspaceBranch,
|
|
30
|
+
formatChannelContext,
|
|
31
|
+
} from './github-channel-context.js';
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
GithubEndpoint,
|
|
35
|
+
defaultCreateClient,
|
|
36
|
+
type GithubEndpointOptions,
|
|
37
|
+
} from './endpoint.js';
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
GITHUB_OAUTH_USERS_TABLE,
|
|
41
|
+
GITHUB_OAUTH_USERS_SCHEMA,
|
|
42
|
+
defineGithubOauthUsersTable,
|
|
43
|
+
lookupGithubOauthAccessToken,
|
|
44
|
+
} from './oauth-users.js';
|
|
45
|
+
|
|
46
|
+
export {
|
|
47
|
+
registerGithubWebhookRoutes,
|
|
48
|
+
handleGithubWebhookRequest,
|
|
49
|
+
dispatchGithubWebhookPayload,
|
|
50
|
+
type GithubWebhookHandler,
|
|
51
|
+
} 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, PluginDatabaseHost } from 'zhin.js';
|
|
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 | PluginDatabaseHost | 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
|
+
}
|