import {
registerDynamicTool,
unregisterDynamicTool,
type ToolRunOptions,
} from "../agent/tools.js";
import type { ToolDefinition, ToolResult } from "../agent/types.js";
import { createPluginStorage } from "./storage.js";
import type {
ExtensionHost,
PluginAPI,
PluginCommand,
PluginCommandInfo,
PluginDescriptor,
PluginHandler,
PluginHookName,
PluginLogger,
PluginSource,
PluginTool,
} from "./types.js";
* 运行时唯一插件查询入口:持有已加载插件记录、hook handler 分桶、命令索引。
* 工具的执行本体注册进 agent/tools.ts 的动态工具表,registry 只记录归属
* (哪个插件注册了哪些名字),reload 时按归属精确摘除。
*/
export class PluginRegistry {
private readonly plugins = new Map<string, { source: PluginSource; path: string }>();
private readonly hooks = new Map<PluginHookName, Array<{ pluginId: string; handler: (event: never) => unknown }>>();
private readonly commands = new Map<string, { pluginId: string; cmd: PluginCommand }>();
private readonly toolOwners = new Map<string, string>();
addPlugin(id: string, source: PluginSource, path: string): void {
this.plugins.set(id, { source, path });
}
rollbackPlugin(id: string): void {
for (const [name, owner] of this.toolOwners) {
if (owner === id) {
unregisterDynamicTool(name);
this.toolOwners.delete(name);
}
}
for (const [event, list] of this.hooks) {
const kept = list.filter((h) => h.pluginId !== id);
if (kept.length !== list.length) this.hooks.set(event, kept);
}
for (const [name, entry] of this.commands) {
if (entry.pluginId === id) this.commands.delete(name);
}
}
addTool(pluginId: string, tool: PluginTool): { ok: boolean; error?: string } {
const definition: ToolDefinition = {
type: "function",
function: {
name: tool.name,
description: tool.description,
parameters: {
type: "object",
properties: tool.parameters.properties ?? {},
...(tool.parameters.required ? { required: tool.parameters.required } : {}),
...(tool.parameters.additionalProperties !== undefined
? { additionalProperties: tool.parameters.additionalProperties }
: {}),
},
},
};
const result = registerDynamicTool(tool.name, {
definition,
...(tool.requiresApproval !== undefined ? { requiresApproval: tool.requiresApproval } : {}),
...(tool.contract !== undefined ? { contract: tool.contract } : {}),
execute: (input: unknown, workspace: string, options: ToolRunOptions): Promise<ToolResult> =>
tool.execute((input ?? {}) as Record<string, unknown>, {
workspace,
...(options.signal ? { signal: options.signal } : {}),
logger: this.loggerOf(pluginId),
}),
});
if (result.ok) this.toolOwners.set(tool.name, pluginId);
return result;
}
toolNamesOf(pluginId: string): string[] {
return [...this.toolOwners].filter(([, owner]) => owner === pluginId).map(([name]) => name);
}
addHook<N extends PluginHookName>(pluginId: string, event: N, handler: PluginHandler<N>): void {
let list = this.hooks.get(event);
if (!list) {
list = [];
this.hooks.set(event, list);
}
list.push({ pluginId, handler: handler as (event: never) => unknown });
}
getHookHandlers(event: PluginHookName): ReadonlyArray<{ pluginId: string; handler: (event: never) => unknown }> {
return this.hooks.get(event) ?? [];
}
hasHooks(event: PluginHookName): boolean {
return (this.hooks.get(event)?.length ?? 0) > 0;
}
hookEventsOf(pluginId: string): string[] {
return [...this.hooks]
.filter(([, list]) => list.some((h) => h.pluginId === pluginId))
.map(([event]) => event);
}
reportHandlerError(pluginId: string, hook: string, error: unknown): void {
const message = error instanceof Error ? error.message : String(error);
console.error(`[plugin:${pluginId}] hook "${hook}" handler failed:`, message);
}
addCommand(pluginId: string, cmd: PluginCommand): boolean {
if (this.commands.has(cmd.name)) return false;
this.commands.set(cmd.name, { pluginId, cmd });
return true;
}
getCommand(name: string): { pluginId: string; cmd: PluginCommand } | undefined {
return this.commands.get(name);
}
listCommands(): PluginCommandInfo[] {
return [...this.commands.entries()].map(([name, { pluginId, cmd }]) => ({
name,
...(cmd.description !== undefined ? { description: cmd.description } : {}),
source: `plugin:${pluginId}`,
}));
}
listDescriptors(): PluginDescriptor[] {
return [...this.plugins.entries()].map(([id, meta]) => {
const toolNames = this.toolNamesOf(id);
const commandNames = [...this.commands]
.filter(([, entry]) => entry.pluginId === id)
.map(([name]) => name);
const hookEvents = this.hookEventsOf(id);
return {
id,
path: meta.path,
source: meta.source,
enabled: true,
...(toolNames.length > 0 ? { toolNames } : {}),
...(commandNames.length > 0 ? { commandNames } : {}),
...(hookEvents.length > 0 ? { hookEvents } : {}),
};
});
}
unregisterAll(): void {
for (const name of this.toolOwners.keys()) unregisterDynamicTool(name);
this.toolOwners.clear();
this.hooks.clear();
this.commands.clear();
this.plugins.clear();
}
createApi(
pluginId: string,
workspace: string,
homeDir: string,
host?: ExtensionHost,
): PluginAPI {
const logger = this.loggerOf(pluginId);
return {
registerTool: (tool: PluginTool): void => {
const result = this.addTool(pluginId, tool);
if (!result.ok) logger.warn(`registerTool("${tool.name}") rejected: ${result.error}`);
},
on: <N extends PluginHookName>(event: N, handler: PluginHandler<N>): void => {
this.addHook(pluginId, event, handler);
},
registerCommand: (cmd: PluginCommand): void => {
if (!this.addCommand(pluginId, cmd)) {
logger.warn(`registerCommand("${cmd.name}") rejected: name already taken`);
}
},
storage: createPluginStorage(pluginId, homeDir),
logger,
workspace,
...(host ? { host } : {}),
};
}
private loggerOf(pluginId: string): PluginLogger {
return {
info: (...args: unknown[]) => console.log(`[plugin:${pluginId}]`, ...args),
warn: (...args: unknown[]) => console.warn(`[plugin:${pluginId}]`, ...args),
error: (...args: unknown[]) => console.error(`[plugin:${pluginId}]`, ...args),
};
}
}