feat(server): 实现 LSP 管理 REST API
- 新增 /api/lsp/servers 端点,列出所有语言服务器 - 新增 /api/lsp/servers/:id 端点,获取服务器详情 - 新增 /api/lsp/servers/:id/install 端点,安装服务器 - 新增 /api/lsp/servers/:id/start 端点,启动服务器 - 新增 /api/lsp/servers/:id/stop 端点,停止服务器 - 新增 /api/lsp/diagnostics 端点,获取诊断信息 - 新增 /api/lsp/running 端点,获取运行中服务器列表 - core/lsp 添加 stopServer, getRunningServers, isServerRunning 方法
This commit is contained in:
@@ -88,10 +88,38 @@ export type {
|
|||||||
} from './permission/index.js';
|
} from './permission/index.js';
|
||||||
|
|
||||||
// LSP
|
// LSP
|
||||||
export { initLSP, shutdownLSP } from './lsp/index.js';
|
export {
|
||||||
|
initLSP,
|
||||||
|
shutdownLSP,
|
||||||
|
getLSPManager,
|
||||||
|
touchFile,
|
||||||
|
getDiagnostics,
|
||||||
|
getFileDiagnostics,
|
||||||
|
formatDiagnostics,
|
||||||
|
getFormattedFileDiagnostics,
|
||||||
|
stopServer,
|
||||||
|
getRunningServers,
|
||||||
|
isServerRunning,
|
||||||
|
listServers,
|
||||||
|
installServer,
|
||||||
|
getLanguageId,
|
||||||
|
isLanguageSupported,
|
||||||
|
getSupportedExtensions,
|
||||||
|
getServerConfig,
|
||||||
|
hasServerConfig,
|
||||||
|
getSupportedLanguages,
|
||||||
|
getUniqueServers,
|
||||||
|
} from './lsp/index.js';
|
||||||
|
|
||||||
|
export type {
|
||||||
|
FileDiagnostic,
|
||||||
|
ServerConfig,
|
||||||
|
InstallConfig,
|
||||||
|
ServerStatus,
|
||||||
|
} from './lsp/index.js';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
printServerList,
|
printServerList,
|
||||||
installServer,
|
|
||||||
installAllServers,
|
installAllServers,
|
||||||
showServerInfo,
|
showServerInfo,
|
||||||
} from './lsp/cli.js';
|
} from './lsp/cli.js';
|
||||||
|
|||||||
@@ -411,4 +411,24 @@ export class LSPClientManager {
|
|||||||
getRunningServers(): LanguageId[] {
|
getRunningServers(): LanguageId[] {
|
||||||
return Array.from(this.clients.keys());
|
return Array.from(this.clients.keys());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止指定语言的服务器
|
||||||
|
*/
|
||||||
|
async stopServer(languageId: LanguageId): Promise<boolean> {
|
||||||
|
const client = this.clients.get(languageId);
|
||||||
|
if (!client) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
client.connection.dispose();
|
||||||
|
client.process.kill();
|
||||||
|
this.clients.delete(languageId);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`停止语言服务器失败 (${languageId}):`, error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,6 +115,36 @@ export async function getFormattedFileDiagnostics(filePath: string): Promise<str
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止指定语言的服务器
|
||||||
|
*/
|
||||||
|
export async function stopServer(languageId: string): Promise<boolean> {
|
||||||
|
if (!lspManager) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return lspManager.stopServer(languageId as import('./language.js').LanguageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取运行中的服务器列表
|
||||||
|
*/
|
||||||
|
export function getRunningServers(): string[] {
|
||||||
|
if (!lspManager) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return lspManager.getRunningServers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查服务器是否运行中
|
||||||
|
*/
|
||||||
|
export function isServerRunning(languageId: string): boolean {
|
||||||
|
if (!lspManager) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return lspManager.isServerRunning(languageId as import('./language.js').LanguageId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 关闭 LSP 系统
|
* 关闭 LSP 系统
|
||||||
*/
|
*/
|
||||||
@@ -127,6 +157,17 @@ export async function shutdownLSP(): Promise<void> {
|
|||||||
|
|
||||||
// 导出类型
|
// 导出类型
|
||||||
export type { FileDiagnostic } from './client.js';
|
export type { FileDiagnostic } from './client.js';
|
||||||
|
export type { ServerConfig, InstallConfig } from './server.js';
|
||||||
|
export type { ServerStatus } from './cli.js';
|
||||||
|
|
||||||
|
// 从 language.js 导出
|
||||||
export { getLanguageId, isLanguageSupported, getSupportedExtensions } from './language.js';
|
export { getLanguageId, isLanguageSupported, getSupportedExtensions } from './language.js';
|
||||||
export { getServerConfig, hasServerConfig, getSupportedLanguages } from './server.js';
|
|
||||||
|
// 从 server.js 导出
|
||||||
|
export { getServerConfig, hasServerConfig, getSupportedLanguages, getUniqueServers } from './server.js';
|
||||||
|
|
||||||
|
// 从 client.js 导出
|
||||||
export { LSPClientManager } from './client.js';
|
export { LSPClientManager } from './client.js';
|
||||||
|
|
||||||
|
// 从 cli.js 导出
|
||||||
|
export { listServers, installServer } from './cli.js';
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { cors } from 'hono/cors';
|
|||||||
import { logger } from 'hono/logger';
|
import { logger } from 'hono/logger';
|
||||||
import { createBunWebSocket } from 'hono/bun';
|
import { createBunWebSocket } from 'hono/bun';
|
||||||
|
|
||||||
import { sessionsRouter, toolsRouter, configRouter, filesRouter, commandsRouter, mcpRouter, hooksRouter, agentsRouter, checkpointsRouter, providersRouter, contextRouter } from './routes/index.js';
|
import { sessionsRouter, toolsRouter, configRouter, filesRouter, commandsRouter, mcpRouter, hooksRouter, agentsRouter, checkpointsRouter, providersRouter, contextRouter, lspRouter } from './routes/index.js';
|
||||||
import {
|
import {
|
||||||
handleWebSocket,
|
handleWebSocket,
|
||||||
handleWebSocketMessage,
|
handleWebSocketMessage,
|
||||||
@@ -88,6 +88,7 @@ api.route('/hooks', hooksRouter);
|
|||||||
api.route('/agents', agentsRouter);
|
api.route('/agents', agentsRouter);
|
||||||
api.route('/checkpoints', checkpointsRouter);
|
api.route('/checkpoints', checkpointsRouter);
|
||||||
api.route('/providers', providersRouter);
|
api.route('/providers', providersRouter);
|
||||||
|
api.route('/lsp', lspRouter);
|
||||||
|
|
||||||
// 上下文压缩相关(挂载到根路径,内部路由包含 /sessions/:id/context)
|
// 上下文压缩相关(挂载到根路径,内部路由包含 /sessions/:id/context)
|
||||||
api.route('/', contextRouter);
|
api.route('/', contextRouter);
|
||||||
|
|||||||
@@ -15,3 +15,4 @@ export { agentsRouter } from './agents.js';
|
|||||||
export { checkpointsRouter } from './checkpoints.js';
|
export { checkpointsRouter } from './checkpoints.js';
|
||||||
export { providersRouter } from './providers.js';
|
export { providersRouter } from './providers.js';
|
||||||
export { contextRouter } from './context.js';
|
export { contextRouter } from './context.js';
|
||||||
|
export { lspRouter } from './lsp.js';
|
||||||
|
|||||||
@@ -0,0 +1,361 @@
|
|||||||
|
/**
|
||||||
|
* LSP API Routes
|
||||||
|
*
|
||||||
|
* 提供 LSP 语言服务器管理的 REST API
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Hono } from 'hono';
|
||||||
|
import { getConfig } from './config.js';
|
||||||
|
import type {
|
||||||
|
FileDiagnostic,
|
||||||
|
ServerStatus,
|
||||||
|
} from '@ai-assistant/core';
|
||||||
|
import {
|
||||||
|
initLSP,
|
||||||
|
listServers,
|
||||||
|
installServer,
|
||||||
|
touchFile,
|
||||||
|
getDiagnostics,
|
||||||
|
getFileDiagnostics,
|
||||||
|
getRunningServers,
|
||||||
|
stopServer,
|
||||||
|
} from '@ai-assistant/core';
|
||||||
|
|
||||||
|
export const lspRouter = new Hono();
|
||||||
|
|
||||||
|
// LSP 初始化标志
|
||||||
|
let lspInitialized = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 LSP 系统
|
||||||
|
*/
|
||||||
|
function ensureLSPInitialized(): boolean {
|
||||||
|
if (lspInitialized) return true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const config = getConfig();
|
||||||
|
initLSP(config.workdir);
|
||||||
|
lspInitialized = true;
|
||||||
|
console.log('[LSP] LSP module initialized');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[LSP] Failed to initialize LSP module:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /lsp/servers - 获取所有语言服务器列表
|
||||||
|
*/
|
||||||
|
lspRouter.get('/servers', async (c) => {
|
||||||
|
if (!ensureLSPInitialized()) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'LSP module not available',
|
||||||
|
},
|
||||||
|
503
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const servers = listServers();
|
||||||
|
const runningServers = new Set(getRunningServers());
|
||||||
|
|
||||||
|
// 添加运行状态
|
||||||
|
const serversWithRunningStatus = servers.map((server) => ({
|
||||||
|
...server,
|
||||||
|
running: runningServers.has(server.id) || server.languages.some(lang => runningServers.has(lang)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
data: serversWithRunningStatus,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /lsp/servers/:id - 获取单个服务器详情
|
||||||
|
*/
|
||||||
|
lspRouter.get('/servers/:id', async (c) => {
|
||||||
|
const id = c.req.param('id');
|
||||||
|
|
||||||
|
if (!ensureLSPInitialized()) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'LSP module not available',
|
||||||
|
},
|
||||||
|
503
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const servers = listServers();
|
||||||
|
const server = servers.find(
|
||||||
|
(s) => s.id === id || s.displayName.toLowerCase() === id.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: `Server not found: ${id}`,
|
||||||
|
},
|
||||||
|
404
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const runningServers = new Set(getRunningServers());
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
...server,
|
||||||
|
running: runningServers.has(server.id) || server.languages.some(lang => runningServers.has(lang)),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /lsp/servers/:id/install - 安装语言服务器
|
||||||
|
*/
|
||||||
|
lspRouter.post('/servers/:id/install', async (c) => {
|
||||||
|
const id = c.req.param('id');
|
||||||
|
|
||||||
|
if (!ensureLSPInitialized()) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'LSP module not available',
|
||||||
|
},
|
||||||
|
503
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const success = await installServer(id);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
const servers = listServers();
|
||||||
|
const server = servers.find((s) => s.id === id);
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
message: `Server ${id} installed successfully`,
|
||||||
|
server,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: `Failed to install server: ${id}`,
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Installation failed',
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /lsp/servers/:id/start - 启动语言服务器
|
||||||
|
* Body: { filePath: string } - 需要提供一个文件路径来触发对应语言的服务器
|
||||||
|
*/
|
||||||
|
lspRouter.post('/servers/:id/start', async (c) => {
|
||||||
|
const id = c.req.param('id');
|
||||||
|
|
||||||
|
if (!ensureLSPInitialized()) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'LSP module not available',
|
||||||
|
},
|
||||||
|
503
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = await c.req.json<{ filePath?: string }>();
|
||||||
|
|
||||||
|
if (!body.filePath) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'filePath is required to start the server',
|
||||||
|
},
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFirstStart = await touchFile(body.filePath);
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
message: `Server started for file: ${body.filePath}`,
|
||||||
|
isFirstStart,
|
||||||
|
runningServers: getRunningServers(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Start failed',
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /lsp/servers/:id/stop - 停止语言服务器
|
||||||
|
*/
|
||||||
|
lspRouter.post('/servers/:id/stop', async (c) => {
|
||||||
|
const id = c.req.param('id');
|
||||||
|
|
||||||
|
if (!ensureLSPInitialized()) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'LSP module not available',
|
||||||
|
},
|
||||||
|
503
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// id 可能是命令名或语言 ID,尝试从服务器列表找到对应的语言
|
||||||
|
const servers = listServers();
|
||||||
|
const server = servers.find(
|
||||||
|
(s) => s.id === id || s.displayName.toLowerCase() === id.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 尝试停止对应语言的服务器
|
||||||
|
let stopped = false;
|
||||||
|
if (server) {
|
||||||
|
for (const lang of server.languages) {
|
||||||
|
const result = await stopServer(lang);
|
||||||
|
if (result) {
|
||||||
|
stopped = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 直接尝试用 id 作为语言 ID
|
||||||
|
stopped = await stopServer(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stopped) {
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
message: `Server ${id} stopped`,
|
||||||
|
runningServers: getRunningServers(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: `Server ${id} is not running or could not be stopped`,
|
||||||
|
},
|
||||||
|
400
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Stop failed',
|
||||||
|
},
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /lsp/running - 获取正在运行的服务器列表
|
||||||
|
*/
|
||||||
|
lspRouter.get('/running', async (c) => {
|
||||||
|
if (!ensureLSPInitialized()) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'LSP module not available',
|
||||||
|
},
|
||||||
|
503
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
data: getRunningServers(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /lsp/diagnostics - 获取诊断信息
|
||||||
|
* Query: file (可选) - 指定文件路径
|
||||||
|
*/
|
||||||
|
lspRouter.get('/diagnostics', async (c) => {
|
||||||
|
if (!ensureLSPInitialized()) {
|
||||||
|
return c.json(
|
||||||
|
{
|
||||||
|
success: false,
|
||||||
|
error: 'LSP module not available',
|
||||||
|
},
|
||||||
|
503
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = c.req.query('file');
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
// 获取单个文件的诊断信息
|
||||||
|
const diagnostics = getFileDiagnostics(file);
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
file,
|
||||||
|
diagnostics,
|
||||||
|
summary: {
|
||||||
|
totalErrors: diagnostics.filter((d) => d.severity === 'error').length,
|
||||||
|
totalWarnings: diagnostics.filter((d) => d.severity === 'warning').length,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有文件的诊断信息
|
||||||
|
const allDiagnostics = getDiagnostics();
|
||||||
|
const files: Array<{ file: string; diagnostics: FileDiagnostic[] }> = [];
|
||||||
|
let totalErrors = 0;
|
||||||
|
let totalWarnings = 0;
|
||||||
|
|
||||||
|
for (const [filePath, diagnostics] of allDiagnostics) {
|
||||||
|
files.push({ file: filePath, diagnostics });
|
||||||
|
totalErrors += diagnostics.filter((d) => d.severity === 'error').length;
|
||||||
|
totalWarnings += diagnostics.filter((d) => d.severity === 'warning').length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
files,
|
||||||
|
summary: {
|
||||||
|
totalFiles: files.length,
|
||||||
|
totalErrors,
|
||||||
|
totalWarnings,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user