plugin-build-guide-block 1.1.5 → 1.1.7

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 (123) hide show
  1. package/client-v2.d.ts +2 -0
  2. package/client-v2.js +1 -0
  3. package/dist/client/index.js +2 -2
  4. package/dist/client-v2/73.6b8b2eda7d969c69.js +10 -0
  5. package/dist/client-v2/index.js +10 -0
  6. package/dist/externalVersion.js +9 -8
  7. package/dist/node_modules/sanitize-html/index.js +2 -2
  8. package/dist/node_modules/sanitize-html/package.json +1 -1
  9. package/dist/server/actions/build.js +687 -85
  10. package/dist/server/collections/ai-build-guide-spaces.js +20 -0
  11. package/dist/server/plugin.js +21 -19
  12. package/dist/server/tools/search-guides.js +41 -30
  13. package/package.json +7 -3
  14. package/src/client/components/BuildButton.tsx +20 -9
  15. package/src/client-v2/plugin.tsx +24 -0
  16. package/src/server/actions/build.ts +774 -88
  17. package/src/server/collections/ai-build-guide-spaces.ts +77 -57
  18. package/src/server/plugin.ts +170 -163
  19. package/src/server/tools/search-guides.ts +113 -95
  20. package/dist/client/UserGuideBlock.d.ts +0 -2
  21. package/dist/client/UserGuideBlockInitializer.d.ts +0 -2
  22. package/dist/client/UserGuideBlockProvider.d.ts +0 -2
  23. package/dist/client/UserGuideManager.d.ts +0 -2
  24. package/dist/client/components/BuildButton.d.ts +0 -2
  25. package/dist/client/components/LLMServiceSelect.d.ts +0 -2
  26. package/dist/client/components/ModelSelect.d.ts +0 -2
  27. package/dist/client/components/SpaceSelect.d.ts +0 -2
  28. package/dist/client/components/StatusTag.d.ts +0 -2
  29. package/dist/client/locale.d.ts +0 -3
  30. package/dist/client/models/UserGuideBlockModel.d.ts +0 -9
  31. package/dist/client/models/index.d.ts +0 -9
  32. package/dist/client/plugin.d.ts +0 -5
  33. package/dist/client/schemaSettings.d.ts +0 -2
  34. package/dist/client/schemas/spacesSchema.d.ts +0 -437
  35. package/dist/index.d.ts +0 -2
  36. package/dist/locale/namespace.d.ts +0 -6
  37. package/dist/node_modules/sanitize-html/node_modules/nanoid/async/index.browser.cjs +0 -34
  38. package/dist/node_modules/sanitize-html/node_modules/nanoid/async/index.browser.js +0 -34
  39. package/dist/node_modules/sanitize-html/node_modules/nanoid/async/index.cjs +0 -35
  40. package/dist/node_modules/sanitize-html/node_modules/nanoid/async/index.d.ts +0 -56
  41. package/dist/node_modules/sanitize-html/node_modules/nanoid/async/index.js +0 -35
  42. package/dist/node_modules/sanitize-html/node_modules/nanoid/async/index.native.js +0 -26
  43. package/dist/node_modules/sanitize-html/node_modules/nanoid/async/package.json +0 -12
  44. package/dist/node_modules/sanitize-html/node_modules/nanoid/bin/nanoid.cjs +0 -55
  45. package/dist/node_modules/sanitize-html/node_modules/nanoid/index.browser.cjs +0 -34
  46. package/dist/node_modules/sanitize-html/node_modules/nanoid/index.browser.js +0 -34
  47. package/dist/node_modules/sanitize-html/node_modules/nanoid/index.cjs +0 -45
  48. package/dist/node_modules/sanitize-html/node_modules/nanoid/index.d.cts +0 -91
  49. package/dist/node_modules/sanitize-html/node_modules/nanoid/index.d.ts +0 -91
  50. package/dist/node_modules/sanitize-html/node_modules/nanoid/index.js +0 -45
  51. package/dist/node_modules/sanitize-html/node_modules/nanoid/nanoid.js +0 -1
  52. package/dist/node_modules/sanitize-html/node_modules/nanoid/non-secure/index.cjs +0 -21
  53. package/dist/node_modules/sanitize-html/node_modules/nanoid/non-secure/index.d.ts +0 -33
  54. package/dist/node_modules/sanitize-html/node_modules/nanoid/non-secure/index.js +0 -21
  55. package/dist/node_modules/sanitize-html/node_modules/nanoid/non-secure/package.json +0 -6
  56. package/dist/node_modules/sanitize-html/node_modules/nanoid/package.json +0 -88
  57. package/dist/node_modules/sanitize-html/node_modules/nanoid/url-alphabet/index.cjs +0 -3
  58. package/dist/node_modules/sanitize-html/node_modules/nanoid/url-alphabet/index.js +0 -3
  59. package/dist/node_modules/sanitize-html/node_modules/nanoid/url-alphabet/package.json +0 -6
  60. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/at-rule.d.ts +0 -115
  61. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/at-rule.js +0 -25
  62. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/comment.d.ts +0 -67
  63. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/comment.js +0 -13
  64. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/container.d.ts +0 -452
  65. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/container.js +0 -439
  66. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/css-syntax-error.d.ts +0 -248
  67. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/css-syntax-error.js +0 -100
  68. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/declaration.d.ts +0 -148
  69. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/declaration.js +0 -24
  70. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/document.d.ts +0 -68
  71. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/document.js +0 -33
  72. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/fromJSON.d.ts +0 -9
  73. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/fromJSON.js +0 -54
  74. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/input.d.ts +0 -194
  75. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/input.js +0 -248
  76. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/lazy-result.d.ts +0 -190
  77. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/lazy-result.js +0 -550
  78. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/list.d.ts +0 -57
  79. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/list.js +0 -58
  80. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/map-generator.js +0 -359
  81. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/no-work-result.d.ts +0 -46
  82. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/no-work-result.js +0 -135
  83. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/node.d.ts +0 -536
  84. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/node.js +0 -381
  85. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/parse.d.ts +0 -9
  86. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/parse.js +0 -42
  87. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/parser.js +0 -610
  88. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/postcss.d.mts +0 -72
  89. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/postcss.d.ts +0 -441
  90. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/postcss.js +0 -101
  91. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/previous-map.d.ts +0 -81
  92. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/previous-map.js +0 -142
  93. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/processor.d.ts +0 -115
  94. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/processor.js +0 -67
  95. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/result.d.ts +0 -206
  96. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/result.js +0 -42
  97. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/root.d.ts +0 -86
  98. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/root.js +0 -61
  99. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/rule.d.ts +0 -113
  100. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/rule.js +0 -27
  101. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/stringifier.d.ts +0 -46
  102. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/stringifier.js +0 -353
  103. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/stringify.d.ts +0 -9
  104. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/stringify.js +0 -11
  105. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/symbols.js +0 -5
  106. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/terminal-highlight.js +0 -70
  107. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/tokenize.js +0 -266
  108. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/warn-once.js +0 -13
  109. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/warning.d.ts +0 -147
  110. package/dist/node_modules/sanitize-html/node_modules/postcss/lib/warning.js +0 -37
  111. package/dist/node_modules/sanitize-html/node_modules/postcss/node_modules/.bin/nanoid +0 -15
  112. package/dist/node_modules/sanitize-html/node_modules/postcss/node_modules/.bin/nanoid.cmd +0 -7
  113. package/dist/node_modules/sanitize-html/node_modules/postcss/package.json +0 -88
  114. package/dist/server/actions/build.d.ts +0 -2
  115. package/dist/server/actions/getHtml.d.ts +0 -2
  116. package/dist/server/actions/getMarkdown.d.ts +0 -2
  117. package/dist/server/collections/ai-build-guide-pages.d.ts +0 -2
  118. package/dist/server/collections/ai-build-guide-spaces.d.ts +0 -2
  119. package/dist/server/index.d.ts +0 -2
  120. package/dist/server/plugin.d.ts +0 -16
  121. package/dist/server/tools/index.d.ts +0 -1
  122. package/dist/server/tools/search-guides.d.ts +0 -28
  123. /package/{dist/client/index.d.ts → src/client-v2/index.tsx} +0 -0
