feat: Plugin marketplace - manifest, installer, auto-update, marketplace (Phase 3E)

- PluginManifest: manifest.json record with validation, DeclaredTool/Command/Hook, MarketplaceEntry
- PluginInstaller: HTTP download, JAR install/uninstall, version management, update checking
- MarketplaceManager: remote catalog fetch with 24h cache, search/filter/popular queries
- PluginAutoUpdate: scheduled update checks, parallel check, auto-install option, notification callback
- Enhanced /plugin command: install/remove/update/search subcommands (was: load/unload/reload/info)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
abel533
2026-04-05 10:36:10 +08:00
co-authored by Copilot
parent 76191f5035
commit 12557f23f0
5 changed files with 981 additions and 3 deletions
@@ -0,0 +1,191 @@
package com.claudecode.plugin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.*;
import java.util.*;
import java.util.concurrent.*;
/**
* 插件自动更新器 —— 每日检查已安装插件的更新。
* <p>
* 对应 claude-code 中插件自动更新逻辑。
* <p>
* 特性:
* <ul>
* <li>可配置检查间隔(默认24小时)</li>
* <li>只通知,不自动安装(除非配置 autoInstall=true</li>
* <li>并行检查所有已安装插件</li>
* <li>记录上次检查时间,避免频繁检查</li>
* </ul>
*/
public class PluginAutoUpdate {
private static final Logger log = LoggerFactory.getLogger(PluginAutoUpdate.class);
private final PluginInstaller installer;
private final PluginManager pluginManager;
private final String marketplaceUrl;
/** 是否自动安装更新 */
private boolean autoInstall = false;
/** 检查间隔 */
private Duration checkInterval = Duration.ofHours(24);
/** 上次检查时间 */
private Instant lastCheckTime = Instant.EPOCH;
/** 可用更新缓存 */
private final ConcurrentHashMap<String, PluginInstaller.UpdateCheckResult> pendingUpdates =
new ConcurrentHashMap<>();
/** 更新通知回调 */
private UpdateNotificationCallback notificationCallback;
private final ScheduledExecutorService scheduler =
Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "plugin-auto-update");
t.setDaemon(true);
return t;
});
public PluginAutoUpdate(PluginInstaller installer, PluginManager pluginManager) {
this(installer, pluginManager, "https://marketplace.claude-code.dev/api/v1");
}
public PluginAutoUpdate(PluginInstaller installer, PluginManager pluginManager,
String marketplaceUrl) {
this.installer = installer;
this.pluginManager = pluginManager;
this.marketplaceUrl = marketplaceUrl;
}
/**
* 启动定期检查。
*/
public void start() {
long intervalHours = checkInterval.toHours();
scheduler.scheduleWithFixedDelay(this::checkForUpdates,
1, intervalHours, TimeUnit.HOURS);
log.info("Plugin auto-update started (interval: {}h)", intervalHours);
}
/**
* 停止定期检查。
*/
public void stop() {
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
/**
* 立即检查所有已安装插件的更新。
*/
public Map<String, PluginInstaller.UpdateCheckResult> checkForUpdates() {
Map<String, PluginInstaller.InstalledPluginInfo> installed = installer.listInstalled();
if (installed.isEmpty()) {
log.debug("No installed plugins to check for updates");
return Map.of();
}
log.info("Checking updates for {} plugins...", installed.size());
// 并行检查所有插件
List<CompletableFuture<PluginInstaller.UpdateCheckResult>> futures = new ArrayList<>();
for (var entry : installed.values()) {
futures.add(installer.checkUpdate(
entry.id(), entry.version(), marketplaceUrl));
}
// 等待所有结果
Map<String, PluginInstaller.UpdateCheckResult> results = new HashMap<>();
for (var future : futures) {
try {
var result = future.get(30, TimeUnit.SECONDS);
if (result.hasUpdate()) {
results.put(result.pluginId(), result);
pendingUpdates.put(result.pluginId(), result);
log.info("Update available: {} {} → {}",
result.pluginId(), result.currentVersion(), result.latestVersion());
// 自动安装
if (autoInstall && result.downloadUrl() != null) {
var installed2 = installer.listInstalled().get(result.pluginId());
if (installed2 != null) {
var installResult = installer.install(
result.downloadUrl(), installed2.scope());
log.info("Auto-updated {}: {}", result.pluginId(), installResult.message());
}
}
}
} catch (Exception e) {
log.debug("Update check failed: {}", e.getMessage());
}
}
lastCheckTime = Instant.now();
// 通知回调
if (!results.isEmpty() && notificationCallback != null) {
notificationCallback.onUpdatesAvailable(results);
}
log.info("Update check complete: {} updates available", results.size());
return results;
}
/**
* 获取待处理的更新列表。
*/
public Map<String, PluginInstaller.UpdateCheckResult> getPendingUpdates() {
return Map.copyOf(pendingUpdates);
}
/**
* 清除指定插件的待更新标记。
*/
public void clearPendingUpdate(String pluginId) {
pendingUpdates.remove(pluginId);
}
/**
* 是否需要检查(距上次检查超过 interval)。
*/
public boolean shouldCheck() {
return Duration.between(lastCheckTime, Instant.now()).compareTo(checkInterval) > 0;
}
// ==================== 配置 ====================
public void setAutoInstall(boolean autoInstall) {
this.autoInstall = autoInstall;
}
public void setCheckInterval(Duration interval) {
this.checkInterval = interval;
}
public void setNotificationCallback(UpdateNotificationCallback callback) {
this.notificationCallback = callback;
}
public Instant getLastCheckTime() {
return lastCheckTime;
}
// ==================== 回调接口 ====================
@FunctionalInterface
public interface UpdateNotificationCallback {
void onUpdatesAvailable(Map<String, PluginInstaller.UpdateCheckResult> updates);
}
}