@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.
- package/CHANGELOG.md +52 -0
- package/README.md +4 -0
- package/agents/github/messages.js +72 -0
- package/agents/github/messages.ts +72 -0
- package/agents/github/skills/github-account/handlers.js +30 -25
- package/agents/github/skills/github-account/handlers.ts +30 -25
- package/agents/github/skills/github-account/tools/bind/index.js +1 -1
- package/agents/github/skills/github-account/tools/bind/index.ts +1 -1
- package/agents/github/skills/github-account/tools/install/index.js +1 -1
- package/agents/github/skills/github-account/tools/install/index.ts +1 -1
- package/agents/github/skills/github-account/tools/unbind/index.js +1 -1
- package/agents/github/skills/github-account/tools/unbind/index.ts +1 -1
- package/agents/github/skills/github-account/tools/whoami/index.js +1 -1
- package/agents/github/skills/github-account/tools/whoami/index.ts +1 -1
- package/agents/github/skills/github-repository/handlers.js +82 -35
- package/agents/github/skills/github-repository/handlers.ts +83 -31
- package/agents/github/skills/github-repository/tools/create_pr/index.js +1 -1
- package/agents/github/skills/github-repository/tools/create_pr/index.ts +1 -1
- package/agents/github/skills/github-repository/tools/patch_file/index.js +1 -1
- package/agents/github/skills/github-repository/tools/patch_file/index.ts +1 -1
- package/agents/github/skills/github-repository/tools/prepare_workspace/index.js +1 -1
- package/agents/github/skills/github-repository/tools/prepare_workspace/index.ts +1 -1
- package/agents/github/skills/github-repository/tools/push_branch/index.js +1 -1
- package/agents/github/skills/github-repository/tools/push_branch/index.ts +1 -1
- package/agents/github/skills/github-subscriptions/handlers.js +85 -22
- package/agents/github/skills/github-subscriptions/handlers.ts +86 -22
- package/agents/github/skills/github-subscriptions/tools/star/index.js +1 -1
- package/agents/github/skills/github-subscriptions/tools/star/index.ts +1 -1
- package/agents/github/skills/github-subscriptions/tools/subscribe/index.js +1 -1
- package/agents/github/skills/github-subscriptions/tools/subscribe/index.ts +1 -1
- package/agents/github/skills/github-subscriptions/tools/subscriptions/index.js +1 -1
- package/agents/github/skills/github-subscriptions/tools/subscriptions/index.ts +1 -1
- package/agents/github/skills/github-subscriptions/tools/unsubscribe/index.js +1 -1
- package/agents/github/skills/github-subscriptions/tools/unsubscribe/index.ts +1 -1
- package/commands/github/endpoint/definition.js +2 -2
- package/commands/github/endpoint/definition.ts +2 -2
- package/lib/github-channel-context.d.ts +2 -1
- package/lib/github-channel-context.js +6 -2
- package/lib/workspace-manager.d.ts +8 -7
- package/lib/workspace-manager.js +62 -50
- package/package.json +19 -19
- package/schema.json +41 -6
- package/src/github-channel-context.ts +7 -1
- package/src/workspace-manager.ts +62 -50
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createMessages, resolveLocale, type Locale } from 'zhin.js/i18n';
|
|
1
2
|
import type { Message } from 'zhin.js';
|
|
2
3
|
import { getCurrentCommMessage } from '@zhin.js/agent/security';
|
|
3
4
|
import type { GithubClient } from '../../../../src/client.js';
|
|
@@ -15,41 +16,43 @@ function subscriptionsModel(client: GithubClient) {
|
|
|
15
16
|
} | undefined;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
export async function executeGithubStar(args: { action: 'star' | 'unstar' | 'check'; repo: string }, client: GithubClient, commMessage?: Message) {
|
|
19
|
+
export async function executeGithubStar(args: { action: 'star' | 'unstar' | 'check'; repo: string }, client: GithubClient, commMessage?: Message, preference?: Locale) {
|
|
19
20
|
const msg = commMessage ?? getCurrentCommMessage();
|
|
21
|
+
const locale = resolveLocale(preference ?? msg?.locale);
|
|
20
22
|
const gh = await client.getUserOrDefaultApi(msg?.clientAdapter, msg?.sender?.id);
|
|
21
23
|
switch (args.action) {
|
|
22
24
|
case 'star': {
|
|
23
25
|
const r = await gh.starRepo(args.repo);
|
|
24
|
-
return r.ok ?
|
|
26
|
+
return r.ok ? t(locale, 'starred', { repo: args.repo }) : `❌ ${r.data?.message || JSON.stringify(r.data)}`;
|
|
25
27
|
}
|
|
26
28
|
case 'unstar': {
|
|
27
29
|
const r = await gh.unstarRepo(args.repo);
|
|
28
|
-
return r.ok ?
|
|
30
|
+
return r.ok ? t(locale, 'unstarred', { repo: args.repo }) : `❌ ${r.data?.message || JSON.stringify(r.data)}`;
|
|
29
31
|
}
|
|
30
32
|
case 'check': {
|
|
31
33
|
const starred = await gh.isStarred(args.repo);
|
|
32
|
-
return starred ?
|
|
34
|
+
return starred ? t(locale, 'starred', { repo: args.repo }) : t(locale, 'notStarred', { repo: args.repo });
|
|
33
35
|
}
|
|
34
36
|
default:
|
|
35
|
-
return
|
|
37
|
+
return t(locale, 'unknownAction', { action: args.action });
|
|
36
38
|
}
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
export async function executeGithubSubscribe(args: { repo: string; events?: string }, client: GithubClient, commMessage?: Message) {
|
|
41
|
+
export async function executeGithubSubscribe(args: { repo: string; events?: string }, client: GithubClient, commMessage?: Message, preference?: Locale) {
|
|
40
42
|
const msg = commMessage ?? getCurrentCommMessage();
|
|
43
|
+
const locale = resolveLocale(preference ?? msg?.locale);
|
|
41
44
|
if (!msg?.clientAdapter || !msg?.sender?.id || !msg?.conversation.id || !msg?.endpointId) {
|
|
42
|
-
return
|
|
45
|
+
return t(locale, 'missingChannel');
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
const model = subscriptionsModel(client);
|
|
46
|
-
if (!model) return
|
|
49
|
+
if (!model) return t(locale, 'databaseNotReady');
|
|
47
50
|
|
|
48
51
|
const validEvents: EventType[] = ['push', 'issue', 'star', 'fork', 'unstar', 'pull_request'];
|
|
49
52
|
const events: EventType[] = args.events
|
|
50
53
|
? args.events.split(',').map((s) => s.trim()).filter((e): e is EventType => validEvents.includes(e as EventType))
|
|
51
54
|
: validEvents;
|
|
52
|
-
if (!events.length) return
|
|
55
|
+
if (!events.length) return t(locale, 'invalidEvents', { events: validEvents.join(', ') });
|
|
53
56
|
|
|
54
57
|
const [existing] = await model.select().where({
|
|
55
58
|
repo: args.repo,
|
|
@@ -59,7 +62,7 @@ export async function executeGithubSubscribe(args: { repo: string; events?: stri
|
|
|
59
62
|
});
|
|
60
63
|
if (existing) {
|
|
61
64
|
await model.update({ events, target_type: msg.conversation.kind || 'private' }).where({ id: existing.id });
|
|
62
|
-
return
|
|
65
|
+
return t(locale, 'updated', { repo: args.repo, events: events.join(', ') });
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
await model.insert({
|
|
@@ -71,17 +74,18 @@ export async function executeGithubSubscribe(args: { repo: string; events?: stri
|
|
|
71
74
|
adapter: msg.clientAdapter,
|
|
72
75
|
endpoint: msg.endpointId,
|
|
73
76
|
});
|
|
74
|
-
return
|
|
77
|
+
return t(locale, 'subscribed', { repo: args.repo, events: events.join(', ') });
|
|
75
78
|
}
|
|
76
79
|
|
|
77
|
-
export async function executeGithubUnsubscribe(args: { repo: string }, client: GithubClient, commMessage?: Message) {
|
|
80
|
+
export async function executeGithubUnsubscribe(args: { repo: string }, client: GithubClient, commMessage?: Message, preference?: Locale) {
|
|
78
81
|
const msg = commMessage ?? getCurrentCommMessage();
|
|
82
|
+
const locale = resolveLocale(preference ?? msg?.locale);
|
|
79
83
|
if (!msg?.clientAdapter || !msg?.conversation.id || !msg?.endpointId) {
|
|
80
|
-
return
|
|
84
|
+
return t(locale, 'missingChannel');
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
const model = subscriptionsModel(client);
|
|
84
|
-
if (!model) return
|
|
88
|
+
if (!model) return t(locale, 'databaseNotReady');
|
|
85
89
|
|
|
86
90
|
const [existing] = await model.select().where({
|
|
87
91
|
repo: args.repo,
|
|
@@ -89,31 +93,91 @@ export async function executeGithubUnsubscribe(args: { repo: string }, client: G
|
|
|
89
93
|
adapter: msg.clientAdapter,
|
|
90
94
|
endpoint: msg.endpointId,
|
|
91
95
|
});
|
|
92
|
-
if (!existing) return
|
|
96
|
+
if (!existing) return t(locale, 'notSubscribed', { repo: args.repo });
|
|
93
97
|
|
|
94
98
|
await model.delete().where({ id: existing.id });
|
|
95
|
-
return
|
|
99
|
+
return t(locale, 'unsubscribed', { repo: args.repo });
|
|
96
100
|
}
|
|
97
101
|
|
|
98
|
-
export async function executeGithubSubscriptions(_args: Record<string, never>, client: GithubClient, commMessage?: Message) {
|
|
102
|
+
export async function executeGithubSubscriptions(_args: Record<string, never>, client: GithubClient, commMessage?: Message, preference?: Locale) {
|
|
99
103
|
const msg = commMessage ?? getCurrentCommMessage();
|
|
104
|
+
const locale = resolveLocale(preference ?? msg?.locale);
|
|
100
105
|
if (!msg?.clientAdapter || !msg?.conversation.id || !msg?.endpointId) {
|
|
101
|
-
return
|
|
106
|
+
return t(locale, 'missingChannel');
|
|
102
107
|
}
|
|
103
108
|
|
|
104
109
|
const model = subscriptionsModel(client);
|
|
105
|
-
if (!model) return
|
|
110
|
+
if (!model) return t(locale, 'databaseNotReady');
|
|
106
111
|
|
|
107
112
|
const subs = await model.select().where({
|
|
108
113
|
target_id: msg.conversation.id,
|
|
109
114
|
adapter: msg.clientAdapter,
|
|
110
115
|
endpoint: msg.endpointId,
|
|
111
116
|
});
|
|
112
|
-
if (!subs?.length) return
|
|
117
|
+
if (!subs?.length) return t(locale, 'empty');
|
|
113
118
|
|
|
114
|
-
return
|
|
119
|
+
return t(locale, 'list', { count: subs.length }) +
|
|
115
120
|
subs.map((s: any) => {
|
|
116
121
|
const events = Array.isArray(s.events) ? s.events : [];
|
|
117
|
-
return ` 📦 ${s.repo}\n 📡 ${events.join(', ') || '
|
|
122
|
+
return ` 📦 ${s.repo}\n 📡 ${events.join(', ') || t(locale, 'noEvents')}`;
|
|
118
123
|
}).join('\n\n');
|
|
119
124
|
}
|
|
125
|
+
|
|
126
|
+
const t = createMessages({
|
|
127
|
+
"starred": {
|
|
128
|
+
"zh-CN": "⭐ 已 Star {repo}",
|
|
129
|
+
"en": "⭐ Starred: {repo}"
|
|
130
|
+
},
|
|
131
|
+
"unstarred": {
|
|
132
|
+
"zh-CN": "💔 已取消 Star {repo}",
|
|
133
|
+
"en": "💔 Unstarred: {repo}"
|
|
134
|
+
},
|
|
135
|
+
"notStarred": {
|
|
136
|
+
"zh-CN": "☆ 尚未 Star {repo}",
|
|
137
|
+
"en": "☆ Not starred: {repo}"
|
|
138
|
+
},
|
|
139
|
+
"unknownAction": {
|
|
140
|
+
"zh-CN": "❌ 未知操作: {action}",
|
|
141
|
+
"en": "❌ Unknown action: {action}"
|
|
142
|
+
},
|
|
143
|
+
"missingChannel": {
|
|
144
|
+
"zh-CN": "❌ 无法获取当前聊天通道信息",
|
|
145
|
+
"en": "❌ Current chat channel is unavailable"
|
|
146
|
+
},
|
|
147
|
+
"databaseNotReady": {
|
|
148
|
+
"zh-CN": "❌ 数据库未就绪",
|
|
149
|
+
"en": "❌ Database is not ready"
|
|
150
|
+
},
|
|
151
|
+
"invalidEvents": {
|
|
152
|
+
"zh-CN": "❌ 无效的事件类型,可选: {events}",
|
|
153
|
+
"en": "❌ Invalid event types. Available: {events}"
|
|
154
|
+
},
|
|
155
|
+
"updated": {
|
|
156
|
+
"zh-CN": "✅ 已更新订阅 {repo}\n📡 事件: {events}",
|
|
157
|
+
"en": "✅ Subscription updated: {repo}\n📡 Events: {events}"
|
|
158
|
+
},
|
|
159
|
+
"subscribed": {
|
|
160
|
+
"zh-CN": "✅ 已订阅 {repo}\n📡 事件: {events}\n📌 通知将推送到当前通道",
|
|
161
|
+
"en": "✅ Subscribed to {repo}\n📡 Events: {events}\n📌 Notifications will be sent to this channel"
|
|
162
|
+
},
|
|
163
|
+
"notSubscribed": {
|
|
164
|
+
"zh-CN": "📭 当前通道未订阅 {repo}",
|
|
165
|
+
"en": "📭 This channel is not subscribed to {repo}"
|
|
166
|
+
},
|
|
167
|
+
"unsubscribed": {
|
|
168
|
+
"zh-CN": "✅ 已取消订阅 {repo}",
|
|
169
|
+
"en": "✅ Unsubscribed from {repo}"
|
|
170
|
+
},
|
|
171
|
+
"empty": {
|
|
172
|
+
"zh-CN": "📭 当前通道没有任何 GitHub 订阅",
|
|
173
|
+
"en": "📭 This channel has no GitHub subscriptions"
|
|
174
|
+
},
|
|
175
|
+
"list": {
|
|
176
|
+
"zh-CN": "📋 当前通道订阅 ({count}):\n\n",
|
|
177
|
+
"en": "📋 Channel subscriptions ({count}):\n\n"
|
|
178
|
+
},
|
|
179
|
+
"noEvents": {
|
|
180
|
+
"zh-CN": "(无事件)",
|
|
181
|
+
"en": "(no events)"
|
|
182
|
+
}
|
|
183
|
+
});
|
|
@@ -11,6 +11,6 @@ export default defineAgentTool<{ action: 'star' | 'unstar' | 'check'; repo: stri
|
|
|
11
11
|
}),
|
|
12
12
|
tags: ['github'],
|
|
13
13
|
async execute(input, context) {
|
|
14
|
-
return executeGithubStar(input, context.$client);
|
|
14
|
+
return executeGithubStar(input, context.$client, undefined, context.locale);
|
|
15
15
|
},
|
|
16
16
|
});
|
|
@@ -11,6 +11,6 @@ export default defineAgentTool({
|
|
|
11
11
|
adapter: 'github',
|
|
12
12
|
tags: ['github'],
|
|
13
13
|
async execute(input, context) {
|
|
14
|
-
return executeGithubSubscribe(input, context.$client);
|
|
14
|
+
return executeGithubSubscribe(input, context.$client, undefined, context.locale);
|
|
15
15
|
},
|
|
16
16
|
});
|
|
@@ -11,6 +11,6 @@ export default defineAgentTool<{ repo: string; events?: string }>({
|
|
|
11
11
|
adapter: 'github',
|
|
12
12
|
tags: ['github'],
|
|
13
13
|
async execute(input, context) {
|
|
14
|
-
return executeGithubSubscribe(input, context.$client);
|
|
14
|
+
return executeGithubSubscribe(input, context.$client, undefined, context.locale);
|
|
15
15
|
},
|
|
16
16
|
});
|
|
@@ -8,6 +8,6 @@ export default defineAgentTool<{}>({
|
|
|
8
8
|
adapter: 'github',
|
|
9
9
|
tags: ['github'],
|
|
10
10
|
async execute(input, context) {
|
|
11
|
-
return executeGithubSubscriptions({}, context.$client);
|
|
11
|
+
return executeGithubSubscriptions({}, context.$client, undefined, context.locale);
|
|
12
12
|
},
|
|
13
13
|
});
|
|
@@ -10,6 +10,6 @@ export default defineAgentTool({
|
|
|
10
10
|
adapter: 'github',
|
|
11
11
|
tags: ['github'],
|
|
12
12
|
async execute(input, context) {
|
|
13
|
-
return executeGithubUnsubscribe(input, context.$client);
|
|
13
|
+
return executeGithubUnsubscribe(input, context.$client, undefined, context.locale);
|
|
14
14
|
},
|
|
15
15
|
});
|
|
@@ -10,6 +10,6 @@ export default defineAgentTool<{ repo: string }>({
|
|
|
10
10
|
adapter: 'github',
|
|
11
11
|
tags: ['github'],
|
|
12
12
|
async execute(input, context) {
|
|
13
|
-
return executeGithubUnsubscribe(input, context.$client);
|
|
13
|
+
return executeGithubUnsubscribe(input, context.$client, undefined, context.locale);
|
|
14
14
|
},
|
|
15
15
|
});
|
|
@@ -15,8 +15,8 @@ export const githubEndpointCommands = createEndpointCommands({
|
|
|
15
15
|
adapterDisplayName: 'GitHub',
|
|
16
16
|
fields: [
|
|
17
17
|
{ key: 'app_id', required: true, env: true, description: 'GitHub App ID' },
|
|
18
|
-
{ key: 'private_key', required: true, description: 'PEM 文件路径(推荐,如 ./data/xxx.pem)或 PEM 内容' },
|
|
19
|
-
{ key: 'webhook_secret', env: true, description: 'webhook 签名密钥(不配则 API-only)' },
|
|
18
|
+
{ key: 'private_key', required: true, description: 'PEM 文件路径(推荐,如 ./data/xxx.pem)或 PEM 内容', descriptionI18n: { en: "PEM file path (recommended, such as ./data/app.pem) or PEM content" } },
|
|
19
|
+
{ key: 'webhook_secret', env: true, description: 'webhook 签名密钥(不配则 API-only)', descriptionI18n: { en: "Webhook signing secret (API-only when omitted)" } },
|
|
20
20
|
],
|
|
21
21
|
running: (use) => use(githubRuntimeStateToken).endpoints.values(),
|
|
22
22
|
describeEntry: (entry) => `app_id: ${String(entry.app_id)}`,
|
|
@@ -15,8 +15,8 @@ export const githubEndpointCommands = createEndpointCommands({
|
|
|
15
15
|
adapterDisplayName: 'GitHub',
|
|
16
16
|
fields: [
|
|
17
17
|
{ key: 'app_id', required: true, env: true, description: 'GitHub App ID' },
|
|
18
|
-
{ key: 'private_key', required: true, description: 'PEM 文件路径(推荐,如 ./data/xxx.pem)或 PEM 内容' },
|
|
19
|
-
{ key: 'webhook_secret', env: true, description: 'webhook 签名密钥(不配则 API-only)' },
|
|
18
|
+
{ key: 'private_key', required: true, description: 'PEM 文件路径(推荐,如 ./data/xxx.pem)或 PEM 内容', descriptionI18n: { en: "PEM file path (recommended, such as ./data/app.pem) or PEM content" } },
|
|
19
|
+
{ key: 'webhook_secret', env: true, description: 'webhook 签名密钥(不配则 API-only)', descriptionI18n: { en: "Webhook signing secret (API-only when omitted)" } },
|
|
20
20
|
],
|
|
21
21
|
running: (use) => use(githubRuntimeStateToken).endpoints.values(),
|
|
22
22
|
describeEntry: (entry) => `app_id: ${String(entry.app_id)}`,
|
|
@@ -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 { type Locale } from 'zhin.js/i18n';
|
|
5
6
|
import { type ParsedChannel } from './types.js';
|
|
6
7
|
import type { GhClient } from './gh-client.js';
|
|
7
8
|
export interface GitHubChannelContext extends ParsedChannel {
|
|
@@ -9,7 +10,7 @@ export interface GitHubChannelContext extends ParsedChannel {
|
|
|
9
10
|
}
|
|
10
11
|
export declare function parseMessageChannel(message: Message): GitHubChannelContext | null;
|
|
11
12
|
export declare function issueBranchName(number: number): string;
|
|
12
|
-
export declare function resolveWorkspaceBranch(gh: GhClient, ctx: GitHubChannelContext): Promise<{
|
|
13
|
+
export declare function resolveWorkspaceBranch(gh: GhClient, ctx: GitHubChannelContext, locale?: Locale): Promise<{
|
|
13
14
|
branch: string;
|
|
14
15
|
base: string;
|
|
15
16
|
}>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createMessages } from 'zhin.js/i18n';
|
|
1
2
|
import { buildChannelId, parseChannelId } from './types.js';
|
|
2
3
|
export function parseMessageChannel(message) {
|
|
3
4
|
const channelId = message.conversation.id;
|
|
@@ -11,14 +12,14 @@ export function parseMessageChannel(message) {
|
|
|
11
12
|
export function issueBranchName(number) {
|
|
12
13
|
return `zhin/bot/issue-${number}`;
|
|
13
14
|
}
|
|
14
|
-
export async function resolveWorkspaceBranch(gh, ctx) {
|
|
15
|
+
export async function resolveWorkspaceBranch(gh, ctx, locale) {
|
|
15
16
|
if (ctx.type === 'pr') {
|
|
16
17
|
const pr = await gh.getPR(ctx.repo, ctx.number);
|
|
17
18
|
if (pr.ok && pr.data?.head?.ref) {
|
|
18
19
|
const base = pr.data.base?.ref ?? 'main';
|
|
19
20
|
return { branch: pr.data.head.ref, base };
|
|
20
21
|
}
|
|
21
|
-
throw new Error(
|
|
22
|
+
throw new Error(t(locale, 'missingHead', { number: ctx.number }));
|
|
22
23
|
}
|
|
23
24
|
const repoInfo = await gh.getRepo(ctx.repo);
|
|
24
25
|
const base = repoInfo.ok && repoInfo.data?.default_branch
|
|
@@ -29,3 +30,6 @@ export async function resolveWorkspaceBranch(gh, ctx) {
|
|
|
29
30
|
export function formatChannelContext(ctx) {
|
|
30
31
|
return buildChannelId(ctx.repo, ctx.type, ctx.number);
|
|
31
32
|
}
|
|
33
|
+
const t = createMessages({
|
|
34
|
+
missingHead: { 'zh-CN': '无法读取 PR #{number} 的 head 分支', en: 'Cannot read the head branch of PR #{number}' },
|
|
35
|
+
});
|
|
@@ -1,21 +1,22 @@
|
|
|
1
|
+
import { type Locale } from 'zhin.js/i18n';
|
|
1
2
|
import type { GhClient, GitHubBotIdentity } from './gh-client.js';
|
|
2
3
|
/**
|
|
3
4
|
* 校验 GitHub "owner/name" 全名,防止路径穿越与 git 选项注入。
|
|
4
5
|
* 返回通过校验的原值,便于调用方直接使用「已消毒」的变量。
|
|
5
6
|
*/
|
|
6
|
-
export declare function assertRepoFullName(repo: string): string;
|
|
7
|
+
export declare function assertRepoFullName(repo: string, locale?: Locale): string;
|
|
7
8
|
/**
|
|
8
9
|
* 校验 git ref 名称,拒绝选项注入(`-` 前缀)与 git 非法字符。
|
|
9
10
|
*/
|
|
10
|
-
export declare function assertGitRefName(ref: string): string;
|
|
11
|
+
export declare function assertGitRefName(ref: string, locale?: Locale): string;
|
|
11
12
|
export declare class WorkspaceManager {
|
|
12
13
|
private readonly gh;
|
|
13
14
|
private readonly rootDir;
|
|
14
15
|
constructor(gh: GhClient, rootDir: string);
|
|
15
|
-
getRepoPath(repo: string): string;
|
|
16
|
+
getRepoPath(repo: string, locale?: Locale): string;
|
|
16
17
|
private runGit;
|
|
17
|
-
configureBotGit(repoPath: string, identity: GitHubBotIdentity): Promise<void>;
|
|
18
|
-
ensureRepo(repo: string): Promise<string>;
|
|
19
|
-
checkoutBranch(repo: string, branch: string, baseRef: string): Promise<string>;
|
|
20
|
-
commitAndPush(repo: string, branch: string, message: string): Promise<string>;
|
|
18
|
+
configureBotGit(repoPath: string, identity: GitHubBotIdentity, locale?: Locale): Promise<void>;
|
|
19
|
+
ensureRepo(repo: string, locale?: Locale): Promise<string>;
|
|
20
|
+
checkoutBranch(repo: string, branch: string, baseRef: string, locale?: Locale): Promise<string>;
|
|
21
|
+
commitAndPush(repo: string, branch: string, message: string, locale?: Locale): Promise<string>;
|
|
21
22
|
}
|
package/lib/workspace-manager.js
CHANGED
|
@@ -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 } from 'zhin.js/i18n';
|
|
7
8
|
const REPO_FULL_NAME_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
8
9
|
const GIT_REF_ILLEGAL_CHARS = new Set('~^:?*[\\`;');
|
|
9
10
|
function containsIllegalGitRefCharacter(ref) {
|
|
@@ -19,27 +20,27 @@ function containsIllegalGitRefCharacter(ref) {
|
|
|
19
20
|
* 校验 GitHub "owner/name" 全名,防止路径穿越与 git 选项注入。
|
|
20
21
|
* 返回通过校验的原值,便于调用方直接使用「已消毒」的变量。
|
|
21
22
|
*/
|
|
22
|
-
export function assertRepoFullName(repo) {
|
|
23
|
+
export function assertRepoFullName(repo, locale) {
|
|
23
24
|
if (typeof repo !== 'string' ||
|
|
24
25
|
!REPO_FULL_NAME_RE.test(repo) ||
|
|
25
26
|
repo.split('/').some((seg) => seg === '' || seg.startsWith('-') || /^\.+$/.test(seg))) {
|
|
26
|
-
throw new TypeError(
|
|
27
|
+
throw new TypeError(t(locale, 'invalidRepo', { value: JSON.stringify(repo) }));
|
|
27
28
|
}
|
|
28
29
|
return repo;
|
|
29
30
|
}
|
|
30
31
|
/**
|
|
31
32
|
* 校验 git ref 名称,拒绝选项注入(`-` 前缀)与 git 非法字符。
|
|
32
33
|
*/
|
|
33
|
-
export function assertGitRefName(ref) {
|
|
34
|
+
export function assertGitRefName(ref, locale) {
|
|
34
35
|
if (typeof ref !== 'string' || ref.length === 0) {
|
|
35
|
-
throw new TypeError(
|
|
36
|
+
throw new TypeError(t(locale, 'invalidRef', { value: JSON.stringify(ref) }));
|
|
36
37
|
}
|
|
37
38
|
if (ref.startsWith('-') ||
|
|
38
39
|
ref.includes('..') ||
|
|
39
40
|
containsIllegalGitRefCharacter(ref) ||
|
|
40
41
|
ref.endsWith('/') ||
|
|
41
42
|
ref.endsWith('.')) {
|
|
42
|
-
throw new TypeError(
|
|
43
|
+
throw new TypeError(t(locale, 'invalidRef', { value: JSON.stringify(ref) }));
|
|
43
44
|
}
|
|
44
45
|
return ref;
|
|
45
46
|
}
|
|
@@ -50,12 +51,12 @@ export class WorkspaceManager {
|
|
|
50
51
|
this.gh = gh;
|
|
51
52
|
this.rootDir = rootDir;
|
|
52
53
|
}
|
|
53
|
-
getRepoPath(repo) {
|
|
54
|
-
repo = assertRepoFullName(repo);
|
|
54
|
+
getRepoPath(repo, locale) {
|
|
55
|
+
repo = assertRepoFullName(repo, locale);
|
|
55
56
|
const [owner, name] = repo.split('/');
|
|
56
57
|
return path.join(this.rootDir, owner, name);
|
|
57
58
|
}
|
|
58
|
-
runGit(cwd, args, env) {
|
|
59
|
+
runGit(cwd, args, env, locale) {
|
|
59
60
|
return new Promise((resolve, reject) => {
|
|
60
61
|
const proc = spawn('git', args, {
|
|
61
62
|
cwd,
|
|
@@ -68,7 +69,7 @@ export class WorkspaceManager {
|
|
|
68
69
|
proc.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
69
70
|
proc.on('error', (err) => {
|
|
70
71
|
if (err.code === 'ENOENT')
|
|
71
|
-
reject(new Error(
|
|
72
|
+
reject(new Error(t(locale, 'missingGit')));
|
|
72
73
|
else
|
|
73
74
|
reject(err);
|
|
74
75
|
});
|
|
@@ -80,75 +81,86 @@ export class WorkspaceManager {
|
|
|
80
81
|
});
|
|
81
82
|
});
|
|
82
83
|
}
|
|
83
|
-
configureBotGit(repoPath, identity) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
]).then(() => undefined);
|
|
84
|
+
async configureBotGit(repoPath, identity, locale) {
|
|
85
|
+
// Both writes acquire the same .git/config.lock.
|
|
86
|
+
await this.runGit(repoPath, ['config', 'user.name', identity.login], undefined, locale);
|
|
87
|
+
await this.runGit(repoPath, ['config', 'user.email', identity.email], undefined, locale);
|
|
88
88
|
}
|
|
89
|
-
async ensureRepo(repo) {
|
|
90
|
-
repo = assertRepoFullName(repo);
|
|
89
|
+
async ensureRepo(repo, locale) {
|
|
90
|
+
repo = assertRepoFullName(repo, locale);
|
|
91
91
|
if (!this.gh.isAppAuth) {
|
|
92
|
-
throw new Error(
|
|
92
|
+
throw new Error(t(locale, 'appRequired'));
|
|
93
93
|
}
|
|
94
94
|
const token = await this.gh.ensureInstallationTokenForRepo(repo);
|
|
95
95
|
if (!token)
|
|
96
|
-
throw new Error(
|
|
97
|
-
const repoPath = this.getRepoPath(repo);
|
|
96
|
+
throw new Error(t(locale, 'repoTokenUnavailable', { repo }));
|
|
97
|
+
const repoPath = this.getRepoPath(repo, locale);
|
|
98
98
|
fs.mkdirSync(path.dirname(repoPath), { recursive: true });
|
|
99
99
|
const identity = await this.gh.getBotIdentity();
|
|
100
100
|
if (!identity)
|
|
101
|
-
throw new Error(
|
|
101
|
+
throw new Error(t(locale, 'missingIdentity'));
|
|
102
102
|
const cloneUrl = this.gh.buildCloneUrl(repo, token);
|
|
103
103
|
if (!fs.existsSync(path.join(repoPath, '.git'))) {
|
|
104
|
-
await this.runGit(path.dirname(repoPath), ['clone', cloneUrl, path.basename(repoPath)]);
|
|
104
|
+
await this.runGit(path.dirname(repoPath), ['clone', '--', cloneUrl, path.basename(repoPath)], undefined, locale);
|
|
105
105
|
}
|
|
106
106
|
else {
|
|
107
|
-
await this.runGit(repoPath, ['remote', 'set-url', 'origin', cloneUrl]);
|
|
108
|
-
await this.runGit(repoPath, ['fetch', 'origin', '--prune']);
|
|
107
|
+
await this.runGit(repoPath, ['remote', 'set-url', 'origin', cloneUrl], undefined, locale);
|
|
108
|
+
await this.runGit(repoPath, ['fetch', 'origin', '--prune'], undefined, locale);
|
|
109
109
|
}
|
|
110
|
-
await this.configureBotGit(repoPath, identity);
|
|
110
|
+
await this.configureBotGit(repoPath, identity, locale);
|
|
111
111
|
return repoPath;
|
|
112
112
|
}
|
|
113
|
-
async checkoutBranch(repo, branch, baseRef) {
|
|
114
|
-
repo = assertRepoFullName(repo);
|
|
115
|
-
branch = assertGitRefName(branch);
|
|
116
|
-
baseRef = assertGitRefName(baseRef);
|
|
117
|
-
const repoPath = await this.ensureRepo(repo);
|
|
118
|
-
const localBranches = await this.runGit(repoPath, ['branch', '--list', branch]);
|
|
113
|
+
async checkoutBranch(repo, branch, baseRef, locale) {
|
|
114
|
+
repo = assertRepoFullName(repo, locale);
|
|
115
|
+
branch = assertGitRefName(branch, locale);
|
|
116
|
+
baseRef = assertGitRefName(baseRef, locale);
|
|
117
|
+
const repoPath = await this.ensureRepo(repo, locale);
|
|
118
|
+
const localBranches = await this.runGit(repoPath, ['branch', '--list', branch], undefined, locale);
|
|
119
119
|
if (localBranches.trim()) {
|
|
120
|
-
await this.runGit(repoPath, ['checkout', branch]);
|
|
121
|
-
await this.runGit(repoPath, ['pull', '--rebase', 'origin', branch]).catch(async () => {
|
|
122
|
-
await this.runGit(repoPath, ['fetch', 'origin', branch]);
|
|
120
|
+
await this.runGit(repoPath, ['checkout', branch], undefined, locale);
|
|
121
|
+
await this.runGit(repoPath, ['pull', '--rebase', '--', 'origin', branch], undefined, locale).catch(async () => {
|
|
122
|
+
await this.runGit(repoPath, ['fetch', '--', 'origin', branch], undefined, locale);
|
|
123
123
|
});
|
|
124
124
|
return repoPath;
|
|
125
125
|
}
|
|
126
|
-
const remoteBranch = await this.runGit(repoPath, ['ls-remote', '--heads', 'origin', branch]);
|
|
126
|
+
const remoteBranch = await this.runGit(repoPath, ['ls-remote', '--heads', '--', 'origin', branch], undefined, locale);
|
|
127
127
|
if (remoteBranch.trim()) {
|
|
128
|
-
await this.runGit(repoPath, ['checkout', '-B', branch, `origin/${branch}`]);
|
|
128
|
+
await this.runGit(repoPath, ['checkout', '-B', branch, `origin/${branch}`], undefined, locale);
|
|
129
129
|
return repoPath;
|
|
130
130
|
}
|
|
131
|
-
await this.runGit(repoPath, ['fetch', 'origin', baseRef]);
|
|
132
|
-
await this.runGit(repoPath, ['checkout', '-B', branch, `origin/${baseRef}`]);
|
|
131
|
+
await this.runGit(repoPath, ['fetch', '--', 'origin', baseRef], undefined, locale);
|
|
132
|
+
await this.runGit(repoPath, ['checkout', '-B', branch, `origin/${baseRef}`], undefined, locale);
|
|
133
133
|
return repoPath;
|
|
134
134
|
}
|
|
135
|
-
async commitAndPush(repo, branch, message) {
|
|
136
|
-
repo = assertRepoFullName(repo);
|
|
137
|
-
branch = assertGitRefName(branch);
|
|
138
|
-
const repoPath = this.getRepoPath(repo);
|
|
135
|
+
async commitAndPush(repo, branch, message, locale) {
|
|
136
|
+
repo = assertRepoFullName(repo, locale);
|
|
137
|
+
branch = assertGitRefName(branch, locale);
|
|
138
|
+
const repoPath = this.getRepoPath(repo, locale);
|
|
139
139
|
if (!fs.existsSync(repoPath)) {
|
|
140
|
-
throw new Error(
|
|
140
|
+
throw new Error(t(locale, 'missingWorkspace', { path: repoPath }));
|
|
141
141
|
}
|
|
142
142
|
const token = await this.gh.ensureInstallationTokenForRepo(repo);
|
|
143
143
|
if (!token)
|
|
144
|
-
throw new Error(
|
|
145
|
-
await this.runGit(repoPath, ['remote', 'set-url', 'origin', this.gh.buildCloneUrl(repo, token)]);
|
|
146
|
-
const status = await this.runGit(repoPath, ['status', '--porcelain']);
|
|
144
|
+
throw new Error(t(locale, 'missingToken'));
|
|
145
|
+
await this.runGit(repoPath, ['remote', 'set-url', 'origin', this.gh.buildCloneUrl(repo, token)], undefined, locale);
|
|
146
|
+
const status = await this.runGit(repoPath, ['status', '--porcelain'], undefined, locale);
|
|
147
147
|
if (!status.trim())
|
|
148
|
-
return '
|
|
149
|
-
await this.runGit(repoPath, ['add', '-A']);
|
|
150
|
-
await this.runGit(repoPath, ['commit', '-m', message]);
|
|
151
|
-
await this.runGit(repoPath, ['push', '-u', 'origin', branch]);
|
|
152
|
-
return
|
|
148
|
+
return t(locale, 'noChanges');
|
|
149
|
+
await this.runGit(repoPath, ['add', '-A'], undefined, locale);
|
|
150
|
+
await this.runGit(repoPath, ['commit', '-m', message], undefined, locale);
|
|
151
|
+
await this.runGit(repoPath, ['push', '-u', 'origin', branch], undefined, locale);
|
|
152
|
+
return t(locale, 'pushed', { branch });
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
|
+
const t = createMessages({
|
|
156
|
+
invalidRepo: { 'zh-CN': '非法的仓库全名: {value}', en: 'Invalid repository name: {value}' },
|
|
157
|
+
invalidRef: { 'zh-CN': '非法的 git ref: {value}', en: 'Invalid git ref: {value}' },
|
|
158
|
+
missingGit: { 'zh-CN': 'git 未安装', en: 'git is not installed' },
|
|
159
|
+
appRequired: { 'zh-CN': 'GitHub App 认证未配置,无法托管工作区', en: 'GitHub App authentication is not configured; the workspace is unavailable' },
|
|
160
|
+
repoTokenUnavailable: { 'zh-CN': '无法获取 {repo} 的 Installation Token', en: 'Cannot obtain an Installation Token for {repo}' },
|
|
161
|
+
missingIdentity: { 'zh-CN': '无法解析 GitHub App Bot 身份', en: 'Cannot resolve the GitHub App bot identity' },
|
|
162
|
+
missingWorkspace: { 'zh-CN': '工作区不存在: {path},请先准备工作区', en: 'Workspace does not exist: {path}. Prepare the workspace first' },
|
|
163
|
+
missingToken: { 'zh-CN': 'Installation Token 不可用', en: 'Installation Token is unavailable' },
|
|
164
|
+
noChanges: { 'zh-CN': '没有可提交的变更', en: 'No changes to commit' },
|
|
165
|
+
pushed: { 'zh-CN': '已 push 到 origin/{branch}', en: 'Pushed to origin/{branch}' },
|
|
166
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-github",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.17",
|
|
4
4
|
"description": "Zhin.js GitHub adapter for Plugin Runtime (App auth + webhook)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -39,25 +39,25 @@
|
|
|
39
39
|
"directory": "plugins/adapters/github"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@zhin.js/adapter": "1.1.
|
|
43
|
-
"@zhin.js/agent-feature": "1.1.
|
|
44
|
-
"@zhin.js/core": "1.1.
|
|
45
|
-
"@zhin.js/feature-kit": "1.1.
|
|
46
|
-
"@zhin.js/host-http": "1.1.
|
|
47
|
-
"@zhin.js/im-contract": "1.1.
|
|
42
|
+
"@zhin.js/adapter": "1.1.24",
|
|
43
|
+
"@zhin.js/agent-feature": "1.1.10",
|
|
44
|
+
"@zhin.js/core": "1.1.52",
|
|
45
|
+
"@zhin.js/feature-kit": "1.1.9",
|
|
46
|
+
"@zhin.js/host-http": "1.1.11",
|
|
47
|
+
"@zhin.js/im-contract": "1.1.8",
|
|
48
48
|
"@zhin.js/logger": "1.1.2",
|
|
49
|
-
"@zhin.js/skill": "1.1.
|
|
50
|
-
"@zhin.js/tool": "1.1.
|
|
49
|
+
"@zhin.js/skill": "1.1.10",
|
|
50
|
+
"@zhin.js/tool": "1.1.11"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"zod": "^4.0.0",
|
|
54
|
-
"@zhin.js/adapter": "^1.1.
|
|
55
|
-
"@zhin.js/agent": "^1.1.
|
|
56
|
-
"@zhin.js/command": "^1.1.
|
|
57
|
-
"@zhin.js/core": "^1.1.
|
|
58
|
-
"@zhin.js/host-http": "^1.1.
|
|
59
|
-
"@zhin.js/prompt-section": "^1.1.
|
|
60
|
-
"zhin.js": "^1.1.
|
|
54
|
+
"@zhin.js/adapter": "^1.1.24",
|
|
55
|
+
"@zhin.js/agent": "^1.1.43",
|
|
56
|
+
"@zhin.js/command": "^1.1.11",
|
|
57
|
+
"@zhin.js/core": "^1.1.52",
|
|
58
|
+
"@zhin.js/host-http": "^1.1.11",
|
|
59
|
+
"@zhin.js/prompt-section": "^1.1.9",
|
|
60
|
+
"zhin.js": "^1.1.17"
|
|
61
61
|
},
|
|
62
62
|
"peerDependenciesMeta": {
|
|
63
63
|
"@zhin.js/agent": {
|
|
@@ -81,9 +81,9 @@
|
|
|
81
81
|
"typescript": "^6.0.3",
|
|
82
82
|
"vitest": "^4.1.10",
|
|
83
83
|
"zod": "^4.4.3",
|
|
84
|
-
"@zhin.js/agent": "1.1.
|
|
85
|
-
"@zhin.js/prompt-section": "1.1.
|
|
86
|
-
"zhin.js": "1.1.
|
|
84
|
+
"@zhin.js/agent": "1.1.43",
|
|
85
|
+
"@zhin.js/prompt-section": "1.1.9",
|
|
86
|
+
"zhin.js": "1.1.17"
|
|
87
87
|
},
|
|
88
88
|
"files": [
|
|
89
89
|
"adapters",
|