@zhin.js/adapter-github 1.1.15 → 1.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +4 -0
  3. package/agents/github/messages.js +72 -0
  4. package/agents/github/messages.ts +72 -0
  5. package/agents/github/skills/github-account/handlers.js +30 -25
  6. package/agents/github/skills/github-account/handlers.ts +30 -25
  7. package/agents/github/skills/github-account/tools/bind/index.js +1 -1
  8. package/agents/github/skills/github-account/tools/bind/index.ts +1 -1
  9. package/agents/github/skills/github-account/tools/install/index.js +1 -1
  10. package/agents/github/skills/github-account/tools/install/index.ts +1 -1
  11. package/agents/github/skills/github-account/tools/unbind/index.js +1 -1
  12. package/agents/github/skills/github-account/tools/unbind/index.ts +1 -1
  13. package/agents/github/skills/github-account/tools/whoami/index.js +1 -1
  14. package/agents/github/skills/github-account/tools/whoami/index.ts +1 -1
  15. package/agents/github/skills/github-repository/handlers.js +82 -35
  16. package/agents/github/skills/github-repository/handlers.ts +83 -31
  17. package/agents/github/skills/github-repository/tools/create_pr/index.js +1 -1
  18. package/agents/github/skills/github-repository/tools/create_pr/index.ts +1 -1
  19. package/agents/github/skills/github-repository/tools/patch_file/index.js +1 -1
  20. package/agents/github/skills/github-repository/tools/patch_file/index.ts +1 -1
  21. package/agents/github/skills/github-repository/tools/prepare_workspace/index.js +1 -1
  22. package/agents/github/skills/github-repository/tools/prepare_workspace/index.ts +1 -1
  23. package/agents/github/skills/github-repository/tools/push_branch/index.js +1 -1
  24. package/agents/github/skills/github-repository/tools/push_branch/index.ts +1 -1
  25. package/agents/github/skills/github-subscriptions/handlers.js +85 -22
  26. package/agents/github/skills/github-subscriptions/handlers.ts +86 -22
  27. package/agents/github/skills/github-subscriptions/tools/star/index.js +1 -1
  28. package/agents/github/skills/github-subscriptions/tools/star/index.ts +1 -1
  29. package/agents/github/skills/github-subscriptions/tools/subscribe/index.js +1 -1
  30. package/agents/github/skills/github-subscriptions/tools/subscribe/index.ts +1 -1
  31. package/agents/github/skills/github-subscriptions/tools/subscriptions/index.js +1 -1
  32. package/agents/github/skills/github-subscriptions/tools/subscriptions/index.ts +1 -1
  33. package/agents/github/skills/github-subscriptions/tools/unsubscribe/index.js +1 -1
  34. package/agents/github/skills/github-subscriptions/tools/unsubscribe/index.ts +1 -1
  35. package/commands/github/endpoint/definition.js +2 -2
  36. package/commands/github/endpoint/definition.ts +2 -2
  37. package/lib/github-channel-context.d.ts +2 -1
  38. package/lib/github-channel-context.js +6 -2
  39. package/lib/workspace-manager.d.ts +8 -7
  40. package/lib/workspace-manager.js +62 -50
  41. package/package.json +19 -19
  42. package/schema.json +41 -6
  43. package/src/github-channel-context.ts +7 -1
  44. package/src/workspace-manager.ts +62 -50
package/schema.json CHANGED
@@ -31,7 +31,12 @@
31
31
  "string",
32
32
  "number"
33
33
  ],
34
- "description": "框架 master(GitHub user login or id;AI/工具权限、endpoint 管理)。endpoints[i].master 可逐项覆盖"
34
+ "description": "框架 master(GitHub user login or id;AI/工具权限、endpoint 管理)。endpoints[i].master 可逐项覆盖",
35
+ "x-i18n": {
36
+ "en": {
37
+ "description": "Framework masters (GitHub user login or id; AI/tool permissions and endpoint management). endpoints[i].master can override this value."
38
+ }
39
+ }
35
40
  },
36
41
  "trusted": {
37
42
  "type": "array",
@@ -42,7 +47,12 @@
42
47
  ],
43
48
  "description": "Trusted GitHub user login or id"
44
49
  },
45
- "description": "框架 trusted 用户列表(弱于 master)。endpoints[i].trusted 可逐项追加"
50
+ "description": "框架 trusted 用户列表(弱于 master)。endpoints[i].trusted 可逐项追加",
51
+ "x-i18n": {
52
+ "en": {
53
+ "description": "Framework trusted users, below master authority. endpoints[i].trusted can add users."
54
+ }
55
+ }
46
56
  },
