@m1heng-clawd/feishu 0.1.1 → 0.1.2

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.
Files changed (3) hide show
  1. package/README.md +229 -9
  2. package/package.json +1 -1
  3. package/src/bot.ts +72 -7
package/README.md CHANGED
@@ -2,7 +2,13 @@
2
2
 
3
3
  Feishu/Lark (飞书) channel plugin for [Clawdbot](https://github.com/clawdbot/clawdbot).
4
4
 
5
- ## Installation
5
+ [English](#english) | [中文](#中文)
6
+
7
+ ---
8
+
9
+ ## English
10
+
11
+ ### Installation
6
12
 
7
13
  ```bash
8
14
  clawdbot plugins install @m1heng-clawd/feishu
@@ -14,25 +20,26 @@ Or install via npm:
14
20
  npm install @m1heng-clawd/feishu
15
21
  ```
16
22
 
17
- ## Configuration
23
+ ### Configuration
18
24
 
19
25
  1. Create a self-built app on [Feishu Open Platform](https://open.feishu.cn)
20
26
  2. Get your App ID and App Secret from the Credentials page
21
27
  3. Enable required permissions (see below)
22
- 4. Configure the plugin:
28
+ 4. **Configure event subscriptions** (see below) ⚠️ Important
29
+ 5. Configure the plugin:
23
30
 
24
- ### Required Permissions
31
+ #### Required Permissions
25
32
 
26
33
  | Permission | Scope | Description |
27
34
  |------------|-------|-------------|
28
- | `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) |
29
36
  | `im:message` | Messaging | Send and receive messages |
30
37
  | `im:message.p2p_msg:readonly` | DM | Read direct messages to bot |
31
38
  | `im:message.group_at_msg:readonly` | Group | Receive @mention messages in groups |
32
39
  | `im:message:send_as_bot` | Send | Send messages as the bot |
33
40
  | `im:resource` | Media | Upload and download images/files |
34
41
 
35
- ### Optional Permissions (for full functionality)
42
+ #### Optional Permissions
36
43
 
37
44
  | Permission | Scope | Description |
38
45
  |------------|-------|-------------|
@@ -42,13 +49,31 @@ npm install @m1heng-clawd/feishu
42
49
  | `im:message:recall` | Recall | Recall sent messages |
43
50
  | `im:message.reactions:read` | Reactions | View message reactions |
44
51
 
52
+ #### Event Subscriptions ⚠️
53
+
54
+ > **This is the most commonly missed configuration!** If the bot can send messages but cannot receive them, check this section.
55
+
56
+ In the Feishu Open Platform console, go to **Events & Callbacks**:
57
+
58
+ 1. **Event configuration**: Select **Long connection** (recommended)
59
+ 2. **Add event subscriptions**:
60
+
61
+ | Event | Description |
62
+ |-------|-------------|
63
+ | `im.message.receive_v1` | Receive messages (required) |
64
+ | `im.message.message_read_v1` | Message read receipts |
65
+ | `im.chat.member.bot.added_v1` | Bot added to group |
66
+ | `im.chat.member.bot.deleted_v1` | Bot removed from group |
67
+
68
+ 3. Ensure the event permissions are approved
69
+
45
70
  ```bash
46
71
  clawdbot config set channels.feishu.appId "cli_xxxxx"
47
72
  clawdbot config set channels.feishu.appSecret "your_app_secret"
48
73
  clawdbot config set channels.feishu.enabled true
49
74
  ```
50
75
 
51
- ## Configuration Options
76
+ ### Configuration Options
52
77
 
53
78
  ```yaml
54
79
  channels:
@@ -72,7 +97,7 @@ channels:
72
97
  renderMode: "auto"
73
98
  ```
74
99
 
75
- ### Render Mode
100
+ #### Render Mode
76
101
 
77
102
  | Mode | Description |
78
103
  |------|-------------|
@@ -80,7 +105,7 @@ channels:
80
105
  | `raw` | Always send replies as plain text. Markdown tables are converted to ASCII. |
81
106
  | `card` | Always send replies as interactive cards with full markdown rendering (syntax highlighting, tables, clickable links). |
82
107
 
83
- ## Features
108
+ ### Features
84
109
 
85
110
  - WebSocket and Webhook connection modes
86
111
  - Direct messages and group chats
@@ -92,6 +117,201 @@ channels:
92
117
  - User and group directory lookup
93
118
  - **Card render mode**: Optional markdown rendering with syntax highlighting
94
119
 
120
+ ### FAQ
121
+
122
+ #### Bot cannot receive messages
123
+
124
+ Check the following:
125
+ 1. Have you configured **event subscriptions**? (See Event Subscriptions section)
126
+ 2. Is the event configuration set to **long connection**?
127
+ 3. Did you add the `im.message.receive_v1` event?
128
+ 4. Are the permissions approved?
129
+
130
+ #### 403 error when sending messages
131
+
132
+ Ensure `im:message:send_as_bot` permission is approved.
133
+
134
+ #### How to clear history / start new conversation
135
+
136
+ Send `/new` command in the chat.
137
+
138
+ #### Why is the output not streaming
139
+
140
+ Feishu API has rate limits. Streaming updates can easily trigger throttling. We use complete-then-send approach for stability.
141
+
142
+ #### Windows install error `spawn npm ENOENT`
143
+
144
+ If `clawdbot plugins install` fails, install manually:
145
+
146
+ ```bash
147
+ # 1. Download the package
148
+ curl -O https://registry.npmjs.org/@m1heng-clawd/feishu/-/feishu-0.1.1.tgz
149
+
150
+ # 2. Install from local file
151
+ clawdbot plugins install ./feishu-0.1.1.tgz
152
+ ```
153
+
154
+ #### Cannot find the bot in Feishu
155
+
156
+ 1. Ensure the app is published (at least to test version)
157
+ 2. Search for the bot name in Feishu search box
158
+ 3. Check if your account is in the app's availability scope
159
+
160
+ ---
161
+
162
+ ## 中文
163
+
164
+ ### 安装
165
+
166
+ ```bash
167
+ clawdbot plugins install @m1heng-clawd/feishu
168
+ ```
169
+
170
+ 或通过 npm 安装:
171
+
172
+ ```bash
173
+ npm install @m1heng-clawd/feishu
174
+ ```
175
+
176
+ ### 配置
177
+
178
+ 1. 在 [飞书开放平台](https://open.feishu.cn) 创建自建应用
179
+ 2. 在凭证页面获取 App ID 和 App Secret
180
+ 3. 开启所需权限(见下方)
181
+ 4. **配置事件订阅**(见下方)⚠️ 重要
182
+ 5. 配置插件:
183
+
184
+ #### 必需权限
185
+
186
+ | 权限 | 范围 | 说明 |
187
+ |------|------|------|
188
+ | `contact:user.base:readonly` | 用户信息 | 获取用户基本信息(用于解析发送者姓名,避免群聊/私聊把不同人当成同一说话者) |
189
+ | `im:message` | 消息 | 发送和接收消息 |
190
+ | `im:message.p2p_msg:readonly` | 私聊 | 读取发给机器人的私聊消息 |
191
+ | `im:message.group_at_msg:readonly` | 群聊 | 接收群内 @机器人 的消息 |
192
+ | `im:message:send_as_bot` | 发送 | 以机器人身份发送消息 |
193
+ | `im:resource` | 媒体 | 上传和下载图片/文件 |
194
+
195
+ #### 可选权限
196
+
197
+ | 权限 | 范围 | 说明 |
198
+ |------|------|------|
199
+ | `im:message.group_msg` | 群聊 | 读取所有群消息(敏感) |
200
+ | `im:message:readonly` | 读取 | 获取历史消息 |
201
+ | `im:message:update` | 编辑 | 更新/编辑已发送消息 |
202
+ | `im:message:recall` | 撤回 | 撤回已发送消息 |
203
+ | `im:message.reactions:read` | 表情 | 查看消息表情回复 |
204
+
205
+ #### 事件订阅 ⚠️
206
+
207
+ > **这是最容易遗漏的配置!** 如果机器人能发消息但收不到消息,请检查此项。
208
+
209
+ 在飞书开放平台的应用后台,进入 **事件与回调** 页面:
210
+
211
+ 1. **事件配置方式**:选择 **使用长连接接收事件**(推荐)
212
+ 2. **添加事件订阅**,勾选以下事件:
213
+
214
+ | 事件 | 说明 |
215
+ |------|------|
216
+ | `im.message.receive_v1` | 接收消息(必需) |
217
+ | `im.message.message_read_v1` | 消息已读回执 |
218
+ | `im.chat.member.bot.added_v1` | 机器人进群 |
219
+ | `im.chat.member.bot.deleted_v1` | 机器人被移出群 |
220
+
221
+ 3. 确保事件订阅的权限已申请并通过审核
222
+
223
+ ```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
227
+ ```
228
+
229
+ ### 配置选项
230
+
231
+ ```yaml
232
+ channels:
233
+ feishu:
234
+ enabled: true
235
+ appId: "cli_xxxxx"
236
+ appSecret: "secret"
237
+ # 域名: "feishu" (国内) 或 "lark" (国际)
238
+ domain: "feishu"
239
+ # 连接模式: "websocket" (推荐) 或 "webhook"
240
+ connectionMode: "websocket"
241
+ # 私聊策略: "pairing" | "open" | "allowlist"
242
+ dmPolicy: "pairing"
243
+ # 群聊策略: "open" | "allowlist" | "disabled"
244
+ groupPolicy: "allowlist"
245
+ # 群聊是否需要 @机器人
246
+ requireMention: true
247
+ # 媒体文件最大大小 (MB, 默认 30)
248
+ mediaMaxMb: 30
249
+ # 回复渲染模式: "auto" | "raw" | "card"
250
+ renderMode: "auto"
251
+ ```
252
+
253
+ #### 渲染模式
254
+
255
+ | 模式 | 说明 |
256
+ |------|------|
257
+ | `auto` | (默认)自动检测:有代码块或表格时用卡片,否则纯文本 |
258
+ | `raw` | 始终纯文本,表格转为 ASCII |
259
+ | `card` | 始终使用卡片,支持语法高亮、表格、链接等 |
260
+
261
+ ### 功能
262
+
263
+ - WebSocket 和 Webhook 连接模式
264
+ - 私聊和群聊
265
+ - 消息回复和引用上下文
266
+ - **入站媒体支持**:AI 可以看到图片、读取文件(PDF、Excel 等)、处理富文本中的嵌入图片
267
+ - 图片和文件上传(出站)
268
+ - 输入指示器(通过表情回复实现)
269
+ - 私聊配对审批流程
270
+ - 用户和群组目录查询
271
+ - **卡片渲染模式**:支持语法高亮的 Markdown 渲染
272
+
273
+ ### 常见问题
274
+
275
+ #### 机器人收不到消息
276
+
277
+ 检查以下配置:
278
+ 1. 是否配置了 **事件订阅**?(见上方事件订阅章节)
279
+ 2. 事件配置方式是否选择了 **长连接**?
280
+ 3. 是否添加了 `im.message.receive_v1` 事件?
281
+ 4. 相关权限是否已申请并审核通过?
282
+
283
+ #### 返回消息时 403 错误
284
+
285
+ 确保已申请 `im:message:send_as_bot` 权限,并且权限已审核通过。
286
+
287
+ #### 如何清理历史会话 / 开启新对话
288
+
289
+ 在聊天中发送 `/new` 命令即可开启新对话。
290
+
291
+ #### 消息为什么不是流式输出
292
+
293
+ 飞书 API 有请求频率限制,流式更新消息很容易触发限流。当前采用完整回复后一次性发送的方式,以保证稳定性。
294
+
295
+ #### Windows 安装报错 `spawn npm ENOENT`
296
+
297
+ 如果 `clawdbot plugins install` 失败,可以手动安装:
298
+
299
+ ```bash
300
+ # 1. 下载插件包
301
+ curl -O https://registry.npmjs.org/@m1heng-clawd/feishu/-/feishu-0.1.1.tgz
302
+
303
+ # 2. 从本地安装
304
+ clawdbot plugins install ./feishu-0.1.1.tgz
305
+ ```
306
+
307
+ #### 在飞书里找不到机器人
308
+
309
+ 1. 确保应用已发布(至少发布到测试版本)
310
+ 2. 在飞书搜索框中搜索机器人名称
311
+ 3. 检查应用可用范围是否包含你的账号
312
+
313
+ ---
314
+
95
315
  ## License
96
316
 
97
317
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m1heng-clawd/feishu",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "Clawdbot Feishu/Lark channel plugin",
6
6
  "license": "MIT",
package/src/bot.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  } from "clawdbot/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,
@@ -18,6 +19,52 @@ import { createFeishuReplyDispatcher } from "./reply-dispatcher.js";
18
19
  import { getMessageFeishu } from "./send.js";
19
20
  import { downloadImageFeishu, downloadMessageResourceFeishu } from "./media.js";
20
21
 
22
+ // --- Sender name resolution (so the agent can distinguish who is speaking in group chats) ---
23
+ // Cache display names by open_id to avoid an API call on every message.
24
+ const SENDER_NAME_TTL_MS = 10 * 60 * 1000;
25
+ const senderNameCache = new Map<string, { name: string; expireAt: number }>();
26
+
27
+ async function resolveFeishuSenderName(params: {
28
+ feishuCfg?: FeishuConfig;
29
+ senderOpenId: string;
30
+ log: (...args: any[]) => void;
31
+ }): Promise<string | undefined> {
32
+ const { feishuCfg, senderOpenId, log } = params;
33
+ if (!feishuCfg) return undefined;
34
+ if (!senderOpenId) return undefined;
35
+
36
+ const cached = senderNameCache.get(senderOpenId);
37
+ const now = Date.now();
38
+ if (cached && cached.expireAt > now) return cached.name;
39
+
40
+ try {
41
+ const client = createFeishuClient(feishuCfg);
42
+
43
+ // contact/v3/users/:user_id?user_id_type=open_id
44
+ const res: any = await client.contact.user.get({
45
+ path: { user_id: senderOpenId },
46
+ params: { user_id_type: "open_id" },
47
+ });
48
+
49
+ const name: string | undefined =
50
+ res?.data?.user?.name ||
51
+ res?.data?.user?.display_name ||
52
+ res?.data?.user?.nickname ||
53
+ res?.data?.user?.en_name;
54
+
55
+ if (name && typeof name === "string") {
56
+ senderNameCache.set(senderOpenId, { name, expireAt: now + SENDER_NAME_TTL_MS });
57
+ return name;
58
+ }
59
+
60
+ return undefined;
61
+ } catch (err) {
62
+ // Best-effort. Don't fail message handling if name lookup fails.
63
+ log(`feishu: failed to resolve sender name for ${senderOpenId}: ${String(err)}`);
64
+ return undefined;
65
+ }
66
+ }
67
+
21
68
  export type FeishuMessageEvent = {
22
69
  sender: {
23
70
  sender_id: {
@@ -378,9 +425,17 @@ export async function handleFeishuMessage(params: {
378
425
  const log = runtime?.log ?? console.log;
379
426
  const error = runtime?.error ?? console.error;
380
427
 
381
- const ctx = parseFeishuMessageEvent(event, botOpenId);
428
+ let ctx = parseFeishuMessageEvent(event, botOpenId);
382
429
  const isGroup = ctx.chatType === "group";
383
430
 
431
+ // Resolve sender display name (best-effort) so the agent can attribute messages correctly.
432
+ const senderName = await resolveFeishuSenderName({
433
+ feishuCfg,
434
+ senderOpenId: ctx.senderOpenId,
435
+ log,
436
+ });
437
+ if (senderName) ctx = { ...ctx, senderName };
438
+
384
439
  log(`feishu: received message from ${ctx.senderOpenId} in ${ctx.chatId} (${ctx.chatType})`);
385
440
 
386
441
  const historyLimit = Math.max(
@@ -421,7 +476,7 @@ export async function handleFeishuMessage(params: {
421
476
  limit: historyLimit,
422
477
  entry: {
423
478
  sender: ctx.senderOpenId,
424
- body: ctx.content,
479
+ body: `${ctx.senderName ?? ctx.senderOpenId}: ${ctx.content}`,
425
480
  timestamp: Date.now(),
426
481
  messageId: ctx.messageId,
427
482
  },
@@ -448,7 +503,9 @@ export async function handleFeishuMessage(params: {
448
503
  try {
449
504
  const core = getFeishuRuntime();
450
505
 
451
- const feishuFrom = isGroup ? `feishu:group:${ctx.chatId}` : `feishu:${ctx.senderOpenId}`;
506
+ // In group chats, the session is scoped to the group, but the *speaker* is the sender.
507
+ // Using a group-scoped From causes the agent to treat different users as the same person.
508
+ const feishuFrom = `feishu:${ctx.senderOpenId}`;
452
509
  const feishuTo = isGroup ? `chat:${ctx.chatId}` : `user:${ctx.senderOpenId}`;
453
510
 
454
511
  const route = core.channel.routing.resolveAgentRoute({
@@ -504,9 +561,16 @@ export async function handleFeishuMessage(params: {
504
561
  messageBody = `[Replying to: "${quotedContent}"]\n\n${ctx.content}`;
505
562
  }
506
563
 
564
+ // Include a readable speaker label so the model can attribute instructions.
565
+ // (DMs already have per-sender sessions, but the prefix is still useful for clarity.)
566
+ const speaker = ctx.senderName ?? ctx.senderOpenId;
567
+ messageBody = `${speaker}: ${messageBody}`;
568
+
569
+ const envelopeFrom = isGroup ? `${ctx.chatId}:${ctx.senderOpenId}` : ctx.senderOpenId;
570
+
507
571
  const body = core.channel.reply.formatAgentEnvelope({
508
572
  channel: "Feishu",
509
- from: isGroup ? ctx.chatId : ctx.senderOpenId,
573
+ from: envelopeFrom,
510
574
  timestamp: new Date(),
511
575
  envelope: envelopeOptions,
512
576
  body: messageBody,
@@ -524,9 +588,10 @@ export async function handleFeishuMessage(params: {
524
588
  formatEntry: (entry) =>
525
589
  core.channel.reply.formatAgentEnvelope({
526
590
  channel: "Feishu",
527
- from: ctx.chatId,
591
+ // Preserve speaker identity in group history as well.
592
+ from: `${ctx.chatId}:${entry.sender}`,
528
593
  timestamp: entry.timestamp,
529
- body: `${entry.sender}: ${entry.body}`,
594
+ body: entry.body,
530
595
  envelope: envelopeOptions,
531
596
  }),
532
597
  });
@@ -542,7 +607,7 @@ export async function handleFeishuMessage(params: {
542
607
  AccountId: route.accountId,
543
608
  ChatType: isGroup ? "group" : "direct",
544
609
  GroupSubject: isGroup ? ctx.chatId : undefined,
545
- SenderName: ctx.senderOpenId,
610
+ SenderName: ctx.senderName ?? ctx.senderOpenId,
546
611
  SenderId: ctx.senderOpenId,
547
612
  Provider: "feishu" as const,
548
613
  Surface: "feishu" as const,