@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/adapter.ts CHANGED
@@ -6,10 +6,9 @@ import {
6
6
  Adapter,
7
7
  Plugin,
8
8
  Message,
9
- ZhinTool,
10
9
  } from 'zhin.js';
11
10
  import { GitHubBot } from './bot.js';
12
- import type { GitHubBotConfig, EventType, GenericWebhookPayload, Subscription, PrAction, IssueAction, RepoAction } from './types.js';
11
+ import type { GitHubBotConfig, EventType, GenericWebhookPayload, Subscription } from './types.js';
13
12
  import { GitHubAPI, GitHubOAuthClient, exchangeOAuthCode } from './api.js';
14
13
  import type { IssueCommentPayload, PRReviewCommentPayload, PRReviewPayload } from './types.js';
15
14
 
@@ -29,36 +28,50 @@ const OAUTH_STATE_TTL = 5 * 60 * 1000;
29
28
  function formatNotification(event: string, p: GenericWebhookPayload): string {
30
29
  const repo = p.repository.full_name;
31
30
  const sender = p.sender.login;
31
+ const repoUrl = p.repository.html_url;
32
32
  switch (event) {
33
33
  case 'push': {
34
34
  const branch = p.ref?.replace('refs/heads/', '') || '?';
35
35
  const commits = p.commits || [];
36
- let msg = `๐Ÿ“ฆ ${repo}\n๐ŸŒฟ ${sender} pushed to ${branch}\n\n`;
36
+ const compareUrl = commits.length >= 2
37
+ ? `${repoUrl}/compare/${commits[0].id.substring(0, 12)}...${commits[commits.length - 1].id.substring(0, 12)}`
38
+ : commits.length === 1 ? `${repoUrl}/commit/${commits[0].id}` : '';
39
+ let msg = `๐Ÿ“ฆ ${repo}\n๐ŸŒฟ ${sender} pushed ${commits.length} commit(s) to \`${branch}\`\n`;
37
40
  if (commits.length) {
38
- msg += `๐Ÿ“ ${commits.length} commit(s):\n`;
39
- msg += commits.slice(0, 3).map(c => ` โ€ข ${c.id.substring(0, 7)} ${c.message.split('\n')[0]}`).join('\n');
40
- if (commits.length > 3) msg += `\n ... +${commits.length - 3} more`;
41
+ msg += '\n';
42
+ msg += commits.slice(0, 5).map(c =>
43
+ ` โ€ข [\`${c.id.substring(0, 7)}\`](${repoUrl}/commit/${c.id}) ${c.message.split('\n')[0]}`
44
+ ).join('\n');
45
+ if (commits.length > 5) msg += `\n ... +${commits.length - 5} more`;
41
46
  }
47
+ if (compareUrl) msg += `\n\n๐Ÿ”— ${compareUrl}`;
42
48
  return msg;
43
49
  }
44
50
  case 'issues': {
45
51
  const i = p.issue!;
46
- const act = p.action === 'opened' ? 'opened' : p.action === 'closed' ? 'closed' : 'updated';
47
- return `๐Ÿ› ${repo}\n๐Ÿ‘ค ${sender} ${act} issue #${i.number}\n๐Ÿ“Œ ${i.title}`;
52
+ const act = p.action === 'opened' ? '๐Ÿ“ opened' : p.action === 'closed' ? 'โœ… closed' : `๐Ÿ”„ ${p.action || 'updated'}`;
53
+ let msg = `๐Ÿ› ${repo}\n๐Ÿ‘ค ${sender} ${act} issue #${i.number}\n๐Ÿ“Œ ${i.title}`;
54
+ msg += `\n๐Ÿ”— ${i.html_url}`;
55
+ return msg;
48
56
  }
49
57
  case 'star': {
50
58
  const starred = p.action !== 'deleted';
51
- return `${starred ? 'โญ' : '๐Ÿ’”'} ${repo}\n๐Ÿ‘ค ${sender} ${starred ? 'starred' : 'unstarred'}`;
59
+ return `${starred ? 'โญ' : '๐Ÿ’”'} ${repo}\n๐Ÿ‘ค ${sender} ${starred ? 'starred' : 'unstarred'}\n๐Ÿ”— ${repoUrl}`;
52
60
  }
53
61
  case 'fork':
54
- return `๐Ÿด ${repo}\n๐Ÿ‘ค ${sender} forked โ†’ ${p.forkee!.full_name}`;
62
+ return `๐Ÿด ${repo}\n๐Ÿ‘ค ${sender} forked โ†’ ${p.forkee!.full_name}\n๐Ÿ”— ${p.forkee!.html_url}`;
55
63
  case 'pull_request': {
56
64
  const pr = p.pull_request!;
57
- const act = p.action === 'opened' ? 'opened' : p.action === 'closed' ? 'closed' : 'updated';
58
- return `๐Ÿ”€ ${repo}\n๐Ÿ‘ค ${sender} ${act} PR #${pr.number}\n๐Ÿ“Œ ${pr.title}`;
65
+ const act = p.action === 'opened' ? '๐Ÿ“ opened'
66
+ : p.action === 'closed' ? (pr.state === 'closed' ? 'โŒ closed' : 'โœ… merged')
67
+ : `๐Ÿ”„ ${p.action || 'updated'}`;
68
+ let msg = `๐Ÿ”€ ${repo}\n๐Ÿ‘ค ${sender} ${act} PR #${pr.number}\n๐Ÿ“Œ ${pr.title}`;
69
+ msg += `\n๐ŸŒฟ ${pr.head.ref} โ†’ ${pr.base.ref}`;
70
+ msg += `\n๐Ÿ”— ${pr.html_url}`;
71
+ return msg;
59
72
  }
60
73
  default:
61
- return `๐Ÿ“ฌ ${repo}\n${event} by ${sender}`;
74
+ return `๐Ÿ“ฌ ${repo}\n๐Ÿ“ก ${event}${p.action ? ` (${p.action})` : ''} by ${sender}\n๐Ÿ”— ${repoUrl}`;
62
75
  }
63
76
  }
64
77
 
@@ -77,12 +90,11 @@ export class GitHubAdapter extends Adapter<GitHubBot> {
77
90
  }
78
91
 
79
92
  async start(): Promise<void> {
80
- this.registerGitHubTools();
81
93
  await super.start();
82
94
  }
83
95
 
84
96
  /** ่Žทๅ–็ฌฌไธ€ไธชๅฏ็”จ bot ็š„ API (ๅทฅๅ…ท็”จ) */
