mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-28 18:33:37 +00:00
refactor(feishu): type runtime payload seams
This commit is contained in:
@@ -38,6 +38,10 @@ type FeishuMessageLike = {
|
||||
|
||||
export type GroupSessionScope = "group" | "group_sender" | "group_topic" | "group_topic_sender";
|
||||
|
||||
type FeishuLogger = {
|
||||
(...args: unknown[]): void;
|
||||
};
|
||||
|
||||
export type ResolvedFeishuGroupSession = {
|
||||
peerId: string;
|
||||
parentPeer: { kind: "group"; id: string } | null;
|
||||
@@ -182,10 +186,7 @@ function formatSubMessageContent(content: string, contentType: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
export function parseMergeForwardContent(params: {
|
||||
content: string;
|
||||
log?: (...args: any[]) => void;
|
||||
}): string {
|
||||
export function parseMergeForwardContent(params: { content: string; log?: FeishuLogger }): string {
|
||||
const { content, log } = params;
|
||||
const maxMessages = 50;
|
||||
log?.("feishu: parsing merge_forward sub-messages from API response");
|
||||
|
||||
@@ -31,6 +31,11 @@ export const FEISHU_HTTP_TIMEOUT_MS = 30_000;
|
||||
export const FEISHU_HTTP_TIMEOUT_MAX_MS = 300_000;
|
||||
export const FEISHU_HTTP_TIMEOUT_ENV_VAR = "OPENCLAW_FEISHU_HTTP_TIMEOUT_MS";
|
||||
|
||||
type FeishuHttpInstanceLike = Pick<
|
||||
typeof feishuClientSdk.defaultHttpInstance,
|
||||
"request" | "get" | "post" | "put" | "patch" | "delete" | "head" | "options"
|
||||
>;
|
||||
|
||||
function getWsProxyAgent(): HttpsProxyAgent<string> | undefined {
|
||||
const proxyUrl =
|
||||
process.env.https_proxy ||
|
||||
@@ -66,8 +71,7 @@ function resolveDomain(domain: FeishuDomain | undefined): Lark.Domain | string {
|
||||
* (e.g. when the Feishu API is slow, causing per-chat queue deadlocks).
|
||||
*/
|
||||
function createTimeoutHttpInstance(defaultTimeoutMs: number): Lark.HttpInstance {
|
||||
const base: Lark.HttpInstance =
|
||||
feishuClientSdk.defaultHttpInstance as unknown as Lark.HttpInstance;
|
||||
const base: FeishuHttpInstanceLike = feishuClientSdk.defaultHttpInstance;
|
||||
|
||||
function injectTimeout<D>(opts?: Lark.HttpRequestOptions<D>): Lark.HttpRequestOptions<D> {
|
||||
return { timeout: defaultTimeoutMs, ...opts } as Lark.HttpRequestOptions<D>;
|
||||
|
||||
@@ -159,6 +159,142 @@ type RegisterEventHandlersContext = {
|
||||
fireAndForget?: boolean;
|
||||
};
|
||||
|
||||
type FeishuBotMenuEvent = {
|
||||
event_key?: string;
|
||||
timestamp?: string | number;
|
||||
operator?: {
|
||||
operator_name?: string;
|
||||
operator_id?: { open_id?: string; user_id?: string; union_id?: string };
|
||||
};
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function readString(value: unknown): string | undefined {
|
||||
return typeof value === "string" ? value : undefined;
|
||||
}
|
||||
|
||||
function readStringOrNumber(value: unknown): string | number | undefined {
|
||||
return typeof value === "string" || typeof value === "number" ? value : undefined;
|
||||
}
|
||||
|
||||
function parseFeishuMessageEventPayload(value: unknown): FeishuMessageEvent | null {
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
const sender = value.sender;
|
||||
const message = value.message;
|
||||
if (!isRecord(sender) || !isRecord(message)) {
|
||||
return null;
|
||||
}
|
||||
const senderId = sender.sender_id;
|
||||
if (!isRecord(senderId)) {
|
||||
return null;
|
||||
}
|
||||
const messageId = readString(message.message_id);
|
||||
const chatId = readString(message.chat_id);
|
||||
const chatType = normalizeFeishuChatType(message.chat_type);
|
||||
const messageType = readString(message.message_type);
|
||||
const content = readString(message.content);
|
||||
if (!messageId || !chatId || !chatType || !messageType || !content) {
|
||||
return null;
|
||||
}
|
||||
return value as FeishuMessageEvent;
|
||||
}
|
||||
|
||||
function parseFeishuBotAddedEventPayload(value: unknown): FeishuBotAddedEvent | null {
|
||||
if (!isRecord(value) || !readString(value.chat_id) || !isRecord(value.operator_id)) {
|
||||
return null;
|
||||
}
|
||||
return value as FeishuBotAddedEvent;
|
||||
}
|
||||
|
||||
function parseFeishuBotRemovedChatId(value: unknown): string | null {
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
return readString(value.chat_id) ?? null;
|
||||
}
|
||||
|
||||
function parseFeishuBotMenuEvent(value: unknown): FeishuBotMenuEvent | null {
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
const operator = value.operator;
|
||||
if (operator !== undefined && !isRecord(operator)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
event_key: readString(value.event_key),
|
||||
timestamp: readStringOrNumber(value.timestamp),
|
||||
operator: operator
|
||||
? {
|
||||
operator_name: readString(operator.operator_name),
|
||||
operator_id: isRecord(operator.operator_id)
|
||||
? {
|
||||
open_id: readString(operator.operator_id.open_id),
|
||||
user_id: readString(operator.operator_id.user_id),
|
||||
union_id: readString(operator.operator_id.union_id),
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function parseFeishuCardActionEventPayload(value: unknown): FeishuCardActionEvent | null {
|
||||
if (!isRecord(value)) {
|
||||
return null;
|
||||
}
|
||||
const operator = value.operator;
|
||||
const action = value.action;
|
||||
const context = value.context;
|
||||
if (!isRecord(operator) || !isRecord(action) || !isRecord(context)) {
|
||||
return null;
|
||||
}
|
||||
const token = readString(value.token);
|
||||
const openId = readString(operator.open_id);
|
||||
const userId = readString(operator.user_id);
|
||||
const unionId = readString(operator.union_id);
|
||||
const tag = readString(action.tag);
|
||||
const actionValue = action.value;
|
||||
const contextOpenId = readString(context.open_id);
|
||||
const contextUserId = readString(context.user_id);
|
||||
const chatId = readString(context.chat_id);
|
||||
if (
|
||||
!token ||
|
||||
!openId ||
|
||||
!userId ||
|
||||
!unionId ||
|
||||
!tag ||
|
||||
!isRecord(actionValue) ||
|
||||
!contextOpenId ||
|
||||
!contextUserId ||
|
||||
!chatId
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
operator: {
|
||||
open_id: openId,
|
||||
user_id: userId,
|
||||
union_id: unionId,
|
||||
},
|
||||
token,
|
||||
action: {
|
||||
value: actionValue,
|
||||
tag,
|
||||
},
|
||||
context: {
|
||||
open_id: contextOpenId,
|
||||
user_id: contextUserId,
|
||||
chat_id: chatId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-chat serial queue that ensures messages from the same chat are processed
|
||||
* in arrival order while allowing different chats to run concurrently.
|
||||
@@ -410,7 +546,11 @@ function registerEventHandlers(
|
||||
|
||||
eventDispatcher.register({
|
||||
"im.message.receive_v1": async (data) => {
|
||||
const event = data as unknown as FeishuMessageEvent;
|
||||
const event = parseFeishuMessageEventPayload(data);
|
||||
if (!event) {
|
||||
error(`feishu[${accountId}]: ignoring malformed message event payload`);
|
||||
return;
|
||||
}
|
||||
const messageId = event.message?.message_id?.trim();
|
||||
if (!tryBeginFeishuMessageProcessing(messageId, accountId)) {
|
||||
log(`feishu[${accountId}]: dropping duplicate event for message ${messageId}`);
|
||||
@@ -438,7 +578,10 @@ function registerEventHandlers(
|
||||
},
|
||||
"im.chat.member.bot.added_v1": async (data) => {
|
||||
try {
|
||||
const event = data as unknown as FeishuBotAddedEvent;
|
||||
const event = parseFeishuBotAddedEventPayload(data);
|
||||
if (!event) {
|
||||
return;
|
||||
}
|
||||
log(`feishu[${accountId}]: bot added to chat ${event.chat_id}`);
|
||||
} catch (err) {
|
||||
error(`feishu[${accountId}]: error handling bot added event: ${String(err)}`);
|
||||
@@ -446,8 +589,11 @@ function registerEventHandlers(
|
||||
},
|
||||
"im.chat.member.bot.deleted_v1": async (data) => {
|
||||
try {
|
||||
const event = data as unknown as { chat_id: string };
|
||||
log(`feishu[${accountId}]: bot removed from chat ${event.chat_id}`);
|
||||
const chatId = parseFeishuBotRemovedChatId(data);
|
||||
if (!chatId) {
|
||||
return;
|
||||
}
|
||||
log(`feishu[${accountId}]: bot removed from chat ${chatId}`);
|
||||
} catch (err) {
|
||||
error(`feishu[${accountId}]: error handling bot removed event: ${String(err)}`);
|
||||
}
|
||||
@@ -513,14 +659,10 @@ function registerEventHandlers(
|
||||
},
|
||||
"application.bot.menu_v6": async (data) => {
|
||||
try {
|
||||
const event = data as {
|
||||
event_key?: string;
|
||||
timestamp?: string | number;
|
||||
operator?: {
|
||||
operator_name?: string;
|
||||
operator_id?: { open_id?: string; user_id?: string; union_id?: string };
|
||||
};
|
||||
};
|
||||
const event = parseFeishuBotMenuEvent(data);
|
||||
if (!event) {
|
||||
return;
|
||||
}
|
||||
const operatorOpenId = event.operator?.operator_id?.open_id?.trim();
|
||||
const eventKey = event.event_key?.trim();
|
||||
if (!operatorOpenId || !eventKey) {
|
||||
@@ -598,7 +740,11 @@ function registerEventHandlers(
|
||||
},
|
||||
"card.action.trigger": async (data: unknown) => {
|
||||
try {
|
||||
const event = data as unknown as FeishuCardActionEvent;
|
||||
const event = parseFeishuCardActionEventPayload(data);
|
||||
if (!event) {
|
||||
error(`feishu[${accountId}]: ignoring malformed card action payload`);
|
||||
return;
|
||||
}
|
||||
const promise = handleFeishuCardAction({
|
||||
cfg,
|
||||
event,
|
||||
|
||||
@@ -24,6 +24,15 @@ type FeishuBotInfoResponse = {
|
||||
data?: { bot?: { bot_name?: string; open_id?: string } };
|
||||
};
|
||||
|
||||
type FeishuRequestClient = ReturnType<typeof createFeishuClient> & {
|
||||
request(params: {
|
||||
method: "GET";
|
||||
url: string;
|
||||
data: Record<string, never>;
|
||||
timeout: number;
|
||||
}): Promise<FeishuBotInfoResponse>;
|
||||
};
|
||||
|
||||
function setCachedProbeResult(
|
||||
cacheKey: string,
|
||||
result: FeishuProbeResult,
|
||||
@@ -70,16 +79,15 @@ export async function probeFeishu(
|
||||
}
|
||||
|
||||
try {
|
||||
const client = createFeishuClient(creds);
|
||||
const client = createFeishuClient(creds) as FeishuRequestClient;
|
||||
// Use bot/v3/info API to get bot information
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- SDK generic request method
|
||||
const responseResult = await raceWithTimeoutAndAbort<FeishuBotInfoResponse>(
|
||||
(client as any).request({
|
||||
client.request({
|
||||
method: "GET",
|
||||
url: "/open-apis/bot/v3/info",
|
||||
data: {},
|
||||
timeout: timeoutMs,
|
||||
}) as Promise<FeishuBotInfoResponse>,
|
||||
}),
|
||||
{
|
||||
timeoutMs,
|
||||
abortSignal: options.abortSignal,
|
||||
|
||||
Reference in New Issue
Block a user