mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-19 22:10:51 +00:00
Plugins: add LSP server loader and surface in inspect reports
This commit is contained in:
@@ -796,6 +796,14 @@ export function registerPluginsCli(program: Command) {
|
||||
),
|
||||
),
|
||||
);
|
||||
lines.push(
|
||||
...formatInspectSection(
|
||||
"LSP servers",
|
||||
inspect.lspServers.map((entry) =>
|
||||
entry.hasStdioTransport ? entry.name : `${entry.name} (unsupported transport)`,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (inspect.httpRouteCount > 0) {
|
||||
lines.push(...formatInspectSection("HTTP routes", [String(inspect.httpRouteCount)]));
|
||||
}
|
||||
|
||||
212
src/plugins/bundle-lsp.ts
Normal file
212
src/plugins/bundle-lsp.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { applyMergePatch } from "../config/merge-patch.js";
|
||||
import { openBoundaryFileSync } from "../infra/boundary-file-read.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
import {
|
||||
CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH,
|
||||
mergeBundlePathLists,
|
||||
normalizeBundlePathList,
|
||||
} from "./bundle-manifest.js";
|
||||
import { normalizePluginsConfig, resolveEffectiveEnableState } from "./config-state.js";
|
||||
import { loadPluginManifestRegistry } from "./manifest-registry.js";
|
||||
import type { PluginBundleFormat } from "./types.js";
|
||||
|
||||
export type BundleLspServerConfig = Record<string, unknown>;
|
||||
|
||||
export type BundleLspConfig = {
|
||||
lspServers: Record<string, BundleLspServerConfig>;
|
||||
};
|
||||
|
||||
export type BundleLspRuntimeSupport = {
|
||||
hasStdioServer: boolean;
|
||||
supportedServerNames: string[];
|
||||
unsupportedServerNames: string[];
|
||||
diagnostics: string[];
|
||||
};
|
||||
|
||||
const MANIFEST_PATH_BY_FORMAT: Partial<Record<PluginBundleFormat, string>> = {
|
||||
claude: CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH,
|
||||
};
|
||||
|
||||
function readPluginJsonObject(params: {
|
||||
rootDir: string;
|
||||
relativePath: string;
|
||||
}): { ok: true; raw: Record<string, unknown> } | { ok: false; error: string } {
|
||||
const absolutePath = path.join(params.rootDir, params.relativePath);
|
||||
const opened = openBoundaryFileSync({
|
||||
absolutePath,
|
||||
rootPath: params.rootDir,
|
||||
boundaryLabel: "plugin root",
|
||||
rejectHardlinks: true,
|
||||
});
|
||||
if (!opened.ok) {
|
||||
return { ok: true, raw: {} };
|
||||
}
|
||||
try {
|
||||
const raw = JSON.parse(fs.readFileSync(opened.fd, "utf-8")) as unknown;
|
||||
if (!isRecord(raw)) {
|
||||
return { ok: false, error: `${params.relativePath} must contain a JSON object` };
|
||||
}
|
||||
return { ok: true, raw };
|
||||
} catch (error) {
|
||||
return { ok: false, error: `failed to parse ${params.relativePath}: ${String(error)}` };
|
||||
} finally {
|
||||
fs.closeSync(opened.fd);
|
||||
}
|
||||
}
|
||||
|
||||
function extractLspServerMap(raw: unknown): Record<string, BundleLspServerConfig> {
|
||||
if (!isRecord(raw)) {
|
||||
return {};
|
||||
}
|
||||
const nested = isRecord(raw.lspServers) ? raw.lspServers : raw;
|
||||
if (!isRecord(nested)) {
|
||||
return {};
|
||||
}
|
||||
const result: Record<string, BundleLspServerConfig> = {};
|
||||
for (const [serverName, serverRaw] of Object.entries(nested)) {
|
||||
if (!isRecord(serverRaw)) {
|
||||
continue;
|
||||
}
|
||||
result[serverName] = { ...serverRaw };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function resolveBundleLspConfigPaths(params: {
|
||||
raw: Record<string, unknown>;
|
||||
rootDir: string;
|
||||
}): string[] {
|
||||
const declared = normalizeBundlePathList(params.raw.lspServers);
|
||||
const defaults = fs.existsSync(path.join(params.rootDir, ".lsp.json")) ? [".lsp.json"] : [];
|
||||
return mergeBundlePathLists(defaults, declared);
|
||||
}
|
||||
|
||||
function loadBundleLspConfigFile(params: {
|
||||
rootDir: string;
|
||||
relativePath: string;
|
||||
}): BundleLspConfig {
|
||||
const absolutePath = path.resolve(params.rootDir, params.relativePath);
|
||||
const opened = openBoundaryFileSync({
|
||||
absolutePath,
|
||||
rootPath: params.rootDir,
|
||||
boundaryLabel: "plugin root",
|
||||
rejectHardlinks: true,
|
||||
});
|
||||
if (!opened.ok) {
|
||||
return { lspServers: {} };
|
||||
}
|
||||
try {
|
||||
const stat = fs.fstatSync(opened.fd);
|
||||
if (!stat.isFile()) {
|
||||
return { lspServers: {} };
|
||||
}
|
||||
const raw = JSON.parse(fs.readFileSync(opened.fd, "utf-8")) as unknown;
|
||||
return { lspServers: extractLspServerMap(raw) };
|
||||
} finally {
|
||||
fs.closeSync(opened.fd);
|
||||
}
|
||||
}
|
||||
|
||||
function loadBundleLspConfig(params: {
|
||||
pluginId: string;
|
||||
rootDir: string;
|
||||
bundleFormat: PluginBundleFormat;
|
||||
}): { config: BundleLspConfig; diagnostics: string[] } {
|
||||
const manifestRelativePath = MANIFEST_PATH_BY_FORMAT[params.bundleFormat];
|
||||
if (!manifestRelativePath) {
|
||||
return { config: { lspServers: {} }, diagnostics: [] };
|
||||
}
|
||||
|
||||
const manifestLoaded = readPluginJsonObject({
|
||||
rootDir: params.rootDir,
|
||||
relativePath: manifestRelativePath,
|
||||
});
|
||||
if (!manifestLoaded.ok) {
|
||||
return { config: { lspServers: {} }, diagnostics: [manifestLoaded.error] };
|
||||
}
|
||||
|
||||
let merged: BundleLspConfig = { lspServers: {} };
|
||||
const filePaths = resolveBundleLspConfigPaths({
|
||||
raw: manifestLoaded.raw,
|
||||
rootDir: params.rootDir,
|
||||
});
|
||||
for (const relativePath of filePaths) {
|
||||
merged = applyMergePatch(
|
||||
merged,
|
||||
loadBundleLspConfigFile({
|
||||
rootDir: params.rootDir,
|
||||
relativePath,
|
||||
}),
|
||||
) as BundleLspConfig;
|
||||
}
|
||||
|
||||
return { config: merged, diagnostics: [] };
|
||||
}
|
||||
|
||||
export function inspectBundleLspRuntimeSupport(params: {
|
||||
pluginId: string;
|
||||
rootDir: string;
|
||||
bundleFormat: PluginBundleFormat;
|
||||
}): BundleLspRuntimeSupport {
|
||||
const loaded = loadBundleLspConfig(params);
|
||||
const supportedServerNames: string[] = [];
|
||||
const unsupportedServerNames: string[] = [];
|
||||
let hasStdioServer = false;
|
||||
for (const [serverName, server] of Object.entries(loaded.config.lspServers)) {
|
||||
if (typeof server.command === "string" && server.command.trim().length > 0) {
|
||||
hasStdioServer = true;
|
||||
supportedServerNames.push(serverName);
|
||||
continue;
|
||||
}
|
||||
unsupportedServerNames.push(serverName);
|
||||
}
|
||||
return {
|
||||
hasStdioServer,
|
||||
supportedServerNames,
|
||||
unsupportedServerNames,
|
||||
diagnostics: loaded.diagnostics,
|
||||
};
|
||||
}
|
||||
|
||||
export function loadEnabledBundleLspConfig(params: {
|
||||
workspaceDir: string;
|
||||
cfg?: OpenClawConfig;
|
||||
}): { config: BundleLspConfig; diagnostics: Array<{ pluginId: string; message: string }> } {
|
||||
const registry = loadPluginManifestRegistry({
|
||||
workspaceDir: params.workspaceDir,
|
||||
config: params.cfg,
|
||||
});
|
||||
const normalizedPlugins = normalizePluginsConfig(params.cfg?.plugins);
|
||||
const diagnostics: Array<{ pluginId: string; message: string }> = [];
|
||||
let merged: BundleLspConfig = { lspServers: {} };
|
||||
|
||||
for (const record of registry.plugins) {
|
||||
if (record.format !== "bundle" || !record.bundleFormat) {
|
||||
continue;
|
||||
}
|
||||
const enableState = resolveEffectiveEnableState({
|
||||
id: record.id,
|
||||
origin: record.origin,
|
||||
config: normalizedPlugins,
|
||||
rootConfig: params.cfg,
|
||||
});
|
||||
if (!enableState.enabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const loaded = loadBundleLspConfig({
|
||||
pluginId: record.id,
|
||||
rootDir: record.rootDir,
|
||||
bundleFormat: record.bundleFormat,
|
||||
});
|
||||
merged = applyMergePatch(merged, loaded.config) as BundleLspConfig;
|
||||
for (const message of loaded.diagnostics) {
|
||||
diagnostics.push({ pluginId: record.id, message });
|
||||
}
|
||||
}
|
||||
|
||||
return { config: merged, diagnostics };
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent
|
||||
import { resolveDefaultAgentWorkspaceDir } from "../agents/workspace.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import { inspectBundleLspRuntimeSupport } from "./bundle-lsp.js";
|
||||
import { inspectBundleMcpRuntimeSupport } from "./bundle-mcp.js";
|
||||
import { normalizePluginsConfig } from "./config-state.js";
|
||||
import { loadOpenClawPlugins } from "./loader.js";
|
||||
@@ -69,6 +70,10 @@ export type PluginInspectReport = {
|
||||
name: string;
|
||||
hasStdioTransport: boolean;
|
||||
}>;
|
||||
lspServers: Array<{
|
||||
name: string;
|
||||
hasStdioTransport: boolean;
|
||||
}>;
|
||||
httpRouteCount: number;
|
||||
bundleCapabilities: string[];
|
||||
diagnostics: PluginDiagnostic[];
|
||||
@@ -252,6 +257,26 @@ export function buildPluginInspectReport(params: {
|
||||
];
|
||||
}
|
||||
|
||||
// Populate LSP server info for bundle-format plugins with a known rootDir.
|
||||
let lspServers: PluginInspectReport["lspServers"] = [];
|
||||
if (plugin.format === "bundle" && plugin.bundleFormat && plugin.rootDir) {
|
||||
const lspSupport = inspectBundleLspRuntimeSupport({
|
||||
pluginId: plugin.id,
|
||||
rootDir: plugin.rootDir,
|
||||
bundleFormat: plugin.bundleFormat,
|
||||
});
|
||||
lspServers = [
|
||||
...lspSupport.supportedServerNames.map((name) => ({
|
||||
name,
|
||||
hasStdioTransport: true,
|
||||
})),
|
||||
...lspSupport.unsupportedServerNames.map((name) => ({
|
||||
name,
|
||||
hasStdioTransport: false,
|
||||
})),
|
||||
];
|
||||
}
|
||||
|
||||
const usesLegacyBeforeAgentStart = typedHooks.some(
|
||||
(entry) => entry.name === "before_agent_start",
|
||||
);
|
||||
@@ -275,6 +300,7 @@ export function buildPluginInspectReport(params: {
|
||||
services: [...plugin.services],
|
||||
gatewayMethods: [...plugin.gatewayMethods],
|
||||
mcpServers,
|
||||
lspServers,
|
||||
httpRouteCount: plugin.httpRoutes,
|
||||
bundleCapabilities: plugin.bundleCapabilities ?? [],
|
||||
diagnostics,
|
||||
|
||||
Reference in New Issue
Block a user