glab-agent 0.2.6 → 0.2.8
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/package.json +1 -1
- package/src/local-agent/agent-config.ts +9 -1
- package/src/local-agent/agent-runner.ts +44 -0
- package/src/local-agent/claude-runner.ts +189 -2
- package/src/local-agent/codex-runner.ts +107 -2
- package/src/local-agent/feishu-client.ts +226 -0
- package/src/local-agent/gitlab-glab-client.ts +82 -2
- package/src/local-agent/notifier.ts +71 -8
- package/src/local-agent/watcher.ts +496 -12
|
@@ -17,6 +17,12 @@ export interface GitlabTodoItem {
|
|
|
17
17
|
targetIid?: number;
|
|
18
18
|
body?: string;
|
|
19
19
|
sourceBranch?: string;
|
|
20
|
+
/** Note ID of the @mention comment that created this todo (for emoji reactions) */
|
|
21
|
+
noteId?: number;
|
|
22
|
+
/** GitLab user ID of the todo's author (the person who @mentioned the bot) */
|
|
23
|
+
authorId?: number;
|
|
24
|
+
/** GitLab username of the todo's author */
|
|
25
|
+
authorUsername?: string;
|
|
20
26
|
}
|
|
21
27
|
|
|
22
28
|
export interface GitlabIssue {
|
|
@@ -99,6 +105,11 @@ export interface GitlabClient {
|
|
|
99
105
|
createProject(name: string, options?: { visibility?: string; initializeWithReadme?: boolean }): Promise<{ id: number }>;
|
|
100
106
|
getRepositoryFile(projectId: number | string, filePath: string, ref?: string): Promise<{ content: string } | undefined>;
|
|
101
107
|
createOrUpdateRepositoryFile(projectId: number | string, filePath: string, content: string, commitMessage: string, options?: { create?: boolean }): Promise<void>;
|
|
108
|
+
addAwardEmoji(projectId: number | string, issueIid: number, noteId: number, emojiName: string): Promise<{ id: number }>;
|
|
109
|
+
removeAwardEmoji(projectId: number | string, issueIid: number, noteId: number, awardId: number): Promise<void>;
|
|
110
|
+
listAwardEmoji(projectId: number | string, issueIid: number, noteId: number): Promise<Array<{ id: number; name: string; user: { id: number } }>>;
|
|
111
|
+
findMergeRequestByBranch(projectId: number | string, sourceBranch: string): Promise<{ iid: number; webUrl: string } | undefined>;
|
|
112
|
+
getUserById(userId: number): Promise<{ id: number; email?: string; username: string }>;
|
|
102
113
|
}
|
|
103
114
|
|
|
104
115
|
interface GitlabGlabClientOptions {
|
|
@@ -224,6 +235,13 @@ export function parseTodoUniversal(payload: Payload): GitlabTodoItem | undefined
|
|
|
224
235
|
? target.source_branch as string
|
|
225
236
|
: undefined;
|
|
226
237
|
|
|
238
|
+
const note = payload.note as Payload | undefined;
|
|
239
|
+
const noteId = parseInteger(note?.id);
|
|
240
|
+
|
|
241
|
+
const author = payload.author as Payload | undefined;
|
|
242
|
+
const authorId = parseInteger(author?.id);
|
|
243
|
+
const authorUsername = typeof author?.username === "string" ? author.username as string : undefined;
|
|
244
|
+
|
|
227
245
|
return {
|
|
228
246
|
id,
|
|
229
247
|
projectId,
|
|
@@ -237,7 +255,10 @@ export function parseTodoUniversal(payload: Payload): GitlabTodoItem | undefined
|
|
|
237
255
|
createdAt: typeof payload.created_at === "string" ? payload.created_at : undefined,
|
|
238
256
|
labels: parseLabels(target),
|
|
239
257
|
body: typeof payload.body === "string" ? payload.body : undefined,
|
|
240
|
-
sourceBranch
|
|
258
|
+
sourceBranch,
|
|
259
|
+
noteId,
|
|
260
|
+
authorId,
|
|
261
|
+
authorUsername
|
|
241
262
|
};
|
|
242
263
|
}
|
|
243
264
|
|
|
@@ -749,6 +770,65 @@ export class GitlabGlabClient implements GitlabClient {
|
|
|
749
770
|
});
|
|
750
771
|
}
|
|
751
772
|
|
|
773
|
+
async addAwardEmoji(projectId: number | string, issueIid: number, noteId: number, emojiName: string): Promise<{ id: number }> {
|
|
774
|
+
const stdout = await this.request(`projects/${projectId}/issues/${issueIid}/notes/${noteId}/award_emoji`, {
|
|
775
|
+
method: "POST",
|
|
776
|
+
fields: { name: emojiName }
|
|
777
|
+
});
|
|
778
|
+
const payload = JSON.parse(stdout) as Payload;
|
|
779
|
+
return { id: Number(payload.id) };
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
async removeAwardEmoji(projectId: number | string, issueIid: number, noteId: number, awardId: number): Promise<void> {
|
|
783
|
+
await this.request(`projects/${projectId}/issues/${issueIid}/notes/${noteId}/award_emoji/${awardId}`, {
|
|
784
|
+
method: "DELETE",
|
|
785
|
+
fields: {}
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
async listAwardEmoji(projectId: number | string, issueIid: number, noteId: number): Promise<Array<{ id: number; name: string; user: { id: number } }>> {
|
|
790
|
+
const payload = await this.readJson(`projects/${projectId}/issues/${issueIid}/notes/${noteId}/award_emoji`);
|
|
791
|
+
if (!Array.isArray(payload)) return [];
|
|
792
|
+
return payload.map((item: unknown) => {
|
|
793
|
+
const p = (item ?? {}) as Record<string, unknown>;
|
|
794
|
+
return {
|
|
795
|
+
id: Number(p.id),
|
|
796
|
+
name: String(p.name ?? ""),
|
|
797
|
+
user: { id: Number((p.user as Record<string, unknown> | undefined)?.id ?? 0) }
|
|
798
|
+
};
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
async findMergeRequestByBranch(projectId: number | string, sourceBranch: string): Promise<{ iid: number; webUrl: string } | undefined> {
|
|
803
|
+
try {
|
|
804
|
+
const endpoint = this.withQuery(`projects/${projectId}/merge_requests`, {
|
|
805
|
+
source_branch: sourceBranch,
|
|
806
|
+
state: "opened",
|
|
807
|
+
per_page: "1"
|
|
808
|
+
});
|
|
809
|
+
const payload = await this.readJson(endpoint);
|
|
810
|
+
if (!Array.isArray(payload) || payload.length === 0) return undefined;
|
|
811
|
+
const mr = (payload[0] ?? {}) as Record<string, unknown>;
|
|
812
|
+
const iid = Number(mr.iid);
|
|
813
|
+
const webUrl = String(mr.web_url ?? "");
|
|
814
|
+
return (iid && webUrl) ? { iid, webUrl } : undefined;
|
|
815
|
+
} catch {
|
|
816
|
+
return undefined;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
async getUserById(userId: number): Promise<{ id: number; email?: string; username: string }> {
|
|
821
|
+
const payload = (await this.readJson(`users/${userId}`)) as Payload;
|
|
822
|
+
const p = payload ?? {};
|
|
823
|
+
return {
|
|
824
|
+
id: Number(p.id),
|
|
825
|
+
email: typeof p.email === "string" && p.email
|
|
826
|
+
? p.email
|
|
827
|
+
: (typeof p.public_email === "string" && p.public_email ? p.public_email : undefined),
|
|
828
|
+
username: typeof p.username === "string" ? p.username : ""
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
|
|
752
832
|
/**
|
|
753
833
|
* Direct HTTP call using Node's built-in fetch.
|
|
754
834
|
* Used for endpoints where glab CLI's response parsing has issues (e.g. PUT /user/status).
|
|
@@ -798,7 +878,7 @@ export class GitlabGlabClient implements GitlabClient {
|
|
|
798
878
|
private async request(
|
|
799
879
|
endpoint: string,
|
|
800
880
|
options: {
|
|
801
|
-
method?: "GET" | "POST" | "PUT";
|
|
881
|
+
method?: "GET" | "POST" | "PUT" | "DELETE";
|
|
802
882
|
fields?: Record<string, string>;
|
|
803
883
|
} = {}
|
|
804
884
|
): Promise<string> {
|
|
@@ -6,6 +6,7 @@ export interface WebhookPayload {
|
|
|
6
6
|
title: string;
|
|
7
7
|
message: string;
|
|
8
8
|
url?: string;
|
|
9
|
+
status?: "accepted" | "completed" | "failed" | "busy";
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
function isFeishuUrl(url: string): boolean {
|
|
@@ -16,18 +17,58 @@ function isSlackUrl(url: string): boolean {
|
|
|
16
17
|
return url.includes("hooks.slack.com");
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
function buildFeishuCard(payload: WebhookPayload): unknown {
|
|
21
|
+
const statusConfig = {
|
|
22
|
+
accepted: { color: "blue", title: "已接单" },
|
|
23
|
+
completed: { color: "green", title: "已完成" },
|
|
24
|
+
failed: { color: "red", title: "执行失败" },
|
|
25
|
+
busy: { color: "orange", title: "排队中" },
|
|
26
|
+
};
|
|
27
|
+
const cfg = statusConfig[payload.status ?? "accepted"];
|
|
28
|
+
|
|
29
|
+
const elements: unknown[] = [
|
|
30
|
+
{
|
|
31
|
+
tag: "div",
|
|
32
|
+
text: { tag: "lark_md", content: payload.message }
|
|
33
|
+
}
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// Add URL button if available
|
|
37
|
+
if (payload.url) {
|
|
38
|
+
elements.push({
|
|
39
|
+
tag: "action",
|
|
40
|
+
actions: [
|
|
41
|
+
{
|
|
42
|
+
tag: "button",
|
|
43
|
+
text: { tag: "plain_text", content: payload.status === "completed" ? "查看 MR" : "查看 Issue" },
|
|
44
|
+
type: "primary",
|
|
45
|
+
url: payload.url
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
msg_type: "interactive",
|
|
53
|
+
card: {
|
|
54
|
+
header: {
|
|
55
|
+
title: { tag: "plain_text", content: payload.title },
|
|
56
|
+
template: cfg.color
|
|
57
|
+
},
|
|
58
|
+
elements
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
19
63
|
function buildWebhookBody(url: string, payload: WebhookPayload): unknown {
|
|
64
|
+
if (isFeishuUrl(url)) {
|
|
65
|
+
return buildFeishuCard(payload);
|
|
66
|
+
}
|
|
67
|
+
|
|
20
68
|
const text = payload.url
|
|
21
69
|
? `${payload.title}\n${payload.message}\n${payload.url}`
|
|
22
70
|
: `${payload.title}\n${payload.message}`;
|
|
23
71
|
|
|
24
|
-
if (isFeishuUrl(url)) {
|
|
25
|
-
return {
|
|
26
|
-
msg_type: "text",
|
|
27
|
-
content: { text }
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
72
|
if (isSlackUrl(url)) {
|
|
32
73
|
return { text };
|
|
33
74
|
}
|
|
@@ -69,12 +110,15 @@ export async function notifyAccepted(
|
|
|
69
110
|
agentName: string,
|
|
70
111
|
issueIid: number,
|
|
71
112
|
issueTitle: string,
|
|
113
|
+
issueUrl?: string,
|
|
72
114
|
webhookUrl?: string
|
|
73
115
|
): Promise<void> {
|
|
74
116
|
if (!webhookUrl) return;
|
|
75
117
|
await sendWebhook(webhookUrl, {
|
|
76
118
|
title: `\u{1F916} ${agentName} 已接单`,
|
|
77
119
|
message: `#${issueIid} ${issueTitle}`,
|
|
120
|
+
url: issueUrl,
|
|
121
|
+
status: "accepted",
|
|
78
122
|
});
|
|
79
123
|
}
|
|
80
124
|
|
|
@@ -90,6 +134,7 @@ export async function notifyCompleted(
|
|
|
90
134
|
title: `\u2705 ${agentName} 已完成`,
|
|
91
135
|
message: `#${issueIid} ${issueTitle}`,
|
|
92
136
|
url: mrUrl,
|
|
137
|
+
status: "completed",
|
|
93
138
|
});
|
|
94
139
|
}
|
|
95
140
|
|
|
@@ -103,6 +148,24 @@ export async function notifyFailed(
|
|
|
103
148
|
if (!webhookUrl) return;
|
|
104
149
|
await sendWebhook(webhookUrl, {
|
|
105
150
|
title: `\u274C ${agentName} 执行失败`,
|
|
106
|
-
message: `#${issueIid} ${issueTitle}\n${summary.slice(0,
|
|
151
|
+
message: `#${issueIid} ${issueTitle}\n${summary.slice(0, 200)}`,
|
|
152
|
+
status: "failed",
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function notifyQueued(
|
|
157
|
+
agentName: string,
|
|
158
|
+
issueIid: number,
|
|
159
|
+
issueTitle: string,
|
|
160
|
+
queuePosition: number,
|
|
161
|
+
issueUrl?: string,
|
|
162
|
+
webhookUrl?: string
|
|
163
|
+
): Promise<void> {
|
|
164
|
+
if (!webhookUrl) return;
|
|
165
|
+
await sendWebhook(webhookUrl, {
|
|
166
|
+
title: `\u23F3 ${agentName} 排队中`,
|
|
167
|
+
message: `#${issueIid} ${issueTitle}\n排在第 ${queuePosition} 位`,
|
|
168
|
+
url: issueUrl,
|
|
169
|
+
status: "busy",
|
|
107
170
|
});
|
|
108
171
|
}
|