mateclaw/mateclaw-server/src/main/java/vip/mate/plugin/PluginContextImpl.java
倪程伟 0a58b3fb35
feat(plugin): 插件化搜索 Provider — PluginType.SEARCH + PluginSearchProvider SPI (#477) (#479)
* docs: add plugin search provider design spec and plan (#477)

Claude-Session: https://claude.ai/code/session_013uyvXEazZkhNw27geRtakK

* feat(plugin-api): add SEARCH plugin type and PluginSearchProvider SPI (#477)

Claude-Session: https://claude.ai/code/session_013uyvXEazZkhNw27geRtakK

* feat(search): make SearchProviderRegistry accept runtime plugin providers (#477)

Claude-Session: https://claude.ai/code/session_013uyvXEazZkhNw27geRtakK

* test(search): cover blank plugin provider id rejection (#477)

Claude-Session: https://claude.ai/code/session_013uyvXEazZkhNw27geRtakK

* feat(plugin): bridge PluginSearchProvider to the core SearchProvider chain (#477)

Claude-Session: https://claude.ai/code/session_013uyvXEazZkhNw27geRtakK

* feat(plugin): registerSearchProvider lifecycle — register, disable, rollback (#477)

Claude-Session: https://claude.ai/code/session_013uyvXEazZkhNw27geRtakK

* fix(plugin): preserve cause when wrapping registry conflict as PluginException (#477)

Claude-Session: https://claude.ai/code/session_013uyvXEazZkhNw27geRtakK

* feat(plugin): add search provider sample plugin module (#477)

Claude-Session: https://claude.ai/code/session_013uyvXEazZkhNw27geRtakK

* docs(plugin): note unused query params and narrow parse exception in search sample (#477)

Claude-Session: https://claude.ai/code/session_013uyvXEazZkhNw27geRtakK

* docs(architecture): document the standalone-jar plugin system and SEARCH type (#477)

Claude-Session: https://claude.ai/code/session_013uyvXEazZkhNw27geRtakK
2026-07-03 16:57:27 +08:00

141 lines
5.3 KiB
Java

package vip.mate.plugin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.tool.ToolCallback;
import vip.mate.channel.ChannelManager;
import vip.mate.llm.service.ModelProviderService;
import vip.mate.memory.spi.MemoryManager;
import vip.mate.plugin.api.PluginContext;
import vip.mate.plugin.api.PluginException;
import vip.mate.plugin.api.PluginManifest;
import vip.mate.plugin.api.channel.PluginChannelAdapter;
import vip.mate.plugin.api.memory.PluginMemoryProvider;
import vip.mate.plugin.api.search.PluginSearchProvider;
import vip.mate.plugin.bridge.PluginChannelBridge;
import vip.mate.plugin.bridge.PluginMemoryBridge;
import vip.mate.plugin.bridge.PluginSearchBridge;
import vip.mate.tool.ToolRegistry;
import vip.mate.tool.search.SearchProviderRegistry;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Map;
import java.util.function.Supplier;
/**
* Platform-side implementation of {@link PluginContext}.
* Bridges plugin registrations to the corresponding platform services.
*
* @author MateClaw Team
*/
public class PluginContextImpl implements PluginContext {
private final LoadedPlugin loadedPlugin;
private final PluginManifest manifest;
private final ToolRegistry toolRegistry;
private final ChannelManager channelManager;
private final MemoryManager memoryManager;
private final ModelProviderService modelProviderService;
private final SearchProviderRegistry searchProviderRegistry;
private final Map<String, Object> configMap;
private final Logger logger;
private final ObjectMapper objectMapper = new ObjectMapper();
public PluginContextImpl(LoadedPlugin loadedPlugin,
PluginManifest manifest,
ToolRegistry toolRegistry,
ChannelManager channelManager,
MemoryManager memoryManager,
ModelProviderService modelProviderService,
SearchProviderRegistry searchProviderRegistry,
String configJson) {
this.loadedPlugin = loadedPlugin;
this.manifest = manifest;
this.toolRegistry = toolRegistry;
this.channelManager = channelManager;
this.memoryManager = memoryManager;
this.modelProviderService = modelProviderService;
this.searchProviderRegistry = searchProviderRegistry;
this.logger = LoggerFactory.getLogger("plugin." + manifest.getName());
this.configMap = parseConfig(configJson);
}
@SuppressWarnings("unchecked")
private Map<String, Object> parseConfig(String configJson) {
if (configJson == null || configJson.isBlank()) {
return Map.of();
}
try {
return objectMapper.readValue(configJson, Map.class);
} catch (Exception e) {
logger.warn("Failed to parse plugin config JSON: {}", e.getMessage());
return Map.of();
}
}
@Override
public void registerTool(ToolCallback tool) {
registerTool(tool, () -> true);
}
@Override
public void registerTool(ToolCallback tool, Supplier<Boolean> availabilityCheck) {
toolRegistry.registerPluginTool(tool, availabilityCheck);
loadedPlugin.getRegisteredTools().add(tool.getToolDefinition().name());
}
@Override
public void registerProvider(String providerId, ChatModel chatModel) {
modelProviderService.registerPluginChatModel(providerId, chatModel);
loadedPlugin.setRegisteredProvider(providerId);
}
@Override
public void registerChannel(PluginChannelAdapter channel) {
PluginChannelBridge bridge = new PluginChannelBridge(channel);
channelManager.registerPluginChannel(manifest.getName(), bridge);
loadedPlugin.getRegisteredChannels().add(channel.getChannelType());
}
@Override
public void registerMemoryProvider(PluginMemoryProvider provider) {
if (memoryManager.hasExternalProvider()) {
throw new PluginException(
"Only one external memory provider allowed. Current: " +
memoryManager.getExternalProviderName());
}
PluginMemoryBridge bridge = new PluginMemoryBridge(provider);
memoryManager.registerPluginProvider(bridge);
loadedPlugin.setRegisteredMemoryProvider(provider.id());
}
@Override
public void registerSearchProvider(PluginSearchProvider provider) {
if (provider == null || provider.id() == null || provider.id().isBlank()) {
throw new PluginException("Search provider id must not be blank");
}
try {
searchProviderRegistry.registerPluginProvider(new PluginSearchBridge(provider));
} catch (IllegalArgumentException e) {
throw new PluginException(e.getMessage(), e);
}
loadedPlugin.getRegisteredSearchProviders().add(provider.id());
}
@Override
@SuppressWarnings("unchecked")
public <T> T getConfig(String key, Class<T> type) {
Object value = configMap.get(key);
if (value == null) return null;
if (type.isInstance(value)) return (T) value;
return objectMapper.convertValue(value, type);
}
@Override
public Logger getLogger() {
return logger;
}
}