refactor(plugin): extract message_received hook and slim index imports
This commit is contained in:
115
plugin/hooks/message-received.ts
Normal file
115
plugin/hooks/message-received.ts
Normal file
@@ -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<string, unknown>, event: Record<string, unknown>) => Record<string, unknown>;
|
||||
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<string, string>;
|
||||
};
|
||||
|
||||
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<string, unknown>;
|
||||
const e = (event || {}) as Record<string, unknown>;
|
||||
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<string, unknown>).metadata as Record<string, unknown> | undefined;
|
||||
const from =
|
||||
(typeof metadata?.senderId === "string" && metadata.senderId) ||
|
||||
(typeof (e as Record<string, unknown>).from === "string" ? ((e as Record<string, unknown>).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<string, unknown>).content as string) || ((e as Record<string, unknown>).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)}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
109
plugin/index.ts
109
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<string, unknown>;
|
||||
const e = (event || {}) as Record<string, unknown>;
|
||||
// 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<string, unknown>).metadata as Record<string, unknown> | undefined;
|
||||
const from = (typeof metadata?.senderId === "string" && metadata.senderId)
|
||||
|| (typeof (e as Record<string, unknown>).from === "string" ? (e as Record<string, unknown>).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<string, unknown>).content as string
|
||||
|| (e as Record<string, unknown>).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({
|
||||
|
||||
Reference in New Issue
Block a user