@max1874/feishu 0.2.0 → 0.2.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.
package/src/media.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
1
+ import type { ClawdbotConfig } from "openclaw/plugin-sdk";
2
2
  import type { FeishuConfig } from "./types.js";
3
3
  import { createFeishuClient } from "./client.js";
4
4
  import { resolveReceiveIdType, normalizeFeishuTarget } from "./targets.js";
package/src/mention.ts ADDED
@@ -0,0 +1,121 @@
1
+ import type { FeishuMessageEvent } from "./bot.js";
2
+
3
+ /**
4
+ * Mention target user info
5
+ */
6
+ export type MentionTarget = {
7
+ openId: string;
8
+ name: string;
9
+ key: string; // Placeholder in original message, e.g. @_user_1
10
+ };
11
+
12
+ /**
13
+ * Extract mention targets from message event (excluding the bot itself)
14
+ */
15
+ export function extractMentionTargets(
16
+ event: FeishuMessageEvent,
17
+ botOpenId?: string,
18
+ ): MentionTarget[] {
19
+ const mentions = event.message.mentions ?? [];
20
+
21
+ return mentions
22
+ .filter((m) => {
23
+ // Exclude the bot itself
24
+ if (botOpenId && m.id.open_id === botOpenId) return false;
25
+ // Must have open_id
26
+ return !!m.id.open_id;
27
+ })
28
+ .map((m) => ({
29
+ openId: m.id.open_id!,
30
+ name: m.name,
31
+ key: m.key,
32
+ }));
33
+ }
34
+
35
+ /**
36
+ * Check if message is a mention forward request
37
+ * Rules:
38
+ * - Group: message mentions bot + at least one other user
39
+ * - DM: message mentions any user (no need to mention bot)
40
+ */
41
+ export function isMentionForwardRequest(
42
+ event: FeishuMessageEvent,
43
+ botOpenId?: string,
44
+ ): boolean {
45
+ const mentions = event.message.mentions ?? [];
46
+ if (mentions.length === 0) return false;
47
+
48
+ const isDirectMessage = event.message.chat_type === "p2p";
49
+ const hasOtherMention = mentions.some((m) => m.id.open_id !== botOpenId);
50
+
51
+ if (isDirectMessage) {
52
+ // DM: trigger if any non-bot user is mentioned
53
+ return hasOtherMention;
54
+ } else {
55
+ // Group: need to mention both bot and other users
56
+ const hasBotMention = mentions.some((m) => m.id.open_id === botOpenId);
57
+ return hasBotMention && hasOtherMention;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Extract message body from text (remove @ placeholders)
63
+ */
64
+ export function extractMessageBody(text: string, allMentionKeys: string[]): string {
65
+ let result = text;
66
+
67
+ // Remove all @ placeholders
68
+ for (const key of allMentionKeys) {
69
+ result = result.replace(new RegExp(key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"), "");
70
+ }
71
+
72
+ return result.replace(/\s+/g, " ").trim();
73
+ }
74
+
75
+ /**
76
+ * Format @mention for text message
77
+ */
78
+ export function formatMentionForText(target: MentionTarget): string {
79
+ return `<at user_id="${target.openId}">${target.name}</at>`;
80
+ }
81
+
82
+ /**
83
+ * Format @everyone for text message
84
+ */
85
+ export function formatMentionAllForText(): string {
86
+ return `<at user_id="all">Everyone</at>`;
87
+ }
88
+
89
+ /**
90
+ * Format @mention for card message (lark_md)
91
+ */
92
+ export function formatMentionForCard(target: MentionTarget): string {
93
+ return `<at id=${target.openId}></at>`;
94
+ }
95
+
96
+ /**
97
+ * Format @everyone for card message
98
+ */
99
+ export function formatMentionAllForCard(): string {
100
+ return `<at id=all></at>`;
101
+ }
102
+
103
+ /**
104
+ * Build complete message with @mentions (text format)
105
+ */
106
+ export function buildMentionedMessage(targets: MentionTarget[], message: string): string {
107
+ if (targets.length === 0) return message;
108
+
109
+ const mentionParts = targets.map((t) => formatMentionForText(t));
110
+ return `${mentionParts.join(" ")} ${message}`;
111
+ }
112
+
113
+ /**
114
+ * Build card content with @mentions (Markdown format)
115
+ */
116
+ export function buildMentionedCardContent(targets: MentionTarget[], message: string): string {
117
+ if (targets.length === 0) return message;
118
+
119
+ const mentionParts = targets.map((t) => formatMentionForCard(t));
120
+ return `${mentionParts.join(" ")} ${message}`;
121
+ }
package/src/monitor.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import http from "node:http";
2
2
  import * as Lark from "@larksuiteoapi/node-sdk";
3
- import type { ClawdbotConfig, RuntimeEnv, HistoryEntry } from "clawdbot/plugin-sdk";
3
+ import type { ClawdbotConfig, RuntimeEnv, HistoryEntry } from "openclaw/plugin-sdk";
4
4
  import type { FeishuConfig } from "./types.js";
5
5
  import { createFeishuWSClient, createEventDispatcher } from "./client.js";
6
6
  import { resolveFeishuCredentials } from "./accounts.js";
package/src/onboarding.ts CHANGED
@@ -4,8 +4,8 @@ import type {
4
4
  ClawdbotConfig,
5
5
  DmPolicy,
6
6
  WizardPrompter,
7
- } from "clawdbot/plugin-sdk";
8
- import { addWildcardAllowFrom, DEFAULT_ACCOUNT_ID, formatDocsLink } from "clawdbot/plugin-sdk";
7
+ } from "openclaw/plugin-sdk";
8
+ import { addWildcardAllowFrom, DEFAULT_ACCOUNT_ID, formatDocsLink } from "openclaw/plugin-sdk";
9
9
 
10
10
  import { resolveFeishuCredentials } from "./accounts.js";
11
11
  import { probeFeishu } from "./probe.js";
package/src/outbound.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ChannelOutboundAdapter } from "clawdbot/plugin-sdk";
1
+ import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk";
2
2
  import { getFeishuRuntime } from "./runtime.js";
3
3
  import { sendMessageFeishu } from "./send.js";
4
4
  import { sendMediaFeishu } from "./media.js";
package/src/policy.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ChannelGroupContext, GroupToolPolicyConfig } from "clawdbot/plugin-sdk";
1
+ import type { ChannelGroupContext, GroupToolPolicyConfig } from "openclaw/plugin-sdk";
2
2
  import type { FeishuConfig, FeishuGroupConfig } from "./types.js";
3
3
 
4
4
  export type FeishuAllowlistMatch = {
package/src/reactions.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
1
+ import type { ClawdbotConfig } from "openclaw/plugin-sdk";
2
2
  import type { FeishuConfig } from "./types.js";
3
3
  import { createFeishuClient } from "./client.js";
4
4
 
@@ -5,10 +5,11 @@ import {
5
5
  type ClawdbotConfig,
6
6
  type RuntimeEnv,
7
7
  type ReplyPayload,
8
- } from "clawdbot/plugin-sdk";
8
+ } from "openclaw/plugin-sdk";
9
9
  import { getFeishuRuntime } from "./runtime.js";
10
10
  import { sendMessageFeishu, sendMarkdownCardFeishu } from "./send.js";
11
11
  import type { FeishuConfig } from "./types.js";
12
+ import type { MentionTarget } from "./mention.js";
12
13
  import {
13
14
  addTypingIndicator,
14
15
  removeTypingIndicator,
@@ -33,11 +34,13 @@ export type CreateFeishuReplyDispatcherParams = {
33
34
  runtime: RuntimeEnv;
34
35
  chatId: string;
35
36
  replyToMessageId?: string;
37
+ /** Mention targets, will be auto-included in replies */
38
+ mentionTargets?: MentionTarget[];
36
39
  };
37
40
 
38
41
  export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherParams) {
39
42
  const core = getFeishuRuntime();
40
- const { cfg, agentId, chatId, replyToMessageId } = params;
43
+ const { cfg, agentId, chatId, replyToMessageId, mentionTargets } = params;
41
44
 
42
45
  const prefixContext = createReplyPrefixContext({
43
46
  cfg,
@@ -111,6 +114,9 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
111
114
  const useCard =
112
115
  renderMode === "card" || (renderMode === "auto" && shouldUseCard(text));
113
116
 
117
+ // Only include @mentions in the first chunk (avoid duplicate @s)
118
+ let isFirstChunk = true;
119
+
114
120
  if (useCard) {
115
121
  // Card mode: send as interactive card with markdown rendering
116
122
  const chunks = core.channel.text.chunkTextWithMode(text, textChunkLimit, chunkMode);
@@ -121,7 +127,9 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
121
127
  to: chatId,
122
128
  text: chunk,
123
129
  replyToMessageId,
130
+ mentions: isFirstChunk ? mentionTargets : undefined,
124
131
  });
132
+ isFirstChunk = false;
125
133
  }
126
134
  } else {
127
135
  // Raw mode: send as plain text with table conversion
@@ -134,7 +142,9 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
134
142
  to: chatId,
135
143
  text: chunk,
136
144
  replyToMessageId,
145
+ mentions: isFirstChunk ? mentionTargets : undefined,
137
146
  });
