mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-03 07:40:22 +00:00
feat(memory-wiki): add gateway control methods
This commit is contained in:
111
extensions/memory-wiki/src/gateway.test.ts
Normal file
111
extensions/memory-wiki/src/gateway.test.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createTestPluginApi } from "../../../test/helpers/plugins/plugin-api.js";
|
||||
import type { OpenClawPluginApi } from "../api.js";
|
||||
import { resolveMemoryWikiConfig } from "./config.js";
|
||||
import { registerMemoryWikiGatewayMethods } from "./gateway.js";
|
||||
import { renderWikiMarkdown } from "./markdown.js";
|
||||
import { initializeMemoryWikiVault } from "./vault.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })));
|
||||
});
|
||||
|
||||
function createGatewayApi() {
|
||||
const registerGatewayMethod = vi.fn();
|
||||
const api = createTestPluginApi({
|
||||
id: "memory-wiki",
|
||||
name: "Memory Wiki",
|
||||
source: "test",
|
||||
config: {},
|
||||
runtime: {} as OpenClawPluginApi["runtime"],
|
||||
registerGatewayMethod,
|
||||
}) as OpenClawPluginApi;
|
||||
return { api, registerGatewayMethod };
|
||||
}
|
||||
|
||||
function findGatewayHandler(
|
||||
registerGatewayMethod: ReturnType<typeof vi.fn>,
|
||||
method: string,
|
||||
):
|
||||
| ((ctx: {
|
||||
params: Record<string, unknown>;
|
||||
respond: (ok: boolean, payload?: unknown, error?: unknown) => void;
|
||||
}) => Promise<void>)
|
||||
| undefined {
|
||||
return registerGatewayMethod.mock.calls.find((call) => call[0] === method)?.[1];
|
||||
}
|
||||
|
||||
describe("memory-wiki gateway methods", () => {
|
||||
it("returns wiki status over the gateway", async () => {
|
||||
const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "memory-wiki-gateway-"));
|
||||
tempDirs.push(rootDir);
|
||||
const { api, registerGatewayMethod } = createGatewayApi();
|
||||
const config = resolveMemoryWikiConfig(
|
||||
{ vault: { path: rootDir } },
|
||||
{ homedir: "/Users/tester" },
|
||||
);
|
||||
await initializeMemoryWikiVault(config);
|
||||
|
||||
registerMemoryWikiGatewayMethods({ api, config });
|
||||
const handler = findGatewayHandler(registerGatewayMethod, "wiki.status");
|
||||
if (!handler) {
|
||||
throw new Error("wiki.status handler missing");
|
||||
}
|
||||
const respond = vi.fn();
|
||||
|
||||
await handler({
|
||||
params: {},
|
||||
respond,
|
||||
});
|
||||
|
||||
expect(respond).toHaveBeenCalledWith(
|
||||
true,
|
||||
expect.objectContaining({
|
||||
vaultMode: "isolated",
|
||||
vaultExists: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("validates required query params for wiki.search", async () => {
|
||||
const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "memory-wiki-gateway-"));
|
||||
tempDirs.push(rootDir);
|
||||
const { api, registerGatewayMethod } = createGatewayApi();
|
||||
const config = resolveMemoryWikiConfig(
|
||||
{ vault: { path: rootDir } },
|
||||
{ homedir: "/Users/tester" },
|
||||
);
|
||||
await initializeMemoryWikiVault(config);
|
||||
await fs.writeFile(
|
||||
path.join(rootDir, "sources", "alpha.md"),
|
||||
renderWikiMarkdown({
|
||||
frontmatter: { pageType: "source", id: "source.alpha", title: "Alpha" },
|
||||
body: "# Alpha\n",
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
registerMemoryWikiGatewayMethods({ api, config });
|
||||
const handler = findGatewayHandler(registerGatewayMethod, "wiki.search");
|
||||
if (!handler) {
|
||||
throw new Error("wiki.search handler missing");
|
||||
}
|
||||
const respond = vi.fn();
|
||||
|
||||
await handler({
|
||||
params: {},
|
||||
respond,
|
||||
});
|
||||
|
||||
expect(respond).toHaveBeenCalledWith(
|
||||
false,
|
||||
undefined,
|
||||
expect.objectContaining({ message: "query is required." }),
|
||||
);
|
||||
});
|
||||
});
|
||||
178
extensions/memory-wiki/src/gateway.ts
Normal file
178
extensions/memory-wiki/src/gateway.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import type { OpenClawConfig, OpenClawPluginApi } from "../api.js";
|
||||
import { compileMemoryWikiVault } from "./compile.js";
|
||||
import type { ResolvedMemoryWikiConfig } from "./config.js";
|
||||
import { lintMemoryWikiVault } from "./lint.js";
|
||||
import { probeObsidianCli } from "./obsidian.js";
|
||||
import { getMemoryWikiPage, searchMemoryWiki } from "./query.js";
|
||||
import { syncMemoryWikiImportedSources } from "./source-sync.js";
|
||||
import { buildMemoryWikiDoctorReport, resolveMemoryWikiStatus } from "./status.js";
|
||||
|
||||
const READ_SCOPE = "operator.read" as const;
|
||||
const WRITE_SCOPE = "operator.write" as const;
|
||||
|
||||
function readStringParam(
|
||||
params: Record<string, unknown>,
|
||||
key: string,
|
||||
options?: { required?: boolean },
|
||||
): string | undefined {
|
||||
const value = params[key];
|
||||
if (typeof value === "string" && value.trim()) {
|
||||
return value.trim();
|
||||
}
|
||||
if (options?.required) {
|
||||
throw new Error(`${key} is required.`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function readNumberParam(params: Record<string, unknown>, key: string): number | undefined {
|
||||
const value = params[key];
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === "string" && value.trim()) {
|
||||
const parsed = Number(value);
|
||||
if (Number.isFinite(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function respondError(
|
||||
respond: Parameters<OpenClawPluginApi["registerGatewayMethod"]>[1] extends (
|
||||
ctx: infer T,
|
||||
) => unknown
|
||||
? T["respond"]
|
||||
: never,
|
||||
error: unknown,
|
||||
) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
respond(false, undefined, { message });
|
||||
}
|
||||
|
||||
async function syncImportedSourcesIfNeeded(
|
||||
config: ResolvedMemoryWikiConfig,
|
||||
appConfig?: OpenClawConfig,
|
||||
) {
|
||||
await syncMemoryWikiImportedSources({ config, appConfig });
|
||||
}
|
||||
|
||||
export function registerMemoryWikiGatewayMethods(params: {
|
||||
api: OpenClawPluginApi;
|
||||
config: ResolvedMemoryWikiConfig;
|
||||
appConfig?: OpenClawConfig;
|
||||
}) {
|
||||
const { api, config, appConfig } = params;
|
||||
|
||||
api.registerGatewayMethod(
|
||||
"wiki.status",
|
||||
async ({ respond }) => {
|
||||
try {
|
||||
await syncImportedSourcesIfNeeded(config, appConfig);
|
||||
respond(true, await resolveMemoryWikiStatus(config));
|
||||
} catch (error) {
|
||||
respondError(respond, error);
|
||||
}
|
||||
},
|
||||
{ scope: READ_SCOPE },
|
||||
);
|
||||
|
||||
api.registerGatewayMethod(
|
||||
"wiki.doctor",
|
||||
async ({ respond }) => {
|
||||
try {
|
||||
await syncImportedSourcesIfNeeded(config, appConfig);
|
||||
const status = await resolveMemoryWikiStatus(config);
|
||||
respond(true, buildMemoryWikiDoctorReport(status));
|
||||
} catch (error) {
|
||||
respondError(respond, error);
|
||||
}
|
||||
},
|
||||
{ scope: READ_SCOPE },
|
||||
);
|
||||
|
||||
api.registerGatewayMethod(
|
||||
"wiki.compile",
|
||||
async ({ respond }) => {
|
||||
try {
|
||||
await syncImportedSourcesIfNeeded(config, appConfig);
|
||||
respond(true, await compileMemoryWikiVault(config));
|
||||
} catch (error) {
|
||||
respondError(respond, error);
|
||||
}
|
||||
},
|
||||
{ scope: WRITE_SCOPE },
|
||||
);
|
||||
|
||||
api.registerGatewayMethod(
|
||||
"wiki.lint",
|
||||
async ({ respond }) => {
|
||||
try {
|
||||
await syncImportedSourcesIfNeeded(config, appConfig);
|
||||
respond(true, await lintMemoryWikiVault(config));
|
||||
} catch (error) {
|
||||
respondError(respond, error);
|
||||
}
|
||||
},
|
||||
{ scope: WRITE_SCOPE },
|
||||
);
|
||||
|
||||
api.registerGatewayMethod(
|
||||
"wiki.search",
|
||||
async ({ params: requestParams, respond }) => {
|
||||
try {
|
||||
await syncImportedSourcesIfNeeded(config, appConfig);
|
||||
const query = readStringParam(requestParams, "query", { required: true });
|
||||
const maxResults = readNumberParam(requestParams, "maxResults");
|
||||
respond(
|
||||
true,
|
||||
await searchMemoryWiki({
|
||||
config,
|
||||
query,
|
||||
maxResults,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
respondError(respond, error);
|
||||
}
|
||||
},
|
||||
{ scope: READ_SCOPE },
|
||||
);
|
||||
|
||||
api.registerGatewayMethod(
|
||||
"wiki.get",
|
||||
async ({ params: requestParams, respond }) => {
|
||||
try {
|
||||
await syncImportedSourcesIfNeeded(config, appConfig);
|
||||
const lookup = readStringParam(requestParams, "lookup", { required: true });
|
||||
const fromLine = readNumberParam(requestParams, "fromLine");
|
||||
const lineCount = readNumberParam(requestParams, "lineCount");
|
||||
respond(
|
||||
true,
|
||||
await getMemoryWikiPage({
|
||||
config,
|
||||
lookup,
|
||||
fromLine,
|
||||
lineCount,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
respondError(respond, error);
|
||||
}
|
||||
},
|
||||
{ scope: READ_SCOPE },
|
||||
);
|
||||
|
||||
api.registerGatewayMethod(
|
||||
"wiki.obsidian.status",
|
||||
async ({ respond }) => {
|
||||
try {
|
||||
respond(true, await probeObsidianCli());
|
||||
} catch (error) {
|
||||
respondError(respond, error);
|
||||
}
|
||||
},
|
||||
{ scope: READ_SCOPE },
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user