@@ -36,7 +36,11 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
36
36
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
37
37
  var build_exports = {};
38
38
  __export(build_exports, {
39
- build: () => build
39
+ WORKER_JOB_BUILD_GUIDE_PROCESS: () => WORKER_JOB_BUILD_GUIDE_PROCESS,
40
+ build: () => build,
41
+ recoverInterruptedBuilds: () => recoverInterruptedBuilds,
42
+ registerBuildGuideQueue: () => registerBuildGuideQueue,
43
+ unregisterBuildGuideQueue: () => unregisterBuildGuideQueue
40
44
  });
41
45
  module.exports = __toCommonJS(build_exports);
42
46
  var import_sanitize_html = __toESM(require("sanitize-html"));
@@ -50,6 +54,66 @@ const MAX_SOURCE_CHARS = 9e4;
50
54
  const MIN_CHAPTERS = 1;
51
55
  const MAX_CHAPTERS = 12;
52
56
  const DEFAULT_TARGET_CHAPTERS = 5;
57
+ const WORKER_JOB_BUILD_GUIDE_PROCESS = "build-guide:process";
58
+ const BUILD_GUIDE_QUEUE_CHANNEL = "plugin-build-guide-block.build";
59
+ const BUILD_GUIDE_QUEUE_CONCURRENCY = Math.max(
60
+ 1,
61
+ Number.parseInt(process.env.BUILD_GUIDE_QUEUE_CONCURRENCY || process.env.BUILD_GUIDE_MAX_CONCURRENCY || "1", 10) || 1
62
+ );
63
+ const BUILD_GUIDE_QUEUE_TIMEOUT_MS = Math.max(
64
+ 6e4,
65
+ Number.parseInt(process.env.BUILD_GUIDE_QUEUE_TIMEOUT_MS || "", 10) || 30 * 60 * 1e3
66
+ );
67
+ const BUILD_GUIDE_QUEUE_POLL_INTERVAL_MS = Math.max(
68
+ 1e3,
69
+ Number.parseInt(process.env.BUILD_GUIDE_QUEUE_POLL_INTERVAL_MS || "", 10) || 5e3
70
+ );
71
+ const BUILD_GUIDE_QUEUE_WAKE_CHANNEL = "plugin-build-guide-block.build.wake";
72
+ const BUILD_GUIDE_QUEUE_REDIS_CONNECTION = "plugin-build-guide-block.build.queue";
73
+ const BUILD_TRIGGER_LOCK_TTL_MS = 3e4;
74
+ const BUILD_RUN_LOCK_TTL_MS = Math.max(
75
+ 6e4,
76
+ Number.parseInt(process.env.BUILD_GUIDE_RUN_LOCK_TTL_MS || "", 10) || 24 * 60 * 60 * 1e3
77
+ );
78
+ const BUILD_HEARTBEAT_INTERVAL_MS = Math.max(
79
+ 5e3,
80
+ Number.parseInt(process.env.BUILD_GUIDE_HEARTBEAT_MS || "", 10) || 3e4
81
+ );
82
+ const BUILD_STALE_MS = Math.max(
83
+ BUILD_HEARTBEAT_INTERVAL_MS * 2,
84
+ Number.parseInt(process.env.BUILD_GUIDE_STALE_MS || "", 10) || 12e4
85
+ );
86
+ const TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
87
+ ".txt",
88
+ ".md",
89
+ ".markdown",
90
+ ".csv",
91
+ ".tsv",
92
+ ".json",
93
+ ".xml",
94
+ ".html",
95
+ ".htm",
96
+ ".yaml",
97
+ ".yml",
98
+ ".log",
99
+ ".sql",
100
+ ".js",
101
+ ".jsx",
102
+ ".ts",
103
+ ".tsx",
104
+ ".css",
105
+ ".scss",
106
+ ".less"
107
+ ]);
108
+ const TEXT_MIMETYPES = /* @__PURE__ */ new Set([
109
+ "application/json",
110
+ "application/xml",
111
+ "application/yaml",
112
+ "application/x-yaml",
113
+ "application/javascript",
114
+ "application/typescript",
115
+ "image/svg+xml"
116
+ ]);
53
117
  const SANITIZE_OPTIONS = {
54
118
  allowedTags: [
55
119
  "div",
@@ -94,12 +158,93 @@ const SANITIZE_OPTIONS = {
94
158
  }
95
159
  }
96
160
  };
