@superbenxxxh/feishu 1.0.0 → 2.0.1
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 +16 -14
- package/package.json +2 -2
- package/src/bot.ts +46 -71
- package/src/channel.ts +4 -4
- package/src/config-schema.ts +4 -4
- package/src/post.ts +129 -0
- package/src/reply-dispatcher.ts +27 -23
- package/src/send.ts +129 -89
package/README.md
CHANGED
|
@@ -93,17 +93,18 @@ channels:
|
|
|
93
93
|
requireMention: true
|
|
94
94
|
# Max media size in MB (default: 30)
|
|
95
95
|
mediaMaxMb: 30
|
|
96
|
-
# Render mode for bot replies: "auto" | "raw" | "card"
|
|
97
|
-
renderMode: "
|
|
96
|
+
# Render mode for bot replies: "post" | "auto" | "raw" | "card"
|
|
97
|
+
renderMode: "post"
|
|
98
98
|
```
|
|
99
99
|
|
|
100
100
|
#### Render Mode
|
|
101
101
|
|
|
102
|
-
| Mode | Description |
|
|
103
|
-
|------|-------------|
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
102
|
+
| Mode | Description |
|
|
103
|
+
|------|-------------|
|
|
104
|
+
| `post` | (Default) Send replies as rich text posts using `msg_type=post` + `tag=md`. |
|
|
105
|
+
| `auto` | Automatically detect: use card for messages with code blocks or tables, plain text otherwise. |
|
|
106
|
+
| `raw` | Always send replies as plain text. Markdown tables are converted to ASCII. |
|
|
107
|
+
| `card` | Always send replies as interactive cards with full markdown rendering (syntax highlighting, tables, clickable links). |
|
|
107
108
|
|
|
108
109
|
### Features
|
|
109
110
|
|
|
@@ -256,17 +257,18 @@ channels:
|
|
|
256
257
|
requireMention: true
|
|
257
258
|
# 媒体文件最大大小 (MB, 默认 30)
|
|
258
259
|
mediaMaxMb: 30
|
|
259
|
-
# 回复渲染模式: "auto" | "raw" | "card"
|
|
260
|
-
renderMode: "
|
|
260
|
+
# 回复渲染模式: "post" | "auto" | "raw" | "card"
|
|
261
|
+
renderMode: "post"
|
|
261
262
|
```
|
|
262
263
|
|
|
263
264
|
#### 渲染模式
|
|
264
265
|
|
|
265
|
-
| 模式 | 说明 |
|
|
266
|
-
|------|------|
|
|
267
|
-
| `
|
|
268
|
-
| `
|
|
269
|
-
| `
|
|
266
|
+
| 模式 | 说明 |
|
|
267
|
+
|------|------|
|
|
268
|
+
| `post` | (默认)使用富文本 post(`msg_type=post` + `tag=md`)。 |
|
|
269
|
+
| `auto` | 自动检测:有代码块或表格时用卡片,否则纯文本 |
|
|
270
|
+
| `raw` | 始终纯文本,表格转为 ASCII |
|
|
271
|
+
| `card` | 始终使用卡片,支持语法高亮、表格、链接等 |
|
|
270
272
|
|
|
271
273
|
### 功能
|
|
272
274
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@superbenxxxh/feishu",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw Feishu/Lark channel plugin",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"order": 70
|
|
40
40
|
},
|
|
41
41
|
"install": {
|
|
42
|
-
"npmSpec": "@
|
|
42
|
+
"npmSpec": "@superbenxxxh/feishu",
|
|
43
43
|
"localPath": ".",
|
|
44
44
|
"defaultChoice": "npm"
|
|
45
45
|
}
|
package/src/bot.ts
CHANGED
|
@@ -7,8 +7,8 @@ import {
|
|
|
7
7
|
type HistoryEntry,
|
|
8
8
|
} from "openclaw/plugin-sdk";
|
|
9
9
|
import type { FeishuConfig, FeishuMessageContext, FeishuMediaInfo } from "./types.js";
|
|
10
|
-
import { getFeishuRuntime } from "./runtime.js";
|
|
11
|
-
import { createFeishuClient } from "./client.js";
|
|
10
|
+
import { getFeishuRuntime } from "./runtime.js";
|
|
11
|
+
import { createFeishuClient } from "./client.js";
|
|
12
12
|
import {
|
|
13
13
|
resolveFeishuGroupConfig,
|
|
14
14
|
resolveFeishuReplyPolicy,
|
|
@@ -18,11 +18,12 @@ import {
|
|
|
18
18
|
import { createFeishuReplyDispatcher } from "./reply-dispatcher.js";
|
|
19
19
|
import { getMessageFeishu } from "./send.js";
|
|
20
20
|
import { downloadImageFeishu, downloadMessageResourceFeishu } from "./media.js";
|
|
21
|
-
import {
|
|
22
|
-
extractMentionTargets,
|
|
23
|
-
extractMessageBody,
|
|
24
|
-
isMentionForwardRequest,
|
|
25
|
-
} from "./mention.js";
|
|
21
|
+
import {
|
|
22
|
+
extractMentionTargets,
|
|
23
|
+
extractMessageBody,
|
|
24
|
+
isMentionForwardRequest,
|
|
25
|
+
} from "./mention.js";
|
|
26
|
+
import { parsePostContent } from "./post.js";
|
|
26
27
|
|
|
27
28
|
// --- Sender name resolution (so the agent can distinguish who is speaking in group chats) ---
|
|
28
29
|
// Cache display names by open_id to avoid an API call on every message.
|
|
@@ -183,49 +184,9 @@ function parseMediaKeys(
|
|
|
183
184
|
* Parse post (rich text) content and extract embedded image keys.
|
|
184
185
|
* Post structure: { title?: string, content: [[{ tag, text?, image_key?, ... }]] }
|
|
185
186
|
*/
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
} {
|
|
190
|
-
try {
|
|
191
|
-
const parsed = JSON.parse(content);
|
|
192
|
-
const title = parsed.title || "";
|
|
193
|
-
const contentBlocks = parsed.content || [];
|
|
194
|
-
let textContent = title ? `${title}\n\n` : "";
|
|
195
|
-
const imageKeys: string[] = [];
|
|
196
|
-
|
|
197
|
-
for (const paragraph of contentBlocks) {
|
|
198
|
-
if (Array.isArray(paragraph)) {
|
|
199
|
-
for (const element of paragraph) {
|
|
200
|
-
if (element.tag === "text") {
|
|
201
|
-
textContent += element.text || "";
|
|
202
|
-
} else if (element.tag === "a") {
|
|
203
|
-
// Link: show text or href
|
|
204
|
-
textContent += element.text || element.href || "";
|
|
205
|
-
} else if (element.tag === "at") {
|
|
206
|
-
// Mention: @username
|
|
207
|
-
textContent += `@${element.user_name || element.user_id || ""}`;
|
|
208
|
-
} else if (element.tag === "img" && element.image_key) {
|
|
209
|
-
// Embedded image
|
|
210
|
-
imageKeys.push(element.image_key);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
textContent += "\n";
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return {
|
|
218
|
-
textContent: textContent.trim() || "[富文本消息]",
|
|
219
|
-
imageKeys,
|
|
220
|
-
};
|
|
221
|
-
} catch {
|
|
222
|
-
return { textContent: "[富文本消息]", imageKeys: [] };
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Infer placeholder text based on message type.
|
|
228
|
-
*/
|
|
187
|
+
/**
|
|
188
|
+
* Infer placeholder text based on message type.
|
|
189
|
+
*/
|
|
229
190
|
function inferPlaceholder(messageType: string): string {
|
|
230
191
|
switch (messageType) {
|
|
231
192
|
case "image":
|
|
@@ -467,27 +428,41 @@ export async function handleFeishuMessage(params: {
|
|
|
467
428
|
feishuCfg?.historyLimit ?? cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT,
|
|
468
429
|
);
|
|
469
430
|
|
|
470
|
-
if (isGroup) {
|
|
471
|
-
const groupPolicy = feishuCfg?.groupPolicy ?? "open";
|
|
472
|
-
const groupAllowFrom = feishuCfg?.groupAllowFrom ?? [];
|
|
473
|
-
const groupConfig = resolveFeishuGroupConfig({ cfg: feishuCfg, groupId: ctx.chatId });
|
|
474
|
-
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
431
|
+
if (isGroup) {
|
|
432
|
+
const groupPolicy = feishuCfg?.groupPolicy ?? "open";
|
|
433
|
+
const groupAllowFrom = feishuCfg?.groupAllowFrom ?? [];
|
|
434
|
+
const groupConfig = resolveFeishuGroupConfig({ cfg: feishuCfg, groupId: ctx.chatId });
|
|
435
|
+
|
|
436
|
+
const groupAllowed = isFeishuGroupAllowed({
|
|
437
|
+
groupPolicy,
|
|
438
|
+
allowFrom: groupAllowFrom,
|
|
439
|
+
senderId: ctx.chatId,
|
|
440
|
+
senderName: undefined,
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
if (!groupAllowed) {
|
|
444
|
+
log(`feishu: group ${ctx.chatId} not in allowlist`);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const senderAllowFrom = groupConfig?.allowFrom ?? [];
|
|
449
|
+
if (senderAllowFrom.length > 0) {
|
|
450
|
+
const senderAllowed = isFeishuGroupAllowed({
|
|
451
|
+
groupPolicy: "allowlist",
|
|
452
|
+
allowFrom: senderAllowFrom,
|
|
453
|
+
senderId: ctx.senderOpenId,
|
|
454
|
+
senderName: ctx.senderName,
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
if (!senderAllowed) {
|
|
458
|
+
log(`feishu: sender ${ctx.senderOpenId} not in group ${ctx.chatId} allowlist`);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const { requireMention } = resolveFeishuReplyPolicy({
|
|
464
|
+
isDirectMessage: false,
|
|
465
|
+
globalConfig: feishuCfg,
|
|
491
466
|
groupConfig,
|
|
492
467
|
});
|
|
493
468
|
|
package/src/channel.ts
CHANGED
|
@@ -85,10 +85,10 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
|
85
85
|
textChunkLimit: { type: "integer", minimum: 1 },
|
|
86
86
|
chunkMode: { type: "string", enum: ["length", "newline"] },
|
|
87
87
|
mediaMaxMb: { type: "number", minimum: 0 },
|
|
88
|
-
renderMode: { type: "string", enum: ["auto", "raw", "card"] },
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
},
|
|
88
|
+
renderMode: { type: "string", enum: ["auto", "raw", "post", "card"] },
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
92
|
config: {
|
|
93
93
|
listAccountIds: () => [DEFAULT_ACCOUNT_ID],
|
|
94
94
|
resolveAccount: (cfg) => resolveFeishuAccount({ cfg }),
|
package/src/config-schema.ts
CHANGED
|
@@ -30,8 +30,8 @@ const MarkdownConfigSchema = z
|
|
|
30
30
|
.strict()
|
|
31
31
|
.optional();
|
|
32
32
|
|
|
33
|
-
// Message render mode:
|
|
34
|
-
const RenderModeSchema = z.enum(["auto", "raw", "card"]).optional();
|
|
33
|
+
// Message render mode: post (default) = rich text, auto = detect markdown, raw = plain text, card = interactive card
|
|
34
|
+
const RenderModeSchema = z.enum(["auto", "raw", "post", "card"]).optional();
|
|
35
35
|
|
|
36
36
|
const BlockStreamingCoalesceSchema = z
|
|
37
37
|
.object({
|
|
@@ -89,8 +89,8 @@ export const FeishuConfigSchema = z
|
|
|
89
89
|
blockStreamingCoalesce: BlockStreamingCoalesceSchema,
|
|
90
90
|
mediaMaxMb: z.number().positive().optional(),
|
|
91
91
|
heartbeat: ChannelHeartbeatVisibilitySchema,
|
|
92
|
-
renderMode: RenderModeSchema, //
|
|
93
|
-
})
|
|
92
|
+
renderMode: RenderModeSchema, // post = rich text (default), raw = plain text, card = interactive card
|
|
93
|
+
})
|
|
94
94
|
.strict()
|
|
95
95
|
.superRefine((value, ctx) => {
|
|
96
96
|
if (value.dmPolicy === "open") {
|
package/src/post.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { MentionTarget } from "./mention.js";
|
|
2
|
+
|
|
3
|
+
type PostElement = {
|
|
4
|
+
tag: "text" | "a" | "at" | "img" | "md";
|
|
5
|
+
text?: string;
|
|
6
|
+
href?: string;
|
|
7
|
+
user_id?: string;
|
|
8
|
+
user_name?: string;
|
|
9
|
+
image_key?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type PostLocalePayload = {
|
|
13
|
+
title?: string;
|
|
14
|
+
content?: PostElement[][];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type PostPayload = {
|
|
18
|
+
zh_cn?: PostLocalePayload;
|
|
19
|
+
en_us?: PostLocalePayload;
|
|
20
|
+
post?: {
|
|
21
|
+
zh_cn?: PostLocalePayload;
|
|
22
|
+
en_us?: PostLocalePayload;
|
|
23
|
+
};
|
|
24
|
+
title?: string;
|
|
25
|
+
content?: PostElement[][];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function resolvePostPayload(parsed: PostPayload): PostLocalePayload {
|
|
29
|
+
return (
|
|
30
|
+
parsed.zh_cn ||
|
|
31
|
+
parsed.en_us ||
|
|
32
|
+
parsed.post?.zh_cn ||
|
|
33
|
+
parsed.post?.en_us ||
|
|
34
|
+
{
|
|
35
|
+
title: parsed.title,
|
|
36
|
+
content: parsed.content,
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function buildPostElements(messageText: string, mentions?: MentionTarget[]): PostElement[][] {
|
|
42
|
+
const elements: PostElement[] = [];
|
|
43
|
+
|
|
44
|
+
if (mentions && mentions.length > 0) {
|
|
45
|
+
for (const mention of mentions) {
|
|
46
|
+
elements.push({
|
|
47
|
+
tag: "at",
|
|
48
|
+
user_id: mention.openId,
|
|
49
|
+
user_name: mention.name,
|
|
50
|
+
});
|
|
51
|
+
elements.push({ tag: "text", text: " " });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
elements.push({
|
|
56
|
+
tag: "md",
|
|
57
|
+
text: messageText,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return [elements];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function buildFeishuPostMessagePayload(params: {
|
|
64
|
+
messageText: string;
|
|
65
|
+
mentions?: MentionTarget[];
|
|
66
|
+
}): {
|
|
67
|
+
content: string;
|
|
68
|
+
msgType: "post";
|
|
69
|
+
} {
|
|
70
|
+
const { messageText, mentions } = params;
|
|
71
|
+
const contentBlocks = buildPostElements(messageText, mentions);
|
|
72
|
+
|
|
73
|
+
const payload = {
|
|
74
|
+
zh_cn: { content: contentBlocks },
|
|
75
|
+
en_us: { content: contentBlocks },
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
content: JSON.stringify(payload),
|
|
80
|
+
msgType: "post",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Parse post (rich text) content and extract embedded image keys.
|
|
86
|
+
* Post structure: { title?: string, content: [[{ tag, text?, image_key?, ... }]] }
|
|
87
|
+
*/
|
|
88
|
+
export function parsePostContent(content: string): {
|
|
89
|
+
textContent: string;
|
|
90
|
+
imageKeys: string[];
|
|
91
|
+
} {
|
|
92
|
+
try {
|
|
93
|
+
const parsed = JSON.parse(content) as PostPayload;
|
|
94
|
+
const resolved = resolvePostPayload(parsed);
|
|
95
|
+
const title = resolved.title || "";
|
|
96
|
+
const contentBlocks = resolved.content || [];
|
|
97
|
+
let textContent = title ? `${title}\n\n` : "";
|
|
98
|
+
const imageKeys: string[] = [];
|
|
99
|
+
|
|
100
|
+
for (const paragraph of contentBlocks) {
|
|
101
|
+
if (Array.isArray(paragraph)) {
|
|
102
|
+
for (const element of paragraph) {
|
|
103
|
+
if (element.tag === "text") {
|
|
104
|
+
textContent += element.text || "";
|
|
105
|
+
} else if (element.tag === "a") {
|
|
106
|
+
// Link: show text or href
|
|
107
|
+
textContent += element.text || element.href || "";
|
|
108
|
+
} else if (element.tag === "at") {
|
|
109
|
+
// Mention: @username
|
|
110
|
+
textContent += `@${element.user_name || element.user_id || ""}`;
|
|
111
|
+
} else if (element.tag === "img" && element.image_key) {
|
|
112
|
+
// Embedded image
|
|
113
|
+
imageKeys.push(element.image_key);
|
|
114
|
+
} else if (element.tag === "md") {
|
|
115
|
+
textContent += element.text || "";
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
textContent += "\n";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
textContent: textContent.trim() || "[富文本消息]",
|
|
124
|
+
imageKeys,
|
|
125
|
+
};
|
|
126
|
+
} catch {
|
|
127
|
+
return { textContent: "[富文本消息]", imageKeys: [] };
|
|
128
|
+
}
|
|
129
|
+
}
|
package/src/reply-dispatcher.ts
CHANGED
|
@@ -106,13 +106,14 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
|
|
106
106
|
return;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
// Check render mode:
|
|
110
|
-
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
111
|
-
const renderMode = feishuCfg?.renderMode ?? "
|
|
112
|
-
|
|
113
|
-
// Determine if we should use card for this message
|
|
114
|
-
const useCard =
|
|
115
|
-
renderMode === "card" || (renderMode === "auto" && shouldUseCard(text));
|
|
109
|
+
// Check render mode: post (default), auto, raw, or card
|
|
110
|
+
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
111
|
+
const renderMode = feishuCfg?.renderMode ?? "post";
|
|
112
|
+
|
|
113
|
+
// Determine if we should use card for this message
|
|
114
|
+
const useCard =
|
|
115
|
+
renderMode === "card" || (renderMode === "auto" && shouldUseCard(text));
|
|
116
|
+
const usePost = renderMode === "post";
|
|
116
117
|
|
|
117
118
|
// Only include @mentions in the first chunk (avoid duplicate @s)
|
|
118
119
|
let isFirstChunk = true;
|
|
@@ -131,22 +132,25 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
|
|
|
131
132
|
});
|
|
132
133
|
isFirstChunk = false;
|
|
133
134
|
}
|
|
134
|
-
} else {
|
|
135
|
-
// Raw mode: send as plain text with table conversion
|
|
136
|
-
const converted = core.channel.text.convertMarkdownTables(text, tableMode);
|
|
137
|
-
const chunks = core.channel.text.chunkTextWithMode(converted, textChunkLimit, chunkMode);
|
|
138
|
-
params.runtime.log?.(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
135
|
+
} else {
|
|
136
|
+
// Raw or post mode: send as plain text or rich post with table conversion
|
|
137
|
+
const converted = core.channel.text.convertMarkdownTables(text, tableMode);
|
|
138
|
+
const chunks = core.channel.text.chunkTextWithMode(converted, textChunkLimit, chunkMode);
|
|
139
|
+
params.runtime.log?.(
|
|
140
|
+
`feishu deliver: sending ${chunks.length} ${usePost ? "post" : "text"} chunks to ${chatId}`,
|
|
141
|
+
);
|
|
142
|
+
for (const chunk of chunks) {
|
|
143
|
+
await sendMessageFeishu({
|
|
144
|
+
cfg,
|
|
145
|
+
to: chatId,
|
|
146
|
+
text: chunk,
|
|
147
|
+
replyToMessageId,
|
|
148
|
+
mentions: isFirstChunk ? mentionTargets : undefined,
|
|
149
|
+
messageType: usePost ? "post" : "text",
|
|
150
|
+
});
|
|
151
|
+
isFirstChunk = false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
150
154
|
},
|
|
151
155
|
onError: (err, info) => {
|
|
152
156
|
params.runtime.error?.(`feishu ${info.kind} reply failed: ${String(err)}`);
|
package/src/send.ts
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
2
|
-
import type { FeishuConfig, FeishuSendResult } from "./types.js";
|
|
3
|
-
import type { MentionTarget } from "./mention.js";
|
|
4
|
-
import { buildMentionedMessage, buildMentionedCardContent } from "./mention.js";
|
|
5
|
-
import { createFeishuClient } from "./client.js";
|
|
6
|
-
import { resolveReceiveIdType, normalizeFeishuTarget } from "./targets.js";
|
|
7
|
-
import { getFeishuRuntime } from "./runtime.js";
|
|
8
|
-
|
|
1
|
+
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
2
|
+
import type { FeishuConfig, FeishuSendResult } from "./types.js";
|
|
3
|
+
import type { MentionTarget } from "./mention.js";
|
|
4
|
+
import { buildMentionedMessage, buildMentionedCardContent } from "./mention.js";
|
|
5
|
+
import { createFeishuClient } from "./client.js";
|
|
6
|
+
import { resolveReceiveIdType, normalizeFeishuTarget } from "./targets.js";
|
|
7
|
+
import { getFeishuRuntime } from "./runtime.js";
|
|
8
|
+
import { buildFeishuPostMessagePayload, parsePostContent } from "./post.js";
|
|
9
|
+
|
|
10
|
+
type MarkdownTableMode = ReturnType<
|
|
11
|
+
ReturnType<typeof getFeishuRuntime>["channel"]["text"]["resolveMarkdownTableMode"]
|
|
12
|
+
>;
|
|
13
|
+
|
|
9
14
|
export type FeishuMessageInfo = {
|
|
10
15
|
messageId: string;
|
|
11
16
|
chatId: string;
|
|
@@ -63,16 +68,20 @@ export async function getMessageFeishu(params: {
|
|
|
63
68
|
return null;
|
|
64
69
|
}
|
|
65
70
|
|
|
66
|
-
// Parse content based on message type
|
|
67
|
-
let content = item.body?.content ?? "";
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
// Parse content based on message type
|
|
72
|
+
let content = item.body?.content ?? "";
|
|
73
|
+
if (item.msg_type === "post") {
|
|
74
|
+
content = parsePostContent(content).textContent;
|
|
75
|
+
} else {
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(content);
|
|
78
|
+
if (item.msg_type === "text" && parsed.text) {
|
|
79
|
+
content = parsed.text;
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
// Keep raw content if parsing fails
|
|
83
|
+
}
|
|
84
|
+
}
|
|
76
85
|
|
|
77
86
|
return {
|
|
78
87
|
messageId: item.message_id ?? messageId,
|
|
@@ -88,22 +97,42 @@ export async function getMessageFeishu(params: {
|
|
|
88
97
|
}
|
|
89
98
|
}
|
|
90
99
|
|
|
91
|
-
export type SendFeishuMessageParams = {
|
|
92
|
-
cfg: ClawdbotConfig;
|
|
93
|
-
to: string;
|
|
94
|
-
text: string;
|
|
95
|
-
replyToMessageId?: string;
|
|
96
|
-
/** Mention target users */
|
|
97
|
-
mentions?: MentionTarget[];
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
100
|
+
export type SendFeishuMessageParams = {
|
|
101
|
+
cfg: ClawdbotConfig;
|
|
102
|
+
to: string;
|
|
103
|
+
text: string;
|
|
104
|
+
replyToMessageId?: string;
|
|
105
|
+
/** Mention target users */
|
|
106
|
+
mentions?: MentionTarget[];
|
|
107
|
+
/** Send as text (default) or post (rich text) */
|
|
108
|
+
messageType?: "text" | "post";
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
function buildFeishuTextMessagePayload(params: {
|
|
112
|
+
rawText: string;
|
|
113
|
+
tableMode: MarkdownTableMode;
|
|
114
|
+
mentions?: MentionTarget[];
|
|
115
|
+
}): {
|
|
116
|
+
content: string;
|
|
117
|
+
msgType: "text";
|
|
118
|
+
} {
|
|
119
|
+
const { rawText, tableMode, mentions } = params;
|
|
120
|
+
const textWithMentions =
|
|
121
|
+
mentions && mentions.length > 0 ? buildMentionedMessage(mentions, rawText) : rawText;
|
|
122
|
+
const text = getFeishuRuntime().channel.text.convertMarkdownTables(textWithMentions, tableMode);
|
|
123
|
+
return {
|
|
124
|
+
content: JSON.stringify({ text }),
|
|
125
|
+
msgType: "text",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function sendMessageFeishu(params: SendFeishuMessageParams): Promise<FeishuSendResult> {
|
|
130
|
+
const { cfg, to, text, replyToMessageId, mentions, messageType } = params;
|
|
131
|
+
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
132
|
+
if (!feishuCfg) {
|
|
133
|
+
throw new Error("Feishu channel not configured");
|
|
134
|
+
}
|
|
135
|
+
|
|
107
136
|
const client = createFeishuClient(feishuCfg);
|
|
108
137
|
const receiveId = normalizeFeishuTarget(to);
|
|
109
138
|
if (!receiveId) {
|
|
@@ -111,28 +140,34 @@ export async function sendMessageFeishu(params: SendFeishuMessageParams): Promis
|
|
|
111
140
|
}
|
|
112
141
|
|
|
113
142
|
const receiveIdType = resolveReceiveIdType(receiveId);
|
|
114
|
-
const tableMode = getFeishuRuntime().channel.text.resolveMarkdownTableMode({
|
|
115
|
-
cfg,
|
|
116
|
-
channel: "feishu",
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
143
|
+
const tableMode = getFeishuRuntime().channel.text.resolveMarkdownTableMode({
|
|
144
|
+
cfg,
|
|
145
|
+
channel: "feishu",
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const rawText = text ?? "";
|
|
149
|
+
const baseMessageText = getFeishuRuntime().channel.text.convertMarkdownTables(rawText, tableMode);
|
|
150
|
+
|
|
151
|
+
const payload =
|
|
152
|
+
messageType === "post"
|
|
153
|
+
? buildFeishuPostMessagePayload({
|
|
154
|
+
messageText: baseMessageText,
|
|
155
|
+
mentions,
|
|
156
|
+
})
|
|
157
|
+
: buildFeishuTextMessagePayload({
|
|
158
|
+
rawText,
|
|
159
|
+
tableMode,
|
|
160
|
+
mentions,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (replyToMessageId) {
|
|
164
|
+
const response = await client.im.message.reply({
|
|
165
|
+
path: { message_id: replyToMessageId },
|
|
166
|
+
data: {
|
|
167
|
+
content: payload.content,
|
|
168
|
+
msg_type: payload.msgType,
|
|
169
|
+
},
|
|
170
|
+
});
|
|
136
171
|
|
|
137
172
|
if (response.code !== 0) {
|
|
138
173
|
throw new Error(`Feishu reply failed: ${response.msg || `code ${response.code}`}`);
|
|
@@ -144,14 +179,14 @@ export async function sendMessageFeishu(params: SendFeishuMessageParams): Promis
|
|
|
144
179
|
};
|
|
145
180
|
}
|
|
146
181
|
|
|
147
|
-
const response = await client.im.message.create({
|
|
148
|
-
params: { receive_id_type: receiveIdType },
|
|
149
|
-
data: {
|
|
150
|
-
receive_id: receiveId,
|
|
151
|
-
content,
|
|
152
|
-
msg_type:
|
|
153
|
-
},
|
|
154
|
-
});
|
|
182
|
+
const response = await client.im.message.create({
|
|
183
|
+
params: { receive_id_type: receiveIdType },
|
|
184
|
+
data: {
|
|
185
|
+
receive_id: receiveId,
|
|
186
|
+
content: payload.content,
|
|
187
|
+
msg_type: payload.msgType,
|
|
188
|
+
},
|
|
189
|
+
});
|
|
155
190
|
|
|
156
191
|
if (response.code !== 0) {
|
|
157
192
|
throw new Error(`Feishu send failed: ${response.msg || `code ${response.code}`}`);
|
|
@@ -292,32 +327,37 @@ export async function sendMarkdownCardFeishu(params: {
|
|
|
292
327
|
* Edit an existing text message.
|
|
293
328
|
* Note: Feishu only allows editing messages within 24 hours.
|
|
294
329
|
*/
|
|
295
|
-
export async function editMessageFeishu(params: {
|
|
296
|
-
cfg: ClawdbotConfig;
|
|
297
|
-
messageId: string;
|
|
298
|
-
text: string;
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
330
|
+
export async function editMessageFeishu(params: {
|
|
331
|
+
cfg: ClawdbotConfig;
|
|
332
|
+
messageId: string;
|
|
333
|
+
text: string;
|
|
334
|
+
messageType?: "text" | "post";
|
|
335
|
+
}): Promise<void> {
|
|
336
|
+
const { cfg, messageId, text, messageType } = params;
|
|
337
|
+
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
338
|
+
if (!feishuCfg) {
|
|
339
|
+
throw new Error("Feishu channel not configured");
|
|
340
|
+
}
|
|
341
|
+
|
|
306
342
|
const client = createFeishuClient(feishuCfg);
|
|
307
|
-
const tableMode = getFeishuRuntime().channel.text.resolveMarkdownTableMode({
|
|
308
|
-
cfg,
|
|
309
|
-
channel: "feishu",
|
|
310
|
-
});
|
|
311
|
-
const
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
},
|
|
320
|
-
|
|
343
|
+
const tableMode = getFeishuRuntime().channel.text.resolveMarkdownTableMode({
|
|
344
|
+
cfg,
|
|
345
|
+
channel: "feishu",
|
|
346
|
+
});
|
|
347
|
+
const rawText = text ?? "";
|
|
348
|
+
const baseMessageText = getFeishuRuntime().channel.text.convertMarkdownTables(rawText, tableMode);
|
|
349
|
+
const payload =
|
|
350
|
+
messageType === "post"
|
|
351
|
+
? buildFeishuPostMessagePayload({ messageText: baseMessageText })
|
|
352
|
+
: buildFeishuTextMessagePayload({ rawText, tableMode });
|
|
353
|
+
|
|
354
|
+
const response = await client.im.message.update({
|
|
355
|
+
path: { message_id: messageId },
|
|
356
|
+
data: {
|
|
357
|
+
msg_type: payload.msgType,
|
|
358
|
+
content: payload.content,
|
|
359
|
+
},
|
|
360
|
+
});
|
|
321
361
|
|
|
322
362
|
if (response.code !== 0) {
|
|
323
363
|
throw new Error(`Feishu message edit failed: ${response.msg || `code ${response.code}`}`);
|