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.
Files changed (38) hide show
  1. package/dist/client/187.d5545b7cc8b90bfc.js +1 -0
  2. package/dist/client/228.7588a0707cb3694a.js +0 -9
  3. package/dist/client/index.js +1 -10
  4. package/dist/externalVersion.js +5 -14
  5. package/dist/index.js +0 -9
  6. package/dist/locale/en-US.json +2 -0
  7. package/dist/locale/vi-VN.json +2 -0
  8. package/dist/server/actions/git-actions.js +15 -21
  9. package/dist/server/actions/gitlab-api.js +0 -9
  10. package/dist/server/actions/poller.js +0 -9
  11. package/dist/server/actions/review.d.ts +5 -2
  12. package/dist/server/actions/review.js +430 -60
  13. package/dist/server/ai-tools.js +0 -9
  14. package/dist/server/collections/gitCodeReviews.d.ts +1 -1
  15. package/dist/server/collections/gitCodeReviews.js +1 -9
  16. package/dist/server/collections/gitRepositories.d.ts +1 -1
  17. package/dist/server/collections/gitRepositories.js +0 -9
  18. package/dist/server/collections/gitReviewFlows.d.ts +1 -1
  19. package/dist/server/collections/gitReviewFlows.js +0 -9
  20. package/dist/server/index.js +0 -9
  21. package/dist/server/migrations/20260508000000-add-auto-review-flow-id.js +0 -9
  22. package/dist/server/plugin.js +5 -9
  23. package/dist/server/poller.js +0 -9
  24. package/dist/server/utils/gitlab-url.js +0 -9
  25. package/dist/server/utils/redact.js +0 -9
  26. package/package.json +2 -2
  27. package/src/client/components/CommitHistory.tsx +3 -0
  28. package/src/client/components/FileExplorer.tsx +29 -24
  29. package/src/client/components/GitOperations.tsx +3 -0
  30. package/src/client/components/ReviewFlows.tsx +11 -1
  31. package/src/client/components/ReviewHistory.tsx +14 -1
  32. package/src/locale/en-US.json +2 -0
  33. package/src/locale/vi-VN.json +2 -0
  34. package/src/server/actions/git-actions.ts +15 -12
  35. package/src/server/actions/review.ts +504 -63
  36. package/src/server/collections/gitCodeReviews.ts +1 -0
  37. package/src/server/plugin.ts +25 -20
  38. 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 AIEmployee.invoke runs in
18
- * the background. The action returns immediately with the reviewId.
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
- // Stamp startedAt synchronously so `recoverStuckReviews` can sweep rows
146
- // that get stuck in `pending` (process died before runReview ran).
147
- // runReview's own update will refresh this on actual start.
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.get("postMode") === "disabled" ? "skipped" : "pending_approval",
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
- setImmediate(
170
- () => runReview(app, {
171
- reviewId,
172
- flow,
173
- repo,
174
- targetType: args.targetType,
175
- mrIid: args.targetType === "mr" ? args.mrIid : null,
176
- commitSha: args.targetType === "commit" ? args.commitSha : null,
177
- branch: args.branch || void 0,
178
- headSha,
179
- aiEmployeeUsername,
180
- extraInstructions: args.extraInstructions,
181
- userId: args.userId ?? null
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) throw new Error("AIEmployee class not found \u2014 plugin-ai may not be installed or its exports changed");
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
- (_, reject) => setTimeout(() => reject(new Error("AI review timed out after 5 minutes")), REVIEW_TIMEOUT_MS)
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.get("postMode");
361
- let postStatus = "pending_approval";
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(`Use the \`git_get_merge_request\` tool with repositoryId=${args.repo.get("id")} and mrIid=${args.mrIid} to fetch the diff and metadata.`);
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(`Use the \`git_get_commit\` tool with repositoryId=${args.repo.get("id")} and commitHash=${args.commitSha} to fetch the diff.`);
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(`Use \`git_list_commits\`, \`git_get_diff\`, and \`git_get_file_content\` (with repositoryId=${args.repo.get("id")}) to inspect recent changes on this branch.`);
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("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.");
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: { "Authorization": `Bearer ${pat}`, Accept: "application/vnd.github.v3+json" }
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
- "Authorization": `Bearer ${pat}`,
876
+ Authorization: `Bearer ${pat}`,
529
877
  "Content-Type": "application/json",
530
- "Accept": "application/vnd.github.v3+json"
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
- "Accept": "application/json"
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(filter)}. Flow will not match any branch.`
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: { $in: ["running", "pending"] },
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
  });
@@ -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;