autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
342 lines (341 loc) • 15.4 kB
JavaScript
/**
* SkillHooks — Skill 生命周期钩子管理器 (v2)
*
* 每个 Skill 目录可以包含一个 hooks.js 文件,导出生命周期回调。
* SkillHooks 在启动时扫描并注册所有钩子,在特定事件发生时按模式调用。
*
* v2 升级:
* - 扩展到 16+ 钩子,覆盖知识/Guard/Skill/搜索/推荐/Bootstrap/信号全生命周期
* - 4 种执行模式: series / parallel / waterfall / bail
* - Handler 支持 priority、timeout、name 元数据
* - 完全向后兼容旧版 hooks.js (直接导出函数)
* - 新格式支持: export default { hooks: { onXxx: { handler, priority, timeout } } }
*
* 加载顺序: 内置 skills/ → 项目级 AutoSnippet/skills/(同名覆盖)
*/
import fs from 'node:fs';
import path from 'node:path';
import { resolveProjectRoot } from '#shared/resolveProjectRoot.js';
import { getProjectSkillsPath } from '../../infrastructure/config/Paths.js';
import Logger from '../../infrastructure/logging/Logger.js';
import { SKILLS_DIR } from '../../shared/package-root.js';
// ═══════════════════════════════════════════════════════
// Hook Registry — 声明所有支持的钩子及其执行模式
// ═══════════════════════════════════════════════════════
const HOOK_REGISTRY = [
// ── 知识生命周期 ──
{ name: 'onKnowledgeSubmit', mode: 'bail', description: '知识提交前拦截' },
{ name: 'onKnowledgeCreated', mode: 'parallel', description: '知识创建后通知' },
{ name: 'onKnowledgeUpdated', mode: 'parallel', description: '知识更新后通知' },
{ name: 'onKnowledgeExpired', mode: 'parallel', description: '知识过期/废弃后通知' },
// ── Guard ──
{ name: 'onGuardCheck', mode: 'waterfall', description: 'Guard 检查,可修改违规结果' },
{ name: 'onGuardViolation', mode: 'parallel', description: 'Guard 违规后通知' },
// ── Skill 生命周期 ──
{ name: 'onSkillLoad', mode: 'series', description: 'Skill 被加载时' },
{ name: 'onSkillCreated', mode: 'parallel', description: 'Skill 创建后' },
{ name: 'onSkillExpired', mode: 'parallel', description: 'Skill 过期/删除后' },
// ── 搜索 ──
{ name: 'onSearch', mode: 'waterfall', description: '搜索结果后处理(可修改排序)' },
{ name: 'onSearchMiss', mode: 'parallel', description: '搜索无结果时' },
// ── 推荐 ──
{ name: 'onRecommendation', mode: 'waterfall', description: '推荐结果后处理(可过滤/排序)' },
{ name: 'onRecommendFeedback', mode: 'parallel', description: '推荐反馈事件' },
// ── Bootstrap ──
{ name: 'onBootstrapStart', mode: 'series', description: '冷启动开始前' },
{ name: 'onBootstrapComplete', mode: 'parallel', description: '冷启动完成后' },
// ── 信号 ──
{ name: 'onSignalCollected', mode: 'parallel', description: '新信号收集完成' },
// ── 向后兼容 (旧名映射) ──
{ name: 'onCandidateSubmit', mode: 'bail', description: '(compat) 同 onKnowledgeSubmit' },
{ name: 'onRecipeCreated', mode: 'parallel', description: '(compat) Recipe 创建后通知' },
];
/** hookName → HookDefinition 快速查表 */
const HOOK_DEF_MAP = new Map(HOOK_REGISTRY.map((d) => [d.name, d]));
/** 所有合法 hook 名称集合 */
const HOOK_NAMES = new Set(HOOK_REGISTRY.map((d) => d.name));
/** 默认 handler 超时 (ms) */
const DEFAULT_HANDLER_TIMEOUT = 10_000;
/** 默认 handler 优先级 */
const DEFAULT_HANDLER_PRIORITY = 100;
/**
* 获取项目级 Skills 目录(运行时动态解析)
* 路径: {projectRoot}/AutoSnippet/skills/
*/
function _getProjectSkillsDir(container) {
const projectRoot = resolveProjectRoot(container);
return getProjectSkillsPath(projectRoot);
}
/** 带超时的 Promise 包装 */
function withTimeout(promise, ms, label) {
if (ms <= 0) {
return promise;
}
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`Hook handler "${label}" timed out after ${ms}ms`));
}, ms);
promise.then((v) => {
clearTimeout(timer);
resolve(v);
}, (e) => {
clearTimeout(timer);
reject(e);
});
});
}
// ═══════════════════════════════════════════════════════
// SkillHooks 主类
// ═══════════════════════════════════════════════════════
export class SkillHooks {
hooks;
logger;
constructor() {
this.logger = Logger.getInstance();
this.hooks = new Map([...HOOK_NAMES].map((n) => [n, []]));
}
// ─── 公共 API ──────────────────────────────────────────
/**
* 扫描 skills 目录,加载所有 hooks.js
* 项目级 hooks 覆盖同名内置 hooks
*/
async load(container) {
const loaded = new Map();
// 1. 内置 skills
await this.#loadFromDir(SKILLS_DIR, loaded);
// 2. 项目级 skills(覆盖同名)
await this.#loadFromDir(_getProjectSkillsDir(container), loaded);
// 3. 注册所有钩子
for (const [skillName, mod] of loaded) {
this.#registerModule(skillName, mod);
}
// 4. 按优先级排序所有 hook 列表
for (const handlers of this.hooks.values()) {
handlers.sort((a, b) => a.priority - b.priority);
}
const totalHooks = [...this.hooks.values()].reduce((s, a) => s + a.length, 0);
if (totalHooks > 0) {
this.logger.info(`SkillHooks: loaded ${totalHooks} hooks from ${loaded.size} skills`);
}
}
/** 手动注册 handler (用于代码级注册,非 hooks.js) */
tap(hookName, handler, options) {
if (!HOOK_NAMES.has(hookName)) {
this.logger.warn(`SkillHooks.tap: unknown hook "${hookName}", registering dynamically`);
this.hooks.set(hookName, []);
}
const registered = {
fn: handler,
name: options?.name ?? 'anonymous',
priority: options?.priority ?? DEFAULT_HANDLER_PRIORITY,
timeout: options?.timeout ?? DEFAULT_HANDLER_TIMEOUT,
};
const handlers = this.hooks.get(hookName);
handlers.push(registered);
// 保持优先级排序
handlers.sort((a, b) => a.priority - b.priority);
}
/**
* 触发钩子 — 根据 hook 定义的模式自动选择执行策略
*
* 向后兼容: 旧版 run() 签名不变,行为维持一致。
* - bail 模式: 首个返回 { block: true } 的 handler 立即终止
* - waterfall 模式: 前一个 handler 的返回值传给下一个
* - parallel 模式: 所有 handler 并行执行 (fire-and-forget)
* - series 模式: 按优先级顺序串行执行,忽略返回值
*/
async run(hookName, ...args) {
const handlers = this.hooks.get(hookName);
if (!handlers || handlers.length === 0) {
return undefined;
}
const def = HOOK_DEF_MAP.get(hookName);
const mode = def?.mode ?? 'bail'; // 未知 hook 默认 bail (兼容旧行为)
switch (mode) {
case 'bail':
return this.#runBail(hookName, handlers, args);
case 'waterfall':
return this.#runWaterfall(hookName, handlers, args);
case 'parallel':
return this.#runParallel(hookName, handlers, args);
case 'series':
return this.#runSeries(hookName, handlers, args);
default:
return this.#runBail(hookName, handlers, args);
}
}
/** 检查是否有任何钩子注册 */
has(hookName) {
const handlers = this.hooks.get(hookName);
return handlers !== undefined && handlers.length > 0;
}
/** 获取指定 hook 的 handler 数量 */
count(hookName) {
return this.hooks.get(hookName)?.length ?? 0;
}
/** 获取已注册的所有 hook 名称 */
getRegisteredHooks() {
return [...this.hooks.entries()]
.filter(([, handlers]) => handlers.length > 0)
.map(([name]) => name);
}
/** 获取 Hook Registry 信息 (用于 Dashboard / 调试) */
static getHookRegistry() {
return HOOK_REGISTRY;
}
// ─── 执行模式实现 ─────────────────────────────────────
/** Bail 模式: 串行执行,首个返回 truthy/{block:true} 的 handler 终止链 */
async #runBail(hookName, handlers, args) {
let result;
for (const h of handlers) {
try {
const promise = Promise.resolve(h.fn(...args));
result = await withTimeout(promise, h.timeout, h.name);
// block 短路
if (result &&
typeof result === 'object' &&
'block' in result &&
result.block) {
return result;
}
}
catch (err) {
this.logger.warn(`SkillHook error in ${hookName} (handler: ${h.name})`, {
error: err instanceof Error ? err.message : String(err),
});
}
}
return result;
}
/** Waterfall 模式: 串行传值,前一个返回值替换 args[0] */
async #runWaterfall(hookName, handlers, args) {
let current = args[0];
const rest = args.slice(1);
for (const h of handlers) {
try {
const promise = Promise.resolve(h.fn(current, ...rest));
const result = await withTimeout(promise, h.timeout, h.name);
// 只有返回了有效值才替换
if (result !== undefined && result !== null) {
current = result;
}
}
catch (err) {
this.logger.warn(`SkillHook waterfall error in ${hookName} (handler: ${h.name})`, {
error: err instanceof Error ? err.message : String(err),
});
// waterfall 模式下出错继续传递当前值
}
}
return current;
}
/** Parallel 模式: 所有 handler 并行执行 (fire-and-forget) */
async #runParallel(hookName, handlers, args) {
const results = await Promise.allSettled(handlers.map((h) => {
const promise = Promise.resolve(h.fn(...args));
return withTimeout(promise, h.timeout, h.name);
}));
// 记录失败但不阻塞
for (let i = 0; i < results.length; i++) {
const r = results[i];
if (r.status === 'rejected') {
this.logger.warn(`SkillHook parallel error in ${hookName} (handler: ${handlers[i].name})`, {
error: r.reason instanceof Error ? r.reason.message : String(r.reason),
});
}
}
return undefined;
}
/** Series 模式: 按优先级顺序串行执行,忽略返回值 */
async #runSeries(hookName, handlers, args) {
for (const h of handlers) {
try {
const promise = Promise.resolve(h.fn(...args));
await withTimeout(promise, h.timeout, h.name);
}
catch (err) {
this.logger.warn(`SkillHook series error in ${hookName} (handler: ${h.name})`, {
error: err instanceof Error ? err.message : String(err),
});
}
}
return undefined;
}
// ─── 模块注册 ─────────────────────────────────────────
/**
* 注册一个 skill 模块的钩子 — 支持新旧两种格式
*
* 旧格式 (v1):
* export function onGuardCheck(violation, ctx) { ... }
*
* 新格式 (v2):
* export default { hooks: { onGuardCheck: { handler, priority, timeout } } }
*/
#registerModule(skillName, mod) {
// 尝试新格式: mod.hooks 是一个对象
const hooksObj = mod.hooks;
if (hooksObj && typeof hooksObj === 'object') {
for (const [hookName, config] of Object.entries(hooksObj)) {
if (!this.hooks.has(hookName)) {
this.logger.warn(`SkillHooks: ${skillName} exports unknown hook "${hookName}", skipping`);
continue;
}
if (typeof config === 'function') {
// 新格式但直接是函数: { hooks: { onXxx: fn } }
this.#addHandler(hookName, config, skillName);
}
else if (config && typeof config === 'object' && 'handler' in config) {
// 新格式含元数据: { hooks: { onXxx: { handler, priority, timeout } } }
const cfg = config;
this.#addHandler(hookName, cfg.handler, skillName, cfg.priority, cfg.timeout);
}
}
return;
}
// 旧格式: module 顶层直接导出函数
for (const hookName of HOOK_NAMES) {
if (typeof mod[hookName] === 'function') {
this.#addHandler(hookName, mod[hookName], skillName);
}
}
}
#addHandler(hookName, fn, skillName, priority, timeout) {
const handler = {
fn,
name: `${skillName}.${hookName}`,
priority: priority ?? DEFAULT_HANDLER_PRIORITY,
timeout: timeout ?? DEFAULT_HANDLER_TIMEOUT,
};
this.hooks.get(hookName).push(handler);
this.logger.debug(`SkillHook registered: ${handler.name} (priority=${handler.priority})`);
}
// ─── 目录扫描 ─────────────────────────────────────────
async #loadFromDir(dir, loaded) {
let dirs;
try {
dirs = fs
.readdirSync(dir, { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => d.name);
}
catch {
return; // 目录不存在
}
for (const name of dirs) {
const hooksPath = path.join(dir, name, 'hooks.js');
if (!fs.existsSync(hooksPath)) {
continue;
}
try {
const mod = await import(hooksPath);
loaded.set(name, mod.default || mod);
}
catch (err) {
this.logger.warn(`SkillHooks: failed to load ${name}/hooks.js`, {
error: err instanceof Error ? err.message : String(err),
});
}
}
}
}
export default SkillHooks;