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