342 lines
10 KiB
TypeScript
342 lines
10 KiB
TypeScript
import type { OpenClawPluginApi } from "../runtime-api.js";
|
|
import { buildFeishuConversationId, parseFeishuConversationId } from "./conversation-id.js";
|
|
import { normalizeFeishuTarget } from "./targets.js";
|
|
import { getFeishuThreadBindingManager } from "./thread-bindings.js";
|
|
|
|
function summarizeError(err: unknown): string {
|
|
if (err instanceof Error) {
|
|
return err.message;
|
|
}
|
|
if (typeof err === "string") {
|
|
return err;
|
|
}
|
|
return "error";
|
|
}
|
|
|
|
function stripProviderPrefix(raw: string): string {
|
|
return raw.replace(/^(feishu|lark):/i, "").trim();
|
|
}
|
|
|
|
function resolveFeishuRequesterConversation(params: {
|
|
accountId?: string;
|
|
to?: string;
|
|
threadId?: string | number;
|
|
requesterSessionKey?: string;
|
|
}): {
|
|
accountId: string;
|
|
conversationId: string;
|
|
parentConversationId?: string;
|
|
} | null {
|
|
const manager = getFeishuThreadBindingManager(params.accountId);
|
|
if (!manager) {
|
|
return null;
|
|
}
|
|
const rawTo = params.to?.trim();
|
|
const withoutProviderPrefix = rawTo ? stripProviderPrefix(rawTo) : "";
|
|
const normalizedTarget = rawTo ? normalizeFeishuTarget(rawTo) : null;
|
|
const threadId =
|
|
params.threadId != null && params.threadId !== "" ? String(params.threadId).trim() : "";
|
|
const isChatTarget = /^(chat|group|channel):/i.test(withoutProviderPrefix);
|
|
const parsedRequesterTopic =
|
|
normalizedTarget && threadId && isChatTarget
|
|
? parseFeishuConversationId({
|
|
conversationId: buildFeishuConversationId({
|
|
chatId: normalizedTarget,
|
|
scope: "group_topic",
|
|
topicId: threadId,
|
|
}),
|
|
parentConversationId: normalizedTarget,
|
|
})
|
|
: null;
|
|
const requesterSessionKey = params.requesterSessionKey?.trim();
|
|
if (requesterSessionKey) {
|
|
const existingBindings = manager.listBySessionKey(requesterSessionKey);
|
|
if (existingBindings.length === 1) {
|
|
const existing = existingBindings[0];
|
|
return {
|
|
accountId: existing.accountId,
|
|
conversationId: existing.conversationId,
|
|
parentConversationId: existing.parentConversationId,
|
|
};
|
|
}
|
|
if (existingBindings.length > 1) {
|
|
if (rawTo && normalizedTarget && !threadId && !isChatTarget) {
|
|
const directMatches = existingBindings.filter(
|
|
(entry) =>
|
|
entry.accountId === manager.accountId &&
|
|
entry.conversationId === normalizedTarget &&
|
|
!entry.parentConversationId,
|
|
);
|
|
if (directMatches.length === 1) {
|
|
const existing = directMatches[0];
|
|
return {
|
|
accountId: existing.accountId,
|
|
conversationId: existing.conversationId,
|
|
parentConversationId: existing.parentConversationId,
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
if (parsedRequesterTopic) {
|
|
const matchingTopicBindings = existingBindings.filter((entry) => {
|
|
const parsed = parseFeishuConversationId({
|
|
conversationId: entry.conversationId,
|
|
parentConversationId: entry.parentConversationId,
|
|
});
|
|
return (
|
|
parsed?.chatId === parsedRequesterTopic.chatId &&
|
|
parsed?.topicId === parsedRequesterTopic.topicId
|
|
);
|
|
});
|
|
if (matchingTopicBindings.length === 1) {
|
|
const existing = matchingTopicBindings[0];
|
|
return {
|
|
accountId: existing.accountId,
|
|
conversationId: existing.conversationId,
|
|
parentConversationId: existing.parentConversationId,
|
|
};
|
|
}
|
|
const senderScopedTopicBindings = matchingTopicBindings.filter((entry) => {
|
|
const parsed = parseFeishuConversationId({
|
|
conversationId: entry.conversationId,
|
|
parentConversationId: entry.parentConversationId,
|
|
});
|
|
return parsed?.scope === "group_topic_sender";
|
|
});
|
|
if (
|
|
senderScopedTopicBindings.length === 1 &&
|
|
matchingTopicBindings.length === senderScopedTopicBindings.length
|
|
) {
|
|
const existing = senderScopedTopicBindings[0];
|
|
return {
|
|
accountId: existing.accountId,
|
|
conversationId: existing.conversationId,
|
|
parentConversationId: existing.parentConversationId,
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!rawTo) {
|
|
return null;
|
|
}
|
|
if (!normalizedTarget) {
|
|
return null;
|
|
}
|
|
|
|
if (threadId) {
|
|
if (!isChatTarget) {
|
|
return null;
|
|
}
|
|
return {
|
|
accountId: manager.accountId,
|
|
conversationId: buildFeishuConversationId({
|
|
chatId: normalizedTarget,
|
|
scope: "group_topic",
|
|
topicId: threadId,
|
|
}),
|
|
parentConversationId: normalizedTarget,
|
|
};
|
|
}
|
|
|
|
if (isChatTarget) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
accountId: manager.accountId,
|
|
conversationId: normalizedTarget,
|
|
};
|
|
}
|
|
|
|
function resolveFeishuDeliveryOrigin(params: {
|
|
conversationId: string;
|
|
parentConversationId?: string;
|
|
accountId: string;
|
|
deliveryTo?: string;
|
|
deliveryThreadId?: string;
|
|
}): {
|
|
channel: "feishu";
|
|
accountId: string;
|
|
to: string;
|
|
threadId?: string;
|
|
} {
|
|
const deliveryTo = params.deliveryTo?.trim();
|
|
const deliveryThreadId = params.deliveryThreadId?.trim();
|
|
if (deliveryTo) {
|
|
return {
|
|
channel: "feishu",
|
|
accountId: params.accountId,
|
|
to: deliveryTo,
|
|
...(deliveryThreadId ? { threadId: deliveryThreadId } : {}),
|
|
};
|
|
}
|
|
const parsed = parseFeishuConversationId({
|
|
conversationId: params.conversationId,
|
|
parentConversationId: params.parentConversationId,
|
|
});
|
|
if (parsed?.topicId) {
|
|
return {
|
|
channel: "feishu",
|
|
accountId: params.accountId,
|
|
to: `chat:${params.parentConversationId?.trim() || parsed.chatId}`,
|
|
threadId: parsed.topicId,
|
|
};
|
|
}
|
|
return {
|
|
channel: "feishu",
|
|
accountId: params.accountId,
|
|
to: `user:${params.conversationId}`,
|
|
};
|
|
}
|
|
|
|
function resolveMatchingChildBinding(params: {
|
|
accountId?: string;
|
|
childSessionKey: string;
|
|
requesterSessionKey?: string;
|
|
requesterOrigin?: {
|
|
to?: string;
|
|
threadId?: string | number;
|
|
};
|
|
}) {
|
|
const manager = getFeishuThreadBindingManager(params.accountId);
|
|
if (!manager) {
|
|
return null;
|
|
}
|
|
const childBindings = manager.listBySessionKey(params.childSessionKey.trim());
|
|
if (childBindings.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const requesterConversation = resolveFeishuRequesterConversation({
|
|
accountId: manager.accountId,
|
|
to: params.requesterOrigin?.to,
|
|
threadId: params.requesterOrigin?.threadId,
|
|
requesterSessionKey: params.requesterSessionKey,
|
|
});
|
|
if (requesterConversation) {
|
|
const matched = childBindings.find(
|
|
(entry) =>
|
|
entry.accountId === requesterConversation.accountId &&
|
|
entry.conversationId === requesterConversation.conversationId &&
|
|
(entry.parentConversationId?.trim() || undefined) ===
|
|
(requesterConversation.parentConversationId?.trim() || undefined),
|
|
);
|
|
if (matched) {
|
|
return matched;
|
|
}
|
|
}
|
|
|
|
return childBindings.length === 1 ? childBindings[0] : null;
|
|
}
|
|
|
|
export function registerFeishuSubagentHooks(api: OpenClawPluginApi) {
|
|
api.on("subagent_spawning", async (event, ctx) => {
|
|
if (!event.threadRequested) {
|
|
return;
|
|
}
|
|
const requesterChannel = event.requester?.channel?.trim().toLowerCase();
|
|
if (requesterChannel !== "feishu") {
|
|
return;
|
|
}
|
|
|
|
const manager = getFeishuThreadBindingManager(event.requester?.accountId);
|
|
if (!manager) {
|
|
return {
|
|
status: "error" as const,
|
|
error:
|
|
"Feishu current-conversation binding is unavailable because the Feishu account monitor is not active.",
|
|
};
|
|
}
|
|
|
|
const conversation = resolveFeishuRequesterConversation({
|
|
accountId: event.requester?.accountId,
|
|
to: event.requester?.to,
|
|
threadId: event.requester?.threadId,
|
|
requesterSessionKey: ctx.requesterSessionKey,
|
|
});
|
|
if (!conversation) {
|
|
return {
|
|
status: "error" as const,
|
|
error:
|
|
"Feishu current-conversation binding is only available in direct messages or topic conversations.",
|
|
};
|
|
}
|
|
|
|
try {
|
|
const binding = manager.bindConversation({
|
|
conversationId: conversation.conversationId,
|
|
parentConversationId: conversation.parentConversationId,
|
|
targetKind: "subagent",
|
|
targetSessionKey: event.childSessionKey,
|
|
metadata: {
|
|
agentId: event.agentId,
|
|
label: event.label,
|
|
boundBy: "system",
|
|
deliveryTo: event.requester?.to,
|
|
deliveryThreadId:
|
|
event.requester?.threadId != null && event.requester.threadId !== ""
|
|
? String(event.requester.threadId)
|
|
: undefined,
|
|
},
|
|
});
|
|
if (!binding) {
|
|
return {
|
|
status: "error" as const,
|
|
error:
|
|
"Unable to bind this Feishu conversation to the spawned subagent session. Session mode is unavailable for this target.",
|
|
};
|
|
}
|
|
return {
|
|
status: "ok" as const,
|
|
threadBindingReady: true,
|
|
};
|
|
} catch (err) {
|
|
return {
|
|
status: "error" as const,
|
|
error: `Feishu conversation bind failed: ${summarizeError(err)}`,
|
|
};
|
|
}
|
|
});
|
|
|
|
api.on("subagent_delivery_target", (event) => {
|
|
if (!event.expectsCompletionMessage) {
|
|
return;
|
|
}
|
|
const requesterChannel = event.requesterOrigin?.channel?.trim().toLowerCase();
|
|
if (requesterChannel !== "feishu") {
|
|
return;
|
|
}
|
|
|
|
const binding = resolveMatchingChildBinding({
|
|
accountId: event.requesterOrigin?.accountId,
|
|
childSessionKey: event.childSessionKey,
|
|
requesterSessionKey: event.requesterSessionKey,
|
|
requesterOrigin: {
|
|
to: event.requesterOrigin?.to,
|
|
threadId: event.requesterOrigin?.threadId,
|
|
},
|
|
});
|
|
if (!binding) {
|
|
return;
|
|
}
|
|
|
|
return {
|
|
origin: resolveFeishuDeliveryOrigin({
|
|
conversationId: binding.conversationId,
|
|
parentConversationId: binding.parentConversationId,
|
|
accountId: binding.accountId,
|
|
deliveryTo: binding.deliveryTo,
|
|
deliveryThreadId: binding.deliveryThreadId,
|
|
}),
|
|
};
|
|
});
|
|
|
|
api.on("subagent_ended", (event) => {
|
|
const manager = getFeishuThreadBindingManager(event.accountId);
|
|
manager?.unbindBySessionKey(event.targetSessionKey);
|
|
});
|
|
}
|