package vip.mate.hook;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import vip.mate.hook.model.HookEntity;
import vip.mate.hook.repository.HookMapper;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Hook 注册表:按 {@code event_type} 建立 O(1) 索引,在派发器热路径被读。
*
*
加载来源 M3 只支持 DB;YAML 文件加载放 M3.x。索引结构:
*
* event_type ──► List<HookMatch>(预装配的 action 实例)
*
* 支持完全匹配 + 前缀通配(例 {@code tool:*} 匹配所有 tool:xxx 事件)。
*
* 索引在启动时一次性构建;运行时修改通过 {@link #reload()} 重建(UI CRUD 后调用)。
* 读路径完全无锁({@link ConcurrentHashMap} 的 get 只读)。
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class HookRegistry {
private final HookMapper hookMapper;
private final HookActionFactory actionFactory;
/** 精确匹配索引:event_type → matches。 */
private final ConcurrentHashMap> exactIndex = new ConcurrentHashMap<>();
/** 通配索引:domain(agent / tool / ...)→ matches(当条目 event_type 形如 'tool:*')。 */
private final ConcurrentHashMap> wildcardIndex = new ConcurrentHashMap<>();
@PostConstruct
public void init() {
reload();
}
/** Spring 上下文完成后再次 reload,确保 Flyway 迁移先行。 */
@EventListener(ContextRefreshedEvent.class)
public void onContextRefreshed() {
reload();
}
public synchronized void reload() {
var wrapper = new LambdaQueryWrapper()
.eq(HookEntity::getEnabled, true);
List all;
try {
all = hookMapper.selectList(wrapper);
} catch (Exception e) {
log.warn("hook registry reload skipped (table not ready?): {}", e.getMessage());
return;
}
var nextExact = new ConcurrentHashMap>();
var nextWild = new ConcurrentHashMap>();
int ok = 0, bad = 0;
for (HookEntity e : all) {
try {
var match = new HookMatch(e, actionFactory.build(e));
String type = e.getEventType();
if (type.endsWith(":*")) {
String domain = type.substring(0, type.length() - 2);
nextWild.computeIfAbsent(domain, k -> new java.util.ArrayList<>()).add(match);
} else {
nextExact.computeIfAbsent(type, k -> new java.util.ArrayList<>()).add(match);
}
ok++;
} catch (Exception ex) {
log.warn("skip invalid hook id={} name={}: {}", e.getId(), e.getName(), ex.getMessage());
bad++;
}
}
this.exactIndex.clear();
this.exactIndex.putAll(nextExact);
this.wildcardIndex.clear();
this.wildcardIndex.putAll(nextWild);
log.info("hook registry loaded: {} hooks ({} skipped)", ok, bad);
}
/** 查匹配 hook。O(1) + 小列表线性合并。 */
public List match(String eventType) {
var exact = exactIndex.get(eventType);
int colon = eventType.indexOf(':');
String domain = (colon > 0) ? eventType.substring(0, colon) : eventType;
var wild = wildcardIndex.get(domain);
if (exact == null && wild == null) return List.of();
if (exact == null) return wild;
if (wild == null) return exact;
var combined = new java.util.ArrayList(exact.size() + wild.size());
combined.addAll(exact);
combined.addAll(wild);
return combined;
}
/** 仅测试 / 观察用。 */
public Map> exactIndexSnapshot() {
return Map.copyOf(exactIndex);
}
}