161
+ let buildQueueTimer = null;
162
+ let buildQueueKickTimer = null;
163
+ let buildQueueProcessing = false;
164
+ let buildQueueWakeHandler = null;
165
+ class StaleBuildRunError extends Error {
166
+ constructor(spaceId, runId) {
167
+ super(`Build run ${runId} for space ${spaceId} is no longer current`);
168
+ this.name = "StaleBuildRunError";
169
+ }
170
+ }
97
171
  function clampChapterCount(value) {
98
172
  const count = Number(value);
99
173
  if (!Number.isFinite(count)) return DEFAULT_TARGET_CHAPTERS;
100
174
  return Math.max(MIN_CHAPTERS, Math.min(MAX_CHAPTERS, Math.round(count)));
101
175
  }
102
- async function fetchFileContent(app, file) {
176
+ function resolveExtname(file) {
177
+ const explicit = file == null ? void 0 : file.extname;
178
+ if (typeof explicit === "string" && explicit) return explicit.toLowerCase();
179
+ const name = (file == null ? void 0 : file.filename) || (file == null ? void 0 : file.name) || "";
180
+ const index = String(name).lastIndexOf(".");
181
+ return index >= 0 ? String(name).slice(index).toLowerCase() : "";
182
+ }
183
+ function isTextDocument(file) {
184
+ const mimetype = String((file == null ? void 0 : file.mimetype) || "").toLowerCase();
185
+ if (mimetype.startsWith("text/")) return true;
186
+ if (TEXT_MIMETYPES.has(mimetype)) return true;
187
+ return TEXT_EXTENSIONS.has(resolveExtname(file));
188
+ }
189
+ function createParserContext(app) {
190
+ const headers = { "x-timezone": "+00:00", "x-locale": "en-US" };
191
+ return {
192
+ app,
193
+ db: app.db,
194
+ log: app.log || app.logger || console,
195
+ logger: app.logger || app.log || console,
196
+ state: {},
197
+ auth: {},
198
+ req: { headers },
199
+ request: { headers },
200
+ get(name) {
201
+ return headers[String(name).toLowerCase()] || "";
202
+ },
203
+ getCurrentLocale() {
204
+ return "en-US";
205
+ },
206
+ t(key) {
207
+ return key;
208
+ },
209
+ i18n: {
210
+ t(key) {
211
+ return key;
212
+ }
213
+ }
214
+ };
215
+ }
216
+ function extractParsedText(value) {
217
+ if (!value) return "";
218
+ if (typeof value === "string") return value;
219
+ if (Array.isArray(value)) {
220
+ return value.map(extractParsedText).filter(Boolean).join("\n");
221
+ }
222
+ if (typeof value === "object") {
223
+ if (typeof value.text === "string") return value.text;
224
+ if (typeof value.content === "string") return value.content;
225
+ if (value.content) return extractParsedText(value.content);
226
+ if (value.message) return extractParsedText(value.message);
227
+ }
228
+ return "";
229
+ }
230
+ function getDocumentParserPlugin(app) {
231
+ var _a, _b, _c, _d;
232
+ return ((_b = (_a = app.pm) == null ? void 0 : _a.get) == null ? void 0 : _b.call(_a, "@nocobase/plugin-document-parser")) || ((_d = (_c = app.pm) == null ? void 0 : _c.get) == null ? void 0 : _d.call(_c, "plugin-document-parser")) || null;
233
+ }
234
+ function unsupportedDocumentMessage(file) {
235
+ const filename = (file == null ? void 0 : file.filename) || (file == null ? void 0 : file.name) || (file == null ? void 0 : file.id) || "document";
236
+ const type = (file == null ? void 0 : file.mimetype) || resolveExtname(file) || "unknown type";
237
+ return `[Unsupported document type: ${filename} (${type}). Install or enable plugin-document-parser/MarkItDown to extract this file.]`;
238
+ }
239
+ async function fetchTextFileContent(app, file) {
240
+ if (!isTextDocument(file)) {
241
+ return unsupportedDocumentMessage(file);
242
+ }
243
+ const docParserPlugin = getDocumentParserPlugin(app);
244
+ if (docParserPlugin == null ? void 0 : docParserPlugin.fetchFileBuffer) {
245
+ const { buffer } = await docParserPlugin.fetchFileBuffer(createParserContext(app), file);
246
+ return buffer.toString("utf8");
247
+ }
103
248
  const fileManager = app.pm.get("file-manager");
104
249
  if (!fileManager) return "";
105
250
  const url = await fileManager.getFileURL(file);
@@ -119,6 +264,42 @@ async function fetchFileContent(app, file) {
119
264
  return `[Failed to read document: ${file.filename}]`;
120
265
  }
121
266
  }
267
+ async function parseWithDocumentParser(app, file) {
268
+ var _a, _b, _c, _d, _e;
269
+ const docParserPlugin = getDocumentParserPlugin(app);
270
+ if (!docParserPlugin) return "";
271
+ const parserCtx = createParserContext(app);
272
+ const defaultParser = async () => ({
273
+ placement: "contentBlocks",
274
+ content: {
275
+ type: "text",
276
+ text: await fetchTextFileContent(app, file)
277
+ }
278
+ });
279
+ try {
280
+ if ((_a = docParserPlugin.parseRouter) == null ? void 0 : _a.route) {
281
+ const result = await docParserPlugin.parseRouter.route(parserCtx, file, defaultParser);
282
+ const text = extractParsedText(result == null ? void 0 : result.content);
283
+ if (text && !text.startsWith("[Unsupported document type:")) {
284
+ return text;
285
+ }
286
+ }
287
+ if ((_b = docParserPlugin.internalParserRegistry) == null ? void 0 : _b.parse) {
288
+ const result = await docParserPlugin.internalParserRegistry.parse(file, parserCtx);
289
+ if ((result == null ? void 0 : result.handled) && ((_c = result == null ? void 0 : result.text) == null ? void 0 : _c.trim())) {
290
+ return result.text;
291
+ }
292
+ }
293
+ } catch (err) {
294
+ (_e = (_d = app.log) == null ? void 0 : _d.warn) == null ? void 0 : _e.call(_d, `[plugin-build-guide-block] Document parser failed for ${(file == null ? void 0 : file.filename) || (file == null ? void 0 : file.id)}`, err);
295
+ }
296
+ return "";
297
+ }
298
+ async function fetchFileContent(app, file) {
299
+ const parsedText = await parseWithDocumentParser(app, file);
300
+ if (parsedText) return parsedText;
301
+ return fetchTextFileContent(app, file);
302
+ }
122
303
  function toPlainText(value) {
123
304
  if (typeof value === "string") return value;
124
305
  if (Array.isArray(value)) {
@@ -135,6 +316,9 @@ function toPlainText(value) {
135
316
  }
136
317
  return JSON.stringify(value);
137
318
  }
319
+ function stripThink(text) {
320
+ return text.replace(/<think>[\s\S]*?(?:<\/think>|$)/gi, "").trim();
321
+ }
138
322
  function stripFence(text) {
139
323
  return text.replace(/^```(?:json|markdown|md|html)?\s*/i, "").replace(/```\s*$/i, "").trim();
140
324
  }
@@ -193,7 +377,7 @@ function createFallbackPlan(guideTitle, targetChapterCount) {
193
377
  }
194
378
  function normalizePlan(rawText, guideTitle, targetChapterCount) {
195
379
  const targetCount = clampChapterCount(targetChapterCount);
196
- const cleanText = stripFence(rawText);
380
+ const cleanText = stripFence(stripThink(rawText));
197
381
  const jsonStart = cleanText.indexOf("{");
198
382
  const jsonEnd = cleanText.lastIndexOf("}");
199
383
  const jsonText = jsonStart >= 0 && jsonEnd > jsonStart ? cleanText.slice(jsonStart, jsonEnd + 1) : cleanText;
@@ -298,7 +482,7 @@ Source documents:
298
482
  ${documentsText.slice(0, MAX_SOURCE_CHARS)}`)
299
483
  );
300
484
  const response = await provider.chatModel.invoke(messages);
301
- return stripFence(toPlainText(response.content));
485
+ return stripFence(stripThink(toPlainText(response.content)));
302
486
  }
303
487
  async function markdownToCleanHtml(markdown) {
304
488
  const renderedHtml = await import_marked.marked.parse(markdown, { async: true });
@@ -319,59 +503,114 @@ ${content}
319
503
  );
320
504
  return texts.join("\n");
321
505
  }
322
- async function runBuild(app, db, filterByTk) {
506
+ function getSpaceModel(app) {
507
+ return app.db.getModel("aiBuildGuideSpaces");
508
+ }
509
+ async function updateSpaceForRun(app, run, values, optional = false) {
510
+ const SpaceModel = getSpaceModel(app);
511
+ const [affected] = await SpaceModel.update(values, {
512
+ where: {
513
+ id: run.spaceId,
514
+ buildRunId: run.runId
515
+ }
516
+ });
517
+ if (!affected && !optional) {
518
+ throw new StaleBuildRunError(run.spaceId, run.runId);
519
+ }
520
+ return affected > 0;
521
+ }
522
+ async function claimBuildRun(app, run, workerId) {
523
+ const now = /* @__PURE__ */ new Date();
524
+ const SpaceModel = getSpaceModel(app);
525
+ const [affected] = await SpaceModel.update(
526
+ {
527
+ buildPhase: "running",
528
+ buildStartedAt: now,
529
+ buildHeartbeatAt: now,
530
+ buildWorkerId: workerId
531
+ },
532
+ {
533
+ where: {
534
+ id: run.spaceId,
535
+ status: "building",
536
+ buildPhase: "queued",
537
+ buildRunId: run.runId
538
+ }
539
+ }
540
+ );
541
+ return affected > 0;
542
+ }
543
+ function getBuildWorkerId(app) {
544
+ return [
545
+ process.env.HOSTNAME || process.env.COMPUTERNAME || "worker",
546
+ app.name || "app",
547
+ app.instanceId || "0",
548
+ process.pid
549
+ ].join(":");
550
+ }
551
+ function startBuildHeartbeat(app, run) {
552
+ const timer = setInterval(() => {
553
+ updateSpaceForRun(
554
+ app,
555
+ run,
556
+ {
557
+ buildHeartbeatAt: /* @__PURE__ */ new Date()
558
+ },
559
+ true
560
+ ).catch((error) => {
561
+ var _a, _b;
562
+ (_b = (_a = app.log) == null ? void 0 : _a.warn) == null ? void 0 : _b.call(_a, `[plugin-build-guide-block] Failed to update heartbeat for build ${run.runId}`, error);
563
+ });
564
+ }, BUILD_HEARTBEAT_INTERVAL_MS);
565
+ return () => clearInterval(timer);
566
+ }
567
+ async function runBuild(app, db, run) {
323
568
  const spaceRepo = db.getRepository("aiBuildGuideSpaces");
324
569
  const pageRepo = db.getRepository("aiBuildGuidePages");
325
- const space = await spaceRepo.findById(filterByTk);
570
+ const space = await spaceRepo.findById(run.spaceId);
326
571
  if (!space) {
327
572
  throw new Error("Space not found");
328
573
  }
574
+ if (space.get("buildRunId") !== run.runId) {
575
+ throw new StaleBuildRunError(run.spaceId, run.runId);
576
+ }
329
577
  const { llmService, model } = space.get();
330
578
  if (!llmService || !model) {
331
579
  throw new Error("LLM Service or model is missing in space configuration");
332
580
  }
333
581
  await pageRepo.destroy({
334
582
  filter: {
335
- spaceId: filterByTk
583
+ spaceId: run.spaceId
336
584
  }
337
585
  });
338
- await spaceRepo.update({
339
- filterByTk,
340
- values: {
341
- buildPhase: "reading",
342
- buildLog: "Reading source documents",
343
- generatedHtml: null,
344
- generatedMarkdown: null,
345
- planJson: null,
346
- pageCount: 0
347
- }
586
+ await updateSpaceForRun(app, run, {
587
+ buildPhase: "reading",
588
+ buildLog: "Reading source documents",
589
+ generatedHtml: null,
590
+ generatedMarkdown: null,
591
+ planJson: null,
592
+ pageCount: 0
348
593
  });
349
594
  const documentsText = await readDocuments(app, space);
350
595
  const sourceHash = import_crypto.default.createHash("sha256").update(documentsText).digest("hex");
351
596
  const provider = await getLLMProvider(app, llmService, model);
352
- await spaceRepo.update({
353
- filterByTk,
354
- values: {
355
- buildPhase: "planning",
356
- buildLog: "Creating guide breakdown plan",
357
- sourceHash
358
- }
597
+ await updateSpaceForRun(app, run, {
598
+ buildPhase: "planning",
599
+ buildLog: "Creating guide breakdown plan",
600
+ sourceHash
359
601
  });
360
602
  const plan = await buildPlan(provider, space, documentsText);
361
- await spaceRepo.update({
362
- filterByTk,
363
- values: {
364
- planJson: plan,
365
- pageCount: plan.chapters.length,
366
- buildPhase: "building_pages",
367
- buildLog: `Plan created with ${plan.chapters.length} chapters`
368
- }
603
+ await updateSpaceForRun(app, run, {
604
+ planJson: plan,
605
+ pageCount: plan.chapters.length,
606
+ buildPhase: "building_pages",
607
+ buildLog: `Plan created with ${plan.chapters.length} chapters`
369
608
  });
370
609
  const pageRecords = [];
371
610
  for (const [index, chapter] of plan.chapters.entries()) {
372
611
  const page = await pageRepo.create({
373
612
  values: {
374
- spaceId: filterByTk,
613
+ spaceId: run.spaceId,
375
614
  sort: index + 1,
376
615
  title: chapter.title,
377
616
  slug: slugify(chapter.title, `chapter-${index + 1}`),
@@ -392,12 +631,9 @@ async function runBuild(app, db, filterByTk) {
392
631
  buildLog: "Building chapter with LLM"
393
632
  }
394
633
  });
395
- await spaceRepo.update({
396
- filterByTk,
397
- values: {
398
- buildPhase: "building_pages",
399
- buildLog: `Building chapter ${index + 1}/${pageRecords.length}: ${chapter.title}`
400
- }
634
+ await updateSpaceForRun(app, run, {
635
+ buildPhase: "building_pages",
636
+ buildLog: `Building chapter ${index + 1}/${pageRecords.length}: ${chapter.title}`
401
637
  });
402
638
  try {
403
639
  const markdown = await buildPageMarkdown(provider, space, plan, chapter, documentsText);
@@ -424,76 +660,442 @@ async function runBuild(app, db, filterByTk) {
424
660
  }
425
661
  const completedPages = await pageRepo.find({
426
662
  filter: {
427
- spaceId: filterByTk,
663
+ spaceId: run.spaceId,
428
664
  status: "completed"
429
665
  },
430
666
  sort: ["sort"]
431
667
  });
432
668
  const combinedMarkdown = completedPages.map((page) => page.get("generatedMarkdown")).filter(Boolean).join("\n\n---\n\n");
433
669
  const combinedHtml = await markdownToCleanHtml(combinedMarkdown);
434
- await spaceRepo.update({
435
- filterByTk,
670
+ await updateSpaceForRun(app, run, {
671
+ status: "completed",
672
+ buildPhase: "completed",
673
+ buildLog: `Built ${completedPages.length} chapters successfully`,
674
+ generatedMarkdown: combinedMarkdown,
675
+ generatedHtml: combinedHtml,
676
+ buildHeartbeatAt: /* @__PURE__ */ new Date()
677
+ });
678
+ }
679
+ function isBuildGuideWorker(app) {
680
+ const workerMode = process.env.WORKER_MODE || "";
681
+ return app.serving(WORKER_JOB_BUILD_GUIDE_PROCESS) || workerMode === "worker" || workerMode === "task" || process.env.APP_ROLE === "worker";
682
+ }
683
+ function clearLocalBuildMemoryQueue(app) {
684
+ var _a, _b, _c, _d, _e;
685
+ const eventQueue = app.eventQueue;
686
+ const adapter = eventQueue == null ? void 0 : eventQueue.adapter;
687
+ const fullChannel = (_a = eventQueue == null ? void 0 : eventQueue.getFullChannel) == null ? void 0 : _a.call(eventQueue, BUILD_GUIDE_QUEUE_CHANNEL);
688
+ const queue = fullChannel ? (_c = (_b = adapter == null ? void 0 : adapter.queues) == null ? void 0 : _b.get) == null ? void 0 : _c.call(_b, fullChannel) : null;
689
+ if (!(queue == null ? void 0 : queue.length)) return;
690
+ adapter.queues.set(fullChannel, []);
691
+ (_e = (_d = app.log) == null ? void 0 : _d.warn) == null ? void 0 : _e.call(
692
+ _d,
693
+ `[plugin-build-guide-block] Cleared ${queue.length} stale local memory message(s) on non-worker node; queued DB builds will be picked up by workers`
694
+ );
695
+ }
696
+ function getBuildQueueRedisKey(app) {
697
+ const appName = app.name || process.env.APP_NAME || "main";
698
+ return `${appName}:plugin-build-guide-block:build:queue`;
699
+ }
700
+ async function getBuildQueueRedis(app) {
701
+ var _a, _b;
702
+ const manager = app.redisConnectionManager;
703
+ if (!(manager == null ? void 0 : manager.getConnectionSync)) {
704
+ return null;
705
+ }
706
+ try {
707
+ const connectionString = process.env.QUEUE_ADAPTER_REDIS_URL || process.env.REDIS_URL;
708
+ return await manager.getConnectionSync(
709
+ BUILD_GUIDE_QUEUE_REDIS_CONNECTION,
710
+ connectionString ? { connectionString } : void 0
711
+ );
712
+ } catch (error) {
713
+ (_b = (_a = app.log) == null ? void 0 : _a.debug) == null ? void 0 : _b.call(
714
+ _a,
715
+ `[plugin-build-guide-block] Redis queue unavailable; DB polling fallback active: ${(error == null ? void 0 : error.message) || error}`
716
+ );
717
+ return null;
718
+ }
719
+ }
720
+ async function enqueueBuildToRedis(app, message) {
721
+ var _a, _b, _c, _d;
722
+ const redis = await getBuildQueueRedis(app);
723
+ if (!redis) return false;
724
+ try {
725
+ await redis.sendCommand(["RPUSH", getBuildQueueRedisKey(app), JSON.stringify(message)]);
726
+ (_b = (_a = app.log) == null ? void 0 : _a.debug) == null ? void 0 : _b.call(
727
+ _a,
728
+ `[plugin-build-guide-block] Enqueued build ${message.runId} for space "${message.spaceId}" to Redis`
729
+ );
730
+ return true;
731
+ } catch (error) {
732
+ (_d = (_c = app.log) == null ? void 0 : _c.warn) == null ? void 0 : _d.call(_c, `[plugin-build-guide-block] Failed to enqueue build to Redis; DB polling fallback active`, error);
733
+ return false;
734
+ }
735
+ }
736
+ async function publishBuildQueueWake(app, message) {
737
+ var _a, _b, _c, _d;
738
+ try {
739
+ await ((_b = (_a = app.pubSubManager) == null ? void 0 : _a.publish) == null ? void 0 : _b.call(
740
+ _a,
741
+ BUILD_GUIDE_QUEUE_WAKE_CHANNEL,
742
+ { spaceId: message == null ? void 0 : message.spaceId, runId: message == null ? void 0 : message.runId },
743
+ { skipSelf: !isBuildGuideWorker(app) }
744
+ ));
745
+ } catch (error) {
746
+ (_d = (_c = app.log) == null ? void 0 : _c.debug) == null ? void 0 : _d.call(_c, `[plugin-build-guide-block] Wake publish skipped: ${(error == null ? void 0 : error.message) || error}`);
747
+ }
748
+ }
749
+ function startBuildGuideQueueProcessor(app) {
750
+ var _a, _b, _c, _d, _e, _f, _g;
751
+ if (!isBuildGuideWorker(app)) {
752
+ (_b = (_a = app.log) == null ? void 0 : _a.debug) == null ? void 0 : _b.call(_a, "[plugin-build-guide-block] Build queue processor disabled on non-worker node");
753
+ return;
754
+ }
755
+ if (buildQueueTimer) return;
756
+ buildQueueWakeHandler = async () => {
757
+ scheduleBuildQueueTick(app, 0);
758
+ };
759
+ const subscribe = (_d = (_c = app.pubSubManager) == null ? void 0 : _c.subscribe) == null ? void 0 : _d.call(_c, BUILD_GUIDE_QUEUE_WAKE_CHANNEL, buildQueueWakeHandler);
760
+ if (subscribe == null ? void 0 : subscribe.catch) {
761
+ subscribe.catch((error) => {
762
+ var _a2, _b2;
763
+ (_b2 = (_a2 = app.log) == null ? void 0 : _a2.debug) == null ? void 0 : _b2.call(_a2, `[plugin-build-guide-block] Wake subscribe skipped: ${(error == null ? void 0 : error.message) || error}`);
764
+ });
765
+ }
766
+ buildQueueTimer = setInterval(() => scheduleBuildQueueTick(app, 0), BUILD_GUIDE_QUEUE_POLL_INTERVAL_MS);
767
+ (_e = buildQueueTimer.unref) == null ? void 0 : _e.call(buildQueueTimer);
768
+ scheduleBuildQueueTick(app, 1e3);
769
+ (_g = (_f = app.log) == null ? void 0 : _f.info) == null ? void 0 : _g.call(
770
+ _f,
771
+ `[plugin-build-guide-block] Build queue processor started (interval ${BUILD_GUIDE_QUEUE_POLL_INTERVAL_MS}ms)`
772
+ );
773
+ }
774
+ function stopBuildGuideQueueProcessor(app) {
775
+ var _a, _b;
776
+ if (buildQueueTimer) {
777
+ clearInterval(buildQueueTimer);
778
+ buildQueueTimer = null;
779
+ }
780
+ if (buildQueueKickTimer) {
781
+ clearTimeout(buildQueueKickTimer);
782
+ buildQueueKickTimer = null;
783
+ }
784
+ if (buildQueueWakeHandler) {
785
+ const unsubscribe = (_b = (_a = app.pubSubManager) == null ? void 0 : _a.unsubscribe) == null ? void 0 : _b.call(
786
+ _a,
787
+ BUILD_GUIDE_QUEUE_WAKE_CHANNEL,
788
+ buildQueueWakeHandler
789
+ );
790
+ if (unsubscribe == null ? void 0 : unsubscribe.catch) {
791
+ unsubscribe.catch(() => void 0);
792
+ }
793
+ buildQueueWakeHandler = null;
794
+ }
795
+ buildQueueProcessing = false;
796
+ }
797
+ function scheduleBuildQueueTick(app, delayMs) {
798
+ var _a;
799
+ if (buildQueueKickTimer) return;
800
+ buildQueueKickTimer = setTimeout(() => {
801
+ buildQueueKickTimer = null;
802
+ runBuildQueueTick(app).catch((error) => {
803
+ var _a2, _b;
804
+ (_b = (_a2 = app.log) == null ? void 0 : _a2.error) == null ? void 0 : _b.call(_a2, "[plugin-build-guide-block] Build queue tick failed", error);
805
+ });
806
+ }, delayMs);
807
+ (_a = buildQueueKickTimer.unref) == null ? void 0 : _a.call(buildQueueKickTimer);
808
+ }
809
+ async function runBuildQueueTick(app) {
810
+ if (buildQueueProcessing || !isBuildGuideWorker(app)) return;
811
+ buildQueueProcessing = true;
812
+ try {
813
+ const redisMessages = await drainRedisBuildQueue(app, BUILD_GUIDE_QUEUE_CONCURRENCY);
814
+ await processBuildQueueMessages(app, redisMessages);
815
+ const remaining = Math.max(1, BUILD_GUIDE_QUEUE_CONCURRENCY - redisMessages.length);
816
+ await processQueuedBuildsFromDb(app, remaining);
817
+ } finally {
818
+ buildQueueProcessing = false;
819
+ }
820
+ }
821
+ async function drainRedisBuildQueue(app, count) {
822
+ var _a, _b;
823
+ const redis = await getBuildQueueRedis(app);
824
+ if (!redis) return [];
825
+ const key = getBuildQueueRedisKey(app);
826
+ const messages = [];
827
+ for (let i = 0; i < count; i += 1) {
828
+ const raw = await redis.sendCommand(["LPOP", key]);
829
+ if (!raw) break;
830
+ try {
831
+ messages.push(JSON.parse(String(raw)));
832
+ } catch (error) {
833
+ (_b = (_a = app.log) == null ? void 0 : _a.warn) == null ? void 0 : _b.call(_a, `[plugin-build-guide-block] Dropped invalid Redis build message: ${(error == null ? void 0 : error.message) || error}`);
834
+ }
835
+ }
836
+ return messages;
837
+ }
838
+ function createBuildQueueMessageFromSpace(space) {
839
+ const runId = space.get("buildRunId");
840
+ if (!runId) return null;
841
+ return {
842
+ spaceId: String(space.get("id")),
843
+ runId: String(runId),
844
+ queuedAt: space.get("buildQueuedAt") ? new Date(space.get("buildQueuedAt")).toISOString() : void 0
845
+ };
846
+ }
847
+ async function processQueuedBuildsFromDb(app, count) {
848
+ const spaceRepo = app.db.getRepository("aiBuildGuideSpaces");
849
+ const spaces = await spaceRepo.find({
850
+ filter: {
851
+ status: "building",
852
+ buildPhase: "queued"
853
+ },
854
+ sort: ["buildQueuedAt"],
855
+ limit: count
856
+ });
857
+ const messages = spaces.map(createBuildQueueMessageFromSpace).filter(Boolean);
858
+ await processBuildQueueMessages(app, messages);
859
+ }
860
+ async function processBuildQueueMessages(app, messages) {
861
+ if (!messages.length) return;
862
+ await Promise.all(messages.map((message) => processQueuedBuild(app, message)));
863
+ }
864
+ async function markBuildError(app, spaceId, runId, error) {
865
+ const buildLog = (error == null ? void 0 : error.message) || String(error);
866
+ let updated = false;
867
+ if (runId) {
868
+ updated = await updateSpaceForRun(
869
+ app,
870
+ { spaceId, runId },
871
+ {
872
+ status: "error",
873
+ buildPhase: "error",
874
+ buildLog,
875
+ buildHeartbeatAt: /* @__PURE__ */ new Date()
876
+ },
877
+ true
878
+ );
879
+ } else {
880
+ await app.db.getRepository("aiBuildGuideSpaces").update({
881
+ filterByTk: spaceId,
882
+ values: {
883
+ status: "error",
884
+ buildPhase: "error",
885
+ buildLog
886
+ }
887
+ });
888
+ updated = true;
889
+ }
890
+ if (!updated) {
891
+ return;
892
+ }
893
+ await app.db.getRepository("aiBuildGuidePages").update({
894
+ filter: {
895
+ spaceId,
896
+ status: "building"
897
+ },
436
898
  values: {
437
- status: "completed",
438
- buildPhase: "completed",
439
- buildLog: `Built ${completedPages.length} chapters successfully`,
440
- generatedMarkdown: combinedMarkdown,
441
- generatedHtml: combinedHtml
899
+ status: "error",
900
+ buildLog
901
+ }
902
+ });
903
+ }
904
+ async function enqueueBuild(app, message) {
905
+ var _a, _b;
906
+ try {
907
+ const queuedInRedis = await enqueueBuildToRedis(app, message);
908
+ if (queuedInRedis) {
909
+ await publishBuildQueueWake(app, message);
910
+ return;
911
+ }
912
+ await publishBuildQueueWake(app, message);
913
+ if (isBuildGuideWorker(app)) {
914
+ await app.eventQueue.publish(BUILD_GUIDE_QUEUE_CHANNEL, message, {
915
+ timeout: BUILD_GUIDE_QUEUE_TIMEOUT_MS,
916
+ maxRetries: 0
917
+ });
918
+ return;
919
+ }
920
+ (_b = (_a = app.log) == null ? void 0 : _a.warn) == null ? void 0 : _b.call(
921
+ _a,
922
+ `[plugin-build-guide-block] Redis queue is unavailable; build ${message.runId} for space "${message.spaceId}" will remain queued until a worker DB poller picks it up`
923
+ );
924
+ } catch (error) {
925
+ await markBuildError(app, message.spaceId, message.runId, error);
926
+ throw error;
927
+ }
928
+ }
929
+ async function processQueuedBuild(app, message) {
930
+ var _a, _b;
931
+ const spaceId = message == null ? void 0 : message.spaceId;
932
+ const runId = message == null ? void 0 : message.runId;
933
+ if (!spaceId || !runId) {
934
+ (_b = (_a = app.log) == null ? void 0 : _a.warn) == null ? void 0 : _b.call(_a, "[plugin-build-guide-block] Build queue message missing spaceId or runId");
935
+ return;
936
+ }
937
+ await withBuildRunLock(app, spaceId, async () => {
938
+ var _a2, _b2, _c, _d, _e, _f, _g, _h, _i, _j;
939
+ const run = { spaceId, runId };
940
+ const workerId = getBuildWorkerId(app);
941
+ const claimed = await claimBuildRun(app, run, workerId);
942
+ if (!claimed) {
943
+ (_b2 = (_a2 = app.log) == null ? void 0 : _a2.info) == null ? void 0 : _b2.call(_a2, `[plugin-build-guide-block] Build ${runId} for space "${spaceId}" was already claimed or stale`);
944
+ return;
945
+ }
946
+ const spaceRepo = app.db.getRepository("aiBuildGuideSpaces");
947
+ const space = await spaceRepo.findById(spaceId);
948
+ if (!space) {
949
+ (_d = (_c = app.log) == null ? void 0 : _c.warn) == null ? void 0 : _d.call(_c, `[plugin-build-guide-block] Build space "${spaceId}" not found; skipping queued build`);
950
+ return;
951
+ }
952
+ if (space.get("status") !== "building") {
953
+ (_f = (_e = app.log) == null ? void 0 : _e.info) == null ? void 0 : _f.call(
954
+ _e,
955
+ `[plugin-build-guide-block] Build space "${spaceId}" is ${space.get("status")}; skipping queued build`
956
+ );
957
+ return;
958
+ }
959
+ const stopHeartbeat = startBuildHeartbeat(app, run);
960
+ try {
961
+ await runBuild(app, app.db, run);
962
+ } catch (error) {
963
+ if (error instanceof StaleBuildRunError) {
964
+ (_h = (_g = app.log) == null ? void 0 : _g.info) == null ? void 0 : _h.call(_g, `[plugin-build-guide-block] ${error.message}`);
965
+ return;
966
+ }
967
+ (_j = (_i = app.log) == null ? void 0 : _i.error) == null ? void 0 : _j.call(_i, "Build Guide Worker Error", error);
968
+ await markBuildError(app, spaceId, runId, error);
969
+ } finally {
970
+ stopHeartbeat();
971
+ }
972
+ });
973
+ }
974
+ function registerBuildGuideQueue(app) {
975
+ app.eventQueue.subscribe(BUILD_GUIDE_QUEUE_CHANNEL, {
976
+ concurrency: BUILD_GUIDE_QUEUE_CONCURRENCY,
977
+ idle: () => isBuildGuideWorker(app),
978
+ process: async (message) => {
979
+ await processQueuedBuild(app, message);
442
980
  }
443
981
  });
982
+ if (!isBuildGuideWorker(app)) {
983
+ app.on("afterStart", () => clearLocalBuildMemoryQueue(app));
984
+ }
985
+ startBuildGuideQueueProcessor(app);
986
+ }
987
+ function unregisterBuildGuideQueue(app) {
988
+ app.eventQueue.unsubscribe(BUILD_GUIDE_QUEUE_CHANNEL);
989
+ stopBuildGuideQueueProcessor(app);
990
+ }
991
+ async function withBuildTriggerLock(app, spaceId, fn) {
992
+ return app.lockManager.runExclusive(`build-guide:trigger:${spaceId}`, fn, BUILD_TRIGGER_LOCK_TTL_MS);
993
+ }
994
+ async function withBuildRunLock(app, spaceId, fn) {
995
+ return app.lockManager.runExclusive(`build-guide:run:${spaceId}`, fn, BUILD_RUN_LOCK_TTL_MS);
996
+ }
997
+ async function recoverInterruptedBuilds(app) {
998
+ var _a, _b;
999
+ const spaceRepo = app.db.getRepository("aiBuildGuideSpaces");
1000
+ const pageRepo = app.db.getRepository("aiBuildGuidePages");
1001
+ const staleBefore = new Date(Date.now() - BUILD_STALE_MS);
1002
+ const spaces = await spaceRepo.find({
1003
+ filter: {
1004
+ status: "building",
1005
+ $or: [{ buildHeartbeatAt: null }, { buildHeartbeatAt: { $lt: staleBefore } }]
1006
+ }
1007
+ });
1008
+ for (const space of spaces) {
1009
+ const spaceId = String(space.get("id"));
1010
+ const runId = String(space.get("buildRunId") || import_crypto.default.randomUUID());
1011
+ const SpaceModel = getSpaceModel(app);
1012
+ const [affected] = await SpaceModel.update(
1013
+ {
1014
+ buildPhase: "queued",
1015
+ buildLog: "Build re-queued after worker restart",
1016
+ buildRunId: runId,
1017
+ buildQueuedAt: /* @__PURE__ */ new Date(),
1018
+ buildStartedAt: null,
1019
+ buildHeartbeatAt: null,
1020
+ buildWorkerId: null
1021
+ },
1022
+ {
1023
+ where: {
1024
+ id: spaceId,
1025
+ status: "building",
1026
+ buildRunId: space.get("buildRunId") || null
1027
+ }
1028
+ }
1029
+ );
1030
+ if (!affected) {
1031
+ continue;
1032
+ }
1033
+ await pageRepo.update({
1034
+ filter: {
1035
+ spaceId,
1036
+ status: "building"
1037
+ },
1038
+ values: {
1039
+ status: "pending",
1040
+ buildLog: "Build re-queued after worker restart"
1041
+ }
1042
+ });
1043
+ await enqueueBuild(app, {
1044
+ spaceId,
1045
+ runId,
1046
+ queuedAt: (/* @__PURE__ */ new Date()).toISOString()
1047
+ });
1048
+ }
1049
+ if (spaces.length) {
1050
+ (_b = (_a = app.log) == null ? void 0 : _a.info) == null ? void 0 : _b.call(_a, `[plugin-build-guide-block] Re-queued ${spaces.length} interrupted build(s)`);
1051
+ }
444
1052
  }
445
1053
  async function build(ctx, next) {
446
1054
  const { filterByTk } = ctx.action.params;
447
- const repository = ctx.db.getRepository("aiBuildGuideSpaces");
448
- const space = await repository.findById(filterByTk);
449
- if (!space) {
450
- ctx.throw(404, "Space not found");
451
- }
452
- if (space.get("status") === "building") {
453
- ctx.throw(409, "A build is already in progress for this space");
1055
+ if (!filterByTk) {
1056
+ ctx.throw(400, "Space id is required");
454
1057
  }
455
1058
  const app = ctx.app;
456
- const db = ctx.db;
457
- try {
1059
+ const repository = ctx.db.getRepository("aiBuildGuideSpaces");
1060
+ const body = await withBuildTriggerLock(app, String(filterByTk), async () => {
1061
+ var _a, _b;
1062
+ const runId = import_crypto.default.randomUUID();
1063
+ const space = await repository.findById(filterByTk);
1064
+ if (!space) {
1065
+ ctx.throw(404, "Space not found");
1066
+ }
1067
+ if (space.get("status") === "building") {
1068
+ ctx.throw(409, "A build is already in progress for this space");
1069
+ }
458
1070
  await repository.update({
459
1071
  filterByTk,
460
1072
  values: {
461
1073
  status: "building",
462
1074
  buildPhase: "queued",
463
- buildLog: "Build queued"
1075
+ buildLog: "Build queued",
1076
+ buildRunId: runId,
1077
+ buildQueuedAt: /* @__PURE__ */ new Date(),
1078
+ buildStartedAt: null,
1079
+ buildHeartbeatAt: null,
1080
+ buildWorkerId: null
464
1081
  }
465
1082
  });
466
- runBuild(app, db, filterByTk).catch(async (error) => {
467
- app.log.error("Build Guide Background Error", error);
468
- try {
469
- await repository.update({
470
- filterByTk,
471
- values: {
472
- status: "error",
473
- buildPhase: "error",
474
- buildLog: error.message || String(error)
475
- }
476
- });
477
- } catch (updateErr) {
478
- app.log.error("Failed to persist build error status", updateErr);
479
- }
1083
+ await enqueueBuild(app, {
1084
+ spaceId: String(filterByTk),
1085
+ runId,
1086
+ userId: ((_b = (_a = ctx.state) == null ? void 0 : _a.currentUser) == null ? void 0 : _b.id) ?? null,
1087
+ queuedAt: (/* @__PURE__ */ new Date()).toISOString()
480
1088
  });
481
- ctx.body = { status: "building" };
482
- } catch (error) {
483
- app.log.error("Build Guide Error", error);
484
- await repository.update({
485
- filterByTk,
486
- values: {
487
- status: "error",
488
- buildPhase: "error",
489
- buildLog: error.message || String(error)
490
- }
491
- });
492
- ctx.throw(500, error.message || "Error occurred during build");
493
- }
1089
+ return { status: "building" };
1090
+ });
1091
+ ctx.body = body;
494
1092
  await next();
495
1093
  }
496
1094
  // Annotate the CommonJS export names for ESM import in node:
497
1095
  0 && (module.exports = {
498
- build
1096
+ WORKER_JOB_BUILD_GUIDE_PROCESS,
1097
+ build,
1098
+ recoverInterruptedBuilds,
1099
+ registerBuildGuideQueue,
1100
+ unregisterBuildGuideQueue
499
1101
  });