Lumi/plugins/okf/index.js
2026-07-18 02:56:03 +02:00

620 lines
23 KiB
JavaScript

const path = require("path");
const { getSetting, setSetting } = require("../../src/services/settings");
const {
PERMISSION_LEVELS,
REVIEW_STATES,
STATUS_VALUES,
VISIBILITY_VALUES,
accessForUser,
createEntry,
ensureTables,
getEditableEntry,
getEntryBySlug,
grantPermission,
listEntries,
listPermissions,
listVersions,
revokePermission,
restoreVersion,
searchForAi,
setEntryWorkflow,
updateEntry
} = require("./backend/okf_store");
const {
ensureKnowledgeDirs,
getCommunityKnowledgeFile,
getVisibleCommunityKnowledge,
listCommunityKnowledgeFiles,
listCorrectionKnowledgeFiles,
listVisibleCommunityKnowledge,
loadKnowledgeEntries,
migrateSingleBracePlaceholders,
registerKnowledgePlaceholderDefinitions,
saveCommunityKnowledgeFile,
saveCorrectionKnowledgeFile,
searchFileKnowledge
} = require("./backend/file_knowledge");
const { generateKnowledgeFiles } = require("./backend/generate_knowledge");
const { renderMarkdown } = require("./backend/markdown");
const placeholderService = require("../../src/services/placeholders");
const PLUGIN_ID = "okf";
module.exports = {
id: PLUGIN_ID,
init({ web, db, placeholders = placeholderService }) {
if (typeof web.auth?.requireAuth !== "function") {
throw new Error("OKF requires Lumi's shared WebUI authentication helpers.");
}
const requireLogin = web.auth.requireAuth;
ensureTables(db);
ensureKnowledgeDirs(process.cwd());
if (!getSetting("okf_single_brace_placeholder_migration_v1", false)) {
migrateSingleBracePlaceholders(process.cwd());
setSetting("okf_single_brace_placeholder_migration_v1", true);
}
generateKnowledgeFiles(process.cwd());
registerOkfPlaceholderFields(placeholders);
registerKnowledgePlaceholderDefinitions(placeholders, { rootDir: process.cwd() });
const searchOkfForAi = ({ query = "", user, limit = 5, okf_retrieval = "light" } = {}) => {
if (okf_retrieval === "none" || Number(limit) === 0) return [];
const normalizedLimit = Math.max(1, Math.min(Number(limit) || 5, 25));
const candidateLimit = Math.min(25, Math.max(normalizedLimit * 3, 8));
const fileResults = searchFileKnowledge({ query, user, limit: candidateLimit, rootDir: process.cwd() });
const dbResults = searchForAi(db, query || "", user, candidateLimit);
const candidates = [...fileResults, ...dbResults];
const selected = rankCombinedKnowledge(candidates, normalizedLimit);
Object.defineProperty(selected, "candidateCount", {
value: candidates.length,
enumerable: false
});
return selected;
};
const okfContextProvider = ({ message, user, limit, okf_retrieval } = {}) => {
const normalizedLimit = limit ?? 5;
const query = message || "";
const results = searchOkfForAi({ query, user, limit: normalizedLimit, okf_retrieval });
return {
blocks: formatAiContext(results),
diagnostics: {
provider: "okf",
kind: "okf",
query,
okf_retrieval: okf_retrieval || "light",
limit: normalizedLimit,
candidate_count: Number(results.candidateCount) || results.length,
returned_count: results.length,
reason: okf_retrieval === "none" || Number(normalizedLimit) === 0 ? "disabled_by_controller" : "search_complete"
}
};
};
if (!global.lumiFrameworks) {
global.lumiFrameworks = {};
}
global.lumiFrameworks.okf = {
search: searchOkfForAi,
context: okfContextProvider,
accessForUser: (user) => accessForUser(db, user),
saveCorrection(values, actor) {
if (!actor?.isAdmin) throw new Error("Only admins can create correction knowledge files.");
return saveCorrectionKnowledgeFile(process.cwd(), values);
}
};
global.lumiFrameworks.ai?.registerContext?.(PLUGIN_ID, okfContextProvider);
const router = web.createRouter();
router.use((req, res, next) => {
res.locals.okfAccess = accessForUser(db, req.session.user);
next();
});
router.get("/", requireLogin, (req, res) => {
const filters = {
q: req.query.q || "",
category: req.query.category || "",
tag: req.query.tag || ""
};
const generalEntries = listEntries(db, filters, req.session.user)
.map((entry) => ({
...entry,
source_kind: "general",
href: `/plugins/${PLUGIN_ID}/${encodeURIComponent(entry.slug)}`
}));
const communityEntries = listVisibleCommunityKnowledge({
user: req.session.user,
filters,
rootDir: process.cwd()
});
const entries = [...generalEntries, ...communityEntries]
.sort((left, right) => left.title.localeCompare(right.title));
const filterEntries = [
...listEntries(db, {}, req.session.user),
...listVisibleCommunityKnowledge({ user: req.session.user, rootDir: process.cwd() })
];
res.render(view("index"), {
title: "Knowledge",
entries,
filters,
categories: categoriesFor(filterEntries),
tags: tagsFor(filterEntries)
});
});
router.get("/admin", requireOkfManagement(db), (req, res) => {
renderAdmin(req, res, db);
});
router.post("/admin/preview", requireOkfManagement(db), (req, res) => {
const markdown = String(req.body?.markdown || "");
if (markdown.length > 24000) return res.status(400).json({ error: "Markdown preview is limited to 24,000 characters." });
return res.json({ html: renderMarkdown(markdown) });
});
router.post("/admin/community", requireOkfEdit(db), (req, res) => {
try {
validateOkfPlaceholderFields({
body: req.body.body
}, req.session.user, placeholders);
const entry = saveCommunityKnowledgeFile(process.cwd(), communityFileValuesForUser(db, req.session.user, req.body));
registerKnowledgePlaceholderDefinitions(placeholders, { rootDir: process.cwd() });
req.session.flash = { type: "success", message: "Community OKF file saved." };
res.redirect(`/plugins/${PLUGIN_ID}/admin?tab=community&community=${encodeURIComponent(entry.slug)}#okf-community-files`);
} catch (error) {
req.session.flash = { type: "error", message: error.message };
res.redirect(`/plugins/${PLUGIN_ID}/admin?tab=community#okf-community-files`);
}
});
router.post("/admin/community/:slug", requireOkfEdit(db), (req, res) => {
try {
validateOkfPlaceholderFields({
body: req.body.body
}, req.session.user, placeholders);
const existing = getCommunityKnowledgeFile(process.cwd(), req.params.slug);
const entry = saveCommunityKnowledgeFile(process.cwd(), communityFileValuesForUser(db, req.session.user, {
...req.body,
existing_slug: req.params.slug
}, existing));
registerKnowledgePlaceholderDefinitions(placeholders, { rootDir: process.cwd() });
req.session.flash = { type: "success", message: "Community OKF file updated." };
res.redirect(`/plugins/${PLUGIN_ID}/admin?tab=community&community=${encodeURIComponent(entry.slug)}#okf-community-files`);
} catch (error) {
req.session.flash = { type: "error", message: error.message };
res.redirect(`/plugins/${PLUGIN_ID}/admin?tab=community&community=${encodeURIComponent(req.params.slug)}#okf-community-files`);
}
});
router.post("/admin/entries", requireOkfEdit(db), (req, res) => {
try {
validateOkfPlaceholderFields(req.body, req.session.user, placeholders);
const entry = createEntry(db, req.body, req.session.user);
req.session.flash = { type: "success", message: "OKF entry created." };
res.redirect(`/plugins/${PLUGIN_ID}/admin?tab=general&edit=${encodeURIComponent(entry.slug)}`);
} catch (error) {
req.session.flash = { type: "error", message: error.message };
res.redirect(`/plugins/${PLUGIN_ID}/admin?tab=general`);
}
});
router.post("/admin/entries/:slug", requireOkfEdit(db), (req, res) => {
try {
validateOkfPlaceholderFields(req.body, req.session.user, placeholders);
const entry = updateEntry(db, req.params.slug, req.body, req.session.user);
req.session.flash = { type: "success", message: "OKF entry updated." };
res.redirect(`/plugins/${PLUGIN_ID}/admin?tab=general&edit=${encodeURIComponent(entry.slug)}`);
} catch (error) {
req.session.flash = { type: "error", message: error.message };
res.redirect(`/plugins/${PLUGIN_ID}/admin?tab=general&edit=${encodeURIComponent(req.params.slug)}`);
}
});
router.post("/admin/entries/:slug/:action", requireOkfManagement(db), (req, res) => {
try {
const entry = setEntryWorkflow(db, req.params.slug, req.params.action, req.session.user, req.body.note || "");
req.session.flash = { type: "success", message: "OKF workflow updated." };
res.redirect(`/plugins/${PLUGIN_ID}/admin?tab=general&edit=${encodeURIComponent(entry.slug)}`);
} catch (error) {
req.session.flash = { type: "error", message: error.message };
res.redirect(`/plugins/${PLUGIN_ID}/admin?tab=general&edit=${encodeURIComponent(req.params.slug)}`);
}
});
router.post("/admin/entries/:slug/versions/:version/restore", requireOkfManagement(db), (req, res) => {
try {
const selected = getEditableEntry(db, req.params.slug, req.session.user);
if (!selected) throw new Error("OKF entry was not found.");
const entry = restoreVersion(
db,
selected.id,
req.params.version,
req.session.user,
req.body.note || ""
);
req.session.flash = { type: "success", message: "OKF version restored." };
res.redirect(`/plugins/${PLUGIN_ID}/admin?tab=general&edit=${encodeURIComponent(entry.slug)}`);
} catch (error) {
req.session.flash = { type: "error", message: error.message };
res.redirect(`/plugins/${PLUGIN_ID}/admin?tab=general&edit=${encodeURIComponent(req.params.slug)}`);
}
});
router.post("/admin/permissions", requireAdmin, (req, res) => {
try {
grantPermission(db, req.body, req.session.user);
req.session.flash = { type: "success", message: "OKF permission granted." };
} catch (error) {
req.session.flash = { type: "error", message: error.message };
}
res.redirect(`/plugins/${PLUGIN_ID}/admin?tab=general#okf-permissions`);
});
router.post("/admin/permissions/:id/revoke", requireAdmin, (req, res) => {
try {
revokePermission(db, req.params.id, req.session.user);
req.session.flash = { type: "success", message: "OKF permission revoked." };
} catch (error) {
req.session.flash = { type: "error", message: error.message };
}
res.redirect(`/plugins/${PLUGIN_ID}/admin?tab=general#okf-permissions`);
});
router.get("/community/:slug", requireLogin, (req, res) => {
const entry = getVisibleCommunityKnowledge({
user: req.session.user,
slug: req.params.slug,
rootDir: process.cwd()
});
if (!entry) {
return res.status(404).render("error", {
title: "Knowledge entry unavailable",
message: "That community knowledge file was not found or is not visible to your account."
});
}
return res.render(view("entry"), {
title: entry.title,
entry,
renderMarkdown
});
});
router.get("/:slug", requireLogin, async (req, res) => {
const entry = await resolveOkfEntryPlaceholders(
getEntryBySlug(db, req.params.slug, req.session.user),
req.session.user,
placeholders
);
if (!entry) {
return res.status(404).render("error", {
title: "Knowledge entry unavailable",
message: "That OKF entry was not found or is not visible to your account."
});
}
res.render(view("entry"), {
title: entry.title,
entry,
renderMarkdown
});
});
web.mount(`/plugins/${PLUGIN_ID}`, router, {
label: "Knowledge",
role: "public",
authRequired: true,
section: "community",
canAccess: (user) => Boolean(user)
});
web.addNavItem({
label: "OKF Management",
path: `/plugins/${PLUGIN_ID}/admin`,
role: "public",
authRequired: true,
section: "admin",
canAccess: (user) => user?.isAdmin === true
});
web.addNavItem({
label: "Contribute to OKF",
path: `/plugins/${PLUGIN_ID}/admin`,
role: "public",
authRequired: true,
section: "community",
canAccess: (user) => user?.isAdmin !== true && accessForUser(db, user).canEdit
});
return () => {
global.lumiFrameworks?.ai?.unregisterContext?.(PLUGIN_ID);
if (global.lumiFrameworks?.okf?.context === okfContextProvider) {
delete global.lumiFrameworks.okf;
}
};
}
};
function formatAiContext(entries) {
if (!entries.length) return [];
const prepared = preferSpecificKnowledgeChunks(entries).slice(0, 3);
const blocks = [];
let remaining = 4200;
for (const entry of prepared) {
const facts = trimForContext(entry.facts, Math.min(1400, remaining));
const block = [
`OKF entry: ${entry.title}`,
`Evidence tier: ${knowledgeEvidenceTier(entry)}`,
`Source: ${entry.source}`,
entry.source_metadata ? `Source metadata: ${formatSourceMetadata(entry.source_metadata)}` : "",
entry.category ? `Category: ${entry.category}` : "",
entry.summary ? `Summary: ${trimForContext(entry.summary, 360)}` : "",
facts ? `Facts:\n${facts}` : ""
].filter(Boolean).join("\n");
if (!block || remaining <= 0) continue;
const trimmedBlock = trimForContext(block, remaining);
blocks.push(trimmedBlock);
remaining -= trimmedBlock.length + 2;
}
return blocks;
}
function knowledgeEvidenceTier(entry = {}) {
const source = String(entry.source_metadata?.path || entry.source || "").replace(/\\/g, "/");
if (source.includes("knowledge/corrections/")) return "admin-approved correction";
if (source.includes("knowledge/community/")) return "approved community OKF";
if (source.startsWith("/plugins/okf/")) return "approved database OKF";
if (source.includes("knowledge/plugins/")) return "generated plugin reference";
if (source.includes("knowledge/core/")) return "generated core reference";
return "approved OKF reference";
}
function rankCombinedKnowledge(entries, limit) {
const ranked = [...entries].sort((left, right) =>
Number(right.source_metadata?.score || 0) - Number(left.source_metadata?.score || 0) ||
String(left.source || "").localeCompare(String(right.source || ""))
);
const selected = [];
const exactSources = new Set();
const perEntry = new Map();
for (const entry of ranked) {
const exactKey = `${entry.source || ""}:${entry.source_metadata?.heading || ""}`;
if (exactSources.has(exactKey)) continue;
const entryKey = String(entry.id || entry.source || exactKey);
const usedForEntry = perEntry.get(entryKey) || 0;
if (usedForEntry >= 2) continue;
exactSources.add(exactKey);
perEntry.set(entryKey, usedForEntry + 1);
selected.push(entry);
if (selected.length >= limit) break;
}
return selected;
}
function preferSpecificKnowledgeChunks(entries) {
const hasSpecificRoute = entries.some((entry) => {
const heading = String(entry.source_metadata?.heading || "");
return /^(GET|POST|PUT|PATCH|DELETE|MOUNT)\s+\//i.test(heading);
});
if (!hasSpecificRoute) return entries;
return entries.filter((entry) => {
const heading = String(entry.source_metadata?.heading || "");
return !["Routes", "Web Routes", "Route Reference"].includes(heading);
});
}
function trimForContext(value, maximum) {
const text = String(value || "").trim();
const limit = Math.max(0, Number(maximum) || 0);
if (!text || !limit) return "";
if (text.length <= limit) return text;
return `${text.slice(0, Math.max(0, limit - 24)).trimEnd()}\n[truncated]`;
}
function formatSourceMetadata(metadata = {}) {
return [
metadata.path ? `path=${metadata.path}` : "",
metadata.id ? `id=${metadata.id}` : "",
metadata.heading ? `heading=${metadata.heading}` : "",
Number.isFinite(Number(metadata.score)) ? `score=${Math.round(Number(metadata.score))}` : "",
metadata.excerpt ? `excerpt=${String(metadata.excerpt).slice(0, 240)}` : ""
].filter(Boolean).join("; ");
}
function registerOkfPlaceholderFields(placeholders) {
if (!placeholders?.registerFieldPolicy) return;
placeholders.registerFieldPolicy({
field_id: "okf.markdown",
label: "OKF Markdown",
field_type: "okf_markdown",
output_audience: "user",
min_editor_role: "user",
allowed_namespaces: [],
max_sensitivity: "admin",
description: "Role-gated OKF Markdown content."
});
}
function validateOkfPlaceholderFields(values, user, placeholders) {
if (!placeholders?.validateTemplate) return;
const checks = [
["user_markdown", "user"],
["moderator_markdown", "mod"],
["admin_markdown", "admin"],
["ai_facts_markdown", "admin"],
["body", "user"]
];
for (const [field, outputAudience] of checks) {
if (values[field] === undefined) continue;
const result = placeholders.validateTemplate({
fieldId: "okf.markdown",
template: values[field],
outputAudience,
user
});
if (!result.ok) {
const tokens = result.errors
.map((error) => error.token || error.id || error.reason)
.filter(Boolean)
.slice(0, 5)
.join(", ");
throw new Error(`Unavailable or unauthorized OKF placeholder${tokens ? `: ${tokens}` : "."}`);
}
}
}
async function resolveOkfEntryPlaceholders(entry, user, placeholders) {
if (!entry || !placeholders?.renderTemplate) return entry;
const resolved = { ...entry };
const fields = [
["user_markdown", "user"],
["moderator_markdown", "mod"],
["admin_markdown", "admin"],
["ai_facts_markdown", "admin"]
];
for (const [field, outputAudience] of fields) {
if (!resolved[field]) continue;
const result = await placeholders.renderTemplate({
fieldId: "okf.markdown",
template: resolved[field],
outputAudience,
user,
runtimeContext: {
runtime: true,
okf: { entry: resolved }
},
fallback: "[unavailable]"
});
resolved[field] = result.rendered;
}
return resolved;
}
function renderAdmin(req, res, db) {
const access = accessForUser(db, req.session.user);
const activeTab = resolveAdminTab(req.query, access.canManagePermissions);
const filters = {
q: req.query.q || "",
status: req.query.status || "",
category: req.query.category || "",
tag: req.query.tag || ""
};
const entries = listEntries(db, filters, req.session.user, { management: true });
const allEntries = listEntries(db, {}, req.session.user, { management: true });
const selected = req.query.edit ? getEditableEntry(db, req.query.edit, req.session.user) : null;
const communityFiles = listCommunityKnowledgeFiles(process.cwd()).filter((entry) => canReadOkfVisibility(access, entry.visibility));
const requestedCommunity = req.query.community ? getCommunityKnowledgeFile(process.cwd(), req.query.community) : null;
const selectedCommunity = requestedCommunity && canReadOkfVisibility(access, requestedCommunity.visibility) ? requestedCommunity : null;
const systemFiles = listSystemKnowledgeFiles(process.cwd()).filter((entry) => canReadOkfVisibility(access, entry.visibility));
const selectedSystemFile = req.query.system ? findSystemKnowledgeFile(systemFiles, req.query.system) : null;
const correctionFiles = access.canManagePermissions
? listCorrectionKnowledgeFiles(process.cwd()).filter((entry) => canReadOkfVisibility(access, entry.visibility))
: [];
const selectedCorrection = req.query.correction ? findSystemKnowledgeFile(correctionFiles, req.query.correction) : null;
const suggestionEntries = [...allEntries, ...communityFiles];
const versions = selected ? listVersions(db, selected.id, req.session.user) : [];
res.render(view("admin"), {
title: "OKF Management",
activeTab,
entries,
filters,
selected,
communityFiles,
selectedCommunity,
systemFiles,
selectedSystemFile,
correctionFiles,
selectedCorrection,
versions,
permissions: listPermissions(db, req.session.user),
levels: PERMISSION_LEVELS,
statuses: STATUS_VALUES,
visibilityValues: VISIBILITY_VALUES.filter((value) => canReadOkfVisibility(access, value)),
reviewStates: REVIEW_STATES,
categories: categoriesFor(suggestionEntries),
tags: tagsFor(suggestionEntries),
renderMarkdown
});
}
function resolveAdminTab(query = {}, allowCorrections = false) {
if (query.edit) return "general";
if (query.community) return "community";
if (query.system) return "system";
if (allowCorrections && query.correction) return "corrections";
const allowed = allowCorrections ? ["general", "community", "system", "corrections"] : ["general", "community", "system"];
return allowed.includes(query.tab) ? query.tab : "general";
}
function listSystemKnowledgeFiles(rootDir = process.cwd()) {
return loadKnowledgeEntries(rootDir, { includeHidden: true })
.filter((entry) => ["core", "plugins"].includes(entry.scope) && (entry.generated || !entry.editable))
.map((entry) => ({
...entry,
slug: entry.file_slug,
entry_slug: entry.slug,
path: `knowledge/${entry.path}`
}))
.sort((a, b) => a.scope.localeCompare(b.scope) || a.title.localeCompare(b.title));
}
function canReadOkfVisibility(access, visibility) {
if (visibility === "admin") return access.capabilities.read_admin_fields;
if (visibility === "mod") return access.capabilities.read_mod_fields;
return access.capabilities.read_user_fields;
}
function communityFileValuesForUser(db, user, values, existing = null) {
const access = accessForUser(db, user);
if (existing && !canReadOkfVisibility(access, existing.visibility)) {
throw new Error("That community knowledge file is not available to your account.");
}
const requestedVisibility = VISIBILITY_VALUES.includes(values.visibility) ? values.visibility : existing?.visibility || "user";
if (!canReadOkfVisibility(access, requestedVisibility)) {
throw new Error(`You cannot create or edit ${requestedVisibility}-only knowledge.`);
}
return { ...values, visibility: requestedVisibility };
}
function findSystemKnowledgeFile(files, key) {
const value = String(key || "");
return files.find((file) =>
file.slug === value ||
file.entry_slug === value ||
file.id === value ||
file.path === value
) || null;
}
function view(name) {
return path.join(__dirname, "views", `${name}.ejs`);
}
function requireAdmin(req, res, next) {
if (req.session.user?.isAdmin) return next();
return denied(res);
}
function requireOkfManagement(db) {
return (req, res, next) => {
if (accessForUser(db, req.session.user).canEdit) return next();
return denied(res);
};
}
function requireOkfEdit(db) {
return (req, res, next) => {
if (accessForUser(db, req.session.user).canEdit) return next();
return denied(res);
};
}
function denied(res) {
return res.status(403).render("error", {
title: "Access denied",
message: "You do not have access to that page."
});
}
function categoriesFor(entries) {
return Array.from(new Set(entries.map((entry) => entry.category).filter(Boolean))).sort();
}
function tagsFor(entries) {
return Array.from(new Set(entries.flatMap((entry) => entry.tags || []).filter(Boolean))).sort();
}