@m1heng-clawd/feishu 0.1.10 → 0.1.12
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 +280 -7
- package/index.ts +6 -6
- package/package.json +13 -3
- package/skills/feishu-doc/SKILL.md +64 -2
- package/skills/feishu-task/SKILL.md +210 -0
- package/src/bitable-tools/index.ts +1 -0
- package/src/bot.ts +182 -71
- package/src/channel.ts +2 -0
- package/src/config-schema.ts +4 -0
- package/src/doc-tools/actions.ts +341 -0
- package/src/doc-tools/common.ts +33 -0
- package/src/doc-tools/index.ts +2 -0
- package/src/doc-tools/register.ts +90 -0
- package/src/{doc-schema.ts → doc-tools/schemas.ts} +39 -1
- package/src/{docx.ts → doc-write-service.ts} +204 -351
- package/src/drive-tools/actions.ts +182 -0
- package/src/drive-tools/common.ts +18 -0
- package/src/drive-tools/index.ts +2 -0
- package/src/drive-tools/register.ts +71 -0
- package/src/{drive-schema.ts → drive-tools/schemas.ts} +2 -1
- package/src/media.ts +5 -3
- package/src/onboarding.ts +14 -4
- package/src/perm-tools/actions.ts +111 -0
- package/src/perm-tools/common.ts +18 -0
- package/src/perm-tools/index.ts +2 -0
- package/src/perm-tools/register.ts +65 -0
- package/src/policy.ts +13 -0
- package/src/reply-dispatcher.ts +6 -4
- package/src/send.ts +33 -2
- package/src/task-tools/actions.ts +466 -13
- package/src/task-tools/constants.ts +13 -0
- package/src/task-tools/index.ts +1 -0
- package/src/task-tools/register.ts +175 -13
- package/src/task-tools/schemas.ts +433 -4
- package/src/text/markdown-links.ts +104 -0
- package/src/types.ts +1 -0
- package/src/wiki-tools/actions.ts +166 -0
- package/src/wiki-tools/common.ts +18 -0
- package/src/wiki-tools/index.ts +2 -0
- package/src/wiki-tools/register.ts +66 -0
- package/src/bitable.ts +0 -1
- package/src/drive.ts +0 -264
- package/src/perm.ts +0 -165
- package/src/task.ts +0 -1
- package/src/wiki.ts +0 -223
- /package/src/{perm-schema.ts → perm-tools/schemas.ts} +0 -0
- /package/src/{wiki-schema.ts → wiki-tools/schemas.ts} +0 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: feishu-task
|
|
3
|
+
description: |
|
|
4
|
+
Feishu Task, tasklist, subtask, comment, and attachment management. Activate when user mentions tasks, tasklists, subtasks, task comments, task attachments, or task links.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Feishu Task Tools
|
|
8
|
+
|
|
9
|
+
Tools:
|
|
10
|
+
- `feishu_task_create`
|
|
11
|
+
- `feishu_task_subtask_create`
|
|
12
|
+
- `feishu_task_get`
|
|
13
|
+
- `feishu_task_update`
|
|
14
|
+
- `feishu_task_delete`
|
|
15
|
+
- `feishu_task_comment_create`
|
|
16
|
+
- `feishu_task_comment_list`
|
|
17
|
+
- `feishu_task_comment_get`
|
|
18
|
+
- `feishu_task_comment_update`
|
|
19
|
+
- `feishu_task_comment_delete`
|
|
20
|
+
- `feishu_task_attachment_upload`
|
|
21
|
+
- `feishu_task_attachment_list`
|
|
22
|
+
- `feishu_task_attachment_get`
|
|
23
|
+
- `feishu_task_attachment_delete`
|
|
24
|
+
- `feishu_task_add_tasklist`
|
|
25
|
+
- `feishu_task_remove_tasklist`
|
|
26
|
+
- `feishu_tasklist_create`
|
|
27
|
+
- `feishu_tasklist_get`
|
|
28
|
+
- `feishu_tasklist_list`
|
|
29
|
+
- `feishu_tasklist_update`
|
|
30
|
+
- `feishu_tasklist_delete`
|
|
31
|
+
- `feishu_tasklist_add_members`
|
|
32
|
+
- `feishu_tasklist_remove_members`
|
|
33
|
+
|
|
34
|
+
## Notes
|
|
35
|
+
|
|
36
|
+
- `task_guid` can be taken from a task URL (guid query param) or from `feishu_task_get` output.
|
|
37
|
+
- `comment_id` can be obtained from `feishu_task_comment_list` output.
|
|
38
|
+
- `attachment_guid` can be obtained from `feishu_task_attachment_list` output.
|
|
39
|
+
- `user_id_type` controls returned/accepted user identity type (`open_id`, `user_id`, `union_id`).
|
|
40
|
+
- If no assignee is specified, set the assignee to the requesting user. Avoid creating unassigned tasks because the user may not be able to view them.
|
|
41
|
+
- Task visibility: users can only view tasks when they are included as assignee.
|
|
42
|
+
- Current limitation: the bot can only create subtasks for tasks created by itself.
|
|
43
|
+
- Attachment upload supports local `file_path` and remote `file_url`. Remote URLs are fetched with runtime media safety checks and size limit (`mediaMaxMb`).
|
|
44
|
+
- Keep tasklist owner as the bot. Add users as members to avoid losing bot access.
|
|
45
|
+
- Use tasklist tools for tasklist membership changes; do not use `feishu_task_update` to move tasks between tasklists.
|
|
46
|
+
|
|
47
|
+
## Create Task
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"summary": "Quarterly review",
|
|
52
|
+
"description": "Prepare review notes",
|
|
53
|
+
"due": { "timestamp": "1735689600000", "is_all_day": true },
|
|
54
|
+
"members": [
|
|
55
|
+
{ "id": "ou_xxx", "role": "assignee", "type": "user" }
|
|
56
|
+
],
|
|
57
|
+
"user_id_type": "open_id"
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Create Subtask
|
|
62
|
+
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"task_guid": "e297ddff-06ca-4166-b917-4ce57cd3a7a0",
|
|
66
|
+
"summary": "Draft report outline",
|
|
67
|
+
"description": "Collect key metrics",
|
|
68
|
+
"due": { "timestamp": "1735689600000", "is_all_day": true },
|
|
69
|
+
"members": [
|
|
70
|
+
{ "id": "ou_xxx", "role": "assignee", "type": "user" }
|
|
71
|
+
],
|
|
72
|
+
"user_id_type": "open_id"
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Create Comment
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"task_guid": "e297ddff-06ca-4166-b917-4ce57cd3a7a0",
|
|
81
|
+
"content": "Looks good to me",
|
|
82
|
+
"user_id_type": "open_id"
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Upload Attachment (file_path)
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"task_guid": "e297ddff-06ca-4166-b917-4ce57cd3a7a0",
|
|
91
|
+
"file_path": "/path/to/report.pdf",
|
|
92
|
+
"user_id_type": "open_id"
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Upload Attachment (file_url)
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"task_guid": "e297ddff-06ca-4166-b917-4ce57cd3a7a0",
|
|
101
|
+
"file_url": "https://oss-example.com/bucket/report.pdf",
|
|
102
|
+
"filename": "report.pdf",
|
|
103
|
+
"user_id_type": "open_id"
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Tasklist Membership For Tasks
|
|
108
|
+
|
|
109
|
+
### Add Task to Tasklist
|
|
110
|
+
|
|
111
|
+
```json
|
|
112
|
+
{
|
|
113
|
+
"task_guid": "e297ddff-06ca-4166-b917-4ce57cd3a7a0",
|
|
114
|
+
"tasklist_guid": "cc371766-6584-cf50-a222-c22cd9055004",
|
|
115
|
+
"section_guid": "6d0f9f48-2e06-4e3d-8a0f-acde196e8c61",
|
|
116
|
+
"user_id_type": "open_id"
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Remove Task from Tasklist
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"task_guid": "e297ddff-06ca-4166-b917-4ce57cd3a7a0",
|
|
125
|
+
"tasklist_guid": "cc371766-6584-cf50-a222-c22cd9055004",
|
|
126
|
+
"user_id_type": "open_id"
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Tasklists
|
|
131
|
+
|
|
132
|
+
Tasklists support three roles: owner (read/edit/manage), editor (read/edit), viewer (read).
|
|
133
|
+
|
|
134
|
+
### Create Tasklist
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"name": "Project Alpha Tasklist",
|
|
139
|
+
"members": [
|
|
140
|
+
{ "id": "ou_xxx", "type": "user", "role": "editor" }
|
|
141
|
+
],
|
|
142
|
+
"user_id_type": "open_id"
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Get Tasklist
|
|
147
|
+
|
|
148
|
+
```json
|
|
149
|
+
{
|
|
150
|
+
"tasklist_guid": "cc371766-6584-cf50-a222-c22cd9055004",
|
|
151
|
+
"user_id_type": "open_id"
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### List Tasklists
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"page_size": 50,
|
|
160
|
+
"page_token": "aWQ9NzEwMjMzMjMxMDE=",
|
|
161
|
+
"user_id_type": "open_id"
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Update Tasklist
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"tasklist_guid": "cc371766-6584-cf50-a222-c22cd9055004",
|
|
170
|
+
"tasklist": {
|
|
171
|
+
"name": "Renamed Tasklist",
|
|
172
|
+
"owner": { "id": "ou_xxx", "type": "user", "role": "owner" }
|
|
173
|
+
},
|
|
174
|
+
"update_fields": ["name", "owner"],
|
|
175
|
+
"origin_owner_to_role": "editor",
|
|
176
|
+
"user_id_type": "open_id"
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Delete Tasklist
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{
|
|
184
|
+
"tasklist_guid": "cc371766-6584-cf50-a222-c22cd9055004"
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Add Tasklist Members
|
|
189
|
+
|
|
190
|
+
```json
|
|
191
|
+
{
|
|
192
|
+
"tasklist_guid": "cc371766-6584-cf50-a222-c22cd9055004",
|
|
193
|
+
"members": [
|
|
194
|
+
{ "id": "ou_xxx", "type": "user", "role": "editor" }
|
|
195
|
+
],
|
|
196
|
+
"user_id_type": "open_id"
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Remove Tasklist Members
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
{
|
|
204
|
+
"tasklist_guid": "cc371766-6584-cf50-a222-c22cd9055004",
|
|
205
|
+
"members": [
|
|
206
|
+
{ "id": "ou_xxx", "type": "user", "role": "viewer" }
|
|
207
|
+
],
|
|
208
|
+
"user_id_type": "open_id"
|
|
209
|
+
}
|
|
210
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { registerFeishuBitableTools } from "./register.js";
|
package/src/bot.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
recordPendingHistoryEntryIfEnabled,
|
|
5
5
|
clearHistoryEntriesIfEnabled,
|
|
6
6
|
DEFAULT_GROUP_HISTORY_LIMIT,
|
|
7
|
+
resolveMentionGatingWithBypass,
|
|
7
8
|
type HistoryEntry,
|
|
8
9
|
} from "openclaw/plugin-sdk";
|
|
9
10
|
import type { FeishuConfig, FeishuMessageContext, FeishuMediaInfo, ResolvedFeishuAccount } from "./types.js";
|
|
@@ -14,6 +15,7 @@ import { tryRecordMessage } from "./dedup.js";
|
|
|
14
15
|
import {
|
|
15
16
|
resolveFeishuGroupConfig,
|
|
16
17
|
resolveFeishuReplyPolicy,
|
|
18
|
+
resolveFeishuGroupCommandMentionBypass,
|
|
17
19
|
resolveFeishuAllowlistMatch,
|
|
18
20
|
isFeishuGroupAllowed,
|
|
19
21
|
} from "./policy.js";
|
|
@@ -145,6 +147,40 @@ async function resolveFeishuSenderName(params: {
|
|
|
145
147
|
}
|
|
146
148
|
}
|
|
147
149
|
|
|
150
|
+
// Cache group bot counts for command mention bypass policy checks.
|
|
151
|
+
const GROUP_BOT_COUNT_TTL_MS = 10 * 60 * 1000;
|
|
152
|
+
const groupBotCountCache = new Map<string, { count: number; expireAt: number }>();
|
|
153
|
+
|
|
154
|
+
async function resolveFeishuGroupBotCount(params: {
|
|
155
|
+
account: ResolvedFeishuAccount;
|
|
156
|
+
chatId: string;
|
|
157
|
+
log: (...args: any[]) => void;
|
|
158
|
+
}): Promise<number | undefined> {
|
|
159
|
+
const { account, chatId, log } = params;
|
|
160
|
+
if (!account.configured || !chatId) return undefined;
|
|
161
|
+
|
|
162
|
+
const cacheKey = `${account.accountId}:${chatId}`;
|
|
163
|
+
const now = Date.now();
|
|
164
|
+
const cached = groupBotCountCache.get(cacheKey);
|
|
165
|
+
if (cached && cached.expireAt > now) return cached.count;
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const client = createFeishuClient(account);
|
|
169
|
+
const res: any = await client.im.chat.get({
|
|
170
|
+
path: { chat_id: chatId },
|
|
171
|
+
});
|
|
172
|
+
const parsed = Number.parseInt(String(res?.data?.bot_count ?? ""), 10);
|
|
173
|
+
if (Number.isFinite(parsed) && parsed >= 0) {
|
|
174
|
+
groupBotCountCache.set(cacheKey, { count: parsed, expireAt: now + GROUP_BOT_COUNT_TTL_MS });
|
|
175
|
+
return parsed;
|
|
176
|
+
}
|
|
177
|
+
return undefined;
|
|
178
|
+
} catch (err) {
|
|
179
|
+
log(`feishu[${account.accountId}]: failed to resolve bot_count for ${chatId}: ${String(err)}`);
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
148
184
|
export type FeishuMessageEvent = {
|
|
149
185
|
sender: {
|
|
150
186
|
sender_id: {
|
|
@@ -204,13 +240,21 @@ function parseMessageContent(content: string, messageType: string): string {
|
|
|
204
240
|
}
|
|
205
241
|
}
|
|
206
242
|
|
|
207
|
-
function checkBotMentioned(
|
|
243
|
+
function checkBotMentioned(
|
|
244
|
+
event: FeishuMessageEvent,
|
|
245
|
+
botOpenId?: string,
|
|
246
|
+
postMentionIds: string[] = [],
|
|
247
|
+
): boolean {
|
|
248
|
+
const normalizedBotOpenId = botOpenId?.trim();
|
|
249
|
+
// Keep explicit bot mention semantics: without a resolved botOpenId, do not trigger.
|
|
250
|
+
if (!normalizedBotOpenId) return false;
|
|
251
|
+
|
|
208
252
|
const mentions = event.message.mentions ?? [];
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
253
|
+
return (
|
|
254
|
+
mentions.some(
|
|
255
|
+
(m) => m.id.open_id === normalizedBotOpenId || m.id.user_id === normalizedBotOpenId,
|
|
256
|
+
) || postMentionIds.some((id) => id === normalizedBotOpenId)
|
|
257
|
+
);
|
|
214
258
|
}
|
|
215
259
|
|
|
216
260
|
function stripBotMention(text: string, mentions?: FeishuMessageEvent["message"]["mentions"]): string {
|
|
@@ -263,6 +307,7 @@ function parseMediaKeys(
|
|
|
263
307
|
function parsePostContent(content: string): {
|
|
264
308
|
textContent: string;
|
|
265
309
|
imageKeys: string[];
|
|
310
|
+
mentionIds: string[];
|
|
266
311
|
} {
|
|
267
312
|
try {
|
|
268
313
|
const parsed = JSON.parse(content);
|
|
@@ -270,6 +315,7 @@ function parsePostContent(content: string): {
|
|
|
270
315
|
const contentBlocks = parsed.content || [];
|
|
271
316
|
let textContent = title ? `${title}\n\n` : "";
|
|
272
317
|
const imageKeys: string[] = [];
|
|
318
|
+
const mentionIds: string[] = [];
|
|
273
319
|
|
|
274
320
|
for (const paragraph of contentBlocks) {
|
|
275
321
|
if (Array.isArray(paragraph)) {
|
|
@@ -281,7 +327,11 @@ function parsePostContent(content: string): {
|
|
|
281
327
|
textContent += element.text || element.href || "";
|
|
282
328
|
} else if (element.tag === "at") {
|
|
283
329
|
// Mention: @username
|
|
284
|
-
|
|
330
|
+
const mentionId =
|
|
331
|
+
String(element.open_id ?? element.user_id ?? element.union_id ?? "").trim() ||
|
|
332
|
+
undefined;
|
|
333
|
+
if (mentionId) mentionIds.push(mentionId);
|
|
334
|
+
textContent += `@${element.user_name || mentionId || ""}`;
|
|
285
335
|
} else if (element.tag === "img" && element.image_key) {
|
|
286
336
|
// Embedded image
|
|
287
337
|
imageKeys.push(element.image_key);
|
|
@@ -294,9 +344,10 @@ function parsePostContent(content: string): {
|
|
|
294
344
|
return {
|
|
295
345
|
textContent: textContent.trim() || "[富文本消息]",
|
|
296
346
|
imageKeys,
|
|
347
|
+
mentionIds,
|
|
297
348
|
};
|
|
298
349
|
} catch {
|
|
299
|
-
return { textContent: "[富文本消息]", imageKeys: [] };
|
|
350
|
+
return { textContent: "[富文本消息]", imageKeys: [], mentionIds: [] };
|
|
300
351
|
}
|
|
301
352
|
}
|
|
302
353
|
|
|
@@ -480,8 +531,12 @@ export function parseFeishuMessageEvent(
|
|
|
480
531
|
event: FeishuMessageEvent,
|
|
481
532
|
botOpenId?: string,
|
|
482
533
|
): FeishuMessageContext {
|
|
534
|
+
const parsedPost =
|
|
535
|
+
event.message.message_type === "post" ? parsePostContent(event.message.content) : undefined;
|
|
483
536
|
const rawContent = parseMessageContent(event.message.content, event.message.message_type);
|
|
484
|
-
const mentionedBot = checkBotMentioned(event, botOpenId);
|
|
537
|
+
const mentionedBot = checkBotMentioned(event, botOpenId, parsedPost?.mentionIds ?? []);
|
|
538
|
+
const hasAnyMention =
|
|
539
|
+
(event.message.mentions?.length ?? 0) > 0 || (parsedPost?.mentionIds.length ?? 0) > 0;
|
|
485
540
|
const content = stripBotMention(rawContent, event.message.mentions);
|
|
486
541
|
|
|
487
542
|
const ctx: FeishuMessageContext = {
|
|
@@ -495,6 +550,7 @@ export function parseFeishuMessageEvent(
|
|
|
495
550
|
parentId: event.message.parent_id || undefined,
|
|
496
551
|
content,
|
|
497
552
|
contentType: event.message.message_type,
|
|
553
|
+
hasAnyMention,
|
|
498
554
|
};
|
|
499
555
|
|
|
500
556
|
// Detect mention forward request: message mentions bot + at least one other user
|
|
@@ -520,11 +576,11 @@ export async function handleFeishuMessage(params: {
|
|
|
520
576
|
accountId?: string;
|
|
521
577
|
}): Promise<void> {
|
|
522
578
|
const { cfg, event, botOpenId, runtime, chatHistories, accountId } = params;
|
|
523
|
-
|
|
579
|
+
|
|
524
580
|
// Resolve account with merged config
|
|
525
581
|
const account = resolveFeishuAccount({ cfg, accountId });
|
|
526
582
|
const feishuCfg = account.config;
|
|
527
|
-
|
|
583
|
+
|
|
528
584
|
const log = runtime?.log ?? console.log;
|
|
529
585
|
const error = runtime?.error ?? console.error;
|
|
530
586
|
|
|
@@ -579,69 +635,17 @@ export async function handleFeishuMessage(params: {
|
|
|
579
635
|
const configAllowFrom = feishuCfg?.allowFrom ?? [];
|
|
580
636
|
const useAccessGroups = cfg.commands?.useAccessGroups !== false;
|
|
581
637
|
|
|
582
|
-
if (isGroup) {
|
|
583
|
-
const groupPolicy = feishuCfg?.groupPolicy ?? "open";
|
|
584
|
-
const groupAllowFrom = feishuCfg?.groupAllowFrom ?? [];
|
|
585
|
-
|
|
586
|
-
// Check if this GROUP is allowed (groupAllowFrom contains group IDs like oc_xxx, not user IDs)
|
|
587
|
-
const groupAllowed = isFeishuGroupAllowed({
|
|
588
|
-
groupPolicy,
|
|
589
|
-
allowFrom: groupAllowFrom,
|
|
590
|
-
senderId: ctx.chatId, // Check group ID, not sender ID
|
|
591
|
-
senderName: undefined,
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
if (!groupAllowed) {
|
|
595
|
-
log(`feishu[${account.accountId}]: sender ${ctx.senderOpenId} not in group allowlist`);
|
|
596
|
-
return;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// Additional sender-level allowlist check if group has specific allowFrom config
|
|
600
|
-
const senderAllowFrom = groupConfig?.allowFrom ?? [];
|
|
601
|
-
if (senderAllowFrom.length > 0) {
|
|
602
|
-
const senderAllowed = isFeishuGroupAllowed({
|
|
603
|
-
groupPolicy: "allowlist",
|
|
604
|
-
allowFrom: senderAllowFrom,
|
|
605
|
-
senderId: ctx.senderOpenId,
|
|
606
|
-
senderName: ctx.senderName,
|
|
607
|
-
});
|
|
608
|
-
if (!senderAllowed) {
|
|
609
|
-
log(`feishu: sender ${ctx.senderOpenId} not in group ${ctx.chatId} sender allowlist`);
|
|
610
|
-
return;
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
const { requireMention } = resolveFeishuReplyPolicy({
|
|
615
|
-
isDirectMessage: false,
|
|
616
|
-
globalConfig: feishuCfg,
|
|
617
|
-
groupConfig,
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
if (requireMention && !ctx.mentionedBot) {
|
|
621
|
-
log(`feishu[${account.accountId}]: message in group ${ctx.chatId} did not mention bot, recording to history`);
|
|
622
|
-
if (chatHistories) {
|
|
623
|
-
recordPendingHistoryEntryIfEnabled({
|
|
624
|
-
historyMap: chatHistories,
|
|
625
|
-
historyKey: ctx.chatId,
|
|
626
|
-
limit: historyLimit,
|
|
627
|
-
entry: {
|
|
628
|
-
sender: ctx.senderOpenId,
|
|
629
|
-
body: `${ctx.senderName ?? ctx.senderOpenId}: ${ctx.content}`,
|
|
630
|
-
timestamp: Date.now(),
|
|
631
|
-
messageId: ctx.messageId,
|
|
632
|
-
},
|
|
633
|
-
});
|
|
634
|
-
}
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
638
|
try {
|
|
640
639
|
const core = getFeishuRuntime();
|
|
641
640
|
const shouldComputeCommandAuthorized = core.channel.commands.shouldComputeCommandAuthorized(
|
|
642
641
|
ctx.content,
|
|
643
642
|
cfg,
|
|
644
643
|
);
|
|
644
|
+
const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
|
|
645
|
+
cfg,
|
|
646
|
+
surface: "feishu",
|
|
647
|
+
});
|
|
648
|
+
const hasControlCommand = core.channel.text.hasControlCommand(ctx.content, cfg);
|
|
645
649
|
const storeAllowFrom =
|
|
646
650
|
!isGroup && (dmPolicy !== "open" || shouldComputeCommandAuthorized)
|
|
647
651
|
? await core.channel.pairing.readAllowFromStore("feishu").catch(() => [])
|
|
@@ -687,7 +691,11 @@ export async function handleFeishuMessage(params: {
|
|
|
687
691
|
return;
|
|
688
692
|
}
|
|
689
693
|
|
|
690
|
-
const commandAllowFrom = isGroup
|
|
694
|
+
const commandAllowFrom = isGroup
|
|
695
|
+
? groupConfig?.allowFrom && groupConfig.allowFrom.length > 0
|
|
696
|
+
? groupConfig.allowFrom
|
|
697
|
+
: configAllowFrom
|
|
698
|
+
: effectiveDmAllowFrom;
|
|
691
699
|
const senderAllowedForCommands = resolveFeishuAllowlistMatch({
|
|
692
700
|
allowFrom: commandAllowFrom,
|
|
693
701
|
senderId: ctx.senderOpenId,
|
|
@@ -701,6 +709,105 @@ export async function handleFeishuMessage(params: {
|
|
|
701
709
|
],
|
|
702
710
|
})
|
|
703
711
|
: undefined;
|
|
712
|
+
let effectiveWasMentioned = ctx.mentionedBot;
|
|
713
|
+
|
|
714
|
+
if (isGroup) {
|
|
715
|
+
if (groupConfig?.enabled === false) {
|
|
716
|
+
log(`feishu[${account.accountId}]: group ${ctx.chatId} is disabled`);
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const groupPolicy = feishuCfg?.groupPolicy ?? "open";
|
|
721
|
+
const groupAllowFrom = feishuCfg?.groupAllowFrom ?? [];
|
|
722
|
+
|
|
723
|
+
// Check if this GROUP is allowed (groupAllowFrom contains group IDs like oc_xxx, not user IDs)
|
|
724
|
+
const groupAllowed = isFeishuGroupAllowed({
|
|
725
|
+
groupPolicy,
|
|
726
|
+
allowFrom: groupAllowFrom,
|
|
727
|
+
senderId: ctx.chatId, // Check group ID, not sender ID
|
|
728
|
+
senderName: undefined,
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
if (!groupAllowed) {
|
|
732
|
+
log(`feishu[${account.accountId}]: group ${ctx.chatId} not in groupAllowFrom (groupPolicy=${groupPolicy})`);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// Additional sender-level allowlist check if group has specific allowFrom config
|
|
737
|
+
const senderAllowFrom = groupConfig?.allowFrom ?? [];
|
|
738
|
+
if (senderAllowFrom.length > 0) {
|
|
739
|
+
const senderAllowed = isFeishuGroupAllowed({
|
|
740
|
+
groupPolicy: "allowlist",
|
|
741
|
+
allowFrom: senderAllowFrom,
|
|
742
|
+
senderId: ctx.senderOpenId,
|
|
743
|
+
senderName: ctx.senderName,
|
|
744
|
+
});
|
|
745
|
+
if (!senderAllowed) {
|
|
746
|
+
log(`feishu: sender ${ctx.senderOpenId} not in group ${ctx.chatId} sender allowlist`);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const { requireMention } = resolveFeishuReplyPolicy({
|
|
752
|
+
isDirectMessage: false,
|
|
753
|
+
globalConfig: feishuCfg,
|
|
754
|
+
groupConfig,
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
if (requireMention) {
|
|
758
|
+
const bypassPolicy = resolveFeishuGroupCommandMentionBypass({
|
|
759
|
+
globalConfig: feishuCfg,
|
|
760
|
+
groupConfig,
|
|
761
|
+
});
|
|
762
|
+
let bypassAllowedByPolicy = bypassPolicy === "always";
|
|
763
|
+
|
|
764
|
+
if (!bypassAllowedByPolicy && bypassPolicy === "single_bot" && hasControlCommand) {
|
|
765
|
+
const botCount = await resolveFeishuGroupBotCount({
|
|
766
|
+
account,
|
|
767
|
+
chatId: ctx.chatId,
|
|
768
|
+
log,
|
|
769
|
+
});
|
|
770
|
+
bypassAllowedByPolicy = botCount !== undefined && botCount <= 1;
|
|
771
|
+
if (botCount === undefined) {
|
|
772
|
+
log(
|
|
773
|
+
`feishu[${account.accountId}]: unable to resolve bot count for ${ctx.chatId}, command mention bypass disabled`,
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const mentionGate = resolveMentionGatingWithBypass({
|
|
779
|
+
isGroup: true,
|
|
780
|
+
requireMention,
|
|
781
|
+
canDetectMention: true,
|
|
782
|
+
wasMentioned: ctx.mentionedBot,
|
|
783
|
+
hasAnyMention: ctx.hasAnyMention,
|
|
784
|
+
allowTextCommands: allowTextCommands && bypassAllowedByPolicy,
|
|
785
|
+
hasControlCommand,
|
|
786
|
+
commandAuthorized: commandAuthorized === true,
|
|
787
|
+
});
|
|
788
|
+
effectiveWasMentioned = mentionGate.effectiveWasMentioned;
|
|
789
|
+
|
|
790
|
+
if (mentionGate.shouldSkip) {
|
|
791
|
+
log(
|
|
792
|
+
`feishu[${account.accountId}]: message in group ${ctx.chatId} skipped (mention required)`,
|
|
793
|
+
);
|
|
794
|
+
if (chatHistories) {
|
|
795
|
+
recordPendingHistoryEntryIfEnabled({
|
|
796
|
+
historyMap: chatHistories,
|
|
797
|
+
historyKey: ctx.chatId,
|
|
798
|
+
limit: historyLimit,
|
|
799
|
+
entry: {
|
|
800
|
+
sender: ctx.senderOpenId,
|
|
801
|
+
body: `${ctx.senderName ?? ctx.senderOpenId}: ${ctx.content}`,
|
|
802
|
+
timestamp: Date.now(),
|
|
803
|
+
messageId: ctx.messageId,
|
|
804
|
+
},
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
704
811
|
|
|
705
812
|
// In group chats, the session is scoped to the group, but the *speaker* is the sender.
|
|
706
813
|
// Using a group-scoped From causes the agent to treat different users as the same person.
|
|
@@ -764,8 +871,11 @@ export async function handleFeishuMessage(params: {
|
|
|
764
871
|
const inboundLabel = isGroup
|
|
765
872
|
? `Feishu[${account.accountId}] message in group ${ctx.chatId}`
|
|
766
873
|
: `Feishu[${account.accountId}] DM from ${ctx.senderOpenId}`;
|
|
874
|
+
const systemEventText = permissionErrorForAgent
|
|
875
|
+
? inboundLabel
|
|
876
|
+
: `${inboundLabel}: ${preview}`;
|
|
767
877
|
|
|
768
|
-
core.system.enqueueSystemEvent(
|
|
878
|
+
core.system.enqueueSystemEvent(systemEventText, {
|
|
769
879
|
sessionKey: route.sessionKey,
|
|
770
880
|
contextKey: `feishu:message:${ctx.chatId}:${ctx.messageId}`,
|
|
771
881
|
});
|
|
@@ -929,10 +1039,11 @@ export async function handleFeishuMessage(params: {
|
|
|
929
1039
|
Surface: "feishu" as const,
|
|
930
1040
|
MessageSid: ctx.messageId,
|
|
931
1041
|
Timestamp: Date.now(),
|
|
932
|
-
WasMentioned:
|
|
1042
|
+
WasMentioned: effectiveWasMentioned,
|
|
933
1043
|
CommandAuthorized: commandAuthorized,
|
|
934
1044
|
OriginatingChannel: "feishu" as const,
|
|
935
1045
|
OriginatingTo: feishuTo,
|
|
1046
|
+
ReplyToBody: quotedContent,
|
|
936
1047
|
...mediaPayload,
|
|
937
1048
|
});
|
|
938
1049
|
|
package/src/channel.ts
CHANGED
|
@@ -90,6 +90,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
90
90
|
groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
|
|
91
91
|
groupAllowFrom: { type: "array", items: { oneOf: [{ type: "string" }, { type: "number" }] } },
|
|
92
92
|
requireMention: { type: "boolean" },
|
|
93
|
+
groupCommandMentionBypass: { type: "string", enum: ["never", "single_bot", "always"] },
|
|
93
94
|
topicSessionMode: { type: "string", enum: ["disabled", "enabled"] },
|
|
94
95
|
historyLimit: { type: "integer", minimum: 0 },
|
|
95
96
|
dmHistoryLimit: { type: "integer", minimum: 0 },
|
|
@@ -110,6 +111,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
110
111
|
verificationToken: { type: "string" },
|
|
111
112
|
domain: { type: "string", enum: ["feishu", "lark"] },
|
|
112
113
|
connectionMode: { type: "string", enum: ["websocket", "webhook"] },
|
|
114
|
+
groupCommandMentionBypass: { type: "string", enum: ["never", "single_bot", "always"] },
|
|
113
115
|
},
|
|
114
116
|
},
|
|
115
117
|
},
|
package/src/config-schema.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { z };
|
|
|
3
3
|
|
|
4
4
|
const DmPolicySchema = z.enum(["open", "pairing", "allowlist"]);
|
|
5
5
|
const GroupPolicySchema = z.enum(["open", "allowlist", "disabled"]);
|
|
6
|
+
const GroupCommandMentionBypassSchema = z.enum(["never", "single_bot", "always"]).optional();
|
|
6
7
|
const FeishuDomainSchema = z.union([
|
|
7
8
|
z.enum(["feishu", "lark"]),
|
|
8
9
|
z.string().url().startsWith("https://"),
|
|
@@ -104,6 +105,7 @@ const TopicSessionModeSchema = z.enum(["disabled", "enabled"]).optional();
|
|
|
104
105
|
export const FeishuGroupSchema = z
|
|
105
106
|
.object({
|
|
106
107
|
requireMention: z.boolean().optional(),
|
|
108
|
+
groupCommandMentionBypass: GroupCommandMentionBypassSchema,
|
|
107
109
|
tools: ToolPolicySchema,
|
|
108
110
|
skills: z.array(z.string()).optional(),
|
|
109
111
|
enabled: z.boolean().optional(),
|
|
@@ -137,6 +139,7 @@ export const FeishuAccountConfigSchema = z
|
|
|
137
139
|
groupPolicy: GroupPolicySchema.optional(),
|
|
138
140
|
groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
139
141
|
requireMention: z.boolean().optional(),
|
|
142
|
+
groupCommandMentionBypass: GroupCommandMentionBypassSchema,
|
|
140
143
|
groups: z.record(z.string(), FeishuGroupSchema.optional()).optional(),
|
|
141
144
|
historyLimit: z.number().int().min(0).optional(),
|
|
142
145
|
dmHistoryLimit: z.number().int().min(0).optional(),
|
|
@@ -172,6 +175,7 @@ export const FeishuConfigSchema = z
|
|
|
172
175
|
groupPolicy: GroupPolicySchema.optional().default("allowlist"),
|
|
173
176
|
groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
174
177
|
requireMention: z.boolean().optional().default(true),
|
|
178
|
+
groupCommandMentionBypass: GroupCommandMentionBypassSchema.default("single_bot"),
|
|
175
179
|
groups: z.record(z.string(), FeishuGroupSchema.optional()).optional(),
|
|
176
180
|
topicSessionMode: TopicSessionModeSchema,
|
|
177
181
|
historyLimit: z.number().int().min(0).optional(),
|