@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 +63 -17
- package/index.ts +16 -2
- package/openclaw.plugin.json +1 -0
- package/package.json +7 -6
- package/src/bot.ts +156 -9
- package/src/docx.ts +711 -0
- package/src/mention.ts +121 -0
- package/src/reply-dispatcher.ts +11 -1
- package/src/send.ts +124 -4
- package/src/types.ts +5 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# clawd-feishu
|
|
2
2
|
|
|
3
|
-
Feishu/Lark (飞书) channel plugin for [
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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 `
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
如果 `
|
|
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.
|
|
347
|
+
curl -O https://registry.npmjs.org/@m1heng-clawd/feishu/-/feishu-0.1.3.tgz
|
|
302
348
|
|
|
303
349
|
# 2. 从本地安装
|
|
304
|
-
|
|
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 {
|
|
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:
|
|
48
|
+
register(api: OpenClawPluginApi) {
|
|
36
49
|
setFeishuRuntime(api.runtime);
|
|
37
50
|
api.registerChannel({ plugin: feishuPlugin });
|
|
51
|
+
registerFeishuDocTools(api);
|
|
38
52
|
},
|
|
39
53
|
};
|
|
40
54
|
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@max1874/feishu",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "
|
|
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/
|
|
14
|
+
"url": "git+https://github.com/max1874/clawdbot-feishu.git"
|
|
15
15
|
},
|
|
16
16
|
"keywords": [
|
|
17
|
-
"
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
672
|
+
// Preserve speaker identity in group history as well.
|
|
673
|
+
from: `${ctx.chatId}:${entry.sender}`,
|
|
528
674
|
timestamp: entry.timestamp,
|
|
529
|
-
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})`);
|