refactor(feishu): lazy-load subagent hooks

This commit is contained in:
Vincent Koc
2026-04-03 23:18:38 +09:00
parent 52008e2e60
commit 78fb352506
2 changed files with 155 additions and 97 deletions

View File

@@ -6,7 +6,6 @@ import { registerFeishuDocTools } from "./src/docx.js";
import { registerFeishuDriveTools } from "./src/drive.js";
import { registerFeishuPermTools } from "./src/perm.js";
import { setFeishuRuntime } from "./src/runtime.js";
import { registerFeishuSubagentHooks } from "./src/subagent-hooks.js";
import { registerFeishuWikiTools } from "./src/wiki.js";
export { feishuPlugin } from "./src/channel.js";
@@ -46,14 +45,21 @@ export {
} from "./src/mention.js";
type MonitorFeishuProvider = typeof import("./src/monitor.js").monitorFeishuProvider;
type FeishuSubagentHooksModule = typeof import("./src/subagent-hooks.js");
let feishuMonitorPromise: Promise<typeof import("./src/monitor.js")> | null = null;
let feishuSubagentHooksPromise: Promise<FeishuSubagentHooksModule> | null = null;
function loadFeishuMonitorModule() {
feishuMonitorPromise ??= import("./src/monitor.js");
return feishuMonitorPromise;
}
function loadFeishuSubagentHooksModule() {
feishuSubagentHooksPromise ??= import("./src/subagent-hooks.js");
return feishuSubagentHooksPromise;
}
export async function monitorFeishuProvider(
...args: Parameters<MonitorFeishuProvider>
): ReturnType<MonitorFeishuProvider> {
@@ -68,7 +74,18 @@ export default defineChannelPluginEntry({
plugin: feishuPlugin,
setRuntime: setFeishuRuntime,
registerFull(api) {
registerFeishuSubagentHooks(api);
api.on("subagent_spawning", async (event, ctx) => {
const { handleFeishuSubagentSpawning } = await loadFeishuSubagentHooksModule();
return await handleFeishuSubagentSpawning(event, ctx);
});
api.on("subagent_delivery_target", async (event) => {
const { handleFeishuSubagentDeliveryTarget } = await loadFeishuSubagentHooksModule();
return await handleFeishuSubagentDeliveryTarget(event);
});
api.on("subagent_ended", async (event) => {
const { handleFeishuSubagentEnded } = await loadFeishuSubagentHooksModule();
await handleFeishuSubagentEnded(event);
});
registerFeishuDocTools(api);
registerFeishuChatTools(api);
registerFeishuWikiTools(api);

View File

@@ -232,110 +232,151 @@ function resolveMatchingChildBinding(params: {
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;
}
type FeishuSubagentContext = {
requesterSessionKey?: string;
};
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.",
};
}
type FeishuSubagentSpawningEvent = {
threadRequested?: boolean;
requester?: {
channel?: string;
accountId?: string;
to?: string;
threadId?: string | number;
};
childSessionKey: string;
agentId?: string;
label?: string;
};
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.",
};
}
type FeishuSubagentDeliveryTargetEvent = {
expectsCompletionMessage?: boolean;
requesterOrigin?: {
channel?: string;
accountId?: string;
to?: string;
threadId?: string | number;
};
childSessionKey: string;
requesterSessionKey?: string;
};
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)}`,
};
}
type FeishuSubagentEndedEvent = {
accountId?: string;
targetSessionKey: string;
};
export async function handleFeishuSubagentSpawning(
event: FeishuSubagentSpawningEvent,
ctx: FeishuSubagentContext,
) {
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.",
};
}
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,
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;
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 {
origin: resolveFeishuDeliveryOrigin({
conversationId: binding.conversationId,
parentConversationId: binding.parentConversationId,
accountId: binding.accountId,
deliveryTo: binding.deliveryTo,
deliveryThreadId: binding.deliveryThreadId,
}),
status: "ok" as const,
threadBindingReady: true,
};
});
api.on("subagent_ended", (event) => {
const manager = getFeishuThreadBindingManager(event.accountId);
manager?.unbindBySessionKey(event.targetSessionKey);
});
} catch (err) {
return {
status: "error" as const,
error: `Feishu conversation bind failed: ${summarizeError(err)}`,
};
}
}
export function handleFeishuSubagentDeliveryTarget(event: FeishuSubagentDeliveryTargetEvent) {
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,
}),
};
}
export function handleFeishuSubagentEnded(event: FeishuSubagentEndedEvent) {
const manager = getFeishuThreadBindingManager(event.accountId);
manager?.unbindBySessionKey(event.targetSessionKey);
}
export function registerFeishuSubagentHooks(api: OpenClawPluginApi) {
api.on("subagent_spawning", (event, ctx) => handleFeishuSubagentSpawning(event, ctx));
api.on("subagent_delivery_target", (event) => handleFeishuSubagentDeliveryTarget(event));
api.on("subagent_ended", (event) => handleFeishuSubagentEnded(event));
}