@openclaw/feishu 2026.2.21 → 2026.2.23

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/dedup.ts CHANGED
@@ -1,33 +1,54 @@
1
- // Prevent duplicate processing when WebSocket reconnects or Feishu redelivers messages.
2
- const DEDUP_TTL_MS = 30 * 60 * 1000; // 30 minutes
3
- const DEDUP_MAX_SIZE = 1_000;
4
- const DEDUP_CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // cleanup every 5 minutes
5
- const processedMessageIds = new Map<string, number>(); // messageId -> timestamp
6
- let lastCleanupTime = Date.now();
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { createDedupeCache, createPersistentDedupe } from "openclaw/plugin-sdk";
7
4
 
8
- export function tryRecordMessage(messageId: string): boolean {
9
- const now = Date.now();
5
+ // Persistent TTL: 24 hours — survives restarts & WebSocket reconnects.
6
+ const DEDUP_TTL_MS = 24 * 60 * 60 * 1000;
7
+ const MEMORY_MAX_SIZE = 1_000;
8
+ const FILE_MAX_ENTRIES = 10_000;
10
9
 
11
- // Throttled cleanup: evict expired entries at most once per interval.
12
- if (now - lastCleanupTime > DEDUP_CLEANUP_INTERVAL_MS) {
13
- for (const [id, ts] of processedMessageIds) {
14
- if (now - ts > DEDUP_TTL_MS) {
15
- processedMessageIds.delete(id);
16
- }
17
- }
18
- lastCleanupTime = now;
19
- }
10
+ const memoryDedupe = createDedupeCache({ ttlMs: DEDUP_TTL_MS, maxSize: MEMORY_MAX_SIZE });
20
11
 
21
- if (processedMessageIds.has(messageId)) {
22
- return false;
12
+ function resolveStateDirFromEnv(env: NodeJS.ProcessEnv = process.env): string {
13
+ const stateOverride = env.OPENCLAW_STATE_DIR?.trim() || env.CLAWDBOT_STATE_DIR?.trim();
14
+ if (stateOverride) {
15
+ return stateOverride;
23
16
  }
24
-
25
- // Evict oldest entries if cache is full.
26
- if (processedMessageIds.size >= DEDUP_MAX_SIZE) {
27
- const first = processedMessageIds.keys().next().value!;
28
- processedMessageIds.delete(first);
17
+ if (env.VITEST || env.NODE_ENV === "test") {
18
+ return path.join(os.tmpdir(), ["openclaw-vitest", String(process.pid)].join("-"));
29
19
  }
20
+ return path.join(os.homedir(), ".openclaw");
21
+ }
22
+
23
+ function resolveNamespaceFilePath(namespace: string): string {
24
+ const safe = namespace.replace(/[^a-zA-Z0-9_-]/g, "_");
25
+ return path.join(resolveStateDirFromEnv(), "feishu", "dedup", `${safe}.json`);
26
+ }
27
+
28
+ const persistentDedupe = createPersistentDedupe({
29
+ ttlMs: DEDUP_TTL_MS,
30
+ memoryMaxSize: MEMORY_MAX_SIZE,
31
+ fileMaxEntries: FILE_MAX_ENTRIES,
32
+ resolveFilePath: resolveNamespaceFilePath,
33
+ });
34
+
35
+ /**
36
+ * Synchronous dedup — memory only.
37
+ * Kept for backward compatibility; prefer {@link tryRecordMessagePersistent}.
38
+ */
39
+ export function tryRecordMessage(messageId: string): boolean {
40
+ return !memoryDedupe.check(messageId);
41
+ }
30
42
 
31
- processedMessageIds.set(messageId, now);
32
- return true;
43
+ export async function tryRecordMessagePersistent(
44
+ messageId: string,
45
+ namespace = "global",
46
+ log?: (...args: unknown[]) => void,
47
+ ): Promise<boolean> {
48
+ return persistentDedupe.checkAndRecord(messageId, {
49
+ namespace,
50
+ onDiskError: (error) => {
51
+ log?.(`feishu-dedup: disk error, falling back to memory: ${String(error)}`);
52
+ },
53
+ });
33
54
  }
package/src/media.test.ts CHANGED
@@ -38,6 +38,16 @@ vi.mock("./runtime.js", () => ({
38
38
 
39
39
  import { downloadImageFeishu, downloadMessageResourceFeishu, sendMediaFeishu } from "./media.js";
40
40
 
41
+ function expectPathIsolatedToTmpRoot(pathValue: string, key: string): void {
42
+ expect(pathValue).not.toContain(key);
43
+ expect(pathValue).not.toContain("..");
44
+
45
+ const tmpRoot = path.resolve(os.tmpdir());
46
+ const resolved = path.resolve(pathValue);
47
+ const rel = path.relative(tmpRoot, resolved);
48
+ expect(rel === ".." || rel.startsWith(`..${path.sep}`)).toBe(false);
49
+ }
50
+
41
51
  describe("sendMediaFeishu msg_type routing", () => {
42
52
  beforeEach(() => {
43
53
  vi.clearAllMocks();
@@ -217,13 +227,7 @@ describe("sendMediaFeishu msg_type routing", () => {
217
227
 
218
228
  expect(result.buffer).toEqual(Buffer.from("image-data"));
219
229
  expect(capturedPath).toBeDefined();
220
- expect(capturedPath).not.toContain(imageKey);
221
- expect(capturedPath).not.toContain("..");
222
-
223
- const tmpRoot = path.resolve(os.tmpdir());
224
- const resolved = path.resolve(capturedPath as string);
225
- const rel = path.relative(tmpRoot, resolved);
226
- expect(rel === ".." || rel.startsWith(`..${path.sep}`)).toBe(false);
230
+ expectPathIsolatedToTmpRoot(capturedPath as string, imageKey);
227
231
  });
228
232
 
229
233
  it("uses isolated temp paths for message resource downloads", async () => {
@@ -246,13 +250,7 @@ describe("sendMediaFeishu msg_type routing", () => {
246
250
 
247
251
  expect(result.buffer).toEqual(Buffer.from("resource-data"));
248
252
  expect(capturedPath).toBeDefined();
249
- expect(capturedPath).not.toContain(fileKey);
250
- expect(capturedPath).not.toContain("..");
251
-
252
- const tmpRoot = path.resolve(os.tmpdir());
253
- const resolved = path.resolve(capturedPath as string);
254
- const rel = path.relative(tmpRoot, resolved);
255
- expect(rel === ".." || rel.startsWith(`..${path.sep}`)).toBe(false);
253
+ expectPathIsolatedToTmpRoot(capturedPath as string, fileKey);
256
254
  });
257
255
 
258
256
  it("rejects invalid image keys before calling feishu api", async () => {
package/src/media.ts CHANGED
@@ -7,7 +7,7 @@ import { createFeishuClient } from "./client.js";
7
7
  import { normalizeFeishuExternalKey } from "./external-keys.js";
8
8
  import { getFeishuRuntime } from "./runtime.js";
9
9
  import { assertFeishuMessageApiSuccess, toFeishuSendResult } from "./send-result.js";
10
- import { resolveReceiveIdType, normalizeFeishuTarget } from "./targets.js";
10
+ import { resolveFeishuSendTarget } from "./send-target.js";
11
11
 
12
12
  export type DownloadImageResult = {
13
13
  buffer: Buffer;
@@ -268,18 +268,11 @@ export async function sendImageFeishu(params: {
268
268
  accountId?: string;
269
269
  }): Promise<SendMediaResult> {
270
270
  const { cfg, to, imageKey, replyToMessageId, accountId } = params;
271
- const account = resolveFeishuAccount({ cfg, accountId });
272
- if (!account.configured) {
273
- throw new Error(`Feishu account "${account.accountId}" not configured`);
274
- }
275
-
276
- const client = createFeishuClient(account);
277
- const receiveId = normalizeFeishuTarget(to);
278
- if (!receiveId) {
279
- throw new Error(`Invalid Feishu target: ${to}`);
280
- }
281
-
282
- const receiveIdType = resolveReceiveIdType(receiveId);
271
+ const { client, receiveId, receiveIdType } = resolveFeishuSendTarget({
272
+ cfg,
273
+ to,
274
+ accountId,
275
+ });
283
276
  const content = JSON.stringify({ image_key: imageKey });
284
277
 
285
278
  if (replyToMessageId) {
@@ -320,18 +313,11 @@ export async function sendFileFeishu(params: {
320
313
  }): Promise<SendMediaResult> {
321
314
  const { cfg, to, fileKey, replyToMessageId, accountId } = params;
322
315
  const msgType = params.msgType ?? "file";
323
- const account = resolveFeishuAccount({ cfg, accountId });
324
- if (!account.configured) {
325
- throw new Error(`Feishu account "${account.accountId}" not configured`);
326
- }
327
-
328
- const client = createFeishuClient(account);
329
- const receiveId = normalizeFeishuTarget(to);
330
- if (!receiveId) {
331
- throw new Error(`Invalid Feishu target: ${to}`);
332
- }
333
-
334
- const receiveIdType = resolveReceiveIdType(receiveId);
316
+ const { client, receiveId, receiveIdType } = resolveFeishuSendTarget({
317
+ cfg,
318
+ to,
319
+ accountId,
320
+ });
335
321
  const content = JSON.stringify({ file_key: fileKey });
336
322
 
337
323
  if (replyToMessageId) {
@@ -78,6 +78,41 @@ function buildConfig(params: {
78
78
  } as ClawdbotConfig;
79
79
  }
80
80
 
81
+ async function withRunningWebhookMonitor(
82
+ params: {
83
+ accountId: string;
84
+ path: string;
85
+ verificationToken: string;
86
+ },
87
+ run: (url: string) => Promise<void>,
88
+ ) {
89
+ const port = await getFreePort();
90
+ const cfg = buildConfig({
91
+ accountId: params.accountId,
92
+ path: params.path,
93
+ port,
94
+ verificationToken: params.verificationToken,
95
+ });
96
+
97
+ const abortController = new AbortController();
98
+ const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
99
+ const monitorPromise = monitorFeishuProvider({
100
+ config: cfg,
101
+ runtime,
102
+ abortSignal: abortController.signal,
103
+ });
104
+
105
+ const url = `http://127.0.0.1:${port}${params.path}`;
106
+ await waitUntilServerReady(url);
107
+
108
+ try {
109
+ await run(url);
110
+ } finally {
111
+ abortController.abort();
112
+ await monitorPromise;
113
+ }
114
+ }
115
+
81
116
  afterEach(() => {
82
117
  stopFeishuMonitor();
83
118
  });
@@ -99,76 +134,50 @@ describe("Feishu webhook security hardening", () => {
99
134
 
100
135
  it("returns 415 for POST requests without json content type", async () => {
101
136
  probeFeishuMock.mockResolvedValue({ ok: true, botOpenId: "bot_open_id" });
102
- const port = await getFreePort();
103
- const path = "/hook-content-type";
104
- const cfg = buildConfig({
105
- accountId: "content-type",
106
- path,
107
- port,
108
- verificationToken: "verify_token",
109
- });
110
-
111
- const abortController = new AbortController();
112
- const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
113
- const monitorPromise = monitorFeishuProvider({
114
- config: cfg,
115
- runtime,
116
- abortSignal: abortController.signal,
117
- });
118
-
119
- await waitUntilServerReady(`http://127.0.0.1:${port}${path}`);
120
-
121
- const response = await fetch(`http://127.0.0.1:${port}${path}`, {
122
- method: "POST",
123
- headers: { "content-type": "text/plain" },
124
- body: "{}",
125
- });
126
-
127
- expect(response.status).toBe(415);
128
- expect(await response.text()).toBe("Unsupported Media Type");
129
-
130
- abortController.abort();
131
- await monitorPromise;
137
+ await withRunningWebhookMonitor(
138
+ {
139
+ accountId: "content-type",
140
+ path: "/hook-content-type",
141
+ verificationToken: "verify_token",
142
+ },
143
+ async (url) => {
144
+ const response = await fetch(url, {
145
+ method: "POST",
146
+ headers: { "content-type": "text/plain" },
147
+ body: "{}",
148
+ });
149
+
150
+ expect(response.status).toBe(415);
151
+ expect(await response.text()).toBe("Unsupported Media Type");
152
+ },
153
+ );
132
154
  });
133
155
 
134
156
  it("rate limits webhook burst traffic with 429", async () => {
135
157
  probeFeishuMock.mockResolvedValue({ ok: true, botOpenId: "bot_open_id" });
136
- const port = await getFreePort();
137
- const path = "/hook-rate-limit";
138
- const cfg = buildConfig({
139
- accountId: "rate-limit",
140
- path,
141
- port,
142
- verificationToken: "verify_token",
143
- });
144
-
145
- const abortController = new AbortController();
146
- const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
147
- const monitorPromise = monitorFeishuProvider({
148
- config: cfg,
149
- runtime,
150
- abortSignal: abortController.signal,
151
- });
152
-
153
- await waitUntilServerReady(`http://127.0.0.1:${port}${path}`);
154
-
155
- let saw429 = false;
156
- for (let i = 0; i < 130; i += 1) {
157
- const response = await fetch(`http://127.0.0.1:${port}${path}`, {
158
- method: "POST",
159
- headers: { "content-type": "text/plain" },
160
- body: "{}",
161
- });
162
- if (response.status === 429) {
163
- saw429 = true;
164
- expect(await response.text()).toBe("Too Many Requests");
165
- break;
166
- }
167
- }
168
-
169
- expect(saw429).toBe(true);
170
-
171
- abortController.abort();
172
- await monitorPromise;
158
+ await withRunningWebhookMonitor(
159
+ {
160
+ accountId: "rate-limit",
161
+ path: "/hook-rate-limit",
162
+ verificationToken: "verify_token",
163
+ },
164
+ async (url) => {
165
+ let saw429 = false;
166
+ for (let i = 0; i < 130; i += 1) {
167
+ const response = await fetch(url, {
168
+ method: "POST",
169
+ headers: { "content-type": "text/plain" },
170
+ body: "{}",
171
+ });
172
+ if (response.status === 429) {
173
+ saw429 = true;
174
+ expect(await response.text()).toBe("Too Many Requests");
175
+ break;
176
+ }
177
+ }
178
+
179
+ expect(saw429).toBe(true);
180
+ },
181
+ );
173
182
  });
174
183
  });
package/src/onboarding.ts CHANGED
@@ -104,6 +104,25 @@ async function noteFeishuCredentialHelp(prompter: WizardPrompter): Promise<void>
104
104
  );
105
105
  }
106
106
 
107
+ async function promptFeishuCredentials(prompter: WizardPrompter): Promise<{
108
+ appId: string;
109
+ appSecret: string;
110
+ }> {
111
+ const appId = String(
112
+ await prompter.text({
113
+ message: "Enter Feishu App ID",
114
+ validate: (value) => (value?.trim() ? undefined : "Required"),
115
+ }),
116
+ ).trim();
117
+ const appSecret = String(
118
+ await prompter.text({
119
+ message: "Enter Feishu App Secret",
120
+ validate: (value) => (value?.trim() ? undefined : "Required"),
121
+ }),
122
+ ).trim();
123
+ return { appId, appSecret };
124
+ }
125
+
107
126
  function setFeishuGroupPolicy(
108
127
  cfg: ClawdbotConfig,
109
128
  groupPolicy: "open" | "allowlist" | "disabled",
@@ -210,18 +229,9 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
210
229
  },
211
230
  };
212
231
  } else {
213
- appId = String(
214
- await prompter.text({
215
- message: "Enter Feishu App ID",
216
- validate: (value) => (value?.trim() ? undefined : "Required"),
217
- }),
218
- ).trim();
219
- appSecret = String(
220
- await prompter.text({
221
- message: "Enter Feishu App Secret",
222
- validate: (value) => (value?.trim() ? undefined : "Required"),
223
- }),
224
- ).trim();
232
+ const entered = await promptFeishuCredentials(prompter);
233
+ appId = entered.appId;
234
+ appSecret = entered.appSecret;
225
235
  }
226
236
  } else if (hasConfigCreds) {
227
237
  const keep = await prompter.confirm({
@@ -229,32 +239,14 @@ export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
229
239
  initialValue: true,
230
240
  });
231
241
  if (!keep) {
232
- appId = String(
233
- await prompter.text({
234
- message: "Enter Feishu App ID",
235
- validate: (value) => (value?.trim() ? undefined : "Required"),
236
- }),
237
- ).trim();
238
- appSecret = String(
239
- await prompter.text({
240
- message: "Enter Feishu App Secret",
241
- validate: (value) => (value?.trim() ? undefined : "Required"),
242
- }),
243
- ).trim();
242
+ const entered = await promptFeishuCredentials(prompter);
243
+ appId = entered.appId;
244
+ appSecret = entered.appSecret;
244
245
  }
245
246
  } else {
246
- appId = String(
247
- await prompter.text({
248
- message: "Enter Feishu App ID",
249
- validate: (value) => (value?.trim() ? undefined : "Required"),
250
- }),
251
- ).trim();
252
- appSecret = String(
253
- await prompter.text({
254
- message: "Enter Feishu App Secret",
255
- validate: (value) => (value?.trim() ? undefined : "Required"),
256
- }),
257
- ).trim();
247
+ const entered = await promptFeishuCredentials(prompter);
248
+ appId = entered.appId;
249
+ appSecret = entered.appSecret;
258
250
  }
259
251
 
260
252
  if (appId && appSecret) {
@@ -0,0 +1,59 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { isFeishuGroupAllowed, resolveFeishuAllowlistMatch } from "./policy.js";
3
+
4
+ describe("feishu policy", () => {
5
+ describe("resolveFeishuAllowlistMatch", () => {
6
+ it("allows wildcard", () => {
7
+ expect(
8
+ resolveFeishuAllowlistMatch({
9
+ allowFrom: ["*"],
10
+ senderId: "ou-attacker",
11
+ }),
12
+ ).toEqual({ allowed: true, matchKey: "*", matchSource: "wildcard" });
13
+ });
14
+
15
+ it("matches normalized ID entries", () => {
16
+ expect(
17
+ resolveFeishuAllowlistMatch({
18
+ allowFrom: ["feishu:user:OU_ALLOWED"],
19
+ senderId: "ou_allowed",
20
+ }),
21
+ ).toEqual({ allowed: true, matchKey: "ou_allowed", matchSource: "id" });
22
+ });
23
+
24
+ it("supports user_id as an additional immutable sender candidate", () => {
25
+ expect(
26
+ resolveFeishuAllowlistMatch({
27
+ allowFrom: ["on_user_123"],
28
+ senderId: "ou_other",
29
+ senderIds: ["on_user_123"],
30
+ }),
31
+ ).toEqual({ allowed: true, matchKey: "on_user_123", matchSource: "id" });
32
+ });
33
+
34
+ it("does not authorize based on display-name collision", () => {
35
+ const victimOpenId = "ou_4f4ec5aa111122223333444455556666";
36
+
37
+ expect(
38
+ resolveFeishuAllowlistMatch({
39
+ allowFrom: [victimOpenId],
40
+ senderId: "ou_attacker_real_open_id",
41
+ senderIds: ["on_attacker_user_id"],
42
+ senderName: victimOpenId,
43
+ }),
44
+ ).toEqual({ allowed: false });
45
+ });
46
+ });
47
+
48
+ describe("isFeishuGroupAllowed", () => {
49
+ it("matches group IDs with chat: prefix", () => {
50
+ expect(
51
+ isFeishuGroupAllowed({
52
+ groupPolicy: "allowlist",
53
+ allowFrom: ["chat:oc_group_123"],
54
+ senderId: "oc_group_123",
55
+ }),
56
+ ).toBe(true);
57
+ });
58
+ });
59
+ });
package/src/policy.ts CHANGED
@@ -3,17 +3,52 @@ import type {
3
3
  ChannelGroupContext,
4
4
  GroupToolPolicyConfig,
5
5
  } from "openclaw/plugin-sdk";
6
- import { resolveAllowlistMatchSimple } from "openclaw/plugin-sdk";
6
+ import { normalizeFeishuTarget } from "./targets.js";
7
7
  import type { FeishuConfig, FeishuGroupConfig } from "./types.js";
8
8
 
9
- export type FeishuAllowlistMatch = AllowlistMatch<"wildcard" | "id" | "name">;
9
+ export type FeishuAllowlistMatch = AllowlistMatch<"wildcard" | "id">;
10
+
11
+ function normalizeFeishuAllowEntry(raw: string): string {
12
+ const trimmed = raw.trim();
13
+ if (!trimmed) {
14
+ return "";
15
+ }
16
+ if (trimmed === "*") {
17
+ return "*";
18
+ }
19
+ const withoutProviderPrefix = trimmed.replace(/^feishu:/i, "");
20
+ const normalized = normalizeFeishuTarget(withoutProviderPrefix) ?? withoutProviderPrefix;
21
+ return normalized.trim().toLowerCase();
22
+ }
10
23
 
11
24
  export function resolveFeishuAllowlistMatch(params: {
12
25
  allowFrom: Array<string | number>;
13
26
  senderId: string;
27
+ senderIds?: Array<string | null | undefined>;
14
28
  senderName?: string | null;
15
29
  }): FeishuAllowlistMatch {
16
- return resolveAllowlistMatchSimple(params);
30
+ const allowFrom = params.allowFrom
31
+ .map((entry) => normalizeFeishuAllowEntry(String(entry)))
32
+ .filter(Boolean);
33
+ if (allowFrom.length === 0) {
34
+ return { allowed: false };
35
+ }
36
+ if (allowFrom.includes("*")) {
37
+ return { allowed: true, matchKey: "*", matchSource: "wildcard" };
38
+ }
39
+
40
+ // Feishu allowlists are ID-based; mutable display names must never grant access.
41
+ const senderCandidates = [params.senderId, ...(params.senderIds ?? [])]
42
+ .map((entry) => normalizeFeishuAllowEntry(String(entry ?? "")))
43
+ .filter(Boolean);
44
+
45
+ for (const senderId of senderCandidates) {
46
+ if (allowFrom.includes(senderId)) {
47
+ return { allowed: true, matchKey: senderId, matchSource: "id" };
48
+ }
49
+ }
50
+
51
+ return { allowed: false };
17
52
  }
18
53
 
19
54
  export function resolveFeishuGroupConfig(params: {
@@ -56,6 +91,7 @@ export function isFeishuGroupAllowed(params: {
56
91
  groupPolicy: "open" | "allowlist" | "disabled";
57
92
  allowFrom: Array<string | number>;
58
93
  senderId: string;
94
+ senderIds?: Array<string | null | undefined>;
59
95
  senderName?: string | null;
60
96
  }): boolean {
61
97
  const { groupPolicy } = params;
@@ -0,0 +1,25 @@
1
+ import type { ClawdbotConfig } from "openclaw/plugin-sdk";
2
+ import { resolveFeishuAccount } from "./accounts.js";
3
+ import { createFeishuClient } from "./client.js";
4
+ import { resolveReceiveIdType, normalizeFeishuTarget } from "./targets.js";
5
+
6
+ export function resolveFeishuSendTarget(params: {
7
+ cfg: ClawdbotConfig;
8
+ to: string;
9
+ accountId?: string;
10
+ }) {
11
+ const account = resolveFeishuAccount({ cfg: params.cfg, accountId: params.accountId });
12
+ if (!account.configured) {
13
+ throw new Error(`Feishu account "${account.accountId}" not configured`);
14
+ }
15
+ const client = createFeishuClient(account);
16
+ const receiveId = normalizeFeishuTarget(params.to);
17
+ if (!receiveId) {
18
+ throw new Error(`Invalid Feishu target: ${params.to}`);
19
+ }
20
+ return {
21
+ client,
22
+ receiveId,
23
+ receiveIdType: resolveReceiveIdType(receiveId),
24
+ };
25
+ }
package/src/send.ts CHANGED
@@ -5,8 +5,8 @@ import type { MentionTarget } from "./mention.js";
5
5
  import { buildMentionedMessage, buildMentionedCardContent } from "./mention.js";
6
6
  import { getFeishuRuntime } from "./runtime.js";
7
7
  import { assertFeishuMessageApiSuccess, toFeishuSendResult } from "./send-result.js";
8
- import { resolveReceiveIdType, normalizeFeishuTarget } from "./targets.js";
9
- import type { FeishuSendResult, ResolvedFeishuAccount } from "./types.js";
8
+ import { resolveFeishuSendTarget } from "./send-target.js";
9
+ import type { FeishuSendResult } from "./types.js";
10
10
 
11
11
  export type FeishuMessageInfo = {
12
12
  messageId: string;
@@ -128,18 +128,7 @@ export async function sendMessageFeishu(
128
128
  params: SendFeishuMessageParams,
129
129
  ): Promise<FeishuSendResult> {
130
130
  const { cfg, to, text, replyToMessageId, mentions, accountId } = params;
131
- const account = resolveFeishuAccount({ cfg, accountId });
132
- if (!account.configured) {
133
- throw new Error(`Feishu account "${account.accountId}" not configured`);
134
- }
135
-
136
- const client = createFeishuClient(account);
137
- const receiveId = normalizeFeishuTarget(to);
138
- if (!receiveId) {
139
- throw new Error(`Invalid Feishu target: ${to}`);
140
- }
141
-
142
- const receiveIdType = resolveReceiveIdType(receiveId);
131
+ const { client, receiveId, receiveIdType } = resolveFeishuSendTarget({ cfg, to, accountId });
143
132
  const tableMode = getFeishuRuntime().channel.text.resolveMarkdownTableMode({
144
133
  cfg,
145
134
  channel: "feishu",
@@ -188,18 +177,7 @@ export type SendFeishuCardParams = {
188
177
 
189
178
  export async function sendCardFeishu(params: SendFeishuCardParams): Promise<FeishuSendResult> {
190
179
  const { cfg, to, card, replyToMessageId, accountId } = params;
191
- const account = resolveFeishuAccount({ cfg, accountId });
192
- if (!account.configured) {
193
- throw new Error(`Feishu account "${account.accountId}" not configured`);
194
- }
195
-
196
- const client = createFeishuClient(account);
197
- const receiveId = normalizeFeishuTarget(to);
198
- if (!receiveId) {
199
- throw new Error(`Invalid Feishu target: ${to}`);
200
- }
201
-
202
- const receiveIdType = resolveReceiveIdType(receiveId);
180
+ const { client, receiveId, receiveIdType } = resolveFeishuSendTarget({ cfg, to, accountId });
203
181
  const content = JSON.stringify(card);
204
182
 
205
183
  if (replyToMessageId) {