mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-11 10:33:18 +08:00
重构主控编排与运行时预热链路,统一工作区提示词/专家调度协议并补齐 wiki 记忆注入与写回闭环。
同时收敛启动与运维脚本默认行为(含 wiki worker)、更新 Admin 可观测性与相关测试,降低首轮时延并提高运行稳定性。 Made-with: Cursor
This commit is contained in:
parent
4a23b715a2
commit
dbbe3add6a
14438 changed files with 2693620 additions and 2546 deletions
235
openclaw/extensions/memory-wiki/src/prompt-section.ts
Normal file
235
openclaw/extensions/memory-wiki/src/prompt-section.ts
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { MemoryPromptSectionBuilder } from "openclaw/plugin-sdk/memory-host-core";
|
||||
import { resolveMemoryWikiConfig, type ResolvedMemoryWikiConfig } from "./config.js";
|
||||
|
||||
const AGENT_DIGEST_PATH = ".openclaw-wiki/cache/agent-digest.json";
|
||||
const DIGEST_MAX_PAGES = 4;
|
||||
const DIGEST_MAX_CLAIMS_PER_PAGE = 2;
|
||||
|
||||
type PromptDigestClaim = {
|
||||
text: string;
|
||||
status?: string;
|
||||
confidence?: number;
|
||||
freshnessLevel?: string;
|
||||
};
|
||||
|
||||
type PromptDigestPage = {
|
||||
title: string;
|
||||
kind: string;
|
||||
claimCount: number;
|
||||
questions?: string[];
|
||||
contradictions?: string[];
|
||||
topClaims?: PromptDigestClaim[];
|
||||
};
|
||||
|
||||
type PromptDigest = {
|
||||
pageCounts?: Record<string, number>;
|
||||
claimCount?: number;
|
||||
contradictionClusters?: Array<unknown>;
|
||||
pages?: PromptDigestPage[];
|
||||
};
|
||||
|
||||
function tryReadPromptDigest(config: ResolvedMemoryWikiConfig): PromptDigest | null {
|
||||
const digestPath = path.join(config.vault.path, AGENT_DIGEST_PATH);
|
||||
try {
|
||||
const raw = fs.readFileSync(digestPath, "utf8");
|
||||
const parsed = JSON.parse(raw) as PromptDigest;
|
||||
if (!parsed || typeof parsed !== "object") {
|
||||
return null;
|
||||
}
|
||||
return parsed;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function rankPromptDigestPage(page: PromptDigestPage): number {
|
||||
return (
|
||||
(page.contradictions?.length ?? 0) * 6 +
|
||||
(page.questions?.length ?? 0) * 4 +
|
||||
Math.min(page.claimCount ?? 0, 6) * 2 +
|
||||
Math.min(page.topClaims?.length ?? 0, 3)
|
||||
);
|
||||
}
|
||||
|
||||
function rankPromptClaimFreshness(level?: string): number {
|
||||
switch (level) {
|
||||
case "fresh":
|
||||
return 3;
|
||||
case "aging":
|
||||
return 2;
|
||||
case "stale":
|
||||
return 1;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function sortPromptClaims(claims: PromptDigestClaim[]): PromptDigestClaim[] {
|
||||
return [...claims].toSorted((left, right) => {
|
||||
const leftConfidence = typeof left.confidence === "number" ? left.confidence : -1;
|
||||
const rightConfidence = typeof right.confidence === "number" ? right.confidence : -1;
|
||||
if (leftConfidence !== rightConfidence) {
|
||||
return rightConfidence - leftConfidence;
|
||||
}
|
||||
const leftFreshness = rankPromptClaimFreshness(left.freshnessLevel);
|
||||
const rightFreshness = rankPromptClaimFreshness(right.freshnessLevel);
|
||||
if (leftFreshness !== rightFreshness) {
|
||||
return rightFreshness - leftFreshness;
|
||||
}
|
||||
return left.text.localeCompare(right.text);
|
||||
});
|
||||
}
|
||||
|
||||
function formatPromptClaim(claim: PromptDigestClaim): string {
|
||||
const qualifiers = [
|
||||
claim.status?.trim() ? `status ${claim.status.trim()}` : null,
|
||||
typeof claim.confidence === "number" ? `confidence ${claim.confidence.toFixed(2)}` : null,
|
||||
claim.freshnessLevel?.trim() ? `freshness ${claim.freshnessLevel.trim()}` : null,
|
||||
].filter(Boolean);
|
||||
if (qualifiers.length === 0) {
|
||||
return claim.text;
|
||||
}
|
||||
return `${claim.text} (${qualifiers.join(", ")})`;
|
||||
}
|
||||
|
||||
function buildDigestPromptSection(config: ResolvedMemoryWikiConfig): string[] {
|
||||
if (!config.context.includeCompiledDigestPrompt) {
|
||||
return [];
|
||||
}
|
||||
const digest = tryReadPromptDigest(config);
|
||||
if (!digest?.pages?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const selectedPages = [...digest.pages]
|
||||
.filter(
|
||||
(page) =>
|
||||
(page.claimCount ?? 0) > 0 ||
|
||||
(page.questions?.length ?? 0) > 0 ||
|
||||
(page.contradictions?.length ?? 0) > 0,
|
||||
)
|
||||
.toSorted((left, right) => {
|
||||
const leftScore = rankPromptDigestPage(left);
|
||||
const rightScore = rankPromptDigestPage(right);
|
||||
if (leftScore !== rightScore) {
|
||||
return rightScore - leftScore;
|
||||
}
|
||||
return left.title.localeCompare(right.title);
|
||||
})
|
||||
.slice(0, DIGEST_MAX_PAGES);
|
||||
|
||||
if (selectedPages.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const lines = [
|
||||
"## Compiled Wiki Snapshot",
|
||||
`Compiled wiki currently tracks ${digest.claimCount ?? 0} claims across ${selectedPages.length} high-signal pages.`,
|
||||
];
|
||||
if (Array.isArray(digest.contradictionClusters)) {
|
||||
lines.push(`Contradiction clusters: ${digest.contradictionClusters.length}.`);
|
||||
}
|
||||
for (const page of selectedPages) {
|
||||
const details = [
|
||||
page.kind,
|
||||
`${page.claimCount} claims`,
|
||||
(page.questions?.length ?? 0) > 0 ? `${page.questions?.length} open questions` : null,
|
||||
(page.contradictions?.length ?? 0) > 0
|
||||
? `${page.contradictions?.length} contradiction notes`
|
||||
: null,
|
||||
].filter(Boolean);
|
||||
lines.push(`- ${page.title}: ${details.join(", ")}`);
|
||||
for (const claim of sortPromptClaims(page.topClaims ?? []).slice(
|
||||
0,
|
||||
DIGEST_MAX_CLAIMS_PER_PAGE,
|
||||
)) {
|
||||
lines.push(` - ${formatPromptClaim(claim)}`);
|
||||
}
|
||||
}
|
||||
lines.push("");
|
||||
return lines;
|
||||
}
|
||||
|
||||
function buildWikiToolGuidance(availableTools: Set<string>): string[] {
|
||||
const hasMemorySearch = availableTools.has("memory_search");
|
||||
const hasMemoryGet = availableTools.has("memory_get");
|
||||
const hasWikiSearch = availableTools.has("wiki_search");
|
||||
const hasWikiGet = availableTools.has("wiki_get");
|
||||
const hasWikiApply = availableTools.has("wiki_apply");
|
||||
const hasWikiLint = availableTools.has("wiki_lint");
|
||||
|
||||
if (
|
||||
!hasMemorySearch &&
|
||||
!hasMemoryGet &&
|
||||
!hasWikiSearch &&
|
||||
!hasWikiGet &&
|
||||
!hasWikiApply &&
|
||||
!hasWikiLint
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const lines = [
|
||||
"## Compiled Wiki",
|
||||
"Use the wiki when the answer depends on accumulated project knowledge, prior syntheses, entity pages, or source-backed notes that should survive beyond one conversation.",
|
||||
];
|
||||
|
||||
if (hasMemorySearch) {
|
||||
lines.push(
|
||||
"Prefer `memory_search` with `corpus=all` for one recall pass across durable memory and the compiled wiki when both are relevant.",
|
||||
);
|
||||
}
|
||||
if (hasMemoryGet) {
|
||||
lines.push(
|
||||
"Use `memory_get` with `corpus=wiki` or `corpus=all` when you already know the page path and want a small excerpt without leaving the shared memory tool flow.",
|
||||
);
|
||||
}
|
||||
|
||||
if (hasWikiSearch && hasWikiGet) {
|
||||
lines.push(
|
||||
"Workflow: `wiki_search` first, then `wiki_get` for the exact page or imported memory file you need. Use this when you want wiki-specific ranking or provenance details instead of the broader shared memory flow.",
|
||||
);
|
||||
} else if (hasWikiSearch) {
|
||||
lines.push(
|
||||
"Use `wiki_search` before answering from stored knowledge when you want wiki-specific ranking or provenance details.",
|
||||
);
|
||||
} else if (hasWikiGet) {
|
||||
lines.push(
|
||||
"Use `wiki_get` to inspect specific wiki pages or imported memory files by path/id.",
|
||||
);
|
||||
}
|
||||
|
||||
if (hasWikiApply) {
|
||||
lines.push(
|
||||
"Use `wiki_apply` for narrow synthesis filing and metadata repair instead of rewriting managed markdown blocks by hand.",
|
||||
);
|
||||
}
|
||||
if (hasWikiLint) {
|
||||
lines.push("After meaningful wiki updates, run `wiki_lint` before trusting the vault.");
|
||||
}
|
||||
lines.push("");
|
||||
return lines;
|
||||
}
|
||||
|
||||
export function createWikiPromptSectionBuilder(
|
||||
config: ResolvedMemoryWikiConfig,
|
||||
): MemoryPromptSectionBuilder {
|
||||
return ({ availableTools }) => {
|
||||
const digestLines = buildDigestPromptSection(config);
|
||||
const toolGuidance = buildWikiToolGuidance(availableTools);
|
||||
if (digestLines.length === 0 && toolGuidance.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return [...toolGuidance, ...digestLines];
|
||||
};
|
||||
}
|
||||
|
||||
export const buildWikiPromptSection: MemoryPromptSectionBuilder = ({ availableTools }) =>
|
||||
createWikiPromptSectionBuilder(
|
||||
resolveMemoryWikiConfig({
|
||||
vault: { path: "" },
|
||||
context: { includeCompiledDigestPrompt: false },
|
||||
}),
|
||||
)({ availableTools });
|
||||
Loading…
Add table
Add a link
Reference in a new issue