mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-16 11:41:08 +00:00
319 lines
10 KiB
TypeScript
319 lines
10 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import {
|
|
replaceManagedMarkdownBlock,
|
|
withTrailingNewline,
|
|
} from "openclaw/plugin-sdk/memory-host-markdown";
|
|
import { compileMemoryWikiVault, type CompileMemoryWikiResult } from "./compile.js";
|
|
import type { ResolvedMemoryWikiConfig } from "./config.js";
|
|
import {
|
|
parseWikiMarkdown,
|
|
renderWikiMarkdown,
|
|
slugifyWikiSegment,
|
|
normalizeSourceIds,
|
|
normalizeWikiClaims,
|
|
type WikiClaim,
|
|
} from "./markdown.js";
|
|
import {
|
|
readQueryableWikiPages,
|
|
resolveQueryableWikiPageByLookup,
|
|
type QueryableWikiPage,
|
|
} from "./query.js";
|
|
import { initializeMemoryWikiVault } from "./vault.js";
|
|
|
|
const GENERATED_START = "<!-- openclaw:wiki:generated:start -->";
|
|
const GENERATED_END = "<!-- openclaw:wiki:generated:end -->";
|
|
const HUMAN_START = "<!-- openclaw:human:start -->";
|
|
const HUMAN_END = "<!-- openclaw:human:end -->";
|
|
|
|
export type CreateSynthesisMemoryWikiMutation = {
|
|
op: "create_synthesis";
|
|
title: string;
|
|
body: string;
|
|
sourceIds: string[];
|
|
claims?: WikiClaim[];
|
|
contradictions?: string[];
|
|
questions?: string[];
|
|
confidence?: number;
|
|
status?: string;
|
|
};
|
|
|
|
export type UpdateMetadataMemoryWikiMutation = {
|
|
op: "update_metadata";
|
|
lookup: string;
|
|
sourceIds?: string[];
|
|
claims?: WikiClaim[];
|
|
contradictions?: string[];
|
|
questions?: string[];
|
|
confidence?: number | null;
|
|
status?: string;
|
|
};
|
|
|
|
export type ApplyMemoryWikiMutation =
|
|
| CreateSynthesisMemoryWikiMutation
|
|
| UpdateMetadataMemoryWikiMutation;
|
|
|
|
export type ApplyMemoryWikiMutationResult = {
|
|
changed: boolean;
|
|
operation: ApplyMemoryWikiMutation["op"];
|
|
pagePath: string;
|
|
pageId?: string;
|
|
compile: CompileMemoryWikiResult;
|
|
};
|
|
|
|
export function normalizeMemoryWikiMutationInput(rawParams: unknown): ApplyMemoryWikiMutation {
|
|
const params = rawParams as {
|
|
op: ApplyMemoryWikiMutation["op"];
|
|
title?: string;
|
|
body?: string;
|
|
lookup?: string;
|
|
sourceIds?: string[];
|
|
claims?: WikiClaim[];
|
|
contradictions?: string[];
|
|
questions?: string[];
|
|
confidence?: number | null;
|
|
status?: string;
|
|
};
|
|
if (params.op === "create_synthesis") {
|
|
if (!params.title?.trim()) {
|
|
throw new Error("wiki mutation requires title for create_synthesis.");
|
|
}
|
|
if (!params.body?.trim()) {
|
|
throw new Error("wiki mutation requires body for create_synthesis.");
|
|
}
|
|
if (!params.sourceIds || params.sourceIds.length === 0) {
|
|
throw new Error("wiki mutation requires at least one sourceId for create_synthesis.");
|
|
}
|
|
return {
|
|
op: "create_synthesis",
|
|
title: params.title,
|
|
body: params.body,
|
|
sourceIds: params.sourceIds,
|
|
...(Array.isArray(params.claims) ? { claims: normalizeWikiClaims(params.claims) } : {}),
|
|
...(params.contradictions ? { contradictions: params.contradictions } : {}),
|
|
...(params.questions ? { questions: params.questions } : {}),
|
|
...(typeof params.confidence === "number" ? { confidence: params.confidence } : {}),
|
|
...(params.status ? { status: params.status } : {}),
|
|
};
|
|
}
|
|
if (!params.lookup?.trim()) {
|
|
throw new Error("wiki mutation requires lookup for update_metadata.");
|
|
}
|
|
return {
|
|
op: "update_metadata",
|
|
lookup: params.lookup,
|
|
...(params.sourceIds ? { sourceIds: params.sourceIds } : {}),
|
|
...(Array.isArray(params.claims) ? { claims: normalizeWikiClaims(params.claims) } : {}),
|
|
...(params.contradictions ? { contradictions: params.contradictions } : {}),
|
|
...(params.questions ? { questions: params.questions } : {}),
|
|
...(params.confidence !== undefined ? { confidence: params.confidence } : {}),
|
|
...(params.status ? { status: params.status } : {}),
|
|
};
|
|
}
|
|
|
|
function normalizeUniqueStrings(values: string[] | undefined): string[] | undefined {
|
|
if (!values) {
|
|
return undefined;
|
|
}
|
|
const normalized = values
|
|
.map((value) => value.trim())
|
|
.filter(Boolean)
|
|
.filter((value, index, all) => all.indexOf(value) === index);
|
|
return normalized;
|
|
}
|
|
|
|
function ensureHumanNotesBlock(body: string): string {
|
|
if (body.includes(HUMAN_START) && body.includes(HUMAN_END)) {
|
|
return body;
|
|
}
|
|
const trimmed = body.trimEnd();
|
|
const prefix = trimmed.length > 0 ? `${trimmed}\n\n` : "";
|
|
return `${prefix}## Notes\n${HUMAN_START}\n${HUMAN_END}\n`;
|
|
}
|
|
|
|
function buildSynthesisBody(params: {
|
|
title: string;
|
|
originalBody?: string;
|
|
generatedBody: string;
|
|
}): string {
|
|
const base = params.originalBody?.trim().length
|
|
? params.originalBody
|
|
: `# ${params.title}\n\n## Notes\n${HUMAN_START}\n${HUMAN_END}\n`;
|
|
const withGenerated = replaceManagedMarkdownBlock({
|
|
original: base,
|
|
heading: "## Summary",
|
|
startMarker: GENERATED_START,
|
|
endMarker: GENERATED_END,
|
|
body: params.generatedBody,
|
|
});
|
|
return ensureHumanNotesBlock(withGenerated);
|
|
}
|
|
|
|
async function writeWikiPage(params: {
|
|
absolutePath: string;
|
|
frontmatter: Record<string, unknown>;
|
|
body: string;
|
|
}): Promise<boolean> {
|
|
const rendered = withTrailingNewline(
|
|
renderWikiMarkdown({
|
|
frontmatter: params.frontmatter,
|
|
body: params.body,
|
|
}),
|
|
);
|
|
const existing = await fs.readFile(params.absolutePath, "utf8").catch(() => "");
|
|
if (existing === rendered) {
|
|
return false;
|
|
}
|
|
await fs.mkdir(path.dirname(params.absolutePath), { recursive: true });
|
|
await fs.writeFile(params.absolutePath, rendered, "utf8");
|
|
return true;
|
|
}
|
|
|
|
async function resolveWritablePage(params: {
|
|
config: ResolvedMemoryWikiConfig;
|
|
lookup: string;
|
|
}): Promise<QueryableWikiPage | null> {
|
|
const pages = await readQueryableWikiPages(params.config.vault.path);
|
|
return resolveQueryableWikiPageByLookup(pages, params.lookup);
|
|
}
|
|
|
|
async function applyCreateSynthesisMutation(params: {
|
|
config: ResolvedMemoryWikiConfig;
|
|
mutation: CreateSynthesisMemoryWikiMutation;
|
|
}): Promise<{ changed: boolean; pagePath: string; pageId: string }> {
|
|
const slug = slugifyWikiSegment(params.mutation.title);
|
|
const pagePath = path.join("syntheses", `${slug}.md`).replace(/\\/g, "/");
|
|
const absolutePath = path.join(params.config.vault.path, pagePath);
|
|
const existing = await fs.readFile(absolutePath, "utf8").catch(() => "");
|
|
const parsed = parseWikiMarkdown(existing);
|
|
const pageId =
|
|
(typeof parsed.frontmatter.id === "string" && parsed.frontmatter.id.trim()) ||
|
|
`synthesis.${slug}`;
|
|
const changed = await writeWikiPage({
|
|
absolutePath,
|
|
frontmatter: {
|
|
...parsed.frontmatter,
|
|
pageType: "synthesis",
|
|
id: pageId,
|
|
title: params.mutation.title,
|
|
sourceIds: normalizeSourceIds(params.mutation.sourceIds),
|
|
...(params.mutation.claims ? { claims: normalizeWikiClaims(params.mutation.claims) } : {}),
|
|
...(normalizeUniqueStrings(params.mutation.contradictions)
|
|
? { contradictions: normalizeUniqueStrings(params.mutation.contradictions) }
|
|
: {}),
|
|
...(normalizeUniqueStrings(params.mutation.questions)
|
|
? { questions: normalizeUniqueStrings(params.mutation.questions) }
|
|
: {}),
|
|
...(typeof params.mutation.confidence === "number"
|
|
? { confidence: params.mutation.confidence }
|
|
: {}),
|
|
status: params.mutation.status?.trim() || "active",
|
|
updatedAt: new Date().toISOString(),
|
|
},
|
|
body: buildSynthesisBody({
|
|
title: params.mutation.title,
|
|
originalBody: parsed.body,
|
|
generatedBody: params.mutation.body.trim(),
|
|
}),
|
|
});
|
|
return { changed, pagePath, pageId };
|
|
}
|
|
|
|
function buildUpdatedFrontmatter(params: {
|
|
original: Record<string, unknown>;
|
|
mutation: UpdateMetadataMemoryWikiMutation;
|
|
}): Record<string, unknown> {
|
|
const frontmatter: Record<string, unknown> = {
|
|
...params.original,
|
|
updatedAt: new Date().toISOString(),
|
|
};
|
|
if (params.mutation.sourceIds) {
|
|
frontmatter.sourceIds = normalizeSourceIds(params.mutation.sourceIds);
|
|
}
|
|
if (params.mutation.claims) {
|
|
const claims = normalizeWikiClaims(params.mutation.claims);
|
|
if (claims.length > 0) {
|
|
frontmatter.claims = claims;
|
|
} else {
|
|
delete frontmatter.claims;
|
|
}
|
|
}
|
|
if (params.mutation.contradictions) {
|
|
const contradictions = normalizeUniqueStrings(params.mutation.contradictions) ?? [];
|
|
if (contradictions.length > 0) {
|
|
frontmatter.contradictions = contradictions;
|
|
} else {
|
|
delete frontmatter.contradictions;
|
|
}
|
|
}
|
|
if (params.mutation.questions) {
|
|
const questions = normalizeUniqueStrings(params.mutation.questions) ?? [];
|
|
if (questions.length > 0) {
|
|
frontmatter.questions = questions;
|
|
} else {
|
|
delete frontmatter.questions;
|
|
}
|
|
}
|
|
if (params.mutation.confidence === null) {
|
|
delete frontmatter.confidence;
|
|
} else if (typeof params.mutation.confidence === "number") {
|
|
frontmatter.confidence = params.mutation.confidence;
|
|
}
|
|
if (params.mutation.status?.trim()) {
|
|
frontmatter.status = params.mutation.status.trim();
|
|
}
|
|
return frontmatter;
|
|
}
|
|
|
|
async function applyUpdateMetadataMutation(params: {
|
|
config: ResolvedMemoryWikiConfig;
|
|
mutation: UpdateMetadataMemoryWikiMutation;
|
|
}): Promise<{ changed: boolean; pagePath: string; pageId?: string }> {
|
|
const page = await resolveWritablePage({
|
|
config: params.config,
|
|
lookup: params.mutation.lookup,
|
|
});
|
|
if (!page) {
|
|
throw new Error(`Wiki page not found: ${params.mutation.lookup}`);
|
|
}
|
|
const parsed = parseWikiMarkdown(page.raw);
|
|
const changed = await writeWikiPage({
|
|
absolutePath: page.absolutePath,
|
|
frontmatter: buildUpdatedFrontmatter({
|
|
original: parsed.frontmatter,
|
|
mutation: params.mutation,
|
|
}),
|
|
body: parsed.body,
|
|
});
|
|
return {
|
|
changed,
|
|
pagePath: page.relativePath,
|
|
...(page.id ? { pageId: page.id } : {}),
|
|
};
|
|
}
|
|
|
|
export async function applyMemoryWikiMutation(params: {
|
|
config: ResolvedMemoryWikiConfig;
|
|
mutation: ApplyMemoryWikiMutation;
|
|
}): Promise<ApplyMemoryWikiMutationResult> {
|
|
await initializeMemoryWikiVault(params.config);
|
|
const result =
|
|
params.mutation.op === "create_synthesis"
|
|
? await applyCreateSynthesisMutation({
|
|
config: params.config,
|
|
mutation: params.mutation,
|
|
})
|
|
: await applyUpdateMetadataMutation({
|
|
config: params.config,
|
|
mutation: params.mutation,
|
|
});
|
|
const compile = await compileMemoryWikiVault(params.config);
|
|
return {
|
|
changed: result.changed,
|
|
operation: params.mutation.op,
|
|
pagePath: result.pagePath,
|
|
...(result.pageId ? { pageId: result.pageId } : {}),
|
|
compile,
|
|
};
|
|
}
|