@openclaw/feishu 2026.3.10 → 2026.3.12

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": "@openclaw/feishu",
3
- "version": "2026.3.10",
3
+ "version": "2026.3.12",
4
4
  "description": "OpenClaw Feishu/Lark channel plugin (community maintained by @m1heng)",
5
5
  "type": "module",
6
6
  "dependencies": {
@@ -241,6 +241,25 @@ describe("resolveFeishuCredentials", () => {
241
241
  domain: "feishu",
242
242
  });
243
243
  });
244
+
245
+ it("does not resolve encryptKey SecretRefs outside webhook mode", () => {
246
+ const creds = resolveFeishuCredentials(
247
+ asConfig({
248
+ connectionMode: "websocket",
249
+ appId: "cli_123",
250
+ appSecret: "secret_456",
251
+ encryptKey: { source: "file", provider: "default", id: "path/to/secret" } as never,
252
+ }),
253
+ );
254
+
255
+ expect(creds).toEqual({
256
+ appId: "cli_123",
257
+ appSecret: "secret_456", // pragma: allowlist secret
258
+ encryptKey: undefined,
259
+ verificationToken: undefined,
260
+ domain: "feishu",
261
+ });
262
+ });
244
263
  });
245
264
 
246
265
  describe("resolveFeishuAccount", () => {
package/src/accounts.ts CHANGED
@@ -169,10 +169,14 @@ export function resolveFeishuCredentials(
169
169
  if (!appId || !appSecret) {
170
170
  return null;
171
171
  }
172
+ const connectionMode = cfg?.connectionMode ?? "websocket";
172
173
  return {
173
174
  appId,
174
175
  appSecret,
175
- encryptKey: normalizeString(cfg?.encryptKey),
176
+ encryptKey:
177
+ connectionMode === "webhook"
178
+ ? resolveSecretLike(cfg?.encryptKey, "channels.feishu.encryptKey")
179
+ : normalizeString(cfg?.encryptKey),
176
180
  verificationToken: resolveSecretLike(
177
181
  cfg?.verificationToken,
178
182
  "channels.feishu.verificationToken",
package/src/channel.ts CHANGED
@@ -129,7 +129,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
129
129
  defaultAccount: { type: "string" },
130
130
  appId: { type: "string" },
131
131
  appSecret: secretInputJsonSchema,
132
- encryptKey: { type: "string" },
132
+ encryptKey: secretInputJsonSchema,
133
133
  verificationToken: secretInputJsonSchema,
134
134
  domain: {
135
135
  oneOf: [
@@ -170,7 +170,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
170
170
  name: { type: "string" },
171
171
  appId: { type: "string" },
172
172
  appSecret: secretInputJsonSchema,
173
- encryptKey: { type: "string" },
173
+ encryptKey: secretInputJsonSchema,
174
174
  verificationToken: secretInputJsonSchema,
175
175
  domain: { type: "string", enum: ["feishu", "lark"] },
176
176
  connectionMode: { type: "string", enum: ["websocket", "webhook"] },
@@ -47,7 +47,7 @@ describe("FeishuConfigSchema webhook validation", () => {
47
47
  }
48
48
  });
49
49
 
50
- it("accepts top-level webhook mode with verificationToken", () => {
50
+ it("rejects top-level webhook mode without encryptKey", () => {
51
51
  const result = FeishuConfigSchema.safeParse({
52
52
  connectionMode: "webhook",
53
53
  verificationToken: "token_top",
@@ -55,6 +55,21 @@ describe("FeishuConfigSchema webhook validation", () => {
55
55
  appSecret: "secret_top", // pragma: allowlist secret
56
56
  });
57
57
 
58
+ expect(result.success).toBe(false);
59
+ if (!result.success) {
60
+ expect(result.error.issues.some((issue) => issue.path.join(".") === "encryptKey")).toBe(true);
61
+ }
62
+ });
63
+
64
+ it("accepts top-level webhook mode with verificationToken and encryptKey", () => {
65
+ const result = FeishuConfigSchema.safeParse({
66
+ connectionMode: "webhook",
67
+ verificationToken: "token_top",
68
+ encryptKey: "encrypt_top",
69
+ appId: "cli_top",
70
+ appSecret: "secret_top", // pragma: allowlist secret
71
+ });
72
+
58
73
  expect(result.success).toBe(true);
59
74
  });
60
75
 
@@ -79,9 +94,30 @@ describe("FeishuConfigSchema webhook validation", () => {
79
94
  }
80
95
  });
81
96
 
82
- it("accepts account webhook mode inheriting top-level verificationToken", () => {
97
+ it("rejects account webhook mode without encryptKey", () => {
98
+ const result = FeishuConfigSchema.safeParse({
99
+ accounts: {
100
+ main: {
101
+ connectionMode: "webhook",
102
+ verificationToken: "token_main",
103
+ appId: "cli_main",
104
+ appSecret: "secret_main", // pragma: allowlist secret
105
+ },
106
+ },
107
+ });
108
+
109
+ expect(result.success).toBe(false);
110
+ if (!result.success) {
111
+ expect(
112
+ result.error.issues.some((issue) => issue.path.join(".") === "accounts.main.encryptKey"),
113
+ ).toBe(true);
114
+ }
115
+ });
116
+
117
+ it("accepts account webhook mode inheriting top-level verificationToken and encryptKey", () => {
83
118
  const result = FeishuConfigSchema.safeParse({
84
119
  verificationToken: "token_top",
120
+ encryptKey: "encrypt_top",
85
121
  accounts: {
86
122
  main: {
87
123
  connectionMode: "webhook",
@@ -102,6 +138,31 @@ describe("FeishuConfigSchema webhook validation", () => {
102
138
  provider: "default",
103
139
  id: "FEISHU_VERIFICATION_TOKEN",
104
140
  },
141
+ encryptKey: "encrypt_top",
142
+ appId: "cli_top",
143
+ appSecret: {
144
+ source: "env",
145
+ provider: "default",
146
+ id: "FEISHU_APP_SECRET",
147
+ },
148
+ });
149
+
150
+ expect(result.success).toBe(true);
151
+ });
152
+
153
+ it("accepts SecretRef encryptKey in webhook mode", () => {
154
+ const result = FeishuConfigSchema.safeParse({
155
+ connectionMode: "webhook",
156
+ verificationToken: {
157
+ source: "env",
158
+ provider: "default",
159
+ id: "FEISHU_VERIFICATION_TOKEN",
160
+ },
161
+ encryptKey: {
162
+ source: "env",
163
+ provider: "default",
164
+ id: "FEISHU_ENCRYPT_KEY",
165
+ },
105
166
  appId: "cli_top",
106
167
  appSecret: {
107
168
  source: "env",
@@ -186,7 +186,7 @@ export const FeishuAccountConfigSchema = z
186
186
  name: z.string().optional(), // Display name for this account
187
187
  appId: z.string().optional(),
188
188
  appSecret: buildSecretInputSchema().optional(),
189
- encryptKey: z.string().optional(),
189
+ encryptKey: buildSecretInputSchema().optional(),
190
190
  verificationToken: buildSecretInputSchema().optional(),
191
191
  domain: FeishuDomainSchema.optional(),
192
192
  connectionMode: FeishuConnectionModeSchema.optional(),
@@ -204,7 +204,7 @@ export const FeishuConfigSchema = z
204
204
  // Top-level credentials (backward compatible for single-account mode)
205
205
  appId: z.string().optional(),
206
206
  appSecret: buildSecretInputSchema().optional(),
207
- encryptKey: z.string().optional(),
207
+ encryptKey: buildSecretInputSchema().optional(),
208
208
  verificationToken: buildSecretInputSchema().optional(),
209
209
  domain: FeishuDomainSchema.optional().default("feishu"),
210
210
  connectionMode: FeishuConnectionModeSchema.optional().default("websocket"),
@@ -240,13 +240,23 @@ export const FeishuConfigSchema = z
240
240
 
241
241
  const defaultConnectionMode = value.connectionMode ?? "websocket";
242
242
  const defaultVerificationTokenConfigured = hasConfiguredSecretInput(value.verificationToken);
243
- if (defaultConnectionMode === "webhook" && !defaultVerificationTokenConfigured) {
244
- ctx.addIssue({
245
- code: z.ZodIssueCode.custom,
246
- path: ["verificationToken"],
247
- message:
248
- 'channels.feishu.connectionMode="webhook" requires channels.feishu.verificationToken',
249
- });
243
+ const defaultEncryptKeyConfigured = hasConfiguredSecretInput(value.encryptKey);
244
+ if (defaultConnectionMode === "webhook") {
245
+ if (!defaultVerificationTokenConfigured) {
246
+ ctx.addIssue({
247
+ code: z.ZodIssueCode.custom,
248
+ path: ["verificationToken"],
249
+ message:
250
+ 'channels.feishu.connectionMode="webhook" requires channels.feishu.verificationToken',
251
+ });
252
+ }
253
+ if (!defaultEncryptKeyConfigured) {
254
+ ctx.addIssue({
255
+ code: z.ZodIssueCode.custom,
256
+ path: ["encryptKey"],
257
+ message: 'channels.feishu.connectionMode="webhook" requires channels.feishu.encryptKey',
258
+ });
259
+ }
250
260
  }
251
261
 
252
262
  for (const [accountId, account] of Object.entries(value.accounts ?? {})) {
@@ -259,6 +269,8 @@ export const FeishuConfigSchema = z
259
269
  }
260
270
  const accountVerificationTokenConfigured =
261
271
  hasConfiguredSecretInput(account.verificationToken) || defaultVerificationTokenConfigured;
272
+ const accountEncryptKeyConfigured =
273
+ hasConfiguredSecretInput(account.encryptKey) || defaultEncryptKeyConfigured;
262
274
  if (!accountVerificationTokenConfigured) {
263
275
  ctx.addIssue({
264
276
  code: z.ZodIssueCode.custom,
@@ -268,6 +280,15 @@ export const FeishuConfigSchema = z
268
280
  "a verificationToken (account-level or top-level)",
269
281
  });
270
282
  }
283
+ if (!accountEncryptKeyConfigured) {
284
+ ctx.addIssue({
285
+ code: z.ZodIssueCode.custom,
286
+ path: ["accounts", accountId, "encryptKey"],
287
+ message:
288
+ `channels.feishu.accounts.${accountId}.connectionMode="webhook" requires ` +
289
+ "an encryptKey (account-level or top-level)",
290
+ });
291
+ }
271
292
  }
272
293
 
273
294
  if (value.dmPolicy === "open") {
@@ -24,14 +24,14 @@ import { botNames, botOpenIds } from "./monitor.state.js";
24
24
  import { monitorWebhook, monitorWebSocket } from "./monitor.transport.js";
25
25
  import { getFeishuRuntime } from "./runtime.js";
26
26
  import { getMessageFeishu } from "./send.js";
27
- import type { ResolvedFeishuAccount } from "./types.js";
27
+ import type { FeishuChatType, ResolvedFeishuAccount } from "./types.js";
28
28
 
29
29
  const FEISHU_REACTION_VERIFY_TIMEOUT_MS = 1_500;
30
30
 
31
31
  export type FeishuReactionCreatedEvent = {
32
32
  message_id: string;
33
33
  chat_id?: string;
34
- chat_type?: "p2p" | "group" | "private";
34
+ chat_type?: string;
35
35
  reaction_type?: { emoji_type?: string };
36
36
  operator_type?: string;
37
37
  user_id?: { open_id?: string };
@@ -105,10 +105,19 @@ export async function resolveReactionSyntheticEvent(
105
105
  return null;
106
106
  }
107
107
 
108
+ const fallbackChatType = reactedMsg.chatType;
109
+ const normalizedEventChatType = normalizeFeishuChatType(event.chat_type);
110
+ const resolvedChatType = normalizedEventChatType ?? fallbackChatType;
111
+ if (!resolvedChatType) {
112
+ logger?.(
113
+ `feishu[${accountId}]: skipping reaction ${emoji} on ${messageId} without chat type context`,
114
+ );
115
+ return null;
116
+ }
117
+
108
118
  const syntheticChatIdRaw = event.chat_id ?? reactedMsg.chatId;
109
119
  const syntheticChatId = syntheticChatIdRaw?.trim() ? syntheticChatIdRaw : `p2p:${senderId}`;
110
- const syntheticChatType: "p2p" | "group" | "private" =
111
- event.chat_type === "group" ? "group" : "p2p";
120
+ const syntheticChatType: FeishuChatType = resolvedChatType;
112
121
  return {
113
122
  sender: {
114
123
  sender_id: { open_id: senderId },
@@ -126,6 +135,10 @@ export async function resolveReactionSyntheticEvent(
126
135
  };
127
136
  }
128
137
 
138
+ function normalizeFeishuChatType(value: unknown): FeishuChatType | undefined {
139
+ return value === "group" || value === "private" || value === "p2p" ? value : undefined;
140
+ }
141
+
129
142
  type RegisterEventHandlersContext = {
130
143
  cfg: ClawdbotConfig;
131
144
  accountId: string;
@@ -521,6 +534,9 @@ export async function monitorSingleAccount(params: MonitorSingleAccountParams):
521
534
  if (connectionMode === "webhook" && !account.verificationToken?.trim()) {
522
535
  throw new Error(`Feishu account "${accountId}" webhook mode requires verificationToken`);
523
536
  }
537
+ if (connectionMode === "webhook" && !account.encryptKey?.trim()) {
538
+ throw new Error(`Feishu account "${accountId}" webhook mode requires encryptKey`);
539
+ }
524
540
 
525
541
  const warmupCount = await warmupDedupFromDisk(accountId, log);
526
542
  if (warmupCount > 0) {
@@ -51,10 +51,11 @@ function makeReactionEvent(
51
51
  };
52
52
  }
53
53
 
54
- function createFetchedReactionMessage(chatId: string) {
54
+ function createFetchedReactionMessage(chatId: string, chatType?: "p2p" | "group" | "private") {
55
55
  return {
56
56
  messageId: "om_msg1",
57
57
  chatId,
58
+ chatType,
58
59
  senderOpenId: "ou_bot",
59
60
  content: "hello",
60
61
  contentType: "text",
@@ -64,13 +65,15 @@ function createFetchedReactionMessage(chatId: string) {
64
65
  async function resolveReactionWithLookup(params: {
65
66
  event?: FeishuReactionCreatedEvent;
66
67
  lookupChatId: string;
68
+ lookupChatType?: "p2p" | "group" | "private";
67
69
  }) {
68
70
  return await resolveReactionSyntheticEvent({
69
71
  cfg,
70
72
  accountId: "default",
71
73
  event: params.event ?? makeReactionEvent(),
72
74
  botOpenId: "ou_bot",
73
- fetchMessage: async () => createFetchedReactionMessage(params.lookupChatId),
75
+ fetchMessage: async () =>
76
+ createFetchedReactionMessage(params.lookupChatId, params.lookupChatType),
74
77
  uuid: () => "fixed-uuid",
75
78
  });
76
79
  }
@@ -268,6 +271,7 @@ describe("resolveReactionSyntheticEvent", () => {
268
271
  fetchMessage: async () => ({
269
272
  messageId: "om_msg1",
270
273
  chatId: "oc_group",
274
+ chatType: "group",
271
275
  senderOpenId: "ou_other",
272
276
  senderType: "user",
273
277
  content: "hello",
@@ -293,6 +297,7 @@ describe("resolveReactionSyntheticEvent", () => {
293
297
  fetchMessage: async () => ({
294
298
  messageId: "om_msg1",
295
299
  chatId: "oc_group",
300
+ chatType: "group",
296
301
  senderOpenId: "ou_other",
297
302
  senderType: "user",
298
303
  content: "hello",
@@ -348,21 +353,43 @@ describe("resolveReactionSyntheticEvent", () => {
348
353
  it("falls back to reacted message chat_id when event chat_id is absent", async () => {
349
354
  const result = await resolveReactionWithLookup({
350
355
  lookupChatId: "oc_group_from_lookup",
356
+ lookupChatType: "group",
351
357
  });
352
358
 
353
359
  expect(result?.message.chat_id).toBe("oc_group_from_lookup");
354
- expect(result?.message.chat_type).toBe("p2p");
360
+ expect(result?.message.chat_type).toBe("group");
355
361
  });
356
362
 
357
363
  it("falls back to sender p2p chat when lookup returns empty chat_id", async () => {
358
364
  const result = await resolveReactionWithLookup({
359
365
  lookupChatId: "",
366
+ lookupChatType: "p2p",
360
367
  });
361
368
 
362
369
  expect(result?.message.chat_id).toBe("p2p:ou_user1");
363
370
  expect(result?.message.chat_type).toBe("p2p");
364
371
  });
365
372
 
373
+ it("drops reactions without chat context when lookup does not provide chat_type", async () => {
374
+ const result = await resolveReactionWithLookup({
375
+ lookupChatId: "oc_group_from_lookup",
376
+ });
377
+
378
+ expect(result).toBeNull();
379
+ });
380
+
381
+ it("drops reactions when event chat_type is invalid and lookup cannot recover it", async () => {
382
+ const result = await resolveReactionWithLookup({
383
+ event: makeReactionEvent({
384
+ chat_id: "oc_group_from_event",
385
+ chat_type: "bogus" as "group",
386
+ }),
387
+ lookupChatId: "oc_group_from_lookup",
388
+ });
389
+
390
+ expect(result).toBeNull();
391
+ });
392
+
366
393
  it("logs and drops reactions when lookup throws", async () => {
367
394
  const log = vi.fn();
368
395
  const event = makeReactionEvent();
@@ -64,6 +64,7 @@ function buildConfig(params: {
64
64
  path: string;
65
65
  port: number;
66
66
  verificationToken?: string;
67
+ encryptKey?: string;
67
68
  }): ClawdbotConfig {
68
69
  return {
69
70
  channels: {
@@ -78,6 +79,7 @@ function buildConfig(params: {
78
79
  webhookHost: "127.0.0.1",
79
80
  webhookPort: params.port,
80
81
  webhookPath: params.path,
82
+ encryptKey: params.encryptKey,
81
83
  verificationToken: params.verificationToken,
82
84
  },
83
85
  },
@@ -91,6 +93,7 @@ async function withRunningWebhookMonitor(
91
93
  accountId: string;
92
94
  path: string;
93
95
  verificationToken: string;
96
+ encryptKey: string;
94
97
  },
95
98
  run: (url: string) => Promise<void>,
96
99
  ) {
@@ -99,6 +102,7 @@ async function withRunningWebhookMonitor(
99
102
  accountId: params.accountId,
100
103
  path: params.path,
101
104
  port,
105
+ encryptKey: params.encryptKey,
102
106
  verificationToken: params.verificationToken,
103
107
  });
104
108
 
@@ -141,6 +145,19 @@ describe("Feishu webhook security hardening", () => {
141
145
  );
142
146
  });
143
147
 
148
+ it("rejects webhook mode without encryptKey", async () => {
149
+ probeFeishuMock.mockResolvedValue({ ok: true, botOpenId: "bot_open_id" });
150
+
151
+ const cfg = buildConfig({
152
+ accountId: "missing-encrypt-key",
153
+ path: "/hook-missing-encrypt",
154
+ port: await getFreePort(),
155
+ verificationToken: "verify_token",
156
+ });
157
+
158
+ await expect(monitorFeishuProvider({ config: cfg })).rejects.toThrow(/requires encryptKey/i);
159
+ });
160
+
144
161
  it("returns 415 for POST requests without json content type", async () => {
145
162
  probeFeishuMock.mockResolvedValue({ ok: true, botOpenId: "bot_open_id" });
146
163
  await withRunningWebhookMonitor(
@@ -148,6 +165,7 @@ describe("Feishu webhook security hardening", () => {
148
165
  accountId: "content-type",
149
166
  path: "/hook-content-type",
150
167
  verificationToken: "verify_token",
168
+ encryptKey: "encrypt_key",
151
169
  },
152
170
  async (url) => {
153
171
  const response = await fetch(url, {
@@ -169,6 +187,7 @@ describe("Feishu webhook security hardening", () => {
169
187
  accountId: "rate-limit",
170
188
  path: "/hook-rate-limit",
171
189
  verificationToken: "verify_token",
190
+ encryptKey: "encrypt_key",
172
191
  },
173
192
  async (url) => {
174
193
  let saw429 = false;
package/src/onboarding.ts CHANGED
@@ -370,6 +370,37 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
370
370
  },
371
371
  };
372
372
  }
373
+ const currentEncryptKey = (next.channels?.feishu as FeishuConfig | undefined)?.encryptKey;
374
+ const encryptKeyPromptState = buildSingleChannelSecretPromptState({
375
+ accountConfigured: hasConfiguredSecretInput(currentEncryptKey),
376
+ hasConfigToken: hasConfiguredSecretInput(currentEncryptKey),
377
+ allowEnv: false,
378
+ });
379
+ const encryptKeyResult = await promptSingleChannelSecretInput({
380
+ cfg: next,
381
+ prompter,
382
+ providerHint: "feishu-webhook",
383
+ credentialLabel: "encrypt key",
384
+ accountConfigured: encryptKeyPromptState.accountConfigured,
385
+ canUseEnv: encryptKeyPromptState.canUseEnv,
386
+ hasConfigToken: encryptKeyPromptState.hasConfigToken,
387
+ envPrompt: "",
388
+ keepPrompt: "Feishu encrypt key already configured. Keep it?",
389
+ inputPrompt: "Enter Feishu encrypt key",
390
+ preferredEnvVar: "FEISHU_ENCRYPT_KEY",
391
+ });
392
+ if (encryptKeyResult.action === "set") {
393
+ next = {
394
+ ...next,
395
+ channels: {
396
+ ...next.channels,
397
+ feishu: {
398
+ ...next.channels?.feishu,
399
+ encryptKey: encryptKeyResult.value,
400
+ },
401
+ },
402
+ };
403
+ }
373
404
  const currentWebhookPath = (next.channels?.feishu as FeishuConfig | undefined)?.webhookPath;
374
405
  const webhookPath = String(
375
406
  await prompter.text({
package/src/send.ts CHANGED
@@ -7,7 +7,7 @@ import { parsePostContent } from "./post.js";
7
7
  import { getFeishuRuntime } from "./runtime.js";
8
8
  import { assertFeishuMessageApiSuccess, toFeishuSendResult } from "./send-result.js";
9
9
  import { resolveFeishuSendTarget } from "./send-target.js";
10
- import type { FeishuSendResult } from "./types.js";
10
+ import type { FeishuChatType, FeishuMessageInfo, FeishuSendResult } from "./types.js";
11
11
 
12
12
  const WITHDRAWN_REPLY_ERROR_CODES = new Set([230011, 231003]);
13
13
 
@@ -74,17 +74,6 @@ async function sendFallbackDirect(
74
74
  return toFeishuSendResult(response, params.receiveId);
75
75
  }
76
76
 
77
- export type FeishuMessageInfo = {
78
- messageId: string;
79
- chatId: string;
80
- senderId?: string;
81
- senderOpenId?: string;
82
- senderType?: string;
83
- content: string;
84
- contentType: string;
85
- createTime?: number;
86
- };
87
-
88
77
  function parseInteractiveCardContent(parsed: unknown): string {
89
78
  if (!parsed || typeof parsed !== "object") {
90
79
  return "[Interactive Card]";
@@ -184,6 +173,7 @@ export async function getMessageFeishu(params: {
184
173
  items?: Array<{
185
174
  message_id?: string;
186
175
  chat_id?: string;
176
+ chat_type?: FeishuChatType;
187
177
  msg_type?: string;
188
178
  body?: { content?: string };
189
179
  sender?: {
@@ -195,6 +185,7 @@ export async function getMessageFeishu(params: {
195
185
  }>;
196
186
  message_id?: string;
197
187
  chat_id?: string;
188
+ chat_type?: FeishuChatType;
198
189
  msg_type?: string;
199
190
  body?: { content?: string };
200
191
  sender?: {
@@ -228,6 +219,10 @@ export async function getMessageFeishu(params: {
228
219
  return {
229
220
  messageId: item.message_id ?? messageId,
230
221
  chatId: item.chat_id ?? "",
222
+ chatType:
223
+ item.chat_type === "group" || item.chat_type === "private" || item.chat_type === "p2p"
224
+ ? item.chat_type
225
+ : undefined,
231
226
  senderId: item.sender?.id,
232
227
  senderOpenId: item.sender?.id_type === "open_id" ? item.sender?.id : undefined,
233
228
  senderType: item.sender?.sender_type,
package/src/types.ts CHANGED
@@ -60,6 +60,20 @@ export type FeishuSendResult = {
60
60
  chatId: string;
61
61
  };
62
62
 
63
+ export type FeishuChatType = "p2p" | "group" | "private";
64
+
65
+ export type FeishuMessageInfo = {
66
+ messageId: string;
67
+ chatId: string;
68
+ chatType?: FeishuChatType;
69
+ senderId?: string;
70
+ senderOpenId?: string;
71
+ senderType?: string;
72
+ content: string;
73
+ contentType: string;
74
+ createTime?: number;
75
+ };
76
+
63
77
  export type FeishuProbeResult = BaseProbeResult<string> & {
64
78
  appId?: string;
65
79
  botName?: string;