package vip.mate.tool.search; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import vip.mate.system.model.SystemSettingsDTO; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.stream.Collectors; /** * 搜索提供商注册表 — 收集所有 {@link SearchProvider} 实现,提供优先级排序与自动探测 * *
provider 自动探测机制: *
id 规则:不允许为空或含首尾空白(拒绝而非 trim——注册键必须与
* {@code provider.id()} 完全一致,反注册才能对得上);存储与查找大小写敏感,
* 但冲突检测大小写不敏感,防止 "Serper" 这类变体在 UI 上与内置 "serper" 混淆。
*
* @throws IllegalArgumentException id 为空、含首尾空白,或与内置/已注册插件 provider 冲突
*/
public void registerPluginProvider(SearchProvider provider) {
String id = provider.id();
if (id == null || id.isBlank()) {
throw new IllegalArgumentException("Search provider id must not be blank");
}
if (!id.equals(id.trim())) {
throw new IllegalArgumentException(
"Search provider id must not contain leading/trailing whitespace: '" + id + "'");
}
synchronized (registrationLock) {
if (containsIgnoreCase(providerMap.keySet(), id)) {
throw new IllegalArgumentException(
"Search provider id conflicts with a built-in provider: " + id);
}
if (containsIgnoreCase(pluginProviders.keySet(), id)) {
throw new IllegalArgumentException(
"Search provider id already registered by another plugin: " + id);
}
pluginProviders.put(id, provider);
}
log.info("插件搜索提供商已注册: {} (order={})", id, provider.autoDetectOrder());
}
private static boolean containsIgnoreCase(Set 有插件注册时每次调用重新合并排序——provider 总数 <10,无需缓存。
*/
public List 解析策略:
*
*
*
* @return 选中的 provider,或 null(完全无可用 provider)
*/
public ResolvedProvider resolve(SystemSettingsDTO config) {
// 1. 用户显式配置的 primary provider
String configuredId = config.getSearchProvider();
if (configuredId != null && !configuredId.isBlank()) {
SearchProvider configured = getById(configuredId);
if (configured != null && configured.isAvailable(config)) {
return new ResolvedProvider(configured, "configured");
}
}
// 2. 按优先级遍历,先找有 credential 的
SearchProvider keylessFallback = null;
for (SearchProvider p : allSorted()) {
if (!p.requiresCredential()) {
// 记住第一个可用的 keyless provider
if (keylessFallback == null && p.isAvailable(config)) {
keylessFallback = p;
}
continue;
}
if (p.isAvailable(config)) {
return new ResolvedProvider(p, "auto-detect");
}
}
// 3. 回退到 keyless
if (keylessFallback != null) {
return new ResolvedProvider(keylessFallback, "keyless-fallback");
}
return null;
}
/**
* 解析结果
*/
public record ResolvedProvider(SearchProvider provider, String source) {
}
}