diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 1a5348d..f597a47 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -88,10 +88,38 @@ export type { } from './permission/index.js'; // 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 { printServerList, - installServer, installAllServers, showServerInfo, } from './lsp/cli.js'; diff --git a/packages/core/src/lsp/client.ts b/packages/core/src/lsp/client.ts index df08aba..b593a8e 100644 --- a/packages/core/src/lsp/client.ts +++ b/packages/core/src/lsp/client.ts @@ -411,4 +411,24 @@ export class LSPClientManager { getRunningServers(): LanguageId[] { return Array.from(this.clients.keys()); } + + /** + * 停止指定语言的服务器 + */ + async stopServer(languageId: LanguageId): Promise { + 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; + } + } } diff --git a/packages/core/src/lsp/index.ts b/packages/core/src/lsp/index.ts index 2e9fe25..729f04a 100644 --- a/packages/core/src/lsp/index.ts +++ b/packages/core/src/lsp/index.ts @@ -115,6 +115,36 @@ export async function getFormattedFileDiagnostics(filePath: string): Promise { + 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 系统 */ @@ -127,6 +157,17 @@ export async function shutdownLSP(): Promise { // 导出类型 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 { getServerConfig, hasServerConfig, getSupportedLanguages } from './server.js'; + +// 从 server.js 导出 +export { getServerConfig, hasServerConfig, getSupportedLanguages, getUniqueServers } from './server.js'; + +// 从 client.js 导出 export { LSPClientManager } from './client.js'; + +// 从 cli.js 导出 +export { listServers, installServer } from './cli.js'; diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index ca84a09..2bca182 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -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); diff --git a/packages/server/src/routes/index.ts b/packages/server/src/routes/index.ts index 7df9b05..3b380a2 100644 --- a/packages/server/src/routes/index.ts +++ b/packages/server/src/routes/index.ts @@ -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'; diff --git a/packages/server/src/routes/lsp.ts b/packages/server/src/routes/lsp.ts new file mode 100644 index 0000000..0b0179c --- /dev/null +++ b/packages/server/src/routes/lsp.ts @@ -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, + }, + }, + }); +});