| |
| |
| |
| |
| |
| |
|
|
| import type { WorkspaceBootstrapFile } from "../agents/workspace.js"; |
| import type { CliDeps } from "../cli/deps.js"; |
| import type { OpenClawConfig } from "../config/config.js"; |
| import { createSubsystemLogger } from "../logging/subsystem.js"; |
|
|
| export type InternalHookEventType = "command" | "session" | "agent" | "gateway" | "message"; |
|
|
| export type AgentBootstrapHookContext = { |
| workspaceDir: string; |
| bootstrapFiles: WorkspaceBootstrapFile[]; |
| cfg?: OpenClawConfig; |
| sessionKey?: string; |
| sessionId?: string; |
| agentId?: string; |
| }; |
|
|
| export type AgentBootstrapHookEvent = InternalHookEvent & { |
| type: "agent"; |
| action: "bootstrap"; |
| context: AgentBootstrapHookContext; |
| }; |
|
|
| export type GatewayStartupHookContext = { |
| cfg?: OpenClawConfig; |
| deps?: CliDeps; |
| workspaceDir?: string; |
| }; |
|
|
| export type GatewayStartupHookEvent = InternalHookEvent & { |
| type: "gateway"; |
| action: "startup"; |
| context: GatewayStartupHookContext; |
| }; |
|
|
| |
| |
| |
|
|
| export type MessageReceivedHookContext = { |
| |
| from: string; |
| |
| content: string; |
| |
| timestamp?: number; |
| |
| channelId: string; |
| |
| accountId?: string; |
| |
| conversationId?: string; |
| |
| messageId?: string; |
| |
| metadata?: Record<string, unknown>; |
| }; |
|
|
| export type MessageReceivedHookEvent = InternalHookEvent & { |
| type: "message"; |
| action: "received"; |
| context: MessageReceivedHookContext; |
| }; |
|
|
| export type MessageSentHookContext = { |
| |
| to: string; |
| |
| content: string; |
| |
| success: boolean; |
| |
| error?: string; |
| |
| channelId: string; |
| |
| accountId?: string; |
| |
| conversationId?: string; |
| |
| messageId?: string; |
| |
| isGroup?: boolean; |
| |
| groupId?: string; |
| }; |
|
|
| export type MessageSentHookEvent = InternalHookEvent & { |
| type: "message"; |
| action: "sent"; |
| context: MessageSentHookContext; |
| }; |
|
|
| type MessageEnrichedBodyHookContext = { |
| |
| from?: string; |
| |
| to?: string; |
| |
| body?: string; |
| |
| bodyForAgent?: string; |
| |
| timestamp?: number; |
| |
| channelId: string; |
| |
| conversationId?: string; |
| |
| messageId?: string; |
| |
| senderId?: string; |
| |
| senderName?: string; |
| |
| senderUsername?: string; |
| |
| provider?: string; |
| |
| surface?: string; |
| |
| mediaPath?: string; |
| |
| mediaType?: string; |
| }; |
|
|
| export type MessageTranscribedHookContext = MessageEnrichedBodyHookContext & { |
| |
| transcript: string; |
| }; |
|
|
| export type MessageTranscribedHookEvent = InternalHookEvent & { |
| type: "message"; |
| action: "transcribed"; |
| context: MessageTranscribedHookContext; |
| }; |
|
|
| export type MessagePreprocessedHookContext = MessageEnrichedBodyHookContext & { |
| |
| transcript?: string; |
| |
| isGroup?: boolean; |
| |
| groupId?: string; |
| }; |
|
|
| export type MessagePreprocessedHookEvent = InternalHookEvent & { |
| type: "message"; |
| action: "preprocessed"; |
| context: MessagePreprocessedHookContext; |
| }; |
|
|
| export interface InternalHookEvent { |
| |
| type: InternalHookEventType; |
| |
| action: string; |
| |
| sessionKey: string; |
| |
| context: Record<string, unknown>; |
| |
| timestamp: Date; |
| |
| messages: string[]; |
| } |
|
|
| export type InternalHookHandler = (event: InternalHookEvent) => Promise<void> | void; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| const _g = globalThis as typeof globalThis & { |
| __openclaw_internal_hook_handlers__?: Map<string, InternalHookHandler[]>; |
| }; |
| const handlers = (_g.__openclaw_internal_hook_handlers__ ??= new Map< |
| string, |
| InternalHookHandler[] |
| >()); |
| const log = createSubsystemLogger("internal-hooks"); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function registerInternalHook(eventKey: string, handler: InternalHookHandler): void { |
| if (!handlers.has(eventKey)) { |
| handlers.set(eventKey, []); |
| } |
| handlers.get(eventKey)!.push(handler); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function unregisterInternalHook(eventKey: string, handler: InternalHookHandler): void { |
| const eventHandlers = handlers.get(eventKey); |
| if (!eventHandlers) { |
| return; |
| } |
|
|
| const index = eventHandlers.indexOf(handler); |
| if (index !== -1) { |
| eventHandlers.splice(index, 1); |
| } |
|
|
| |
| if (eventHandlers.length === 0) { |
| handlers.delete(eventKey); |
| } |
| } |
|
|
| |
| |
| |
| export function clearInternalHooks(): void { |
| handlers.clear(); |
| } |
|
|
| |
| |
| |
| export function getRegisteredEventKeys(): string[] { |
| return Array.from(handlers.keys()); |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function triggerInternalHook(event: InternalHookEvent): Promise<void> { |
| const typeHandlers = handlers.get(event.type) ?? []; |
| const specificHandlers = handlers.get(`${event.type}:${event.action}`) ?? []; |
|
|
| const allHandlers = [...typeHandlers, ...specificHandlers]; |
|
|
| if (allHandlers.length === 0) { |
| return; |
| } |
|
|
| for (const handler of allHandlers) { |
| try { |
| await handler(event); |
| } catch (err) { |
| const message = err instanceof Error ? err.message : String(err); |
| log.error(`Hook error [${event.type}:${event.action}]: ${message}`); |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export function createInternalHookEvent( |
| type: InternalHookEventType, |
| action: string, |
| sessionKey: string, |
| context: Record<string, unknown> = {}, |
| ): InternalHookEvent { |
| return { |
| type, |
| action, |
| sessionKey, |
| context, |
| timestamp: new Date(), |
| messages: [], |
| }; |
| } |
|
|
| function isHookEventTypeAndAction( |
| event: InternalHookEvent, |
| type: InternalHookEventType, |
| action: string, |
| ): boolean { |
| return event.type === type && event.action === action; |
| } |
|
|
| function getHookContext<T extends Record<string, unknown>>( |
| event: InternalHookEvent, |
| ): Partial<T> | null { |
| const context = event.context as Partial<T> | null; |
| if (!context || typeof context !== "object") { |
| return null; |
| } |
| return context; |
| } |
|
|
| function hasStringContextField<T extends Record<string, unknown>>( |
| context: Partial<T>, |
| key: keyof T, |
| ): boolean { |
| return typeof context[key] === "string"; |
| } |
|
|
| function hasBooleanContextField<T extends Record<string, unknown>>( |
| context: Partial<T>, |
| key: keyof T, |
| ): boolean { |
| return typeof context[key] === "boolean"; |
| } |
|
|
| export function isAgentBootstrapEvent(event: InternalHookEvent): event is AgentBootstrapHookEvent { |
| if (!isHookEventTypeAndAction(event, "agent", "bootstrap")) { |
| return false; |
| } |
| const context = getHookContext<AgentBootstrapHookContext>(event); |
| if (!context) { |
| return false; |
| } |
| if (!hasStringContextField(context, "workspaceDir")) { |
| return false; |
| } |
| return Array.isArray(context.bootstrapFiles); |
| } |
|
|
| export function isGatewayStartupEvent(event: InternalHookEvent): event is GatewayStartupHookEvent { |
| if (!isHookEventTypeAndAction(event, "gateway", "startup")) { |
| return false; |
| } |
| return Boolean(getHookContext<GatewayStartupHookContext>(event)); |
| } |
|
|
| export function isMessageReceivedEvent( |
| event: InternalHookEvent, |
| ): event is MessageReceivedHookEvent { |
| if (!isHookEventTypeAndAction(event, "message", "received")) { |
| return false; |
| } |
| const context = getHookContext<MessageReceivedHookContext>(event); |
| if (!context) { |
| return false; |
| } |
| return hasStringContextField(context, "from") && hasStringContextField(context, "channelId"); |
| } |
|
|
| export function isMessageSentEvent(event: InternalHookEvent): event is MessageSentHookEvent { |
| if (!isHookEventTypeAndAction(event, "message", "sent")) { |
| return false; |
| } |
| const context = getHookContext<MessageSentHookContext>(event); |
| if (!context) { |
| return false; |
| } |
| return ( |
| hasStringContextField(context, "to") && |
| hasStringContextField(context, "channelId") && |
| hasBooleanContextField(context, "success") |
| ); |
| } |
|
|
| export function isMessageTranscribedEvent( |
| event: InternalHookEvent, |
| ): event is MessageTranscribedHookEvent { |
| if (!isHookEventTypeAndAction(event, "message", "transcribed")) { |
| return false; |
| } |
| const context = getHookContext<MessageTranscribedHookContext>(event); |
| if (!context) { |
| return false; |
| } |
| return ( |
| hasStringContextField(context, "transcript") && hasStringContextField(context, "channelId") |
| ); |
| } |
|
|
| export function isMessagePreprocessedEvent( |
| event: InternalHookEvent, |
| ): event is MessagePreprocessedHookEvent { |
| if (!isHookEventTypeAndAction(event, "message", "preprocessed")) { |
| return false; |
| } |
| const context = getHookContext<MessagePreprocessedHookContext>(event); |
| if (!context) { |
| return false; |
| } |
| return hasStringContextField(context, "channelId"); |
| } |
|
|