Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion examples/openclaw-plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -840,10 +840,12 @@ const mergeFindResults = (results: FindResult[]): FindResult => {
label: "Import (OpenViking)",
description:
"Import an OpenViking resource or skill only when the user explicitly asks to import, add, or index one. " +
"When the user asks to save, upload, add, or import an attached document from a '[media attached: /path ...]' message, " +
"call this tool with source set to that exact local media path. Do not invent OpenViking upload REST endpoints. " +
"Defaults to resource; set kind=skill for SKILL.md, skill directories, raw skill content, or MCP tool dicts.",
parameters: Type.Object({
kind: Type.Optional(Type.Union([Type.Literal("resource"), Type.Literal("skill")], { description: "Import kind. Default: resource" })),
source: Type.Optional(Type.String({ description: "Local path, directory path, public URL, or Git URL" })),
source: Type.Optional(Type.String({ description: "Local path, OpenClaw media attachment path, directory path, public URL, or Git URL" })),
data: Type.Optional(Type.Any({ description: "Skill only: raw SKILL.md content or MCP tool dict" })),
to: Type.Optional(Type.String({ description: "Resource only: exact target URI, e.g. viking://resources/project-docs" })),
parent: Type.Optional(Type.String({ description: "Resource only: parent URI under viking://resources" })),
Expand Down
41 changes: 41 additions & 0 deletions examples/openclaw-plugin/tests/ut/tools.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";

import contextEnginePlugin, {
parseOvImportCommandArgs,
Expand Down Expand Up @@ -342,9 +345,12 @@ describe("Tool: ov_import and memory_search (registration)", () => {
const tool = tools.get("ov_import");
expect(tool).toBeDefined();
expect(tool!.description).toContain("explicitly asks");
expect(tool!.description).toContain("[media attached: /path");
expect(tool!.description).toContain("Do not invent OpenViking upload REST endpoints");
const props = (tool!.parameters as any).properties;
expect(props).toHaveProperty("kind");
expect(props).toHaveProperty("source");
expect(props.source.description).toContain("OpenClaw media attachment path");
expect(props).toHaveProperty("data");
expect(props).toHaveProperty("to");
expect(props).toHaveProperty("parent");
Expand Down Expand Up @@ -730,6 +736,41 @@ describe("Plugin registration", () => {
expect(headers.get("X-OpenViking-User")).toBe("alice");
});

it("import tool uploads local media attachment paths as resources", async () => {
const tempDir = await mkdtemp(join(tmpdir(), "openclaw-media-"));
const filePath = join(tempDir, "大秦-TOP20.xlsx");
await writeFile(filePath, "spreadsheet bytes");

const fetchMock = vi
.fn()
.mockResolvedValueOnce(okResponse({ temp_file_id: "upload_sheet.xlsx" }))
.mockResolvedValueOnce(okResponse({ root_uri: "viking://resources/sheet", status: "success" }));
vi.stubGlobal("fetch", fetchMock);

try {
const { tools, api } = setupPlugin();
contextEnginePlugin.register(api as any);

const tool = tools.get("ov_import")!;
const result = await tool.execute("tc-import-local-media", {
kind: "resource",
source: filePath,
wait: true,
}) as ToolResult;

expect(result.content[0]!.text).toContain("Imported OpenViking resource");
expect(fetchMock.mock.calls[0]![0]).toBe("http://127.0.0.1:1933/api/v1/resources/temp_upload");
expect(fetchMock.mock.calls[1]![0]).toBe("http://127.0.0.1:1933/api/v1/resources");
const body = JSON.parse(String(fetchMock.mock.calls[1]![1]!.body));
expect(body).toMatchObject({
temp_file_id: "upload_sheet.xlsx",
wait: true,
});
} finally {
await rm(tempDir, { recursive: true, force: true });
}
});

it("slash commands honor bypassSessionPatterns", async () => {
const fetchMock = vi.fn(async () => okResponse({}));
vi.stubGlobal("fetch", fetchMock);
Expand Down