47
57
  "endpoints": {
48
58
  "type": "array",
@@ -57,7 +67,12 @@
57
67
  "string",
58
68
  "number"
59
69
  ],
60
- "description": "本 endpoint 的框架 master(GitHub user login or id);覆盖顶层 master"
70
+ "description": "本 endpoint 的框架 master(GitHub user login or id);覆盖顶层 master",
71
+ "x-i18n": {
72
+ "en": {
73
+ "description": "Framework masters for this endpoint (GitHub user login or id), overriding the top-level master list."
74
+ }
75
+ }
61
76
  },
62
77
  "trusted": {
63
78
  "type": "array",
@@ -68,7 +83,12 @@
68
83
  ],
69
84
  "description": "Trusted GitHub user login or id"
70
85
  },
71
- "description": "本 endpoint 的 trusted 列表"
86
+ "description": "本 endpoint 的 trusted 列表",
87
+ "x-i18n": {
88
+ "en": {
89
+ "description": "Trusted users for this endpoint."
90
+ }
91
+ }
72
92
  },
73
93
  "app_id": {
74
94
  "type": [
@@ -115,7 +135,12 @@
115
135
  },
116
136
  "commandPrefix": {
117
137
  "type": "string",
118
- "description": "本 endpoint 的命令前缀"
138
+ "description": "本 endpoint 的命令前缀",
139
+ "x-i18n": {
140
+ "en": {
141
+ "description": "Command prefix for this endpoint."
142
+ }
143
+ }
119
144
  }
120
145
  },
121
146
  "required": [
@@ -123,12 +148,22 @@
123
148
  "app_id",
124
149
  "private_key"
125
150
  ]
151
+ },
152
+ "x-i18n": {
153
+ "en": {
154
+ "description": "Multiple accounts: mount multiple endpoints in one plugin instance."
155
+ }
126
156
  }
127
157
  },
128
158
  "commandPrefix": {
129
159
  "type": "string",
130
160
  "default": "",
131
- "description": "命令前缀(默认 '' 无前缀,任意文本按命令匹配;如 '/' 要求 / 开头)。endpoints[i] 可逐项覆盖"
161
+ "description": "命令前缀(默认 '' 无前缀,任意文本按命令匹配;如 '/' 要求 / 开头)。endpoints[i] 可逐项覆盖",
162
+ "x-i18n": {
163
+ "en": {
164
+ "description": "Command prefix. The default empty string allows unprefixed commands; '/' requires a leading slash. Each endpoints[i] can override it."
165
+ }
166
+ }
132
167
  }
133
168
  },
