forge-openclaw-plugin 0.2.20 → 0.2.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{board-DGbXWEuu.js → board-_C6oMy5w.js} +2 -2
- package/dist/assets/{board-DGbXWEuu.js.map → board-_C6oMy5w.js.map} +1 -1
- package/dist/assets/index-Ch_xeZ2u.js +63 -0
- package/dist/assets/index-Ch_xeZ2u.js.map +1 -0
- package/dist/assets/index-DvVM7K6j.css +1 -0
- package/dist/assets/{motion-B5Qoz2Ci.js → motion-D4sZgCHd.js} +2 -2
- package/dist/assets/{motion-B5Qoz2Ci.js.map → motion-D4sZgCHd.js.map} +1 -1
- package/dist/assets/{table-D_iurDQu.js → table-BWzTaky1.js} +2 -2
- package/dist/assets/{table-D_iurDQu.js.map → table-BWzTaky1.js.map} +1 -1
- package/dist/assets/{ui-D5QUYUq4.js → ui-BzK4azQb.js} +2 -2
- package/dist/assets/{ui-D5QUYUq4.js.map → ui-BzK4azQb.js.map} +1 -1
- package/dist/assets/vendor-De38P6YR.js +729 -0
- package/dist/assets/vendor-De38P6YR.js.map +1 -0
- package/dist/assets/{viz-BD9WSxHz.js → viz-C6hfyqzu.js} +2 -2
- package/dist/assets/{viz-BD9WSxHz.js.map → viz-C6hfyqzu.js.map} +1 -1
- package/dist/index.html +8 -8
- package/dist/server/app.js +328 -19
- package/dist/server/health.js +82 -21
- package/dist/server/managers/platform/background-job-manager.js +103 -8
- package/dist/server/managers/platform/llm-manager.js +91 -5
- package/dist/server/managers/platform/openai-responses-provider.js +683 -70
- package/dist/server/repositories/diagnostic-logs.js +243 -0
- package/dist/server/repositories/wiki-memory.js +619 -66
- package/dist/server/types.js +56 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server/migrations/023_diagnostic_logs.sql +28 -0
- package/skills/forge-openclaw/SKILL.md +14 -0
- package/dist/assets/index-4-1WI9i7.css +0 -1
- package/dist/assets/index-BZbHajNK.js +0 -63
- package/dist/assets/index-BZbHajNK.js.map +0 -1
- package/dist/assets/vendor-KARp8LAR.js +0 -706
- package/dist/assets/vendor-KARp8LAR.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import AdmZip from "adm-zip";
|
|
3
|
-
import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, unlinkSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { z } from "zod";
|
|
@@ -8,6 +8,8 @@ import { resolveDataDir, getDatabase } from "../db.js";
|
|
|
8
8
|
import { decorateOwnedEntity } from "./entity-ownership.js";
|
|
9
9
|
import { createNoteLinkSchema, crudEntityTypeSchema, noteKindSchema, noteSchema as persistedNoteSchema, wikiSearchModeSchema, wikiSpaceVisibilitySchema } from "../types.js";
|
|
10
10
|
import { deleteEncryptedSecret, readEncryptedSecret, storeEncryptedSecret } from "./calendar.js";
|
|
11
|
+
import { isEntityDeleted } from "./deleted-entities.js";
|
|
12
|
+
import { recordDiagnosticLog } from "./diagnostic-logs.js";
|
|
11
13
|
const wikiSpaceSchema = z.object({
|
|
12
14
|
id: z.string(),
|
|
13
15
|
slug: z.string(),
|
|
@@ -201,13 +203,52 @@ const wikiIngestJobPayloadSchema = z.object({
|
|
|
201
203
|
});
|
|
202
204
|
const listWikiIngestJobsQuerySchema = z.object({
|
|
203
205
|
spaceId: z.string().trim().optional(),
|
|
204
|
-
limit: z.coerce.number().int().positive().max(
|
|
206
|
+
limit: z.coerce.number().int().positive().max(200).default(20)
|
|
205
207
|
});
|
|
206
208
|
export const reviewWikiIngestJobSchema = z.object({
|
|
207
209
|
decisions: z
|
|
208
|
-
.array(z
|
|
210
|
+
.array(z
|
|
211
|
+
.object({
|
|
209
212
|
candidateId: z.string().trim().min(1),
|
|
210
|
-
keep: z.boolean()
|
|
213
|
+
keep: z.boolean().optional(),
|
|
214
|
+
action: z
|
|
215
|
+
.enum(["keep", "discard", "map_existing", "merge_existing"])
|
|
216
|
+
.optional(),
|
|
217
|
+
mappedEntityType: crudEntityTypeSchema.optional(),
|
|
218
|
+
mappedEntityId: z.string().trim().min(1).optional(),
|
|
219
|
+
targetNoteId: z.string().trim().min(1).optional()
|
|
220
|
+
})
|
|
221
|
+
.superRefine((value, context) => {
|
|
222
|
+
if (value.action === "map_existing") {
|
|
223
|
+
if (!value.mappedEntityType) {
|
|
224
|
+
context.addIssue({
|
|
225
|
+
code: z.ZodIssueCode.custom,
|
|
226
|
+
path: ["mappedEntityType"],
|
|
227
|
+
message: "mappedEntityType is required when action is map_existing"
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
if (!value.mappedEntityId) {
|
|
231
|
+
context.addIssue({
|
|
232
|
+
code: z.ZodIssueCode.custom,
|
|
233
|
+
path: ["mappedEntityId"],
|
|
234
|
+
message: "mappedEntityId is required when action is map_existing"
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (value.action === "merge_existing" && !value.targetNoteId) {
|
|
239
|
+
context.addIssue({
|
|
240
|
+
code: z.ZodIssueCode.custom,
|
|
241
|
+
path: ["targetNoteId"],
|
|
242
|
+
message: "targetNoteId is required when action is merge_existing"
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
if (value.action === undefined && value.keep === undefined) {
|
|
246
|
+
context.addIssue({
|
|
247
|
+
code: z.ZodIssueCode.custom,
|
|
248
|
+
path: ["action"],
|
|
249
|
+
message: "Either keep or action is required"
|
|
250
|
+
});
|
|
251
|
+
}
|
|
211
252
|
}))
|
|
212
253
|
.min(1)
|
|
213
254
|
});
|
|
@@ -218,6 +259,14 @@ export const createWikiSpaceSchema = z.object({
|
|
|
218
259
|
ownerUserId: z.string().trim().nullable().optional(),
|
|
219
260
|
visibility: wikiSpaceVisibilitySchema.default("personal")
|
|
220
261
|
});
|
|
262
|
+
const wikiLlmReasoningEffortSchema = z.enum([
|
|
263
|
+
"none",
|
|
264
|
+
"low",
|
|
265
|
+
"medium",
|
|
266
|
+
"high",
|
|
267
|
+
"xhigh"
|
|
268
|
+
]);
|
|
269
|
+
const wikiLlmVerbositySchema = z.enum(["low", "medium", "high"]);
|
|
221
270
|
export const upsertWikiLlmProfileSchema = z.object({
|
|
222
271
|
id: z.string().trim().optional(),
|
|
223
272
|
label: z.string().trim().min(1),
|
|
@@ -226,9 +275,20 @@ export const upsertWikiLlmProfileSchema = z.object({
|
|
|
226
275
|
model: z.string().trim().min(1),
|
|
227
276
|
apiKey: z.string().trim().optional(),
|
|
228
277
|
systemPrompt: z.string().trim().default(""),
|
|
278
|
+
reasoningEffort: wikiLlmReasoningEffortSchema.optional(),
|
|
279
|
+
verbosity: wikiLlmVerbositySchema.optional(),
|
|
229
280
|
enabled: z.boolean().default(true),
|
|
230
281
|
metadata: z.record(z.string(), z.unknown()).default({})
|
|
231
282
|
});
|
|
283
|
+
export const testWikiLlmProfileSchema = z.object({
|
|
284
|
+
profileId: z.string().trim().optional(),
|
|
285
|
+
provider: z.string().trim().min(1).default("openai-responses"),
|
|
286
|
+
baseUrl: z.string().trim().default("https://api.openai.com/v1"),
|
|
287
|
+
model: z.string().trim().min(1),
|
|
288
|
+
apiKey: z.string().trim().optional(),
|
|
289
|
+
reasoningEffort: wikiLlmReasoningEffortSchema.optional(),
|
|
290
|
+
verbosity: wikiLlmVerbositySchema.optional()
|
|
291
|
+
});
|
|
232
292
|
export const upsertWikiEmbeddingProfileSchema = z.object({
|
|
233
293
|
id: z.string().trim().optional(),
|
|
234
294
|
label: z.string().trim().min(1),
|
|
@@ -390,6 +450,10 @@ function parseJsonStringArray(raw) {
|
|
|
390
450
|
return [];
|
|
391
451
|
}
|
|
392
452
|
}
|
|
453
|
+
function readStringRecordValue(record, key) {
|
|
454
|
+
const value = record[key];
|
|
455
|
+
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
|
456
|
+
}
|
|
393
457
|
function listLinkRowsForNotes(noteIds) {
|
|
394
458
|
if (noteIds.length === 0) {
|
|
395
459
|
return [];
|
|
@@ -465,6 +529,20 @@ function getNoteBySlugRaw(spaceId, slug, exceptNoteId) {
|
|
|
465
529
|
.get(...(exceptNoteId ? [spaceId, slug, exceptNoteId] : [spaceId, slug]));
|
|
466
530
|
return row;
|
|
467
531
|
}
|
|
532
|
+
function getActiveNoteByIdRaw(noteId) {
|
|
533
|
+
const row = getNoteByIdRaw(noteId);
|
|
534
|
+
if (!row || isEntityDeleted("note", row.id)) {
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
return row;
|
|
538
|
+
}
|
|
539
|
+
function getActiveNoteBySlugRaw(spaceId, slug, exceptNoteId) {
|
|
540
|
+
const row = getNoteBySlugRaw(spaceId, slug, exceptNoteId);
|
|
541
|
+
if (!row || isEntityDeleted("note", row.id)) {
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
return row;
|
|
545
|
+
}
|
|
468
546
|
function buildContentPlain(markdown) {
|
|
469
547
|
return markdown
|
|
470
548
|
.replace(/^---[\s\S]*?---\s*/m, "")
|
|
@@ -497,6 +575,32 @@ function inferSummary(markdown) {
|
|
|
497
575
|
const plain = buildContentPlain(markdown).replace(/\s+/g, " ").trim();
|
|
498
576
|
return plain.slice(0, 240);
|
|
499
577
|
}
|
|
578
|
+
function stripLeadingHeading(markdown, title) {
|
|
579
|
+
const normalizedTitle = title.trim().toLowerCase();
|
|
580
|
+
const trimmed = markdown.trim();
|
|
581
|
+
const match = trimmed.match(/^#\s+(.+?)\n+/);
|
|
582
|
+
if (!match) {
|
|
583
|
+
return trimmed;
|
|
584
|
+
}
|
|
585
|
+
const heading = match[1]?.trim().toLowerCase() ?? "";
|
|
586
|
+
if (heading !== normalizedTitle) {
|
|
587
|
+
return trimmed;
|
|
588
|
+
}
|
|
589
|
+
return trimmed.slice(match[0].length).trim();
|
|
590
|
+
}
|
|
591
|
+
function mergeWikiPageContent(targetMarkdown, incoming) {
|
|
592
|
+
const mergedBody = stripLeadingHeading(incoming.markdown, incoming.title) ||
|
|
593
|
+
incoming.markdown.trim();
|
|
594
|
+
return [
|
|
595
|
+
targetMarkdown.trim(),
|
|
596
|
+
"",
|
|
597
|
+
`## ${incoming.title}`,
|
|
598
|
+
"",
|
|
599
|
+
mergedBody
|
|
600
|
+
]
|
|
601
|
+
.filter((part) => part.trim().length > 0)
|
|
602
|
+
.join("\n");
|
|
603
|
+
}
|
|
500
604
|
function slugify(value) {
|
|
501
605
|
const normalized = value
|
|
502
606
|
.toLowerCase()
|
|
@@ -1376,7 +1480,9 @@ function listAllNotes() {
|
|
|
1376
1480
|
current.push(link);
|
|
1377
1481
|
linksByNoteId.set(link.note_id, current);
|
|
1378
1482
|
}
|
|
1379
|
-
return rows
|
|
1483
|
+
return rows
|
|
1484
|
+
.filter((row) => !isEntityDeleted("note", row.id))
|
|
1485
|
+
.map((row) => mapNoteRow(row, linksByNoteId.get(row.id) ?? []));
|
|
1380
1486
|
}
|
|
1381
1487
|
export function listWikiSpaces() {
|
|
1382
1488
|
ensureSharedWikiSpace();
|
|
@@ -1441,7 +1547,7 @@ export function listWikiPageTree(query) {
|
|
|
1441
1547
|
export function getWikiHomePageDetail(input = {}) {
|
|
1442
1548
|
const spaceId = resolveSpaceId(input.spaceId, null);
|
|
1443
1549
|
ensureWikiSpaceSeedPages(spaceId);
|
|
1444
|
-
const home =
|
|
1550
|
+
const home = getActiveNoteBySlugRaw(spaceId, "index");
|
|
1445
1551
|
if (!home) {
|
|
1446
1552
|
return null;
|
|
1447
1553
|
}
|
|
@@ -1450,14 +1556,14 @@ export function getWikiHomePageDetail(input = {}) {
|
|
|
1450
1556
|
export function getWikiPageDetailBySlug(input) {
|
|
1451
1557
|
const spaceId = resolveSpaceId(input.spaceId, null);
|
|
1452
1558
|
ensureWikiSpaceSeedPages(spaceId);
|
|
1453
|
-
const row =
|
|
1559
|
+
const row = getActiveNoteBySlugRaw(spaceId, input.slug.trim());
|
|
1454
1560
|
if (!row) {
|
|
1455
1561
|
return null;
|
|
1456
1562
|
}
|
|
1457
1563
|
return getWikiPageDetail(row.id);
|
|
1458
1564
|
}
|
|
1459
1565
|
export function getWikiPageDetail(noteId) {
|
|
1460
|
-
const row =
|
|
1566
|
+
const row = getActiveNoteByIdRaw(noteId);
|
|
1461
1567
|
if (!row) {
|
|
1462
1568
|
return null;
|
|
1463
1569
|
}
|
|
@@ -1876,6 +1982,21 @@ export function upsertWikiLlmProfile(input, secrets) {
|
|
|
1876
1982
|
`wiki_llm_secret_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
|
|
1877
1983
|
storeEncryptedSecret(secretId, secrets.sealJson({ apiKey: parsed.apiKey.trim() }), `${parsed.label} wiki LLM profile`);
|
|
1878
1984
|
}
|
|
1985
|
+
const metadata = {
|
|
1986
|
+
...parsed.metadata
|
|
1987
|
+
};
|
|
1988
|
+
if (parsed.reasoningEffort) {
|
|
1989
|
+
metadata.reasoningEffort = parsed.reasoningEffort;
|
|
1990
|
+
}
|
|
1991
|
+
else {
|
|
1992
|
+
delete metadata.reasoningEffort;
|
|
1993
|
+
}
|
|
1994
|
+
if (parsed.verbosity) {
|
|
1995
|
+
metadata.verbosity = parsed.verbosity;
|
|
1996
|
+
}
|
|
1997
|
+
else {
|
|
1998
|
+
delete metadata.verbosity;
|
|
1999
|
+
}
|
|
1879
2000
|
getDatabase()
|
|
1880
2001
|
.prepare(`INSERT INTO wiki_llm_profiles (id, label, provider, base_url, model, secret_id, system_prompt, enabled, metadata_json, created_at, updated_at)
|
|
1881
2002
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
@@ -1889,7 +2010,7 @@ export function upsertWikiLlmProfile(input, secrets) {
|
|
|
1889
2010
|
enabled = excluded.enabled,
|
|
1890
2011
|
metadata_json = excluded.metadata_json,
|
|
1891
2012
|
updated_at = excluded.updated_at`)
|
|
1892
|
-
.run(id, parsed.label, parsed.provider, parsed.baseUrl, parsed.model, secretId, parsed.systemPrompt, parsed.enabled ? 1 : 0, JSON.stringify(
|
|
2013
|
+
.run(id, parsed.label, parsed.provider, parsed.baseUrl, parsed.model, secretId, parsed.systemPrompt, parsed.enabled ? 1 : 0, JSON.stringify(metadata), now, now);
|
|
1893
2014
|
return listWikiLlmProfiles().find((entry) => entry.id === id);
|
|
1894
2015
|
}
|
|
1895
2016
|
export function upsertWikiEmbeddingProfile(input, secrets) {
|
|
@@ -2088,11 +2209,103 @@ function updateWikiIngestJob(jobId, patch) {
|
|
|
2088
2209
|
.run(next.status, next.phase, next.progressPercent, next.totalFiles, next.processedFiles, next.createdPageCount, next.createdEntityCount, next.acceptedCount, next.rejectedCount, next.latestMessage, next.sourceLocator, next.mimeType, next.summary, next.pageNoteId, next.errorMessage, next.completedAt, next.updatedAt, jobId);
|
|
2089
2210
|
return getWikiIngestJob(jobId);
|
|
2090
2211
|
}
|
|
2091
|
-
function createWikiIngestLog(jobId, message, level = "info", metadata = {}) {
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2212
|
+
function createWikiIngestLog(jobId, message, level = "info", metadata = {}, options = {}) {
|
|
2213
|
+
const createdAt = nowIso();
|
|
2214
|
+
const logMetadata = options.aggregateKey
|
|
2215
|
+
? { ...metadata, aggregateKey: options.aggregateKey }
|
|
2216
|
+
: metadata;
|
|
2217
|
+
const aggregateKey = options.aggregateKey?.trim() || null;
|
|
2218
|
+
if (aggregateKey) {
|
|
2219
|
+
const current = [...listWikiIngestJobLogsInternal(jobId)]
|
|
2220
|
+
.reverse()
|
|
2221
|
+
.find((entry) => {
|
|
2222
|
+
const parsed = parseJsonRecord(entry.metadata_json);
|
|
2223
|
+
return parsed.aggregateKey === aggregateKey;
|
|
2224
|
+
});
|
|
2225
|
+
if (current) {
|
|
2226
|
+
getDatabase()
|
|
2227
|
+
.prepare(`UPDATE wiki_ingest_job_logs
|
|
2228
|
+
SET level = ?, message = ?, metadata_json = ?, created_at = ?
|
|
2229
|
+
WHERE id = ?`)
|
|
2230
|
+
.run(level, message, JSON.stringify(logMetadata), createdAt, current.id);
|
|
2231
|
+
}
|
|
2232
|
+
else {
|
|
2233
|
+
getDatabase()
|
|
2234
|
+
.prepare(`INSERT INTO wiki_ingest_job_logs (id, job_id, level, message, metadata_json, created_at)
|
|
2235
|
+
VALUES (?, ?, ?, ?, ?, ?)`)
|
|
2236
|
+
.run(`wiki_ingest_log_${randomUUID().replaceAll("-", "").slice(0, 10)}`, jobId, level, message, JSON.stringify(logMetadata), createdAt);
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
else {
|
|
2240
|
+
getDatabase()
|
|
2241
|
+
.prepare(`INSERT INTO wiki_ingest_job_logs (id, job_id, level, message, metadata_json, created_at)
|
|
2242
|
+
VALUES (?, ?, ?, ?, ?, ?)`)
|
|
2243
|
+
.run(`wiki_ingest_log_${randomUUID().replaceAll("-", "").slice(0, 10)}`, jobId, level, message, JSON.stringify(logMetadata), createdAt);
|
|
2244
|
+
}
|
|
2245
|
+
if (options.recordDiagnostic !== false) {
|
|
2246
|
+
recordDiagnosticLog({
|
|
2247
|
+
level,
|
|
2248
|
+
source: "server",
|
|
2249
|
+
scope: typeof metadata.scope === "string" && metadata.scope.trim()
|
|
2250
|
+
? metadata.scope
|
|
2251
|
+
: "wiki_ingest",
|
|
2252
|
+
eventKey: typeof metadata.eventKey === "string" && metadata.eventKey.trim()
|
|
2253
|
+
? metadata.eventKey
|
|
2254
|
+
: "wiki_ingest_log",
|
|
2255
|
+
message,
|
|
2256
|
+
functionName: "createWikiIngestLog",
|
|
2257
|
+
entityType: "wiki_ingest_job",
|
|
2258
|
+
entityId: jobId,
|
|
2259
|
+
jobId,
|
|
2260
|
+
details: logMetadata
|
|
2261
|
+
});
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
function findOpenAiResponseIdForJobAsset(input) {
|
|
2265
|
+
const normalizedFileName = input.fileName.trim().toLowerCase();
|
|
2266
|
+
const normalizedSourceLocator = input.sourceLocator.trim().toLowerCase();
|
|
2267
|
+
const normalizedChecksum = input.checksum.trim().toLowerCase();
|
|
2268
|
+
const sameNamedAssets = listWikiIngestJobAssetsInternal(input.jobId).filter((asset) => asset.file_name.trim().toLowerCase() === normalizedFileName).length;
|
|
2269
|
+
const logs = [...listWikiIngestJobLogsInternal(input.jobId)].reverse();
|
|
2270
|
+
for (const entry of logs) {
|
|
2271
|
+
const metadata = parseJsonRecord(entry.metadata_json);
|
|
2272
|
+
const responseId = readStringRecordValue(metadata, "responseId");
|
|
2273
|
+
if (!responseId) {
|
|
2274
|
+
continue;
|
|
2275
|
+
}
|
|
2276
|
+
const loggedAssetId = readStringRecordValue(metadata, "sourceAssetId") ??
|
|
2277
|
+
readStringRecordValue(metadata, "assetId");
|
|
2278
|
+
if (loggedAssetId) {
|
|
2279
|
+
if (loggedAssetId === input.assetId) {
|
|
2280
|
+
return responseId;
|
|
2281
|
+
}
|
|
2282
|
+
continue;
|
|
2283
|
+
}
|
|
2284
|
+
const loggedSourceLocator = readStringRecordValue(metadata, "sourceLocator");
|
|
2285
|
+
if (loggedSourceLocator) {
|
|
2286
|
+
if (loggedSourceLocator.trim().toLowerCase() === normalizedSourceLocator) {
|
|
2287
|
+
return responseId;
|
|
2288
|
+
}
|
|
2289
|
+
continue;
|
|
2290
|
+
}
|
|
2291
|
+
const loggedChecksum = readStringRecordValue(metadata, "checksum");
|
|
2292
|
+
if (loggedChecksum) {
|
|
2293
|
+
if (loggedChecksum.trim().toLowerCase() === normalizedChecksum) {
|
|
2294
|
+
return responseId;
|
|
2295
|
+
}
|
|
2296
|
+
continue;
|
|
2297
|
+
}
|
|
2298
|
+
const loggedFileName = readStringRecordValue(metadata, "currentFileName") ??
|
|
2299
|
+
readStringRecordValue(metadata, "fileName");
|
|
2300
|
+
if (!loggedFileName) {
|
|
2301
|
+
return responseId;
|
|
2302
|
+
}
|
|
2303
|
+
if (sameNamedAssets === 1 &&
|
|
2304
|
+
loggedFileName.trim().toLowerCase() === normalizedFileName) {
|
|
2305
|
+
return responseId;
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
return null;
|
|
2096
2309
|
}
|
|
2097
2310
|
function createWikiIngestAssetRecord(input) {
|
|
2098
2311
|
const id = `wiki_ingest_asset_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
|
|
@@ -2121,6 +2334,15 @@ function updateWikiIngestAsset(assetId, patch) {
|
|
|
2121
2334
|
.run(patch.status ?? current.status, patch.filePath ?? current.file_path, patch.mimeType ?? current.mime_type, patch.sizeBytes ?? current.size_bytes, patch.checksum ?? current.checksum, JSON.stringify(patch.metadata ?? parseJsonRecord(current.metadata_json)), nowIso(), assetId);
|
|
2122
2335
|
}
|
|
2123
2336
|
function createWikiIngestCandidate(input) {
|
|
2337
|
+
const existing = listWikiIngestCandidatesInternal(input.jobId).find((candidate) => candidate.source_asset_id === (input.sourceAssetId ?? null) &&
|
|
2338
|
+
candidate.candidate_type === input.candidateType &&
|
|
2339
|
+
candidate.title === (input.title ?? "") &&
|
|
2340
|
+
candidate.summary === (input.summary ?? "") &&
|
|
2341
|
+
candidate.target_key === (input.targetKey ?? "") &&
|
|
2342
|
+
candidate.payload_json === JSON.stringify(input.payload));
|
|
2343
|
+
if (existing) {
|
|
2344
|
+
return existing.id;
|
|
2345
|
+
}
|
|
2124
2346
|
const id = `wiki_ingest_candidate_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
|
|
2125
2347
|
const now = nowIso();
|
|
2126
2348
|
getDatabase()
|
|
@@ -2338,8 +2560,13 @@ function createPageCandidatePayload(input) {
|
|
|
2338
2560
|
summary: input.summary,
|
|
2339
2561
|
contentMarkdown: input.markdown,
|
|
2340
2562
|
kind: input.createAsKind,
|
|
2563
|
+
aliases: normalizeTags(input.aliases),
|
|
2564
|
+
parentSlug: typeof input.parentSlug === "string" && input.parentSlug.trim().length > 0
|
|
2565
|
+
? input.parentSlug.trim()
|
|
2566
|
+
: null,
|
|
2341
2567
|
sourceLocator: input.sourceLocator,
|
|
2342
|
-
links: input.linkedEntityHints
|
|
2568
|
+
links: input.linkedEntityHints,
|
|
2569
|
+
tags: normalizeTags(input.tags)
|
|
2343
2570
|
};
|
|
2344
2571
|
}
|
|
2345
2572
|
export async function processWikiIngestJob(jobId, options) {
|
|
@@ -2355,12 +2582,114 @@ export async function processWikiIngestJob(jobId, options) {
|
|
|
2355
2582
|
? (listWikiLlmProfiles().find((entry) => entry.id === parsed.llmProfileId) ?? null)
|
|
2356
2583
|
: null;
|
|
2357
2584
|
const space = getWikiSpaceById(job.space_id) ?? ensureSharedWikiSpace();
|
|
2585
|
+
if (parsed.llmProfileId && !llmProfile) {
|
|
2586
|
+
updateWikiIngestJob(jobId, {
|
|
2587
|
+
status: "failed",
|
|
2588
|
+
phase: "failed",
|
|
2589
|
+
latestMessage: "The selected LLM profile could not be found.",
|
|
2590
|
+
errorMessage: "The selected LLM profile could not be found.",
|
|
2591
|
+
completedAt: nowIso()
|
|
2592
|
+
});
|
|
2593
|
+
createWikiIngestLog(jobId, "The selected LLM profile could not be found.", "error");
|
|
2594
|
+
return getWikiIngestJob(jobId);
|
|
2595
|
+
}
|
|
2358
2596
|
updateWikiIngestJob(jobId, {
|
|
2359
2597
|
status: "processing",
|
|
2360
2598
|
phase: "processing",
|
|
2361
|
-
latestMessage: "Processing queued wiki ingest sources."
|
|
2599
|
+
latestMessage: "Processing queued wiki ingest sources.",
|
|
2600
|
+
errorMessage: "",
|
|
2601
|
+
completedAt: null
|
|
2362
2602
|
});
|
|
2363
2603
|
createWikiIngestLog(jobId, "Started background wiki ingestion.");
|
|
2604
|
+
let currentAssetContext = null;
|
|
2605
|
+
let currentPollCount = 0;
|
|
2606
|
+
const updateCurrentAssetMetadata = (patch) => {
|
|
2607
|
+
if (!currentAssetContext) {
|
|
2608
|
+
return;
|
|
2609
|
+
}
|
|
2610
|
+
const asset = listWikiIngestJobAssetsInternal(jobId).find((entry) => entry.id === currentAssetContext?.assetId);
|
|
2611
|
+
if (!asset) {
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
updateWikiIngestAsset(asset.id, {
|
|
2615
|
+
metadata: {
|
|
2616
|
+
...parseJsonRecord(asset.metadata_json),
|
|
2617
|
+
...patch
|
|
2618
|
+
}
|
|
2619
|
+
});
|
|
2620
|
+
};
|
|
2621
|
+
const llmDiagnosticLogger = ({ level, message, details = {} }) => {
|
|
2622
|
+
const enrichedDetails = {
|
|
2623
|
+
...details,
|
|
2624
|
+
sourceAssetId: currentAssetContext?.assetId ?? null,
|
|
2625
|
+
currentFileName: currentAssetContext?.fileName ?? null,
|
|
2626
|
+
sourceLocator: currentAssetContext?.sourceLocator ?? null,
|
|
2627
|
+
checksum: currentAssetContext?.checksum ?? null,
|
|
2628
|
+
currentFileIndex: currentAssetContext?.fileIndex ?? null,
|
|
2629
|
+
currentFileTotal: currentAssetContext?.totalFiles ?? null,
|
|
2630
|
+
chunkIndex: typeof details.chunkIndex === "number"
|
|
2631
|
+
? details.chunkIndex
|
|
2632
|
+
: currentAssetContext?.chunkIndex ?? null,
|
|
2633
|
+
chunkCount: typeof details.chunkCount === "number"
|
|
2634
|
+
? details.chunkCount
|
|
2635
|
+
: currentAssetContext?.chunkCount ?? null
|
|
2636
|
+
};
|
|
2637
|
+
const eventKey = typeof details.eventKey === "string" ? details.eventKey : "";
|
|
2638
|
+
if (eventKey === "llm_compile_background_started") {
|
|
2639
|
+
currentPollCount = 0;
|
|
2640
|
+
updateCurrentAssetMetadata({
|
|
2641
|
+
openAiResponseId: typeof details.responseId === "string" ? details.responseId : null,
|
|
2642
|
+
openAiResponseStatus: typeof details.status === "string" ? details.status : "queued",
|
|
2643
|
+
openAiLastPolledAt: nowIso()
|
|
2644
|
+
});
|
|
2645
|
+
}
|
|
2646
|
+
if (eventKey === "llm_compile_background_polled") {
|
|
2647
|
+
currentPollCount += 1;
|
|
2648
|
+
const pollStatus = typeof details.status === "string" ? details.status : "in_progress";
|
|
2649
|
+
const chunkSuffix = (currentAssetContext?.chunkCount ?? 1) > 1
|
|
2650
|
+
? ` · chunk ${currentAssetContext?.chunkIndex ?? 1}/${currentAssetContext?.chunkCount ?? 1}`
|
|
2651
|
+
: "";
|
|
2652
|
+
const fileLabel = currentAssetContext?.fileName ?? "current source";
|
|
2653
|
+
const progressMessage = `Waiting for OpenAI on ${fileLabel}${chunkSuffix}. Poll ${currentPollCount} · ${pollStatus}.`;
|
|
2654
|
+
updateWikiIngestJob(jobId, {
|
|
2655
|
+
latestMessage: progressMessage
|
|
2656
|
+
});
|
|
2657
|
+
updateCurrentAssetMetadata({
|
|
2658
|
+
openAiResponseId: typeof details.responseId === "string" ? details.responseId : null,
|
|
2659
|
+
openAiResponseStatus: pollStatus,
|
|
2660
|
+
openAiLastPolledAt: nowIso(),
|
|
2661
|
+
openAiPollCount: currentPollCount
|
|
2662
|
+
});
|
|
2663
|
+
createWikiIngestLog(jobId, progressMessage, "info", {
|
|
2664
|
+
...enrichedDetails,
|
|
2665
|
+
pollCount: currentPollCount,
|
|
2666
|
+
status: pollStatus
|
|
2667
|
+
}, {
|
|
2668
|
+
aggregateKey: `llm_compile_background_polled:${currentAssetContext?.assetId ?? "job"}`,
|
|
2669
|
+
recordDiagnostic: false
|
|
2670
|
+
});
|
|
2671
|
+
return;
|
|
2672
|
+
}
|
|
2673
|
+
if (eventKey === "llm_compile_success" ||
|
|
2674
|
+
eventKey === "llm_compile_unparseable" ||
|
|
2675
|
+
eventKey === "llm_compile_background_terminal_error") {
|
|
2676
|
+
updateCurrentAssetMetadata({
|
|
2677
|
+
openAiResponseId: typeof details.responseId === "string" ? details.responseId : null,
|
|
2678
|
+
openAiResponseStatus: eventKey === "llm_compile_background_terminal_error"
|
|
2679
|
+
? typeof details.status === "string"
|
|
2680
|
+
? details.status
|
|
2681
|
+
: "failed"
|
|
2682
|
+
: "completed",
|
|
2683
|
+
openAiLastPolledAt: nowIso(),
|
|
2684
|
+
openAiPollCount: currentPollCount
|
|
2685
|
+
});
|
|
2686
|
+
}
|
|
2687
|
+
createWikiIngestLog(jobId, message, level === "debug" ? "info" : level, {
|
|
2688
|
+
scope: "wiki_llm",
|
|
2689
|
+
eventKey: "wiki_llm_event",
|
|
2690
|
+
...enrichedDetails
|
|
2691
|
+
});
|
|
2692
|
+
};
|
|
2364
2693
|
const refreshCounts = () => {
|
|
2365
2694
|
const candidates = listWikiIngestCandidatesInternal(jobId);
|
|
2366
2695
|
const pageCount = candidates.filter((candidate) => candidate.candidate_type === "page").length;
|
|
@@ -2368,11 +2697,12 @@ export async function processWikiIngestJob(jobId, options) {
|
|
|
2368
2697
|
return { pageCount, entityCount };
|
|
2369
2698
|
};
|
|
2370
2699
|
const assetQueue = () => listWikiIngestJobAssetsInternal(jobId).filter((asset) => ["queued", "processing"].includes(asset.status));
|
|
2371
|
-
|
|
2372
|
-
let
|
|
2700
|
+
const initialAssets = listWikiIngestJobAssetsInternal(jobId);
|
|
2701
|
+
let processedFiles = initialAssets.filter((asset) => asset.status === "completed").length;
|
|
2702
|
+
let totalFiles = Math.max(job.total_files, initialAssets.length);
|
|
2373
2703
|
let hadSuccess = false;
|
|
2374
2704
|
while (assetQueue().length > 0) {
|
|
2375
|
-
const nextAsset = assetQueue().find((asset) =>
|
|
2705
|
+
const nextAsset = assetQueue().find((asset) => ["processing", "queued"].includes(asset.status));
|
|
2376
2706
|
if (!nextAsset) {
|
|
2377
2707
|
break;
|
|
2378
2708
|
}
|
|
@@ -2433,7 +2763,27 @@ export async function processWikiIngestJob(jobId, options) {
|
|
|
2433
2763
|
continue;
|
|
2434
2764
|
}
|
|
2435
2765
|
updateWikiIngestAsset(nextAsset.id, { status: "processing" });
|
|
2436
|
-
|
|
2766
|
+
currentAssetContext = {
|
|
2767
|
+
assetId: nextAsset.id,
|
|
2768
|
+
fileName: nextAsset.file_name || nextAsset.source_locator || "Source",
|
|
2769
|
+
sourceLocator: nextAsset.source_locator,
|
|
2770
|
+
checksum: nextAsset.checksum,
|
|
2771
|
+
fileIndex: Math.min(totalFiles, processedFiles + 1),
|
|
2772
|
+
totalFiles,
|
|
2773
|
+
chunkIndex: 1,
|
|
2774
|
+
chunkCount: 1
|
|
2775
|
+
};
|
|
2776
|
+
currentPollCount = 0;
|
|
2777
|
+
createWikiIngestLog(jobId, `Processing ${currentAssetContext.fileName} (${currentAssetContext.fileIndex}/${currentAssetContext.totalFiles}).`, "info", {
|
|
2778
|
+
sourceAssetId: currentAssetContext.assetId,
|
|
2779
|
+
fileName: currentAssetContext.fileName,
|
|
2780
|
+
sourceLocator: currentAssetContext.sourceLocator,
|
|
2781
|
+
checksum: currentAssetContext.checksum,
|
|
2782
|
+
fileIndex: currentAssetContext.fileIndex,
|
|
2783
|
+
totalFiles: currentAssetContext.totalFiles,
|
|
2784
|
+
chunkIndex: currentAssetContext.chunkIndex,
|
|
2785
|
+
chunkCount: currentAssetContext.chunkCount
|
|
2786
|
+
});
|
|
2437
2787
|
try {
|
|
2438
2788
|
const fetched = nextAsset.source_kind === "url"
|
|
2439
2789
|
? await getFetchedContent("url", {
|
|
@@ -2451,6 +2801,15 @@ export async function processWikiIngestJob(jobId, options) {
|
|
|
2451
2801
|
jobId,
|
|
2452
2802
|
fetched
|
|
2453
2803
|
});
|
|
2804
|
+
const existingAssetMetadata = parseJsonRecord(nextAsset.metadata_json);
|
|
2805
|
+
const resumeResponseId = readStringRecordValue(existingAssetMetadata, "openAiResponseId") ??
|
|
2806
|
+
findOpenAiResponseIdForJobAsset({
|
|
2807
|
+
jobId,
|
|
2808
|
+
assetId: nextAsset.id,
|
|
2809
|
+
fileName: nextAsset.file_name,
|
|
2810
|
+
sourceLocator: nextAsset.source_locator,
|
|
2811
|
+
checksum: nextAsset.checksum
|
|
2812
|
+
});
|
|
2454
2813
|
const compiled = llmProfile && parsed.parseStrategy !== "text_only"
|
|
2455
2814
|
? await options.llm.compileWikiIngest(llmProfile, {
|
|
2456
2815
|
titleHint: parsed.titleHint,
|
|
@@ -2458,7 +2817,9 @@ export async function processWikiIngestJob(jobId, options) {
|
|
|
2458
2817
|
binary: fetched.binary,
|
|
2459
2818
|
mimeType: fetched.mimeType,
|
|
2460
2819
|
parseStrategy: parsed.parseStrategy
|
|
2461
|
-
}
|
|
2820
|
+
}, {
|
|
2821
|
+
resumeResponseId
|
|
2822
|
+
}, llmDiagnosticLogger)
|
|
2462
2823
|
: llmProfile && fetched.contentText
|
|
2463
2824
|
? await options.llm.compileWikiIngest(llmProfile, {
|
|
2464
2825
|
titleHint: parsed.titleHint,
|
|
@@ -2466,8 +2827,13 @@ export async function processWikiIngestJob(jobId, options) {
|
|
|
2466
2827
|
binary: fetched.binary,
|
|
2467
2828
|
mimeType: fetched.mimeType,
|
|
2468
2829
|
parseStrategy: "text_only"
|
|
2469
|
-
}
|
|
2830
|
+
}, {
|
|
2831
|
+
resumeResponseId
|
|
2832
|
+
}, llmDiagnosticLogger)
|
|
2470
2833
|
: null;
|
|
2834
|
+
if (llmProfile && !compiled) {
|
|
2835
|
+
throw new Error("The LLM did not produce structured draft candidates. Check the OpenAI settings and try again.");
|
|
2836
|
+
}
|
|
2471
2837
|
const title = compiled?.title ||
|
|
2472
2838
|
parsed.titleHint ||
|
|
2473
2839
|
inferTitle(fetched.contentText || fetched.fileName, "Imported source");
|
|
@@ -2491,6 +2857,7 @@ export async function processWikiIngestJob(jobId, options) {
|
|
|
2491
2857
|
markdown,
|
|
2492
2858
|
sourceLocator: fetched.locator,
|
|
2493
2859
|
createAsKind: parsed.createAsKind,
|
|
2860
|
+
tags: compiled?.tags,
|
|
2494
2861
|
linkedEntityHints: parsed.linkedEntityHints
|
|
2495
2862
|
})
|
|
2496
2863
|
});
|
|
@@ -2551,11 +2918,23 @@ export async function processWikiIngestJob(jobId, options) {
|
|
|
2551
2918
|
payload: createPageCandidatePayload({
|
|
2552
2919
|
title: articleTitle,
|
|
2553
2920
|
summary: articleSummary,
|
|
2554
|
-
markdown:
|
|
2555
|
-
|
|
2556
|
-
|
|
2921
|
+
markdown: typeof candidate.markdown === "string" &&
|
|
2922
|
+
candidate.markdown.trim().length > 0
|
|
2923
|
+
? candidate.markdown
|
|
2924
|
+
: `# ${articleTitle}\n\n${articleSummary}\n\n${articleRationale
|
|
2925
|
+
? `## Why this page\n\n${articleRationale}\n`
|
|
2926
|
+
: ""}`,
|
|
2557
2927
|
sourceLocator: fetched.locator,
|
|
2558
2928
|
createAsKind: "wiki",
|
|
2929
|
+
aliases: Array.isArray(candidate.aliases)
|
|
2930
|
+
? candidate.aliases.filter((alias) => typeof alias === "string")
|
|
2931
|
+
: [],
|
|
2932
|
+
parentSlug: typeof candidate.parentSlug === "string"
|
|
2933
|
+
? candidate.parentSlug
|
|
2934
|
+
: null,
|
|
2935
|
+
tags: Array.isArray(candidate.tags)
|
|
2936
|
+
? candidate.tags.filter((tag) => typeof tag === "string")
|
|
2937
|
+
: [],
|
|
2559
2938
|
linkedEntityHints: parsed.linkedEntityHints
|
|
2560
2939
|
})
|
|
2561
2940
|
});
|
|
@@ -2568,7 +2947,9 @@ export async function processWikiIngestJob(jobId, options) {
|
|
|
2568
2947
|
checksum: rawSource.checksum,
|
|
2569
2948
|
metadata: {
|
|
2570
2949
|
...(parseJsonRecord(nextAsset.metadata_json) ?? {}),
|
|
2571
|
-
rawSourcePath: rawSource.filePath
|
|
2950
|
+
rawSourcePath: rawSource.filePath,
|
|
2951
|
+
openAiResponseStatus: llmProfile ? "completed" : null,
|
|
2952
|
+
openAiLastPolledAt: llmProfile ? nowIso() : null
|
|
2572
2953
|
}
|
|
2573
2954
|
});
|
|
2574
2955
|
hadSuccess = true;
|
|
@@ -2586,20 +2967,38 @@ export async function processWikiIngestJob(jobId, options) {
|
|
|
2586
2967
|
latestMessage: `Prepared candidates from ${fetched.fileName}.`
|
|
2587
2968
|
});
|
|
2588
2969
|
createWikiIngestLog(jobId, `Prepared candidates from ${fetched.fileName}.`, "info", {
|
|
2970
|
+
fileName: currentAssetContext?.fileName ?? fetched.fileName,
|
|
2971
|
+
sourceAssetId: currentAssetContext?.assetId ?? null,
|
|
2972
|
+
fileIndex: currentAssetContext?.fileIndex ?? processedFiles,
|
|
2973
|
+
totalFiles: currentAssetContext?.totalFiles ?? totalFiles,
|
|
2589
2974
|
pageCandidates: counts.pageCount,
|
|
2590
2975
|
entityCandidates: counts.entityCount
|
|
2591
2976
|
});
|
|
2977
|
+
currentAssetContext = null;
|
|
2978
|
+
currentPollCount = 0;
|
|
2592
2979
|
}
|
|
2593
2980
|
catch (error) {
|
|
2594
2981
|
processedFiles += 1;
|
|
2595
2982
|
updateWikiIngestAsset(nextAsset.id, { status: "failed" });
|
|
2983
|
+
updateCurrentAssetMetadata({
|
|
2984
|
+
openAiResponseStatus: "failed",
|
|
2985
|
+
openAiLastPolledAt: nowIso(),
|
|
2986
|
+
openAiPollCount: currentPollCount
|
|
2987
|
+
});
|
|
2596
2988
|
updateWikiIngestJob(jobId, {
|
|
2597
2989
|
processedFiles,
|
|
2598
2990
|
totalFiles,
|
|
2599
2991
|
progressPercent: calculateProgress(totalFiles, processedFiles),
|
|
2600
2992
|
latestMessage: error instanceof Error ? error.message : "Source ingest failed."
|
|
2601
2993
|
});
|
|
2602
|
-
createWikiIngestLog(jobId, error instanceof Error ? error.message : "Source ingest failed.", "error"
|
|
2994
|
+
createWikiIngestLog(jobId, error instanceof Error ? error.message : "Source ingest failed.", "error", {
|
|
2995
|
+
fileName: currentAssetContext?.fileName ?? null,
|
|
2996
|
+
fileIndex: currentAssetContext?.fileIndex ?? null,
|
|
2997
|
+
totalFiles: currentAssetContext?.totalFiles ?? totalFiles,
|
|
2998
|
+
pollCount: currentPollCount
|
|
2999
|
+
});
|
|
3000
|
+
currentAssetContext = null;
|
|
3001
|
+
currentPollCount = 0;
|
|
2603
3002
|
}
|
|
2604
3003
|
}
|
|
2605
3004
|
const counts = refreshCounts();
|
|
@@ -2635,6 +3034,94 @@ export function listWikiIngestJobs(input = {}) {
|
|
|
2635
3034
|
.map((row) => getWikiIngestJob(row.id))
|
|
2636
3035
|
.filter((job) => job !== null);
|
|
2637
3036
|
}
|
|
3037
|
+
export async function rerunWikiIngestJob(jobId, options = {}) {
|
|
3038
|
+
const job = readWikiIngestJobRow(jobId);
|
|
3039
|
+
if (!job) {
|
|
3040
|
+
return null;
|
|
3041
|
+
}
|
|
3042
|
+
if (["queued", "processing"].includes(job.status)) {
|
|
3043
|
+
throw new Error("Wiki ingest jobs can only be rerun after processing ends.");
|
|
3044
|
+
}
|
|
3045
|
+
const ingestInput = readWikiIngestInput(jobId);
|
|
3046
|
+
if (!ingestInput) {
|
|
3047
|
+
throw new Error("Wiki ingest input could not be restored.");
|
|
3048
|
+
}
|
|
3049
|
+
const rootAssets = listWikiIngestJobAssetsInternal(jobId).filter((asset) => {
|
|
3050
|
+
const metadata = parseJsonRecord(asset.metadata_json);
|
|
3051
|
+
return !metadata?.parentAssetId && asset.source_kind !== "url";
|
|
3052
|
+
});
|
|
3053
|
+
const replayResult = rootAssets.length > 0
|
|
3054
|
+
? await createUploadedWikiIngestJob({
|
|
3055
|
+
spaceId: ingestInput.spaceId,
|
|
3056
|
+
titleHint: ingestInput.titleHint,
|
|
3057
|
+
llmProfileId: ingestInput.llmProfileId,
|
|
3058
|
+
parseStrategy: ingestInput.parseStrategy,
|
|
3059
|
+
entityProposalMode: ingestInput.entityProposalMode,
|
|
3060
|
+
userId: ingestInput.userId ?? null,
|
|
3061
|
+
createAsKind: ingestInput.createAsKind,
|
|
3062
|
+
linkedEntityHints: ingestInput.linkedEntityHints
|
|
3063
|
+
}, await Promise.all(rootAssets.map(async (asset) => ({
|
|
3064
|
+
fileName: asset.file_name,
|
|
3065
|
+
mimeType: asset.mime_type,
|
|
3066
|
+
payload: await readFile(asset.file_path)
|
|
3067
|
+
}))), {
|
|
3068
|
+
actor: options.actor ?? job.created_by_actor ?? null
|
|
3069
|
+
})
|
|
3070
|
+
: await ingestWikiSource(ingestInput, {
|
|
3071
|
+
actor: options.actor ?? job.created_by_actor ?? null
|
|
3072
|
+
});
|
|
3073
|
+
const nextJobId = replayResult.job?.job.id ?? null;
|
|
3074
|
+
if (nextJobId) {
|
|
3075
|
+
createWikiIngestLog(nextJobId, `Reran ingest from ${jobId}.`, "info", {
|
|
3076
|
+
scope: "wiki_ingest",
|
|
3077
|
+
eventKey: "wiki_ingest_rerun",
|
|
3078
|
+
sourceJobId: jobId
|
|
3079
|
+
});
|
|
3080
|
+
createWikiIngestLog(jobId, `Created rerun job ${nextJobId}.`, "info", {
|
|
3081
|
+
scope: "wiki_ingest",
|
|
3082
|
+
eventKey: "wiki_ingest_rerun_requested",
|
|
3083
|
+
rerunJobId: nextJobId
|
|
3084
|
+
});
|
|
3085
|
+
}
|
|
3086
|
+
return replayResult;
|
|
3087
|
+
}
|
|
3088
|
+
export function deleteWikiIngestJob(jobId) {
|
|
3089
|
+
const job = readWikiIngestJobRow(jobId);
|
|
3090
|
+
if (!job) {
|
|
3091
|
+
return null;
|
|
3092
|
+
}
|
|
3093
|
+
if (["queued", "processing"].includes(job.status)) {
|
|
3094
|
+
throw new Error("Wiki ingest jobs can only be deleted after processing ends.");
|
|
3095
|
+
}
|
|
3096
|
+
const candidates = listWikiIngestCandidatesInternal(jobId);
|
|
3097
|
+
const assets = listWikiIngestJobAssetsInternal(jobId);
|
|
3098
|
+
const preservedAssetIds = new Set(candidates
|
|
3099
|
+
.filter((candidate) => candidate.status === "applied")
|
|
3100
|
+
.map((candidate) => candidate.source_asset_id)
|
|
3101
|
+
.filter((assetId) => typeof assetId === "string"));
|
|
3102
|
+
for (const asset of assets) {
|
|
3103
|
+
if (preservedAssetIds.has(asset.id)) {
|
|
3104
|
+
continue;
|
|
3105
|
+
}
|
|
3106
|
+
const metadata = parseJsonRecord(asset.metadata_json);
|
|
3107
|
+
const rawSourcePath = typeof metadata?.rawSourcePath === "string"
|
|
3108
|
+
? metadata.rawSourcePath
|
|
3109
|
+
: null;
|
|
3110
|
+
if (rawSourcePath && existsSync(rawSourcePath)) {
|
|
3111
|
+
unlinkSync(rawSourcePath);
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
const jobDir = getWikiIngestJobDir(jobId);
|
|
3115
|
+
if (existsSync(jobDir)) {
|
|
3116
|
+
rmSync(jobDir, { recursive: true, force: true });
|
|
3117
|
+
}
|
|
3118
|
+
getDatabase()
|
|
3119
|
+
.prepare(`DELETE FROM wiki_ingest_jobs WHERE id = ?`)
|
|
3120
|
+
.run(jobId);
|
|
3121
|
+
return {
|
|
3122
|
+
id: jobId
|
|
3123
|
+
};
|
|
3124
|
+
}
|
|
2638
3125
|
export async function reviewWikiIngestJob(jobId, input, options) {
|
|
2639
3126
|
const parsed = reviewWikiIngestJobSchema.parse(input);
|
|
2640
3127
|
const job = readWikiIngestJobRow(jobId);
|
|
@@ -2647,6 +3134,7 @@ export async function reviewWikiIngestJob(jobId, input, options) {
|
|
|
2647
3134
|
}
|
|
2648
3135
|
const candidates = listWikiIngestCandidatesInternal(jobId);
|
|
2649
3136
|
let acceptedCount = 0;
|
|
3137
|
+
let mappedCount = 0;
|
|
2650
3138
|
let rejectedCount = 0;
|
|
2651
3139
|
let firstPublishedPageId = null;
|
|
2652
3140
|
for (const decision of parsed.decisions) {
|
|
@@ -2654,7 +3142,12 @@ export async function reviewWikiIngestJob(jobId, input, options) {
|
|
|
2654
3142
|
if (!candidate) {
|
|
2655
3143
|
continue;
|
|
2656
3144
|
}
|
|
2657
|
-
if (
|
|
3145
|
+
if (candidate.status === "applied") {
|
|
3146
|
+
continue;
|
|
3147
|
+
}
|
|
3148
|
+
const action = decision.action ??
|
|
3149
|
+
(decision.keep === false ? "discard" : "keep");
|
|
3150
|
+
if (action === "discard") {
|
|
2658
3151
|
rejectedCount += 1;
|
|
2659
3152
|
updateWikiIngestCandidate(candidate.id, { status: "rejected" });
|
|
2660
3153
|
continue;
|
|
@@ -2662,36 +3155,77 @@ export async function reviewWikiIngestJob(jobId, input, options) {
|
|
|
2662
3155
|
try {
|
|
2663
3156
|
const payload = parseJsonRecord(candidate.payload_json);
|
|
2664
3157
|
if (candidate.candidate_type === "page") {
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
sourcePath: "",
|
|
2675
|
-
frontmatter: {},
|
|
2676
|
-
revisionHash: "",
|
|
2677
|
-
spaceId: job.space_id,
|
|
2678
|
-
contentMarkdown: typeof payload.contentMarkdown === "string"
|
|
3158
|
+
if (action === "merge_existing") {
|
|
3159
|
+
const target = decision.targetNoteId
|
|
3160
|
+
? (getWikiPageDetail(decision.targetNoteId)?.page ?? null)
|
|
3161
|
+
: null;
|
|
3162
|
+
if (!target || target.spaceId !== job.space_id) {
|
|
3163
|
+
throw new Error("Merge target page was not found.");
|
|
3164
|
+
}
|
|
3165
|
+
const incomingTitle = typeof payload.title === "string" ? payload.title : candidate.title;
|
|
3166
|
+
const incomingMarkdown = typeof payload.contentMarkdown === "string"
|
|
2679
3167
|
? payload.contentMarkdown
|
|
2680
|
-
: `# ${candidate.title}\n
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
3168
|
+
: `# ${candidate.title}\n`;
|
|
3169
|
+
const mergedContentMarkdown = mergeWikiPageContent(target.contentMarkdown, {
|
|
3170
|
+
title: incomingTitle,
|
|
3171
|
+
markdown: incomingMarkdown
|
|
3172
|
+
});
|
|
3173
|
+
const mergedSummary = inferSummary(mergedContentMarkdown);
|
|
3174
|
+
const updated = options.updateNote(target.id, {
|
|
3175
|
+
contentMarkdown: mergedContentMarkdown,
|
|
3176
|
+
summary: mergedSummary
|
|
3177
|
+
});
|
|
3178
|
+
if (!updated) {
|
|
3179
|
+
throw new Error("Merge target page could not be updated.");
|
|
3180
|
+
}
|
|
3181
|
+
acceptedCount += 1;
|
|
3182
|
+
firstPublishedPageId = firstPublishedPageId ?? updated.id;
|
|
3183
|
+
updateWikiIngestCandidate(candidate.id, {
|
|
3184
|
+
status: "applied",
|
|
3185
|
+
publishedNoteId: updated.id
|
|
3186
|
+
});
|
|
3187
|
+
}
|
|
3188
|
+
else {
|
|
3189
|
+
const note = options.createNote({
|
|
3190
|
+
kind: payload.kind === "evidence" ? "evidence" : "wiki",
|
|
3191
|
+
title: typeof payload.title === "string"
|
|
3192
|
+
? payload.title
|
|
3193
|
+
: candidate.title,
|
|
3194
|
+
slug: "",
|
|
3195
|
+
indexOrder: 0,
|
|
3196
|
+
aliases: Array.isArray(payload.aliases)
|
|
3197
|
+
? payload.aliases.filter((alias) => typeof alias === "string")
|
|
3198
|
+
: [],
|
|
3199
|
+
summary: typeof payload.summary === "string"
|
|
3200
|
+
? payload.summary
|
|
3201
|
+
: candidate.summary,
|
|
3202
|
+
sourcePath: "",
|
|
3203
|
+
frontmatter: {},
|
|
3204
|
+
revisionHash: "",
|
|
3205
|
+
spaceId: job.space_id,
|
|
3206
|
+
parentSlug: typeof payload.parentSlug === "string"
|
|
3207
|
+
? payload.parentSlug
|
|
3208
|
+
: null,
|
|
3209
|
+
contentMarkdown: typeof payload.contentMarkdown === "string"
|
|
3210
|
+
? payload.contentMarkdown
|
|
3211
|
+
: `# ${candidate.title}\n`,
|
|
3212
|
+
author: ingestInput.userId ?? null,
|
|
3213
|
+
links: Array.isArray(payload.links)
|
|
3214
|
+
? payload.links
|
|
3215
|
+
: ingestInput.linkedEntityHints,
|
|
3216
|
+
tags: Array.isArray(payload.tags)
|
|
3217
|
+
? payload.tags.filter((tag) => typeof tag === "string")
|
|
3218
|
+
: [],
|
|
3219
|
+
destroyAt: null,
|
|
3220
|
+
userId: ingestInput.userId ?? null
|
|
3221
|
+
});
|
|
3222
|
+
acceptedCount += 1;
|
|
3223
|
+
firstPublishedPageId = firstPublishedPageId ?? note.id;
|
|
3224
|
+
updateWikiIngestCandidate(candidate.id, {
|
|
3225
|
+
status: "applied",
|
|
3226
|
+
publishedNoteId: note.id
|
|
3227
|
+
});
|
|
3228
|
+
}
|
|
2695
3229
|
}
|
|
2696
3230
|
else if (candidate.candidate_type === "page_update") {
|
|
2697
3231
|
const targetSlug = typeof payload.targetSlug === "string"
|
|
@@ -2723,13 +3257,32 @@ export async function reviewWikiIngestJob(jobId, input, options) {
|
|
|
2723
3257
|
}
|
|
2724
3258
|
}
|
|
2725
3259
|
else if (candidate.candidate_type === "entity") {
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
3260
|
+
if (action === "map_existing") {
|
|
3261
|
+
const proposedType = typeof payload.entityType === "string" ? payload.entityType : "";
|
|
3262
|
+
if (decision.mappedEntityType !== proposedType) {
|
|
3263
|
+
throw new Error("Mapped entity type must match the proposed entity type.");
|
|
3264
|
+
}
|
|
3265
|
+
const mapped = options.resolveMappedEntity(decision.mappedEntityType, decision.mappedEntityId);
|
|
3266
|
+
if (!mapped) {
|
|
3267
|
+
throw new Error("Mapped entity was not found.");
|
|
3268
|
+
}
|
|
3269
|
+
acceptedCount += 1;
|
|
3270
|
+
mappedCount += 1;
|
|
3271
|
+
updateWikiIngestCandidate(candidate.id, {
|
|
3272
|
+
status: "applied",
|
|
3273
|
+
publishedEntityType: mapped.entityType,
|
|
3274
|
+
publishedEntityId: mapped.entityId
|
|
3275
|
+
});
|
|
3276
|
+
}
|
|
3277
|
+
else {
|
|
3278
|
+
const result = options.publishEntity(payload);
|
|
3279
|
+
acceptedCount += 1;
|
|
3280
|
+
updateWikiIngestCandidate(candidate.id, {
|
|
3281
|
+
status: "applied",
|
|
3282
|
+
publishedEntityType: result.entityType,
|
|
3283
|
+
publishedEntityId: result.entityId
|
|
3284
|
+
});
|
|
3285
|
+
}
|
|
2733
3286
|
}
|
|
2734
3287
|
}
|
|
2735
3288
|
catch {
|
|
@@ -2742,10 +3295,10 @@ export async function reviewWikiIngestJob(jobId, input, options) {
|
|
|
2742
3295
|
rejectedCount,
|
|
2743
3296
|
pageNoteId: firstPublishedPageId,
|
|
2744
3297
|
latestMessage: acceptedCount > 0 || rejectedCount > 0
|
|
2745
|
-
? `Review saved with ${acceptedCount} kept and ${rejectedCount} discarded.`
|
|
3298
|
+
? `Review saved with ${acceptedCount} kept${mappedCount > 0 ? ` (${mappedCount} mapped)` : ""} and ${rejectedCount} discarded.`
|
|
2746
3299
|
: "Review saved."
|
|
2747
3300
|
});
|
|
2748
|
-
createWikiIngestLog(jobId, `Review saved with ${acceptedCount} kept and ${rejectedCount} discarded.`);
|
|
3301
|
+
createWikiIngestLog(jobId, `Review saved with ${acceptedCount} kept${mappedCount > 0 ? ` (${mappedCount} mapped)` : ""} and ${rejectedCount} discarded.`);
|
|
2749
3302
|
return getWikiIngestJob(jobId);
|
|
2750
3303
|
}
|
|
2751
3304
|
export function getWikiIngestJob(jobId) {
|