@m1heng-clawd/feishu 0.1.16 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -0
- package/index.ts +4 -0
- package/package.json +2 -1
- package/skills/feishu-message/SKILL.md +111 -0
- package/skills/feishu-reaction/SKILL.md +120 -0
- package/src/accounts.ts +27 -5
- package/src/bot.ts +18 -5
- package/src/channel.ts +12 -7
- package/src/config-schema.ts +22 -0
- package/src/drive-tools/register.ts +1 -1
- package/src/media-duration.ts +43 -8
- package/src/media.ts +29 -12
- package/src/message-tools/actions.ts +154 -0
- package/src/message-tools/index.ts +2 -0
- package/src/message-tools/register.ts +58 -0
- package/src/message-tools/schemas.ts +56 -0
- package/src/onboarding.ts +4 -2
- package/src/reaction-tools/actions.ts +127 -0
- package/src/reaction-tools/index.ts +2 -0
- package/src/reaction-tools/register.ts +51 -0
- package/src/reaction-tools/schemas.ts +34 -0
- package/src/reply-dispatcher.ts +23 -33
- package/src/streaming-card.ts +34 -11
- package/src/text/markdown-links.ts +16 -1
- package/src/tools-common/tool-context.ts +1 -0
- package/src/tools-config.ts +2 -0
- package/src/types.ts +4 -0
package/README.md
CHANGED
|
@@ -16,6 +16,22 @@ Feishu/Lark (飞书) channel plugin for [OpenClaw](https://github.com/openclaw/o
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
+
## Sponsor
|
|
20
|
+
|
|
21
|
+

|
|
22
|
+
|
|
23
|
+
Thanks to our sponsors for supporting the project.
|
|
24
|
+
|
|
25
|
+
If you need an API key platform whose models can be used as an OpenClaw provider and work with Kimi, MiniMax, Claude Code, Codex, or direct API usage, YouYun Zhisuan is worth considering:
|
|
26
|
+
|
|
27
|
+
> UCloud's YouYun Zhisuan is an AI cloud platform that provides stable and comprehensive domestic and international model APIs through a single API key. Its high-value Coding Plan offers both monthly and pay-as-you-go options, typically at 20% to 50% of official pricing. It supports Claude Code, Codex, and direct API integrations, as well as enterprise-grade high concurrency, 24/7 technical support, and self-service invoicing. Register through [this link](https://passport.compshare.cn/register?referral_code=5oWQU772rtSCeExpcLFxwy&ytag=GPU_YY_YX_git_clawdbot-feishu) to receive a free RMB 5 platform trial credit.
|
|
28
|
+
|
|
29
|
+
感谢赞助方对本项目的支持。
|
|
30
|
+
|
|
31
|
+
如果你需要一个可作为 OpenClaw 模型 provider,并适用于 Kimi、MiniMax、Claude Code、Codex 或直接 API 调用的稳定 API Key 平台,可以了解一下优云智算:
|
|
32
|
+
|
|
33
|
+
> 优云智算是UCloud旗下AI云平台,提供稳定、全面的国内外模型API,仅一个key即可调用。主打包月、按量的高性价比 Coding Plan 套餐,基于官方2~5折优惠。支持接入 Claude Code、Codex 及 API 调用。支持企业高并发、7*24技术支持、自助开票。通过[此链接](https://passport.compshare.cn/register?referral_code=5oWQU772rtSCeExpcLFxwy&ytag=GPU_YY_YX_git_clawdbot-feishu)注册的用户,可得免费5元平台体验金!
|
|
34
|
+
|
|
19
35
|
## English
|
|
20
36
|
|
|
21
37
|
### Installation
|
|
@@ -113,6 +129,9 @@ openclaw plugins list | rg -i feishu
|
|
|
113
129
|
| `task:attachment:read` | `feishu_task_attachment_list`, `feishu_task_attachment_get` | List/get task attachments |
|
|
114
130
|
| `im:chat.announcement:read` | `feishu_chat` | Read group announcement |
|
|
115
131
|
| `im:chat:readonly` | `feishu_chat` | Get chat info, check bot membership |
|
|
132
|
+
| `im:message` or `im:message:readonly` | `feishu_message` | Read single/DM messages |
|
|
133
|
+
| `im:message.group_at_msg:readonly` | `feishu_message` | Read all messages in group chats |
|
|
134
|
+
| `im:message.reactions:read` | `feishu_reaction` | List reactions on messages |
|
|
116
135
|
|
|
117
136
|
**Read-write** (optional, for create/edit/delete operations):
|
|
118
137
|
|
|
@@ -131,6 +150,7 @@ openclaw plugins list | rg -i feishu
|
|
|
131
150
|
| `im:chat.announcement` | `feishu_chat` | Write/update group announcement |
|
|
132
151
|
| `im:chat` | `feishu_chat` | Create and delete group chats |
|
|
133
152
|
| `im:chat.members` | `feishu_chat` | Add members to group chats |
|
|
153
|
+
| `im:message.reactions:write_only` | `feishu_reaction` | Add and remove reactions |
|
|
134
154
|
|
|
135
155
|
> Task scope names may vary slightly in Feishu console UI. If needed, search for Task / Tasklist / Comment / Attachment-related permissions and grant read/write accordingly.
|
|
136
156
|
|
|
@@ -479,6 +499,8 @@ session:
|
|
|
479
499
|
- **Task tools**: Create, get details, update, and delete tasks via Feishu Task v2 API
|
|
480
500
|
- **Chat tools**: Read and write group announcements, create group chats, add members, check bot membership, delete chats (`feishu_chat`)
|
|
481
501
|
- **Urgent notification tools**: Send buzz/urgent notifications (app, SMS, voice call) via `feishu_urgent`
|
|
502
|
+
- **Message tools**: Read single messages or list recent chat history with time range and pagination (`feishu_message`)
|
|
503
|
+
- **Reaction tools**: Add, remove, and list emoji reactions on messages (`feishu_reaction`)
|
|
482
504
|
- **@mention forwarding**: When you @mention someone in your message, the bot's reply will automatically @mention them too
|
|
483
505
|
- **Permission error notification**: When the bot encounters a Feishu API permission error, it automatically notifies the user with the permission grant URL
|
|
484
506
|
- **Dynamic agent creation**: Each DM user can have their own isolated agent instance with dedicated workspace (optional)
|
|
@@ -629,6 +651,9 @@ openclaw plugins list | rg -i feishu
|
|
|
629
651
|
| `task:attachment:read` | `feishu_task_attachment_list`, `feishu_task_attachment_get` | 列出/获取任务附件 |
|
|
630
652
|
| `im:chat.announcement:read` | `feishu_chat` | 读取群公告 |
|
|
631
653
|
| `im:chat:readonly` | `feishu_chat` | 获取群信息、检查机器人是否在群内 |
|
|
654
|
+
| `im:message` 或 `im:message:readonly` | `feishu_message` | 读取单条/私聊消息 |
|
|
655
|
+
| `im:message.group_at_msg:readonly` | `feishu_message` | 读取群聊中所有消息 |
|
|
656
|
+
| `im:message.reactions:read` | `feishu_reaction` | 查看消息表情回复 |
|
|
632
657
|
|
|
633
658
|
**读写权限**(可选,用于创建/编辑/删除操作):
|
|
634
659
|
|
|
@@ -647,6 +672,7 @@ openclaw plugins list | rg -i feishu
|
|
|
647
672
|
| `im:chat.announcement` | `feishu_chat` | 写入/更新群公告 |
|
|
648
673
|
| `im:chat` | `feishu_chat` | 创建和删除群聊 |
|
|
649
674
|
| `im:chat.members` | `feishu_chat` | 向群聊添加成员 |
|
|
675
|
+
| `im:message.reactions:write_only` | `feishu_reaction` | 发送、删除消息表情回复 |
|
|
650
676
|
|
|
651
677
|
> 飞书控制台中任务权限的显示名称可能略有差异,必要时可按关键字 `task` / `tasklist` / `comment` / `attachment` 搜索并授予对应读写权限。
|
|
652
678
|
|
|
@@ -994,6 +1020,8 @@ session:
|
|
|
994
1020
|
- **任务工具**:基于 Task v2 API 支持任务创建、获取详情、更新和删除
|
|
995
1021
|
- **群聊工具**:读写群公告、创建群聊、添加成员、检查机器人是否在群内、删除群聊(`feishu_chat`)
|
|
996
1022
|
- **加急通知工具**:发送应用内加急(buzz)、短信、语音电话加急通知(`feishu_urgent`)
|
|
1023
|
+
- **消息工具**:读取单条消息或列出聊天历史记录,支持时间范围和分页(`feishu_message`)
|
|
1024
|
+
- **表情工具**:添加、删除、列出消息上的表情回复(`feishu_reaction`)
|
|
997
1025
|
- **@ 转发功能**:在消息中 @ 某人,机器人的回复会自动 @ 该用户
|
|
998
1026
|
- **权限错误提示**:当机器人遇到飞书 API 权限错误时,会自动通知用户并提供权限授权链接
|
|
999
1027
|
- **动态 Agent 创建**:每个私聊用户可拥有独立的 agent 实例和专属 workspace(可选)
|
package/index.ts
CHANGED
|
@@ -9,6 +9,8 @@ import { registerFeishuPermTools } from "./src/perm-tools/index.js";
|
|
|
9
9
|
import { registerFeishuTaskTools } from "./src/task-tools/index.js";
|
|
10
10
|
import { registerFeishuChatTools } from "./src/chat-tools/index.js";
|
|
11
11
|
import { registerFeishuUrgentTools } from "./src/urgent-tools/index.js";
|
|
12
|
+
import { registerFeishuMessageTools } from "./src/message-tools/index.js";
|
|
13
|
+
import { registerFeishuReactionTools } from "./src/reaction-tools/index.js";
|
|
12
14
|
import { registerFeishuWikiTools } from "./src/wiki-tools/index.js";
|
|
13
15
|
|
|
14
16
|
export { monitorFeishuProvider } from "./src/monitor.js";
|
|
@@ -64,6 +66,8 @@ const plugin = {
|
|
|
64
66
|
registerFeishuTaskTools(api);
|
|
65
67
|
registerFeishuChatTools(api);
|
|
66
68
|
registerFeishuUrgentTools(api);
|
|
69
|
+
registerFeishuMessageTools(api);
|
|
70
|
+
registerFeishuReactionTools(api);
|
|
67
71
|
},
|
|
68
72
|
};
|
|
69
73
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@m1heng-clawd/feishu",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw Feishu/Lark channel plugin",
|
|
6
6
|
"scripts": {
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"@larksuiteoapi/node-sdk": "^1.59.0",
|
|
58
58
|
"@sinclair/typebox": "0.34.48",
|
|
59
59
|
"https-proxy-agent": "^7.0.6",
|
|
60
|
+
"music-metadata": "^11.12.1",
|
|
60
61
|
"zod": "^4.3.6"
|
|
61
62
|
},
|
|
62
63
|
"devDependencies": {
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: feishu-message
|
|
3
|
+
description: |
|
|
4
|
+
Feishu message reading. Activate when user mentions reading messages, chat history, message lookup, or finding previous messages.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Feishu Message Tool
|
|
8
|
+
|
|
9
|
+
Single tool `feishu_message` for reading Feishu messages — get a single message by ID or list recent messages in a chat.
|
|
10
|
+
|
|
11
|
+
## Actions
|
|
12
|
+
|
|
13
|
+
### Get Single Message
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"action": "get",
|
|
18
|
+
"message_id": "om_xxx"
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"ok": true,
|
|
27
|
+
"action": "get",
|
|
28
|
+
"found": true,
|
|
29
|
+
"message_id": "om_xxx",
|
|
30
|
+
"msg_type": "text",
|
|
31
|
+
"content": "Hello world",
|
|
32
|
+
"sender_id": "ou_xxx",
|
|
33
|
+
"sender_type": "user",
|
|
34
|
+
"chat_id": "oc_xxx",
|
|
35
|
+
"create_time": "1710000000000",
|
|
36
|
+
"update_time": "1710000000000",
|
|
37
|
+
"mentions": []
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### List Recent Messages
|
|
42
|
+
|
|
43
|
+
List recent messages in a chat (DM or group), newest first by default.
|
|
44
|
+
Omit `chat_id` to use the current conversation's chat:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"action": "list"
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
With custom page size, sort order, and time range:
|
|
53
|
+
|
|
54
|
+
```json
|
|
55
|
+
{
|
|
56
|
+
"action": "list",
|
|
57
|
+
"chat_id": "oc_xxx",
|
|
58
|
+
"page_size": 20,
|
|
59
|
+
"sort_type": "ByCreateTimeAsc",
|
|
60
|
+
"start_time": "1710000000",
|
|
61
|
+
"end_time": "1710086400"
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"ok": true,
|
|
70
|
+
"action": "list",
|
|
71
|
+
"chat_id": "oc_xxx",
|
|
72
|
+
"total": 10,
|
|
73
|
+
"messages": [
|
|
74
|
+
{
|
|
75
|
+
"message_id": "om_xxx",
|
|
76
|
+
"msg_type": "text",
|
|
77
|
+
"content_preview": "Hello world",
|
|
78
|
+
"sender_id": "ou_xxx",
|
|
79
|
+
"sender_type": "user",
|
|
80
|
+
"create_time": "1710000000000",
|
|
81
|
+
"chat_id": "oc_xxx"
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Parameters
|
|
88
|
+
|
|
89
|
+
| Parameter | Required | Description |
|
|
90
|
+
|-----------|----------|-------------|
|
|
91
|
+
| `action` | Yes | `get` or `list` |
|
|
92
|
+
| `message_id` | get: Yes | Feishu message ID (e.g., `om_xxx`) |
|
|
93
|
+
| `chat_id` | list: Optional | Chat ID (e.g., `oc_xxx`). Omit to use current chat. |
|
|
94
|
+
| `page_size` | list: Optional | Number of messages (default: 10, max: 50) |
|
|
95
|
+
| `sort_type` | list: Optional | `ByCreateTimeDesc` (default) or `ByCreateTimeAsc` |
|
|
96
|
+
| `start_time` | list: Optional | Unix timestamp in seconds (e.g., `"1710000000"`). No lower bound if omitted. |
|
|
97
|
+
| `end_time` | list: Optional | Unix timestamp in seconds (e.g., `"1710086400"`). No upper bound if omitted. |
|
|
98
|
+
|
|
99
|
+
## Configuration
|
|
100
|
+
|
|
101
|
+
```yaml
|
|
102
|
+
channels:
|
|
103
|
+
feishu:
|
|
104
|
+
tools:
|
|
105
|
+
message: true # default: true
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Permissions
|
|
109
|
+
|
|
110
|
+
- `im:message` or `im:message:readonly` — read single/DM messages
|
|
111
|
+
- `im:message.group_at_msg:readonly` — read all messages in group chats (not just @bot messages)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: feishu-reaction
|
|
3
|
+
description: |
|
|
4
|
+
Feishu message emoji reactions. Activate when user mentions emoji, reaction, thumbsup, like, or responding to messages with emoji.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Feishu Reaction Tool
|
|
8
|
+
|
|
9
|
+
Single tool `feishu_reaction` for adding, removing, and listing emoji reactions on Feishu messages.
|
|
10
|
+
|
|
11
|
+
## Workflow: Reacting to Previous Messages
|
|
12
|
+
|
|
13
|
+
To react to a message other than the current one, first use `feishu_message` with `action: "list"` to fetch recent messages and find the target `message_id`, then use `feishu_reaction` with that `message_id`.
|
|
14
|
+
|
|
15
|
+
## Actions
|
|
16
|
+
|
|
17
|
+
### Add Reaction
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"action": "add",
|
|
22
|
+
"message_id": "om_xxx",
|
|
23
|
+
"emoji_type": "THUMBSUP"
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"ok": true,
|
|
32
|
+
"action": "add",
|
|
33
|
+
"message_id": "om_xxx",
|
|
34
|
+
"emoji_type": "THUMBSUP",
|
|
35
|
+
"reaction_id": "ZCaCIjUBVVWSrm5L-3ZTw"
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Remove Reaction
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"action": "remove",
|
|
44
|
+
"message_id": "om_xxx",
|
|
45
|
+
"reaction_id": "ZCaCIjUBVVWSrm5L-3ZTw"
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### List Reactions
|
|
50
|
+
|
|
51
|
+
List all reactions on a message:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"action": "list",
|
|
56
|
+
"message_id": "om_xxx"
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Filter by emoji type:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"action": "list",
|
|
65
|
+
"message_id": "om_xxx",
|
|
66
|
+
"emoji_type": "THUMBSUP"
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"ok": true,
|
|
75
|
+
"action": "list",
|
|
76
|
+
"message_id": "om_xxx",
|
|
77
|
+
"emoji_type_filter": "THUMBSUP",
|
|
78
|
+
"total": 2,
|
|
79
|
+
"reactions": [
|
|
80
|
+
{
|
|
81
|
+
"reaction_id": "ZCaCIjUBVVWSrm5L-3ZTw",
|
|
82
|
+
"emoji_type": "THUMBSUP",
|
|
83
|
+
"operator_type": "user",
|
|
84
|
+
"operator_id": "ou_xxx"
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Parameters
|
|
91
|
+
|
|
92
|
+
| Parameter | Required | Description |
|
|
93
|
+
|-----------|----------|-------------|
|
|
94
|
+
| `action` | Yes | `add`, `remove`, or `list` |
|
|
95
|
+
| `message_id` | Yes | Feishu message ID (e.g., `om_xxx`) |
|
|
96
|
+
| `emoji_type` | add: Yes, list: Optional | Emoji type (e.g., `THUMBSUP`, `HEART`, `SMILE`) |
|
|
97
|
+
| `reaction_id` | remove: Yes | Reaction ID from add or list results |
|
|
98
|
+
|
|
99
|
+
## Common Emoji Types
|
|
100
|
+
|
|
101
|
+
| Emoji | Type |
|
|
102
|
+
|-------|------|
|
|
103
|
+
| THUMBSUP | THUMBSDOWN | HEART | SMILE | GRINNING |
|
|
104
|
+
| FIRE | CLAP | OK | CHECK | CROSS |
|
|
105
|
+
| PARTY | PRAY | CRY | ANGRY | THINKING |
|
|
106
|
+
| SURPRISED | LAUGHING | FIST | QUESTION | EXCLAMATION |
|
|
107
|
+
|
|
108
|
+
## Configuration
|
|
109
|
+
|
|
110
|
+
```yaml
|
|
111
|
+
channels:
|
|
112
|
+
feishu:
|
|
113
|
+
tools:
|
|
114
|
+
reaction: true # default: true
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Permissions
|
|
118
|
+
|
|
119
|
+
- `im:message.reactions:write_only` — add and remove reactions
|
|
120
|
+
- `im:message.reactions:read` — list reactions on messages
|
package/src/accounts.ts
CHANGED
|
@@ -10,7 +10,11 @@ function listConfiguredAccountIds(cfg: ClawdbotConfig): string[] {
|
|
|
10
10
|
if (!accounts || typeof accounts !== "object") {
|
|
11
11
|
return [];
|
|
12
12
|
}
|
|
13
|
-
return
|
|
13
|
+
return [...new Set(
|
|
14
|
+
Object.keys(accounts)
|
|
15
|
+
.filter((accountId) => accountId.trim())
|
|
16
|
+
.map((accountId) => normalizeAccountId(accountId)),
|
|
17
|
+
)];
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
/**
|
|
@@ -38,17 +42,35 @@ export function resolveDefaultFeishuAccountId(cfg: ClawdbotConfig): string {
|
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
/**
|
|
41
|
-
*
|
|
45
|
+
* Resolve the configured account key for a normalized account id.
|
|
46
|
+
* Preserves the original config key casing so write paths can avoid shadow entries.
|
|
42
47
|
*/
|
|
43
|
-
function
|
|
48
|
+
export function resolveConfiguredFeishuAccountKey(
|
|
44
49
|
cfg: ClawdbotConfig,
|
|
45
50
|
accountId: string,
|
|
46
|
-
):
|
|
51
|
+
): string | undefined {
|
|
47
52
|
const accounts = (cfg.channels?.feishu as FeishuConfig)?.accounts;
|
|
48
53
|
if (!accounts || typeof accounts !== "object") {
|
|
49
54
|
return undefined;
|
|
50
55
|
}
|
|
51
|
-
|
|
56
|
+
if (Object.prototype.hasOwnProperty.call(accounts, accountId)) {
|
|
57
|
+
return accountId;
|
|
58
|
+
}
|
|
59
|
+
const normalizedId = normalizeAccountId(accountId);
|
|
60
|
+
return Object.keys(accounts).find((key) => normalizeAccountId(key) === normalizedId);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get the raw account-specific config.
|
|
65
|
+
* Preserves mixed-case config keys by resolving through the normalized account id.
|
|
66
|
+
*/
|
|
67
|
+
function resolveAccountConfig(
|
|
68
|
+
cfg: ClawdbotConfig,
|
|
69
|
+
accountId: string,
|
|
70
|
+
): FeishuAccountConfig | undefined {
|
|
71
|
+
const accounts = (cfg.channels?.feishu as FeishuConfig)?.accounts;
|
|
72
|
+
const matchedKey = resolveConfiguredFeishuAccountKey(cfg, accountId);
|
|
73
|
+
return matchedKey && accounts ? accounts[matchedKey] : undefined;
|
|
52
74
|
}
|
|
53
75
|
|
|
54
76
|
/**
|
package/src/bot.ts
CHANGED
|
@@ -94,6 +94,7 @@ const senderNameCache = new Map<string, { name: string; expireAt: number }>();
|
|
|
94
94
|
// Key: appId or "default", Value: timestamp of last notification
|
|
95
95
|
const permissionErrorNotifiedAt = new Map<string, number>();
|
|
96
96
|
const PERMISSION_ERROR_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes
|
|
97
|
+
const DEFAULT_MAX_MESSAGE_AGE_MS = 300_000; // 5 minutes
|
|
97
98
|
|
|
98
99
|
type SenderNameResult = {
|
|
99
100
|
name?: string;
|
|
@@ -918,13 +919,20 @@ export async function handleFeishuMessage(params: {
|
|
|
918
919
|
return;
|
|
919
920
|
}
|
|
920
921
|
|
|
921
|
-
let ctx = parseFeishuMessageEvent(event, botOpenId);
|
|
922
|
-
|
|
923
922
|
// Parse message create_time (Feishu uses millisecond epoch string).
|
|
924
923
|
const messageCreateTimeMs = event.message.create_time
|
|
925
924
|
? parseInt(event.message.create_time, 10)
|
|
926
925
|
: undefined;
|
|
927
926
|
|
|
927
|
+
// Discard stale messages (e.g. replayed after gateway restart).
|
|
928
|
+
const maxAgeMs = feishuCfg.maxMessageAgeMs ?? DEFAULT_MAX_MESSAGE_AGE_MS;
|
|
929
|
+
if (messageCreateTimeMs && Date.now() - messageCreateTimeMs > maxAgeMs) {
|
|
930
|
+
log(`feishu[${account.accountId}]: discarding stale message ${messageId} (age: ${Math.round((Date.now() - messageCreateTimeMs) / 1000)}s, max: ${maxAgeMs / 1000}s)`);
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
let ctx = parseFeishuMessageEvent(event, botOpenId);
|
|
935
|
+
|
|
928
936
|
const messageType = event.message.message_type;
|
|
929
937
|
const isForwardedInbound = isForwardedMessageType(messageType);
|
|
930
938
|
const forwardedDispatchKey = getForwardedKey({
|
|
@@ -1304,9 +1312,12 @@ export async function handleFeishuMessage(params: {
|
|
|
1304
1312
|
}
|
|
1305
1313
|
}
|
|
1306
1314
|
|
|
1307
|
-
// In group chats,
|
|
1308
|
-
//
|
|
1309
|
-
|
|
1315
|
+
// In group chats, From must be group-scoped so the core's resolveGroupSessionKey can
|
|
1316
|
+
// detect group context and route replies back to the group — not to a user's DM session.
|
|
1317
|
+
// We include senderOpenId to preserve per-speaker identity within the group.
|
|
1318
|
+
const feishuFrom = isGroup
|
|
1319
|
+
? `feishu:${ctx.chatId}:${ctx.senderOpenId}`
|
|
1320
|
+
: `feishu:${ctx.senderOpenId}`;
|
|
1310
1321
|
const feishuTo = isGroup ? `chat:${ctx.chatId}` : `user:${ctx.senderOpenId}`;
|
|
1311
1322
|
|
|
1312
1323
|
// Resolve peer ID for session routing
|
|
@@ -1489,6 +1500,7 @@ export async function handleFeishuMessage(params: {
|
|
|
1489
1500
|
accountId: account.accountId,
|
|
1490
1501
|
sessionKey: route.sessionKey,
|
|
1491
1502
|
senderOpenId: ctx.senderOpenId,
|
|
1503
|
+
chatId: ctx.chatId,
|
|
1492
1504
|
},
|
|
1493
1505
|
// Keep account context available while the agent executes plugin tools.
|
|
1494
1506
|
() =>
|
|
@@ -1576,6 +1588,7 @@ export async function handleFeishuMessage(params: {
|
|
|
1576
1588
|
accountId: account.accountId,
|
|
1577
1589
|
sessionKey: route.sessionKey,
|
|
1578
1590
|
senderOpenId: ctx.senderOpenId,
|
|
1591
|
+
chatId: ctx.chatId,
|
|
1579
1592
|
},
|
|
1580
1593
|
// Tool calls produced by this turn should resolve to the same inbound account.
|
|
1581
1594
|
() =>
|
package/src/channel.ts
CHANGED
|
@@ -2,11 +2,13 @@ import type { ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
|
2
2
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId, PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk";
|
|
3
3
|
import type { ResolvedFeishuAccount, FeishuConfig } from "./types.js";
|
|
4
4
|
import {
|
|
5
|
+
resolveConfiguredFeishuAccountKey,
|
|
5
6
|
resolveFeishuAccount,
|
|
6
7
|
resolveFeishuCredentials,
|
|
7
8
|
listFeishuAccountIds,
|
|
8
9
|
resolveDefaultFeishuAccountId,
|
|
9
10
|
} from "./accounts.js";
|
|
11
|
+
import { monitorFeishuProvider } from "./monitor.js";
|
|
10
12
|
import { feishuOutbound } from "./outbound.js";
|
|
11
13
|
import { probeFeishu } from "./probe.js";
|
|
12
14
|
import { resolveFeishuGroupToolPolicy } from "./policy.js";
|
|
@@ -60,6 +62,8 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
60
62
|
messageToolHints: () => [
|
|
61
63
|
"- Feishu targeting: omit `target` to reply to the current conversation (auto-inferred). Explicit targets: `user:open_id` or `chat:chat_id`.",
|
|
62
64
|
"- Feishu supports interactive cards for rich messages.",
|
|
65
|
+
"- To send a file from the OpenClaw agent's local filesystem workspace (or another allowed local path) as a Feishu attachment, reply with a standalone `MEDIA:<absolute-path-or-url>` line.",
|
|
66
|
+
"- `feishu_drive` is for Feishu cloud Drive content, not for the OpenClaw agent's local filesystem workspace.",
|
|
63
67
|
],
|
|
64
68
|
},
|
|
65
69
|
groups: {
|
|
@@ -127,7 +131,6 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
127
131
|
resolveAccount: (cfg, accountId) => resolveFeishuAccount({ cfg, accountId }),
|
|
128
132
|
defaultAccountId: (cfg) => resolveDefaultFeishuAccountId(cfg),
|
|
129
133
|
setAccountEnabled: ({ cfg, accountId, enabled }) => {
|
|
130
|
-
const account = resolveFeishuAccount({ cfg, accountId });
|
|
131
134
|
const isDefault = accountId === DEFAULT_ACCOUNT_ID;
|
|
132
135
|
|
|
133
136
|
if (isDefault) {
|
|
@@ -146,6 +149,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
146
149
|
|
|
147
150
|
// For named accounts, set enabled in accounts[accountId]
|
|
148
151
|
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
152
|
+
const accountKey = resolveConfiguredFeishuAccountKey(cfg, accountId) ?? accountId;
|
|
149
153
|
return {
|
|
150
154
|
...cfg,
|
|
151
155
|
channels: {
|
|
@@ -154,8 +158,8 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
154
158
|
...feishuCfg,
|
|
155
159
|
accounts: {
|
|
156
160
|
...feishuCfg?.accounts,
|
|
157
|
-
[
|
|
158
|
-
...feishuCfg?.accounts?.[
|
|
161
|
+
[accountKey]: {
|
|
162
|
+
...feishuCfg?.accounts?.[accountKey],
|
|
159
163
|
enabled,
|
|
160
164
|
},
|
|
161
165
|
},
|
|
@@ -182,7 +186,8 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
182
186
|
// Delete specific account from accounts
|
|
183
187
|
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
184
188
|
const accounts = { ...feishuCfg?.accounts };
|
|
185
|
-
|
|
189
|
+
const accountKey = resolveConfiguredFeishuAccountKey(cfg, accountId) ?? accountId;
|
|
190
|
+
delete accounts[accountKey];
|
|
186
191
|
|
|
187
192
|
return {
|
|
188
193
|
...cfg,
|
|
@@ -245,6 +250,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
245
250
|
}
|
|
246
251
|
|
|
247
252
|
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
253
|
+
const accountKey = resolveConfiguredFeishuAccountKey(cfg, accountId) ?? accountId;
|
|
248
254
|
return {
|
|
249
255
|
...cfg,
|
|
250
256
|
channels: {
|
|
@@ -253,8 +259,8 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
253
259
|
...feishuCfg,
|
|
254
260
|
accounts: {
|
|
255
261
|
...feishuCfg?.accounts,
|
|
256
|
-
[
|
|
257
|
-
...feishuCfg?.accounts?.[
|
|
262
|
+
[accountKey]: {
|
|
263
|
+
...feishuCfg?.accounts?.[accountKey],
|
|
258
264
|
enabled: true,
|
|
259
265
|
},
|
|
260
266
|
},
|
|
@@ -322,7 +328,6 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
322
328
|
},
|
|
323
329
|
gateway: {
|
|
324
330
|
startAccount: async (ctx) => {
|
|
325
|
-
const { monitorFeishuProvider } = await import("./monitor.js");
|
|
326
331
|
const account = resolveFeishuAccount({ cfg: ctx.cfg, accountId: ctx.accountId });
|
|
327
332
|
const port = account.config?.webhookPort ?? null;
|
|
328
333
|
ctx.setStatus({ accountId: ctx.accountId, port });
|
package/src/config-schema.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeAccountId } from "openclaw/plugin-sdk";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
export { z };
|
|
3
4
|
|
|
@@ -91,6 +92,8 @@ const FeishuToolsConfigSchema = z
|
|
|
91
92
|
task: z.boolean().optional(), // Task operations (default: true)
|
|
92
93
|
chat: z.boolean().optional(), // Chat management operations (default: true)
|
|
93
94
|
urgent: z.boolean().optional(), // Buzz/urgent notifications (default: true)
|
|
95
|
+
message: z.boolean().optional(), // Message reading (default: true)
|
|
96
|
+
reaction: z.boolean().optional(), // Emoji reactions (default: true)
|
|
94
97
|
})
|
|
95
98
|
.strict()
|
|
96
99
|
.optional();
|
|
@@ -155,6 +158,7 @@ export const FeishuAccountConfigSchema = z
|
|
|
155
158
|
mediaMaxMb: z.number().positive().optional(),
|
|
156
159
|
mediaLocalRoots: z.array(z.string()).optional(),
|
|
157
160
|
heartbeat: ChannelHeartbeatVisibilitySchema,
|
|
161
|
+
maxMessageAgeMs: z.number().positive().optional(),
|
|
158
162
|
renderMode: RenderModeSchema,
|
|
159
163
|
streaming: StreamingModeSchema,
|
|
160
164
|
tools: FeishuToolsConfigSchema,
|
|
@@ -194,6 +198,7 @@ export const FeishuConfigSchema = z
|
|
|
194
198
|
mediaMaxMb: z.number().positive().optional(),
|
|
195
199
|
mediaLocalRoots: z.array(z.string()).optional(),
|
|
196
200
|
heartbeat: ChannelHeartbeatVisibilitySchema,
|
|
201
|
+
maxMessageAgeMs: z.number().positive().optional(),
|
|
197
202
|
renderMode: RenderModeSchema, // raw = plain text (default), card = interactive card with markdown
|
|
198
203
|
streaming: StreamingModeSchema,
|
|
199
204
|
tools: FeishuToolsConfigSchema,
|
|
@@ -204,6 +209,23 @@ export const FeishuConfigSchema = z
|
|
|
204
209
|
})
|
|
205
210
|
.strict()
|
|
206
211
|
.superRefine((value, ctx) => {
|
|
212
|
+
const normalizedAccountIds = new Map<string, string>();
|
|
213
|
+
for (const accountId of Object.keys(value.accounts ?? {})) {
|
|
214
|
+
const normalizedAccountId = normalizeAccountId(accountId);
|
|
215
|
+
const previousAccountId = normalizedAccountIds.get(normalizedAccountId);
|
|
216
|
+
if (previousAccountId && previousAccountId !== accountId) {
|
|
217
|
+
ctx.addIssue({
|
|
218
|
+
code: z.ZodIssueCode.custom,
|
|
219
|
+
path: ["accounts", accountId],
|
|
220
|
+
message:
|
|
221
|
+
`channels.feishu.accounts contains duplicate account ids after normalization: ` +
|
|
222
|
+
`"${previousAccountId}" and "${accountId}" both normalize to "${normalizedAccountId}"`,
|
|
223
|
+
});
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
normalizedAccountIds.set(normalizedAccountId, accountId);
|
|
227
|
+
}
|
|
228
|
+
|
|
207
229
|
if (value.dmPolicy === "open") {
|
|
208
230
|
const allowFrom = value.allowFrom ?? [];
|
|
209
231
|
const hasWildcard = allowFrom.some((entry) => String(entry).trim() === "*");
|
|
@@ -59,7 +59,7 @@ export function registerFeishuDriveTools(api: OpenClawPluginApi) {
|
|
|
59
59
|
name: "feishu_drive",
|
|
60
60
|
label: "Feishu Drive",
|
|
61
61
|
description:
|
|
62
|
-
"Feishu cloud
|
|
62
|
+
"Feishu cloud Drive operations. This tool works with Feishu Drive folders/files/documents, not the OpenClaw agent's local filesystem workspace. Actions: list, info, create_folder, move, delete, import_document. Use 'import_document' to create documents from Markdown with better structure preservation than block-by-block writing.",
|
|
63
63
|
parameters: FeishuDriveSchema,
|
|
64
64
|
run: async ({ client, account }, params) => {
|
|
65
65
|
const mediaMaxBytes = (account.config?.mediaMaxMb ?? 30) * 1024 * 1024;
|