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:
2025-12-17 10:19:05 +08:00
parent b63b79e51e
commit cfb2175916
6 changed files with 456 additions and 4 deletions
+2 -1
View File
@@ -9,7 +9,7 @@ import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
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 {
handleWebSocket,
handleWebSocketMessage,
@@ -88,6 +88,7 @@ api.route('/hooks', hooksRouter);
api.route('/agents', agentsRouter);
api.route('/checkpoints', checkpointsRouter);
api.route('/providers', providersRouter);
api.route('/lsp', lspRouter);
// 上下文压缩相关(挂载到根路径,内部路由包含 /sessions/:id/context
api.route('/', contextRouter);
+1
View File
@@ -15,3 +15,4 @@ export { agentsRouter } from './agents.js';
export { checkpointsRouter } from './checkpoints.js';
export { providersRouter } from './providers.js';
export { contextRouter } from './context.js';
export { lspRouter } from './lsp.js';
+361
View File
@@ -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,
},
},
});
});