mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-25 19:31:10 +00:00
* refactor(channels): unify inbound replay-guard orchestration on shared factory * fix(plugin-sdk): bind replay-guard settlement to claim handles * fix(plugin-sdk): claim-handle settlement (rest) * fix(discord): align skipped queue cleanup * style(feishu): simplify bot menu claim settlement * refactor(plugin-sdk): extract replay dedupe contracts
162 lines
5.5 KiB
TypeScript
162 lines
5.5 KiB
TypeScript
// Feishu plugin module implements monitor.bot menu handler behavior.
|
|
import { isRecord, readStringValue as readString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
import type { ClawdbotConfig, HistoryEntry, PluginRuntime, RuntimeEnv } from "../runtime-api.js";
|
|
import { handleFeishuMessage, type FeishuMessageEvent } from "./bot.js";
|
|
import { maybeHandleFeishuQuickActionMenu } from "./card-ux-launcher.js";
|
|
import { claimUnprocessedFeishuMessage, forgetProcessedFeishuMessage } from "./dedup.js";
|
|
import { botNames, botOpenIds } from "./monitor.state.js";
|
|
import { isFeishuRetryableSyntheticEventError } from "./monitor.synthetic-error.js";
|
|
|
|
type FeishuBotMenuEvent = {
|
|
event_key?: string;
|
|
timestamp?: string | number;
|
|
operator?: {
|
|
operator_name?: string;
|
|
operator_id?: { open_id?: string; user_id?: string; union_id?: string };
|
|
};
|
|
};
|
|
|
|
function readStringOrNumber(value: unknown): string | number | undefined {
|
|
return typeof value === "string" || typeof value === "number" ? value : undefined;
|
|
}
|
|
|
|
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,
|
|
};
|
|
}
|
|
|
|
export function createFeishuBotMenuHandler(params: {
|
|
cfg: ClawdbotConfig;
|
|
accountId: string;
|
|
runtime?: RuntimeEnv;
|
|
channelRuntime?: PluginRuntime["channel"];
|
|
chatHistories: Map<string, HistoryEntry[]>;
|
|
fireAndForget?: boolean;
|
|
getBotOpenId?: (accountId: string) => string | undefined;
|
|
getBotName?: (accountId: string) => string | undefined;
|
|
}): (data: unknown) => Promise<void> {
|
|
const { cfg, accountId, runtime, chatHistories, fireAndForget } = params;
|
|
const log = runtime?.log ?? console.log;
|
|
const error = runtime?.error ?? console.error;
|
|
const getBotOpenId = params.getBotOpenId ?? ((id) => botOpenIds.get(id));
|
|
const getBotName = params.getBotName ?? ((id) => botNames.get(id));
|
|
|
|
return async (data) => {
|
|
try {
|
|
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) {
|
|
return;
|
|
}
|
|
const syntheticEvent: FeishuMessageEvent = {
|
|
sender: {
|
|
sender_id: {
|
|
open_id: operatorOpenId,
|
|
user_id: event.operator?.operator_id?.user_id,
|
|
union_id: event.operator?.operator_id?.union_id,
|
|
},
|
|
sender_type: "user",
|
|
},
|
|
message: {
|
|
message_id: `bot-menu:${eventKey}:${event.timestamp ?? Date.now()}`,
|
|
suppress_reply_target: true,
|
|
chat_id: `p2p:${operatorOpenId}`,
|
|
chat_type: "p2p",
|
|
message_type: "text",
|
|
content: JSON.stringify({
|
|
text: `/menu ${eventKey}`,
|
|
}),
|
|
},
|
|
};
|
|
const syntheticMessageId = syntheticEvent.message.message_id;
|
|
const claim = await claimUnprocessedFeishuMessage({
|
|
messageId: syntheticMessageId,
|
|
namespace: accountId,
|
|
log,
|
|
});
|
|
if (claim.kind === "duplicate") {
|
|
log(`feishu[${accountId}]: dropping duplicate bot-menu event for ${syntheticMessageId}`);
|
|
return;
|
|
}
|
|
if (claim.kind === "inflight") {
|
|
log(`feishu[${accountId}]: dropping in-flight bot-menu event for ${syntheticMessageId}`);
|
|
return;
|
|
}
|
|
const handleLegacyMenu = () =>
|
|
handleFeishuMessage({
|
|
cfg,
|
|
event: syntheticEvent,
|
|
botOpenId: getBotOpenId(accountId),
|
|
botName: getBotName(accountId),
|
|
runtime,
|
|
channelRuntime: params.channelRuntime,
|
|
chatHistories,
|
|
accountId,
|
|
processingClaim: claim.kind === "claimed" ? claim.handle : undefined,
|
|
});
|
|
|
|
const promise = maybeHandleFeishuQuickActionMenu({
|
|
cfg,
|
|
eventKey,
|
|
operatorOpenId,
|
|
runtime,
|
|
accountId,
|
|
})
|
|
.then(async (handledMenu) => {
|
|
if (handledMenu) {
|
|
if (claim.kind === "claimed") {
|
|
await claim.handle.commit();
|
|
}
|
|
return;
|
|
}
|
|
return await handleLegacyMenu();
|
|
})
|
|
.catch(async (err: unknown) => {
|
|
if (isFeishuRetryableSyntheticEventError(err)) {
|
|
await forgetProcessedFeishuMessage(syntheticMessageId, accountId, log);
|
|
if (claim.kind === "claimed") {
|
|
claim.handle.release({ error: err });
|
|
}
|
|
} else if (claim.kind === "claimed") {
|
|
await claim.handle.commit();
|
|
}
|
|
throw err;
|
|
});
|
|
if (fireAndForget) {
|
|
promise.catch((err: unknown) => {
|
|
error(`feishu[${accountId}]: error handling bot menu event: ${String(err)}`);
|
|
});
|
|
return;
|
|
}
|
|
await promise;
|
|
} catch (err) {
|
|
error(`feishu[${accountId}]: error handling bot menu event: ${String(err)}`);
|
|
}
|
|
};
|
|
}
|