134
169
  "required": [
@@ -2,6 +2,7 @@
2
2
  * Parse GitHub Issue/PR channel context from IM messages.
3
3
  */
4
4
  import type { Message } from 'zhin.js';
5
+ import { createMessages, type Locale } from 'zhin.js/i18n';
5
6
  import { buildChannelId, parseChannelId, type ParsedChannel } from './types.js';
6
7
  import type { GhClient } from './gh-client.js';
7
8
 
@@ -24,6 +25,7 @@ export function issueBranchName(number: number): string {
24
25
  export async function resolveWorkspaceBranch(
25
26
  gh: GhClient,
26
27
  ctx: GitHubChannelContext,
28
+ locale?: Locale,
27
29
  ): Promise<{ branch: string; base: string }> {
28
30
  if (ctx.type === 'pr') {
29
31
  const pr = await gh.getPR(ctx.repo, ctx.number);
@@ -31,7 +33,7 @@ export async function resolveWorkspaceBranch(
31
33
  const base = pr.data.base?.ref ?? 'main';
32
34
  return { branch: pr.data.head.ref, base };
33
35
  }
34
- throw new Error(`无法读取 PR #${ctx.number} 的 head 分支`);
36
+ throw new Error(t(locale, 'missingHead', { number: ctx.number }));
35
37
  }
36
38
 
37
39
  const repoInfo = await gh.getRepo(ctx.repo);
@@ -44,3 +46,7 @@ export async function resolveWorkspaceBranch(
44
46
  export function formatChannelContext(ctx: GitHubChannelContext): string {
45
47
  return buildChannelId(ctx.repo, ctx.type, ctx.number);
46
48
  }
49
+
50
+ const t = createMessages({
51
+ missingHead: { 'zh-CN': '无法读取 PR #{number} 的 head 分支', en: 'Cannot read the head branch of PR #{number}' },
52
+ });
@@ -4,6 +4,7 @@
4
4
  import { spawn } from 'node:child_process';
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
+ import { createMessages, type Locale } from 'zhin.js/i18n';
7
8
  import type { GhClient, GitHubBotIdentity } from './gh-client.js';
8
9
 
9
10
  const REPO_FULL_NAME_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
@@ -23,13 +24,13 @@ function containsIllegalGitRefCharacter(ref: string): boolean {
23
24
  * 校验 GitHub "owner/name" 全名,防止路径穿越与 git 选项注入。
24
25
  * 返回通过校验的原值,便于调用方直接使用「已消毒」的变量。
25
26
  */
26
- export function assertRepoFullName(repo: string): string {
27
+ export function assertRepoFullName(repo: string, locale?: Locale): string {
27
28
  if (
28
29
  typeof repo !== 'string' ||
29
30
  !REPO_FULL_NAME_RE.test(repo) ||
30
31
  repo.split('/').some((seg) => seg === '' || seg.startsWith('-') || /^\.+$/.test(seg))
31
32
  ) {
32
- throw new TypeError(`非法的仓库全名: ${JSON.stringify(repo)}`);
33
+ throw new TypeError(t(locale, 'invalidRepo', { value: JSON.stringify(repo) }));
33
34
  }
34
35
  return repo;
35
36
  }
@@ -37,9 +38,9 @@ export function assertRepoFullName(repo: string): string {
37
38
  /**
38
39
  * 校验 git ref 名称,拒绝选项注入(`-` 前缀)与 git 非法字符。
39
40
  */
40
- export function assertGitRefName(ref: string): string {
41
+ export function assertGitRefName(ref: string, locale?: Locale): string {
41
42
  if (typeof ref !== 'string' || ref.length === 0) {
42
- throw new TypeError(`非法的 git ref: ${JSON.stringify(ref)}`);
43
+ throw new TypeError(t(locale, 'invalidRef', { value: JSON.stringify(ref) }));
43
44
  }
44
45
  if (
45
46
  ref.startsWith('-') ||
@@ -48,7 +49,7 @@ export function assertGitRefName(ref: string): string {
48
49
  ref.endsWith('/') ||
49
50
  ref.endsWith('.')
50
51
  ) {
51
- throw new TypeError(`非法的 git ref: ${JSON.stringify(ref)}`);
52
+ throw new TypeError(t(locale, 'invalidRef', { value: JSON.stringify(ref) }));
52
53
  }
53
54
  return ref;
54
55
  }
@@ -59,13 +60,13 @@ export class WorkspaceManager {
59
60
  private readonly rootDir: string,
60
61
  ) {}
61
62
 
62
- getRepoPath(repo: string): string {
63
- repo = assertRepoFullName(repo);
63
+ getRepoPath(repo: string, locale?: Locale): string {
64
+ repo = assertRepoFullName(repo, locale);
64
65
  const [owner, name] = repo.split('/');
65
66
  return path.join(this.rootDir, owner, name);
66
67
  }
67
68
 
68
- private runGit(cwd: string, args: string[], env?: NodeJS.ProcessEnv): Promise<string> {
69
+ private runGit(cwd: string, args: string[], env?: NodeJS.ProcessEnv, locale?: Locale): Promise<string> {
69
70
  return new Promise((resolve, reject) => {
70
71
  const proc = spawn('git', args, {
71
72
  cwd,
@@ -77,7 +78,7 @@ export class WorkspaceManager {
77
78
  proc.stdout.on('data', (d: Buffer) => { stdout += d.toString(); });
78
79
  proc.stderr.on('data', (d: Buffer) => { stderr += d.toString(); });
79
80
  proc.on('error', (err: NodeJS.ErrnoException) => {
80
- if (err.code === 'ENOENT') reject(new Error('git 未安装'));
81
+ if (err.code === 'ENOENT') reject(new Error(t(locale, 'missingGit')));
81
82
  else reject(err);
82
83
  });
83
84
  proc.on('close', (code) => {
@@ -87,82 +88,93 @@ export class WorkspaceManager {
87
88
  });
88
89
  }
89
90
 
90
- configureBotGit(repoPath: string, identity: GitHubBotIdentity): Promise<void> {
91
- return Promise.all([
92
- this.runGit(repoPath, ['config', 'user.name', identity.login]),
93
- this.runGit(repoPath, ['config', 'user.email', identity.email]),
94
- ]).then(() => undefined);
91
+ async configureBotGit(repoPath: string, identity: GitHubBotIdentity, locale?: Locale): Promise<void> {
92
+ // Both writes acquire the same .git/config.lock.
93
+ await this.runGit(repoPath, ['config', 'user.name', identity.login], undefined, locale);
94
+ await this.runGit(repoPath, ['config', 'user.email', identity.email], undefined, locale);
95
95
  }
96
96
 
97
- async ensureRepo(repo: string): Promise<string> {
98
- repo = assertRepoFullName(repo);
97
+ async ensureRepo(repo: string, locale?: Locale): Promise<string> {
98
+ repo = assertRepoFullName(repo, locale);
99
99
  if (!this.gh.isAppAuth) {
100
- throw new Error('GitHub App 认证未配置,无法托管工作区');
100
+ throw new Error(t(locale, 'appRequired'));
101
101
  }
102
102
  const token = await this.gh.ensureInstallationTokenForRepo(repo);
103
- if (!token) throw new Error(`无法获取 ${repo} 的 Installation Token`);
103
+ if (!token) throw new Error(t(locale, 'repoTokenUnavailable', { repo }));
104
104
 
105
- const repoPath = this.getRepoPath(repo);
105
+ const repoPath = this.getRepoPath(repo, locale);
106
106
  fs.mkdirSync(path.dirname(repoPath), { recursive: true });
107
107
 
108
108
  const identity = await this.gh.getBotIdentity();
109
- if (!identity) throw new Error('无法解析 GitHub App Bot 身份');
109
+ if (!identity) throw new Error(t(locale, 'missingIdentity'));
110
110
 
111
111
  const cloneUrl = this.gh.buildCloneUrl(repo, token);
112
112
  if (!fs.existsSync(path.join(repoPath, '.git'))) {
113
- await this.runGit(path.dirname(repoPath), ['clone', cloneUrl, path.basename(repoPath)]);
113
+ await this.runGit(path.dirname(repoPath), ['clone', '--', cloneUrl, path.basename(repoPath)], undefined, locale);
114
114
  } else {
115
- await this.runGit(repoPath, ['remote', 'set-url', 'origin', cloneUrl]);
116
- await this.runGit(repoPath, ['fetch', 'origin', '--prune']);
115
+ await this.runGit(repoPath, ['remote', 'set-url', 'origin', cloneUrl], undefined, locale);
116
+ await this.runGit(repoPath, ['fetch', 'origin', '--prune'], undefined, locale);
117
117
  }
118
118
 
119
- await this.configureBotGit(repoPath, identity);
119
+ await this.configureBotGit(repoPath, identity, locale);
120
120
  return repoPath;
121
121
  }
122
122
 
123
- async checkoutBranch(repo: string, branch: string, baseRef: string): Promise<string> {
124
- repo = assertRepoFullName(repo);
125
- branch = assertGitRefName(branch);
126
- baseRef = assertGitRefName(baseRef);
127
- const repoPath = await this.ensureRepo(repo);
128
- const localBranches = await this.runGit(repoPath, ['branch', '--list', branch]);
123
+ async checkoutBranch(repo: string, branch: string, baseRef: string, locale?: Locale): Promise<string> {
124
+ repo = assertRepoFullName(repo, locale);
125
+ branch = assertGitRefName(branch, locale);
126
+ baseRef = assertGitRefName(baseRef, locale);
127
+ const repoPath = await this.ensureRepo(repo, locale);
128
+ const localBranches = await this.runGit(repoPath, ['branch', '--list', branch], undefined, locale);
129
129
  if (localBranches.trim()) {
130
- await this.runGit(repoPath, ['checkout', branch]);
131
- await this.runGit(repoPath, ['pull', '--rebase', 'origin', branch]).catch(async () => {
132
- await this.runGit(repoPath, ['fetch', 'origin', branch]);
130
+ await this.runGit(repoPath, ['checkout', branch], undefined, locale);
131
+ await this.runGit(repoPath, ['pull', '--rebase', '--', 'origin', branch], undefined, locale).catch(async () => {
132
+ await this.runGit(repoPath, ['fetch', '--', 'origin', branch], undefined, locale);
133
133
  });
134
134
  return repoPath;
135
135
  }
136
136
 
137
- const remoteBranch = await this.runGit(repoPath, ['ls-remote', '--heads', 'origin', branch]);
137
+ const remoteBranch = await this.runGit(repoPath, ['ls-remote', '--heads', '--', 'origin', branch], undefined, locale);
138
138
  if (remoteBranch.trim()) {
139
- await this.runGit(repoPath, ['checkout', '-B', branch, `origin/${branch}`]);
139
+ await this.runGit(repoPath, ['checkout', '-B', branch, `origin/${branch}`], undefined, locale);
140
140
  return repoPath;
141
141
  }
142
142
 
143
- await this.runGit(repoPath, ['fetch', 'origin', baseRef]);
144
- await this.runGit(repoPath, ['checkout', '-B', branch, `origin/${baseRef}`]);
143
+ await this.runGit(repoPath, ['fetch', '--', 'origin', baseRef], undefined, locale);
144
+ await this.runGit(repoPath, ['checkout', '-B', branch, `origin/${baseRef}`], undefined, locale);
145
145
  return repoPath;
146
146
  }
147
147
 
148
- async commitAndPush(repo: string, branch: string, message: string): Promise<string> {
149
- repo = assertRepoFullName(repo);
150
- branch = assertGitRefName(branch);
151
- const repoPath = this.getRepoPath(repo);
148
+ async commitAndPush(repo: string, branch: string, message: string, locale?: Locale): Promise<string> {
149
+ repo = assertRepoFullName(repo, locale);
150
+ branch = assertGitRefName(branch, locale);
151
+ const repoPath = this.getRepoPath(repo, locale);
152
152
  if (!fs.existsSync(repoPath)) {
153
- throw new Error(`工作区不存在: ${repoPath},请先 github_prepare_workspace`);
153
+ throw new Error(t(locale, 'missingWorkspace', { path: repoPath }));
154
154
  }
155
155
 
156
156
  const token = await this.gh.ensureInstallationTokenForRepo(repo);
157
- if (!token) throw new Error('Installation Token 不可用');
158
- await this.runGit(repoPath, ['remote', 'set-url', 'origin', this.gh.buildCloneUrl(repo, token)]);
157
+ if (!token) throw new Error(t(locale, 'missingToken'));
158
+ await this.runGit(repoPath, ['remote', 'set-url', 'origin', this.gh.buildCloneUrl(repo, token)], undefined, locale);
159
159
 
160
- const status = await this.runGit(repoPath, ['status', '--porcelain']);
161
- if (!status.trim()) return '没有可提交的变更';
160
+ const status = await this.runGit(repoPath, ['status', '--porcelain'], undefined, locale);
161
+ if (!status.trim()) return t(locale, 'noChanges');
162
162
 
163
- await this.runGit(repoPath, ['add', '-A']);
164
- await this.runGit(repoPath, ['commit', '-m', message]);
165
- await this.runGit(repoPath, ['push', '-u', 'origin', branch]);
166
- return `已 push 到 origin/${branch}`;
163
+ await this.runGit(repoPath, ['add', '-A'], undefined, locale);
164
+ await this.runGit(repoPath, ['commit', '-m', message], undefined, locale);
165
+ await this.runGit(repoPath, ['push', '-u', 'origin', branch], undefined, locale);
166
+ return t(locale, 'pushed', { branch });
167
167
  }
168
168
  }
169
+ const t = createMessages({
170
+ invalidRepo: { 'zh-CN': '非法的仓库全名: {value}', en: 'Invalid repository name: {value}' },
171
+ invalidRef: { 'zh-CN': '非法的 git ref: {value}', en: 'Invalid git ref: {value}' },
172
+ missingGit: { 'zh-CN': 'git 未安装', en: 'git is not installed' },
173
+ appRequired: { 'zh-CN': 'GitHub App 认证未配置,无法托管工作区', en: 'GitHub App authentication is not configured; the workspace is unavailable' },
174
+ repoTokenUnavailable: { 'zh-CN': '无法获取 {repo} 的 Installation Token', en: 'Cannot obtain an Installation Token for {repo}' },
175
+ missingIdentity: { 'zh-CN': '无法解析 GitHub App Bot 身份', en: 'Cannot resolve the GitHub App bot identity' },
176
+ missingWorkspace: { 'zh-CN': '工作区不存在: {path},请先准备工作区', en: 'Workspace does not exist: {path}. Prepare the workspace first' },
177
+ missingToken: { 'zh-CN': 'Installation Token 不可用', en: 'Installation Token is unavailable' },
178
+ noChanges: { 'zh-CN': '没有可提交的变更', en: 'No changes to commit' },
179
+ pushed: { 'zh-CN': '已 push 到 origin/{branch}', en: 'Pushed to origin/{branch}' },
180
+ });