Merge c40b050f7776fb8b57e81186698c38979cf0b05a into 5e417b44e1540f528d2ae63e3e20229a902d1db2
This commit is contained in:
commit
23efcf5f32
136
docs/extensions/feishu-reactions.md
Normal file
136
docs/extensions/feishu-reactions.md
Normal file
@ -0,0 +1,136 @@
|
||||
# Feishu Reaction Tool
|
||||
|
||||
The Feishu reaction tool allows agents to add, remove, and list emoji reactions on messages.
|
||||
|
||||
## Setup
|
||||
|
||||
Enable the reaction tool in your Feishu config:
|
||||
|
||||
```json
|
||||
{
|
||||
"channels": {
|
||||
"feishu": {
|
||||
"tools": {
|
||||
"reaction": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Add a reaction
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "add",
|
||||
"message_id": "msg_xxxxxx",
|
||||
"emoji_type": "THUMBSUP"
|
||||
}
|
||||
```
|
||||
|
||||
### Remove a reaction
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "remove",
|
||||
"message_id": "msg_xxxxxx",
|
||||
"reaction_id": "react_xxxxxx"
|
||||
}
|
||||
```
|
||||
|
||||
### List reactions
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "list",
|
||||
"message_id": "msg_xxxxxx"
|
||||
}
|
||||
```
|
||||
|
||||
### List reactions with specific emoji
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "list",
|
||||
"message_id": "msg_xxxxxx",
|
||||
"emoji_type": "HEART"
|
||||
}
|
||||
```
|
||||
|
||||
## Available Emoji Types
|
||||
|
||||
Common emoji types:
|
||||
|
||||
- `THUMBSUP` 👍
|
||||
- `THUMBSDOWN` 👎
|
||||
- `HEART` ❤️
|
||||
- `SMILE` 😊
|
||||
- `GRINNING` 😀
|
||||
- `LAUGHING` 😂
|
||||
- `CRY` 😢
|
||||
- `ANGRY` 😠
|
||||
- `SURPRISED` 😲
|
||||
- `THINKING` 🤔
|
||||
- `CLAP` 👏
|
||||
- `OK` 👌
|
||||
- `FIST` ✊
|
||||
- `PRAY` 🙏
|
||||
- `FIRE` 🔥
|
||||
- `PARTY` 🎉
|
||||
- `CHECK` ✅
|
||||
- `CROSS` ❌
|
||||
- `QUESTION` ❓
|
||||
- `EXCLAMATION` ❗
|
||||
|
||||
For a complete list, see the [Feishu emoji documentation](https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce).
|
||||
|
||||
## Examples
|
||||
|
||||
### React to a message with thumbs up
|
||||
|
||||
```typescript
|
||||
await feishu_reaction({
|
||||
action: "add",
|
||||
message_id: "msg_123",
|
||||
emoji_type: "THUMBSUP"
|
||||
});
|
||||
```
|
||||
|
||||
### Check all reactions on a message
|
||||
|
||||
```typescript
|
||||
const result = await feishu_reaction({
|
||||
action: "list",
|
||||
message_id: "msg_123"
|
||||
});
|
||||
// Returns: { reactions: [{ reactionId, emojiType, operatorType, operatorId }] }
|
||||
```
|
||||
|
||||
### Remove a specific reaction
|
||||
|
||||
```typescript
|
||||
await feishu_reaction({
|
||||
action: "remove",
|
||||
message_id: "msg_123",
|
||||
reaction_id: "react_456"
|
||||
});
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The tool returns errors in the following cases:
|
||||
|
||||
- Missing required `emoji_type` for add action
|
||||
- Missing required `reaction_id` for remove action
|
||||
- Invalid action type
|
||||
- API failures (network, auth, etc.)
|
||||
|
||||
Example error response:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "emoji_type is required for add action"
|
||||
}
|
||||
```
|
||||
@ -5,6 +5,7 @@ import { registerFeishuChatTools } from "./src/chat.js";
|
||||
import { registerFeishuDocTools } from "./src/docx.js";
|
||||
import { registerFeishuDriveTools } from "./src/drive.js";
|
||||
import { registerFeishuPermTools } from "./src/perm.js";
|
||||
import { registerFeishuReactionTools } from "./src/reaction.js";
|
||||
import { setFeishuRuntime } from "./src/runtime.js";
|
||||
import { registerFeishuSubagentHooks } from "./src/subagent-hooks.js";
|
||||
import { registerFeishuWikiTools } from "./src/wiki.js";
|
||||
@ -60,5 +61,6 @@ export default defineChannelPluginEntry({
|
||||
registerFeishuDriveTools(api);
|
||||
registerFeishuPermTools(api);
|
||||
registerFeishuBitableTools(api);
|
||||
registerFeishuReactionTools(api);
|
||||
},
|
||||
});
|
||||
|
||||
156
extensions/feishu/src/reaction-schema.test.ts
Normal file
156
extensions/feishu/src/reaction-schema.test.ts
Normal file
@ -0,0 +1,156 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { FeishuReactionSchema } from "./reaction-schema.js";
|
||||
import { TypeCompiler } from "@sinclair/typebox/compiler";
|
||||
|
||||
const ReactionValidator = TypeCompiler.Compile(FeishuReactionSchema);
|
||||
|
||||
describe("FeishuReactionSchema", () => {
|
||||
describe("action field", () => {
|
||||
it("accepts valid action: add", () => {
|
||||
const result = ReactionValidator.Check({
|
||||
action: "add",
|
||||
message_id: "msg_123",
|
||||
emoji_type: "THUMBSUP",
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts valid action: remove", () => {
|
||||
const result = ReactionValidator.Check({
|
||||
action: "remove",
|
||||
message_id: "msg_123",
|
||||
reaction_id: "react_456",
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts valid action: list", () => {
|
||||
const result = ReactionValidator.Check({
|
||||
action: "list",
|
||||
message_id: "msg_123",
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects invalid action", () => {
|
||||
const result = ReactionValidator.Check({
|
||||
action: "invalid",
|
||||
message_id: "msg_123",
|
||||
});
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("message_id field", () => {
|
||||
it("accepts valid message_id", () => {
|
||||
const result = ReactionValidator.Check({
|
||||
action: "list",
|
||||
message_id: "msg_123",
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects missing message_id", () => {
|
||||
const result = ReactionValidator.Check({
|
||||
action: "list",
|
||||
});
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects empty message_id", () => {
|
||||
const result = ReactionValidator.Check({
|
||||
action: "list",
|
||||
message_id: "",
|
||||
});
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("emoji_type field", () => {
|
||||
it("accepts common emoji types", () => {
|
||||
const types = ["THUMBSUP", "HEART", "SMILE", "FIRE", "CLAP", "OK", "PRAY"];
|
||||
for (const type of types) {
|
||||
const result = ReactionValidator.Check({
|
||||
action: "add",
|
||||
message_id: "msg_123",
|
||||
emoji_type: type,
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts any string emoji type", () => {
|
||||
const result = ReactionValidator.Check({
|
||||
action: "add",
|
||||
message_id: "msg_123",
|
||||
emoji_type: "CUSTOM_EMOJI",
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("allows optional emoji_type for list action", () => {
|
||||
const result = ReactionValidator.Check({
|
||||
action: "list",
|
||||
message_id: "msg_123",
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("reaction_id field", () => {
|
||||
it("accepts valid reaction_id", () => {
|
||||
const result = ReactionValidator.Check({
|
||||
action: "remove",
|
||||
message_id: "msg_123",
|
||||
reaction_id: "react_456",
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("allows optional reaction_id for add/list actions", () => {
|
||||
const result = ReactionValidator.Check({
|
||||
action: "add",
|
||||
message_id: "msg_123",
|
||||
emoji_type: "THUMBSUP",
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("account_id field", () => {
|
||||
it("accepts optional account_id", () => {
|
||||
const result = ReactionValidator.Check({
|
||||
action: "list",
|
||||
message_id: "msg_123",
|
||||
account_id: "acc_123",
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validation errors", () => {
|
||||
it("provides error details for invalid data", () => {
|
||||
const errors = Array.from(ReactionValidator.Errors({
|
||||
action: "invalid",
|
||||
message_id: "msg_123",
|
||||
}));
|
||||
expect(errors.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("rejects wrong type for action", () => {
|
||||
const result = ReactionValidator.Check({
|
||||
action: 123,
|
||||
message_id: "msg_123",
|
||||
});
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects wrong type for message_id", () => {
|
||||
const result = ReactionValidator.Check({
|
||||
action: "list",
|
||||
message_id: 123,
|
||||
});
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
26
extensions/feishu/src/reaction-schema.ts
Normal file
26
extensions/feishu/src/reaction-schema.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { Type, type Static } from "@sinclair/typebox";
|
||||
|
||||
const REACTION_ACTION_VALUES = ["add", "remove", "list"] as const;
|
||||
|
||||
export const FeishuReactionSchema = Type.Object({
|
||||
action: Type.Unsafe<(typeof REACTION_ACTION_VALUES)[number]>({
|
||||
type: "string",
|
||||
enum: [...REACTION_ACTION_VALUES],
|
||||
description: "Action to run: add | remove | list",
|
||||
}),
|
||||
message_id: Type.String({ description: "Message ID to react to" }),
|
||||
emoji_type: Type.Optional(
|
||||
Type.String({
|
||||
description:
|
||||
'Feishu emoji type (required for add/remove). Examples: THUMBSUP, HEART, SMILE, FIRE, CLAP, OK, PRAY',
|
||||
}),
|
||||
),
|
||||
reaction_id: Type.Optional(
|
||||
Type.String({
|
||||
description: "Reaction ID (required for remove action, returned by add/list)",
|
||||
}),
|
||||
),
|
||||
account_id: Type.Optional(Type.String({ description: "Feishu account ID (optional)" })),
|
||||
});
|
||||
|
||||
export type FeishuReactionParams = Static<typeof FeishuReactionSchema>;
|
||||
148
extensions/feishu/src/reaction.test.ts
Normal file
148
extensions/feishu/src/reaction.test.ts
Normal file
@ -0,0 +1,148 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import { registerFeishuReactionTools } from "./reaction.js";
|
||||
import * as reactions from "./reactions.js";
|
||||
|
||||
describe("feishu_reaction tool", () => {
|
||||
let mockApi: Partial<OpenClawPluginApi>;
|
||||
let registeredTool: any;
|
||||
|
||||
beforeEach(() => {
|
||||
registeredTool = null;
|
||||
mockApi = {
|
||||
config: {
|
||||
channels: {
|
||||
feishu: {
|
||||
appId: "test_app_id",
|
||||
appSecret: "test_app_secret",
|
||||
encryptKey: "test_encrypt_key",
|
||||
verificationToken: "test_token",
|
||||
},
|
||||
},
|
||||
} as any,
|
||||
logger: {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
registerTool: vi.fn((tool) => {
|
||||
registeredTool = tool;
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
it("registers feishu_reaction tool when reaction is enabled", () => {
|
||||
registerFeishuReactionTools(mockApi as OpenClawPluginApi);
|
||||
expect(mockApi.registerTool).toHaveBeenCalled();
|
||||
expect(registeredTool?.name).toBe("feishu_reaction");
|
||||
});
|
||||
|
||||
it("execute add action calls addReactionFeishu", async () => {
|
||||
vi.spyOn(reactions, "addReactionFeishu").mockResolvedValue({ reactionId: "react_123" });
|
||||
|
||||
registerFeishuReactionTools(mockApi as OpenClawPluginApi);
|
||||
|
||||
const result = await registeredTool.execute("call_1", {
|
||||
action: "add",
|
||||
message_id: "msg_123",
|
||||
emoji_type: "THUMBSUP",
|
||||
});
|
||||
|
||||
expect(reactions.addReactionFeishu).toHaveBeenCalledWith({
|
||||
cfg: mockApi.config,
|
||||
messageId: "msg_123",
|
||||
emojiType: "THUMBSUP",
|
||||
accountId: undefined,
|
||||
});
|
||||
expect(result.content[0].text).toContain("reaction_123");
|
||||
});
|
||||
|
||||
it("add action returns error when emoji_type is missing", async () => {
|
||||
registerFeishuReactionTools(mockApi as OpenClawPluginApi);
|
||||
|
||||
const result = await registeredTool.execute("call_1", {
|
||||
action: "add",
|
||||
message_id: "msg_123",
|
||||
});
|
||||
|
||||
expect(result.content[0].text).toContain("emoji_type is required");
|
||||
});
|
||||
|
||||
it("execute remove action calls removeReactionFeishu", async () => {
|
||||
vi.spyOn(reactions, "removeReactionFeishu").mockResolvedValue();
|
||||
|
||||
registerFeishuReactionTools(mockApi as OpenClawPluginApi);
|
||||
|
||||
const result = await registeredTool.execute("call_1", {
|
||||
action: "remove",
|
||||
message_id: "msg_123",
|
||||
reaction_id: "react_123",
|
||||
});
|
||||
|
||||
expect(reactions.removeReactionFeishu).toHaveBeenCalledWith({
|
||||
cfg: mockApi.config,
|
||||
messageId: "msg_123",
|
||||
reactionId: "react_123",
|
||||
accountId: undefined,
|
||||
});
|
||||
expect(result.content[0].text).toContain("success");
|
||||
});
|
||||
|
||||
it("remove action returns error when reaction_id is missing", async () => {
|
||||
registerFeishuReactionTools(mockApi as OpenClawPluginApi);
|
||||
|
||||
const result = await registeredTool.execute("call_1", {
|
||||
action: "remove",
|
||||
message_id: "msg_123",
|
||||
});
|
||||
|
||||
expect(result.content[0].text).toContain("reaction_id is required");
|
||||
});
|
||||
|
||||
it("execute list action calls listReactionsFeishu", async () => {
|
||||
vi.spyOn(reactions, "listReactionsFeishu").mockResolvedValue([
|
||||
{ reactionId: "r1", emojiType: "THUMBSUP", operatorType: "user", operatorId: "u1" },
|
||||
]);
|
||||
|
||||
registerFeishuReactionTools(mockApi as OpenClawPluginApi);
|
||||
|
||||
const result = await registeredTool.execute("call_1", {
|
||||
action: "list",
|
||||
message_id: "msg_123",
|
||||
});
|
||||
|
||||
expect(reactions.listReactionsFeishu).toHaveBeenCalledWith({
|
||||
cfg: mockApi.config,
|
||||
messageId: "msg_123",
|
||||
emojiType: undefined,
|
||||
accountId: undefined,
|
||||
});
|
||||
expect(result.content[0].text).toContain("THUMBSUP");
|
||||
});
|
||||
|
||||
it("returns error for unknown action", async () => {
|
||||
registerFeishuReactionTools(mockApi as OpenClawPluginApi);
|
||||
|
||||
const result = await registeredTool.execute("call_1", {
|
||||
action: "unknown" as any,
|
||||
message_id: "msg_123",
|
||||
});
|
||||
|
||||
expect(result.content[0].text).toContain("Unknown action");
|
||||
});
|
||||
|
||||
it("handles errors gracefully", async () => {
|
||||
vi.spyOn(reactions, "addReactionFeishu").mockRejectedValue(new Error("API failed"));
|
||||
|
||||
registerFeishuReactionTools(mockApi as OpenClawPluginApi);
|
||||
|
||||
const result = await registeredTool.execute("call_1", {
|
||||
action: "add",
|
||||
message_id: "msg_123",
|
||||
emoji_type: "THUMBSUP",
|
||||
});
|
||||
|
||||
expect(result.content[0].text).toContain("API failed");
|
||||
});
|
||||
});
|
||||
95
extensions/feishu/src/reaction.ts
Normal file
95
extensions/feishu/src/reaction.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import { listEnabledFeishuAccounts } from "./accounts.js";
|
||||
import { createFeishuClient } from "./client.js";
|
||||
import { FeishuReactionSchema, type FeishuReactionParams } from "./reaction-schema.js";
|
||||
import {
|
||||
addReactionFeishu,
|
||||
removeReactionFeishu,
|
||||
listReactionsFeishu,
|
||||
FeishuEmoji,
|
||||
} from "./reactions.js";
|
||||
import { resolveToolsConfig } from "./tools-config.js";
|
||||
|
||||
function json(data: unknown) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
|
||||
details: data,
|
||||
};
|
||||
}
|
||||
|
||||
export function registerFeishuReactionTools(api: OpenClawPluginApi) {
|
||||
if (!api.config) {
|
||||
api.logger.debug?.("feishu_reaction: No config available, skipping reaction tools");
|
||||
return;
|
||||
}
|
||||
|
||||
const accounts = listEnabledFeishuAccounts(api.config);
|
||||
if (accounts.length === 0) {
|
||||
api.logger.debug?.("feishu_reaction: No Feishu accounts configured, skipping reaction tools");
|
||||
return;
|
||||
}
|
||||
|
||||
const firstAccount = accounts[0];
|
||||
const toolsCfg = resolveToolsConfig(firstAccount.config.tools);
|
||||
if (!toolsCfg.reaction) {
|
||||
api.logger.debug?.("feishu_reaction: reaction tool disabled in config");
|
||||
return;
|
||||
}
|
||||
|
||||
api.registerTool(
|
||||
{
|
||||
name: "feishu_reaction",
|
||||
label: "Feishu Reaction",
|
||||
description:
|
||||
"Feishu message reaction operations. Actions: add, remove, list. Common emojis: THUMBSUP, HEART, SMILE, FIRE, CLAP, OK, PRAY",
|
||||
parameters: FeishuReactionSchema,
|
||||
async execute(_toolCallId, params) {
|
||||
const p = params as FeishuReactionParams;
|
||||
try {
|
||||
switch (p.action) {
|
||||
case "add": {
|
||||
if (!p.emoji_type) {
|
||||
return json({ error: "emoji_type is required for add action" });
|
||||
}
|
||||
const result = await addReactionFeishu({
|
||||
cfg: api.config,
|
||||
messageId: p.message_id,
|
||||
emojiType: p.emoji_type,
|
||||
accountId: p.account_id,
|
||||
});
|
||||
return json({ success: true, ...result });
|
||||
}
|
||||
case "remove": {
|
||||
if (!p.reaction_id) {
|
||||
return json({ error: "reaction_id is required for remove action" });
|
||||
}
|
||||
await removeReactionFeishu({
|
||||
cfg: api.config,
|
||||
messageId: p.message_id,
|
||||
reactionId: p.reaction_id,
|
||||
accountId: p.account_id,
|
||||
});
|
||||
return json({ success: true, action: "removed" });
|
||||
}
|
||||
case "list": {
|
||||
const reactions = await listReactionsFeishu({
|
||||
cfg: api.config,
|
||||
messageId: p.message_id,
|
||||
emojiType: p.emoji_type,
|
||||
accountId: p.account_id,
|
||||
});
|
||||
return json({ reactions });
|
||||
}
|
||||
default:
|
||||
return json({ error: `Unknown action: ${String(p.action)}` });
|
||||
}
|
||||
} catch (err) {
|
||||
return json({ error: err instanceof Error ? err.message : String(err) });
|
||||
}
|
||||
},
|
||||
},
|
||||
{ name: "feishu_reaction" },
|
||||
);
|
||||
|
||||
api.logger.info?.("feishu_reaction: Registered feishu_reaction tool");
|
||||
}
|
||||
@ -2,7 +2,7 @@ import type { FeishuToolsConfig } from "./types.js";
|
||||
|
||||
/**
|
||||
* Default tool configuration.
|
||||
* - doc, chat, wiki, drive, scopes: enabled by default
|
||||
* - doc, chat, wiki, drive, scopes, reaction: enabled by default
|
||||
* - perm: disabled by default (sensitive operation)
|
||||
*/
|
||||
export const DEFAULT_TOOLS_CONFIG: Required<FeishuToolsConfig> = {
|
||||
@ -12,6 +12,7 @@ export const DEFAULT_TOOLS_CONFIG: Required<FeishuToolsConfig> = {
|
||||
drive: true,
|
||||
perm: false,
|
||||
scopes: true,
|
||||
reaction: true,
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@ -95,6 +95,7 @@ export type FeishuToolsConfig = {
|
||||
drive?: boolean;
|
||||
perm?: boolean;
|
||||
scopes?: boolean;
|
||||
reaction?: boolean;
|
||||
};
|
||||
|
||||
export type DynamicAgentCreationConfig = {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user