@max1874/feishu 0.1.5 → 0.1.7

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # clawd-feishu
2
2
 
3
- Feishu/Lark (飞书) channel plugin for [Clawdbot](https://github.com/clawdbot/clawdbot).
3
+ Feishu/Lark (飞书) channel plugin for [OpenClaw](https://github.com/openclaw/openclaw).
4
4
 
5
5
  [English](#english) | [中文](#中文)
6
6
 
@@ -11,7 +11,7 @@ Feishu/Lark (飞书) channel plugin for [Clawdbot](https://github.com/clawdbot/c
11
11
  ### Installation
12
12
 
13
13
  ```bash
14
- clawdbot plugins install @m1heng-clawd/feishu
14
+ openclaw plugins install @m1heng-clawd/feishu
15
15
  ```
16
16
 
17
17
  Or install via npm:
@@ -32,7 +32,7 @@ npm install @m1heng-clawd/feishu
32
32
 
33
33
  | Permission | Scope | Description |
34
34
  |------------|-------|-------------|
35
- | `contact:user.base:readonly` | User info | Get basic user information |
35
+ | `contact:user.base:readonly` | User info | Get basic user info (required to resolve sender display names for speaker attribution) |
36
36
  | `im:message` | Messaging | Send and receive messages |
37
37
  | `im:message.p2p_msg:readonly` | DM | Read direct messages to bot |
38
38
  | `im:message.group_at_msg:readonly` | Group | Receive @mention messages in groups |
@@ -49,6 +49,18 @@ npm install @m1heng-clawd/feishu
49
49
  | `im:message:recall` | Recall | Recall sent messages |
50
50
  | `im:message.reactions:read` | Reactions | View message reactions |
51
51
 
52
+ #### Document Tools Permissions
53
+
54
+ Required if using Feishu document tools (`feishu_doc_*`):
55
+
56
+ | Permission | Description |
57
+ |------------|-------------|
58
+ | `docx:document` | Create/edit documents |
59
+ | `docx:document:readonly` | Read documents |
60
+ | `docx:document.block:convert` | Markdown to blocks conversion (required for write/append) |
61
+ | `drive:drive` | Upload images to documents |
62
+ | `drive:drive:readonly` | List folders |
63
+
52
64
  #### Event Subscriptions ⚠️
53
65
 
54
66
  > **This is the most commonly missed configuration!** If the bot can send messages but cannot receive them, check this section.
@@ -68,9 +80,9 @@ In the Feishu Open Platform console, go to **Events & Callbacks**:
68
80
  3. Ensure the event permissions are approved
69
81
 
70
82
  ```bash
71
- clawdbot config set channels.feishu.appId "cli_xxxxx"
72
- clawdbot config set channels.feishu.appSecret "your_app_secret"
73
- clawdbot config set channels.feishu.enabled true
83
+ openclaw config set channels.feishu.appId "cli_xxxxx"
84
+ openclaw config set channels.feishu.appSecret "your_app_secret"
85
+ openclaw config set channels.feishu.enabled true
74
86
  ```
75
87
 
76
88
  ### Configuration Options
@@ -116,6 +128,17 @@ channels:
116
128
  - Pairing flow for DM approval
117
129
  - User and group directory lookup
118
130
  - **Card render mode**: Optional markdown rendering with syntax highlighting
131
+ - **Document tools**: Read, create, and write Feishu documents with markdown (tables not supported due to API limitations)
132
+ - **@mention forwarding**: When you @mention someone in your message, the bot's reply will automatically @mention them too
133
+
134
+ #### @Mention Forwarding
135
+
136
+ When you want the bot to @mention someone in its reply, simply @mention them in your message:
137
+
138
+ - **In DM**: `@张三 say hello` → Bot replies with `@张三 Hello!`
139
+ - **In Group**: `@bot @张三 say hello` → Bot replies with `@张三 Hello!`
140
+
141
+ The bot automatically detects @mentions in your message and includes them in its reply. No extra permissions required beyond the standard messaging permissions.
119
142
 
120
143
  ### FAQ
121
144
 
@@ -141,14 +164,14 @@ Feishu API has rate limits. Streaming updates can easily trigger throttling. We
141
164
 
142
165
  #### Windows install error `spawn npm ENOENT`
143
166
 
144
- If `clawdbot plugins install` fails, install manually:
167
+ If `openclaw plugins install` fails, install manually:
145
168
 
146
169
  ```bash
147
170
  # 1. Download the package
148
- curl -O https://registry.npmjs.org/@m1heng-clawd/feishu/-/feishu-0.1.1.tgz
171
+ curl -O https://registry.npmjs.org/@m1heng-clawd/feishu/-/feishu-0.1.3.tgz
149
172
 
150
173
  # 2. Install from local file
151
- clawdbot plugins install ./feishu-0.1.1.tgz
174
+ openclaw plugins install ./feishu-0.1.3.tgz
152
175
  ```
153
176
 
154
177
  #### Cannot find the bot in Feishu
@@ -164,7 +187,7 @@ clawdbot plugins install ./feishu-0.1.1.tgz
164
187
  ### 安装
165
188
 
166
189
  ```bash
167
- clawdbot plugins install @m1heng-clawd/feishu
190
+ openclaw plugins install @m1heng-clawd/feishu
168
191
  ```
169
192
 
170
193
  或通过 npm 安装:
@@ -185,7 +208,7 @@ npm install @m1heng-clawd/feishu
185
208
 
186
209
  | 权限 | 范围 | 说明 |
187
210
  |------|------|------|
188
- | `contact:user.base:readonly` | 用户信息 | 获取用户基本信息 |
211
+ | `contact:user.base:readonly` | 用户信息 | 获取用户基本信息(用于解析发送者姓名,避免群聊/私聊把不同人当成同一说话者) |
189
212
  | `im:message` | 消息 | 发送和接收消息 |
190
213
  | `im:message.p2p_msg:readonly` | 私聊 | 读取发给机器人的私聊消息 |
191
214
  | `im:message.group_at_msg:readonly` | 群聊 | 接收群内 @机器人 的消息 |
@@ -202,6 +225,18 @@ npm install @m1heng-clawd/feishu
202
225
  | `im:message:recall` | 撤回 | 撤回已发送消息 |
203
226
  | `im:message.reactions:read` | 表情 | 查看消息表情回复 |
204
227
 
228
+ #### 文档工具权限
229
+
230
+ 使用飞书文档工具(`feishu_doc_*`)需要以下权限:
231
+
232
+ | 权限 | 说明 |
233
+ |------|------|
234
+ | `docx:document` | 创建/编辑文档 |
235
+ | `docx:document:readonly` | 读取文档 |
236
+ | `docx:document.block:convert` | Markdown 转 blocks(write/append 必需) |
237
+ | `drive:drive` | 上传图片到文档 |
238
+ | `drive:drive:readonly` | 列出文件夹 |
239
+
205
240
  #### 事件订阅 ⚠️
206
241
 
207
242
  > **这是最容易遗漏的配置!** 如果机器人能发消息但收不到消息,请检查此项。
@@ -221,9 +256,9 @@ npm install @m1heng-clawd/feishu
221
256
  3. 确保事件订阅的权限已申请并通过审核
222
257
 
223
258
  ```bash
224
- clawdbot config set channels.feishu.appId "cli_xxxxx"
225
- clawdbot config set channels.feishu.appSecret "your_app_secret"
226
- clawdbot config set channels.feishu.enabled true
259
+ openclaw config set channels.feishu.appId "cli_xxxxx"
260
+ openclaw config set channels.feishu.appSecret "your_app_secret"
261
+ openclaw config set channels.feishu.enabled true
227
262
  ```
228
263
 
229
264
  ### 配置选项
@@ -269,6 +304,17 @@ channels:
269
304
  - 私聊配对审批流程
270
305
  - 用户和群组目录查询
271
306
  - **卡片渲染模式**:支持语法高亮的 Markdown 渲染
307
+ - **文档工具**:读取、创建、用 Markdown 写入飞书文档(表格因 API 限制不支持)
308
+ - **@ 转发功能**:在消息中 @ 某人,机器人的回复会自动 @ 该用户
309
+
310
+ #### @ 转发功能
311
+
312
+ 如果你希望机器人的回复中 @ 某人,只需在你的消息中 @ 他们:
313
+
314
+ - **私聊**:`@张三 跟他问好` → 机器人回复 `@张三 你好!`
315
+ - **群聊**:`@机器人 @张三 跟他问好` → 机器人回复 `@张三 你好!`
316
+
317
+ 机器人会自动检测消息中的 @ 并在回复时带上。无需额外权限。
272
318
 
273
319
  ### 常见问题
274
320
 
@@ -294,14 +340,14 @@ channels:
294
340
 
295
341
  #### Windows 安装报错 `spawn npm ENOENT`
296
342
 
297
- 如果 `clawdbot plugins install` 失败,可以手动安装:
343
+ 如果 `openclaw plugins install` 失败,可以手动安装:
298
344
 
299
345
  ```bash
300
346
  # 1. 下载插件包
301
- curl -O https://registry.npmjs.org/@m1heng-clawd/feishu/-/feishu-0.1.1.tgz
347
+ curl -O https://registry.npmjs.org/@m1heng-clawd/feishu/-/feishu-0.1.3.tgz
302
348
 
303
349
  # 2. 从本地安装
304
- clawdbot plugins install ./feishu-0.1.1.tgz
350
+ openclaw plugins install ./feishu-0.1.3.tgz
305
351
  ```
306
352
 
307
353
  #### 在飞书里找不到机器人
package/index.ts CHANGED
@@ -1,7 +1,8 @@
1
- import type { ClawdbotPluginApi } from "openclaw/plugin-sdk";
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
2
2
  import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
3
3
  import { feishuPlugin } from "./src/channel.js";
4
4
  import { setFeishuRuntime } from "./src/runtime.js";
5
+ import { registerFeishuDocTools } from "./src/docx.js";
5
6
 
6
7
  export { monitorFeishuProvider } from "./src/monitor.js";
7
8
  export {
@@ -25,6 +26,18 @@ export {
25
26
  listReactionsFeishu,
26
27
  FeishuEmoji,
27
28
  } from "./src/reactions.js";
29
+ export {
30
+ extractMentionTargets,
31
+ extractMessageBody,
32
+ isMentionForwardRequest,
33
+ formatMentionForText,
34
+ formatMentionForCard,
35
+ formatMentionAllForText,
36
+ formatMentionAllForCard,
37
+ buildMentionedMessage,
38
+ buildMentionedCardContent,
39
+ type MentionTarget,
40
+ } from "./src/mention.js";
28
41
  export { feishuPlugin } from "./src/channel.js";
29
42
 
30
43
  const plugin = {
@@ -32,9 +45,10 @@ const plugin = {
32
45
  name: "Feishu",
33
46
  description: "Feishu/Lark channel plugin",
34
47
  configSchema: emptyPluginConfigSchema(),
35
- register(api: ClawdbotPluginApi) {
48
+ register(api: OpenClawPluginApi) {
36
49
  setFeishuRuntime(api.runtime);
37
50
  api.registerChannel({ plugin: feishuPlugin });
51
+ registerFeishuDocTools(api);
38
52
  },
39
53
  };
40
54
 
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "id": "feishu",
3
3
  "channels": ["feishu"],
4
+ "skills": ["./skills"],
4
5
  "configSchema": {
5
6
  "type": "object",
6
7
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@max1874/feishu",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
- "description": "Clawdbot Feishu/Lark channel plugin",
5
+ "description": "OpenClaw Feishu/Lark channel plugin",
6
6
  "license": "MIT",
7
7
  "files": [
8
8
  "index.ts",
@@ -11,10 +11,10 @@
11
11
  ],
12
12
  "repository": {
13
13
  "type": "git",
14
- "url": "git+https://github.com/m1heng/clawdbot-feishu.git"
14
+ "url": "git+https://github.com/max1874/clawdbot-feishu.git"
15
15
  },
16
16
  "keywords": [
17
- "clawdbot",
17
+ "openclaw",
18
18
  "feishu",
19
19
  "lark",
20
20
  "飞书",
@@ -46,15 +46,16 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@larksuiteoapi/node-sdk": "^1.30.0",
49
+ "@sinclair/typebox": "^0.34.48",
49
50
  "zod": "^4.3.6"
50
51
  },
51
52
  "devDependencies": {
52
53
  "@types/node": "^25.0.10",
53
- "openclaw": "2026.1.24-2",
54
+ "openclaw": "2026.1.29",
54
55
  "tsx": "^4.21.0",
55
56
  "typescript": "^5.7.0"
56
57
  },
57
58
  "peerDependencies": {
58
- "openclaw": ">=2026.1.24"
59
+ "openclaw": ">=2026.1.29"
59
60
  }
60
61
  }
package/src/bot.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  } from "openclaw/plugin-sdk";
9
9
  import type { FeishuConfig, FeishuMessageContext, FeishuMediaInfo } from "./types.js";
10
10
  import { getFeishuRuntime } from "./runtime.js";
11
+ import { createFeishuClient } from "./client.js";
11
12
  import {
12
13
  resolveFeishuGroupConfig,
13
14
  resolveFeishuReplyPolicy,
@@ -15,8 +16,59 @@ import {
15
16
  isFeishuGroupAllowed,
16
17
  } from "./policy.js";
17
18
  import { createFeishuReplyDispatcher } from "./reply-dispatcher.js";
18
- import { getMessageFeishu } from "./send.js";
19
+ import { getMessageFeishu, getMergeForwardMessagesFeishu } from "./send.js";
19
20
  import { downloadImageFeishu, downloadMessageResourceFeishu } from "./media.js";
21
+ import {
22
+ extractMentionTargets,
23
+ extractMessageBody,
24
+ isMentionForwardRequest,
25
+ } from "./mention.js";
26
+
27
+ // --- Sender name resolution (so the agent can distinguish who is speaking in group chats) ---
28
+ // Cache display names by open_id to avoid an API call on every message.
29
+ const SENDER_NAME_TTL_MS = 10 * 60 * 1000;
30
+ const senderNameCache = new Map<string, { name: string; expireAt: number }>();
31
+
32
+ async function resolveFeishuSenderName(params: {
33
+ feishuCfg?: FeishuConfig;
34
+ senderOpenId: string;
35
+ log: (...args: any[]) => void;
36
+ }): Promise<string | undefined> {
37
+ const { feishuCfg, senderOpenId, log } = params;
38
+ if (!feishuCfg) return undefined;
39
+ if (!senderOpenId) return undefined;
40
+
41
+ const cached = senderNameCache.get(senderOpenId);
42
+ const now = Date.now();
43
+ if (cached && cached.expireAt > now) return cached.name;
44
+
45
+ try {
46
+ const client = createFeishuClient(feishuCfg);
47
+
48
+ // contact/v3/users/:user_id?user_id_type=open_id
49
+ const res: any = await client.contact.user.get({
50
+ path: { user_id: senderOpenId },
51
+ params: { user_id_type: "open_id" },
52
+ });
53
+
54
+ const name: string | undefined =
55
+ res?.data?.user?.name ||
56
+ res?.data?.user?.display_name ||
57
+ res?.data?.user?.nickname ||
58
+ res?.data?.user?.en_name;
59
+
60
+ if (name && typeof name === "string") {
61
+ senderNameCache.set(senderOpenId, { name, expireAt: now + SENDER_NAME_TTL_MS });
62
+ return name;
63
+ }
64
+
65
+ return undefined;
66
+ } catch (err) {
67
+ // Best-effort. Don't fail message handling if name lookup fails.
68
+ log(`feishu: failed to resolve sender name for ${senderOpenId}: ${String(err)}`);
69
+ return undefined;
70
+ }
71
+ }
20
72
 
21
73
  export type FeishuMessageEvent = {
22
74
  sender: {
@@ -171,6 +223,40 @@ function parsePostContent(content: string): {
171
223
  }
172
224
  }
173
225
 
226
+ /**
227
+ * Format merge_forward child messages into readable text.
228
+ */
229
+ function formatMergeForwardContent(
230
+ messages: Array<{ content: string; contentType: string }>,
231
+ ): string {
232
+ if (messages.length === 0) {
233
+ return "[转发的聊天记录 - 无内容]";
234
+ }
235
+
236
+ const lines: string[] = ["[转发的聊天记录]", "---"];
237
+
238
+ for (const msg of messages) {
239
+ if (msg.contentType === "text" || msg.contentType === "post") {
240
+ lines.push(msg.content);
241
+ } else if (msg.contentType === "image") {
242
+ lines.push("[图片]");
243
+ } else if (msg.contentType === "file") {
244
+ lines.push("[文件]");
245
+ } else if (msg.contentType === "audio") {
246
+ lines.push("[语音]");
247
+ } else if (msg.contentType === "video") {
248
+ lines.push("[视频]");
249
+ } else if (msg.contentType === "sticker") {
250
+ lines.push("[表情]");
251
+ } else {
252
+ lines.push(`[${msg.contentType}]`);
253
+ }
254
+ }
255
+
256
+ lines.push("---");
257
+ return lines.join("\n");
258
+ }
259
+
174
260
  /**
175
261
  * Infer placeholder text based on message type.
176
262
  */
@@ -352,7 +438,7 @@ export function parseFeishuMessageEvent(
352
438
  const mentionedBot = checkBotMentioned(event, botOpenId);
353
439
  const content = stripBotMention(rawContent, event.message.mentions);
354
440
 
355
- return {
441
+ const ctx: FeishuMessageContext = {
356
442
  chatId: event.message.chat_id,
357
443
  messageId: event.message.message_id,
358
444
  senderId: event.sender.sender_id.user_id || event.sender.sender_id.open_id || "",
@@ -364,6 +450,19 @@ export function parseFeishuMessageEvent(
364
450
  content,
365
451
  contentType: event.message.message_type,
366
452
  };
453
+
454
+ // Detect mention forward request: message mentions bot + at least one other user
455
+ if (isMentionForwardRequest(event, botOpenId)) {
456
+ const mentionTargets = extractMentionTargets(event, botOpenId);
457
+ if (mentionTargets.length > 0) {
458
+ ctx.mentionTargets = mentionTargets;
459
+ // Extract message body (remove all @ placeholders)
460
+ const allMentionKeys = (event.message.mentions ?? []).map((m) => m.key);
461
+ ctx.mentionMessageBody = extractMessageBody(content, allMentionKeys);
462
+ }
463
+ }
464
+
465
+ return ctx;
367
466
  }
368
467
 
369
468
  export async function handleFeishuMessage(params: {
@@ -378,11 +477,42 @@ export async function handleFeishuMessage(params: {
378
477
  const log = runtime?.log ?? console.log;
379
478
  const error = runtime?.error ?? console.error;
380
479
 
381
- const ctx = parseFeishuMessageEvent(event, botOpenId);
480
+ let ctx = parseFeishuMessageEvent(event, botOpenId);
382
481
  const isGroup = ctx.chatType === "group";
383
482
 
483
+ // Handle merge_forward messages: fetch child messages and format as readable text
484
+ if (ctx.contentType === "merge_forward") {
485
+ try {
486
+ log(`feishu: detected merge_forward message, fetching child messages...`);
487
+ const childMessages = await getMergeForwardMessagesFeishu({
488
+ cfg,
489
+ messageId: ctx.messageId,
490
+ });
491
+ const formattedContent = formatMergeForwardContent(childMessages);
492
+ ctx = { ...ctx, content: formattedContent };
493
+ log(`feishu: resolved merge_forward with ${childMessages.length} child message(s)`);
494
+ } catch (err) {
495
+ log(`feishu: failed to fetch merge_forward content: ${String(err)}`);
496
+ ctx = { ...ctx, content: "[转发的聊天记录 - 获取失败]" };
497
+ }
498
+ }
499
+
500
+ // Resolve sender display name (best-effort) so the agent can attribute messages correctly.
501
+ const senderName = await resolveFeishuSenderName({
502
+ feishuCfg,
503
+ senderOpenId: ctx.senderOpenId,
504
+ log,
505
+ });
506
+ if (senderName) ctx = { ...ctx, senderName };
507
+
384
508
  log(`feishu: received message from ${ctx.senderOpenId} in ${ctx.chatId} (${ctx.chatType})`);
385
509
 
510
+ // Log mention targets if detected
511
+ if (ctx.mentionTargets && ctx.mentionTargets.length > 0) {
512
+ const names = ctx.mentionTargets.map((t) => t.name).join(", ");
513
+ log(`feishu: detected @ forward request, targets: [${names}]`);
514
+ }
515
+
386
516
  const historyLimit = Math.max(
387
517
  0,
388
518
  feishuCfg?.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT,
@@ -421,7 +551,7 @@ export async function handleFeishuMessage(params: {
421
551
  limit: historyLimit,
422
552
  entry: {
423
553
  sender: ctx.senderOpenId,
424
- body: ctx.content,
554
+ body: `${ctx.senderName ?? ctx.senderOpenId}: ${ctx.content}`,
425
555
  timestamp: Date.now(),
426
556
  messageId: ctx.messageId,
427
557
  },
@@ -448,7 +578,9 @@ export async function handleFeishuMessage(params: {
448
578
  try {
449
579
  const core = getFeishuRuntime();
450
580
 
451
- const feishuFrom = isGroup ? `feishu:group:${ctx.chatId}` : `feishu:${ctx.senderOpenId}`;
581
+ // In group chats, the session is scoped to the group, but the *speaker* is the sender.
582
+ // Using a group-scoped From causes the agent to treat different users as the same person.
583
+ const feishuFrom = `feishu:${ctx.senderOpenId}`;
452
584
  const feishuTo = isGroup ? `chat:${ctx.chatId}` : `user:${ctx.senderOpenId}`;
453
585
 
454
586
  const route = core.channel.routing.resolveAgentRoute({
@@ -504,9 +636,22 @@ export async function handleFeishuMessage(params: {
504
636
  messageBody = `[Replying to: "${quotedContent}"]\n\n${ctx.content}`;
505
637
  }
506
638
 
639
+ // Include a readable speaker label so the model can attribute instructions.
640
+ // (DMs already have per-sender sessions, but the prefix is still useful for clarity.)
641
+ const speaker = ctx.senderName ?? ctx.senderOpenId;
642
+ messageBody = `${speaker}: ${messageBody}`;
643
+
644
+ // If there are mention targets, inform the agent that replies will auto-mention them
645
+ if (ctx.mentionTargets && ctx.mentionTargets.length > 0) {
646
+ const targetNames = ctx.mentionTargets.map((t) => t.name).join(", ");
647
+ messageBody += `\n\n[System: Your reply will automatically @mention: ${targetNames}. Do not write @xxx yourself.]`;
648
+ }
649
+
650
+ const envelopeFrom = isGroup ? `${ctx.chatId}:${ctx.senderOpenId}` : ctx.senderOpenId;
651
+
507
652
  const body = core.channel.reply.formatAgentEnvelope({
508
653
  channel: "Feishu",
509
- from: isGroup ? ctx.chatId : ctx.senderOpenId,
654
+ from: envelopeFrom,
510
655
  timestamp: new Date(),
511
656
  envelope: envelopeOptions,
512
657
  body: messageBody,
@@ -524,9 +669,10 @@ export async function handleFeishuMessage(params: {
524
669
  formatEntry: (entry) =>
525
670
  core.channel.reply.formatAgentEnvelope({
526
671
  channel: "Feishu",
527
- from: ctx.chatId,
672
+ // Preserve speaker identity in group history as well.
673
+ from: `${ctx.chatId}:${entry.sender}`,
528
674
  timestamp: entry.timestamp,
529
- body: `${entry.sender}: ${entry.body}`,
675
+ body: entry.body,
530
676
  envelope: envelopeOptions,
531
677
  }),
532
678
  });
@@ -542,7 +688,7 @@ export async function handleFeishuMessage(params: {
542
688
  AccountId: route.accountId,
543
689
  ChatType: isGroup ? "group" : "direct",
544
690
  GroupSubject: isGroup ? ctx.chatId : undefined,
545
- SenderName: ctx.senderOpenId,
691
+ SenderName: ctx.senderName ?? ctx.senderOpenId,
546
692
  SenderId: ctx.senderOpenId,
547
693
  Provider: "feishu" as const,
548
694
  Surface: "feishu" as const,
@@ -561,6 +707,7 @@ export async function handleFeishuMessage(params: {
561
707
  runtime: runtime as RuntimeEnv,
562
708
  chatId: ctx.chatId,
563
709
  replyToMessageId: ctx.messageId,
710
+ mentionTargets: ctx.mentionTargets,
564
711
  });
565
712
 
566
713
  log(`feishu: dispatching to agent (session=${route.sessionKey})`);