@max1874/feishu 0.2.23 → 0.2.25

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@max1874/feishu",
3
- "version": "0.2.23",
3
+ "version": "0.2.25",
4
4
  "type": "module",
5
5
  "description": "OpenClaw Feishu/Lark channel plugin",
6
6
  "license": "MIT",
package/src/bot.ts CHANGED
@@ -752,12 +752,30 @@ export async function handleFeishuMessage(params: {
752
752
  ...mediaPayload,
753
753
  });
754
754
 
755
+ // Resolve replyToMode: per-chat-type override > global > default ("all")
756
+ const chatTypeKey = isGroup ? "group" : "direct";
757
+ const replyToMode =
758
+ feishuCfg?.replyToModeByChatType?.[chatTypeKey] ??
759
+ feishuCfg?.replyToMode ??
760
+ "all";
761
+
762
+ // Compute effective replyToMessageId based on mode
763
+ let effectiveReplyToMessageId: string | undefined;
764
+ if (replyToMode === "all") {
765
+ // Always thread: reply to the triggering message
766
+ effectiveReplyToMessageId = ctx.messageId;
767
+ } else {
768
+ // "off": only thread if the triggering message is already in a thread
769
+ effectiveReplyToMessageId = ctx.rootId;
770
+ }
771
+
755
772
  const { dispatcher, replyOptions, markDispatchIdle } = createFeishuReplyDispatcher({
756
773
  cfg,
757
774
  agentId: route.agentId,
758
775
  runtime: runtime as RuntimeEnv,
759
776
  chatId: ctx.chatId,
760
- replyToMessageId: ctx.messageId,
777
+ replyToMessageId: effectiveReplyToMessageId,
778
+ replyInThread: !!effectiveReplyToMessageId,
761
779
  mentionTargets: ctx.mentionTargets,
762
780
  });
763
781
 
@@ -33,6 +33,20 @@ const MarkdownConfigSchema = z
33
33
  // Message render mode: auto (default) = detect markdown, raw = plain text, card = always card
34
34
  const RenderModeSchema = z.enum(["auto", "raw", "card"]).optional();
35
35
 
36
+ // Reply-to (threading) mode: controls whether bot replies are sent as threaded replies.
37
+ // - "all" (default): always reply in thread (current behavior)
38
+ // - "off": reply in main chat; only thread if the triggering message was already in a thread
39
+ const ReplyToModeSchema = z.enum(["off", "all"]).optional();
40
+
41
+ // Per-chat-type overrides for replyToMode
42
+ const ReplyToModeByChatTypeSchema = z
43
+ .object({
44
+ direct: ReplyToModeSchema,
45
+ group: ReplyToModeSchema,
46
+ })
47
+ .strict()
48
+ .optional();
49
+
36
50
  const BlockStreamingCoalesceSchema = z
37
51
  .object({
38
52
  enabled: z.boolean().optional(),
@@ -90,6 +104,8 @@ export const FeishuConfigSchema = z
90
104
  mediaMaxMb: z.number().positive().optional(),
91
105
  heartbeat: ChannelHeartbeatVisibilitySchema,
92
106
  renderMode: RenderModeSchema, // raw = plain text (default), card = interactive card with markdown
107
+ replyToMode: ReplyToModeSchema, // "all" = always thread (default), "off" = main chat unless already in thread
108
+ replyToModeByChatType: ReplyToModeByChatTypeSchema, // per-chat-type overrides for replyToMode
93
109
  })
94
110
  .strict()
