




























@@ -2,6 +2,7 @@ import type { ClawdbotConfig, RuntimeEnv } from "../runtime-api.js";
22import { resolveFeishuRuntimeAccount } from "./accounts.js";
33import { handleFeishuMessage, type FeishuMessageEvent } from "./bot.js";
44import { decodeFeishuCardAction, buildFeishuCardActionTextFallback } from "./card-interaction.js";
5+import { createFeishuClient } from "./client.js";
56import {
67createApprovalCard,
78FEISHU_APPROVAL_CANCEL_ACTION,
@@ -104,7 +105,7 @@ function releaseFeishuCardActionToken(params: { token: string; accountId: string
104105function buildSyntheticMessageEvent(
105106event: FeishuCardActionEvent,
106107content: string,
107-chatType?: "p2p" | "group",
108+chatType: "p2p" | "group",
108109): FeishuMessageEvent {
109110return {
110111sender: {
@@ -117,7 +118,7 @@ function buildSyntheticMessageEvent(
117118message: {
118119message_id: `card-action-${event.token}`,
119120chat_id: event.context.chat_id || event.operator.open_id,
120-chat_type: chatType ?? (event.context.chat_id ? "group" : "p2p"),
121+chat_type: chatType,
121122message_type: "text",
122123content: JSON.stringify({ text: content }),
123124},
@@ -136,20 +137,124 @@ async function dispatchSyntheticCommand(params: {
136137cfg: ClawdbotConfig;
137138event: FeishuCardActionEvent;
138139command: string;
140+account: ReturnType<typeof resolveFeishuRuntimeAccount>;
139141botOpenId?: string;
140142runtime?: RuntimeEnv;
141143accountId?: string;
142144chatType?: "p2p" | "group";
143145}): Promise<void> {
146+const resolvedChatType = await resolveCardActionChatType({
147+event: params.event,
148+account: params.account,
149+chatType: params.chatType,
150+log: params.runtime?.log ?? console.log,
151+});
144152await handleFeishuMessage({
145153cfg: params.cfg,
146-event: buildSyntheticMessageEvent(params.event, params.command, params.chatType),
154+event: buildSyntheticMessageEvent(params.event, params.command, resolvedChatType),
147155botOpenId: params.botOpenId,
148156runtime: params.runtime,
149157accountId: params.accountId,
150158});
151159}
152160161+// Feishu's im.chat.get returns two fields:
162+// chat_mode: conversation type — "p2p" | "group" | "topic"
163+// chat_type: privacy classification — "private" | "public"
164+// We check chat_mode first because it directly indicates conversation type.
165+// "private" maps to "p2p" as the safe-failure direction (restrictive DM
166+// policy) — a private group chat misclassified as p2p is safer than the
167+// reverse. "topic" and "public" are treated as group semantics.
168+function normalizeResolvedCardActionChatType(value: unknown): "p2p" | "group" | undefined {
169+if (value === "group" || value === "topic" || value === "public") {
170+return "group";
171+}
172+if (value === "p2p" || value === "private") {
173+return "p2p";
174+}
175+return undefined;
176+}
177+178+const resolvedChatTypeCache = new Map<string, { value: "p2p" | "group"; expiresAt: number }>();
179+const CHAT_TYPE_CACHE_TTL_MS = 30 * 60_000;
180+const CHAT_TYPE_CACHE_MAX_SIZE = 5_000;
181+182+function pruneChatTypeCache(now: number): void {
183+for (const [key, entry] of resolvedChatTypeCache.entries()) {
184+if (entry.expiresAt <= now) {
185+resolvedChatTypeCache.delete(key);
186+}
187+}
188+if (resolvedChatTypeCache.size > CHAT_TYPE_CACHE_MAX_SIZE) {
189+const excess = resolvedChatTypeCache.size - CHAT_TYPE_CACHE_MAX_SIZE;
190+const iter = resolvedChatTypeCache.keys();
191+for (let i = 0; i < excess; i++) {
192+const key = iter.next().value;
193+if (key !== undefined) {
194+resolvedChatTypeCache.delete(key);
195+}
196+}
197+}
198+}
199+200+function sanitizeLogValue(v: string): string {
201+return v.replace(/[\r\n]/g, " ").slice(0, 500);
202+}
203+204+async function resolveCardActionChatType(params: {
205+event: FeishuCardActionEvent;
206+account: ReturnType<typeof resolveFeishuRuntimeAccount>;
207+chatType?: "p2p" | "group";
208+log: (message: string) => void;
209+}): Promise<"p2p" | "group"> {
210+const explicitChatType = normalizeResolvedCardActionChatType(params.chatType);
211+if (explicitChatType) {
212+return explicitChatType;
213+}
214+215+const chatId = params.event.context.chat_id?.trim();
216+if (!chatId) {
217+return "p2p";
218+}
219+220+const cacheKey = `${params.account.accountId}:${chatId}`;
221+const now = Date.now();
222+pruneChatTypeCache(now);
223+const cached = resolvedChatTypeCache.get(cacheKey);
224+if (cached) {
225+return cached.value;
226+}
227+228+try {
229+const response = (await createFeishuClient(params.account).im.chat.get({
230+path: { chat_id: chatId },
231+})) as { code?: number; msg?: string; data?: { chat_type?: unknown; chat_mode?: unknown } };
232+if (response.code === 0) {
233+const resolvedChatType =
234+normalizeResolvedCardActionChatType(response.data?.chat_mode) ??
235+normalizeResolvedCardActionChatType(response.data?.chat_type);
236+if (resolvedChatType) {
237+resolvedChatTypeCache.set(cacheKey, { value: resolvedChatType, expiresAt: now + CHAT_TYPE_CACHE_TTL_MS });
238+return resolvedChatType;
239+}
240+params.log(
241+`feishu[${params.account.accountId}]: card action missing chat type for chat; defaulting to p2p`,
242+);
243+} else {
244+params.log(
245+`feishu[${params.account.accountId}]: failed to resolve chat type: ${sanitizeLogValue(response.msg ?? "unknown error")}; defaulting to p2p`,
246+);
247+}
248+} catch (err) {
249+const message = err instanceof Error ? err.message : "unknown";
250+params.log(
251+`feishu[${params.account.accountId}]: failed to resolve chat type: ${sanitizeLogValue(message)}; defaulting to p2p`,
252+);
253+}
254+255+return "p2p";
256+}
257+153258async function sendInvalidInteractionNotice(params: {
154259cfg: ClawdbotConfig;
155260event: FeishuCardActionEvent;
@@ -246,7 +351,12 @@ export async function handleFeishuCardAction(params: {
246351 prompt,
247352sessionKey: envelope.c?.s,
248353expiresAt: Date.now() + FEISHU_APPROVAL_CARD_TTL_MS,
249-chatType: envelope.c?.t ?? (event.context.chat_id ? "group" : "p2p"),
354+chatType: await resolveCardActionChatType({
355+ event,
356+ account,
357+chatType: envelope.c?.t,
358+ log,
359+}),
250360confirmLabel: command === "/reset" ? "Reset" : "Confirm",
251361}),
252362 accountId,
@@ -282,10 +392,11 @@ export async function handleFeishuCardAction(params: {
282392 cfg,
283393 event,
284394 command,
395+ account,
285396botOpenId: params.botOpenId,
286397 runtime,
287398 accountId,
288-chatType: envelope.c?.t ?? (event.context.chat_id ? "group" : "p2p"),
399+chatType: envelope.c?.t,
289400});
290401completeFeishuCardActionToken({ token: event.token, accountId: account.accountId });
291402return;
@@ -311,6 +422,7 @@ export async function handleFeishuCardAction(params: {
311422 cfg,
312423 event,
313424command: content,
425+ account,
314426botOpenId: params.botOpenId,
315427 runtime,
316428 accountId,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。