forge-openclaw-plugin 0.2.20 → 0.2.21
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-B4A6TooJ.js +63 -0
- package/dist/assets/index-B4A6TooJ.js.map +1 -0
- package/dist/assets/index-D6Xs_2mo.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 +301 -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 +595 -62
- 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,7 @@ 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 { recordDiagnosticLog } from "./diagnostic-logs.js";
|
|
11
12
|
const wikiSpaceSchema = z.object({
|
|
12
13
|
id: z.string(),
|
|
13
14
|
slug: z.string(),
|
|
@@ -201,13 +202,52 @@ const wikiIngestJobPayloadSchema = z.object({
|
|
|
201
202
|
});
|
|
202
203
|
const listWikiIngestJobsQuerySchema = z.object({
|
|
203
204
|
spaceId: z.string().trim().optional(),
|
|
204
|
-
limit: z.coerce.number().int().positive().max(
|
|
205
|
+
limit: z.coerce.number().int().positive().max(200).default(20)
|
|
205
206
|
});
|
|
206
207
|
export const reviewWikiIngestJobSchema = z.object({
|
|
207
208
|
decisions: z
|
|
208
|
-
.array(z
|
|
209
|
+
.array(z
|
|
210
|
+
.object({
|
|
209
211
|
candidateId: z.string().trim().min(1),
|
|
210
|
-
keep: z.boolean()
|
|
212
|
+
keep: z.boolean().optional(),
|
|
213
|
+
action: z
|
|
214
|
+
.enum(["keep", "discard", "map_existing", "merge_existing"])
|
|
215
|
+
.optional(),
|
|
216
|
+
mappedEntityType: crudEntityTypeSchema.optional(),
|
|
217
|
+
mappedEntityId: z.string().trim().min(1).optional(),
|
|
218
|
+
targetNoteId: z.string().trim().min(1).optional()
|
|
219
|
+
})
|
|
220
|
+
.superRefine((value, context) => {
|
|
221
|
+
if (value.action === "map_existing") {
|
|
222
|
+
if (!value.mappedEntityType) {
|
|
223
|
+
context.addIssue({
|
|
224
|
+
code: z.ZodIssueCode.custom,
|
|
225
|
+
path: ["mappedEntityType"],
|
|
226
|
+
message: "mappedEntityType is required when action is map_existing"
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
if (!value.mappedEntityId) {
|
|
230
|
+
context.addIssue({
|
|
231
|
+
code: z.ZodIssueCode.custom,
|
|
232
|
+
path: ["mappedEntityId"],
|
|
233
|
+
message: "mappedEntityId is required when action is map_existing"
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (value.action === "merge_existing" && !value.targetNoteId) {
|
|
238
|
+
context.addIssue({
|
|
239
|
+
code: z.ZodIssueCode.custom,
|
|
240
|
+
path: ["targetNoteId"],
|
|
241
|
+
message: "targetNoteId is required when action is merge_existing"
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
if (value.action === undefined && value.keep === undefined) {
|
|
245
|
+
context.addIssue({
|
|
246
|
+
code: z.ZodIssueCode.custom,
|
|
247
|
+
path: ["action"],
|
|
248
|
+
message: "Either keep or action is required"
|
|
249
|
+
});
|
|
250
|
+
}
|
|
211
251
|
}))
|
|
212
252
|
.min(1)
|
|
213
253
|
});
|
|
@@ -218,6 +258,14 @@ export const createWikiSpaceSchema = z.object({
|
|
|
218
258
|
ownerUserId: z.string().trim().nullable().optional(),
|
|
219
259
|
visibility: wikiSpaceVisibilitySchema.default("personal")
|
|
220
260
|
});
|
|
261
|
+
const wikiLlmReasoningEffortSchema = z.enum([
|
|
262
|
+
"none",
|
|
263
|
+
"low",
|
|
264
|
+
"medium",
|
|
265
|
+
"high",
|
|
266
|
+
"xhigh"
|
|
267
|
+
]);
|
|
268
|
+
const wikiLlmVerbositySchema = z.enum(["low", "medium", "high"]);
|
|
221
269
|
export const upsertWikiLlmProfileSchema = z.object({
|
|
222
270
|
id: z.string().trim().optional(),
|
|
223
271
|
label: z.string().trim().min(1),
|
|
@@ -226,9 +274,20 @@ export const upsertWikiLlmProfileSchema = z.object({
|
|
|
226
274
|
model: z.string().trim().min(1),
|
|
227
275
|
apiKey: z.string().trim().optional(),
|
|
228
276
|
systemPrompt: z.string().trim().default(""),
|
|
277
|
+
reasoningEffort: wikiLlmReasoningEffortSchema.optional(),
|
|
278
|
+
verbosity: wikiLlmVerbositySchema.optional(),
|
|
229
279
|
enabled: z.boolean().default(true),
|
|
230
280
|
metadata: z.record(z.string(), z.unknown()).default({})
|
|
231
281
|
});
|
|
282
|
+
export const testWikiLlmProfileSchema = z.object({
|
|
283
|
+
profileId: z.string().trim().optional(),
|
|
284
|
+
provider: z.string().trim().min(1).default("openai-responses"),
|
|
285
|
+
baseUrl: z.string().trim().default("https://api.openai.com/v1"),
|
|
286
|
+
model: z.string().trim().min(1),
|
|
287
|
+
apiKey: z.string().trim().optional(),
|
|
288
|
+
reasoningEffort: wikiLlmReasoningEffortSchema.optional(),
|
|
289
|
+
verbosity: wikiLlmVerbositySchema.optional()
|
|
290
|
+
});
|
|
232
291
|
export const upsertWikiEmbeddingProfileSchema = z.object({
|
|
233
292
|
id: z.string().trim().optional(),
|
|
234
293
|
label: z.string().trim().min(1),
|
|
@@ -390,6 +449,10 @@ function parseJsonStringArray(raw) {
|
|
|
390
449
|
return [];
|
|
391
450
|
}
|
|
392
451
|
}
|
|
452
|
+
function readStringRecordValue(record, key) {
|
|
453
|
+
const value = record[key];
|
|
454
|
+
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
|
455
|
+
}
|
|
393
456
|
function listLinkRowsForNotes(noteIds) {
|
|
394
457
|
if (noteIds.length === 0) {
|
|
395
458
|
return [];
|
|
@@ -497,6 +560,32 @@ function inferSummary(markdown) {
|
|
|
497
560
|
const plain = buildContentPlain(markdown).replace(/\s+/g, " ").trim();
|
|
498
561
|
return plain.slice(0, 240);
|
|
499
562
|
}
|
|
563
|
+
function stripLeadingHeading(markdown, title) {
|
|
564
|
+
const normalizedTitle = title.trim().toLowerCase();
|
|
565
|
+
const trimmed = markdown.trim();
|
|
566
|
+
const match = trimmed.match(/^#\s+(.+?)\n+/);
|
|
567
|
+
if (!match) {
|
|
568
|
+
return trimmed;
|
|
569
|
+
}
|
|
570
|
+
const heading = match[1]?.trim().toLowerCase() ?? "";
|
|
571
|
+
if (heading !== normalizedTitle) {
|
|
572
|
+
return trimmed;
|
|
573
|
+
}
|
|
574
|
+
return trimmed.slice(match[0].length).trim();
|
|
575
|
+
}
|
|
576
|
+
function mergeWikiPageContent(targetMarkdown, incoming) {
|
|
577
|
+
const mergedBody = stripLeadingHeading(incoming.markdown, incoming.title) ||
|
|
578
|
+
incoming.markdown.trim();
|
|
579
|
+
return [
|
|
580
|
+
targetMarkdown.trim(),
|
|
581
|
+
"",
|
|
582
|
+
`## ${incoming.title}`,
|
|
583
|
+
"",
|
|
584
|
+
mergedBody
|
|
585
|
+
]
|
|
586
|
+
.filter((part) => part.trim().length > 0)
|
|
587
|
+
.join("\n");
|
|
588
|
+
}
|
|
500
589
|
function slugify(value) {
|
|
501
590
|
const normalized = value
|
|
502
591
|
.toLowerCase()
|
|
@@ -1876,6 +1965,21 @@ export function upsertWikiLlmProfile(input, secrets) {
|
|
|
1876
1965
|
`wiki_llm_secret_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
|
|
1877
1966
|
storeEncryptedSecret(secretId, secrets.sealJson({ apiKey: parsed.apiKey.trim() }), `${parsed.label} wiki LLM profile`);
|
|
1878
1967
|
}
|
|
1968
|
+
const metadata = {
|
|
1969
|
+
...parsed.metadata
|
|
1970
|
+
};
|
|
1971
|
+
if (parsed.reasoningEffort) {
|
|
1972
|
+
metadata.reasoningEffort = parsed.reasoningEffort;
|
|
1973
|
+
}
|
|
1974
|
+
else {
|
|
1975
|
+
delete metadata.reasoningEffort;
|
|
1976
|
+
}
|
|
1977
|
+
if (parsed.verbosity) {
|
|
1978
|
+
metadata.verbosity = parsed.verbosity;
|
|
1979
|
+
}
|
|
1980
|
+
else {
|
|
1981
|
+
delete metadata.verbosity;
|
|
1982
|
+
}
|
|
1879
1983
|
getDatabase()
|
|
1880
1984
|
.prepare(`INSERT INTO wiki_llm_profiles (id, label, provider, base_url, model, secret_id, system_prompt, enabled, metadata_json, created_at, updated_at)
|
|
1881
1985
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
@@ -1889,7 +1993,7 @@ export function upsertWikiLlmProfile(input, secrets) {
|
|
|
1889
1993
|
enabled = excluded.enabled,
|
|
1890
1994
|
metadata_json = excluded.metadata_json,
|
|
1891
1995
|
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(
|
|
1996
|
+
.run(id, parsed.label, parsed.provider, parsed.baseUrl, parsed.model, secretId, parsed.systemPrompt, parsed.enabled ? 1 : 0, JSON.stringify(metadata), now, now);
|
|
1893
1997
|
return listWikiLlmProfiles().find((entry) => entry.id === id);
|
|
1894
1998
|
}
|
|
1895
1999
|
export function upsertWikiEmbeddingProfile(input, secrets) {
|
|
@@ -2088,11 +2192,103 @@ function updateWikiIngestJob(jobId, patch) {
|
|
|
2088
2192
|
.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
2193
|
return getWikiIngestJob(jobId);
|
|
2090
2194
|
}
|
|
2091
|
-
function createWikiIngestLog(jobId, message, level = "info", metadata = {}) {
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2195
|
+
function createWikiIngestLog(jobId, message, level = "info", metadata = {}, options = {}) {
|
|
2196
|
+
const createdAt = nowIso();
|
|
2197
|
+
const logMetadata = options.aggregateKey
|
|
2198
|
+
? { ...metadata, aggregateKey: options.aggregateKey }
|
|
2199
|
+
: metadata;
|
|
2200
|
+
const aggregateKey = options.aggregateKey?.trim() || null;
|
|
2201
|
+
if (aggregateKey) {
|
|
2202
|
+
const current = [...listWikiIngestJobLogsInternal(jobId)]
|
|
2203
|
+
.reverse()
|
|
2204
|
+
.find((entry) => {
|
|
2205
|
+
const parsed = parseJsonRecord(entry.metadata_json);
|
|
2206
|
+
return parsed.aggregateKey === aggregateKey;
|
|
2207
|
+
});
|
|
2208
|
+
if (current) {
|
|
2209
|
+
getDatabase()
|
|
2210
|
+
.prepare(`UPDATE wiki_ingest_job_logs
|
|
2211
|
+
SET level = ?, message = ?, metadata_json = ?, created_at = ?
|
|
2212
|
+
WHERE id = ?`)
|
|
2213
|
+
.run(level, message, JSON.stringify(logMetadata), createdAt, current.id);
|
|
2214
|
+
}
|
|
2215
|
+
else {
|
|
2216
|
+
getDatabase()
|
|
2217
|
+
.prepare(`INSERT INTO wiki_ingest_job_logs (id, job_id, level, message, metadata_json, created_at)
|
|
2218
|
+
VALUES (?, ?, ?, ?, ?, ?)`)
|
|
2219
|
+
.run(`wiki_ingest_log_${randomUUID().replaceAll("-", "").slice(0, 10)}`, jobId, level, message, JSON.stringify(logMetadata), createdAt);
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
else {
|
|
2223
|
+
getDatabase()
|
|
2224
|
+
.prepare(`INSERT INTO wiki_ingest_job_logs (id, job_id, level, message, metadata_json, created_at)
|
|
2225
|
+
VALUES (?, ?, ?, ?, ?, ?)`)
|
|
2226
|
+
.run(`wiki_ingest_log_${randomUUID().replaceAll("-", "").slice(0, 10)}`, jobId, level, message, JSON.stringify(logMetadata), createdAt);
|
|
2227
|
+
}
|
|
2228
|
+
if (options.recordDiagnostic !== false) {
|
|
2229
|
+
recordDiagnosticLog({
|
|
2230
|
+
level,
|
|
2231
|
+
source: "server",
|
|
2232
|
+
scope: typeof metadata.scope === "string" && metadata.scope.trim()
|
|
2233
|
+
? metadata.scope
|
|
2234
|
+
: "wiki_ingest",
|
|
2235
|
+
eventKey: typeof metadata.eventKey === "string" && metadata.eventKey.trim()
|
|
2236
|
+
? metadata.eventKey
|
|
2237
|
+
: "wiki_ingest_log",
|
|
2238
|
+
message,
|
|
2239
|
+
functionName: "createWikiIngestLog",
|
|
2240
|
+
entityType: "wiki_ingest_job",
|
|
2241
|
+
entityId: jobId,
|
|
2242
|
+
jobId,
|
|
2243
|
+
details: logMetadata
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
function findOpenAiResponseIdForJobAsset(input) {
|
|
2248
|
+
const normalizedFileName = input.fileName.trim().toLowerCase();
|
|
2249
|
+
const normalizedSourceLocator = input.sourceLocator.trim().toLowerCase();
|
|
2250
|
+
const normalizedChecksum = input.checksum.trim().toLowerCase();
|
|
2251
|
+
const sameNamedAssets = listWikiIngestJobAssetsInternal(input.jobId).filter((asset) => asset.file_name.trim().toLowerCase() === normalizedFileName).length;
|
|
2252
|
+
const logs = [...listWikiIngestJobLogsInternal(input.jobId)].reverse();
|
|
2253
|
+
for (const entry of logs) {
|
|
2254
|
+
const metadata = parseJsonRecord(entry.metadata_json);
|
|
2255
|
+
const responseId = readStringRecordValue(metadata, "responseId");
|
|
2256
|
+
if (!responseId) {
|
|
2257
|
+
continue;
|
|
2258
|
+
}
|
|
2259
|
+
const loggedAssetId = readStringRecordValue(metadata, "sourceAssetId") ??
|
|
2260
|
+
readStringRecordValue(metadata, "assetId");
|
|
2261
|
+
if (loggedAssetId) {
|
|
2262
|
+
if (loggedAssetId === input.assetId) {
|
|
2263
|
+
return responseId;
|
|
2264
|
+
}
|
|
2265
|
+
continue;
|
|
2266
|
+
}
|
|
2267
|
+
const loggedSourceLocator = readStringRecordValue(metadata, "sourceLocator");
|
|
2268
|
+
if (loggedSourceLocator) {
|
|
2269
|
+
if (loggedSourceLocator.trim().toLowerCase() === normalizedSourceLocator) {
|
|
2270
|
+
return responseId;
|
|
2271
|
+
}
|
|
2272
|
+
continue;
|
|
2273
|
+
}
|
|
2274
|
+
const loggedChecksum = readStringRecordValue(metadata, "checksum");
|
|
2275
|
+
if (loggedChecksum) {
|
|
2276
|
+
if (loggedChecksum.trim().toLowerCase() === normalizedChecksum) {
|
|
2277
|
+
return responseId;
|
|
2278
|
+
}
|
|
2279
|
+
continue;
|
|
2280
|
+
}
|
|
2281
|
+
const loggedFileName = readStringRecordValue(metadata, "currentFileName") ??
|
|
2282
|
+
readStringRecordValue(metadata, "fileName");
|
|
2283
|
+
if (!loggedFileName) {
|
|
2284
|
+
return responseId;
|
|
2285
|
+
}
|
|
2286
|
+
if (sameNamedAssets === 1 &&
|
|
2287
|
+
loggedFileName.trim().toLowerCase() === normalizedFileName) {
|
|
2288
|
+
return responseId;
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
return null;
|
|
2096
2292
|
}
|
|
2097
2293
|
function createWikiIngestAssetRecord(input) {
|
|
2098
2294
|
const id = `wiki_ingest_asset_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
|
|
@@ -2121,6 +2317,15 @@ function updateWikiIngestAsset(assetId, patch) {
|
|
|
2121
2317
|
.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
2318
|
}
|
|
2123
2319
|
function createWikiIngestCandidate(input) {
|
|
2320
|
+
const existing = listWikiIngestCandidatesInternal(input.jobId).find((candidate) => candidate.source_asset_id === (input.sourceAssetId ?? null) &&
|
|
2321
|
+
candidate.candidate_type === input.candidateType &&
|
|
2322
|
+
candidate.title === (input.title ?? "") &&
|
|
2323
|
+
candidate.summary === (input.summary ?? "") &&
|
|
2324
|
+
candidate.target_key === (input.targetKey ?? "") &&
|
|
2325
|
+
candidate.payload_json === JSON.stringify(input.payload));
|
|
2326
|
+
if (existing) {
|
|
2327
|
+
return existing.id;
|
|
2328
|
+
}
|
|
2124
2329
|
const id = `wiki_ingest_candidate_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
|
|
2125
2330
|
const now = nowIso();
|
|
2126
2331
|
getDatabase()
|
|
@@ -2338,8 +2543,13 @@ function createPageCandidatePayload(input) {
|
|
|
2338
2543
|
summary: input.summary,
|
|
2339
2544
|
contentMarkdown: input.markdown,
|
|
2340
2545
|
kind: input.createAsKind,
|
|
2546
|
+
aliases: normalizeTags(input.aliases),
|
|
2547
|
+
parentSlug: typeof input.parentSlug === "string" && input.parentSlug.trim().length > 0
|
|
2548
|
+
? input.parentSlug.trim()
|
|
2549
|
+
: null,
|
|
2341
2550
|
sourceLocator: input.sourceLocator,
|
|
2342
|
-
links: input.linkedEntityHints
|
|
2551
|
+
links: input.linkedEntityHints,
|
|
2552
|
+
tags: normalizeTags(input.tags)
|
|
2343
2553
|
};
|
|
2344
2554
|
}
|
|
2345
2555
|
export async function processWikiIngestJob(jobId, options) {
|
|
@@ -2355,12 +2565,114 @@ export async function processWikiIngestJob(jobId, options) {
|
|
|
2355
2565
|
? (listWikiLlmProfiles().find((entry) => entry.id === parsed.llmProfileId) ?? null)
|
|
2356
2566
|
: null;
|
|
2357
2567
|
const space = getWikiSpaceById(job.space_id) ?? ensureSharedWikiSpace();
|
|
2568
|
+
if (parsed.llmProfileId && !llmProfile) {
|
|
2569
|
+
updateWikiIngestJob(jobId, {
|
|
2570
|
+
status: "failed",
|
|
2571
|
+
phase: "failed",
|
|
2572
|
+
latestMessage: "The selected LLM profile could not be found.",
|
|
2573
|
+
errorMessage: "The selected LLM profile could not be found.",
|
|
2574
|
+
completedAt: nowIso()
|
|
2575
|
+
});
|
|
2576
|
+
createWikiIngestLog(jobId, "The selected LLM profile could not be found.", "error");
|
|
2577
|
+
return getWikiIngestJob(jobId);
|
|
2578
|
+
}
|
|
2358
2579
|
updateWikiIngestJob(jobId, {
|
|
2359
2580
|
status: "processing",
|
|
2360
2581
|
phase: "processing",
|
|
2361
|
-
latestMessage: "Processing queued wiki ingest sources."
|
|
2582
|
+
latestMessage: "Processing queued wiki ingest sources.",
|
|
2583
|
+
errorMessage: "",
|
|
2584
|
+
completedAt: null
|
|
2362
2585
|
});
|
|
2363
2586
|
createWikiIngestLog(jobId, "Started background wiki ingestion.");
|
|
2587
|
+
let currentAssetContext = null;
|
|
2588
|
+
let currentPollCount = 0;
|
|
2589
|
+
const updateCurrentAssetMetadata = (patch) => {
|
|
2590
|
+
if (!currentAssetContext) {
|
|
2591
|
+
return;
|
|
2592
|
+
}
|
|
2593
|
+
const asset = listWikiIngestJobAssetsInternal(jobId).find((entry) => entry.id === currentAssetContext?.assetId);
|
|
2594
|
+
if (!asset) {
|
|
2595
|
+
return;
|
|
2596
|
+
}
|
|
2597
|
+
updateWikiIngestAsset(asset.id, {
|
|
2598
|
+
metadata: {
|
|
2599
|
+
...parseJsonRecord(asset.metadata_json),
|
|
2600
|
+
...patch
|
|
2601
|
+
}
|
|
2602
|
+
});
|
|
2603
|
+
};
|
|
2604
|
+
const llmDiagnosticLogger = ({ level, message, details = {} }) => {
|
|
2605
|
+
const enrichedDetails = {
|
|
2606
|
+
...details,
|
|
2607
|
+
sourceAssetId: currentAssetContext?.assetId ?? null,
|
|
2608
|
+
currentFileName: currentAssetContext?.fileName ?? null,
|
|
2609
|
+
sourceLocator: currentAssetContext?.sourceLocator ?? null,
|
|
2610
|
+
checksum: currentAssetContext?.checksum ?? null,
|
|
2611
|
+
currentFileIndex: currentAssetContext?.fileIndex ?? null,
|
|
2612
|
+
currentFileTotal: currentAssetContext?.totalFiles ?? null,
|
|
2613
|
+
chunkIndex: typeof details.chunkIndex === "number"
|
|
2614
|
+
? details.chunkIndex
|
|
2615
|
+
: currentAssetContext?.chunkIndex ?? null,
|
|
2616
|
+
chunkCount: typeof details.chunkCount === "number"
|
|
2617
|
+
? details.chunkCount
|
|
2618
|
+
: currentAssetContext?.chunkCount ?? null
|
|
2619
|
+
};
|
|
2620
|
+
const eventKey = typeof details.eventKey === "string" ? details.eventKey : "";
|
|
2621
|
+
if (eventKey === "llm_compile_background_started") {
|
|
2622
|
+
currentPollCount = 0;
|
|
2623
|
+
updateCurrentAssetMetadata({
|
|
2624
|
+
openAiResponseId: typeof details.responseId === "string" ? details.responseId : null,
|
|
2625
|
+
openAiResponseStatus: typeof details.status === "string" ? details.status : "queued",
|
|
2626
|
+
openAiLastPolledAt: nowIso()
|
|
2627
|
+
});
|
|
2628
|
+
}
|
|
2629
|
+
if (eventKey === "llm_compile_background_polled") {
|
|
2630
|
+
currentPollCount += 1;
|
|
2631
|
+
const pollStatus = typeof details.status === "string" ? details.status : "in_progress";
|
|
2632
|
+
const chunkSuffix = (currentAssetContext?.chunkCount ?? 1) > 1
|
|
2633
|
+
? ` · chunk ${currentAssetContext?.chunkIndex ?? 1}/${currentAssetContext?.chunkCount ?? 1}`
|
|
2634
|
+
: "";
|
|
2635
|
+
const fileLabel = currentAssetContext?.fileName ?? "current source";
|
|
2636
|
+
const progressMessage = `Waiting for OpenAI on ${fileLabel}${chunkSuffix}. Poll ${currentPollCount} · ${pollStatus}.`;
|
|
2637
|
+
updateWikiIngestJob(jobId, {
|
|
2638
|
+
latestMessage: progressMessage
|
|
2639
|
+
});
|
|
2640
|
+
updateCurrentAssetMetadata({
|
|
2641
|
+
openAiResponseId: typeof details.responseId === "string" ? details.responseId : null,
|
|
2642
|
+
openAiResponseStatus: pollStatus,
|
|
2643
|
+
openAiLastPolledAt: nowIso(),
|
|
2644
|
+
openAiPollCount: currentPollCount
|
|
2645
|
+
});
|
|
2646
|
+
createWikiIngestLog(jobId, progressMessage, "info", {
|
|
2647
|
+
...enrichedDetails,
|
|
2648
|
+
pollCount: currentPollCount,
|
|
2649
|
+
status: pollStatus
|
|
2650
|
+
}, {
|
|
2651
|
+
aggregateKey: `llm_compile_background_polled:${currentAssetContext?.assetId ?? "job"}`,
|
|
2652
|
+
recordDiagnostic: false
|
|
2653
|
+
});
|
|
2654
|
+
return;
|
|
2655
|
+
}
|
|
2656
|
+
if (eventKey === "llm_compile_success" ||
|
|
2657
|
+
eventKey === "llm_compile_unparseable" ||
|
|
2658
|
+
eventKey === "llm_compile_background_terminal_error") {
|
|
2659
|
+
updateCurrentAssetMetadata({
|
|
2660
|
+
openAiResponseId: typeof details.responseId === "string" ? details.responseId : null,
|
|
2661
|
+
openAiResponseStatus: eventKey === "llm_compile_background_terminal_error"
|
|
2662
|
+
? typeof details.status === "string"
|
|
2663
|
+
? details.status
|
|
2664
|
+
: "failed"
|
|
2665
|
+
: "completed",
|
|
2666
|
+
openAiLastPolledAt: nowIso(),
|
|
2667
|
+
openAiPollCount: currentPollCount
|
|
2668
|
+
});
|
|
2669
|
+
}
|
|
2670
|
+
createWikiIngestLog(jobId, message, level === "debug" ? "info" : level, {
|
|
2671
|
+
scope: "wiki_llm",
|
|
2672
|
+
eventKey: "wiki_llm_event",
|
|
2673
|
+
...enrichedDetails
|
|
2674
|
+
});
|
|
2675
|
+
};
|
|
2364
2676
|
const refreshCounts = () => {
|
|
2365
2677
|
const candidates = listWikiIngestCandidatesInternal(jobId);
|
|
2366
2678
|
const pageCount = candidates.filter((candidate) => candidate.candidate_type === "page").length;
|
|
@@ -2368,11 +2680,12 @@ export async function processWikiIngestJob(jobId, options) {
|
|
|
2368
2680
|
return { pageCount, entityCount };
|
|
2369
2681
|
};
|
|
2370
2682
|
const assetQueue = () => listWikiIngestJobAssetsInternal(jobId).filter((asset) => ["queued", "processing"].includes(asset.status));
|
|
2371
|
-
|
|
2372
|
-
let
|
|
2683
|
+
const initialAssets = listWikiIngestJobAssetsInternal(jobId);
|
|
2684
|
+
let processedFiles = initialAssets.filter((asset) => asset.status === "completed").length;
|
|
2685
|
+
let totalFiles = Math.max(job.total_files, initialAssets.length);
|
|
2373
2686
|
let hadSuccess = false;
|
|
2374
2687
|
while (assetQueue().length > 0) {
|
|
2375
|
-
const nextAsset = assetQueue().find((asset) =>
|
|
2688
|
+
const nextAsset = assetQueue().find((asset) => ["processing", "queued"].includes(asset.status));
|
|
2376
2689
|
if (!nextAsset) {
|
|
2377
2690
|
break;
|
|
2378
2691
|
}
|
|
@@ -2433,7 +2746,27 @@ export async function processWikiIngestJob(jobId, options) {
|
|
|
2433
2746
|
continue;
|
|
2434
2747
|
}
|
|
2435
2748
|
updateWikiIngestAsset(nextAsset.id, { status: "processing" });
|
|
2436
|
-
|
|
2749
|
+
currentAssetContext = {
|
|
2750
|
+
assetId: nextAsset.id,
|
|
2751
|
+
fileName: nextAsset.file_name || nextAsset.source_locator || "Source",
|
|
2752
|
+
sourceLocator: nextAsset.source_locator,
|
|
2753
|
+
checksum: nextAsset.checksum,
|
|
2754
|
+
fileIndex: Math.min(totalFiles, processedFiles + 1),
|
|
2755
|
+
totalFiles,
|
|
2756
|
+
chunkIndex: 1,
|
|
2757
|
+
chunkCount: 1
|
|
2758
|
+
};
|
|
2759
|
+
currentPollCount = 0;
|
|
2760
|
+
createWikiIngestLog(jobId, `Processing ${currentAssetContext.fileName} (${currentAssetContext.fileIndex}/${currentAssetContext.totalFiles}).`, "info", {
|
|
2761
|
+
sourceAssetId: currentAssetContext.assetId,
|
|
2762
|
+
fileName: currentAssetContext.fileName,
|
|
2763
|
+
sourceLocator: currentAssetContext.sourceLocator,
|
|
2764
|
+
checksum: currentAssetContext.checksum,
|
|
2765
|
+
fileIndex: currentAssetContext.fileIndex,
|
|
2766
|
+
totalFiles: currentAssetContext.totalFiles,
|
|
2767
|
+
chunkIndex: currentAssetContext.chunkIndex,
|
|
2768
|
+
chunkCount: currentAssetContext.chunkCount
|
|
2769
|
+
});
|
|
2437
2770
|
try {
|
|
2438
2771
|
const fetched = nextAsset.source_kind === "url"
|
|
2439
2772
|
? await getFetchedContent("url", {
|
|
@@ -2451,6 +2784,15 @@ export async function processWikiIngestJob(jobId, options) {
|
|
|
2451
2784
|
jobId,
|
|
2452
2785
|
fetched
|
|
2453
2786
|
});
|
|
2787
|
+
const existingAssetMetadata = parseJsonRecord(nextAsset.metadata_json);
|
|
2788
|
+
const resumeResponseId = readStringRecordValue(existingAssetMetadata, "openAiResponseId") ??
|
|
2789
|
+
findOpenAiResponseIdForJobAsset({
|
|
2790
|
+
jobId,
|
|
2791
|
+
assetId: nextAsset.id,
|
|
2792
|
+
fileName: nextAsset.file_name,
|
|
2793
|
+
sourceLocator: nextAsset.source_locator,
|
|
2794
|
+
checksum: nextAsset.checksum
|
|
2795
|
+
});
|
|
2454
2796
|
const compiled = llmProfile && parsed.parseStrategy !== "text_only"
|
|
2455
2797
|
? await options.llm.compileWikiIngest(llmProfile, {
|
|
2456
2798
|
titleHint: parsed.titleHint,
|
|
@@ -2458,7 +2800,9 @@ export async function processWikiIngestJob(jobId, options) {
|
|
|
2458
2800
|
binary: fetched.binary,
|
|
2459
2801
|
mimeType: fetched.mimeType,
|
|
2460
2802
|
parseStrategy: parsed.parseStrategy
|
|
2461
|
-
}
|
|
2803
|
+
}, {
|
|
2804
|
+
resumeResponseId
|
|
2805
|
+
}, llmDiagnosticLogger)
|
|
2462
2806
|
: llmProfile && fetched.contentText
|
|
2463
2807
|
? await options.llm.compileWikiIngest(llmProfile, {
|
|
2464
2808
|
titleHint: parsed.titleHint,
|
|
@@ -2466,8 +2810,13 @@ export async function processWikiIngestJob(jobId, options) {
|
|
|
2466
2810
|
binary: fetched.binary,
|
|
2467
2811
|
mimeType: fetched.mimeType,
|
|
2468
2812
|
parseStrategy: "text_only"
|
|
2469
|
-
}
|
|
2813
|
+
}, {
|
|
2814
|
+
resumeResponseId
|
|
2815
|
+
}, llmDiagnosticLogger)
|
|
2470
2816
|
: null;
|
|
2817
|
+
if (llmProfile && !compiled) {
|
|
2818
|
+
throw new Error("The LLM did not produce structured draft candidates. Check the OpenAI settings and try again.");
|
|
2819
|
+
}
|
|
2471
2820
|
const title = compiled?.title ||
|
|
2472
2821
|
parsed.titleHint ||
|
|
2473
2822
|
inferTitle(fetched.contentText || fetched.fileName, "Imported source");
|
|
@@ -2491,6 +2840,7 @@ export async function processWikiIngestJob(jobId, options) {
|
|
|
2491
2840
|
markdown,
|
|
2492
2841
|
sourceLocator: fetched.locator,
|
|
2493
2842
|
createAsKind: parsed.createAsKind,
|
|
2843
|
+
tags: compiled?.tags,
|
|
2494
2844
|
linkedEntityHints: parsed.linkedEntityHints
|
|
2495
2845
|
})
|
|
2496
2846
|
});
|
|
@@ -2551,11 +2901,23 @@ export async function processWikiIngestJob(jobId, options) {
|
|
|
2551
2901
|
payload: createPageCandidatePayload({
|
|
2552
2902
|
title: articleTitle,
|
|
2553
2903
|
summary: articleSummary,
|
|
2554
|
-
markdown:
|
|
2555
|
-
|
|
2556
|
-
|
|
2904
|
+
markdown: typeof candidate.markdown === "string" &&
|
|
2905
|
+
candidate.markdown.trim().length > 0
|
|
2906
|
+
? candidate.markdown
|
|
2907
|
+
: `# ${articleTitle}\n\n${articleSummary}\n\n${articleRationale
|
|
2908
|
+
? `## Why this page\n\n${articleRationale}\n`
|
|
2909
|
+
: ""}`,
|
|
2557
2910
|
sourceLocator: fetched.locator,
|
|
2558
2911
|
createAsKind: "wiki",
|
|
2912
|
+
aliases: Array.isArray(candidate.aliases)
|
|
2913
|
+
? candidate.aliases.filter((alias) => typeof alias === "string")
|
|
2914
|
+
: [],
|
|
2915
|
+
parentSlug: typeof candidate.parentSlug === "string"
|
|
2916
|
+
? candidate.parentSlug
|
|
2917
|
+
: null,
|
|
2918
|
+
tags: Array.isArray(candidate.tags)
|
|
2919
|
+
? candidate.tags.filter((tag) => typeof tag === "string")
|
|
2920
|
+
: [],
|
|
2559
2921
|
linkedEntityHints: parsed.linkedEntityHints
|
|
2560
2922
|
})
|
|
2561
2923
|
});
|
|
@@ -2568,7 +2930,9 @@ export async function processWikiIngestJob(jobId, options) {
|
|
|
2568
2930
|
checksum: rawSource.checksum,
|
|
2569
2931
|
metadata: {
|
|
2570
2932
|
...(parseJsonRecord(nextAsset.metadata_json) ?? {}),
|
|
2571
|
-
rawSourcePath: rawSource.filePath
|
|
2933
|
+
rawSourcePath: rawSource.filePath,
|
|
2934
|
+
openAiResponseStatus: llmProfile ? "completed" : null,
|
|
2935
|
+
openAiLastPolledAt: llmProfile ? nowIso() : null
|
|
2572
2936
|
}
|
|
2573
2937
|
});
|
|
2574
2938
|
hadSuccess = true;
|
|
@@ -2586,20 +2950,38 @@ export async function processWikiIngestJob(jobId, options) {
|
|
|
2586
2950
|
latestMessage: `Prepared candidates from ${fetched.fileName}.`
|
|
2587
2951
|
});
|
|
2588
2952
|
createWikiIngestLog(jobId, `Prepared candidates from ${fetched.fileName}.`, "info", {
|
|
2953
|
+
fileName: currentAssetContext?.fileName ?? fetched.fileName,
|
|
2954
|
+
sourceAssetId: currentAssetContext?.assetId ?? null,
|
|
2955
|
+
fileIndex: currentAssetContext?.fileIndex ?? processedFiles,
|
|
2956
|
+
totalFiles: currentAssetContext?.totalFiles ?? totalFiles,
|
|
2589
2957
|
pageCandidates: counts.pageCount,
|
|
2590
2958
|
entityCandidates: counts.entityCount
|
|
2591
2959
|
});
|
|
2960
|
+
currentAssetContext = null;
|
|
2961
|
+
currentPollCount = 0;
|
|
2592
2962
|
}
|
|
2593
2963
|
catch (error) {
|
|
2594
2964
|
processedFiles += 1;
|
|
2595
2965
|
updateWikiIngestAsset(nextAsset.id, { status: "failed" });
|
|
2966
|
+
updateCurrentAssetMetadata({
|
|
2967
|
+
openAiResponseStatus: "failed",
|
|
2968
|
+
openAiLastPolledAt: nowIso(),
|
|
2969
|
+
openAiPollCount: currentPollCount
|
|
2970
|
+
});
|
|
2596
2971
|
updateWikiIngestJob(jobId, {
|
|
2597
2972
|
processedFiles,
|
|
2598
2973
|
totalFiles,
|
|
2599
2974
|
progressPercent: calculateProgress(totalFiles, processedFiles),
|
|
2600
2975
|
latestMessage: error instanceof Error ? error.message : "Source ingest failed."
|
|
2601
2976
|
});
|
|
2602
|
-
createWikiIngestLog(jobId, error instanceof Error ? error.message : "Source ingest failed.", "error"
|
|
2977
|
+
createWikiIngestLog(jobId, error instanceof Error ? error.message : "Source ingest failed.", "error", {
|
|
2978
|
+
fileName: currentAssetContext?.fileName ?? null,
|
|
2979
|
+
fileIndex: currentAssetContext?.fileIndex ?? null,
|
|
2980
|
+
totalFiles: currentAssetContext?.totalFiles ?? totalFiles,
|
|
2981
|
+
pollCount: currentPollCount
|
|
2982
|
+
});
|
|
2983
|
+
currentAssetContext = null;
|
|
2984
|
+
currentPollCount = 0;
|
|
2603
2985
|
}
|
|
2604
2986
|
}
|
|
2605
2987
|
const counts = refreshCounts();
|
|
@@ -2635,6 +3017,94 @@ export function listWikiIngestJobs(input = {}) {
|
|
|
2635
3017
|
.map((row) => getWikiIngestJob(row.id))
|
|
2636
3018
|
.filter((job) => job !== null);
|
|
2637
3019
|
}
|
|
3020
|
+
export async function rerunWikiIngestJob(jobId, options = {}) {
|
|
3021
|
+
const job = readWikiIngestJobRow(jobId);
|
|
3022
|
+
if (!job) {
|
|
3023
|
+
return null;
|
|
3024
|
+
}
|
|
3025
|
+
if (["queued", "processing"].includes(job.status)) {
|
|
3026
|
+
throw new Error("Wiki ingest jobs can only be rerun after processing ends.");
|
|
3027
|
+
}
|
|
3028
|
+
const ingestInput = readWikiIngestInput(jobId);
|
|
3029
|
+
if (!ingestInput) {
|
|
3030
|
+
throw new Error("Wiki ingest input could not be restored.");
|
|
3031
|
+
}
|
|
3032
|
+
const rootAssets = listWikiIngestJobAssetsInternal(jobId).filter((asset) => {
|
|
3033
|
+
const metadata = parseJsonRecord(asset.metadata_json);
|
|
3034
|
+
return !metadata?.parentAssetId && asset.source_kind !== "url";
|
|
3035
|
+
});
|
|
3036
|
+
const replayResult = rootAssets.length > 0
|
|
3037
|
+
? await createUploadedWikiIngestJob({
|
|
3038
|
+
spaceId: ingestInput.spaceId,
|
|
3039
|
+
titleHint: ingestInput.titleHint,
|
|
3040
|
+
llmProfileId: ingestInput.llmProfileId,
|
|
3041
|
+
parseStrategy: ingestInput.parseStrategy,
|
|
3042
|
+
entityProposalMode: ingestInput.entityProposalMode,
|
|
3043
|
+
userId: ingestInput.userId ?? null,
|
|
3044
|
+
createAsKind: ingestInput.createAsKind,
|
|
3045
|
+
linkedEntityHints: ingestInput.linkedEntityHints
|
|
3046
|
+
}, await Promise.all(rootAssets.map(async (asset) => ({
|
|
3047
|
+
fileName: asset.file_name,
|
|
3048
|
+
mimeType: asset.mime_type,
|
|
3049
|
+
payload: await readFile(asset.file_path)
|
|
3050
|
+
}))), {
|
|
3051
|
+
actor: options.actor ?? job.created_by_actor ?? null
|
|
3052
|
+
})
|
|
3053
|
+
: await ingestWikiSource(ingestInput, {
|
|
3054
|
+
actor: options.actor ?? job.created_by_actor ?? null
|
|
3055
|
+
});
|
|
3056
|
+
const nextJobId = replayResult.job?.job.id ?? null;
|
|
3057
|
+
if (nextJobId) {
|
|
3058
|
+
createWikiIngestLog(nextJobId, `Reran ingest from ${jobId}.`, "info", {
|
|
3059
|
+
scope: "wiki_ingest",
|
|
3060
|
+
eventKey: "wiki_ingest_rerun",
|
|
3061
|
+
sourceJobId: jobId
|
|
3062
|
+
});
|
|
3063
|
+
createWikiIngestLog(jobId, `Created rerun job ${nextJobId}.`, "info", {
|
|
3064
|
+
scope: "wiki_ingest",
|
|
3065
|
+
eventKey: "wiki_ingest_rerun_requested",
|
|
3066
|
+
rerunJobId: nextJobId
|
|
3067
|
+
});
|
|
3068
|
+
}
|
|
3069
|
+
return replayResult;
|
|
3070
|
+
}
|
|
3071
|
+
export function deleteWikiIngestJob(jobId) {
|
|
3072
|
+
const job = readWikiIngestJobRow(jobId);
|
|
3073
|
+
if (!job) {
|
|
3074
|
+
return null;
|
|
3075
|
+
}
|
|
3076
|
+
if (["queued", "processing"].includes(job.status)) {
|
|
3077
|
+
throw new Error("Wiki ingest jobs can only be deleted after processing ends.");
|
|
3078
|
+
}
|
|
3079
|
+
const candidates = listWikiIngestCandidatesInternal(jobId);
|
|
3080
|
+
const assets = listWikiIngestJobAssetsInternal(jobId);
|
|
3081
|
+
const preservedAssetIds = new Set(candidates
|
|
3082
|
+
.filter((candidate) => candidate.status === "applied")
|
|
3083
|
+
.map((candidate) => candidate.source_asset_id)
|
|
3084
|
+
.filter((assetId) => typeof assetId === "string"));
|
|
3085
|
+
for (const asset of assets) {
|
|
3086
|
+
if (preservedAssetIds.has(asset.id)) {
|
|
3087
|
+
continue;
|
|
3088
|
+
}
|
|
3089
|
+
const metadata = parseJsonRecord(asset.metadata_json);
|
|
3090
|
+
const rawSourcePath = typeof metadata?.rawSourcePath === "string"
|
|
3091
|
+
? metadata.rawSourcePath
|
|
3092
|
+
: null;
|
|
3093
|
+
if (rawSourcePath && existsSync(rawSourcePath)) {
|
|
3094
|
+
unlinkSync(rawSourcePath);
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
const jobDir = getWikiIngestJobDir(jobId);
|
|
3098
|
+
if (existsSync(jobDir)) {
|
|
3099
|
+
rmSync(jobDir, { recursive: true, force: true });
|
|
3100
|
+
}
|
|
3101
|
+
getDatabase()
|
|
3102
|
+
.prepare(`DELETE FROM wiki_ingest_jobs WHERE id = ?`)
|
|
3103
|
+
.run(jobId);
|
|
3104
|
+
return {
|
|
3105
|
+
id: jobId
|
|
3106
|
+
};
|
|
3107
|
+
}
|
|
2638
3108
|
export async function reviewWikiIngestJob(jobId, input, options) {
|
|
2639
3109
|
const parsed = reviewWikiIngestJobSchema.parse(input);
|
|
2640
3110
|
const job = readWikiIngestJobRow(jobId);
|
|
@@ -2647,6 +3117,7 @@ export async function reviewWikiIngestJob(jobId, input, options) {
|
|
|
2647
3117
|
}
|
|
2648
3118
|
const candidates = listWikiIngestCandidatesInternal(jobId);
|
|
2649
3119
|
let acceptedCount = 0;
|
|
3120
|
+
let mappedCount = 0;
|
|
2650
3121
|
let rejectedCount = 0;
|
|
2651
3122
|
let firstPublishedPageId = null;
|
|
2652
3123
|
for (const decision of parsed.decisions) {
|
|
@@ -2654,7 +3125,9 @@ export async function reviewWikiIngestJob(jobId, input, options) {
|
|
|
2654
3125
|
if (!candidate) {
|
|
2655
3126
|
continue;
|
|
2656
3127
|
}
|
|
2657
|
-
|
|
3128
|
+
const action = decision.action ??
|
|
3129
|
+
(decision.keep === false ? "discard" : "keep");
|
|
3130
|
+
if (action === "discard") {
|
|
2658
3131
|
rejectedCount += 1;
|
|
2659
3132
|
updateWikiIngestCandidate(candidate.id, { status: "rejected" });
|
|
2660
3133
|
continue;
|
|
@@ -2662,36 +3135,77 @@ export async function reviewWikiIngestJob(jobId, input, options) {
|
|
|
2662
3135
|
try {
|
|
2663
3136
|
const payload = parseJsonRecord(candidate.payload_json);
|
|
2664
3137
|
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"
|
|
3138
|
+
if (action === "merge_existing") {
|
|
3139
|
+
const target = decision.targetNoteId
|
|
3140
|
+
? (getWikiPageDetail(decision.targetNoteId)?.page ?? null)
|
|
3141
|
+
: null;
|
|
3142
|
+
if (!target || target.spaceId !== job.space_id) {
|
|
3143
|
+
throw new Error("Merge target page was not found.");
|
|
3144
|
+
}
|
|
3145
|
+
const incomingTitle = typeof payload.title === "string" ? payload.title : candidate.title;
|
|
3146
|
+
const incomingMarkdown = typeof payload.contentMarkdown === "string"
|
|
2679
3147
|
? payload.contentMarkdown
|
|
2680
|
-
: `# ${candidate.title}\n
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
3148
|
+
: `# ${candidate.title}\n`;
|
|
3149
|
+
const mergedContentMarkdown = mergeWikiPageContent(target.contentMarkdown, {
|
|
3150
|
+
title: incomingTitle,
|
|
3151
|
+
markdown: incomingMarkdown
|
|
3152
|
+
});
|
|
3153
|
+
const mergedSummary = inferSummary(mergedContentMarkdown);
|
|
3154
|
+
const updated = options.updateNote(target.id, {
|
|
3155
|
+
contentMarkdown: mergedContentMarkdown,
|
|
3156
|
+
summary: mergedSummary
|
|
3157
|
+
});
|
|
3158
|
+
if (!updated) {
|
|
3159
|
+
throw new Error("Merge target page could not be updated.");
|
|
3160
|
+
}
|
|
3161
|
+
acceptedCount += 1;
|
|
3162
|
+
firstPublishedPageId = firstPublishedPageId ?? updated.id;
|
|
3163
|
+
updateWikiIngestCandidate(candidate.id, {
|
|
3164
|
+
status: "applied",
|
|
3165
|
+
publishedNoteId: updated.id
|
|
3166
|
+
});
|
|
3167
|
+
}
|
|
3168
|
+
else {
|
|
3169
|
+
const note = options.createNote({
|
|
3170
|
+
kind: payload.kind === "evidence" ? "evidence" : "wiki",
|
|
3171
|
+
title: typeof payload.title === "string"
|
|
3172
|
+
? payload.title
|
|
3173
|
+
: candidate.title,
|
|
3174
|
+
slug: "",
|
|
3175
|
+
indexOrder: 0,
|
|
3176
|
+
aliases: Array.isArray(payload.aliases)
|
|
3177
|
+
? payload.aliases.filter((alias) => typeof alias === "string")
|
|
3178
|
+
: [],
|
|
3179
|
+
summary: typeof payload.summary === "string"
|
|
3180
|
+
? payload.summary
|
|
3181
|
+
: candidate.summary,
|
|
3182
|
+
sourcePath: "",
|
|
3183
|
+
frontmatter: {},
|
|
3184
|
+
revisionHash: "",
|
|
3185
|
+
spaceId: job.space_id,
|
|
3186
|
+
parentSlug: typeof payload.parentSlug === "string"
|
|
3187
|
+
? payload.parentSlug
|
|
3188
|
+
: null,
|
|
3189
|
+
contentMarkdown: typeof payload.contentMarkdown === "string"
|
|
3190
|
+
? payload.contentMarkdown
|
|
3191
|
+
: `# ${candidate.title}\n`,
|
|
3192
|
+
author: ingestInput.userId ?? null,
|
|
3193
|
+
links: Array.isArray(payload.links)
|
|
3194
|
+
? payload.links
|
|
3195
|
+
: ingestInput.linkedEntityHints,
|
|
3196
|
+
tags: Array.isArray(payload.tags)
|
|
3197
|
+
? payload.tags.filter((tag) => typeof tag === "string")
|
|
3198
|
+
: [],
|
|
3199
|
+
destroyAt: null,
|
|
3200
|
+
userId: ingestInput.userId ?? null
|
|
3201
|
+
});
|
|
3202
|
+
acceptedCount += 1;
|
|
3203
|
+
firstPublishedPageId = firstPublishedPageId ?? note.id;
|
|
3204
|
+
updateWikiIngestCandidate(candidate.id, {
|
|
3205
|
+
status: "applied",
|
|
3206
|
+
publishedNoteId: note.id
|
|
3207
|
+
});
|
|
3208
|
+
}
|
|
2695
3209
|
}
|
|
2696
3210
|
else if (candidate.candidate_type === "page_update") {
|
|
2697
3211
|
const targetSlug = typeof payload.targetSlug === "string"
|
|
@@ -2723,13 +3237,32 @@ export async function reviewWikiIngestJob(jobId, input, options) {
|
|
|
2723
3237
|
}
|
|
2724
3238
|
}
|
|
2725
3239
|
else if (candidate.candidate_type === "entity") {
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
3240
|
+
if (action === "map_existing") {
|
|
3241
|
+
const proposedType = typeof payload.entityType === "string" ? payload.entityType : "";
|
|
3242
|
+
if (decision.mappedEntityType !== proposedType) {
|
|
3243
|
+
throw new Error("Mapped entity type must match the proposed entity type.");
|
|
3244
|
+
}
|
|
3245
|
+
const mapped = options.resolveMappedEntity(decision.mappedEntityType, decision.mappedEntityId);
|
|
3246
|
+
if (!mapped) {
|
|
3247
|
+
throw new Error("Mapped entity was not found.");
|
|
3248
|
+
}
|
|
3249
|
+
acceptedCount += 1;
|
|
3250
|
+
mappedCount += 1;
|
|
3251
|
+
updateWikiIngestCandidate(candidate.id, {
|
|
3252
|
+
status: "applied",
|
|
3253
|
+
publishedEntityType: mapped.entityType,
|
|
3254
|
+
publishedEntityId: mapped.entityId
|
|
3255
|
+
});
|
|
3256
|
+
}
|
|
3257
|
+
else {
|
|
3258
|
+
const result = options.publishEntity(payload);
|
|
3259
|
+
acceptedCount += 1;
|
|
3260
|
+
updateWikiIngestCandidate(candidate.id, {
|
|
3261
|
+
status: "applied",
|
|
3262
|
+
publishedEntityType: result.entityType,
|
|
3263
|
+
publishedEntityId: result.entityId
|
|
3264
|
+
});
|
|
3265
|
+
}
|
|
2733
3266
|
}
|
|
2734
3267
|
}
|
|
2735
3268
|
catch {
|
|
@@ -2742,10 +3275,10 @@ export async function reviewWikiIngestJob(jobId, input, options) {
|
|
|
2742
3275
|
rejectedCount,
|
|
2743
3276
|
pageNoteId: firstPublishedPageId,
|
|
2744
3277
|
latestMessage: acceptedCount > 0 || rejectedCount > 0
|
|
2745
|
-
? `Review saved with ${acceptedCount} kept and ${rejectedCount} discarded.`
|
|
3278
|
+
? `Review saved with ${acceptedCount} kept${mappedCount > 0 ? ` (${mappedCount} mapped)` : ""} and ${rejectedCount} discarded.`
|
|
2746
3279
|
: "Review saved."
|
|
2747
3280
|
});
|
|
2748
|
-
createWikiIngestLog(jobId, `Review saved with ${acceptedCount} kept and ${rejectedCount} discarded.`);
|
|
3281
|
+
createWikiIngestLog(jobId, `Review saved with ${acceptedCount} kept${mappedCount > 0 ? ` (${mappedCount} mapped)` : ""} and ${rejectedCount} discarded.`);
|
|
2749
3282
|
return getWikiIngestJob(jobId);
|
|
2750
3283
|
}
|
|
2751
3284
|
export function getWikiIngestJob(jobId) {
|