85
- private getAPI(): GitHubAPI | null {
97
+ getAPI(): GitHubAPI | null {
86
98
  const bot = this.bots.values().next().value as GitHubBot | undefined;
87
99
  return bot?.api || null;
88
100
  }
@@ -294,684 +306,6 @@ export class GitHubAdapter extends Adapter<GitHubBot> {
294
306
  this.plugin.logger.debug('GitHub Webhook: POST /pub/github/webhook');
295
307
  }
296
308
 
297
- // โ”€โ”€ GitHub ็ฎก็†ๅทฅๅ…ท โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
298
-
299
- private registerGitHubTools(): void {
300
-
301
- // --- PR ---
302
- this.addTool(
303
- new ZhinTool('github_pr')
304
- .desc('GitHub PR ๆ“ไฝœ๏ผšlist/view/diff/merge/create/review/close')
305
- .keyword('pr', 'pull request', 'ๅˆๅนถ', 'merge', 'review', 'ๅฎกๆŸฅ', 'ๆ‹‰ๅ–่ฏทๆฑ‚')
306
- .tag('github', 'pr')
307
- .param('action', { type: 'string', description: 'list|view|diff|merge|create|review|close', enum: ['list','view','diff','merge','create','review','close'] }, true)
308
- .param('repo', { type: 'string', description: 'owner/repo (ๅฟ…ๅกซ)' }, true)
309
- .param('number', { type: 'number', description: 'PR ็ผ–ๅท' })
310
- .param('title', { type: 'string', description: 'PR ๆ ‡้ข˜ (create)' })
311
- .param('body', { type: 'string', description: 'PR ๆ่ฟฐ / Review ่ฏ„่ฏญ' })
312
- .param('head', { type: 'string', description: 'ๆบๅˆ†ๆ”ฏ (create)' })
313
- .param('base', { type: 'string', description: '็›ฎๆ ‡ๅˆ†ๆ”ฏ (create๏ผŒ้ป˜่ฎค main)' })
314
- .param('state', { type: 'string', description: 'open/closed/all (list)' })
315
- .param('approve', { type: 'boolean', description: 'review ๆ—ถ approve' })
316
- .param('method', { type: 'string', description: 'squash/merge/rebase (merge)' })
317
- .execute(async (args) => {
318
- const api = this.getAPI();
319
- if (!api) return 'โŒ ๆฒกๆœ‰ๅฏ็”จ็š„ GitHub bot';
320
- const action = args.action as PrAction;
321
- const repo = args.repo as string;
322
- const num = args.number as number | undefined;
323
-
324
- switch (action) {
325
- case 'list': {
326
- const r = await api.listPRs(repo, (args.state as string) || 'open');
327
- if (!r.ok) return `โŒ ${JSON.stringify(r.data)}`;
328
- if (!r.data.length) return `๐Ÿ“ญ ๆฒกๆœ‰ ${args.state || 'open'} ็Šถๆ€็š„ PR`;
329
- return r.data.map((p: any) =>
330
- `#${p.number} ${p.draft ? '[Draft] ' : ''}${p.title}\n ๐Ÿ‘ค ${p.user.login} | ๐ŸŒฟ ${p.head.ref} โ†’ ${p.base.ref} | ${p.state}`
331
- ).join('\n\n');
332
- }
333
- case 'view': {
334
- if (!num) return 'โŒ ่ฏทๆไพ› PR ็ผ–ๅท';
335
- const r = await api.getPR(repo, num);
336
- if (!r.ok) return `โŒ ${JSON.stringify(r.data)}`;
337
- const p = r.data;
338
- return [
339
- `#${p.number} ${p.title}`,
340
- `๐Ÿ‘ค ${p.user.login} | ${p.state} | ๐ŸŒฟ ${p.head.ref} โ†’ ${p.base.ref}`,
341
- `๐Ÿ“… ${p.created_at?.split('T')[0]} | +${p.additions} -${p.deletions} (${p.changed_files} files)`,
342
- p.body ? `\n${p.body.slice(0, 500)}${p.body.length > 500 ? '...' : ''}` : '',
343
- `\n๐Ÿ”— ${p.html_url}`,
344
- ].filter(Boolean).join('\n');
345
- }
346
- case 'diff': {
347
- if (!num) return 'โŒ ่ฏทๆไพ› PR ็ผ–ๅท';
348
- const r = await api.getPRDiff(repo, num);
349
- if (!r.ok) return `โŒ ่Žทๅ– diff ๅคฑ่ดฅ`;
350
- const lines = r.data.split('\n');
351
- return lines.length > 100 ? lines.slice(0, 100).join('\n') + `\n\n... (ๅ…ฑ ${lines.length} ่กŒ)` : r.data;
352
- }
353
- case 'merge': {
354
- if (!num) return 'โŒ ่ฏทๆไพ› PR ็ผ–ๅท';
355
- const r = await api.mergePR(repo, num, (args.method as string) || 'squash');
356
- return r.ok ? `โœ… PR #${num} ๅทฒๅˆๅนถ` : `โŒ ${r.data?.message || JSON.stringify(r.data)}`;
357
- }
358
- case 'create': {
359
- if (!args.title) return 'โŒ ่ฏทๆไพ› PR ๆ ‡้ข˜';
360
- if (!args.head) return 'โŒ ่ฏทๆไพ›ๆบๅˆ†ๆ”ฏ (head)';
361
- const r = await api.createPR(repo, args.title as string, (args.body as string) || '', args.head as string, (args.base as string) || 'main');
362
- return r.ok ? `โœ… PR ๅทฒๅˆ›ๅปบ: ${r.data.html_url}` : `โŒ ${r.data?.message || JSON.stringify(r.data)}`;
363
- }
364
- case 'review': {
365
- if (!num) return 'โŒ ่ฏทๆไพ› PR ็ผ–ๅท';
366
- const event = args.approve ? 'APPROVE' : 'COMMENT';
367
- const r = await api.createPRReview(repo, num, event as any, (args.body as string) || undefined);
368
- return r.ok ? `โœ… PR #${num} ${args.approve ? 'ๅทฒๆ‰นๅ‡†' : 'ๅทฒ่ฏ„่ฎบ'}` : `โŒ ${r.data?.message}`;
369
- }
370
- case 'close': {
371
- if (!num) return 'โŒ ่ฏทๆไพ› PR ็ผ–ๅท';
372
- const r = await api.closePR(repo, num);
373
- return r.ok ? `โœ… PR #${num} ๅทฒๅ…ณ้—ญ` : `โŒ ${r.data?.message}`;
374
- }
375
- default: return `โŒ ๆœช็Ÿฅๆ“ไฝœ: ${action}`;
376
- }
377
- }),
378
- );
379
-
380
- // --- Issue ---
381
- this.addTool(
382
- new ZhinTool('github_issue')
383
- .desc('GitHub Issue ๆ“ไฝœ๏ผšlist/view/create/close/comment')
384
- .keyword('issue', '้—ฎ้ข˜', 'bug', 'ๅˆ›ๅปบissue', 'ๅ…ณ้—ญissue')
385
- .tag('github', 'issue')
386
- .param('action', { type: 'string', description: 'list|view|create|close|comment', enum: ['list','view','create','close','comment'] }, true)
387
- .param('repo', { type: 'string', description: 'owner/repo (ๅฟ…ๅกซ)' }, true)
388
- .param('number', { type: 'number', description: 'Issue ็ผ–ๅท' })
389
- .param('title', { type: 'string', description: 'Issue ๆ ‡้ข˜ (create)' })
390
- .param('body', { type: 'string', description: 'Issue ๅ†…ๅฎน / ่ฏ„่ฎบๅ†…ๅฎน' })
391
- .param('labels', { type: 'string', description: 'ๆ ‡็ญพ๏ผŒ้€—ๅทๅˆ†้š” (create)' })
392
- .param('state', { type: 'string', description: 'open/closed/all (list)' })
393
- .execute(async (args) => {
394
- const api = this.getAPI();
395
- if (!api) return 'โŒ ๆฒกๆœ‰ๅฏ็”จ็š„ GitHub bot';
396
- const action = args.action as IssueAction;
397
- const repo = args.repo as string;
398
- const num = args.number as number | undefined;
399
-
400
- switch (action) {
401
- case 'list': {
402
- const r = await api.listIssues(repo, (args.state as string) || 'open');
403
- if (!r.ok) return `โŒ ${JSON.stringify(r.data)}`;
404
- // ่ฟ‡ๆปคๆމ PR (GitHub API ็š„ issues ๆŽฅๅฃไนŸ่ฟ”ๅ›ž PR)
405
- const issues = r.data.filter((i: any) => !i.pull_request);
406
- if (!issues.length) return `๐Ÿ“ญ ๆฒกๆœ‰ ${args.state || 'open'} ็Šถๆ€็š„ Issue`;
407
- return issues.map((i: any) => {
408
- const labels = i.labels?.map((l: any) => l.name).join(', ') || '';
409
- return `#${i.number} ${i.title}\n ๐Ÿ‘ค ${i.user.login}${labels ? ` | ๐Ÿท๏ธ ${labels}` : ''} | ${i.state}`;
410
- }).join('\n\n');
411
- }
412
- case 'view': {
413
- if (!num) return 'โŒ ่ฏทๆไพ› Issue ็ผ–ๅท';
414
- const r = await api.getIssue(repo, num);
415
- if (!r.ok) return `โŒ ${JSON.stringify(r.data)}`;
416
- const i = r.data;
417
- return [
418
- `#${i.number} ${i.title}`,
419
- `๐Ÿ‘ค ${i.user.login} | ${i.state} | ๐Ÿ“… ${i.created_at?.split('T')[0]}`,
420
- i.labels?.length ? `๐Ÿท๏ธ ${i.labels.map((l: any) => l.name).join(', ')}` : null,
421
- i.body ? `\n${i.body.slice(0, 500)}${i.body.length > 500 ? '...' : ''}` : '',
422
- `\n๐Ÿ”— ${i.html_url}`,
423
- ].filter(Boolean).join('\n');
424
- }
425
- case 'create': {
426
- if (!args.title) return 'โŒ ่ฏทๆไพ› Issue ๆ ‡้ข˜';
427
- const labels = args.labels ? (args.labels as string).split(',').map(s => s.trim()) : undefined;
428
- const r = await api.createIssue(repo, args.title as string, (args.body as string) || undefined, labels);
429
- return r.ok ? `โœ… Issue ๅทฒๅˆ›ๅปบ: ${r.data.html_url}` : `โŒ ${r.data?.message}`;
430
- }
431
- case 'close': {
432
- if (!num) return 'โŒ ่ฏทๆไพ› Issue ็ผ–ๅท';
433
- const r = await api.closeIssue(repo, num);
434
- return r.ok ? `โœ… Issue #${num} ๅทฒๅ…ณ้—ญ` : `โŒ ${r.data?.message}`;
435
- }
436
- case 'comment': {
437
- if (!num) return 'โŒ ่ฏทๆไพ› Issue ็ผ–ๅท';
438
- if (!args.body) return 'โŒ ่ฏทๆไพ›่ฏ„่ฎบๅ†…ๅฎน';
439
- const r = await api.createIssueComment(repo, num, args.body as string);
440
- return r.ok ? `โœ… ๅทฒ่ฏ„่ฎบ Issue #${num}` : `โŒ ${JSON.stringify(r.data)}`;
441
- }
442
- default: return `โŒ ๆœช็Ÿฅๆ“ไฝœ: ${action}`;
443
- }
444
- }),
445
- );
446
-
447
- // --- Repo ---
448
- this.addTool(
449
- new ZhinTool('github_repo')
450
- .desc('GitHub ไป“ๅบ“ๆŸฅ่ฏข๏ผšinfo/branches/releases/runs(CI)/stars')
451
- .keyword('ไป“ๅบ“', 'repo', 'star', 'ๅˆ†ๆ”ฏ', 'branch', 'release', 'ๅ‘ๅธƒ', 'CI', 'workflow')
452
- .tag('github', 'repo')
453
- .param('action', { type: 'string', description: 'info|branches|releases|runs|stars', enum: ['info','branches','releases','runs','stars'] }, true)
454
- .param('repo', { type: 'string', description: 'owner/repo (ๅฟ…ๅกซ)' }, true)
455
- .param('limit', { type: 'number', description: '่ฟ”ๅ›žๆ•ฐ้‡๏ผŒ้ป˜่ฎค 10' })
456
- .execute(async (args) => {
457
- const api = this.getAPI();
458
- if (!api) return 'โŒ ๆฒกๆœ‰ๅฏ็”จ็š„ GitHub bot';
459
- const action = args.action as RepoAction;
460
- const repo = args.repo as string;
461
- const limit = (args.limit as number) || 10;
462
-
463
- switch (action) {
464
- case 'info': {
465
- const r = await api.getRepo(repo);
466
- if (!r.ok) return `โŒ ${JSON.stringify(r.data)}`;
467
- const d = r.data;
468
- return [
469
- `๐Ÿ“ฆ ${d.full_name}${d.private ? ' ๐Ÿ”’' : ''}`,
470
- d.description ? `๐Ÿ“ ${d.description}` : null,
471
- `โญ ${d.stargazers_count} | ๐Ÿด ${d.forks_count} | ๐Ÿ‘€ ${d.watchers_count}`,
472
- `๐ŸŒฟ ้ป˜่ฎคๅˆ†ๆ”ฏ: ${d.default_branch}`,
473
- d.license ? `๐Ÿ“„ ${d.license.name}` : null,
474
- d.homepage ? `๐ŸŒ ${d.homepage}` : null,
475
- `๐Ÿ“… ๅˆ›ๅปบ: ${d.created_at?.split('T')[0]} | ๆŽจ้€: ${d.pushed_at?.split('T')[0]}`,
476
- ].filter(Boolean).join('\n');
477
- }
478
- case 'branches': {
479
- const r = await api.listBranches(repo, limit);
480
- if (!r.ok) return `โŒ ${JSON.stringify(r.data)}`;
481
- return r.data.length
482
- ? `๐ŸŒฟ ๅˆ†ๆ”ฏ (${r.data.length}):\n${r.data.map((b: any) => ` โ€ข ${b.name}${b.protected ? ' ๐Ÿ”’' : ''}`).join('\n')}`
483
- : 'ๆฒกๆœ‰ๆ‰พๅˆฐๅˆ†ๆ”ฏ';
484
- }
485
- case 'releases': {
486
- const r = await api.listReleases(repo, limit);
487
- if (!r.ok) return `โŒ ${JSON.stringify(r.data)}`;
488
- if (!r.data.length) return '๐Ÿ“ญ ๆš‚ๆ— ๅ‘ๅธƒ';
489
- return r.data.map((rel: any) =>
490
- `${rel.prerelease ? '๐Ÿงช' : '๐Ÿ“ฆ'} ${rel.tag_name} โ€” ${rel.name || '(no title)'}\n ๐Ÿ“… ${rel.published_at?.split('T')[0]} | ๐Ÿ‘ค ${rel.author?.login}`
491
- ).join('\n\n');
492
- }
493
- case 'runs': {
494
- const r = await api.listWorkflowRuns(repo, limit);
495
- if (!r.ok) return `โŒ ${JSON.stringify(r.data)}`;
496
- const runs = r.data.workflow_runs || [];
497
- if (!runs.length) return '๐Ÿ“ญ ๆš‚ๆ—  CI ่ฎฐๅฝ•';
498
- return runs.map((run: any) => {
499
- const icon = run.conclusion === 'success' ? 'โœ…' : run.conclusion === 'failure' ? 'โŒ' : run.status === 'in_progress' ? '๐Ÿ”„' : 'โณ';
500
- return `${icon} #${run.id} ${run.display_title}\n ๐ŸŒฟ ${run.head_branch} | ${run.status}${run.conclusion ? '/' + run.conclusion : ''}`;
501
- }).join('\n\n');
502
- }
503
- case 'stars': {
504
- const r = await api.getRepo(repo);
505
- if (!r.ok) return `โŒ ${JSON.stringify(r.data)}`;
506
- return `โญ ${r.data.stargazers_count} stars | ๐Ÿด ${r.data.forks_count} forks`;
507
- }
508
- default: return `โŒ ๆœช็ŸฅๆŸฅ่ฏข: ${action}`;
509
- }
510
- }),
511
- );
512
-
513
- // --- Subscribe ---
514
- this.addTool(
515
- new ZhinTool('github_subscribe')
516
- .desc('่ฎข้˜… GitHub ไป“ๅบ“ Webhook ไบ‹ไปถ้€š็Ÿฅ๏ผˆ่ทจๅนณๅฐๆŽจ้€ๅˆฐๅฝ“ๅ‰่Šๅคฉ๏ผ‰')
517
- .tag('github', 'subscription')
518
- .param('repo', { type: 'string', description: 'owner/repo' }, true)
519
- .param('events', { type: 'array', description: 'ไบ‹ไปถ: push, issue, star, fork, unstar, pr๏ผˆ็•™็ฉบ=ๅ…จ้ƒจ๏ผ‰' })
520
- .execute(async ({ repo, events: evts = [] }, ctx) => {
521
- if (!ctx?.message) return 'โŒ ๆ— ๆณ•่Žทๅ–ๆถˆๆฏไธŠไธ‹ๆ–‡';
522
- const msg = ctx.message as Message;
523
- const repoStr = repo as string;
524
- if (!repoStr.includes('/')) return 'โŒ ๆ ผๅผๅบ”ไธบ owner/repo';
525
-
526
- const parsed: EventType[] = [];
527
- for (const e of evts as string[]) {
528
- const n = e.toLowerCase();
529
- if (n === 'pr') parsed.push('pull_request');
530
- else if (VALID_EVENTS.includes(n as EventType)) parsed.push(n as EventType);
531
- else return `โŒ ไธๆ”ฏๆŒ: ${e}`;
532
- }
533
- const subEvents = parsed.length > 0 ? parsed : VALID_EVENTS;
534
- const db = this.plugin.root?.inject('database') as any;
535
- const model = db?.models?.get('github_subscriptions');
536
- if (!model) return 'โŒ ๆ•ฐๆฎๅบ“ๆœชๅฐฑ็ปช';
537
-
538
- const where = { repo: repoStr, target_id: msg.$channel.id, target_type: msg.$channel.type, adapter: msg.$adapter, bot: msg.$bot };
539
- const [existing] = await model.select().where(where);
540
- const eventsJson = JSON.stringify(subEvents);
541
- if (existing) {
542
- await model.update({ events: eventsJson }).where({ id: existing.id });
543
- return `โœ… ๅทฒๆ›ดๆ–ฐ่ฎข้˜… ${repoStr}\n๐Ÿ“ข ${subEvents.join(', ')}`;
544
- }
545
- const { repo: _r, ...rest } = where;
546
- await model.insert({ id: Date.now(), repo: repoStr, events: eventsJson, ...rest });
547
- return `โœ… ๅทฒ่ฎข้˜… ${repoStr}\n๐Ÿ“ข ${subEvents.join(', ')}\n๐Ÿ’ก ่ฎฐๅพ—ๅœจ GitHub App ๆˆ–ไป“ๅบ“ Settings โ†’ Webhooks ไธญ้…็ฝฎ Webhook`;
548
- }),
549
- );
550
-
551
- // --- Unsubscribe ---
552
- this.addTool(
553
- new ZhinTool('github_unsubscribe')
554
- .desc('ๅ–ๆถˆ่ฎข้˜… GitHub ไป“ๅบ“ไบ‹ไปถ้€š็Ÿฅ')
555
- .tag('github', 'subscription')
556
- .param('repo', { type: 'string', description: 'owner/repo' }, true)
557
- .execute(async ({ repo }, ctx) => {
558
- if (!ctx?.message) return 'โŒ ๆ— ๆณ•่Žทๅ–ๆถˆๆฏไธŠไธ‹ๆ–‡';
559
- const msg = ctx.message as Message;
560
- const db = this.plugin.root?.inject('database') as any;
561
- const model = db?.models?.get('github_subscriptions');
562
- if (!model) return 'โŒ ๆ•ฐๆฎๅบ“ๆœชๅฐฑ็ปช';
563
- const [sub] = await model.select().where({ repo: repo as string, target_id: msg.$channel.id, target_type: msg.$channel.type, adapter: msg.$adapter, bot: msg.$bot });
564
- if (!sub) return `โŒ ๆœชๆ‰พๅˆฐ่ฎข้˜…: ${repo}`;
565
- await model.delete({ id: sub.id });
566
- return `โœ… ๅทฒๅ–ๆถˆ่ฎข้˜… ${repo}`;
567
- }),
568
- );
569
-
570
- // --- List subscriptions ---
571
- this.addTool(
572
- new ZhinTool('github_subscriptions')
573
- .desc('ๆŸฅ็œ‹ๅฝ“ๅ‰่Šๅคฉ็š„ GitHub ไบ‹ไปถ่ฎข้˜…ๅˆ—่กจ')
574
- .tag('github', 'subscription')
575
- .execute(async (_args, ctx) => {
576
- if (!ctx?.message) return 'โŒ ๆ— ๆณ•่Žทๅ–ๆถˆๆฏไธŠไธ‹ๆ–‡';
577
- const msg = ctx.message as Message;
578
- const db = this.plugin.root?.inject('database') as any;
579
- const model = db?.models?.get('github_subscriptions');
580
- if (!model) return 'โŒ ๆ•ฐๆฎๅบ“ๆœชๅฐฑ็ปช';
581
- const subs = await model.select().where({ target_id: msg.$channel.id, target_type: msg.$channel.type, adapter: msg.$adapter, bot: msg.$bot });
582
- if (!subs?.length) return '๐Ÿ“ญ ๅฝ“ๅ‰ๆฒกๆœ‰่ฎข้˜…';
583
- return `๐Ÿ“‹ ่ฎข้˜… (${subs.length}):\n\n` + subs.map((s: any, i: number) => {
584
- return `${i + 1}. ${s.repo}\n ๐Ÿ“ข ${safeParseEvents(s.events).join(', ')}`;
585
- }).join('\n\n');
586
- }),
587
- );
588
-
589
- // --- GitHub OAuth Bind ---
590
- this.addTool(
591
- new ZhinTool('github_bind')
592
- .desc('็ป‘ๅฎš GitHub ่ดฆๅท โ€” ้€š่ฟ‡ OAuth ๆŽˆๆƒ๏ผŒ่ฎฉ bot ไปฅไฝ ็š„่บซไปฝๆ‰ง่กŒ star/fork ็ญ‰ๆ“ไฝœ')
593
- .keyword('github bind', '็ป‘ๅฎšgithub', 'github ็ป‘ๅฎš', 'github ๆŽˆๆƒ')
594
- .tag('github', 'oauth')
595
- .execute(async (_args, ctx) => {
596
- if (!ctx?.message) return 'โŒ ๆ— ๆณ•่Žทๅ–ๆถˆๆฏไธŠไธ‹ๆ–‡';
597
- const msg = ctx.message as Message;
598
-
599
- const bot = this.bots.values().next().value as GitHubBot | undefined;
600
- if (!bot?.$config.client_id) {
601
- return 'โŒ GitHub OAuth ๆœช้…็ฝฎ๏ผˆ้œ€่ฆๅœจ bot ้…็ฝฎไธญๆทปๅŠ  client_id ๅ’Œ client_secret๏ผ‰';
602
- }
603
-
604
- const nonce = crypto.randomBytes(16).toString('hex');
605
- const state = `${msg.$adapter}:${msg.$sender.id}:${nonce}`;
606
- oauthStates.set(state, {
607
- platform: msg.$adapter,
608
- platformUid: msg.$sender.id,
609
- expires: Date.now() + OAUTH_STATE_TTL,
610
- });
611
-
612
- const baseUrl = this.publicUrl;
613
- if (!baseUrl) {
614
- return 'โŒ ๆœช้…็ฝฎ public_url๏ผŒๆ— ๆณ•็”Ÿๆˆ OAuth ้“พๆŽฅ\n๐Ÿ’ก ่ฏทๅœจ bot ้…็ฝฎไธญๆทปๅŠ  public_url๏ผˆๅฆ‚ https://bot.example.com๏ผ‰';
615
- }
616
- const link = `${baseUrl}/pub/github/oauth?state=${encodeURIComponent(state)}`;
617
- const fullText = `๐Ÿ”— ่ฏท็‚นๅ‡ปไปฅไธ‹้“พๆŽฅๆŽˆๆƒไฝ ็š„ GitHub ่ดฆๅท๏ผš\n\n${link}\n\nโฑ๏ธ ้“พๆŽฅๆœ‰ๆ•ˆๆœŸ 5 ๅˆ†้’Ÿ`;
618
-
619
- // ็”ฑๅทฅๅ…ท็›ดๆŽฅๅ‘ๅˆฐ็”จๆˆท๏ผŒ้ฟๅ… AI ๆ€ป็ป“ๆ—ถๆŠŠ้“พๆŽฅๅžๆމ
620
- try {
621
- const targetAdapter = this.plugin.root?.inject(msg.$adapter)
622
- if (targetAdapter instanceof Adapter) {
623
- await targetAdapter.sendMessage({
624
- context: msg.$adapter,
625
- bot: msg.$bot,
626
- id: msg.$channel.id,
627
- type: msg.$channel.type,
628
- content: fullText,
629
- });
630
- }
631
- } catch (e) {
632
- this.plugin.logger.warn('github.bind ็›ดๅ‘้“พๆŽฅๅคฑ่ดฅ๏ผŒๅฐ†ไป…้€š่ฟ‡่ฟ”ๅ›žๅ€ผ่ฟ”ๅ›ž้“พๆŽฅ', e);
633
- }
634
-
635
- return 'ๅทฒๅ‘ๅฝ“ๅ‰ไผš่ฏๅ‘้€็ป‘ๅฎš้“พๆŽฅ๏ผŒ่ฏทๆ้†’็”จๆˆทๆŸฅๆ”ถๅนถ็‚นๅ‡ป้“พๆŽฅๅฎŒๆˆๆŽˆๆƒใ€‚';
636
- }),
637
- );
638
-
639
- // --- GitHub OAuth Unbind ---
640
- this.addTool(
641
- new ZhinTool('github_unbind')
642
- .desc('่งฃ้™ค GitHub ่ดฆๅท็ป‘ๅฎš')
643
- .keyword('github unbind', '่งฃ็ป‘github', 'github ่งฃ็ป‘')
644
- .tag('github', 'oauth')
645
- .execute(async (_args, ctx) => {
646
- if (!ctx?.message) return 'โŒ ๆ— ๆณ•่Žทๅ–ๆถˆๆฏไธŠไธ‹ๆ–‡';
647
- const msg = ctx.message as Message;
648
- const db = this.plugin.root?.inject('database') as any;
649
- const model = db?.models?.get('github_oauth_users');
650
- if (!model) return 'โŒ ๆ•ฐๆฎๅบ“ๆœชๅฐฑ็ปช';
651
-
652
- const [existing] = await model.select().where({ platform: msg.$adapter, platform_uid: msg.$sender.id });
653
- if (!existing) return 'โŒ ไฝ ่ฟ˜ๆฒกๆœ‰็ป‘ๅฎš GitHub ่ดฆๅท';
654
-
655
- await model.delete({ id: existing.id });
656
- return `โœ… ๅทฒ่งฃ้™ค GitHub ่ดฆๅท็ป‘ๅฎš๏ผˆ${existing.github_login}๏ผ‰`;
657
- }),
658
- );
659
-
660
- // --- GitHub OAuth Whoami ---
661
- this.addTool(
662
- new ZhinTool('github_whoami')
663
- .desc('ๆŸฅ็œ‹ๅฝ“ๅ‰็ป‘ๅฎš็š„ GitHub ่ดฆๅทไฟกๆฏ')
664
- .keyword('github whoami', 'github ๆˆ‘ๆ˜ฏ่ฐ', 'github ่ดฆๅท')
665
- .tag('github', 'oauth')
666
- .execute(async (_args, ctx) => {
667
- if (!ctx?.message) return 'โŒ ๆ— ๆณ•่Žทๅ–ๆถˆๆฏไธŠไธ‹ๆ–‡';
668
- const msg = ctx.message as Message;
669
- const db = this.plugin.root?.inject('database') as any;
670
- const model = db?.models?.get('github_oauth_users');
671
- if (!model) return 'โŒ ๆ•ฐๆฎๅบ“ๆœชๅฐฑ็ปช';
672
-
673
- const [existing] = await model.select().where({ platform: msg.$adapter, platform_uid: msg.$sender.id });
674
- if (!existing) return 'โŒ ไฝ ่ฟ˜ๆฒกๆœ‰็ป‘ๅฎš GitHub ่ดฆๅท\n๐Ÿ’ก ไฝฟ็”จ /github bind ่ฟ›่กŒ็ป‘ๅฎš';
675
-
676
- const oauthClient = new GitHubOAuthClient(existing.access_token);
677
- const userRes = await oauthClient.getUser();
678
- if (!userRes.ok) {
679
- return `โš ๏ธ ๅทฒ็ป‘ๅฎš ${existing.github_login}๏ผŒไฝ† token ๅฏ่ƒฝๅทฒๅคฑๆ•ˆ\n๐Ÿ’ก ่ฏทไฝฟ็”จ /github bind ้‡ๆ–ฐๆŽˆๆƒ`;
680
- }
681
-
682
- const u = userRes.data;
683
- return [
684
- `๐Ÿ”— GitHub ่ดฆๅทๅทฒ็ป‘ๅฎš`,
685
- `๐Ÿ‘ค ${u.login}${u.name ? ` (${u.name})` : ''}`,
686
- `๐Ÿ”‘ ๆŽˆๆƒ่Œƒๅ›ด: ${existing.scope || 'N/A'}`,
687
- `๐Ÿ“… ็ป‘ๅฎšๆ—ถ้—ด: ${new Date(existing.created_at).toLocaleDateString()}`,
688
- ].join('\n');
689
- }),
690
- );
691
-
692
- // --- GitHub Star (็”จๆˆท็บงๆ“ไฝœ) ---
693
- this.addTool(
694
- new ZhinTool('github_star')
695
- .desc('Star / Unstar ไธ€ไธชไป“ๅบ“๏ผˆไฝฟ็”จไฝ ็š„ GitHub ่ดฆๅท๏ผ‰')
696
- .keyword('star', 'unstar', 'ๆ”ถ่—', 'ๅ–ๆถˆๆ”ถ่—')
697
- .tag('github', 'user')
698
- .param('repo', { type: 'string', description: 'owner/repo (ๅฟ…ๅกซ)' }, true)
699
- .param('unstar', { type: 'boolean', description: '่ฎพไธบ true ๅˆ™ๅ–ๆถˆ star' })
700
- .execute(async (args, ctx) => {
701
- if (!ctx?.message) return 'โŒ ๆ— ๆณ•่Žทๅ–ๆถˆๆฏไธŠไธ‹ๆ–‡';
702
- const msg = ctx.message as Message;
703
- const repo = args.repo as string;
704
- if (!repo.includes('/')) return 'โŒ ๆ ผๅผๅบ”ไธบ owner/repo';
705
-
706
- const oauthClient = await this.getOAuthClient(msg.$adapter, msg.$sender.id);
707
- if (!oauthClient) {
708
- return 'โŒ ไฝ ่ฟ˜ๆฒกๆœ‰็ป‘ๅฎš GitHub ่ดฆๅท๏ผŒstar ้œ€่ฆไฝฟ็”จไฝ ่‡ชๅทฑ็š„่บซไปฝ\n๐Ÿ’ก ไฝฟ็”จ /github bind ็ป‘ๅฎš';
709
- }
710
-
711
- if (args.unstar) {
712
- const r = await oauthClient.unstarRepo(repo);
713
- return r.ok || r.status === 204 ? `โœ… ๅทฒๅ–ๆถˆ star: ${repo}` : `โŒ ๆ“ไฝœๅคฑ่ดฅ: ${JSON.stringify(r.data)}`;
714
- } else {
715
- const r = await oauthClient.starRepo(repo);
716
- return r.ok || r.status === 204 ? `โญ ๅทฒ star: ${repo}` : `โŒ ๆ“ไฝœๅคฑ่ดฅ: ${JSON.stringify(r.data)}`;
717
- }
718
- }),
719
- );
720
-
721
- // --- GitHub Fork (็”จๆˆท็บงๆ“ไฝœ) ---
722
- this.addTool(
723
- new ZhinTool('github_fork')
724
- .desc('Fork ไธ€ไธชไป“ๅบ“ๅˆฐไฝ ็š„ GitHub ่ดฆๅท')
725
- .keyword('fork', 'ๅคๅˆป')
726
- .tag('github', 'user')
727
- .param('repo', { type: 'string', description: 'owner/repo (ๅฟ…ๅกซ)' }, true)
728
- .execute(async (args, ctx) => {
729
- if (!ctx?.message) return 'โŒ ๆ— ๆณ•่Žทๅ–ๆถˆๆฏไธŠไธ‹ๆ–‡';
730
- const msg = ctx.message as Message;
731
- const repo = args.repo as string;
732
- if (!repo.includes('/')) return 'โŒ ๆ ผๅผๅบ”ไธบ owner/repo';
733
-
734
- const oauthClient = await this.getOAuthClient(msg.$adapter, msg.$sender.id);
735
- if (!oauthClient) {
736
- return 'โŒ ไฝ ่ฟ˜ๆฒกๆœ‰็ป‘ๅฎš GitHub ่ดฆๅท๏ผŒfork ้œ€่ฆไฝฟ็”จไฝ ่‡ชๅทฑ็š„่บซไปฝ\n๐Ÿ’ก ไฝฟ็”จ /github bind ็ป‘ๅฎš';
737
- }
738
-
739
- const r = await oauthClient.forkRepo(repo);
740
- return r.ok ? `๐Ÿด ๅทฒ fork: ${r.data.full_name}\n๐Ÿ”— ${r.data.html_url}` : `โŒ ๆ“ไฝœๅคฑ่ดฅ: ${r.data?.message || JSON.stringify(r.data)}`;
741
- }),
742
- );
743
-
744
- // --- Search ---
745
- this.addTool(
746
- new ZhinTool('github_search')
747
- .desc('GitHub ๆœ็ดข๏ผšๅœจ issues/repos/code ไธญๆœ็ดข')
748
- .keyword('search', 'ๆœ็ดข', 'ๆŸฅๆ‰พ', 'github search')
749
- .tag('github', 'search')
750
- .param('action', { type: 'string', description: 'issues|repos|code', enum: ['issues', 'repos', 'code'] }, true)
751
- .param('query', { type: 'string', description: 'ๆœ็ดขๅ…ณ้”ฎ่ฏ' }, true)
752
- .param('limit', { type: 'number', description: '่ฟ”ๅ›žๆ•ฐ้‡๏ผŒ้ป˜่ฎค 10' })
753
- .execute(async (args) => {
754
- const api = this.getAPI();
755
- if (!api) return 'โŒ ๆฒกๆœ‰ๅฏ็”จ็š„ GitHub bot';
756
- const q = args.query as string;
757
- const limit = (args.limit as number) || 10;
758
-
759
- switch (args.action) {
760
- case 'issues': {
761
- const r = await api.searchIssues(q, limit);
762
- if (!r.ok) return `โŒ ${JSON.stringify(r.data)}`;
763
- if (!r.data.items.length) return `๐Ÿ“ญ ๆฒกๆœ‰ๅŒน้…็š„ Issue/PR`;
764
- return `๐Ÿ” ๅ…ฑ ${r.data.total_count} ๆก๏ผŒๆ˜พ็คบๅ‰ ${r.data.items.length}:\n\n` +
765
- r.data.items.map((i: any) =>
766
- `${i.pull_request ? '๐Ÿ”€' : '๐Ÿ›'} ${i.repository_url.replace('https://api.github.com/repos/', '')}#${i.number}\n ${i.title}\n ๐Ÿ‘ค ${i.user.login} | ${i.state}`
767
- ).join('\n\n');
768
- }
769
- case 'repos': {
770
- const r = await api.searchRepos(q, limit);
771
- if (!r.ok) return `โŒ ${JSON.stringify(r.data)}`;
772
- if (!r.data.items.length) return `๐Ÿ“ญ ๆฒกๆœ‰ๅŒน้…็š„ไป“ๅบ“`;
773
- return `๐Ÿ” ๅ…ฑ ${r.data.total_count} ๆก๏ผŒๆ˜พ็คบๅ‰ ${r.data.items.length}:\n\n` +
774
- r.data.items.map((repo: any) =>
775
- `๐Ÿ“ฆ ${repo.full_name}${repo.private ? ' ๐Ÿ”’' : ''}\n ${repo.description || '(ๆ— ๆ่ฟฐ)'}\n โญ ${repo.stargazers_count} | ๐Ÿด ${repo.forks_count} | ๐Ÿ“ ${repo.language || '?'}`
776
- ).join('\n\n');
777
- }
778
- case 'code': {
779
- const r = await api.searchCode(q, limit);
780
- if (!r.ok) return `โŒ ${JSON.stringify(r.data)}`;
781
- if (!r.data.items.length) return `๐Ÿ“ญ ๆฒกๆœ‰ๅŒน้…็š„ไปฃ็ `;
782
- return `๐Ÿ” ๅ…ฑ ${r.data.total_count} ๆก๏ผŒๆ˜พ็คบๅ‰ ${r.data.items.length}:\n\n` +
783
- r.data.items.map((c: any) =>
784
- `๐Ÿ“„ ${c.repository.full_name}/${c.path}\n ๐Ÿ”— ${c.html_url}`
785
- ).join('\n\n');
786
- }
787
- default: return `โŒ ๆœช็Ÿฅๆœ็ดข็ฑปๅž‹: ${args.action}`;
788
- }
789
- }),
790
- );
791
-
792
- // --- Label ---
793
- this.addTool(
794
- new ZhinTool('github_label')
795
- .desc('GitHub ๆ ‡็ญพ็ฎก็†๏ผšๆŸฅ็œ‹/ๆทปๅŠ /็งป้™ค Issue/PR ๆ ‡็ญพ')
796
- .keyword('label', 'ๆ ‡็ญพ', 'tag')
797
- .tag('github', 'label')
798
- .param('action', { type: 'string', description: 'list|add|remove', enum: ['list', 'add', 'remove'] }, true)
799
- .param('repo', { type: 'string', description: 'owner/repo (ๅฟ…ๅกซ)' }, true)
800
- .param('number', { type: 'number', description: 'Issue/PR ็ผ–ๅท (add/remove ๅฟ…ๅกซ)' })
801
- .param('labels', { type: 'string', description: 'ๆ ‡็ญพๅ๏ผŒ้€—ๅทๅˆ†้š” (add/remove)' })
802
- .execute(async (args) => {
803
- const api = this.getAPI();
804
- if (!api) return 'โŒ ๆฒกๆœ‰ๅฏ็”จ็š„ GitHub bot';
805
- const repo = args.repo as string;
806
-
807
- switch (args.action) {
808
- case 'list': {
809
- const r = await api.listLabels(repo);
810
- if (!r.ok) return `โŒ ${JSON.stringify(r.data)}`;
811
- if (!r.data.length) return `๐Ÿ“ญ ไป“ๅบ“ๆฒกๆœ‰ๆ ‡็ญพ`;
812
- return `๐Ÿท๏ธ ${repo} ๆ ‡็ญพ (${r.data.length}):\n` +
813
- r.data.map((l: any) => ` โ€ข ${l.name}${l.description ? ` โ€” ${l.description}` : ''}`).join('\n');
814
- }
815
- case 'add': {
816
- if (!args.number) return 'โŒ ่ฏทๆไพ› Issue/PR ็ผ–ๅท';
817
- if (!args.labels) return 'โŒ ่ฏทๆไพ›ๆ ‡็ญพๅ';
818
- const labels = (args.labels as string).split(',').map(s => s.trim());
819
- const r = await api.addLabels(repo, args.number as number, labels);
820
- return r.ok ? `โœ… ๅทฒๆทปๅŠ ๆ ‡็ญพ: ${labels.join(', ')}` : `โŒ ${JSON.stringify(r.data)}`;
821
- }
822
- case 'remove': {
823
- if (!args.number) return 'โŒ ่ฏทๆไพ› Issue/PR ็ผ–ๅท';
824
- if (!args.labels) return 'โŒ ่ฏทๆไพ›่ฆ็งป้™ค็š„ๆ ‡็ญพๅ';
825
- const labels = (args.labels as string).split(',').map(s => s.trim());
826
- const results: string[] = [];
827
- for (const label of labels) {
828
- const r = await api.removeLabel(repo, args.number as number, label);
829
- results.push(r.ok ? `โœ… ${label}` : `โŒ ${label}: ${r.data?.message || 'failed'}`);
830
- }
831
- return results.join('\n');
832
- }
833
- default: return `โŒ ๆœช็Ÿฅๆ“ไฝœ: ${args.action}`;
834
- }
835
- }),
836
- );
837
-
838
- // --- Assign ---
839
- this.addTool(
840
- new ZhinTool('github_assign')
841
- .desc('GitHub ๆŒ‡ๆดพ็ฎก็†๏ผš็ป™ Issue/PR ๆทปๅŠ /็งป้™คๆŒ‡ๆดพไบบ')
842
- .keyword('assign', 'ๆŒ‡ๆดพ', 'ๅˆ†้…')
843
- .tag('github', 'assign')
844
- .param('action', { type: 'string', description: 'add|remove', enum: ['add', 'remove'] }, true)
845
- .param('repo', { type: 'string', description: 'owner/repo (ๅฟ…ๅกซ)' }, true)
846
- .param('number', { type: 'number', description: 'Issue/PR ็ผ–ๅท (ๅฟ…ๅกซ)' }, true)
847
- .param('assignees', { type: 'string', description: '็”จๆˆทๅ๏ผŒ้€—ๅทๅˆ†้š” (ๅฟ…ๅกซ)' }, true)
848
- .execute(async (args) => {
849
- const api = this.getAPI();
850
- if (!api) return 'โŒ ๆฒกๆœ‰ๅฏ็”จ็š„ GitHub bot';
851
- const repo = args.repo as string;
852
- const num = args.number as number;
853
- const assignees = (args.assignees as string).split(',').map(s => s.trim());
854
-
855
- if (args.action === 'add') {
856
- const r = await api.addAssignees(repo, num, assignees);
857
- return r.ok ? `โœ… ๅทฒๆŒ‡ๆดพ: ${assignees.join(', ')}` : `โŒ ${r.data?.message || JSON.stringify(r.data)}`;
858
- } else {
859
- const r = await api.removeAssignees(repo, num, assignees);
860
- return r.ok ? `โœ… ๅทฒ็งป้™คๆŒ‡ๆดพ: ${assignees.join(', ')}` : `โŒ ${r.data?.message || JSON.stringify(r.data)}`;
861
- }
862
- }),
863
- );
864
-
865
- // --- File ---
866
- this.addTool(
867
- new ZhinTool('github_file')
868
- .desc('่ฏปๅ– GitHub ไป“ๅบ“ไธญ็š„ๆ–‡ไปถๅ†…ๅฎน')
869
- .keyword('file', 'ๆ–‡ไปถ', 'ๆŸฅ็œ‹ๆ–‡ไปถ', '่ฏปๅ–ๆ–‡ไปถ', 'cat')
870
- .tag('github', 'file')
871
- .param('repo', { type: 'string', description: 'owner/repo (ๅฟ…ๅกซ)' }, true)
872
- .param('path', { type: 'string', description: 'ๆ–‡ไปถ่ทฏๅพ„ (ๅฟ…ๅกซ)' }, true)
873
- .param('ref', { type: 'string', description: 'ๅˆ†ๆ”ฏ/tag/commit SHA (ๅฏ้€‰๏ผŒ้ป˜่ฎคไธปๅˆ†ๆ”ฏ)' })
874
- .execute(async (args) => {
875
- const api = this.getAPI();
876
- if (!api) return 'โŒ ๆฒกๆœ‰ๅฏ็”จ็š„ GitHub bot';
877
- const r = await api.getFileContent(args.repo as string, args.path as string, args.ref as string | undefined);
878
- if (!r.ok) return `โŒ ${r.data?.message || JSON.stringify(r.data)}`;
879
-
880
- if (Array.isArray(r.data)) {
881
- return `๐Ÿ“‚ ${args.path} (็›ฎๅฝ•๏ผŒ${r.data.length} ้กน):\n` +
882
- r.data.map((f: any) => ` ${f.type === 'dir' ? '๐Ÿ“' : '๐Ÿ“„'} ${f.name}`).join('\n');
883
- }
884
-
885
- if (r.data.type === 'file' && r.data.content) {
886
- const decoded = Buffer.from(r.data.content, 'base64').toString('utf-8');
887
- const maxLen = 3000;
888
- const truncated = decoded.length > maxLen;
889
- return `๐Ÿ“„ ${r.data.path} (${r.data.size} bytes)\n\n${decoded.slice(0, maxLen)}${truncated ? `\n\n... (ๆˆชๆ–ญ๏ผŒๅ…ฑ ${decoded.length} ๅญ—็ฌฆ)` : ''}`;
890
- }
891
-
892
- return `๐Ÿ“„ ${r.data.path} โ€” ${r.data.type} (${r.data.size} bytes)\n๐Ÿ”— ${r.data.html_url}`;
893
- }),
894
- );
895
-
896
- // --- Commits ---
897
- this.addTool(
898
- new ZhinTool('github_commits')
899
- .desc('GitHub ๆไบคๆŸฅ่ฏข๏ผšๅˆ—ๅ‡บๆไบค่ฎฐๅฝ•ๆˆ–ๅฏนๆฏ”ไธคไธชๅˆ†ๆ”ฏ')
900
- .keyword('commit', 'ๆไบค', 'ๅކๅฒ', 'log', 'compare', 'ๅฏนๆฏ”')
901
- .tag('github', 'commits')
902
- .param('action', { type: 'string', description: 'list|compare', enum: ['list', 'compare'] }, true)
903
- .param('repo', { type: 'string', description: 'owner/repo (ๅฟ…ๅกซ)' }, true)
904
- .param('sha', { type: 'string', description: 'ๅˆ†ๆ”ฏ/SHA (list)' })
905
- .param('path', { type: 'string', description: 'ๆŒ‰ๆ–‡ไปถ่ทฏๅพ„่ฟ‡ๆปค (list)' })
906
- .param('base', { type: 'string', description: 'ๅŸบๅ‡†ๅˆ†ๆ”ฏ (compare)' })
907
- .param('head', { type: 'string', description: '็›ฎๆ ‡ๅˆ†ๆ”ฏ (compare)' })
908
- .param('limit', { type: 'number', description: '่ฟ”ๅ›žๆ•ฐ้‡๏ผŒ้ป˜่ฎค 10' })
909
- .execute(async (args) => {
910
- const api = this.getAPI();
911
- if (!api) return 'โŒ ๆฒกๆœ‰ๅฏ็”จ็š„ GitHub bot';
912
- const repo = args.repo as string;
913
-
914
- if (args.action === 'list') {
915
- const r = await api.listCommits(repo, args.sha as string | undefined, args.path as string | undefined, (args.limit as number) || 10);
916
- if (!r.ok) return `โŒ ${JSON.stringify(r.data)}`;
917
- if (!r.data.length) return '๐Ÿ“ญ ๆฒกๆœ‰ๆ‰พๅˆฐๆไบค่ฎฐๅฝ•';
918
- return r.data.map((c: any) =>
919
- `โ€ข ${c.sha.substring(0, 7)} ${c.commit.message.split('\n')[0]}\n ๐Ÿ‘ค ${c.commit.author?.name || '?'} | ๐Ÿ“… ${c.commit.author?.date?.split('T')[0] || '?'}`
920
- ).join('\n\n');
921
- } else {
922
- if (!args.base || !args.head) return 'โŒ compare ้œ€่ฆ base ๅ’Œ head ๅ‚ๆ•ฐ';
923
- const r = await api.compareCommits(repo, args.base as string, args.head as string);
924
- if (!r.ok) return `โŒ ${r.data?.message || JSON.stringify(r.data)}`;
925
- const d = r.data;
926
- return [
927
- `๐Ÿ”€ ${args.base} โ† ${args.head}`,
928
- `๐Ÿ“Š ${d.status} | ${d.ahead_by} ahead, ${d.behind_by} behind`,
929
- `๐Ÿ“ ${d.total_commits} commits | ${d.files?.length || 0} files changed`,
930
- d.commits?.length ? '\nๆœ€่ฟ‘ๆไบค:\n' + d.commits.slice(0, 5).map((c: any) =>
931
- ` โ€ข ${c.sha.substring(0, 7)} ${c.commit.message.split('\n')[0]}`
932
- ).join('\n') : '',
933
- ].filter(Boolean).join('\n');
934
- }
935
- }),
936
- );
937
-
938
- // --- Edit (Issue/PR) ---
939
- this.addTool(
940
- new ZhinTool('github_edit')
941
- .desc('็ผ–่พ‘ GitHub Issue ๆˆ– PR ็š„ๆ ‡้ข˜ใ€ๆญฃๆ–‡ใ€็Šถๆ€')
942
- .keyword('edit', '็ผ–่พ‘', 'ไฟฎๆ”น', 'update', 'ๆ›ดๆ–ฐ')
943
- .tag('github', 'edit')
944
- .param('type', { type: 'string', description: 'issue|pr', enum: ['issue', 'pr'] }, true)
945
- .param('repo', { type: 'string', description: 'owner/repo (ๅฟ…ๅกซ)' }, true)
946
- .param('number', { type: 'number', description: 'Issue/PR ็ผ–ๅท (ๅฟ…ๅกซ)' }, true)
947
- .param('title', { type: 'string', description: 'ๆ–ฐๆ ‡้ข˜' })
948
- .param('body', { type: 'string', description: 'ๆ–ฐๆญฃๆ–‡' })
949
- .param('state', { type: 'string', description: 'open|closed' })
950
- .execute(async (args) => {
951
- const api = this.getAPI();
952
- if (!api) return 'โŒ ๆฒกๆœ‰ๅฏ็”จ็š„ GitHub bot';
953
- const repo = args.repo as string;
954
- const num = args.number as number;
955
-
956
- const data: any = {};
957
- if (args.title) data.title = args.title;
958
- if (args.body) data.body = args.body;
959
- if (args.state) data.state = args.state;
960
-
961
- if (!Object.keys(data).length) return 'โŒ ่ฏท่‡ณๅฐ‘ๆไพ›ไธ€ไธช่ฆไฟฎๆ”น็š„ๅญ—ๆฎต (title/body/state)';
962
-
963
- const r = args.type === 'pr'
964
- ? await api.updatePR(repo, num, data)
965
- : await api.updateIssue(repo, num, data);
966
-
967
- if (!r.ok) return `โŒ ${r.data?.message || JSON.stringify(r.data)}`;
968
- return `โœ… ${args.type === 'pr' ? 'PR' : 'Issue'} #${num} ๅทฒๆ›ดๆ–ฐ\n๐Ÿ”— ${r.data.html_url}`;
969
- }),
970
- );
971
-
972
- this.plugin.logger.debug('GitHub ๅทฅๅ…ทๅทฒๆณจๅ†Œ: pr, issue, repo, search, label, assign, file, commits, edit, subscribe, unsubscribe, subscriptions, bind, unbind, whoami, star, fork');
973
- }
974
-
975
309
 
976
310
  // โ”€โ”€ ้€š็ŸฅๆŽจ้€ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
977
311