plugin-git-manager 1.1.9 → 1.1.12

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 (42) hide show
  1. package/dist/client/187.d5545b7cc8b90bfc.js +10 -0
  2. package/dist/client/components/RunReviewButton.d.ts +1 -1
  3. package/dist/client/context/GitManagerContext.d.ts +2 -0
  4. package/dist/client/index.js +1 -1
  5. package/dist/externalVersion.js +6 -4
  6. package/dist/locale/en-US.json +10 -1
  7. package/dist/locale/vi-VN.json +2 -0
  8. package/dist/server/actions/git-actions.js +15 -12
  9. package/dist/server/actions/gitlab-api.js +14 -13
  10. package/dist/server/actions/review.d.ts +5 -2
  11. package/dist/server/actions/review.js +184 -37
  12. package/dist/server/ai-tools.js +2 -0
  13. package/dist/server/collections/gitCodeReviews.js +1 -0
  14. package/dist/server/collections/gitRepositories.js +12 -0
  15. package/dist/server/migrations/20260508000000-add-auto-review-flow-id.d.ts +6 -0
  16. package/dist/server/migrations/20260508000000-add-auto-review-flow-id.js +57 -0
  17. package/dist/server/plugin.d.ts +4 -0
  18. package/dist/server/plugin.js +43 -6
  19. package/dist/server/poller.js +3 -1
  20. package/package.json +1 -1
  21. package/src/client/components/CommitHistory.tsx +21 -3
  22. package/src/client/components/FileExplorer.tsx +29 -24
  23. package/src/client/components/GitOperations.tsx +32 -16
  24. package/src/client/components/PollingStatus.tsx +27 -1
  25. package/src/client/components/RepositoryConfig.tsx +76 -3
  26. package/src/client/components/ReviewFlows.tsx +11 -1
  27. package/src/client/components/ReviewHistory.tsx +14 -1
  28. package/src/client/components/RunReviewButton.tsx +375 -278
  29. package/src/client/context/GitManagerContext.tsx +2 -0
  30. package/src/client/index.tsx +31 -31
  31. package/src/locale/en-US.json +10 -1
  32. package/src/locale/vi-VN.json +2 -0
  33. package/src/server/actions/git-actions.ts +15 -12
  34. package/src/server/actions/gitlab-api.ts +8 -4
  35. package/src/server/actions/review.ts +226 -41
  36. package/src/server/ai-tools.ts +1 -0
  37. package/src/server/collections/gitCodeReviews.ts +1 -0
  38. package/src/server/collections/gitRepositories.ts +12 -0
  39. package/src/server/migrations/20260508000000-add-auto-review-flow-id.ts +29 -0
  40. package/src/server/plugin.ts +205 -164
  41. package/src/server/poller.ts +11 -2
  42. package/dist/client/187.eec7be93247463d7.js +0 -10
@@ -32,6 +32,10 @@ __export(gitlab_api_exports, {
32
32
  });
33
33
  module.exports = __toCommonJS(gitlab_api_exports);
34
34
  var import_gitlab_url = require("../utils/gitlab-url");
