504 lines
18 KiB
JavaScript
504 lines
18 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,
|
|
listCommunityKnowledgeFiles,
|
|
loadKnowledgeEntries,
|
|
migrateSingleBracePlaceholders,
|
|
registerKnowledgePlaceholderDefinitions,
|
|
saveCommunityKnowledgeFile,
|
|
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 }) {
|
|
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 fileResults = searchFileKnowledge({ query, user, limit: normalizedLimit, rootDir: process.cwd() });
|
|
const dbResults = searchForAi(db, query || "", user, normalizedLimit);
|
|
const seen = new Set(fileResults.map((entry) => entry.source || entry.id));
|
|
return [
|
|
...fileResults,
|
|
...dbResults.filter((entry) => {
|
|
const key = entry.source || entry.id;
|
|
if (seen.has(key)) return false;
|
|
seen.add(key);
|
|
return true;
|
|
})
|
|
].slice(0, normalizedLimit);
|
|
};
|
|
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: 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)
|
|
};
|
|
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 entries = listEntries(db, filters, req.session.user);
|
|
res.render(view("index"), {
|
|
title: "Knowledge",
|
|
entries,
|
|
filters,
|
|
categories: categoriesFor(entries),
|
|
tags: tagsFor(entries)
|
|
});
|
|
});
|
|
|
|
router.get("/admin", requireOkfManagement(db), (req, res) => {
|
|
renderAdmin(req, res, db);
|
|
});
|
|
|
|
router.post("/admin/community", requireOkfEdit(db), (req, res) => {
|
|
try {
|
|
validateOkfPlaceholderFields({
|
|
body: req.body.body
|
|
}, req.session.user, placeholders);
|
|
const entry = saveCommunityKnowledgeFile(process.cwd(), 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 entry = saveCommunityKnowledgeFile(process.cwd(), {
|
|
...req.body,
|
|
existing_slug: req.params.slug
|
|
});
|
|
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);
|
|
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("/: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) => 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}`,
|
|
`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 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 activeTab = resolveAdminTab(req.query);
|
|
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) : null;
|
|
const communityFiles = listCommunityKnowledgeFiles(process.cwd());
|
|
const selectedCommunity = req.query.community ? getCommunityKnowledgeFile(process.cwd(), req.query.community) : null;
|
|
const systemFiles = listSystemKnowledgeFiles(process.cwd());
|
|
const selectedSystemFile = req.query.system ? findSystemKnowledgeFile(systemFiles, req.query.system) : null;
|
|
const suggestionEntries = [...allEntries, ...communityFiles];
|
|
const versions = selected ? listVersions(db, selected.id) : [];
|
|
res.render(view("admin"), {
|
|
title: "OKF Management",
|
|
activeTab,
|
|
entries,
|
|
filters,
|
|
selected,
|
|
communityFiles,
|
|
selectedCommunity,
|
|
systemFiles,
|
|
selectedSystemFile,
|
|
versions,
|
|
permissions: listPermissions(db),
|
|
levels: PERMISSION_LEVELS,
|
|
statuses: STATUS_VALUES,
|
|
visibilityValues: VISIBILITY_VALUES,
|
|
reviewStates: REVIEW_STATES,
|
|
categories: categoriesFor(suggestionEntries),
|
|
tags: tagsFor(suggestionEntries),
|
|
renderMarkdown
|
|
});
|
|
}
|
|
|
|
function resolveAdminTab(query = {}) {
|
|
if (query.edit) return "general";
|
|
if (query.community) return "community";
|
|
if (query.system) return "system";
|
|
return ["general", "community", "system"].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 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 requireLogin(req, res, next) {
|
|
if (req.session.user) return next();
|
|
return res.redirect("/login");
|
|
}
|
|
|
|
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();
|
|
}
|