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:
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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