This commit is contained in:
Marcello Fitton
2026-03-03 12:29:06 -08:00
parent e94bfa0726
commit 05ff17c661
5 changed files with 171 additions and 113 deletions
+1 -1
View File
@@ -1506,8 +1506,8 @@ function systemEndpoints(app) {
"/system/validate-sql-connection",
[validatedRequest, flexUserRoleValid([ROLES.admin])],
async (request, response) => {
const { engine, connectionString } = reqBody(request);
try {
const { engine, connectionString } = reqBody(request);
if (!engine || !connectionString) {
return response.status(400).json({
success: false,
+12 -8
View File
@@ -1,18 +1,22 @@
const path = require('node:path');
const fs = require('node:fs');
const { parentPort } = require('node:worker_threads');
const path = require("node:path");
const fs = require("node:fs");
const { parentPort } = require("node:worker_threads");
const documentsPath =
process.env.NODE_ENV === "development"
? path.resolve(__dirname, `../../storage/documents`)
: path.resolve(process.env.STORAGE_DIR, `documents`);
function log(stringContent = '') {
if (parentPort) parentPort.postMessage(`\x1b[33m[${process.pid}]\x1b[0m: ${stringContent}`); // running as worker
else process.send(`\x1b[33m[${process.ppid}:${process.pid}]\x1b[0m: ${stringContent}`); // running as child_process
function log(stringContent = "") {
if (parentPort)
parentPort.postMessage(`\x1b[33m[${process.pid}]\x1b[0m: ${stringContent}`); // running as worker
else
process.send(
`\x1b[33m[${process.ppid}:${process.pid}]\x1b[0m: ${stringContent}`
); // running as child_process
}
function conclude() {
if (parentPort) parentPort.postMessage('done');
if (parentPort) parentPort.postMessage("done");
else process.exit(0);
}
@@ -27,4 +31,4 @@ module.exports = {
log,
conclude,
updateSourceDocument,
}
};
+116 -60
View File
@@ -1,152 +1,208 @@
const { Document } = require('../models/documents.js');
const { DocumentSyncQueue } = require('../models/documentSyncQueue.js');
const { CollectorApi } = require('../utils/collectorApi');
const { Document } = require("../models/documents.js");
const { DocumentSyncQueue } = require("../models/documentSyncQueue.js");
const { CollectorApi } = require("../utils/collectorApi");
const { fileData } = require("../utils/files");
const { log, conclude, updateSourceDocument } = require('./helpers/index.js');
const { getVectorDbClass } = require('../utils/helpers/index.js');
const { DocumentSyncRun } = require('../models/documentSyncRun.js');
const { log, conclude, updateSourceDocument } = require("./helpers/index.js");
const { getVectorDbClass } = require("../utils/helpers/index.js");
const { DocumentSyncRun } = require("../models/documentSyncRun.js");
(async () => {
try {
const queuesToProcess = await DocumentSyncQueue.staleDocumentQueues();
if (queuesToProcess.length === 0) {
log('No outstanding documents to sync. Exiting.');
log("No outstanding documents to sync. Exiting.");
return;
}
const collector = new CollectorApi();
if (!(await collector.online())) {
log('Could not reach collector API. Exiting.');
log("Could not reach collector API. Exiting.");
return;
}
log(`${queuesToProcess.length} watched documents have been found to be stale and will be updated now.`)
log(
`${queuesToProcess.length} watched documents have been found to be stale and will be updated now.`
);
for (const queue of queuesToProcess) {
let newContent = null;
const document = queue.workspaceDoc;
const workspace = document.workspace;
const { metadata, type, source } = Document.parseDocumentTypeAndSource(document);
const { metadata, type, source } =
Document.parseDocumentTypeAndSource(document);
if (!metadata || !DocumentSyncQueue.validFileTypes.includes(type)) {
// Document is either broken, invalid, or not supported so drop it from future queues.
log(`Document ${document.filename} has no metadata, is broken, or invalid and has been removed from all future runs.`)
log(
`Document ${document.filename} has no metadata, is broken, or invalid and has been removed from all future runs.`
);
await DocumentSyncQueue.unwatch(document);
continue;
}
if (['link', 'youtube'].includes(type)) {
if (["link", "youtube"].includes(type)) {
const response = await collector.forwardExtensionRequest({
endpoint: "/ext/resync-source-document",
method: "POST",
body: JSON.stringify({
type,
options: { link: source }
})
options: { link: source },
}),
});
newContent = response?.content;
}
if (['confluence', 'github', 'gitlab', 'drupalwiki'].includes(type)) {
if (["confluence", "github", "gitlab", "drupalwiki"].includes(type)) {
const response = await collector.forwardExtensionRequest({
endpoint: "/ext/resync-source-document",
method: "POST",
body: JSON.stringify({
type,
options: { chunkSource: metadata.chunkSource }
})
options: { chunkSource: metadata.chunkSource },
}),
});
newContent = response?.content;
}
if (!newContent) {
// Check if the last "x" runs were all failures (not exits!). If so - remove the job entirely since it is broken.
const failedRunCount = (await DocumentSyncRun.where({ queueId: queue.id }, DocumentSyncQueue.maxRepeatFailures, { createdAt: 'desc' })).filter((run) => run.status === DocumentSyncRun.statuses.failed).length;
const failedRunCount = (
await DocumentSyncRun.where(
{ queueId: queue.id },
DocumentSyncQueue.maxRepeatFailures,
{ createdAt: "desc" }
)
).filter(
(run) => run.status === DocumentSyncRun.statuses.failed
).length;
if (failedRunCount >= DocumentSyncQueue.maxRepeatFailures) {
log(`Document ${document.filename} has failed to refresh ${failedRunCount} times continuously and will now be removed from the watched document set.`)
log(
`Document ${document.filename} has failed to refresh ${failedRunCount} times continuously and will now be removed from the watched document set.`
);
await DocumentSyncQueue.unwatch(document);
continue;
}
log(`Failed to get a new content response from collector for source ${source}. Skipping, but will retry next worker interval. Attempt ${failedRunCount === 0 ? 1 : failedRunCount}/${DocumentSyncQueue.maxRepeatFailures}`);
await DocumentSyncQueue.saveRun(queue.id, DocumentSyncRun.statuses.failed, { filename: document.filename, workspacesModified: [], reason: 'No content found.' })
log(
`Failed to get a new content response from collector for source ${source}. Skipping, but will retry next worker interval. Attempt ${failedRunCount === 0 ? 1 : failedRunCount}/${DocumentSyncQueue.maxRepeatFailures}`
);
await DocumentSyncQueue.saveRun(
queue.id,
DocumentSyncRun.statuses.failed,
{
filename: document.filename,
workspacesModified: [],
reason: "No content found.",
}
);
continue;
}
const currentDocumentData = await fileData(document.docpath)
const currentDocumentData = await fileData(document.docpath);
if (currentDocumentData.pageContent === newContent) {
const nextSync = DocumentSyncQueue.calcNextSync(queue)
log(`Source ${source} is unchanged and will be skipped. Next sync will be ${nextSync.toLocaleString()}.`);
await DocumentSyncQueue._update(
const nextSync = DocumentSyncQueue.calcNextSync(queue);
log(
`Source ${source} is unchanged and will be skipped. Next sync will be ${nextSync.toLocaleString()}.`
);
await DocumentSyncQueue._update(queue.id, {
lastSyncedAt: new Date().toISOString(),
nextSyncAt: nextSync.toISOString(),
});
await DocumentSyncQueue.saveRun(
queue.id,
DocumentSyncRun.statuses.exited,
{
lastSyncedAt: new Date().toISOString(),
nextSyncAt: nextSync.toISOString(),
filename: document.filename,
workspacesModified: [],
reason: "Content unchanged.",
}
);
await DocumentSyncQueue.saveRun(queue.id, DocumentSyncRun.statuses.exited, { filename: document.filename, workspacesModified: [], reason: 'Content unchanged.' })
continue;
}
// update the defined document and workspace vectorDB with the latest information
// it will skip cache and create a new vectorCache file.
const vectorDatabase = getVectorDbClass();
await vectorDatabase.deleteDocumentFromNamespace(workspace.slug, document.docId);
await vectorDatabase.deleteDocumentFromNamespace(
workspace.slug,
document.docId
);
await vectorDatabase.addDocumentToNamespace(
workspace.slug,
{ ...currentDocumentData, pageContent: newContent, docId: document.docId },
document.docpath,
true
);
updateSourceDocument(
document.docpath,
{
...currentDocumentData,
pageContent: newContent,
docId: document.docId,
published: (new Date).toLocaleString(),
// Todo: Update word count and token_estimate?
}
)
log(`Workspace "${workspace.name}" vectors of ${source} updated. Document and vector cache updated.`)
},
document.docpath,
true
);
updateSourceDocument(document.docpath, {
...currentDocumentData,
pageContent: newContent,
docId: document.docId,
published: new Date().toLocaleString(),
// Todo: Update word count and token_estimate?
});
log(
`Workspace "${workspace.name}" vectors of ${source} updated. Document and vector cache updated.`
);
// Now we can bloom the results to all matching documents in all other workspaces
const workspacesModified = [workspace.slug];
const moreReferences = await Document.where({
id: { not: document.id },
filename: document.filename
}, null, null, { workspace: true });
const moreReferences = await Document.where(
{
id: { not: document.id },
filename: document.filename,
},
null,
null,
{ workspace: true }
);
if (moreReferences.length !== 0) {
log(`${source} is referenced in ${moreReferences.length} other workspaces. Updating those workspaces as well...`)
log(
`${source} is referenced in ${moreReferences.length} other workspaces. Updating those workspaces as well...`
);
for (const additionalDocumentRef of moreReferences) {
const additionalWorkspace = additionalDocumentRef.workspace;
workspacesModified.push(additionalWorkspace.slug);
await vectorDatabase.deleteDocumentFromNamespace(additionalWorkspace.slug, additionalDocumentRef.docId);
await vectorDatabase.deleteDocumentFromNamespace(
additionalWorkspace.slug,
additionalDocumentRef.docId
);
await vectorDatabase.addDocumentToNamespace(
additionalWorkspace.slug,
{ ...currentDocumentData, pageContent: newContent, docId: additionalDocumentRef.docId },
additionalDocumentRef.docpath,
{
...currentDocumentData,
pageContent: newContent,
docId: additionalDocumentRef.docId,
},
additionalDocumentRef.docpath
);
log(
`Workspace "${additionalWorkspace.name}" vectors for ${source} was also updated with the new content from cache.`
);
log(`Workspace "${additionalWorkspace.name}" vectors for ${source} was also updated with the new content from cache.`)
}
}
const nextRefresh = DocumentSyncQueue.calcNextSync(queue);
log(`${source} has been refreshed in all workspaces it is currently referenced in. Next refresh will be ${nextRefresh.toLocaleString()}.`)
await DocumentSyncQueue._update(
queue.id,
{
lastSyncedAt: new Date().toISOString(),
nextSyncAt: nextRefresh.toISOString(),
}
log(
`${source} has been refreshed in all workspaces it is currently referenced in. Next refresh will be ${nextRefresh.toLocaleString()}.`
);
await DocumentSyncQueue._update(queue.id, {
lastSyncedAt: new Date().toISOString(),
nextSyncAt: nextRefresh.toISOString(),
});
await DocumentSyncQueue.saveRun(
queue.id,
DocumentSyncRun.statuses.success,
{ filename: document.filename, workspacesModified }
);
await DocumentSyncQueue.saveRun(queue.id, DocumentSyncRun.statuses.success, { filename: document.filename, workspacesModified })
}
} catch (e) {
console.error(e)
log(`errored with ${e.message}`)
console.error(e);
log(`errored with ${e.message}`);
} finally {
conclude();
}
+8 -8
View File
@@ -1,10 +1,10 @@
function waitForElm(selector) {
return new Promise(resolve => {
return new Promise((resolve) => {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector));
}
const observer = new MutationObserver(mutations => {
const observer = new MutationObserver((mutations) => {
if (document.querySelector(selector)) {
resolve(document.querySelector(selector));
observer.disconnect();
@@ -13,16 +13,16 @@ function waitForElm(selector) {
observer.observe(document.body, {
childList: true,
subtree: true
subtree: true,
});
});
}
// Force change the Swagger logo in the header
waitForElm('.topbar-wrapper').then((elm) => {
if (window.SWAGGER_DOCS_ENV === 'development') {
elm.innerHTML = `<img href='${window.location.origin}' src='http://localhost:3000/public/anything-llm-light.png' width='200'/>`
waitForElm(".topbar-wrapper").then((elm) => {
if (window.SWAGGER_DOCS_ENV === "development") {
elm.innerHTML = `<img href='${window.location.origin}' src='http://localhost:3000/public/anything-llm-light.png' width='200'/>`;
} else {
elm.innerHTML = `<img href='${window.location.origin}' src='${window.location.origin}/anything-llm-light.png' width='200'/>`
elm.innerHTML = `<img href='${window.location.origin}' src='${window.location.origin}/anything-llm-light.png' width='200'/>`;
}
});
});
+34 -36
View File
@@ -1,11 +1,11 @@
const fs = require('fs');
const path = require('path');
const swaggerUi = require('swagger-ui-express');
const fs = require("fs");
const path = require("path");
const swaggerUi = require("swagger-ui-express");
function faviconUrl() {
return process.env.NODE_ENV === "production" ?
'/public/favicon.png' :
'http://localhost:3000/public/favicon.png'
return process.env.NODE_ENV === "production"
? "/public/favicon.png"
: "http://localhost:3000/public/favicon.png";
}
function useSwagger(app) {
@@ -15,44 +15,42 @@ function useSwagger(app) {
);
return;
}
app.use('/api/docs', swaggerUi.serve);
app.use("/api/docs", swaggerUi.serve);
const options = {
customCss: [
fs.readFileSync(path.resolve(__dirname, 'index.css')),
fs.readFileSync(path.resolve(__dirname, 'dark-swagger.css'))
].join('\n\n\n'),
customSiteTitle: 'AnythingLLM Developer API Documentation',
fs.readFileSync(path.resolve(__dirname, "index.css")),
fs.readFileSync(path.resolve(__dirname, "dark-swagger.css")),
].join("\n\n\n"),
customSiteTitle: "AnythingLLM Developer API Documentation",
customfavIcon: faviconUrl(),
}
};
if (process.env.NODE_ENV === "production") {
const swaggerDocument = require('./openapi.json');
app.get('/api/docs', swaggerUi.setup(
swaggerDocument,
{
...options,
customJsStr: 'window.SWAGGER_DOCS_ENV = "production";\n\n' + fs.readFileSync(path.resolve(__dirname, 'index.js'), 'utf8'),
},
));
} else {
// we regenerate the html page only in development mode to ensure it is up-to-date when the code is hot-reloaded.
const swaggerDocument = require("./openapi.json");
app.get(
"/api/docs",
async (_, response) => {
// #swagger.ignore = true
const swaggerDocument = require('./openapi.json');
return response.send(
swaggerUi.generateHTML(
swaggerDocument,
{
...options,
customJsStr: 'window.SWAGGER_DOCS_ENV = "development";\n\n' + fs.readFileSync(path.resolve(__dirname, 'index.js'), 'utf8'),
}
)
);
}
swaggerUi.setup(swaggerDocument, {
...options,
customJsStr:
'window.SWAGGER_DOCS_ENV = "production";\n\n' +
fs.readFileSync(path.resolve(__dirname, "index.js"), "utf8"),
})
);
} else {
// we regenerate the html page only in development mode to ensure it is up-to-date when the code is hot-reloaded.
app.get("/api/docs", async (_, response) => {
// #swagger.ignore = true
const swaggerDocument = require("./openapi.json");
return response.send(
swaggerUi.generateHTML(swaggerDocument, {
...options,
customJsStr:
'window.SWAGGER_DOCS_ENV = "development";\n\n' +
fs.readFileSync(path.resolve(__dirname, "index.js"), "utf8"),
})
);
});
}
}
module.exports = { faviconUrl, useSwagger }
module.exports = { faviconUrl, useSwagger };