147
+ isFirstChunk = false;
138
148
  }
139
149
  }
140
150
  },
package/src/runtime.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { PluginRuntime } from "clawdbot/plugin-sdk";
1
+ import type { PluginRuntime } from "openclaw/plugin-sdk";
2
2
 
3
3
  let runtime: PluginRuntime | null = null;
4
4
 
package/src/send.ts CHANGED
@@ -1,5 +1,7 @@
1
- import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
1
+ import type { ClawdbotConfig } from "openclaw/plugin-sdk";
2
2
  import type { FeishuConfig, FeishuSendResult } from "./types.js";
3
+ import type { MentionTarget } from "./mention.js";
4
+ import { buildMentionedMessage, buildMentionedCardContent } from "./mention.js";
3
5
  import { createFeishuClient } from "./client.js";
4
6
  import { resolveReceiveIdType, normalizeFeishuTarget } from "./targets.js";
5
7
  import { getFeishuRuntime } from "./runtime.js";
@@ -14,6 +16,14 @@ export type FeishuMessageInfo = {
14
16
  createTime?: number;
15
17
  };
16
18
 
19
+ export type FeishuMergeForwardMessage = {
20
+ messageId: string;
21
+ chatId?: string;
22
+ content: string;
23
+ contentType: string;
24
+ upperMessageId?: string;
25
+ };
26
+
17
27
  /**
18
28
  * Get a message by its ID.
19
29
  * Useful for fetching quoted/replied message content.
@@ -86,15 +96,112 @@ export async function getMessageFeishu(params: {
86
96
  }
87
97
  }
88
98
 
99
+ /**
100
+ * Get all messages from a merge_forward message.
101
+ * Returns child messages that are linked via upper_message_id.
102
+ */
103
+ export async function getMergeForwardMessagesFeishu(params: {
104
+ cfg: ClawdbotConfig;
105
+ messageId: string;
106
+ }): Promise<FeishuMergeForwardMessage[]> {
107
+ const { cfg, messageId } = params;
108
+ const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
109
+ if (!feishuCfg) {
110
+ throw new Error("Feishu channel not configured");
111
+ }
112
+
113
+ const client = createFeishuClient(feishuCfg);
114
+
115
+ const response = (await client.im.message.get({
116
+ path: { message_id: messageId },
117
+ })) as {
118
+ code?: number;
119
+ msg?: string;
120
+ data?: {
121
+ items?: Array<{
122
+ message_id?: string;
123
+ chat_id?: string;
124
+ msg_type?: string;
125
+ body?: { content?: string };
126
+ upper_message_id?: string;
127
+ }>;
128
+ };
129
+ };
130
+
131
+ if (response.code !== 0) {
132
+ throw new Error(`Feishu get message failed: ${response.msg || `code ${response.code}`}`);
133
+ }
134
+
135
+ const items = response.data?.items ?? [];
136
+ const result: FeishuMergeForwardMessage[] = [];
137
+
138
+ for (const item of items) {
139
+ // Skip the merge_forward container itself
140
+ if (item.msg_type === "merge_forward") {
141
+ continue;
142
+ }
143
+
144
+ // Parse content based on message type
145
+ let content = item.body?.content ?? "";
146
+ try {
147
+ const parsed = JSON.parse(content);
148
+ if (item.msg_type === "text" && parsed.text) {
149
+ content = parsed.text;
150
+ } else if (item.msg_type === "post") {
151
+ // Extract text from rich text post
152
+ content = extractPostText(parsed);
153
+ }
154
+ } catch {
155
+ // Keep raw content if parsing fails
156
+ }
157
+
158
+ result.push({
159
+ messageId: item.message_id ?? "",
160
+ chatId: item.chat_id,
161
+ content,
162
+ contentType: item.msg_type ?? "text",
163
+ upperMessageId: item.upper_message_id,
164
+ });
165
+ }
166
+
167
+ return result;
168
+ }
169
+
170
+ /**
171
+ * Extract plain text from a Feishu post (rich text) content.
172
+ */
173
+ function extractPostText(parsed: { title?: string; content?: Array<Array<{ tag: string; text?: string }>> }): string {
174
+ const title = parsed.title || "";
175
+ const contentBlocks = parsed.content || [];
176
+ let textContent = title ? `${title}\n\n` : "";
177
+
178
+ for (const paragraph of contentBlocks) {
179
+ if (Array.isArray(paragraph)) {
180
+ for (const element of paragraph) {
181
+ if (element.tag === "text" && element.text) {
182
+ textContent += element.text;
183
+ } else if (element.tag === "a") {
184
+ textContent += (element as { text?: string }).text || "";
185
+ }
186
+ }
187
+ textContent += "\n";
188
+ }
189
+ }
190
+
191
+ return textContent.trim() || "[富文本消息]";
192
+ }
193
+
89
194
  export type SendFeishuMessageParams = {
90
195
  cfg: ClawdbotConfig;
91
196
  to: string;
92
197
  text: string;
93
198
  replyToMessageId?: string;
199
+ /** Mention target users */
200
+ mentions?: MentionTarget[];
94
201
  };
