Spaces:
Sleeping
Sleeping
File size: 4,892 Bytes
fb4d8fe | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | /**
* Dynamic loader for hook handlers
*
* Loads hook handlers from external modules based on configuration
* and from directory-based discovery (bundled, managed, workspace)
*/
import path from "node:path";
import { pathToFileURL } from "node:url";
import type { OpenClawConfig } from "../config/config.js";
import type { InternalHookHandler } from "./internal-hooks.js";
import { resolveHookConfig } from "./config.js";
import { shouldIncludeHook } from "./config.js";
import { registerInternalHook } from "./internal-hooks.js";
import { loadWorkspaceHookEntries } from "./workspace.js";
/**
* Load and register all hook handlers
*
* Loads hooks from both:
* 1. Directory-based discovery (bundled, managed, workspace)
* 2. Legacy config handlers (backwards compatibility)
*
* @param cfg - OpenClaw configuration
* @param workspaceDir - Workspace directory for hook discovery
* @returns Number of handlers successfully loaded
*
* @example
* ```ts
* const config = await loadConfig();
* const workspaceDir = resolveAgentWorkspaceDir(config, agentId);
* const count = await loadInternalHooks(config, workspaceDir);
* console.log(`Loaded ${count} hook handlers`);
* ```
*/
export async function loadInternalHooks(
cfg: OpenClawConfig,
workspaceDir: string,
): Promise<number> {
// Check if hooks are enabled
if (!cfg.hooks?.internal?.enabled) {
return 0;
}
let loadedCount = 0;
// 1. Load hooks from directories (new system)
try {
const hookEntries = loadWorkspaceHookEntries(workspaceDir, { config: cfg });
// Filter by eligibility
const eligible = hookEntries.filter((entry) => shouldIncludeHook({ entry, config: cfg }));
for (const entry of eligible) {
const hookConfig = resolveHookConfig(cfg, entry.hook.name);
// Skip if explicitly disabled in config
if (hookConfig?.enabled === false) {
continue;
}
try {
// Import handler module with cache-busting
const url = pathToFileURL(entry.hook.handlerPath).href;
const cacheBustedUrl = `${url}?t=${Date.now()}`;
const mod = (await import(cacheBustedUrl)) as Record<string, unknown>;
// Get handler function (default or named export)
const exportName = entry.metadata?.export ?? "default";
const handler = mod[exportName];
if (typeof handler !== "function") {
console.error(
`Hook error: Handler '${exportName}' from ${entry.hook.name} is not a function`,
);
continue;
}
// Register for all events listed in metadata
const events = entry.metadata?.events ?? [];
if (events.length === 0) {
console.warn(`Hook warning: Hook '${entry.hook.name}' has no events defined in metadata`);
continue;
}
for (const event of events) {
registerInternalHook(event, handler as InternalHookHandler);
}
console.log(
`Registered hook: ${entry.hook.name} -> ${events.join(", ")}${exportName !== "default" ? ` (export: ${exportName})` : ""}`,
);
loadedCount++;
} catch (err) {
console.error(
`Failed to load hook ${entry.hook.name}:`,
err instanceof Error ? err.message : String(err),
);
}
}
} catch (err) {
console.error(
"Failed to load directory-based hooks:",
err instanceof Error ? err.message : String(err),
);
}
// 2. Load legacy config handlers (backwards compatibility)
const handlers = cfg.hooks.internal.handlers ?? [];
for (const handlerConfig of handlers) {
try {
// Resolve module path (absolute or relative to cwd)
const modulePath = path.isAbsolute(handlerConfig.module)
? handlerConfig.module
: path.join(process.cwd(), handlerConfig.module);
// Import the module with cache-busting to ensure fresh reload
const url = pathToFileURL(modulePath).href;
const cacheBustedUrl = `${url}?t=${Date.now()}`;
const mod = (await import(cacheBustedUrl)) as Record<string, unknown>;
// Get the handler function
const exportName = handlerConfig.export ?? "default";
const handler = mod[exportName];
if (typeof handler !== "function") {
console.error(`Hook error: Handler '${exportName}' from ${modulePath} is not a function`);
continue;
}
// Register the handler
registerInternalHook(handlerConfig.event, handler as InternalHookHandler);
console.log(
`Registered hook (legacy): ${handlerConfig.event} -> ${modulePath}${exportName !== "default" ? `#${exportName}` : ""}`,
);
loadedCount++;
} catch (err) {
console.error(
`Failed to load hook handler from ${handlerConfig.module}:`,
err instanceof Error ? err.message : String(err),
);
}
}
return loadedCount;
}
|