From b63c1dfe941f836fabec2ab0a82eede9c3612b5b Mon Sep 17 00:00:00 2001 From: orion Date: Sat, 7 Mar 2026 22:24:48 +0000 Subject: [PATCH] refactor(plugin): extract message_received hook and slim index imports --- plugin/hooks/message-received.ts | 115 +++++++++++++++++++++++++++++++ plugin/index.ts | 109 ++++------------------------- 2 files changed, 128 insertions(+), 96 deletions(-) create mode 100644 plugin/hooks/message-received.ts diff --git a/plugin/hooks/message-received.ts b/plugin/hooks/message-received.ts new file mode 100644 index 0000000..132ca95 --- /dev/null +++ b/plugin/hooks/message-received.ts @@ -0,0 +1,115 @@ +import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; +import { onNewMessage, setMentionOverride, getTurnDebugInfo } from "../turn-manager.js"; +import { extractDiscordChannelId } from "../channel-resolver.js"; +import type { DirigentConfig } from "../rules.js"; + +type DebugConfig = { + enableDebugLogs?: boolean; + debugLogChannelIds?: string[]; +}; + +type MessageReceivedDeps = { + api: OpenClawPluginApi; + baseConfig: DirigentConfig; + getLivePluginConfig: (api: OpenClawPluginApi, fallback: DirigentConfig) => DirigentConfig; + shouldDebugLog: (config: DirigentConfig & DebugConfig, channelId?: string) => boolean; + debugCtxSummary: (ctx: Record, event: Record) => Record; + ensureTurnOrder: (api: OpenClawPluginApi, channelId: string) => void; + getModeratorUserId: (cfg: DirigentConfig) => string | undefined; + recordChannelAccount: (channelId: string, accountId: string) => boolean; + extractMentionedUserIds: (content: string) => string[]; + buildUserIdToAccountIdMap: (api: OpenClawPluginApi) => Map; +}; + +export function registerMessageReceivedHook(deps: MessageReceivedDeps): void { + const { + api, + baseConfig, + getLivePluginConfig, + shouldDebugLog, + debugCtxSummary, + ensureTurnOrder, + getModeratorUserId, + recordChannelAccount, + extractMentionedUserIds, + buildUserIdToAccountIdMap, + } = deps; + + api.on("message_received", async (event, ctx) => { + try { + const c = (ctx || {}) as Record; + const e = (event || {}) as Record; + const preChannelId = extractDiscordChannelId(c, e); + const livePre = getLivePluginConfig(api, baseConfig as DirigentConfig) as DirigentConfig & DebugConfig; + if (shouldDebugLog(livePre, preChannelId)) { + api.logger.info(`dirigent: debug message_received preflight ctx=${JSON.stringify(debugCtxSummary(c, e))}`); + } + + if (preChannelId) { + ensureTurnOrder(api, preChannelId); + const metadata = (e as Record).metadata as Record | undefined; + const from = + (typeof metadata?.senderId === "string" && metadata.senderId) || + (typeof (e as Record).from === "string" ? ((e as Record).from as string) : ""); + + const moderatorUserId = getModeratorUserId(livePre); + if (moderatorUserId && from === moderatorUserId) { + if (shouldDebugLog(livePre, preChannelId)) { + api.logger.info(`dirigent: ignoring moderator message in channel=${preChannelId}`); + } + } else { + const humanList = livePre.humanList || livePre.bypassUserIds || []; + const isHuman = humanList.includes(from); + const senderAccountId = typeof c.accountId === "string" ? c.accountId : undefined; + + if (senderAccountId && senderAccountId !== "default") { + const isNew = recordChannelAccount(preChannelId, senderAccountId); + if (isNew) { + ensureTurnOrder(api, preChannelId); + api.logger.info(`dirigent: new account ${senderAccountId} seen in channel=${preChannelId}, turn order updated`); + } + } + + if (isHuman) { + const messageContent = ((e as Record).content as string) || ((e as Record).text as string) || ""; + const mentionedUserIds = extractMentionedUserIds(messageContent); + + if (mentionedUserIds.length > 0) { + const userIdMap = buildUserIdToAccountIdMap(api); + const mentionedAccountIds = mentionedUserIds.map((uid) => userIdMap.get(uid)).filter((aid): aid is string => !!aid); + + if (mentionedAccountIds.length > 0) { + ensureTurnOrder(api, preChannelId); + const overrideSet = setMentionOverride(preChannelId, mentionedAccountIds); + if (overrideSet) { + api.logger.info( + `dirigent: mention override set channel=${preChannelId} mentionedAgents=${JSON.stringify(mentionedAccountIds)}`, + ); + if (shouldDebugLog(livePre, preChannelId)) { + api.logger.info(`dirigent: turn state after override: ${JSON.stringify(getTurnDebugInfo(preChannelId))}`); + } + } else { + onNewMessage(preChannelId, senderAccountId, isHuman); + } + } else { + onNewMessage(preChannelId, senderAccountId, isHuman); + } + } else { + onNewMessage(preChannelId, senderAccountId, isHuman); + } + } else { + onNewMessage(preChannelId, senderAccountId, isHuman); + } + + if (shouldDebugLog(livePre, preChannelId)) { + api.logger.info( + `dirigent: turn onNewMessage channel=${preChannelId} from=${from} isHuman=${isHuman} accountId=${senderAccountId ?? "unknown"}`, + ); + } + } + } + } catch (err) { + api.logger.warn(`dirigent: message hook failed: ${String(err)}`); + } + }); +} diff --git a/plugin/index.ts b/plugin/index.ts index 81abead..060ff7d 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -3,9 +3,9 @@ import path from "node:path"; import { spawn, type ChildProcess } from "node:child_process"; import type { OpenClawPluginApi } from "openclaw/plugin-sdk"; import { evaluateDecision, resolvePolicy, type ChannelPolicy, type Decision, type DirigentConfig } from "./rules.js"; -import { checkTurn, advanceTurn, resetTurn, onNewMessage, onSpeakerDone, initTurnOrder, getTurnDebugInfo, setMentionOverride, hasMentionOverride, setWaitingForHuman, isWaitingForHuman } from "./turn-manager.js"; +import { advanceTurn, resetTurn, initTurnOrder, getTurnDebugInfo } from "./turn-manager.js"; import { startModeratorPresence, stopModeratorPresence } from "./moderator-presence.js"; -import { extractDiscordChannelId } from "./channel-resolver.js"; +import { registerMessageReceivedHook } from "./hooks/message-received.js"; import { registerBeforeModelResolveHook } from "./hooks/before-model-resolve.js"; import { registerBeforePromptBuildHook } from "./hooks/before-prompt-build.js"; import { registerBeforeMessageWriteHook } from "./hooks/before-message-write.js"; @@ -699,100 +699,17 @@ export default { // Turn management is handled internally by the plugin (not exposed as tools). // Use `/dirigent turn-status`, `/dirigent turn-advance`, `/dirigent turn-reset` for manual control. - api.on("message_received", async (event, ctx) => { - try { - const c = (ctx || {}) as Record; - const e = (event || {}) as Record; - // ctx.channelId is the platform name (e.g. "discord"), NOT the Discord channel snowflake. - // Extract the real Discord channel ID from conversationId or event.to. - const preChannelId = extractDiscordChannelId(c, e); - const livePre = getLivePluginConfig(api, baseConfig as DirigentConfig) as DirigentConfig & DebugConfig; - if (shouldDebugLog(livePre, preChannelId)) { - api.logger.info(`dirigent: debug message_received preflight ctx=${JSON.stringify(debugCtxSummary(c, e))}`); - } - - // Turn management on message received - if (preChannelId) { - ensureTurnOrder(api, preChannelId); - // event.from is often the channel target (e.g. "discord:channel:xxx"), NOT the sender. - // The actual sender ID is in event.metadata.senderId. - const metadata = (e as Record).metadata as Record | undefined; - const from = (typeof metadata?.senderId === "string" && metadata.senderId) - || (typeof (e as Record).from === "string" ? (e as Record).from as string : ""); - - // Ignore moderator bot messages — they don't affect turn state - const moderatorUserId = getModeratorUserId(livePre); - if (moderatorUserId && from === moderatorUserId) { - if (shouldDebugLog(livePre, preChannelId)) { - api.logger.info(`dirigent: ignoring moderator message in channel=${preChannelId}`); - } - // Don't call onNewMessage — moderator messages are transparent to turn logic - } else { - const humanList = livePre.humanList || livePre.bypassUserIds || []; - const isHuman = humanList.includes(from); - const senderAccountId = typeof c.accountId === "string" ? c.accountId : undefined; - - // Track which bot accounts are present in this channel - if (senderAccountId && senderAccountId !== "default") { - const isNew = recordChannelAccount(preChannelId, senderAccountId); - if (isNew) { - // Re-initialize turn order with updated channel membership - ensureTurnOrder(api, preChannelId); - api.logger.info(`dirigent: new account ${senderAccountId} seen in channel=${preChannelId}, turn order updated`); - } - } - - // Human @mention override: when a human mentions specific agents, - // temporarily override the turn order to only those agents. - if (isHuman) { - const messageContent = (e as Record).content as string - || (e as Record).text as string - || ""; - const mentionedUserIds = extractMentionedUserIds(messageContent); - - if (mentionedUserIds.length > 0) { - // Build reverse map: userId → accountId - const userIdMap = buildUserIdToAccountIdMap(api); - // Exclude moderator bot from mention targets - const mentionedAccountIds = mentionedUserIds - .map(uid => userIdMap.get(uid)) - .filter((aid): aid is string => !!aid); - - if (mentionedAccountIds.length > 0) { - ensureTurnOrder(api, preChannelId); - const overrideSet = setMentionOverride(preChannelId, mentionedAccountIds); - if (overrideSet) { - api.logger.info( - `dirigent: mention override set channel=${preChannelId} mentionedAgents=${JSON.stringify(mentionedAccountIds)}`, - ); - if (shouldDebugLog(livePre, preChannelId)) { - api.logger.info(`dirigent: turn state after override: ${JSON.stringify(getTurnDebugInfo(preChannelId))}`); - } - // Skip normal onNewMessage — override already set currentSpeaker - } else { - // No valid agents in mentions, fall through to normal handling - onNewMessage(preChannelId, senderAccountId, isHuman); - } - } else { - // Mentioned users aren't agents, normal human message - onNewMessage(preChannelId, senderAccountId, isHuman); - } - } else { - // No mentions, normal human message - onNewMessage(preChannelId, senderAccountId, isHuman); - } - } else { - onNewMessage(preChannelId, senderAccountId, isHuman); - } - - if (shouldDebugLog(livePre, preChannelId)) { - api.logger.info(`dirigent: turn onNewMessage channel=${preChannelId} from=${from} isHuman=${isHuman} accountId=${senderAccountId ?? "unknown"}`); - } - } - } - } catch (err) { - api.logger.warn(`dirigent: message hook failed: ${String(err)}`); - } + registerMessageReceivedHook({ + api, + baseConfig: baseConfig as DirigentConfig, + getLivePluginConfig, + shouldDebugLog, + debugCtxSummary, + ensureTurnOrder, + getModeratorUserId, + recordChannelAccount, + extractMentionedUserIds, + buildUserIdToAccountIdMap, }); registerBeforeModelResolveHook({