mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
refactor(extensions): reuse shared helper primitives
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
71
extensions/feishu/src/send-message.ts
Normal file
71
extensions/feishu/src/send-message.ts
Normal 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);
|
||||
}
|
||||
32
extensions/feishu/src/tool-result.test.ts
Normal file
32
extensions/feishu/src/tool-result.test.ts
Normal 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" },
|
||||
});
|
||||
});
|
||||
});
|
||||
14
extensions/feishu/src/tool-result.ts
Normal file
14
extensions/feishu/src/tool-result.ts
Normal 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) });
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user