plugin-git-manager 1.1.10 → 1.2.0
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/dist/client/187.d5545b7cc8b90bfc.js +1 -0
- package/dist/client/228.7588a0707cb3694a.js +0 -9
- package/dist/client/index.js +1 -10
- package/dist/externalVersion.js +5 -14
- package/dist/index.js +0 -9
- package/dist/locale/en-US.json +2 -0
- package/dist/locale/vi-VN.json +2 -0
- package/dist/server/actions/git-actions.js +15 -21
- package/dist/server/actions/gitlab-api.js +0 -9
- package/dist/server/actions/poller.js +0 -9
- package/dist/server/actions/review.d.ts +5 -2
- package/dist/server/actions/review.js +430 -60
- package/dist/server/ai-tools.js +0 -9
- package/dist/server/collections/gitCodeReviews.d.ts +1 -1
- package/dist/server/collections/gitCodeReviews.js +1 -9
- package/dist/server/collections/gitRepositories.d.ts +1 -1
- package/dist/server/collections/gitRepositories.js +0 -9
- package/dist/server/collections/gitReviewFlows.d.ts +1 -1
- package/dist/server/collections/gitReviewFlows.js +0 -9
- package/dist/server/index.js +0 -9
- package/dist/server/migrations/20260508000000-add-auto-review-flow-id.js +0 -9
- package/dist/server/plugin.js +5 -9
- package/dist/server/poller.js +0 -9
- package/dist/server/utils/gitlab-url.js +0 -9
- package/dist/server/utils/redact.js +0 -9
- package/package.json +2 -2
- package/src/client/components/CommitHistory.tsx +3 -0
- package/src/client/components/FileExplorer.tsx +29 -24
- package/src/client/components/GitOperations.tsx +3 -0
- package/src/client/components/ReviewFlows.tsx +11 -1
- package/src/client/components/ReviewHistory.tsx +14 -1
- package/src/locale/en-US.json +2 -0
- package/src/locale/vi-VN.json +2 -0
- package/src/server/actions/git-actions.ts +15 -12
- package/src/server/actions/review.ts +504 -63
- package/src/server/collections/gitCodeReviews.ts +1 -0
- package/src/server/plugin.ts +25 -20
- package/dist/client/187.08dd0bf4d0f68036.js +0 -10
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Context } from '@nocobase/actions';
|
|
2
2
|
import type { Application } from '@nocobase/server';
|
|
3
|
+
export declare const WORKER_JOB_GIT_REVIEW_PROCESS = "git-review:process";
|
|
3
4
|
interface TriggerArgs {
|
|
4
5
|
flowId?: number | null;
|
|
5
6
|
repositoryId: number;
|
|
@@ -14,8 +15,8 @@ interface TriggerArgs {
|
|
|
14
15
|
}
|
|
15
16
|
/**
|
|
16
17
|
* Trigger an AI-driven code review for an MR / commit / branch.
|
|
17
|
-
* The review record is upserted synchronously, then
|
|
18
|
-
*
|
|
18
|
+
* The review record is upserted synchronously, then queued for an available
|
|
19
|
+
* git-review worker. The action returns immediately with the reviewId.
|
|
19
20
|
*/
|
|
20
21
|
export declare function triggerReview(ctx: Context, next: () => Promise<void>): Promise<void>;
|
|
21
22
|
/**
|
|
@@ -23,6 +24,8 @@ export declare function triggerReview(ctx: Context, next: () => Promise<void>):
|
|
|
23
24
|
* Returns the reviewId of the upserted record.
|
|
24
25
|
*/
|
|
25
26
|
export declare function triggerReviewInternal(app: Application, args: TriggerArgs): Promise<number>;
|
|
27
|
+
export declare function registerReviewQueue(app: Application): void;
|
|
28
|
+
export declare function unregisterReviewQueue(app: Application): void;
|
|
26
29
|
/**
|
|
27
30
|
* Mark a review as approved and post its content to GitLab as an MR note.
|
|
28
31
|
*/
|
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* This file is part of the NocoBase (R) project.
|
|
3
|
-
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
-
* Authors: NocoBase Team.
|
|
5
|
-
*
|
|
6
|
-
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
-
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
1
|
var __defProp = Object.defineProperty;
|
|
11
2
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
12
3
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
@@ -26,17 +17,41 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
26
17
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
27
18
|
var review_exports = {};
|
|
28
19
|
__export(review_exports, {
|
|
20
|
+
WORKER_JOB_GIT_REVIEW_PROCESS: () => WORKER_JOB_GIT_REVIEW_PROCESS,
|
|
29
21
|
branchMatches: () => branchMatches,
|
|
30
22
|
pickFlowMatchingBranch: () => pickFlowMatchingBranch,
|
|
31
23
|
recoverStuckReviews: () => recoverStuckReviews,
|
|
24
|
+
registerReviewQueue: () => registerReviewQueue,
|
|
32
25
|
reviewApprovePost: () => reviewApprovePost,
|
|
33
26
|
reviewReject: () => reviewReject,
|
|
34
27
|
triggerReview: () => triggerReview,
|
|
35
|
-
triggerReviewInternal: () => triggerReviewInternal
|
|
28
|
+
triggerReviewInternal: () => triggerReviewInternal,
|
|
29
|
+
unregisterReviewQueue: () => unregisterReviewQueue
|
|
36
30
|
});
|
|
37
31
|
module.exports = __toCommonJS(review_exports);
|
|
38
32
|
var import_gitlab_url = require("../utils/gitlab-url");
|
|
39
33
|
var import_redact = require("../utils/redact");
|
|
34
|
+
const WORKER_JOB_GIT_REVIEW_PROCESS = "git-review:process";
|
|
35
|
+
const REVIEW_QUEUE_CHANNEL = "plugin-git-manager.review";
|
|
36
|
+
const REVIEW_QUEUE_CONCURRENCY = Math.max(
|
|
37
|
+
1,
|
|
38
|
+
Number.parseInt(process.env.GIT_REVIEW_QUEUE_CONCURRENCY || process.env.GIT_REVIEW_MAX_CONCURRENCY || "3", 10) || 3
|
|
39
|
+
);
|
|
40
|
+
const REVIEW_QUEUE_TIMEOUT_MS = Math.max(
|
|
41
|
+
6e4,
|
|
42
|
+
Number.parseInt(process.env.GIT_REVIEW_QUEUE_TIMEOUT_MS || "", 10) || 10 * 60 * 1e3
|
|
43
|
+
);
|
|
44
|
+
const REVIEW_QUEUE_POLL_INTERVAL_MS = Math.max(
|
|
45
|
+
1e3,
|
|
46
|
+
Number.parseInt(process.env.GIT_REVIEW_QUEUE_POLL_INTERVAL_MS || "", 10) || 5e3
|
|
47
|
+
);
|
|
48
|
+
const REVIEW_PROCESS_LOCK_TTL_MS = Math.max(REVIEW_QUEUE_TIMEOUT_MS + 6e4, 11 * 60 * 1e3);
|
|
49
|
+
const REVIEW_QUEUE_WAKE_CHANNEL = "plugin-git-manager.review.wake";
|
|
50
|
+
const REVIEW_QUEUE_REDIS_CONNECTION = "plugin-git-manager.review.queue";
|
|
51
|
+
let reviewQueueTimer = null;
|
|
52
|
+
let reviewQueueKickTimer = null;
|
|
53
|
+
let reviewQueueProcessing = false;
|
|
54
|
+
let reviewWakeHandler = null;
|
|
40
55
|
function getActionParams(ctx) {
|
|
41
56
|
var _a, _b;
|
|
42
57
|
return { ...ctx.action.params, ...(_a = ctx.action.params) == null ? void 0 : _a.values, ...((_b = ctx.request) == null ? void 0 : _b.body) || {} };
|
|
@@ -54,15 +69,7 @@ async function withTriggerLock(app, key, fn) {
|
|
|
54
69
|
async function triggerReview(ctx, next) {
|
|
55
70
|
var _a, _b;
|
|
56
71
|
const params = getActionParams(ctx);
|
|
57
|
-
const {
|
|
58
|
-
flowId,
|
|
59
|
-
repositoryId,
|
|
60
|
-
targetType,
|
|
61
|
-
mrIid,
|
|
62
|
-
commitSha,
|
|
63
|
-
branch,
|
|
64
|
-
extraInstructions
|
|
65
|
-
} = params;
|
|
72
|
+
const { flowId, repositoryId, targetType, mrIid, commitSha, branch, extraInstructions } = params;
|
|
66
73
|
if (!repositoryId) ctx.throw(400, "repositoryId is required");
|
|
67
74
|
if (!targetType) ctx.throw(400, "targetType is required");
|
|
68
75
|
if (!["mr", "commit", "branch"].includes(targetType)) ctx.throw(400, "invalid targetType");
|
|
@@ -142,14 +149,20 @@ async function triggerReviewInternalLocked(app, args) {
|
|
|
142
149
|
latestSha: headSha || existingLatestSha || null,
|
|
143
150
|
triggeredBy: args.triggeredBy || "manual",
|
|
144
151
|
status: "pending",
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
startedAt: /* @__PURE__ */ new Date(),
|
|
152
|
+
// `startedAt` is stamped by the queue worker when execution actually
|
|
153
|
+
// starts. Pending rows are durable queue items for worker polling.
|
|
154
|
+
startedAt: null,
|
|
149
155
|
finishedAt: null,
|
|
150
156
|
durationMs: null,
|
|
151
|
-
postStatus: flow.
|
|
152
|
-
error: null
|
|
157
|
+
postStatus: getInitialPostStatus(flow, args.targetType),
|
|
158
|
+
error: null,
|
|
159
|
+
metadata: {
|
|
160
|
+
queuedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
161
|
+
aiEmployeeUsername,
|
|
162
|
+
extraInstructions: args.extraInstructions || null,
|
|
163
|
+
userId: args.userId ?? null,
|
|
164
|
+
flowSnapshot: createFlowSnapshot(flow)
|
|
165
|
+
}
|
|
153
166
|
};
|
|
154
167
|
let reviewId;
|
|
155
168
|
if (existing) {
|
|
@@ -166,26 +179,343 @@ async function triggerReviewInternalLocked(app, args) {
|
|
|
166
179
|
const review = await reviewsRepo.create({ values: baseValues });
|
|
167
180
|
reviewId = review.get("id");
|
|
168
181
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}).catch((err) => {
|
|
183
|
-
var _a, _b;
|
|
184
|
-
(_b = (_a = app.log) == null ? void 0 : _a.error) == null ? void 0 : _b.call(_a, "runReview background error", err);
|
|
185
|
-
})
|
|
186
|
-
);
|
|
182
|
+
await enqueueReview(app, {
|
|
183
|
+
reviewId,
|
|
184
|
+
repositoryId: args.repositoryId,
|
|
185
|
+
targetType: args.targetType,
|
|
186
|
+
mrIid: args.targetType === "mr" ? args.mrIid : null,
|
|
187
|
+
commitSha: args.targetType === "commit" ? args.commitSha : null,
|
|
188
|
+
branch: args.branch || void 0,
|
|
189
|
+
headSha,
|
|
190
|
+
aiEmployeeUsername,
|
|
191
|
+
extraInstructions: args.extraInstructions,
|
|
192
|
+
userId: args.userId ?? null,
|
|
193
|
+
flowSnapshot: createFlowSnapshot(flow)
|
|
194
|
+
});
|
|
187
195
|
return reviewId;
|
|
188
196
|
}
|
|
197
|
+
function registerReviewQueue(app) {
|
|
198
|
+
app.eventQueue.subscribe(REVIEW_QUEUE_CHANNEL, {
|
|
199
|
+
concurrency: REVIEW_QUEUE_CONCURRENCY,
|
|
200
|
+
idle: () => isGitReviewWorker(app),
|
|
201
|
+
process: async (message) => {
|
|
202
|
+
await processQueuedReview(app, message);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
if (!isGitReviewWorker(app)) {
|
|
206
|
+
app.on("afterStart", () => clearLocalReviewMemoryQueue(app));
|
|
207
|
+
}
|
|
208
|
+
startReviewQueueProcessor(app);
|
|
209
|
+
}
|
|
210
|
+
function unregisterReviewQueue(app) {
|
|
211
|
+
app.eventQueue.unsubscribe(REVIEW_QUEUE_CHANNEL);
|
|
212
|
+
stopReviewQueueProcessor(app);
|
|
213
|
+
}
|
|
214
|
+
function createFlowSnapshot(flow) {
|
|
215
|
+
return {
|
|
216
|
+
id: Number(flow.get("id")),
|
|
217
|
+
name: flow.get("name"),
|
|
218
|
+
postMode: flow.get("postMode"),
|
|
219
|
+
llmService: flow.get("llmService"),
|
|
220
|
+
model: flow.get("model"),
|
|
221
|
+
instructions: flow.get("instructions")
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function createFlowFromSnapshot(snapshot, fallback) {
|
|
225
|
+
return {
|
|
226
|
+
get(name) {
|
|
227
|
+
var _a;
|
|
228
|
+
if (snapshot && Object.prototype.hasOwnProperty.call(snapshot, name)) {
|
|
229
|
+
return snapshot[name];
|
|
230
|
+
}
|
|
231
|
+
return (_a = fallback == null ? void 0 : fallback.get) == null ? void 0 : _a.call(fallback, name);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function isGitReviewWorker(app) {
|
|
236
|
+
const workerMode = process.env.WORKER_MODE || "";
|
|
237
|
+
return app.serving(WORKER_JOB_GIT_REVIEW_PROCESS) || workerMode === "worker" || workerMode === "task" || process.env.APP_ROLE === "worker";
|
|
238
|
+
}
|
|
239
|
+
function clearLocalReviewMemoryQueue(app) {
|
|
240
|
+
var _a, _b, _c, _d, _e;
|
|
241
|
+
const eventQueue = app.eventQueue;
|
|
242
|
+
const adapter = eventQueue == null ? void 0 : eventQueue.adapter;
|
|
243
|
+
const fullChannel = (_a = eventQueue == null ? void 0 : eventQueue.getFullChannel) == null ? void 0 : _a.call(eventQueue, REVIEW_QUEUE_CHANNEL);
|
|
244
|
+
const queue = fullChannel ? (_c = (_b = adapter == null ? void 0 : adapter.queues) == null ? void 0 : _b.get) == null ? void 0 : _c.call(_b, fullChannel) : null;
|
|
245
|
+
if (!(queue == null ? void 0 : queue.length)) return;
|
|
246
|
+
adapter.queues.set(fullChannel, []);
|
|
247
|
+
(_e = (_d = app.log) == null ? void 0 : _d.warn) == null ? void 0 : _e.call(
|
|
248
|
+
_d,
|
|
249
|
+
`git review queue: cleared ${queue.length} stale local memory message(s) on non-worker node; pending DB rows will be picked up by workers`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
function getReviewQueueRedisKey(app) {
|
|
253
|
+
const appName = app.name || process.env.APP_NAME || "main";
|
|
254
|
+
return `${appName}:plugin-git-manager:review:queue`;
|
|
255
|
+
}
|
|
256
|
+
async function getReviewQueueRedis(app) {
|
|
257
|
+
var _a, _b;
|
|
258
|
+
const manager = app.redisConnectionManager;
|
|
259
|
+
if (!(manager == null ? void 0 : manager.getConnectionSync)) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
const connectionString = process.env.QUEUE_ADAPTER_REDIS_URL || process.env.REDIS_URL;
|
|
264
|
+
return await manager.getConnectionSync(
|
|
265
|
+
REVIEW_QUEUE_REDIS_CONNECTION,
|
|
266
|
+
connectionString ? { connectionString } : void 0
|
|
267
|
+
);
|
|
268
|
+
} catch (err) {
|
|
269
|
+
(_b = (_a = app.log) == null ? void 0 : _a.debug) == null ? void 0 : _b.call(_a, `git review queue: Redis queue unavailable, falling back to DB polling: ${(err == null ? void 0 : err.message) || err}`);
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
async function enqueueReviewToRedis(app, message) {
|
|
274
|
+
var _a, _b;
|
|
275
|
+
const redis = await getReviewQueueRedis(app);
|
|
276
|
+
if (!redis) return false;
|
|
277
|
+
await redis.sendCommand(["RPUSH", getReviewQueueRedisKey(app), JSON.stringify(message)]);
|
|
278
|
+
(_b = (_a = app.log) == null ? void 0 : _a.debug) == null ? void 0 : _b.call(_a, `git review queue: enqueued review ${message.reviewId} to Redis`);
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
async function publishReviewQueueWake(app, reviewId) {
|
|
282
|
+
var _a, _b, _c, _d;
|
|
283
|
+
try {
|
|
284
|
+
await ((_b = (_a = app.pubSubManager) == null ? void 0 : _a.publish) == null ? void 0 : _b.call(
|
|
285
|
+
_a,
|
|
286
|
+
REVIEW_QUEUE_WAKE_CHANNEL,
|
|
287
|
+
{ reviewId },
|
|
288
|
+
{ skipSelf: !isGitReviewWorker(app) }
|
|
289
|
+
));
|
|
290
|
+
} catch (err) {
|
|
291
|
+
(_d = (_c = app.log) == null ? void 0 : _c.debug) == null ? void 0 : _d.call(_c, `git review queue: wake publish skipped: ${(err == null ? void 0 : err.message) || err}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function startReviewQueueProcessor(app) {
|
|
295
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
296
|
+
if (!isGitReviewWorker(app)) {
|
|
297
|
+
(_b = (_a = app.log) == null ? void 0 : _a.debug) == null ? void 0 : _b.call(_a, "plugin-git-manager: review queue processor disabled on non-worker node");
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (reviewQueueTimer) return;
|
|
301
|
+
reviewWakeHandler = async () => {
|
|
302
|
+
scheduleReviewQueueTick(app, 0);
|
|
303
|
+
};
|
|
304
|
+
const subscribe = (_d = (_c = app.pubSubManager) == null ? void 0 : _c.subscribe) == null ? void 0 : _d.call(_c, REVIEW_QUEUE_WAKE_CHANNEL, reviewWakeHandler);
|
|
305
|
+
if (subscribe == null ? void 0 : subscribe.catch) {
|
|
306
|
+
subscribe.catch((err) => {
|
|
307
|
+
var _a2, _b2;
|
|
308
|
+
return (_b2 = (_a2 = app.log) == null ? void 0 : _a2.debug) == null ? void 0 : _b2.call(_a2, `git review queue: wake subscribe skipped: ${(err == null ? void 0 : err.message) || err}`);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
reviewQueueTimer = setInterval(() => scheduleReviewQueueTick(app, 0), REVIEW_QUEUE_POLL_INTERVAL_MS);
|
|
312
|
+
(_e = reviewQueueTimer.unref) == null ? void 0 : _e.call(reviewQueueTimer);
|
|
313
|
+
scheduleReviewQueueTick(app, 1e3);
|
|
314
|
+
(_g = (_f = app.log) == null ? void 0 : _f.info) == null ? void 0 : _g.call(_f, `plugin-git-manager: review queue processor started (interval ${REVIEW_QUEUE_POLL_INTERVAL_MS}ms)`);
|
|
315
|
+
}
|
|
316
|
+
function stopReviewQueueProcessor(app) {
|
|
317
|
+
var _a, _b;
|
|
318
|
+
if (reviewQueueTimer) {
|
|
319
|
+
clearInterval(reviewQueueTimer);
|
|
320
|
+
reviewQueueTimer = null;
|
|
321
|
+
}
|
|
322
|
+
if (reviewQueueKickTimer) {
|
|
323
|
+
clearTimeout(reviewQueueKickTimer);
|
|
324
|
+
reviewQueueKickTimer = null;
|
|
325
|
+
}
|
|
326
|
+
if (reviewWakeHandler) {
|
|
327
|
+
const unsubscribe = (_b = (_a = app.pubSubManager) == null ? void 0 : _a.unsubscribe) == null ? void 0 : _b.call(_a, REVIEW_QUEUE_WAKE_CHANNEL, reviewWakeHandler);
|
|
328
|
+
if (unsubscribe == null ? void 0 : unsubscribe.catch) {
|
|
329
|
+
unsubscribe.catch(() => void 0);
|
|
330
|
+
}
|
|
331
|
+
reviewWakeHandler = null;
|
|
332
|
+
}
|
|
333
|
+
reviewQueueProcessing = false;
|
|
334
|
+
}
|
|
335
|
+
function scheduleReviewQueueTick(app, delayMs) {
|
|
336
|
+
var _a;
|
|
337
|
+
if (reviewQueueKickTimer) return;
|
|
338
|
+
reviewQueueKickTimer = setTimeout(() => {
|
|
339
|
+
reviewQueueKickTimer = null;
|
|
340
|
+
runReviewQueueTick(app).catch((err) => {
|
|
341
|
+
var _a2, _b;
|
|
342
|
+
return (_b = (_a2 = app.log) == null ? void 0 : _a2.error) == null ? void 0 : _b.call(_a2, "git review queue: processor tick failed", err);
|
|
343
|
+
});
|
|
344
|
+
}, delayMs);
|
|
345
|
+
(_a = reviewQueueKickTimer.unref) == null ? void 0 : _a.call(reviewQueueKickTimer);
|
|
346
|
+
}
|
|
347
|
+
async function runReviewQueueTick(app) {
|
|
348
|
+
if (reviewQueueProcessing || !isGitReviewWorker(app)) return;
|
|
349
|
+
reviewQueueProcessing = true;
|
|
350
|
+
try {
|
|
351
|
+
const redisMessages = await drainRedisReviewQueue(app, REVIEW_QUEUE_CONCURRENCY);
|
|
352
|
+
await processReviewQueueMessages(app, redisMessages);
|
|
353
|
+
const remaining = Math.max(1, REVIEW_QUEUE_CONCURRENCY - redisMessages.length);
|
|
354
|
+
await processPendingReviews(app, remaining);
|
|
355
|
+
} finally {
|
|
356
|
+
reviewQueueProcessing = false;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
async function drainRedisReviewQueue(app, count) {
|
|
360
|
+
var _a, _b;
|
|
361
|
+
const redis = await getReviewQueueRedis(app);
|
|
362
|
+
if (!redis) return [];
|
|
363
|
+
const key = getReviewQueueRedisKey(app);
|
|
364
|
+
const messages = [];
|
|
365
|
+
for (let i = 0; i < count; i += 1) {
|
|
366
|
+
const raw = await redis.sendCommand(["LPOP", key]);
|
|
367
|
+
if (!raw) break;
|
|
368
|
+
try {
|
|
369
|
+
messages.push(JSON.parse(String(raw)));
|
|
370
|
+
} catch (err) {
|
|
371
|
+
(_b = (_a = app.log) == null ? void 0 : _a.warn) == null ? void 0 : _b.call(_a, `git review queue: dropped invalid Redis message: ${(err == null ? void 0 : err.message) || err}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return messages;
|
|
375
|
+
}
|
|
376
|
+
function getQueuedReviewMetadata(review) {
|
|
377
|
+
var _a;
|
|
378
|
+
const raw = (_a = review == null ? void 0 : review.get) == null ? void 0 : _a.call(review, "metadata");
|
|
379
|
+
if (!raw) return {};
|
|
380
|
+
if (typeof raw === "string") {
|
|
381
|
+
try {
|
|
382
|
+
return JSON.parse(raw) || {};
|
|
383
|
+
} catch {
|
|
384
|
+
return {};
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return typeof raw === "object" ? raw : {};
|
|
388
|
+
}
|
|
389
|
+
function toNullableNumber(value) {
|
|
390
|
+
if (value === null || value === void 0 || value === "") return null;
|
|
391
|
+
const parsed = Number(value);
|
|
392
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
393
|
+
}
|
|
394
|
+
function createReviewQueueMessageFromReview(review) {
|
|
395
|
+
const metadata = getQueuedReviewMetadata(review);
|
|
396
|
+
const targetType = review.get("targetType");
|
|
397
|
+
return {
|
|
398
|
+
reviewId: Number(review.get("id")),
|
|
399
|
+
repositoryId: Number(review.get("repositoryId")),
|
|
400
|
+
targetType,
|
|
401
|
+
mrIid: targetType === "mr" ? toNullableNumber(review.get("mrIid")) : null,
|
|
402
|
+
commitSha: targetType === "commit" ? review.get("commitSha") : null,
|
|
403
|
+
branch: targetType === "branch" ? review.get("branch") : null,
|
|
404
|
+
headSha: review.get("headSha"),
|
|
405
|
+
aiEmployeeUsername: metadata.aiEmployeeUsername || "",
|
|
406
|
+
extraInstructions: metadata.extraInstructions || void 0,
|
|
407
|
+
userId: metadata.userId ?? null,
|
|
408
|
+
flowSnapshot: metadata.flowSnapshot
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
async function processPendingReviews(app, count) {
|
|
412
|
+
const pending = await app.db.getRepository("gitCodeReviews").find({
|
|
413
|
+
filter: { status: "pending" },
|
|
414
|
+
sort: ["createdAt"],
|
|
415
|
+
limit: count
|
|
416
|
+
});
|
|
417
|
+
if (!(pending == null ? void 0 : pending.length)) return;
|
|
418
|
+
await processReviewQueueMessages(app, pending.map(createReviewQueueMessageFromReview));
|
|
419
|
+
}
|
|
420
|
+
async function processReviewQueueMessages(app, messages) {
|
|
421
|
+
if (!messages.length) return;
|
|
422
|
+
await Promise.all(messages.map((message) => processQueuedReview(app, message)));
|
|
423
|
+
}
|
|
424
|
+
async function withReviewProcessLock(app, reviewId, fn) {
|
|
425
|
+
const lockKey = `git-review:process:${reviewId}`;
|
|
426
|
+
return app.lockManager.runExclusive(lockKey, fn, REVIEW_PROCESS_LOCK_TTL_MS);
|
|
427
|
+
}
|
|
428
|
+
async function enqueueReview(app, message) {
|
|
429
|
+
var _a, _b;
|
|
430
|
+
try {
|
|
431
|
+
const queuedInRedis = await enqueueReviewToRedis(app, message);
|
|
432
|
+
if (queuedInRedis) {
|
|
433
|
+
await publishReviewQueueWake(app, message.reviewId);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
await publishReviewQueueWake(app, message.reviewId);
|
|
437
|
+
if (isGitReviewWorker(app)) {
|
|
438
|
+
await app.eventQueue.publish(REVIEW_QUEUE_CHANNEL, message, {
|
|
439
|
+
timeout: REVIEW_QUEUE_TIMEOUT_MS,
|
|
440
|
+
maxRetries: 1
|
|
441
|
+
});
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
(_b = (_a = app.log) == null ? void 0 : _a.warn) == null ? void 0 : _b.call(
|
|
445
|
+
_a,
|
|
446
|
+
`git review queue: Redis queue is unavailable; review ${message.reviewId} will remain pending until a worker DB poller picks it up`
|
|
447
|
+
);
|
|
448
|
+
} catch (err) {
|
|
449
|
+
const safeMessage = (0, import_redact.redactPat)((err == null ? void 0 : err.message) || String(err));
|
|
450
|
+
await app.db.getRepository("gitCodeReviews").update({
|
|
451
|
+
filterByTk: message.reviewId,
|
|
452
|
+
values: {
|
|
453
|
+
status: "failed",
|
|
454
|
+
error: `Failed to enqueue review: ${safeMessage}`,
|
|
455
|
+
finishedAt: /* @__PURE__ */ new Date()
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
throw err;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
async function failQueuedReview(app, reviewId, err) {
|
|
462
|
+
const safeMessage = (0, import_redact.redactPat)((err == null ? void 0 : err.message) || String(err));
|
|
463
|
+
await app.db.getRepository("gitCodeReviews").update({
|
|
464
|
+
filterByTk: reviewId,
|
|
465
|
+
values: {
|
|
466
|
+
status: "failed",
|
|
467
|
+
error: safeMessage,
|
|
468
|
+
finishedAt: /* @__PURE__ */ new Date()
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
async function processQueuedReview(app, message) {
|
|
473
|
+
await withReviewProcessLock(app, message.reviewId, async () => {
|
|
474
|
+
var _a, _b, _c, _d, _e, _f;
|
|
475
|
+
const db = app.db;
|
|
476
|
+
const reviewsRepo = db.getRepository("gitCodeReviews");
|
|
477
|
+
const review = await reviewsRepo.findOne({ filterByTk: message.reviewId });
|
|
478
|
+
if (!review) {
|
|
479
|
+
(_b = (_a = app.log) == null ? void 0 : _a.warn) == null ? void 0 : _b.call(_a, `git review queue: review ${message.reviewId} not found, skipping`);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (review.get("status") !== "pending") {
|
|
483
|
+
(_d = (_c = app.log) == null ? void 0 : _c.info) == null ? void 0 : _d.call(_c, `git review queue: review ${message.reviewId} is ${review.get("status")}, skipping`);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const metadata = getQueuedReviewMetadata(review);
|
|
487
|
+
const targetType = message.targetType || review.get("targetType");
|
|
488
|
+
try {
|
|
489
|
+
const repo = await db.getRepository("gitRepositories").findOne({
|
|
490
|
+
filterByTk: message.repositoryId || review.get("repositoryId")
|
|
491
|
+
});
|
|
492
|
+
if (!repo) throw new Error("Repository not found");
|
|
493
|
+
const flowSnapshot = message.flowSnapshot || metadata.flowSnapshot;
|
|
494
|
+
const storedFlow = await db.getRepository("gitReviewFlows").findOne({
|
|
495
|
+
filterByTk: (flowSnapshot == null ? void 0 : flowSnapshot.id) || review.get("flowId")
|
|
496
|
+
});
|
|
497
|
+
const flow = createFlowFromSnapshot(flowSnapshot, storedFlow);
|
|
498
|
+
const aiEmployeeUsername = message.aiEmployeeUsername || metadata.aiEmployeeUsername || flow.get("aiEmployeeUsername");
|
|
499
|
+
if (!aiEmployeeUsername) throw new Error("Flow has no AI employee configured");
|
|
500
|
+
await runReview(app, {
|
|
501
|
+
reviewId: message.reviewId,
|
|
502
|
+
flow,
|
|
503
|
+
repo,
|
|
504
|
+
targetType,
|
|
505
|
+
mrIid: targetType === "mr" ? message.mrIid ?? toNullableNumber(review.get("mrIid")) : null,
|
|
506
|
+
commitSha: targetType === "commit" ? message.commitSha || review.get("commitSha") : null,
|
|
507
|
+
branch: message.branch || review.get("branch"),
|
|
508
|
+
headSha: message.headSha || review.get("headSha"),
|
|
509
|
+
aiEmployeeUsername,
|
|
510
|
+
extraInstructions: message.extraInstructions ?? metadata.extraInstructions ?? void 0,
|
|
511
|
+
userId: message.userId ?? metadata.userId ?? null
|
|
512
|
+
});
|
|
513
|
+
} catch (err) {
|
|
514
|
+
(_f = (_e = app.log) == null ? void 0 : _e.error) == null ? void 0 : _f.call(_e, "git review queue: failed before review execution", err);
|
|
515
|
+
await failQueuedReview(app, message.reviewId, err);
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
}
|
|
189
519
|
async function reviewApprovePost(ctx, next) {
|
|
190
520
|
var _a, _b;
|
|
191
521
|
const params = getActionParams(ctx);
|
|
@@ -211,7 +541,8 @@ async function reviewApprovePost(ctx, next) {
|
|
|
211
541
|
postStatus: "posted",
|
|
212
542
|
postedNoteId: String(noteId),
|
|
213
543
|
approvedBy: userId ? String(userId) : null,
|
|
214
|
-
approvedAt: /* @__PURE__ */ new Date()
|
|
544
|
+
approvedAt: /* @__PURE__ */ new Date(),
|
|
545
|
+
error: null
|
|
215
546
|
}
|
|
216
547
|
});
|
|
217
548
|
ctx.body = { success: true, data: { reviewId, postedNoteId: noteId } };
|
|
@@ -323,7 +654,8 @@ async function runReview(app, args) {
|
|
|
323
654
|
} catch {
|
|
324
655
|
}
|
|
325
656
|
}
|
|
326
|
-
if (!AIEmployee)
|
|
657
|
+
if (!AIEmployee)
|
|
658
|
+
throw new Error("AIEmployee class not found \u2014 plugin-ai may not be installed or its exports changed");
|
|
327
659
|
const llmService = args.flow.get("llmService");
|
|
328
660
|
const model = args.flow.get("model");
|
|
329
661
|
const modelRef = llmService && model ? { llmService, model } : void 0;
|
|
@@ -348,7 +680,7 @@ async function runReview(app, args) {
|
|
|
348
680
|
]
|
|
349
681
|
}),
|
|
350
682
|
new Promise(
|
|
351
|
-
(
|
|
683
|
+
(_resolve, reject) => setTimeout(() => reject(new Error("AI review timed out after 5 minutes")), REVIEW_TIMEOUT_MS)
|
|
352
684
|
)
|
|
353
685
|
]);
|
|
354
686
|
const content = extractLastAiMessageContent(result);
|
|
@@ -357,9 +689,10 @@ async function runReview(app, args) {
|
|
|
357
689
|
}
|
|
358
690
|
const finishedAt = /* @__PURE__ */ new Date();
|
|
359
691
|
const durationMs = finishedAt.getTime() - startedAt.getTime();
|
|
360
|
-
const postMode = args.flow
|
|
361
|
-
let postStatus =
|
|
692
|
+
const postMode = getFlowPostMode(args.flow);
|
|
693
|
+
let postStatus = getInitialPostStatus(args.flow, args.targetType);
|
|
362
694
|
let postedNoteId = null;
|
|
695
|
+
let autoPostError = null;
|
|
363
696
|
if (postMode === "disabled") {
|
|
364
697
|
postStatus = "skipped";
|
|
365
698
|
} else if (postMode === "auto" && args.targetType === "mr" && args.mrIid) {
|
|
@@ -367,6 +700,8 @@ async function runReview(app, args) {
|
|
|
367
700
|
postedNoteId = String(await postNoteToGitLab(args.repo, args.mrIid, content));
|
|
368
701
|
postStatus = "posted";
|
|
369
702
|
} catch (err) {
|
|
703
|
+
autoPostError = (0, import_redact.redactPat)((err == null ? void 0 : err.message) || String(err));
|
|
704
|
+
postStatus = "post_failed";
|
|
370
705
|
(_c = (_b = app.log) == null ? void 0 : _b.error) == null ? void 0 : _c.call(_b, "Auto-post review note failed", err);
|
|
371
706
|
}
|
|
372
707
|
}
|
|
@@ -384,11 +719,14 @@ async function runReview(app, args) {
|
|
|
384
719
|
finishedAt,
|
|
385
720
|
postStatus,
|
|
386
721
|
postedNoteId,
|
|
722
|
+
error: autoPostError ? `Auto-post failed: ${autoPostError}` : null,
|
|
387
723
|
metadata: {
|
|
388
724
|
flowName: args.flow.get("name"),
|
|
389
725
|
aiEmployeeUsername: args.aiEmployeeUsername,
|
|
390
726
|
llmService,
|
|
391
|
-
model
|
|
727
|
+
model,
|
|
728
|
+
postMode,
|
|
729
|
+
autoPostError
|
|
392
730
|
}
|
|
393
731
|
}
|
|
394
732
|
});
|
|
@@ -418,19 +756,29 @@ function buildReviewPrompt(args) {
|
|
|
418
756
|
lines.push("");
|
|
419
757
|
if (args.targetType === "mr") {
|
|
420
758
|
lines.push(`Target: Merge Request !${args.mrIid}.`);
|
|
421
|
-
lines.push(
|
|
759
|
+
lines.push(
|
|
760
|
+
`Use the \`git_get_merge_request\` tool with repositoryId=${args.repo.get("id")} and mrIid=${args.mrIid} to fetch the diff and metadata.`
|
|
761
|
+
);
|
|
422
762
|
lines.push("Optionally call `git_get_merge_request_notes` to avoid duplicating prior comments.");
|
|
423
763
|
} else if (args.targetType === "commit") {
|
|
424
764
|
lines.push(`Target: Commit ${args.commitSha}.`);
|
|
425
|
-
lines.push(
|
|
765
|
+
lines.push(
|
|
766
|
+
`Use the \`git_get_commit\` tool with repositoryId=${args.repo.get("id")} and commitHash=${args.commitSha} to fetch the diff.`
|
|
767
|
+
);
|
|
426
768
|
} else {
|
|
427
769
|
lines.push(`Target: Branch ${args.branch}.`);
|
|
428
|
-
lines.push(
|
|
770
|
+
lines.push(
|
|
771
|
+
`Use \`git_list_commits\`, \`git_get_diff\`, and \`git_get_file_content\` (with repositoryId=${args.repo.get(
|
|
772
|
+
"id"
|
|
773
|
+
)}) to inspect recent changes on this branch.`
|
|
774
|
+
);
|
|
429
775
|
}
|
|
430
776
|
lines.push("");
|
|
431
777
|
lines.push("Produce a thorough but concise code review report in Markdown. Required sections:");
|
|
432
778
|
lines.push("1. **Summary** \u2014 overall assessment.");
|
|
433
|
-
lines.push(
|
|
779
|
+
lines.push(
|
|
780
|
+
"2. **Findings** \u2014 issues grouped by severity (`Critical`, `High`, `Medium`, `Low`, `Info`). For each finding include the file path, line/range when possible, the problem, and a suggested fix."
|
|
781
|
+
);
|
|
434
782
|
lines.push("3. **Suggestions** \u2014 non-blocking improvements.");
|
|
435
783
|
lines.push("4. **Verdict** \u2014 one of: `LGTM`, `Approve with comments`, `Request changes`, `Block`.");
|
|
436
784
|
lines.push("");
|
|
@@ -497,7 +845,7 @@ async function fetchMrHeadSha(repo, mrIid) {
|
|
|
497
845
|
if (isGitHub) {
|
|
498
846
|
const { projectPath } = (0, import_gitlab_url.parseGitLabProject)(repoUrl);
|
|
499
847
|
const response = await fetch(`https://api.github.com/repos/${projectPath}/pulls/${mrIid}`, {
|
|
500
|
-
headers: {
|
|
848
|
+
headers: { Authorization: `Bearer ${pat}`, Accept: "application/vnd.github.v3+json" }
|
|
501
849
|
});
|
|
502
850
|
if (!response.ok) return null;
|
|
503
851
|
const data = await response.json();
|
|
@@ -525,9 +873,9 @@ async function postNoteToGitLab(repo, mrIid, body) {
|
|
|
525
873
|
const response = await fetch(`https://api.github.com/repos/${projectPath}/issues/${mrIid}/comments`, {
|
|
526
874
|
method: "POST",
|
|
527
875
|
headers: {
|
|
528
|
-
|
|
876
|
+
Authorization: `Bearer ${pat}`,
|
|
529
877
|
"Content-Type": "application/json",
|
|
530
|
-
|
|
878
|
+
Accept: "application/vnd.github.v3+json"
|
|
531
879
|
},
|
|
532
880
|
body: JSON.stringify({ body })
|
|
533
881
|
});
|
|
@@ -545,7 +893,7 @@ async function postNoteToGitLab(repo, mrIid, body) {
|
|
|
545
893
|
headers: {
|
|
546
894
|
"PRIVATE-TOKEN": pat,
|
|
547
895
|
"Content-Type": "application/json",
|
|
548
|
-
|
|
896
|
+
Accept: "application/json"
|
|
549
897
|
},
|
|
550
898
|
body: JSON.stringify({ body })
|
|
551
899
|
});
|
|
@@ -559,11 +907,30 @@ async function postNoteToGitLab(repo, mrIid, body) {
|
|
|
559
907
|
}
|
|
560
908
|
const MAX_BRANCH_FILTER_LENGTH = 200;
|
|
561
909
|
const loggedBadFilters = /* @__PURE__ */ new Set();
|
|
910
|
+
function getFlowPostMode(flow) {
|
|
911
|
+
var _a;
|
|
912
|
+
const rawValue = (_a = flow == null ? void 0 : flow.get) == null ? void 0 : _a.call(flow, "postMode");
|
|
913
|
+
const value = rawValue && typeof rawValue === "object" && "value" in rawValue ? rawValue.value : rawValue;
|
|
914
|
+
const normalized = String(value || "manual").trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
915
|
+
if (["auto", "auto_post", "autopost", "auto_post_to_mr"].includes(normalized)) return "auto";
|
|
916
|
+
if (["disabled", "disable", "do_not_post", "dont_post", "none", "skip", "skipped"].includes(normalized)) {
|
|
917
|
+
return "disabled";
|
|
918
|
+
}
|
|
919
|
+
return "manual";
|
|
920
|
+
}
|
|
921
|
+
function getInitialPostStatus(flow, targetType) {
|
|
922
|
+
const postMode = getFlowPostMode(flow);
|
|
923
|
+
if (postMode === "disabled") return "skipped";
|
|
924
|
+
if (postMode === "auto" && targetType !== "mr") return "skipped";
|
|
925
|
+
return "pending_approval";
|
|
926
|
+
}
|
|
562
927
|
function warnInvalidBranchFilter(filter, reason) {
|
|
563
928
|
if (loggedBadFilters.has(filter)) return;
|
|
564
929
|
loggedBadFilters.add(filter);
|
|
565
930
|
console.warn(
|
|
566
|
-
`[plugin-git-manager] branchFilter rejected (${reason}): ${JSON.stringify(
|
|
931
|
+
`[plugin-git-manager] branchFilter rejected (${reason}): ${JSON.stringify(
|
|
932
|
+
filter
|
|
933
|
+
)}. Flow will not match any branch.`
|
|
567
934
|
);
|
|
568
935
|
}
|
|
569
936
|
function branchMatches(flow, branch) {
|
|
@@ -601,7 +968,7 @@ async function recoverStuckReviews(app) {
|
|
|
601
968
|
const cutoff = new Date(Date.now() - STUCK_REVIEW_CUTOFF_MS);
|
|
602
969
|
const stuck = await reviewsRepo.find({
|
|
603
970
|
filter: {
|
|
604
|
-
status:
|
|
971
|
+
status: "running",
|
|
605
972
|
startedAt: { $lt: cutoff }
|
|
606
973
|
}
|
|
607
974
|
});
|
|
@@ -629,11 +996,14 @@ async function recoverStuckReviews(app) {
|
|
|
629
996
|
}
|
|
630
997
|
// Annotate the CommonJS export names for ESM import in node:
|
|
631
998
|
0 && (module.exports = {
|
|
999
|
+
WORKER_JOB_GIT_REVIEW_PROCESS,
|
|
632
1000
|
branchMatches,
|
|
633
1001
|
pickFlowMatchingBranch,
|
|
634
1002
|
recoverStuckReviews,
|
|
1003
|
+
registerReviewQueue,
|
|
635
1004
|
reviewApprovePost,
|
|
636
1005
|
reviewReject,
|
|
637
1006
|
triggerReview,
|
|
638
|
-
triggerReviewInternal
|
|
1007
|
+
triggerReviewInternal,
|
|
1008
|
+
unregisterReviewQueue
|
|
639
1009
|
});
|
package/dist/server/ai-tools.js
CHANGED
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* This file is part of the NocoBase (R) project.
|
|
3
|
-
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
-
* Authors: NocoBase Team.
|
|
5
|
-
*
|
|
6
|
-
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
-
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
1
|
var __create = Object.create;
|
|
11
2
|
var __defProp = Object.defineProperty;
|
|
12
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|