35
+ function getActionParams(ctx) {
36
+ var _a, _b;
37
+ return { ...ctx.action.params, ...(_a = ctx.action.params) == null ? void 0 : _a.values, ...((_b = ctx.request) == null ? void 0 : _b.body) || {} };
38
+ }
35
39
  async function gitlabFetch(apiBase, endpoint, pat, params) {
36
40
  const url = new URL(`${apiBase}${endpoint}`);
37
41
  if (params) {
@@ -57,8 +61,7 @@ async function gitlabFetch(apiBase, endpoint, pat, params) {
57
61
  return { data, totalPages: totalPages ? parseInt(totalPages, 10) : null, total: total ? parseInt(total, 10) : null };
58
62
  }
59
63
  async function getRepoApiContext(ctx) {
60
- var _a;
61
- const params = { ...ctx.action.params, ...(_a = ctx.action.params) == null ? void 0 : _a.values, ...ctx.request.body || {} };
64
+ const params = getActionParams(ctx);
62
65
  const { repositoryId } = params;
63
66
  const repo = await ctx.db.getRepository("gitRepositories").findOne({
64
67
  filterByTk: repositoryId
@@ -105,9 +108,8 @@ async function githubFetch(endpoint, pat, params) {
105
108
  return { data, totalPages, total: null };
106
109
  }
107
110
  async function mergeRequests(ctx, next) {
108
- var _a;
109
111
  const { pat, apiBase, encodedProject, projectPath, isGitHub } = await getRepoApiContext(ctx);
110
- const params = { ...ctx.action.params, ...(_a = ctx.action.params) == null ? void 0 : _a.values, ...ctx.request.body || {} };
112
+ const params = getActionParams(ctx);
111
113
  const {
112
114
  state = "opened",
113
115
  search,
@@ -138,14 +140,14 @@ async function mergeRequests(ctx, next) {
138
140
  result.total = null;
139
141
  }
140
142
  items = pullRequests.map((pr) => {
141
- var _a2, _b;
143
+ var _a, _b;
142
144
  return {
143
145
  id: pr.id,
144
146
  iid: pr.number,
145
147
  title: pr.title,
146
148
  description: pr.body,
147
149
  state: pr.state === "open" ? "opened" : pr.merged_at ? "merged" : "closed",
148
- sourceBranch: (_a2 = pr.head) == null ? void 0 : _a2.ref,
150
+ sourceBranch: (_a = pr.head) == null ? void 0 : _a.ref,
149
151
  targetBranch: (_b = pr.base) == null ? void 0 : _b.ref,
150
152
  author: pr.user ? { name: pr.user.login, username: pr.user.login, avatarUrl: pr.user.avatar_url } : null,
151
153
  assignees: (pr.assignees || []).map((a) => ({ name: a.login, username: a.login, avatarUrl: a.avatar_url })),
@@ -215,9 +217,9 @@ async function mergeRequests(ctx, next) {
215
217
  await next();
216
218
  }
217
219
  async function mergeRequestDetail(ctx, next) {
218
- var _a, _b, _c, _d;
220
+ var _a, _b, _c;
219
221
  const { pat, apiBase, encodedProject, projectPath, isGitHub } = await getRepoApiContext(ctx);
220
- const params = { ...ctx.action.params, ...(_a = ctx.action.params) == null ? void 0 : _a.values, ...ctx.request.body || {} };
222
+ const params = getActionParams(ctx);
221
223
  const { mrIid } = params;
222
224
  if (!mrIid) {
223
225
  ctx.throw(400, "mrIid is required");
@@ -243,8 +245,8 @@ async function mergeRequestDetail(ctx, next) {
243
245
  title: pr.title,
244
246
  description: pr.body,
245
247
  state: pr.state === "open" ? "opened" : pr.merged_at ? "merged" : "closed",
246
- sourceBranch: (_b = pr.head) == null ? void 0 : _b.ref,
247
- targetBranch: (_c = pr.base) == null ? void 0 : _c.ref,
248
+ sourceBranch: (_a = pr.head) == null ? void 0 : _a.ref,
249
+ targetBranch: (_b = pr.base) == null ? void 0 : _b.ref,
248
250
  author: pr.user ? { name: pr.user.login, username: pr.user.login, avatarUrl: pr.user.avatar_url } : null,
249
251
  assignees: (pr.assignees || []).map((a) => ({ name: a.login, username: a.login, avatarUrl: a.avatar_url })),
250
252
  reviewers: (pr.requested_reviewers || []).map((r) => ({ name: r.login, username: r.login, avatarUrl: r.avatar_url })),
@@ -276,7 +278,7 @@ async function mergeRequestDetail(ctx, next) {
276
278
  gitlabFetch(apiBase, `/projects/${encodedProject}/merge_requests/${mrIid}/changes`, pat).catch(() => ({ data: {} }))
277
279
  ]);
278
280
  const mr = mrResult.data;
279
- const changes = (((_d = changesResult.data) == null ? void 0 : _d.changes) || []).map((c) => ({
281
+ const changes = (((_c = changesResult.data) == null ? void 0 : _c.changes) || []).map((c) => ({
280
282
  oldPath: c.old_path,
281
283
  newPath: c.new_path,
282
284
  newFile: c.new_file,
@@ -318,9 +320,8 @@ async function mergeRequestDetail(ctx, next) {
318
320
  await next();
319
321
  }
320
322
  async function mergeRequestNotes(ctx, next) {
321
- var _a;
322
323
  const { pat, apiBase, encodedProject, projectPath, isGitHub } = await getRepoApiContext(ctx);
323
- const params = { ...ctx.action.params, ...(_a = ctx.action.params) == null ? void 0 : _a.values, ...ctx.request.body || {} };
324
+ const params = getActionParams(ctx);
324
325
  const { mrIid, page = 1, perPage = 50 } = params;
325
326
  if (!mrIid) {
326
327
  ctx.throw(400, "mrIid is required");
@@ -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
  */
@@ -26,17 +26,34 @@ var __copyProps = (to, from, except, desc) => {
26
26
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
27
27
  var review_exports = {};
28
28
  __export(review_exports, {
29
+ WORKER_JOB_GIT_REVIEW_PROCESS: () => WORKER_JOB_GIT_REVIEW_PROCESS,
29
30
  branchMatches: () => branchMatches,
30
31
  pickFlowMatchingBranch: () => pickFlowMatchingBranch,
31
32
  recoverStuckReviews: () => recoverStuckReviews,
33
+ registerReviewQueue: () => registerReviewQueue,
32
34
  reviewApprovePost: () => reviewApprovePost,
33
35
  reviewReject: () => reviewReject,
34
36
  triggerReview: () => triggerReview,
35
- triggerReviewInternal: () => triggerReviewInternal
37
+ triggerReviewInternal: () => triggerReviewInternal,
38
+ unregisterReviewQueue: () => unregisterReviewQueue
36
39
  });
37
40
  module.exports = __toCommonJS(review_exports);
38
41
  var import_gitlab_url = require("../utils/gitlab-url");
39
42
  var import_redact = require("../utils/redact");
43
+ const WORKER_JOB_GIT_REVIEW_PROCESS = "git-review:process";
44
+ const REVIEW_QUEUE_CHANNEL = "plugin-git-manager.review";
45
+ const REVIEW_QUEUE_CONCURRENCY = Math.max(
46
+ 1,
47
+ Number.parseInt(process.env.GIT_REVIEW_QUEUE_CONCURRENCY || process.env.GIT_REVIEW_MAX_CONCURRENCY || "3", 10) || 3
48
+ );
49
+ const REVIEW_QUEUE_TIMEOUT_MS = Math.max(
50
+ 6e4,
51
+ Number.parseInt(process.env.GIT_REVIEW_QUEUE_TIMEOUT_MS || "", 10) || 10 * 60 * 1e3
52
+ );
53
+ function getActionParams(ctx) {
54
+ var _a, _b;
55
+ return { ...ctx.action.params, ...(_a = ctx.action.params) == null ? void 0 : _a.values, ...((_b = ctx.request) == null ? void 0 : _b.body) || {} };
56
+ }
40
57
  function targetKey(args) {
41
58
  if (args.targetType === "mr") return `${args.repositoryId}:mr:${args.mrIid}`;
42
59
  if (args.targetType === "commit") return `${args.repositoryId}:commit:${args.commitSha}`;
@@ -48,8 +65,8 @@ async function withTriggerLock(app, key, fn) {
48
65
  return app.lockManager.runExclusive(lockKey, fn, TRIGGER_LOCK_TTL_MS);
49
66
  }
50
67
  async function triggerReview(ctx, next) {
51
- var _a, _b, _c;
52
- const params = { ...ctx.action.params, ...(_a = ctx.action.params) == null ? void 0 : _a.values, ...ctx.request.body || {} };
68
+ var _a, _b;
69
+ const params = getActionParams(ctx);
53
70
  const {
54
71
  flowId,
55
72
  repositoryId,
@@ -65,7 +82,7 @@ async function triggerReview(ctx, next) {
65
82
  if (targetType === "mr" && !mrIid) ctx.throw(400, "mrIid is required for MR review");
66
83
  if (targetType === "commit" && !commitSha) ctx.throw(400, "commitSha is required for commit review");
67
84
  if (targetType === "branch" && !branch) ctx.throw(400, "branch is required for branch review");
68
- const userId = (_c = (_b = ctx.state) == null ? void 0 : _b.currentUser) == null ? void 0 : _c.id;
85
+ const userId = (_b = (_a = ctx.state) == null ? void 0 : _a.currentUser) == null ? void 0 : _b.id;
69
86
  try {
70
87
  const reviewId = await triggerReviewInternal(ctx.app, {
71
88
  flowId,
@@ -138,13 +155,12 @@ async function triggerReviewInternalLocked(app, args) {
138
155
  latestSha: headSha || existingLatestSha || null,
139
156
  triggeredBy: args.triggeredBy || "manual",
140
157
  status: "pending",
141
- // Stamp startedAt synchronously so `recoverStuckReviews` can sweep rows
142
- // that get stuck in `pending` (process died before runReview ran).
143
- // runReview's own update will refresh this on actual start.
144
- startedAt: /* @__PURE__ */ new Date(),
158
+ // `startedAt` is stamped by the queue worker when execution actually
159
+ // starts. Pending rows may be legitimately waiting in Redis.
160
+ startedAt: null,
145
161
  finishedAt: null,
146
162
  durationMs: null,
147
- postStatus: flow.get("postMode") === "disabled" ? "skipped" : "pending_approval",
163
+ postStatus: getInitialPostStatus(flow, args.targetType),
148
164
  error: null
149
165
  };
150
166
  let reviewId;
@@ -162,29 +178,133 @@ async function triggerReviewInternalLocked(app, args) {
162
178
  const review = await reviewsRepo.create({ values: baseValues });
163
179
  reviewId = review.get("id");
164
180
  }
165
- setImmediate(
166
- () => runReview(app, {
167
- reviewId,
181
+ await enqueueReview(app, {
182
+ reviewId,
183
+ repositoryId: args.repositoryId,
184
+ targetType: args.targetType,
185
+ mrIid: args.targetType === "mr" ? args.mrIid : null,
186
+ commitSha: args.targetType === "commit" ? args.commitSha : null,
187
+ branch: args.branch || void 0,
188
+ headSha,
189
+ aiEmployeeUsername,
190
+ extraInstructions: args.extraInstructions,
191
+ userId: args.userId ?? null,
192
+ flowSnapshot: createFlowSnapshot(flow)
193
+ });
194
+ return reviewId;
195
+ }
196
+ function registerReviewQueue(app) {
197
+ app.eventQueue.subscribe(REVIEW_QUEUE_CHANNEL, {
198
+ concurrency: REVIEW_QUEUE_CONCURRENCY,
199
+ idle: () => isGitReviewWorker(app),
200
+ process: async (message) => {
201
+ await processQueuedReview(app, message);
202
+ }
203
+ });
204
+ }
205
+ function unregisterReviewQueue(app) {
206
+ app.eventQueue.unsubscribe(REVIEW_QUEUE_CHANNEL);
207
+ }
208
+ function createFlowSnapshot(flow) {
209
+ return {
210
+ id: Number(flow.get("id")),
211
+ name: flow.get("name"),
212
+ postMode: flow.get("postMode"),
213
+ llmService: flow.get("llmService"),
214
+ model: flow.get("model"),
215
+ instructions: flow.get("instructions")
216
+ };
217
+ }
218
+ function createFlowFromSnapshot(snapshot, fallback) {
219
+ return {
220
+ get(name) {
221
+ var _a;
222
+ if (snapshot && Object.prototype.hasOwnProperty.call(snapshot, name)) {
223
+ return snapshot[name];
224
+ }
225
+ return (_a = fallback == null ? void 0 : fallback.get) == null ? void 0 : _a.call(fallback, name);
226
+ }
227
+ };
228
+ }
229
+ function isGitReviewWorker(app) {
230
+ const workerMode = process.env.WORKER_MODE || "";
231
+ return app.serving(WORKER_JOB_GIT_REVIEW_PROCESS) || workerMode === "worker" || workerMode === "task" || process.env.APP_ROLE === "worker";
232
+ }
233
+ async function enqueueReview(app, message) {
234
+ try {
235
+ await app.eventQueue.publish(REVIEW_QUEUE_CHANNEL, message, {
236
+ timeout: REVIEW_QUEUE_TIMEOUT_MS,
237
+ maxRetries: 1
238
+ });
239
+ } catch (err) {
240
+ const safeMessage = (0, import_redact.redactPat)((err == null ? void 0 : err.message) || String(err));
241
+ await app.db.getRepository("gitCodeReviews").update({
242
+ filterByTk: message.reviewId,
243
+ values: {
244
+ status: "failed",
245
+ error: `Failed to enqueue review: ${safeMessage}`,
246
+ finishedAt: /* @__PURE__ */ new Date()
247
+ }
248
+ });
249
+ throw err;
250
+ }
251
+ }
252
+ async function failQueuedReview(app, reviewId, err) {
253
+ const safeMessage = (0, import_redact.redactPat)((err == null ? void 0 : err.message) || String(err));
254
+ await app.db.getRepository("gitCodeReviews").update({
255
+ filterByTk: reviewId,
256
+ values: {
257
+ status: "failed",
258
+ error: safeMessage,
259
+ finishedAt: /* @__PURE__ */ new Date()
260
+ }
261
+ });
262
+ }
263
+ async function processQueuedReview(app, message) {
264
+ var _a, _b, _c, _d, _e, _f, _g;
265
+ const db = app.db;
266
+ const reviewsRepo = db.getRepository("gitCodeReviews");
267
+ const review = await reviewsRepo.findOne({ filterByTk: message.reviewId });
268
+ if (!review) {
269
+ (_b = (_a = app.log) == null ? void 0 : _a.warn) == null ? void 0 : _b.call(_a, `git review queue: review ${message.reviewId} not found, skipping`);
270
+ return;
271
+ }
272
+ if (review.get("status") !== "pending") {
273
+ (_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`);
274
+ return;
275
+ }
276
+ try {
277
+ const repo = await db.getRepository("gitRepositories").findOne({
278
+ filterByTk: message.repositoryId || review.get("repositoryId")
279
+ });
280
+ if (!repo) throw new Error("Repository not found");
281
+ const storedFlow = await db.getRepository("gitReviewFlows").findOne({
282
+ filterByTk: ((_e = message.flowSnapshot) == null ? void 0 : _e.id) || review.get("flowId")
283
+ });
284
+ const flow = createFlowFromSnapshot(message.flowSnapshot, storedFlow);
285
+ const aiEmployeeUsername = message.aiEmployeeUsername || flow.get("aiEmployeeUsername");
286
+ if (!aiEmployeeUsername) throw new Error("Flow has no AI employee configured");
287
+ await runReview(app, {
288
+ reviewId: message.reviewId,
168
289
  flow,
169
290
  repo,
170
- targetType: args.targetType,
171
- mrIid: args.targetType === "mr" ? args.mrIid : null,
172
- commitSha: args.targetType === "commit" ? args.commitSha : null,
173
- branch: args.branch || void 0,
174
- headSha,
291
+ targetType: message.targetType || review.get("targetType"),
292
+ mrIid: message.targetType === "mr" ? message.mrIid ?? Number(review.get("mrIid")) : null,
293
+ commitSha: message.targetType === "commit" ? message.commitSha || review.get("commitSha") : null,
294
+ branch: message.branch || review.get("branch"),
295
+ headSha: message.headSha || review.get("headSha"),
175
296
  aiEmployeeUsername,
176
- extraInstructions: args.extraInstructions,
177
- userId: args.userId ?? null
178
- }).catch((err) => {
179
- var _a, _b;
180
- (_b = (_a = app.log) == null ? void 0 : _a.error) == null ? void 0 : _b.call(_a, "runReview background error", err);
181
- })
182
- );
183
- return reviewId;
297
+ extraInstructions: message.extraInstructions,
298
+ userId: message.userId ?? null
299
+ });
300
+ } catch (err) {
301
+ (_g = (_f = app.log) == null ? void 0 : _f.error) == null ? void 0 : _g.call(_f, "git review queue: failed before review execution", err);
302
+ await failQueuedReview(app, message.reviewId, err);
303
+ }
184
304
  }
185
305
  async function reviewApprovePost(ctx, next) {
186
- var _a, _b, _c;
187
- const params = { ...ctx.action.params, ...(_a = ctx.action.params) == null ? void 0 : _a.values, ...ctx.request.body || {} };
306
+ var _a, _b;
307
+ const params = getActionParams(ctx);
188
308
  const { reviewId, editedMarkdown } = params;
189
309
  if (!reviewId) ctx.throw(400, "reviewId is required");
190
310
  const reviewsRepo = ctx.db.getRepository("gitCodeReviews");
@@ -199,7 +319,7 @@ async function reviewApprovePost(ctx, next) {
199
319
  });
200
320
  if (!repo) ctx.throw(404, "Repository not found");
201
321
  const noteId = await postNoteToGitLab(repo, Number(review.get("mrIid")), markdown);
202
- const userId = (_c = (_b = ctx.state) == null ? void 0 : _b.currentUser) == null ? void 0 : _c.id;
322
+ const userId = (_b = (_a = ctx.state) == null ? void 0 : _a.currentUser) == null ? void 0 : _b.id;
203
323
  await reviewsRepo.update({
204
324
  filterByTk: reviewId,
205
325
  values: {
@@ -207,21 +327,22 @@ async function reviewApprovePost(ctx, next) {
207
327
  postStatus: "posted",
208
328
  postedNoteId: String(noteId),
209
329
  approvedBy: userId ? String(userId) : null,
210
- approvedAt: /* @__PURE__ */ new Date()
330
+ approvedAt: /* @__PURE__ */ new Date(),
331
+ error: null
211
332
  }
212
333
  });
213
334
  ctx.body = { success: true, data: { reviewId, postedNoteId: noteId } };
214
335
  await next();
215
336
  }
216
337
  async function reviewReject(ctx, next) {
217
- var _a, _b, _c;
218
- const params = { ...ctx.action.params, ...(_a = ctx.action.params) == null ? void 0 : _a.values, ...ctx.request.body || {} };
338
+ var _a, _b;
339
+ const params = getActionParams(ctx);
219
340
  const { reviewId, reason } = params;
220
341
  if (!reviewId) ctx.throw(400, "reviewId is required");
221
342
  const reviewsRepo = ctx.db.getRepository("gitCodeReviews");
222
343
  const review = await reviewsRepo.findOne({ filterByTk: reviewId });
223
344
  if (!review) ctx.throw(404, "Review not found");
224
- const userId = (_c = (_b = ctx.state) == null ? void 0 : _b.currentUser) == null ? void 0 : _c.id;
345
+ const userId = (_b = (_a = ctx.state) == null ? void 0 : _a.currentUser) == null ? void 0 : _b.id;
225
346
  await reviewsRepo.update({
226
347
  filterByTk: reviewId,
227
348
  values: {
@@ -353,9 +474,10 @@ async function runReview(app, args) {
353
474
  }
354
475
  const finishedAt = /* @__PURE__ */ new Date();
355
476
  const durationMs = finishedAt.getTime() - startedAt.getTime();
356
- const postMode = args.flow.get("postMode");
357
- let postStatus = "pending_approval";
477
+ const postMode = getFlowPostMode(args.flow);
478
+ let postStatus = getInitialPostStatus(args.flow, args.targetType);
358
479
  let postedNoteId = null;
480
+ let autoPostError = null;
359
481
  if (postMode === "disabled") {
360
482
  postStatus = "skipped";
361
483
  } else if (postMode === "auto" && args.targetType === "mr" && args.mrIid) {
@@ -363,6 +485,8 @@ async function runReview(app, args) {
363
485
  postedNoteId = String(await postNoteToGitLab(args.repo, args.mrIid, content));
364
486
  postStatus = "posted";
365
487
  } catch (err) {
488
+ autoPostError = (0, import_redact.redactPat)((err == null ? void 0 : err.message) || String(err));
489
+ postStatus = "post_failed";
366
490
  (_c = (_b = app.log) == null ? void 0 : _b.error) == null ? void 0 : _c.call(_b, "Auto-post review note failed", err);
367
491
  }
368
492
  }
@@ -380,11 +504,14 @@ async function runReview(app, args) {
380
504
  finishedAt,
381
505
  postStatus,
382
506
  postedNoteId,
507
+ error: autoPostError ? `Auto-post failed: ${autoPostError}` : null,
383
508
  metadata: {
384
509
  flowName: args.flow.get("name"),
385
510
  aiEmployeeUsername: args.aiEmployeeUsername,
386
511
  llmService,
387
- model
512
+ model,
513
+ postMode,
514
+ autoPostError
388
515
  }
389
516
  }
390
517
  });
@@ -555,6 +682,23 @@ async function postNoteToGitLab(repo, mrIid, body) {
555
682
  }
556
683
  const MAX_BRANCH_FILTER_LENGTH = 200;
557
684
  const loggedBadFilters = /* @__PURE__ */ new Set();
685
+ function getFlowPostMode(flow) {
686
+ var _a;
687
+ const rawValue = (_a = flow == null ? void 0 : flow.get) == null ? void 0 : _a.call(flow, "postMode");
688
+ const value = rawValue && typeof rawValue === "object" && "value" in rawValue ? rawValue.value : rawValue;
689
+ const normalized = String(value || "manual").trim().toLowerCase().replace(/[\s-]+/g, "_");
690
+ if (["auto", "auto_post", "autopost", "auto_post_to_mr"].includes(normalized)) return "auto";
691
+ if (["disabled", "disable", "do_not_post", "dont_post", "none", "skip", "skipped"].includes(normalized)) {
692
+ return "disabled";
693
+ }
694
+ return "manual";
695
+ }
696
+ function getInitialPostStatus(flow, targetType) {
697
+ const postMode = getFlowPostMode(flow);
698
+ if (postMode === "disabled") return "skipped";
699
+ if (postMode === "auto" && targetType !== "mr") return "skipped";
700
+ return "pending_approval";
701
+ }
558
702
  function warnInvalidBranchFilter(filter, reason) {
559
703
  if (loggedBadFilters.has(filter)) return;
560
704
  loggedBadFilters.add(filter);
@@ -597,7 +741,7 @@ async function recoverStuckReviews(app) {
597
741
  const cutoff = new Date(Date.now() - STUCK_REVIEW_CUTOFF_MS);
598
742
  const stuck = await reviewsRepo.find({
599
743
  filter: {
600
- status: { $in: ["running", "pending"] },
744
+ status: "running",
601
745
  startedAt: { $lt: cutoff }
602
746
  }
603
747
  });
@@ -625,11 +769,14 @@ async function recoverStuckReviews(app) {
625
769
  }
626
770
  // Annotate the CommonJS export names for ESM import in node:
627
771
  0 && (module.exports = {
772
+ WORKER_JOB_GIT_REVIEW_PROCESS,
628
773
  branchMatches,
629
774
  pickFlowMatchingBranch,
630
775
  recoverStuckReviews,
776
+ registerReviewQueue,
631
777
  reviewApprovePost,
632
778
  reviewReject,
633
779
  triggerReview,
634
- triggerReviewInternal
780
+ triggerReviewInternal,
781
+ unregisterReviewQueue
635
782
  });
@@ -97,12 +97,14 @@ function registerGitReviewAiTools(app) {
97
97
  return obj;
98
98
  };
99
99
  const runResourceAction = async (ctx, handler, params, gate) => {
100
+ var _a2;
100
101
  enforceAcl(ctx, gate.resource, gate.action, params);
101
102
  const synthCtx = {
102
103
  ...ctx,
103
104
  app: ctx.app,
104
105
  db: ctx.db,
105
106
  action: { params },
107
+ request: { ...ctx.request || {}, body: ((_a2 = ctx.request) == null ? void 0 : _a2.body) || {} },
106
108
  throw: (status, message) => {
107
109
  const err = new Error(message);
108
110
  err.status = status;
@@ -166,6 +166,7 @@ var gitCodeReviews_default = (0, import_database.defineCollection)({
166
166
  { value: "pending_approval", label: "Pending Approval" },
167
167
  { value: "approved", label: "Approved" },
168
168
  { value: "posted", label: "Posted" },
169
+ { value: "post_failed", label: "Post Failed" },
169
170
  { value: "skipped", label: "Skipped" },
170
171
  { value: "rejected", label: "Rejected" }
171
172
  ]
@@ -87,6 +87,18 @@ var gitRepositories_default = (0, import_database.defineCollection)({
87
87
  interface: "checkbox",
88
88
  uiSchema: { title: "Auto Review", type: "boolean", "x-component": "Checkbox" }
89
89
  },
90
+ {
91
+ type: "belongsTo",
92
+ name: "autoReviewFlow",
93
+ target: "gitReviewFlows",
94
+ foreignKey: "autoReviewFlowId",
95
+ interface: "m2o",
96
+ uiSchema: {
97
+ title: "Primary Auto Review Flow",
98
+ "x-component": "AssociationField",
99
+ "x-component-props": { fieldNames: { label: "name", value: "id" } }
100
+ }
101
+ },
90
102
  {
91
103
  type: "date",
92
104
  name: "lastPolledAt",
@@ -0,0 +1,6 @@
1
+ import { Migration } from '@nocobase/server';
2
+ export default class AddAutoReviewFlowIdMigration extends Migration {
3
+ on: string;
4
+ up(): Promise<void>;
5
+ down(): Promise<void>;
6
+ }
@@ -0,0 +1,57 @@
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
+ var __defProp = Object.defineProperty;
11
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
+ var __getOwnPropNames = Object.getOwnPropertyNames;
13
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __export = (target, all) => {
15
+ for (var name in all)
16
+ __defProp(target, name, { get: all[name], enumerable: true });
17
+ };
18
+ var __copyProps = (to, from, except, desc) => {
19
+ if (from && typeof from === "object" || typeof from === "function") {
20
+ for (let key of __getOwnPropNames(from))
21
+ if (!__hasOwnProp.call(to, key) && key !== except)
22
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
23
+ }
24
+ return to;
25
+ };
26
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
27
+ var add_auto_review_flow_id_exports = {};
28
+ __export(add_auto_review_flow_id_exports, {
29
+ default: () => AddAutoReviewFlowIdMigration
30
+ });
31
+ module.exports = __toCommonJS(add_auto_review_flow_id_exports);
32
+ var import_server = require("@nocobase/server");
33
+ var import_sequelize = require("sequelize");
34
+ class AddAutoReviewFlowIdMigration extends import_server.Migration {
35
+ on = "afterLoad";
36
+ async up() {
37
+ var _a;
38
+ const queryInterface = this.db.sequelize.getQueryInterface();
39
+ const tablePrefix = ((_a = this.db.options) == null ? void 0 : _a.tablePrefix) || "";
40
+ const tableName = `${tablePrefix}gitRepositories`;
41
+ const tableInfo = await queryInterface.describeTable(tableName).catch(() => null);
42
+ if (!tableInfo || tableInfo.autoReviewFlowId) return;
43
+ await queryInterface.addColumn(tableName, "autoReviewFlowId", {
44
+ type: import_sequelize.DataTypes.INTEGER,
45
+ allowNull: true
46
+ });
47
+ }
48
+ async down() {
49
+ var _a;
50
+ const queryInterface = this.db.sequelize.getQueryInterface();
51
+ const tablePrefix = ((_a = this.db.options) == null ? void 0 : _a.tablePrefix) || "";
52
+ const tableName = `${tablePrefix}gitRepositories`;
53
+ const tableInfo = await queryInterface.describeTable(tableName).catch(() => null);
54
+ if (!(tableInfo == null ? void 0 : tableInfo.autoReviewFlowId)) return;
55
+ await queryInterface.removeColumn(tableName, "autoReviewFlowId");
56
+ }
57
+ }
@@ -1,8 +1,12 @@
1
1
  import { Plugin } from '@nocobase/server';
2
2
  export declare class PluginGitManagerServer extends Plugin {
3
+ app: any;
4
+ db: any;
5
+ beforeLoad(): Promise<void>;
3
6
  load(): Promise<void>;
4
7
  install(): Promise<void>;
5
8
  beforeDisable(): Promise<void>;
6
9
  beforeUnload(): Promise<void>;
7
10
  }
11
+ export declare function ensureAutoReviewFlowSchema(app: any): Promise<void>;
8
12
  export default PluginGitManagerServer;