95
111
  .superRefine((value, ctx) => {
@@ -34,13 +34,15 @@ export type CreateFeishuReplyDispatcherParams = {
34
34
  runtime: RuntimeEnv;
35
35
  chatId: string;
36
36
  replyToMessageId?: string;
37
+ /** When true, reply only appears in thread (not in main chat timeline) */
38
+ replyInThread?: boolean;
37
39
  /** Mention targets, will be auto-included in replies */
38
40
  mentionTargets?: MentionTarget[];
39
41
  };
40
42
 
41
43
  export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherParams) {
42
44
  const core = getFeishuRuntime();
43
- const { cfg, agentId, chatId, replyToMessageId, mentionTargets } = params;
45
+ const { cfg, agentId, chatId, replyToMessageId, replyInThread, mentionTargets } = params;
44
46
 
45
47
  const prefixContext = createReplyPrefixContext({
46
48
  cfg,
@@ -127,6 +129,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
127
129
  to: chatId,
128
130
  text: chunk,
129
131
  replyToMessageId,
132
+ replyInThread,
130
133
  mentions: isFirstChunk ? mentionTargets : undefined,
131
134
  });
132
135
  isFirstChunk = false;
@@ -142,6 +145,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
142
145
  to: chatId,
143
146
  text: chunk,
144
147
  replyToMessageId,
148
+ replyInThread,
145
149
  mentions: isFirstChunk ? mentionTargets : undefined,
146
150
  });
147
151
  isFirstChunk = false;
package/src/send.ts CHANGED
@@ -201,12 +201,14 @@ export type SendFeishuMessageParams = {
201
201
  to: string;
202
202
  text: string;
203
203
  replyToMessageId?: string;
204
+ /** When true, reply only appears in thread (not in main chat timeline) */
205
+ replyInThread?: boolean;
204
206
  /** Mention target users */
205
207
  mentions?: MentionTarget[];
206
208
  };
207
209
 
208
210
  export async function sendMessageFeishu(params: SendFeishuMessageParams): Promise<FeishuSendResult> {
209
- const { cfg, to, text, replyToMessageId, mentions } = params;
211
+ const { cfg, to, text, replyToMessageId, replyInThread, mentions } = params;
210
212
  const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
211
213
  if (!feishuCfg) {
212
214
  throw new Error("Feishu channel not configured");
@@ -239,6 +241,7 @@ export async function sendMessageFeishu(params: SendFeishuMessageParams): Promis
239
241
  data: {
240
242
  content,
241
243
  msg_type: "text",
244
+ ...(replyInThread ? { reply_in_thread: true } : {}),
242
245
  },
243
246
  });
244
247
 
@@ -276,10 +279,11 @@ export type SendFeishuCardParams = {
276
279
  to: string;
277
280
  card: Record<string, unknown>;
278
281
  replyToMessageId?: string;
282
+ replyInThread?: boolean;
279
283
  };
280
284
 
281
285
  export async function sendCardFeishu(params: SendFeishuCardParams): Promise<FeishuSendResult> {
282
- const { cfg, to, card, replyToMessageId } = params;
286
+ const { cfg, to, card, replyToMessageId, replyInThread } = params;
283
287
  const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
284
288
  if (!feishuCfg) {
285
289
  throw new Error("Feishu channel not configured");
@@ -300,6 +304,7 @@ export async function sendCardFeishu(params: SendFeishuCardParams): Promise<Feis
300
304
  data: {
301
305
  content,
302
306
  msg_type: "interactive",
307
+ ...(replyInThread ? { reply_in_thread: true } : {}),
303
308
  },
304
309
  });
305
310
 
@@ -383,17 +388,18 @@ export async function sendMarkdownCardFeishu(params: {
383
388
  to: string;
384
389
  text: string;
385
390
  replyToMessageId?: string;
391
+ replyInThread?: boolean;
386
392
  /** Mention target users */
387
393
  mentions?: MentionTarget[];
388
394
  }): Promise<FeishuSendResult> {
389
- const { cfg, to, text, replyToMessageId, mentions } = params;
395
+ const { cfg, to, text, replyToMessageId, replyInThread, mentions } = params;
390
396
  // Build message content (with @mention support)
391
397
  let cardText = text;
392
398
  if (mentions && mentions.length > 0) {
393
399
  cardText = buildMentionedCardContent(mentions, text);
394
400
  }
395
401
  const card = buildMarkdownCard(cardText);
396
- return sendCardFeishu({ cfg, to, card, replyToMessageId });
402
+ return sendCardFeishu({ cfg, to, card, replyToMessageId, replyInThread });
397
403
  }
398
404
 
399
405
  /**