95
202
 
96
203
  export async function sendMessageFeishu(params: SendFeishuMessageParams): Promise<FeishuSendResult> {
97
- const { cfg, to, text, replyToMessageId } = params;
204
+ const { cfg, to, text, replyToMessageId, mentions } = params;
98
205
  const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
99
206
  if (!feishuCfg) {
100
207
  throw new Error("Feishu channel not configured");
@@ -111,7 +218,13 @@ export async function sendMessageFeishu(params: SendFeishuMessageParams): Promis
111
218
  cfg,
112
219
  channel: "feishu",
113
220
  });
114
- const messageText = getFeishuRuntime().channel.text.convertMarkdownTables(text ?? "", tableMode);
221
+
222
+ // Build message content (with @mention support)
223
+ let rawText = text ?? "";
224
+ if (mentions && mentions.length > 0) {
225
+ rawText = buildMentionedMessage(mentions, rawText);
226
+ }
227
+ const messageText = getFeishuRuntime().channel.text.convertMarkdownTables(rawText, tableMode);
115
228
 
116
229
  const content = JSON.stringify({ text: messageText });
117
230
 
@@ -265,9 +378,16 @@ export async function sendMarkdownCardFeishu(params: {
265
378
  to: string;
266
379
  text: string;
267
380
  replyToMessageId?: string;
381
+ /** Mention target users */
382
+ mentions?: MentionTarget[];
268
383
  }): Promise<FeishuSendResult> {
269
- const { cfg, to, text, replyToMessageId } = params;
270
- const card = buildMarkdownCard(text);
384
+ const { cfg, to, text, replyToMessageId, mentions } = params;
385
+ // Build message content (with @mention support)
386
+ let cardText = text;
387
+ if (mentions && mentions.length > 0) {
388
+ cardText = buildMentionedCardContent(mentions, text);
389
+ }
390
+ const card = buildMarkdownCard(cardText);
271
391
  return sendCardFeishu({ cfg, to, card, replyToMessageId });
272
392
  }
273
393
 
package/src/types.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { FeishuConfigSchema, FeishuGroupSchema, z } from "./config-schema.js";
2
+ import type { MentionTarget } from "./mention.js";
2
3
 
3
4
  export type FeishuConfig = z.infer<typeof FeishuConfigSchema>;
4
5
  export type FeishuGroupConfig = z.infer<typeof FeishuGroupSchema>;
@@ -28,6 +29,10 @@ export type FeishuMessageContext = {
28
29
  parentId?: string;
29
30
  content: string;
30
31
  contentType: string;
32
+ /** Mention forward targets (excluding the bot itself) */
33
+ mentionTargets?: MentionTarget[];
34
+ /** Extracted message body (after removing @ placeholders) */
35
+ mentionMessageBody?: string;
31
36
  };
32
37
 
33
38
  export type FeishuSendResult = {
package/src/typing.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
1
+ import type { ClawdbotConfig } from "openclaw/plugin-sdk";
2
2
  import type { FeishuConfig } from "./types.js";
3
3
  import { createFeishuClient } from "./client.js";
4
4