@zhin.js/adapter-github 0.1.28 → 0.1.31

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/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * GitHub 适配器入口:类型扩展、模型、导出、注册
3
3
  */
4
- import { usePlugin, type Plugin, type Context } from 'zhin.js';
4
+ import { usePlugin, type Plugin, type Context, type ToolFeature, type Tool } from 'zhin.js';
5
5
  import { GitHubAdapter } from './adapter.js';
6
6
 
7
7
  declare module 'zhin.js' {
@@ -87,4 +87,478 @@ useContext('router', 'github', (router, adapter) => {
87
87
  adapter.setupOAuth(router);
88
88
  });
89
89
 
90
+ // ── Tool 工具注册 ─────────────────────────────────────────────────────────
91
+ useContext('tool', 'github', (toolService: ToolFeature, adapter: GitHubAdapter) => {
92
+ const tools: Tool[] = [
93
+ // --- PR ---
94
+ {
95
+ name: 'github_pr',
96
+ description: 'GitHub PR 操作:list/view/diff/merge/create/review/close',
97
+ parameters: {
98
+ type: 'object' as const,
99
+ properties: {
100
+ action: { type: 'string' as const, description: 'list|view|diff|merge|create|review|close', enum: ['list','view','diff','merge','create','review','close'] },
101
+ repo: { type: 'string' as const, description: 'owner/repo (必填)' },
102
+ number: { type: 'number' as const, description: 'PR 编号' },
103
+ title: { type: 'string' as const, description: 'PR 标题 (create)' },
104
+ body: { type: 'string' as const, description: 'PR 描述 / Review 评语' },
105
+ head: { type: 'string' as const, description: '源分支 (create)' },
106
+ base: { type: 'string' as const, description: '目标分支 (create,默认 main)' },
107
+ state: { type: 'string' as const, description: 'open/closed/all (list)' },
108
+ approve: { type: 'boolean' as const, description: 'review 时 approve' },
109
+ method: { type: 'string' as const, description: 'squash/merge/rebase (merge)' },
110
+ },
111
+ required: ['action', 'repo'],
112
+ },
113
+ platforms: ['github'],
114
+ tags: ['github'],
115
+ execute: async (args: Record<string, any>) => {
116
+ const api = adapter.getAPI();
117
+ if (!api) return '❌ 没有可用的 GitHub bot';
118
+ const { action, repo, number: num, title, body, head, base, state, approve, method } = args;
119
+ switch (action) {
120
+ case 'list': {
121
+ const r = await api.listPRs(repo, state || 'open');
122
+ if (!r.ok) return `❌ ${JSON.stringify(r.data)}`;
123
+ if (!r.data.length) return `📭 没有 ${state || 'open'} 状态的 PR`;
124
+ return r.data.map((p: any) =>
125
+ `#${p.number} ${p.draft ? '[Draft] ' : ''}${p.title}\n 👤 ${p.user.login} | 🌿 ${p.head.ref} → ${p.base.ref} | ${p.state}`
126
+ ).join('\n\n');
127
+ }
128
+ case 'view': {
129
+ if (!num) return '❌ 请提供 PR 编号';
130
+ const r = await api.getPR(repo, num);
131
+ if (!r.ok) return `❌ ${JSON.stringify(r.data)}`;
132
+ const p = r.data;
133
+ return [
134
+ `#${p.number} ${p.title}`,
135
+ `👤 ${p.user.login} | ${p.state} | 🌿 ${p.head.ref} → ${p.base.ref}`,
136
+ `📅 ${p.created_at?.split('T')[0]} | +${p.additions} -${p.deletions} (${p.changed_files} files)`,
137
+ p.body ? `\n${p.body.slice(0, 500)}${p.body.length > 500 ? '...' : ''}` : '',
138
+ `\n🔗 ${p.html_url}`,
139
+ ].filter(Boolean).join('\n');
140
+ }
141
+ case 'diff': {
142
+ if (!num) return '❌ 请提供 PR 编号';
143
+ const r = await api.getPRDiff(repo, num);
144
+ if (!r.ok) return '❌ 获取 diff 失败';
145
+ const lines = r.data.split('\n');
146
+ return lines.length > 100 ? lines.slice(0, 100).join('\n') + `\n\n... (共 ${lines.length} 行)` : r.data;
147
+ }
148
+ case 'merge': {
149
+ if (!num) return '❌ 请提供 PR 编号';
150
+ const r = await api.mergePR(repo, num, method || 'squash');
151
+ return r.ok ? `✅ PR #${num} 已合并` : `❌ ${r.data?.message || JSON.stringify(r.data)}`;
152
+ }
153
+ case 'create': {
154
+ if (!title) return '❌ 请提供 PR 标题';
155
+ if (!head) return '❌ 请提供源分支 (head)';
156
+ const r = await api.createPR(repo, title, body || '', head, base || 'main');
157
+ return r.ok ? `✅ PR 已创建: ${r.data.html_url}` : `❌ ${r.data?.message || JSON.stringify(r.data)}`;
158
+ }
159
+ case 'review': {
160
+ if (!num) return '❌ 请提供 PR 编号';
161
+ const event = approve ? 'APPROVE' : 'COMMENT';
162
+ const r = await api.createPRReview(repo, num, event as any, body || undefined);
163
+ return r.ok ? `✅ PR #${num} ${approve ? '已批准' : '已评论'}` : `❌ ${r.data?.message}`;
164
+ }
165
+ case 'close': {
166
+ if (!num) return '❌ 请提供 PR 编号';
167
+ const r = await api.closePR(repo, num);
168
+ return r.ok ? `✅ PR #${num} 已关闭` : `❌ ${r.data?.message}`;
169
+ }
170
+ default: return `❌ 未知操作: ${action}`;
171
+ }
172
+ },
173
+ },
174
+ // --- Issue ---
175
+ {
176
+ name: 'github_issue',
177
+ description: 'GitHub Issue 操作:list/view/create/close/comment',
178
+ parameters: {
179
+ type: 'object' as const,
180
+ properties: {
181
+ action: { type: 'string' as const, description: 'list|view|create|close|comment', enum: ['list','view','create','close','comment'] },
182
+ repo: { type: 'string' as const, description: 'owner/repo (必填)' },
183
+ number: { type: 'number' as const, description: 'Issue 编号' },
184
+ title: { type: 'string' as const, description: 'Issue 标题 (create)' },
185
+ body: { type: 'string' as const, description: 'Issue 内容 / 评论内容' },
186
+ labels: { type: 'string' as const, description: '标签,逗号分隔 (create)' },
187
+ state: { type: 'string' as const, description: 'open/closed/all (list)' },
188
+ },
189
+ required: ['action', 'repo'],
190
+ },
191
+ platforms: ['github'],
192
+ tags: ['github'],
193
+ execute: async (args: Record<string, any>) => {
194
+ const api = adapter.getAPI();
195
+ if (!api) return '❌ 没有可用的 GitHub bot';
196
+ const { action, repo, number: num, title, body, labels, state } = args;
197
+ switch (action) {
198
+ case 'list': {
199
+ const r = await api.listIssues(repo, state || 'open');
200
+ if (!r.ok) return `❌ ${JSON.stringify(r.data)}`;
201
+ const issues = r.data.filter((i: any) => !i.pull_request);
202
+ if (!issues.length) return `📭 没有 ${state || 'open'} 状态的 Issue`;
203
+ return issues.map((i: any) => {
204
+ const lbls = i.labels?.map((l: any) => l.name).join(', ') || '';
205
+ return `#${i.number} ${i.title}\n 👤 ${i.user.login}${lbls ? ` | 🏷️ ${lbls}` : ''} | ${i.state}`;
206
+ }).join('\n\n');
207
+ }
208
+ case 'view': {
209
+ if (!num) return '❌ 请提供 Issue 编号';
210
+ const r = await api.getIssue(repo, num);
211
+ if (!r.ok) return `❌ ${JSON.stringify(r.data)}`;
212
+ const i = r.data;
213
+ return [
214
+ `#${i.number} ${i.title}`,
215
+ `👤 ${i.user.login} | ${i.state} | 📅 ${i.created_at?.split('T')[0]}`,
216
+ i.labels?.length ? `🏷️ ${i.labels.map((l: any) => l.name).join(', ')}` : null,
217
+ i.body ? `\n${i.body.slice(0, 500)}${i.body.length > 500 ? '...' : ''}` : '',
218
+ `\n🔗 ${i.html_url}`,
219
+ ].filter(Boolean).join('\n');
220
+ }
221
+ case 'create': {
222
+ if (!title) return '❌ 请提供 Issue 标题';
223
+ const labelArr = labels ? labels.split(',').map((s: string) => s.trim()) : undefined;
224
+ const r = await api.createIssue(repo, title, body || undefined, labelArr);
225
+ return r.ok ? `✅ Issue 已创建: ${r.data.html_url}` : `❌ ${r.data?.message}`;
226
+ }
227
+ case 'close': {
228
+ if (!num) return '❌ 请提供 Issue 编号';
229
+ const r = await api.closeIssue(repo, num);
230
+ return r.ok ? `✅ Issue #${num} 已关闭` : `❌ ${r.data?.message}`;
231
+ }
232
+ case 'comment': {
233
+ if (!num) return '❌ 请提供 Issue 编号';
234
+ if (!body) return '❌ 请提供评论内容';
235
+ const r = await api.createIssueComment(repo, num, body);
236
+ return r.ok ? `✅ 已评论 Issue #${num}` : `❌ ${JSON.stringify(r.data)}`;
237
+ }
238
+ default: return `❌ 未知操作: ${action}`;
239
+ }
240
+ },
241
+ },
242
+ // --- Repo ---
243
+ {
244
+ name: 'github_repo',
245
+ description: 'GitHub 仓库查询:info/branches/releases/runs(CI)/stars',
246
+ parameters: {
247
+ type: 'object' as const,
248
+ properties: {
249
+ action: { type: 'string' as const, description: 'info|branches|releases|runs|stars', enum: ['info','branches','releases','runs','stars'] },
250
+ repo: { type: 'string' as const, description: 'owner/repo (必填)' },
251
+ limit: { type: 'number' as const, description: '返回数量,默认 10' },
252
+ },
253
+ required: ['action', 'repo'],
254
+ },
255
+ platforms: ['github'],
256
+ tags: ['github'],
257
+ execute: async (args: Record<string, any>) => {
258
+ const api = adapter.getAPI();
259
+ if (!api) return '❌ 没有可用的 GitHub bot';
260
+ const { action, repo, limit: lim } = args;
261
+ const limit = lim || 10;
262
+ switch (action) {
263
+ case 'info': {
264
+ const r = await api.getRepo(repo);
265
+ if (!r.ok) return `❌ ${JSON.stringify(r.data)}`;
266
+ const d = r.data;
267
+ return [
268
+ `📦 ${d.full_name}${d.private ? ' 🔒' : ''}`,
269
+ d.description ? `📝 ${d.description}` : null,
270
+ `⭐ ${d.stargazers_count} | 🍴 ${d.forks_count} | 👀 ${d.watchers_count}`,
271
+ `🌿 默认分支: ${d.default_branch}`,
272
+ d.license ? `📄 ${d.license.name}` : null,
273
+ d.homepage ? `🌐 ${d.homepage}` : null,
274
+ `📅 创建: ${d.created_at?.split('T')[0]} | 推送: ${d.pushed_at?.split('T')[0]}`,
275
+ ].filter(Boolean).join('\n');
276
+ }
277
+ case 'branches': {
278
+ const r = await api.listBranches(repo, limit);
279
+ if (!r.ok) return `❌ ${JSON.stringify(r.data)}`;
280
+ return r.data.length
281
+ ? `🌿 分支 (${r.data.length}):\n${r.data.map((b: any) => ` • ${b.name}${b.protected ? ' 🔒' : ''}`).join('\n')}`
282
+ : '没有找到分支';
283
+ }
284
+ case 'releases': {
285
+ const r = await api.listReleases(repo, limit);
286
+ if (!r.ok) return `❌ ${JSON.stringify(r.data)}`;
287
+ if (!r.data.length) return '📭 暂无发布';
288
+ return r.data.map((rel: any) =>
289
+ `${rel.prerelease ? '🧪' : '📦'} ${rel.tag_name} — ${rel.name || '(no title)'}\n 📅 ${rel.published_at?.split('T')[0]} | 👤 ${rel.author?.login}`
290
+ ).join('\n\n');
291
+ }
292
+ case 'runs': {
293
+ const r = await api.listWorkflowRuns(repo, limit);
294
+ if (!r.ok) return `❌ ${JSON.stringify(r.data)}`;
295
+ const runs = r.data.workflow_runs || [];
296
+ if (!runs.length) return '📭 暂无 CI 记录';
297
+ return runs.map((run: any) => {
298
+ const icon = run.conclusion === 'success' ? '✅' : run.conclusion === 'failure' ? '❌' : run.status === 'in_progress' ? '🔄' : '⏳';
299
+ return `${icon} #${run.id} ${run.display_title}\n 🌿 ${run.head_branch} | ${run.status}${run.conclusion ? '/' + run.conclusion : ''}`;
300
+ }).join('\n\n');
301
+ }
302
+ case 'stars': {
303
+ const r = await api.getRepo(repo);
304
+ if (!r.ok) return `❌ ${JSON.stringify(r.data)}`;
305
+ return `⭐ ${r.data.stargazers_count} stars | 🍴 ${r.data.forks_count} forks`;
306
+ }
307
+ default: return `❌ 未知查询: ${action}`;
308
+ }
309
+ },
310
+ },
311
+ // --- Search ---
312
+ {
313
+ name: 'github_search',
314
+ description: 'GitHub 搜索:在 issues/repos/code 中搜索',
315
+ parameters: {
316
+ type: 'object' as const,
317
+ properties: {
318
+ action: { type: 'string' as const, description: 'issues|repos|code', enum: ['issues', 'repos', 'code'] },
319
+ query: { type: 'string' as const, description: '搜索关键词' },
320
+ limit: { type: 'number' as const, description: '返回数量,默认 10' },
321
+ },
322
+ required: ['action', 'query'],
323
+ },
324
+ platforms: ['github'],
325
+ tags: ['github'],
326
+ execute: async (args: Record<string, any>) => {
327
+ const api = adapter.getAPI();
328
+ if (!api) return '❌ 没有可用的 GitHub bot';
329
+ const { action, query: q, limit: lim } = args;
330
+ const limit = lim || 10;
331
+ switch (action) {
332
+ case 'issues': {
333
+ const r = await api.searchIssues(q, limit);
334
+ if (!r.ok) return `❌ ${JSON.stringify(r.data)}`;
335
+ if (!r.data.items.length) return '📭 没有匹配的 Issue/PR';
336
+ return `🔍 共 ${r.data.total_count} 条,显示前 ${r.data.items.length}:\n\n` +
337
+ r.data.items.map((i: any) =>
338
+ `${i.pull_request ? '🔀' : '🐛'} ${i.repository_url.replace('https://api.github.com/repos/', '')}#${i.number}\n ${i.title}\n 👤 ${i.user.login} | ${i.state}`
339
+ ).join('\n\n');
340
+ }
341
+ case 'repos': {
342
+ const r = await api.searchRepos(q, limit);
343
+ if (!r.ok) return `❌ ${JSON.stringify(r.data)}`;
344
+ if (!r.data.items.length) return '📭 没有匹配的仓库';
345
+ return `🔍 共 ${r.data.total_count} 条,显示前 ${r.data.items.length}:\n\n` +
346
+ r.data.items.map((repo: any) =>
347
+ `📦 ${repo.full_name}${repo.private ? ' 🔒' : ''}\n ${repo.description || '(无描述)'}\n ⭐ ${repo.stargazers_count} | 🍴 ${repo.forks_count} | 📝 ${repo.language || '?'}`
348
+ ).join('\n\n');
349
+ }
350
+ case 'code': {
351
+ const r = await api.searchCode(q, limit);
352
+ if (!r.ok) return `❌ ${JSON.stringify(r.data)}`;
353
+ if (!r.data.items.length) return '📭 没有匹配的代码';
354
+ return `🔍 共 ${r.data.total_count} 条,显示前 ${r.data.items.length}:\n\n` +
355
+ r.data.items.map((c: any) =>
356
+ `📄 ${c.repository.full_name}/${c.path}\n 🔗 ${c.html_url}`
357
+ ).join('\n\n');
358
+ }
359
+ default: return `❌ 未知搜索类型: ${action}`;
360
+ }
361
+ },
362
+ },
363
+ // --- Label ---
364
+ {
365
+ name: 'github_label',
366
+ description: 'GitHub 标签管理:查看/添加/移除 Issue/PR 标签',
367
+ parameters: {
368
+ type: 'object' as const,
369
+ properties: {
370
+ action: { type: 'string' as const, description: 'list|add|remove', enum: ['list', 'add', 'remove'] },
371
+ repo: { type: 'string' as const, description: 'owner/repo (必填)' },
372
+ number: { type: 'number' as const, description: 'Issue/PR 编号 (add/remove 必填)' },
373
+ labels: { type: 'string' as const, description: '标签名,逗号分隔 (add/remove)' },
374
+ },
375
+ required: ['action', 'repo'],
376
+ },
377
+ platforms: ['github'],
378
+ tags: ['github'],
379
+ execute: async (args: Record<string, any>) => {
380
+ const api = adapter.getAPI();
381
+ if (!api) return '❌ 没有可用的 GitHub bot';
382
+ const { action, repo, number: num, labels } = args;
383
+ switch (action) {
384
+ case 'list': {
385
+ const r = await api.listLabels(repo);
386
+ if (!r.ok) return `❌ ${JSON.stringify(r.data)}`;
387
+ if (!r.data.length) return '📭 仓库没有标签';
388
+ return `🏷️ ${repo} 标签 (${r.data.length}):\n` +
389
+ r.data.map((l: any) => ` • ${l.name}${l.description ? ` — ${l.description}` : ''}`).join('\n');
390
+ }
391
+ case 'add': {
392
+ if (!num) return '❌ 请提供 Issue/PR 编号';
393
+ if (!labels) return '❌ 请提供标签名';
394
+ const labelArr = labels.split(',').map((s: string) => s.trim());
395
+ const r = await api.addLabels(repo, num, labelArr);
396
+ return r.ok ? `✅ 已添加标签: ${labelArr.join(', ')}` : `❌ ${JSON.stringify(r.data)}`;
397
+ }
398
+ case 'remove': {
399
+ if (!num) return '❌ 请提供 Issue/PR 编号';
400
+ if (!labels) return '❌ 请提供要移除的标签名';
401
+ const labelArr = labels.split(',').map((s: string) => s.trim());
402
+ const results: string[] = [];
403
+ for (const label of labelArr) {
404
+ const r = await api.removeLabel(repo, num, label);
405
+ results.push(r.ok ? `✅ ${label}` : `❌ ${label}: ${r.data?.message || 'failed'}`);
406
+ }
407
+ return results.join('\n');
408
+ }
409
+ default: return `❌ 未知操作: ${action}`;
410
+ }
411
+ },
412
+ },
413
+ // --- Assign ---
414
+ {
415
+ name: 'github_assign',
416
+ description: 'GitHub 指派管理:给 Issue/PR 添加/移除指派人',
417
+ parameters: {
418
+ type: 'object' as const,
419
+ properties: {
420
+ action: { type: 'string' as const, description: 'add|remove', enum: ['add', 'remove'] },
421
+ repo: { type: 'string' as const, description: 'owner/repo (必填)' },
422
+ number: { type: 'number' as const, description: 'Issue/PR 编号 (必填)' },
423
+ assignees: { type: 'string' as const, description: '用户名,逗号分隔 (必填)' },
424
+ },
425
+ required: ['action', 'repo', 'number', 'assignees'],
426
+ },
427
+ platforms: ['github'],
428
+ tags: ['github'],
429
+ execute: async (args: Record<string, any>) => {
430
+ const api = adapter.getAPI();
431
+ if (!api) return '❌ 没有可用的 GitHub bot';
432
+ const { action, repo, number: num, assignees } = args;
433
+ const assigneeArr = assignees.split(',').map((s: string) => s.trim());
434
+ if (action === 'add') {
435
+ const r = await api.addAssignees(repo, num, assigneeArr);
436
+ return r.ok ? `✅ 已指派: ${assigneeArr.join(', ')}` : `❌ ${r.data?.message || JSON.stringify(r.data)}`;
437
+ } else {
438
+ const r = await api.removeAssignees(repo, num, assigneeArr);
439
+ return r.ok ? `✅ 已移除指派: ${assigneeArr.join(', ')}` : `❌ ${r.data?.message || JSON.stringify(r.data)}`;
440
+ }
441
+ },
442
+ },
443
+ // --- File ---
444
+ {
445
+ name: 'github_file',
446
+ description: '读取 GitHub 仓库中的文件内容',
447
+ parameters: {
448
+ type: 'object' as const,
449
+ properties: {
450
+ repo: { type: 'string' as const, description: 'owner/repo (必填)' },
451
+ path: { type: 'string' as const, description: '文件路径 (必填)' },
452
+ ref: { type: 'string' as const, description: '分支/tag/commit SHA (可选,默认主分支)' },
453
+ },
454
+ required: ['repo', 'path'],
455
+ },
456
+ platforms: ['github'],
457
+ tags: ['github'],
458
+ execute: async (args: Record<string, any>) => {
459
+ const api = adapter.getAPI();
460
+ if (!api) return '❌ 没有可用的 GitHub bot';
461
+ const r = await api.getFileContent(args.repo, args.path, args.ref);
462
+ if (!r.ok) return `❌ ${r.data?.message || JSON.stringify(r.data)}`;
463
+ if (Array.isArray(r.data)) {
464
+ return `📂 ${args.path} (目录,${r.data.length} 项):\n` +
465
+ r.data.map((f: any) => ` ${f.type === 'dir' ? '📁' : '📄'} ${f.name}`).join('\n');
466
+ }
467
+ if (r.data.type === 'file' && r.data.content) {
468
+ const decoded = Buffer.from(r.data.content, 'base64').toString('utf-8');
469
+ const maxLen = 3000;
470
+ const truncated = decoded.length > maxLen;
471
+ return `📄 ${r.data.path} (${r.data.size} bytes)\n\n${decoded.slice(0, maxLen)}${truncated ? `\n\n... (截断,共 ${decoded.length} 字符)` : ''}`;
472
+ }
473
+ return `📄 ${r.data.path} — ${r.data.type} (${r.data.size} bytes)\n🔗 ${r.data.html_url}`;
474
+ },
475
+ },
476
+ // --- Commits ---
477
+ {
478
+ name: 'github_commits',
479
+ description: 'GitHub 提交查询:列出提交记录或对比两个分支',
480
+ parameters: {
481
+ type: 'object' as const,
482
+ properties: {
483
+ action: { type: 'string' as const, description: 'list|compare', enum: ['list', 'compare'] },
484
+ repo: { type: 'string' as const, description: 'owner/repo (必填)' },
485
+ sha: { type: 'string' as const, description: '分支/SHA (list)' },
486
+ path: { type: 'string' as const, description: '按文件路径过滤 (list)' },
487
+ base: { type: 'string' as const, description: '基准分支 (compare)' },
488
+ head: { type: 'string' as const, description: '目标分支 (compare)' },
489
+ limit: { type: 'number' as const, description: '返回数量,默认 10' },
490
+ },
491
+ required: ['action', 'repo'],
492
+ },
493
+ platforms: ['github'],
494
+ tags: ['github'],
495
+ execute: async (args: Record<string, any>) => {
496
+ const api = adapter.getAPI();
497
+ if (!api) return '❌ 没有可用的 GitHub bot';
498
+ const { action, repo, sha, path, base, head, limit: lim } = args;
499
+ if (action === 'list') {
500
+ const r = await api.listCommits(repo, sha, path, lim || 10);
501
+ if (!r.ok) return `❌ ${JSON.stringify(r.data)}`;
502
+ if (!r.data.length) return '📭 没有找到提交记录';
503
+ return r.data.map((c: any) =>
504
+ `• ${c.sha.substring(0, 7)} ${c.commit.message.split('\n')[0]}\n 👤 ${c.commit.author?.name || '?'} | 📅 ${c.commit.author?.date?.split('T')[0] || '?'}`
505
+ ).join('\n\n');
506
+ } else {
507
+ if (!base || !head) return '❌ compare 需要 base 和 head 参数';
508
+ const r = await api.compareCommits(repo, base, head);
509
+ if (!r.ok) return `❌ ${r.data?.message || JSON.stringify(r.data)}`;
510
+ const d = r.data;
511
+ return [
512
+ `🔀 ${base} ← ${head}`,
513
+ `📊 ${d.status} | ${d.ahead_by} ahead, ${d.behind_by} behind`,
514
+ `📝 ${d.total_commits} commits | ${d.files?.length || 0} files changed`,
515
+ d.commits?.length ? '\n最近提交:\n' + d.commits.slice(0, 5).map((c: any) =>
516
+ ` • ${c.sha.substring(0, 7)} ${c.commit.message.split('\n')[0]}`
517
+ ).join('\n') : '',
518
+ ].filter(Boolean).join('\n');
519
+ }
520
+ },
521
+ },
522
+ // --- Edit (Issue/PR) ---
523
+ {
524
+ name: 'github_edit',
525
+ description: '编辑 GitHub Issue 或 PR 的标题、正文、状态',
526
+ parameters: {
527
+ type: 'object' as const,
528
+ properties: {
529
+ type: { type: 'string' as const, description: 'issue|pr', enum: ['issue', 'pr'] },
530
+ repo: { type: 'string' as const, description: 'owner/repo (必填)' },
531
+ number: { type: 'number' as const, description: 'Issue/PR 编号 (必填)' },
532
+ title: { type: 'string' as const, description: '新标题' },
533
+ body: { type: 'string' as const, description: '新正文' },
534
+ state: { type: 'string' as const, description: 'open|closed' },
535
+ },
536
+ required: ['type', 'repo', 'number'],
537
+ },
538
+ platforms: ['github'],
539
+ tags: ['github'],
540
+ execute: async (args: Record<string, any>) => {
541
+ const api = adapter.getAPI();
542
+ if (!api) return '❌ 没有可用的 GitHub bot';
543
+ const { type: itemType, repo, number: num, title, body, state } = args;
544
+ const data: any = {};
545
+ if (title) data.title = title;
546
+ if (body) data.body = body;
547
+ if (state) data.state = state;
548
+ if (!Object.keys(data).length) return '❌ 请至少提供一个要修改的字段 (title/body/state)';
549
+ const r = itemType === 'pr'
550
+ ? await api.updatePR(repo, num, data)
551
+ : await api.updateIssue(repo, num, data);
552
+ if (!r.ok) return `❌ ${r.data?.message || JSON.stringify(r.data)}`;
553
+ return `✅ ${itemType === 'pr' ? 'PR' : 'Issue'} #${num} 已更新\n🔗 ${r.data.html_url}`;
554
+ },
555
+ },
556
+ ];
557
+
558
+ const disposers = tools.map(t => toolService.addTool(t, 'github'));
559
+ logger.debug(`GitHub 工具已注册: ${tools.map(t => t.name).join(', ')}`);
560
+
561
+ return () => disposers.forEach(d => d());
562
+ });
563
+
90
564
  logger.debug('GitHub 适配器已加载 (GitHub App 认证)');