refactor(extensions): reuse shared helper primitives

This commit is contained in:
Peter Steinberger
2026-03-07 10:40:57 +00:00
parent 3c71e2bd48
commit 1aa77e4603
58 changed files with 1567 additions and 2195 deletions

View File

@@ -1,6 +1,7 @@
import type { ChannelMeta, ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
import {
buildBaseChannelStatusSummary,
buildProbeChannelStatusSummary,
buildRuntimeAccountStatusSnapshot,
createDefaultChannelRuntimeState,
DEFAULT_ACCOUNT_ID,
PAIRING_APPROVED_MESSAGE,
@@ -54,6 +55,30 @@ const secretInputJsonSchema = {
],
} as const;
function setFeishuNamedAccountEnabled(
cfg: ClawdbotConfig,
accountId: string,
enabled: boolean,
): ClawdbotConfig {
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
return {
...cfg,
channels: {
...cfg.channels,
feishu: {
...feishuCfg,
accounts: {
...feishuCfg?.accounts,
[accountId]: {
...feishuCfg?.accounts?.[accountId],
enabled,
},
},
},
},
};
}
export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
id: "feishu",
meta: {
@@ -178,23 +203,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
}
// For named accounts, set enabled in accounts[accountId]
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
return {
...cfg,
channels: {
...cfg.channels,
feishu: {
...feishuCfg,
accounts: {
...feishuCfg?.accounts,
[accountId]: {
...feishuCfg?.accounts?.[accountId],
enabled,
},
},
},
},
};
return setFeishuNamedAccountEnabled(cfg, accountId, enabled);
},
deleteAccount: ({ cfg, accountId }) => {
const isDefault = accountId === DEFAULT_ACCOUNT_ID;
@@ -281,23 +290,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
};
}
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
return {
...cfg,
channels: {
...cfg.channels,
feishu: {
...feishuCfg,
accounts: {
...feishuCfg?.accounts,
[accountId]: {
...feishuCfg?.accounts?.[accountId],
enabled: true,
},
},
},
},
};
return setFeishuNamedAccountEnabled(cfg, accountId, true);
},
},
onboarding: feishuOnboardingAdapter,
@@ -342,12 +335,10 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
outbound: feishuOutbound,
status: {
defaultRuntime: createDefaultChannelRuntimeState(DEFAULT_ACCOUNT_ID, { port: null }),
buildChannelSummary: ({ snapshot }) => ({
...buildBaseChannelStatusSummary(snapshot),
port: snapshot.port ?? null,
probe: snapshot.probe,
lastProbeAt: snapshot.lastProbeAt ?? null,
}),
buildChannelSummary: ({ snapshot }) =>
buildProbeChannelStatusSummary(snapshot, {
port: snapshot.port ?? null,
}),
probeAccount: async ({ account }) => await probeFeishu(account),
buildAccountSnapshot: ({ account, runtime, probe }) => ({
accountId: account.accountId,
@@ -356,12 +347,8 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
name: account.name,
appId: account.appId,
domain: account.domain,
running: runtime?.running ?? false,
lastStartAt: runtime?.lastStartAt ?? null,
lastStopAt: runtime?.lastStopAt ?? null,
lastError: runtime?.lastError ?? null,
...buildRuntimeAccountStatusSnapshot({ runtime, probe }),
port: runtime?.port ?? null,
probe,
}),
},
gateway: {

View File

@@ -3,15 +3,11 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu";
import { listEnabledFeishuAccounts } from "./accounts.js";
import { FeishuDriveSchema, type FeishuDriveParams } from "./drive-schema.js";
import { createFeishuToolClient, resolveAnyEnabledFeishuToolsConfig } from "./tool-account.js";
// ============ Helpers ============
function json(data: unknown) {
return {
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
details: data,
};
}
import {
jsonToolResult,
toolExecutionErrorResult,
unknownToolActionResult,
} from "./tool-result.js";
// ============ Actions ============
@@ -206,21 +202,21 @@ export function registerFeishuDriveTools(api: OpenClawPluginApi) {
});
switch (p.action) {
case "list":
return json(await listFolder(client, p.folder_token));
return jsonToolResult(await listFolder(client, p.folder_token));
case "info":
return json(await getFileInfo(client, p.file_token));
return jsonToolResult(await getFileInfo(client, p.file_token));
case "create_folder":
return json(await createFolder(client, p.name, p.folder_token));
return jsonToolResult(await createFolder(client, p.name, p.folder_token));
case "move":
return json(await moveFile(client, p.file_token, p.type, p.folder_token));
return jsonToolResult(await moveFile(client, p.file_token, p.type, p.folder_token));
case "delete":
return json(await deleteFile(client, p.file_token, p.type));
return jsonToolResult(await deleteFile(client, p.file_token, p.type));
default:
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- exhaustive check fallback
return json({ error: `Unknown action: ${(p as any).action}` });
return unknownToolActionResult((p as { action?: unknown }).action);
}
} catch (err) {
return json({ error: err instanceof Error ? err.message : String(err) });
return toolExecutionErrorResult(err);
}
},
};

