UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

412 lines (411 loc) 18 kB
import { n as readJsonFileWithFallback } from "./json-store-CWaMsrLM.js"; import path from "node:path"; import fs from "node:fs/promises"; import { createHash } from "node:crypto"; //#region extensions/memory-wiki/src/import-runs-state.ts const MEMORY_WIKI_IMPORT_RUN_STATE_NAMESPACE = "import-runs"; const MEMORY_WIKI_IMPORT_RUN_STATE_MAX_ENTRIES = 2e4; let configuredImportRunStore; const memoryImportRunsByVault = /* @__PURE__ */ new Map(); function resolveMemoryWikiImportRunsDir(vaultRoot) { return path.join(vaultRoot, ".openclaw-wiki", "import-runs"); } function resolveVaultRootKey$1(vaultRoot) { return createHash("sha256").update(path.resolve(vaultRoot), "utf8").digest("hex").slice(0, 32); } function resolveStateEntryKey$1(vaultRootKey, runId) { return createHash("sha256").update(`${vaultRootKey}\0meta\0${runId}`, "utf8").digest("hex"); } function resolvePathStateEntryKey(params) { return createHash("sha256").update(`${params.vaultRootKey}\0${params.runId}\0${params.kind}\0${params.index}\0${params.path}`, "utf8").digest("hex"); } function cloneImportRunRecord(record) { return { ...record, createdPaths: [...record.createdPaths], updatedPaths: record.updatedPaths.map((entry) => ({ ...entry })) }; } function asRecord(value) { if (!value || typeof value !== "object" || Array.isArray(value)) return null; return value; } function asStringArray(value) { if (!Array.isArray(value)) return []; return value.filter((entry) => typeof entry === "string" && entry.trim().length > 0); } function asNonNegativeInteger(value) { return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0; } function normalizeMemoryWikiImportRunRecord(raw) { const record = asRecord(raw); if (!record) return null; const runId = typeof record.runId === "string" ? record.runId.trim() : ""; const exportPath = typeof record.exportPath === "string" ? record.exportPath.trim() : ""; const sourcePath = typeof record.sourcePath === "string" ? record.sourcePath.trim() : ""; const appliedAt = typeof record.appliedAt === "string" ? record.appliedAt.trim() : ""; if (record.version !== 1 || record.importType !== "chatgpt" || !runId || !exportPath || !sourcePath || !appliedAt) return null; const updatedPaths = Array.isArray(record.updatedPaths) ? record.updatedPaths.map((entry) => asRecord(entry)).filter((entry) => entry !== null).flatMap((entry) => { const entryPath = typeof entry.path === "string" ? entry.path.trim() : ""; if (!entryPath) return []; const snapshotPath = typeof entry.snapshotPath === "string" && entry.snapshotPath.trim() ? entry.snapshotPath.trim() : void 0; return [{ path: entryPath, ...snapshotPath ? { snapshotPath } : {} }]; }) : []; const rolledBackAt = typeof record.rolledBackAt === "string" && record.rolledBackAt.trim() ? record.rolledBackAt.trim() : void 0; return { version: 1, runId, importType: "chatgpt", exportPath, sourcePath, appliedAt, conversationCount: asNonNegativeInteger(record.conversationCount), createdCount: asNonNegativeInteger(record.createdCount), updatedCount: asNonNegativeInteger(record.updatedCount), skippedCount: asNonNegativeInteger(record.skippedCount), createdPaths: asStringArray(record.createdPaths), updatedPaths, ...rolledBackAt ? { rolledBackAt } : {} }; } function normalizeMetaRecord(raw) { const record = asRecord(raw); if (!record || record.kind !== "meta") return null; const normalized = normalizeMemoryWikiImportRunRecord({ ...record, createdPaths: [], updatedPaths: [] }); const vaultRootKey = typeof record.vaultRootKey === "string" ? record.vaultRootKey : ""; return normalized && vaultRootKey ? { ...normalized, kind: "meta", vaultRootKey } : null; } function normalizePathRecord(raw) { const record = asRecord(raw); if (!record || record.kind !== "created-path" && record.kind !== "updated-path" || typeof record.vaultRootKey !== "string" || typeof record.runId !== "string" || typeof record.path !== "string" || typeof record.index !== "number" || !Number.isFinite(record.index)) return null; const snapshotPath = typeof record.snapshotPath === "string" && record.snapshotPath.trim() ? record.snapshotPath.trim() : void 0; return { kind: record.kind, vaultRootKey: record.vaultRootKey, runId: record.runId, index: Math.max(0, Math.floor(record.index)), path: record.path, ...snapshotPath ? { snapshotPath } : {} }; } function composeImportRunRecord(meta, pathRows) { const createdPaths = pathRows.filter((row) => row.kind === "created-path").toSorted((left, right) => left.index - right.index).map((row) => row.path); const updatedPaths = pathRows.filter((row) => row.kind === "updated-path").toSorted((left, right) => left.index - right.index).map((row) => { const entry = { path: row.path }; if (row.snapshotPath) entry.snapshotPath = row.snapshotPath; return entry; }); return { version: 1, runId: meta.runId, importType: "chatgpt", exportPath: meta.exportPath, sourcePath: meta.sourcePath, appliedAt: meta.appliedAt, conversationCount: meta.conversationCount, createdCount: meta.createdCount, updatedCount: meta.updatedCount, skippedCount: meta.skippedCount, createdPaths, updatedPaths, ...meta.rolledBackAt ? { rolledBackAt: meta.rolledBackAt } : {} }; } function toMetaRecord(vaultRootKey, record) { return { version: 1, kind: "meta", vaultRootKey, runId: record.runId, importType: "chatgpt", exportPath: record.exportPath, sourcePath: record.sourcePath, appliedAt: record.appliedAt, conversationCount: record.conversationCount, createdCount: record.createdCount, updatedCount: record.updatedCount, skippedCount: record.skippedCount, ...record.rolledBackAt ? { rolledBackAt: record.rolledBackAt } : {} }; } function toPathRecords(vaultRootKey, record) { return [...record.createdPaths.map((entryPath, index) => ({ kind: "created-path", vaultRootKey, runId: record.runId, index, path: entryPath })), ...record.updatedPaths.map((entry, index) => ({ kind: "updated-path", vaultRootKey, runId: record.runId, index, path: entry.path, ...entry.snapshotPath ? { snapshotPath: entry.snapshotPath } : {} }))]; } function createMemoryFallbackImportRunStore() { return { async read(vaultRoot, runId) { const vaultRootKey = resolveVaultRootKey$1(vaultRoot); const record = memoryImportRunsByVault.get(vaultRootKey)?.get(runId); return record ? cloneImportRunRecord(record) : null; }, async write(vaultRoot, record) { const vaultRootKey = resolveVaultRootKey$1(vaultRoot); const records = memoryImportRunsByVault.get(vaultRootKey) ?? /* @__PURE__ */ new Map(); records.set(record.runId, cloneImportRunRecord(record)); memoryImportRunsByVault.set(vaultRootKey, records); }, async list(vaultRoot) { const vaultRootKey = resolveVaultRootKey$1(vaultRoot); return [...memoryImportRunsByVault.get(vaultRootKey)?.values() ?? []].map(cloneImportRunRecord); }, async rowCount() { let count = 0; for (const records of memoryImportRunsByVault.values()) for (const record of records.values()) count += 1 + record.createdPaths.length + record.updatedPaths.length; return count; } }; } function createMemoryWikiImportRunStateStore(openKeyedStore) { const openStore = () => openKeyedStore({ namespace: MEMORY_WIKI_IMPORT_RUN_STATE_NAMESPACE, maxEntries: MEMORY_WIKI_IMPORT_RUN_STATE_MAX_ENTRIES }); return { async read(vaultRoot, runId) { const vaultRootKey = resolveVaultRootKey$1(vaultRoot); const meta = normalizeMetaRecord(await openStore().lookup(resolveStateEntryKey$1(vaultRootKey, runId))); if (!meta || meta.vaultRootKey !== vaultRootKey) return null; return composeImportRunRecord(meta, (await openStore().entries()).map((entry) => normalizePathRecord(entry.value)).filter((entry) => entry !== null && entry.vaultRootKey === vaultRootKey && entry.runId === runId)); }, async write(vaultRoot, record) { const vaultRootKey = resolveVaultRootKey$1(vaultRoot); const store = openStore(); await store.register(resolveStateEntryKey$1(vaultRootKey, record.runId), toMetaRecord(vaultRootKey, record)); const nextPathKeys = /* @__PURE__ */ new Set(); for (const pathRecord of toPathRecords(vaultRootKey, record)) { const key = resolvePathStateEntryKey({ vaultRootKey, runId: record.runId, kind: pathRecord.kind, index: pathRecord.index, path: pathRecord.path }); nextPathKeys.add(key); await store.register(key, pathRecord); } for (const row of await store.entries()) { const pathRecord = normalizePathRecord(row.value); if (pathRecord?.vaultRootKey === vaultRootKey && pathRecord.runId === record.runId && !nextPathKeys.has(row.key)) await store.delete(row.key); } }, async list(vaultRoot) { const vaultRootKey = resolveVaultRootKey$1(vaultRoot); const metaRows = /* @__PURE__ */ new Map(); const pathRows = []; for (const row of await openStore().entries()) { const meta = normalizeMetaRecord(row.value); if (meta?.vaultRootKey === vaultRootKey) { metaRows.set(meta.runId, meta); continue; } const pathRecord = normalizePathRecord(row.value); if (pathRecord?.vaultRootKey === vaultRootKey) pathRows.push(pathRecord); } return [...metaRows.values()].map((meta) => composeImportRunRecord(meta, pathRows.filter((row) => row.runId === meta.runId))); }, async rowCount() { return (await openStore().entries()).length; } }; } function configureMemoryWikiImportRunStateStore(store) { configuredImportRunStore = store; } function resolveImportRunStore(store) { return store ?? configuredImportRunStore ?? createMemoryFallbackImportRunStore(); } async function readMemoryWikiImportRunRecord(vaultRoot, runId, store) { return await resolveImportRunStore(store).read(vaultRoot, runId); } async function writeMemoryWikiImportRunRecord(vaultRoot, record, store) { await resolveImportRunStore(store).write(vaultRoot, record); } async function listMemoryWikiImportRunRecords(vaultRoot, store) { return await resolveImportRunStore(store).list(vaultRoot); } async function countMemoryWikiImportRunStateRows(store) { return await resolveImportRunStore(store).rowCount(); } async function readLegacyMemoryWikiImportRunRecords(vaultRoot) { const importRunsDir = resolveMemoryWikiImportRunsDir(vaultRoot); const entries = await fs.readdir(importRunsDir, { withFileTypes: true }).catch((error) => { if (asRecord(error)?.code === "ENOENT") return []; throw error; }); return (await Promise.all(entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map(async (entry) => { const raw = await fs.readFile(path.join(importRunsDir, entry.name), "utf8"); return normalizeMemoryWikiImportRunRecord(JSON.parse(raw)); }))).filter((entry) => entry !== null); } //#endregion //#region extensions/memory-wiki/src/source-sync-state.ts const MEMORY_WIKI_SOURCE_SYNC_STATE_NAMESPACE = "source-sync"; const MEMORY_WIKI_SOURCE_SYNC_STATE_MAX_ENTRIES = 2e4; const EMPTY_STATE = { version: 1, entries: {} }; let configuredSourceSyncStore; const memorySourceSyncStateByVault = /* @__PURE__ */ new Map(); function resolveMemoryWikiSourceSyncStatePath(vaultRoot) { return path.join(vaultRoot, ".openclaw-wiki", "source-sync.json"); } function cloneSourceSyncState(state) { return { version: 1, entries: Object.fromEntries(Object.entries(state.entries).map(([key, value]) => [key, { ...value }])) }; } function normalizeSourceSyncState(value) { if (!value || typeof value !== "object" || Array.isArray(value)) return EMPTY_STATE; const parsed = value; if (parsed.version !== 1 || !parsed.entries || typeof parsed.entries !== "object") return EMPTY_STATE; const entries = {}; for (const [syncKey, entry] of Object.entries(parsed.entries)) { if (!entry || typeof entry !== "object" || Array.isArray(entry) || entry.group !== "bridge" && entry.group !== "unsafe-local" || typeof entry.pagePath !== "string" || typeof entry.sourcePath !== "string" || typeof entry.sourceUpdatedAtMs !== "number" || typeof entry.sourceSize !== "number" || typeof entry.renderFingerprint !== "string") continue; entries[syncKey] = { group: entry.group, pagePath: entry.pagePath, sourcePath: entry.sourcePath, sourceUpdatedAtMs: entry.sourceUpdatedAtMs, sourceSize: entry.sourceSize, renderFingerprint: entry.renderFingerprint }; } return { version: 1, entries }; } function resolveVaultRootKey(vaultRoot) { return createHash("sha256").update(path.resolve(vaultRoot), "utf8").digest("hex").slice(0, 32); } function resolveStateEntryKey(vaultRootKey, syncKey) { return createHash("sha256").update(`${vaultRootKey}\0${syncKey}`, "utf8").digest("hex"); } function createMemoryFallbackStateStore() { return { async read(vaultRoot) { const vaultRootKey = resolveVaultRootKey(vaultRoot); return cloneSourceSyncState(memorySourceSyncStateByVault.get(vaultRootKey) ?? EMPTY_STATE); }, async write(vaultRoot, state) { assertSourceSyncStateWithinLimit(state); const vaultRootKey = resolveVaultRootKey(vaultRoot); memorySourceSyncStateByVault.set(vaultRootKey, cloneSourceSyncState(state)); } }; } function assertSourceSyncStateWithinLimit(state) { const count = Object.keys(state.entries).length; if (count > 2e4) throw new Error(`Memory Wiki source sync state exceeds SQLite entry limit (${count}/${MEMORY_WIKI_SOURCE_SYNC_STATE_MAX_ENTRIES})`); } function assertMemoryWikiSourceSyncStateCapacity(params) { const projectedCount = Object.values(params.state.entries).filter((entry) => entry.group !== params.group).length + params.incomingCount; if (projectedCount > 2e4) throw new Error(`Memory Wiki source sync state exceeds SQLite entry limit (${projectedCount}/${MEMORY_WIKI_SOURCE_SYNC_STATE_MAX_ENTRIES})`); } function createMemoryWikiSourceSyncStateStore(openKeyedStore) { const openStore = () => openKeyedStore({ namespace: MEMORY_WIKI_SOURCE_SYNC_STATE_NAMESPACE, maxEntries: MEMORY_WIKI_SOURCE_SYNC_STATE_MAX_ENTRIES }); return { async read(vaultRoot) { const vaultRootKey = resolveVaultRootKey(vaultRoot); const entries = {}; for (const row of await openStore().entries()) { const value = row.value; if (value.vaultRootKey !== vaultRootKey || typeof value.syncKey !== "string") continue; const entry = normalizeSourceSyncState({ version: 1, entries: { [value.syncKey]: value } }).entries[value.syncKey]; if (entry) entries[value.syncKey] = entry; } return { version: 1, entries }; }, async write(vaultRoot, state) { assertSourceSyncStateWithinLimit(state); const vaultRootKey = resolveVaultRootKey(vaultRoot); const store = openStore(); const normalized = normalizeSourceSyncState(state); const nextKeys = /* @__PURE__ */ new Set(); for (const [syncKey, entry] of Object.entries(normalized.entries)) { const key = resolveStateEntryKey(vaultRootKey, syncKey); nextKeys.add(key); await store.register(key, { ...entry, vaultRootKey, syncKey }); } for (const row of await store.entries()) if (row.value.vaultRootKey === vaultRootKey && !nextKeys.has(row.key)) await store.delete(row.key); } }; } function configureMemoryWikiSourceSyncStateStore(store) { configuredSourceSyncStore = store; } function resolveSourceSyncStore(store) { return store ?? configuredSourceSyncStore ?? createMemoryFallbackStateStore(); } async function readMemoryWikiSourceSyncState(vaultRoot, store) { return await resolveSourceSyncStore(store).read(vaultRoot); } async function readLegacyMemoryWikiSourceSyncState(vaultRoot) { const { value: parsed } = await readJsonFileWithFallback(resolveMemoryWikiSourceSyncStatePath(vaultRoot), EMPTY_STATE); return normalizeSourceSyncState(parsed); } async function writeMemoryWikiSourceSyncState(vaultRoot, state, store) { await resolveSourceSyncStore(store).write(vaultRoot, state); } async function shouldSkipImportedSourceWrite(params) { const entry = params.state.entries[params.syncKey]; if (!entry) return false; if (entry.pagePath !== params.expectedPagePath || entry.sourcePath !== params.expectedSourcePath || entry.sourceUpdatedAtMs !== params.sourceUpdatedAtMs || entry.sourceSize !== params.sourceSize || entry.renderFingerprint !== params.renderFingerprint) return false; const pagePath = path.join(params.vaultRoot, params.expectedPagePath); return await fs.access(pagePath).then(() => true).catch(() => false); } async function pruneImportedSourceEntries(params) { let removedCount = 0; for (const [syncKey, entry] of Object.entries(params.state.entries)) { if (entry.group !== params.group || params.activeKeys.has(syncKey)) continue; const pageAbsPath = path.join(params.vaultRoot, entry.pagePath); await fs.rm(pageAbsPath, { force: true }).catch(() => void 0); delete params.state.entries[syncKey]; removedCount += 1; } return removedCount; } function setImportedSourceEntry(params) { params.state.entries[params.syncKey] = params.entry; } //#endregion export { writeMemoryWikiImportRunRecord as S, createMemoryWikiImportRunStateStore as _, createMemoryWikiSourceSyncStateStore as a, readMemoryWikiImportRunRecord as b, readMemoryWikiSourceSyncState as c, shouldSkipImportedSourceWrite as d, writeMemoryWikiSourceSyncState as f, countMemoryWikiImportRunStateRows as g, configureMemoryWikiImportRunStateStore as h, configureMemoryWikiSourceSyncStateStore as i, resolveMemoryWikiSourceSyncStatePath as l, MEMORY_WIKI_IMPORT_RUN_STATE_NAMESPACE as m, MEMORY_WIKI_SOURCE_SYNC_STATE_NAMESPACE as n, pruneImportedSourceEntries as o, MEMORY_WIKI_IMPORT_RUN_STATE_MAX_ENTRIES as p, assertMemoryWikiSourceSyncStateCapacity as r, readLegacyMemoryWikiSourceSyncState as s, MEMORY_WIKI_SOURCE_SYNC_STATE_MAX_ENTRIES as t, setImportedSourceEntry as u, listMemoryWikiImportRunRecords as v, resolveMemoryWikiImportRunsDir as x, readLegacyMemoryWikiImportRunRecords as y };