View File

@@ -51,6 +51,30 @@ function makeReactionEvent(
};
}
function createFetchedReactionMessage(chatId: string) {
return {
messageId: "om_msg1",
chatId,
senderOpenId: "ou_bot",
content: "hello",
contentType: "text",
};
}
async function resolveReactionWithLookup(params: {
event?: FeishuReactionCreatedEvent;
lookupChatId: string;
}) {
return await resolveReactionSyntheticEvent({
cfg,
accountId: "default",
event: params.event ?? makeReactionEvent(),
botOpenId: "ou_bot",
fetchMessage: async () => createFetchedReactionMessage(params.lookupChatId),
uuid: () => "fixed-uuid",
});
}
type FeishuMention = NonNullable<FeishuMessageEvent["message"]["mentions"]>[number];
function buildDebounceConfig(): ClawdbotConfig {
@@ -152,6 +176,30 @@ function getFirstDispatchedEvent(): FeishuMessageEvent {
return firstParams.event;
}
function setDedupPassThroughMocks(): void {
vi.spyOn(dedup, "tryRecordMessage").mockReturnValue(true);
vi.spyOn(dedup, "tryRecordMessagePersistent").mockResolvedValue(true);
vi.spyOn(dedup, "hasRecordedMessage").mockReturnValue(false);
vi.spyOn(dedup, "hasRecordedMessagePersistent").mockResolvedValue(false);
}
function createMention(params: { openId: string; name: string; key?: string }): FeishuMention {
return {
key: params.key ?? "@_user_1",
id: { open_id: params.openId },
name: params.name,
};
}
async function enqueueDebouncedMessage(
onMessage: (data: unknown) => Promise<void>,
event: FeishuMessageEvent,
): Promise<void> {
await onMessage(event);
await Promise.resolve();
await Promise.resolve();
}
describe("resolveReactionSyntheticEvent", () => {
it("filters app self-reactions", async () => {
const event = makeReactionEvent({ operator_type: "app" });
@@ -272,23 +320,12 @@ describe("resolveReactionSyntheticEvent", () => {
});
it("uses event chat context when provided", async () => {
const event = makeReactionEvent({
chat_id: "oc_group_from_event",
chat_type: "group",
});
const result = await resolveReactionSyntheticEvent({
cfg,
accountId: "default",
event,
botOpenId: "ou_bot",
fetchMessage: async () => ({
messageId: "om_msg1",
chatId: "oc_group_from_lookup",
senderOpenId: "ou_bot",
content: "hello",
contentType: "text",
const result = await resolveReactionWithLookup({
event: makeReactionEvent({
chat_id: "oc_group_from_event",
chat_type: "group",
}),
uuid: () => "fixed-uuid",
lookupChatId: "oc_group_from_lookup",
});
expect(result).toEqual({
@@ -309,20 +346,8 @@ describe("resolveReactionSyntheticEvent", () => {
});
it("falls back to reacted message chat_id when event chat_id is absent", async () => {
const event = makeReactionEvent();
const result = await resolveReactionSyntheticEvent({
cfg,
accountId: "default",
event,
botOpenId: "ou_bot",
fetchMessage: async () => ({
messageId: "om_msg1",
chatId: "oc_group_from_lookup",
senderOpenId: "ou_bot",
content: "hello",
contentType: "text",
}),
uuid: () => "fixed-uuid",
const result = await resolveReactionWithLookup({
lookupChatId: "oc_group_from_lookup",
});
expect(result?.message.chat_id).toBe("oc_group_from_lookup");
@@ -330,20 +355,8 @@ describe("resolveReactionSyntheticEvent", () => {
});
it("falls back to sender p2p chat when lookup returns empty chat_id", async () => {
const event = makeReactionEvent();
const result = await resolveReactionSyntheticEvent({
cfg,
accountId: "default",
event,
botOpenId: "ou_bot",
fetchMessage: async () => ({
messageId: "om_msg1",
chatId: "",
senderOpenId: "ou_bot",
content: "hello",
contentType: "text",
}),
uuid: () => "fixed-uuid",
const result = await resolveReactionWithLookup({
lookupChatId: "",
});
expect(result?.message.chat_id).toBe("p2p:ou_user1");
@@ -396,42 +409,25 @@ describe("Feishu inbound debounce regressions", () => {
});
it("keeps bot mention when per-message mention keys collide across non-forward messages", async () => {
vi.spyOn(dedup, "tryRecordMessage").mockReturnValue(true);
vi.spyOn(dedup, "tryRecordMessagePersistent").mockResolvedValue(true);
vi.spyOn(dedup, "hasRecordedMessage").mockReturnValue(false);
vi.spyOn(dedup, "hasRecordedMessagePersistent").mockResolvedValue(false);
setDedupPassThroughMocks();
const onMessage = await setupDebounceMonitor();
await onMessage(
await enqueueDebouncedMessage(
onMessage,
createTextEvent({
messageId: "om_1",
text: "first",
mentions: [
{
key: "@_user_1",
id: { open_id: "ou_user_a" },
name: "user-a",
},
],
mentions: [createMention({ openId: "ou_user_a", name: "user-a" })],
}),
);
await Promise.resolve();
await Promise.resolve();
await onMessage(
await enqueueDebouncedMessage(
onMessage,
createTextEvent({
messageId: "om_2",
text: "@bot second",
mentions: [
{
key: "@_user_1",
id: { open_id: "ou_bot" },
name: "bot",
},
],
mentions: [createMention({ openId: "ou_bot", name: "bot" })],
}),
);
await Promise.resolve();
await Promise.resolve();
await vi.advanceTimersByTimeAsync(25);
expect(handleFeishuMessageMock).toHaveBeenCalledTimes(1);
@@ -473,42 +469,25 @@ describe("Feishu inbound debounce regressions", () => {
});
it("does not synthesize mention-forward intent across separate messages", async () => {
vi.spyOn(dedup, "tryRecordMessage").mockReturnValue(true);
vi.spyOn(dedup, "tryRecordMessagePersistent").mockResolvedValue(true);
vi.spyOn(dedup, "hasRecordedMessage").mockReturnValue(false);
vi.spyOn(dedup, "hasRecordedMessagePersistent").mockResolvedValue(false);
setDedupPassThroughMocks();
const onMessage = await setupDebounceMonitor();
await onMessage(
await enqueueDebouncedMessage(
onMessage,
createTextEvent({
messageId: "om_user_mention",
text: "@alice first",
mentions: [
{
key: "@_user_1",
id: { open_id: "ou_alice" },
name: "alice",
},
],
mentions: [createMention({ openId: "ou_alice", name: "alice" })],
}),
);
await Promise.resolve();
await Promise.resolve();
await onMessage(
await enqueueDebouncedMessage(
onMessage,
createTextEvent({
messageId: "om_bot_mention",
text: "@bot second",
mentions: [
{
key: "@_user_1",
id: { open_id: "ou_bot" },
name: "bot",
},
],
mentions: [createMention({ openId: "ou_bot", name: "bot" })],
}),
);
await Promise.resolve();
await Promise.resolve();
await vi.advanceTimersByTimeAsync(25);
expect(handleFeishuMessageMock).toHaveBeenCalledTimes(1);
@@ -521,35 +500,24 @@ describe("Feishu inbound debounce regressions", () => {
});
it("preserves bot mention signal when the latest merged message has no mentions", async () => {
vi.spyOn(dedup, "tryRecordMessage").mockReturnValue(true);
vi.spyOn(dedup, "tryRecordMessagePersistent").mockResolvedValue(true);
vi.spyOn(dedup, "hasRecordedMessage").mockReturnValue(false);
vi.spyOn(dedup, "hasRecordedMessagePersistent").mockResolvedValue(false);
setDedupPassThroughMocks();
const onMessage = await setupDebounceMonitor();
await onMessage(
await enqueueDebouncedMessage(
onMessage,
createTextEvent({
messageId: "om_bot_first",
text: "@bot first",
mentions: [
{
key: "@_user_1",
id: { open_id: "ou_bot" },
name: "bot",
},
],
mentions: [createMention({ openId: "ou_bot", name: "bot" })],
}),
);
await Promise.resolve();
await Promise.resolve();
await onMessage(
await enqueueDebouncedMessage(
onMessage,
createTextEvent({
messageId: "om_plain_second",
text: "plain follow-up",
}),
);
await Promise.resolve();
await Promise.resolve();
await vi.advanceTimersByTimeAsync(25);
expect(handleFeishuMessageMock).toHaveBeenCalledTimes(1);

View File

@@ -1,6 +1,10 @@
import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
import { afterEach, describe, expect, it, vi } from "vitest";
import { monitorFeishuProvider, stopFeishuMonitor } from "./monitor.js";
import {
createFeishuClientMockModule,
createFeishuRuntimeMockModule,
} from "./monitor.test-mocks.js";
const probeFeishuMock = vi.hoisted(() => vi.fn());
@@ -8,27 +12,8 @@ vi.mock("./probe.js", () => ({
probeFeishu: probeFeishuMock,
}));
vi.mock("./client.js", () => ({
createFeishuWSClient: vi.fn(() => ({ start: vi.fn() })),
createEventDispatcher: vi.fn(() => ({ register: vi.fn() })),
}));
vi.mock("./runtime.js", () => ({
getFeishuRuntime: () => ({
channel: {
debounce: {
resolveInboundDebounceMs: () => 0,
createInboundDebouncer: () => ({
enqueue: async () => {},
flushKey: async () => {},
}),
},
text: {
hasControlCommand: () => false,
},
},
}),
}));
vi.mock("./client.js", () => createFeishuClientMockModule());
vi.mock("./runtime.js", () => createFeishuRuntimeMockModule());
function buildMultiAccountWebsocketConfig(accountIds: string[]): ClawdbotConfig {
return {

View File

@@ -1,12 +1,27 @@
import { vi } from "vitest";
export const probeFeishuMock: ReturnType<typeof vi.fn> = vi.fn();
export function createFeishuClientMockModule() {
return {
createFeishuWSClient: vi.fn(() => ({ start: vi.fn() })),
createEventDispatcher: vi.fn(() => ({ register: vi.fn() })),
};
}
vi.mock("./probe.js", () => ({
probeFeishu: probeFeishuMock,
}));
vi.mock("./client.js", () => ({
createFeishuWSClient: vi.fn(() => ({ start: vi.fn() })),
createEventDispatcher: vi.fn(() => ({ register: vi.fn() })),
}));
export function createFeishuRuntimeMockModule() {
return {
getFeishuRuntime: () => ({
channel: {
debounce: {
resolveInboundDebounceMs: () => 0,
createInboundDebouncer: () => ({
enqueue: async () => {},
flushKey: async () => {},
}),
},
text: {
hasControlCommand: () => false,
},
},
}),
};
}

View File

@@ -2,6 +2,10 @@ import { createServer } from "node:http";
import type { AddressInfo } from "node:net";
import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
createFeishuClientMockModule,
createFeishuRuntimeMockModule,
} from "./monitor.test-mocks.js";
const probeFeishuMock = vi.hoisted(() => vi.fn());
@@ -9,27 +13,8 @@ vi.mock("./probe.js", () => ({
probeFeishu: probeFeishuMock,
}));
vi.mock("./client.js", () => ({
createFeishuWSClient: vi.fn(() => ({ start: vi.fn() })),
createEventDispatcher: vi.fn(() => ({ register: vi.fn() })),
}));
vi.mock("./runtime.js", () => ({
getFeishuRuntime: () => ({
channel: {
debounce: {
resolveInboundDebounceMs: () => 0,
createInboundDebouncer: () => ({
enqueue: async () => {},
flushKey: async () => {},
}),
},
text: {
hasControlCommand: () => false,
},
},
}),
}));
vi.mock("./client.js", () => createFeishuClientMockModule());
vi.mock("./runtime.js", () => createFeishuRuntimeMockModule());
vi.mock("@larksuiteoapi/node-sdk", () => ({
adaptDefault: vi.fn(

View File

@@ -3,15 +3,11 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu";
import { listEnabledFeishuAccounts } from "./accounts.js";
import { FeishuPermSchema, type FeishuPermParams } from "./perm-schema.js";
import { createFeishuToolClient, resolveAnyEnabledFeishuToolsConfig } from "./tool-account.js";
// ============ Helpers ============
function json(data: unknown) {
return {
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
details: data,
};
}
import {
jsonToolResult,
toolExecutionErrorResult,
unknownToolActionResult,
} from "./tool-result.js";
type ListTokenType =
| "doc"
@@ -154,21 +150,21 @@ export function registerFeishuPermTools(api: OpenClawPluginApi) {
});
switch (p.action) {
case "list":
return json(await listMembers(client, p.token, p.type));
return jsonToolResult(await listMembers(client, p.token, p.type));
case "add":
return json(
return jsonToolResult(
await addMember(client, p.token, p.type, p.member_type, p.member_id, p.perm),
);
case "remove":
return json(
return jsonToolResult(
await removeMember(client, p.token, p.type, p.member_type, p.member_id),
);
default:
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- exhaustive check fallback
return json({ error: `Unknown action: ${(p as any).action}` });
return unknownToolActionResult((p as { action?: unknown }).action);
}
} catch (err) {
return json({ error: err instanceof Error ? err.message : String(err) });
return toolExecutionErrorResult(err);
}
},
};

View File

@@ -0,0 +1,71 @@
import { assertFeishuMessageApiSuccess, toFeishuSendResult } from "./send-result.js";
type FeishuMessageClient = {
im: {
message: {
reply: (params: {
path: { message_id: string };
data: Record<string, unknown>;
}) => Promise<{ code?: number; msg?: string; data?: { message_id?: string } }>;
create: (params: {
params: { receive_id_type: string };
data: Record<string, unknown>;
}) => Promise<{ code?: number; msg?: string; data?: { message_id?: string } }>;
};
};
};
export async function sendFeishuMessageWithOptionalReply(params: {
client: FeishuMessageClient;
receiveId: string;
receiveIdType: string;
content: string;
msgType: string;
replyToMessageId?: string;
replyInThread?: boolean;
sendErrorPrefix: string;
replyErrorPrefix: string;
fallbackSendErrorPrefix?: string;
shouldFallbackFromReply?: (response: { code?: number; msg?: string }) => boolean;
}): Promise<{ messageId: string; chatId: string }> {
const data = {
content: params.content,
msg_type: params.msgType,
};
if (params.replyToMessageId) {
const response = await params.client.im.message.reply({
path: { message_id: params.replyToMessageId },
data: {
...data,
...(params.replyInThread ? { reply_in_thread: true } : {}),
},
});
if (params.shouldFallbackFromReply?.(response)) {
const fallback = await params.client.im.message.create({
params: { receive_id_type: params.receiveIdType },
data: {
receive_id: params.receiveId,
...data,
},
});
assertFeishuMessageApiSuccess(
fallback,
params.fallbackSendErrorPrefix ?? params.sendErrorPrefix,
);
return toFeishuSendResult(fallback, params.receiveId);
}
assertFeishuMessageApiSuccess(response, params.replyErrorPrefix);
return toFeishuSendResult(response, params.receiveId);
}
const response = await params.client.im.message.create({
params: { receive_id_type: params.receiveIdType },
data: {
receive_id: params.receiveId,
...data,
},
});
assertFeishuMessageApiSuccess(response, params.sendErrorPrefix);
return toFeishuSendResult(response, params.receiveId);
}

View File

@@ -0,0 +1,32 @@
import { describe, expect, it } from "vitest";
import {
jsonToolResult,
toolExecutionErrorResult,
unknownToolActionResult,
} from "./tool-result.js";
describe("jsonToolResult", () => {
it("formats tool result with text content and details", () => {
const payload = { ok: true, id: "abc" };
expect(jsonToolResult(payload)).toEqual({
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
details: payload,
});
});
it("formats unknown action errors", () => {
expect(unknownToolActionResult("create")).toEqual({
content: [
{ type: "text", text: JSON.stringify({ error: "Unknown action: create" }, null, 2) },
],
details: { error: "Unknown action: create" },
});
});
it("formats execution errors", () => {
expect(toolExecutionErrorResult(new Error("boom"))).toEqual({
content: [{ type: "text", text: JSON.stringify({ error: "boom" }, null, 2) }],
details: { error: "boom" },
});
});
});

View File

@@ -0,0 +1,14 @@
export function jsonToolResult(data: unknown) {
return {
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
details: data,
};
}
export function unknownToolActionResult(action: unknown) {
return jsonToolResult({ error: `Unknown action: ${String(action)}` });
}
export function toolExecutionErrorResult(error: unknown) {
return jsonToolResult({ error: error instanceof Error ? error.message : String(error) });
}

View File

@@ -2,17 +2,13 @@ import type * as Lark from "@larksuiteoapi/node-sdk";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/feishu";
import { listEnabledFeishuAccounts } from "./accounts.js";
import { createFeishuToolClient, resolveAnyEnabledFeishuToolsConfig } from "./tool-account.js";
import {
jsonToolResult,
toolExecutionErrorResult,
unknownToolActionResult,
} from "./tool-result.js";
import { FeishuWikiSchema, type FeishuWikiParams } from "./wiki-schema.js";
// ============ Helpers ============
function json(data: unknown) {
return {
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
details: data,
};
}
type ObjType = "doc" | "sheet" | "mindnote" | "bitable" | "file" | "docx" | "slides";
// ============ Actions ============
@@ -194,22 +190,22 @@ export function registerFeishuWikiTools(api: OpenClawPluginApi) {
});
switch (p.action) {
case "spaces":
return json(await listSpaces(client));
return jsonToolResult(await listSpaces(client));
case "nodes":
return json(await listNodes(client, p.space_id, p.parent_node_token));
return jsonToolResult(await listNodes(client, p.space_id, p.parent_node_token));
case "get":
return json(await getNode(client, p.token));
return jsonToolResult(await getNode(client, p.token));
case "search":
return json({
return jsonToolResult({
error:
"Search is not available. Use feishu_wiki with action: 'nodes' to browse or action: 'get' to lookup by token.",
});
case "create":
return json(
return jsonToolResult(
await createNode(client, p.space_id, p.title, p.obj_type, p.parent_node_token),
);
case "move":
return json(
return jsonToolResult(
await moveNode(
client,
p.space_id,
@@ -219,13 +215,13 @@ export function registerFeishuWikiTools(api: OpenClawPluginApi) {
),
);
case "rename":
return json(await renameNode(client, p.space_id, p.node_token, p.title));
return jsonToolResult(await renameNode(client, p.space_id, p.node_token, p.title));
default:
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- exhaustive check fallback
return json({ error: `Unknown action: ${(p as any).action}` });
return unknownToolActionResult((p as { action?: unknown }).action);
}
} catch (err) {
return json({ error: err instanceof Error ? err.message : String(err) });
return toolExecutionErrorResult(err);
}
},
};