feat: 添加 LSP 代码诊断功能
- 实现 LSP 客户端,支持与语言服务器通信 - 支持多种语言: TypeScript, Python, Go, Rust, C/C++ 等 - write_file/edit_file 工具集成 LSP 诊断,写入后自动检查代码错误 - 添加 CLI 命令管理语言服务器 (ai-assist lsp list/install/info) - 智能等待机制:首次启动 LSP 等待 2s,后续仅需 300ms - 将 read_file/write_file/edit_file 设为核心工具,确保文件操作使用正确的工具 - 更新系统提示词,引导 AI 使用文件工具而非 bash 命令
This commit is contained in:
Generated
+36
@@ -17,6 +17,8 @@
|
|||||||
"inquirer": "^12.0.0",
|
"inquirer": "^12.0.0",
|
||||||
"ora": "^8.1.0",
|
"ora": "^8.1.0",
|
||||||
"tree-sitter-bash": "^0.25.1",
|
"tree-sitter-bash": "^0.25.1",
|
||||||
|
"vscode-jsonrpc": "^8.2.1",
|
||||||
|
"vscode-languageserver-protocol": "^3.17.5",
|
||||||
"web-tree-sitter": "^0.25.10",
|
"web-tree-sitter": "^0.25.10",
|
||||||
"zod": "^4.1.13"
|
"zod": "^4.1.13"
|
||||||
},
|
},
|
||||||
@@ -1500,6 +1502,40 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/vscode-jsonrpc": {
|
||||||
|
"version": "8.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz",
|
||||||
|
"integrity": "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vscode-languageserver-protocol": {
|
||||||
|
"version": "3.17.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
|
||||||
|
"integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"vscode-jsonrpc": "8.2.0",
|
||||||
|
"vscode-languageserver-types": "3.17.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vscode-languageserver-protocol/node_modules/vscode-jsonrpc": {
|
||||||
|
"version": "8.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
|
||||||
|
"integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vscode-languageserver-types": {
|
||||||
|
"version": "3.17.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
|
||||||
|
"integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/web-tree-sitter": {
|
"node_modules/web-tree-sitter": {
|
||||||
"version": "0.25.10",
|
"version": "0.25.10",
|
||||||
"resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.25.10.tgz",
|
"resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.25.10.tgz",
|
||||||
|
|||||||
@@ -31,6 +31,8 @@
|
|||||||
"inquirer": "^12.0.0",
|
"inquirer": "^12.0.0",
|
||||||
"ora": "^8.1.0",
|
"ora": "^8.1.0",
|
||||||
"tree-sitter-bash": "^0.25.1",
|
"tree-sitter-bash": "^0.25.1",
|
||||||
|
"vscode-jsonrpc": "^8.2.1",
|
||||||
|
"vscode-languageserver-protocol": "^3.17.5",
|
||||||
"web-tree-sitter": "^0.25.10",
|
"web-tree-sitter": "^0.25.10",
|
||||||
"zod": "^4.1.13"
|
"zod": "^4.1.13"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,6 +7,13 @@ import { loadConfig, initConfig } from './utils/config.js';
|
|||||||
import { toolRegistry, todoManager } from './tools/index.js';
|
import { toolRegistry, todoManager } from './tools/index.js';
|
||||||
import { getPermissionManager, promptPermission } from './permission/index.js';
|
import { getPermissionManager, promptPermission } from './permission/index.js';
|
||||||
import { SessionManager } from './session/index.js';
|
import { SessionManager } from './session/index.js';
|
||||||
|
import { initLSP, shutdownLSP } from './lsp/index.js';
|
||||||
|
import {
|
||||||
|
printServerList,
|
||||||
|
installServer,
|
||||||
|
installAllServers,
|
||||||
|
showServerInfo,
|
||||||
|
} from './lsp/cli.js';
|
||||||
|
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
|
|
||||||
@@ -23,6 +30,43 @@ program
|
|||||||
await initConfig();
|
await initConfig();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// LSP 命令组
|
||||||
|
const lspCommand = program
|
||||||
|
.command('lsp')
|
||||||
|
.description('语言服务器管理');
|
||||||
|
|
||||||
|
lspCommand
|
||||||
|
.command('list')
|
||||||
|
.description('列出所有语言服务器及其安装状态')
|
||||||
|
.action(() => {
|
||||||
|
printServerList();
|
||||||
|
});
|
||||||
|
|
||||||
|
lspCommand
|
||||||
|
.command('install [servers...]')
|
||||||
|
.description('安装指定的语言服务器')
|
||||||
|
.option('-a, --all', '安装所有语言服务器')
|
||||||
|
.action(async (servers: string[], options: { all?: boolean }) => {
|
||||||
|
if (options.all) {
|
||||||
|
await installAllServers();
|
||||||
|
} else if (servers.length === 0) {
|
||||||
|
console.log('用法: ai-assist lsp install <server> [server2] ...');
|
||||||
|
console.log(' ai-assist lsp install --all');
|
||||||
|
console.log('\n运行 "ai-assist lsp list" 查看可用的服务器');
|
||||||
|
} else {
|
||||||
|
for (const server of servers) {
|
||||||
|
await installServer(server);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
lspCommand
|
||||||
|
.command('info <server>')
|
||||||
|
.description('显示语言服务器详细信息')
|
||||||
|
.action((server: string) => {
|
||||||
|
showServerInfo(server);
|
||||||
|
});
|
||||||
|
|
||||||
// 初始化权限系统
|
// 初始化权限系统
|
||||||
function setupPermissions(): void {
|
function setupPermissions(): void {
|
||||||
const permissionManager = getPermissionManager();
|
const permissionManager = getPermissionManager();
|
||||||
@@ -61,6 +105,9 @@ program.action(async () => {
|
|||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
const agent = new Agent(config);
|
const agent = new Agent(config);
|
||||||
|
|
||||||
|
// 初始化 LSP 系统
|
||||||
|
initLSP(process.cwd());
|
||||||
|
|
||||||
// 设置工具注册表(支持动态工具发现)
|
// 设置工具注册表(支持动态工具发现)
|
||||||
agent.setRegistry(toolRegistry);
|
agent.setRegistry(toolRegistry);
|
||||||
|
|
||||||
@@ -84,6 +131,7 @@ program.action(async () => {
|
|||||||
// 优雅退出
|
// 优雅退出
|
||||||
process.on('SIGINT', async () => {
|
process.on('SIGINT', async () => {
|
||||||
console.log('\n\n👋 再见!');
|
console.log('\n\n👋 再见!');
|
||||||
|
await shutdownLSP();
|
||||||
await sessionManager.close();
|
await sessionManager.close();
|
||||||
ui.close();
|
ui.close();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|||||||
+284
@@ -0,0 +1,284 @@
|
|||||||
|
/**
|
||||||
|
* LSP CLI 命令
|
||||||
|
* 提供语言服务器的查询和安装功能
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { execSync, spawnSync } from 'child_process';
|
||||||
|
import { getUniqueServers, type ServerConfig, type InstallConfig } from './server.js';
|
||||||
|
import type { LanguageId } from './language.js';
|
||||||
|
|
||||||
|
// 服务器状态
|
||||||
|
export interface ServerStatus {
|
||||||
|
id: string;
|
||||||
|
displayName: string;
|
||||||
|
description: string;
|
||||||
|
command: string;
|
||||||
|
installed: boolean;
|
||||||
|
languages: LanguageId[];
|
||||||
|
install: InstallConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查命令是否存在
|
||||||
|
*/
|
||||||
|
function commandExists(command: string): boolean {
|
||||||
|
try {
|
||||||
|
execSync(`which ${command}`, { stdio: 'ignore' });
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查包管理器是否可用
|
||||||
|
*/
|
||||||
|
function hasPackageManager(manager: string): boolean {
|
||||||
|
return commandExists(manager);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有服务器状态
|
||||||
|
*/
|
||||||
|
export function listServers(): ServerStatus[] {
|
||||||
|
const servers = getUniqueServers();
|
||||||
|
|
||||||
|
return servers.map((server) => ({
|
||||||
|
id: server.id,
|
||||||
|
displayName: server.config.displayName,
|
||||||
|
description: server.config.description,
|
||||||
|
command: server.config.command,
|
||||||
|
installed: commandExists(server.config.command),
|
||||||
|
languages: server.languages,
|
||||||
|
install: server.config.install,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印服务器列表
|
||||||
|
*/
|
||||||
|
export function printServerList(): void {
|
||||||
|
const servers = listServers();
|
||||||
|
|
||||||
|
console.log('\n语言服务器状态:\n');
|
||||||
|
console.log(' 状态 | 服务器 | 支持语言');
|
||||||
|
console.log(' ------+--------------------------------+------------------');
|
||||||
|
|
||||||
|
for (const server of servers) {
|
||||||
|
const status = server.installed ? ' ✓ ' : ' ✗ ';
|
||||||
|
const statusColor = server.installed ? '\x1b[32m' : '\x1b[31m';
|
||||||
|
const reset = '\x1b[0m';
|
||||||
|
const name = server.displayName.padEnd(30);
|
||||||
|
const langs = server.languages.slice(0, 3).join(', ') + (server.languages.length > 3 ? '...' : '');
|
||||||
|
|
||||||
|
console.log(` ${statusColor}${status}${reset} | ${name} | ${langs}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const installed = servers.filter((s) => s.installed).length;
|
||||||
|
console.log(`\n 已安装: ${installed}/${servers.length}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取安装命令
|
||||||
|
*/
|
||||||
|
function getInstallCommand(install: InstallConfig): { command: string; description: string } | null {
|
||||||
|
// 优先使用 npm
|
||||||
|
if (install.npm && hasPackageManager('npm')) {
|
||||||
|
return {
|
||||||
|
command: `npm install -g ${install.npm}`,
|
||||||
|
description: 'npm',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// pip
|
||||||
|
if (install.pip && hasPackageManager('pip3')) {
|
||||||
|
return {
|
||||||
|
command: `pip3 install ${install.pip}`,
|
||||||
|
description: 'pip3',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (install.pip && hasPackageManager('pip')) {
|
||||||
|
return {
|
||||||
|
command: `pip install ${install.pip}`,
|
||||||
|
description: 'pip',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// go
|
||||||
|
if (install.go && hasPackageManager('go')) {
|
||||||
|
return {
|
||||||
|
command: `go install ${install.go}`,
|
||||||
|
description: 'go install',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// rustup
|
||||||
|
if (install.rustup && hasPackageManager('rustup')) {
|
||||||
|
return {
|
||||||
|
command: `rustup component add ${install.rustup}`,
|
||||||
|
description: 'rustup',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// cargo
|
||||||
|
if (install.cargo && hasPackageManager('cargo')) {
|
||||||
|
return {
|
||||||
|
command: `cargo install ${install.cargo}`,
|
||||||
|
description: 'cargo',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// brew
|
||||||
|
if (install.brew && hasPackageManager('brew')) {
|
||||||
|
return {
|
||||||
|
command: `brew install ${install.brew}`,
|
||||||
|
description: 'Homebrew',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// gem
|
||||||
|
if (install.gem && hasPackageManager('gem')) {
|
||||||
|
return {
|
||||||
|
command: `gem install ${install.gem}`,
|
||||||
|
description: 'RubyGems',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// custom
|
||||||
|
if (install.custom) {
|
||||||
|
return {
|
||||||
|
command: install.custom,
|
||||||
|
description: '自定义命令',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安装服务器
|
||||||
|
*/
|
||||||
|
export async function installServer(serverId: string): Promise<boolean> {
|
||||||
|
const servers = listServers();
|
||||||
|
const server = servers.find(
|
||||||
|
(s) => s.id === serverId || s.displayName.toLowerCase() === serverId.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
console.error(`\x1b[31m错误: 未找到服务器 "${serverId}"\x1b[0m`);
|
||||||
|
console.log('\n可用的服务器:');
|
||||||
|
servers.forEach((s) => console.log(` - ${s.displayName} (${s.id})`));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (server.installed) {
|
||||||
|
console.log(`\x1b[32m✓ ${server.displayName} 已安装\x1b[0m`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const installCmd = getInstallCommand(server.install);
|
||||||
|
|
||||||
|
if (!installCmd) {
|
||||||
|
console.error(`\x1b[31m无法自动安装 ${server.displayName}\x1b[0m`);
|
||||||
|
if (server.install.manual) {
|
||||||
|
console.log('\n手动安装说明:');
|
||||||
|
console.log(server.install.manual);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n正在安装 ${server.displayName}...`);
|
||||||
|
console.log(`使用 ${installCmd.description}: ${installCmd.command}\n`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = spawnSync(installCmd.command, {
|
||||||
|
shell: true,
|
||||||
|
stdio: 'inherit',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.status === 0) {
|
||||||
|
console.log(`\n\x1b[32m✓ ${server.displayName} 安装成功\x1b[0m`);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
console.error(`\n\x1b[31m✗ ${server.displayName} 安装失败\x1b[0m`);
|
||||||
|
if (server.install.manual) {
|
||||||
|
console.log('\n手动安装说明:');
|
||||||
|
console.log(server.install.manual);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`\n\x1b[31m安装出错: ${error}\x1b[0m`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安装所有服务器
|
||||||
|
*/
|
||||||
|
export async function installAllServers(): Promise<void> {
|
||||||
|
const servers = listServers();
|
||||||
|
const notInstalled = servers.filter((s) => !s.installed);
|
||||||
|
|
||||||
|
if (notInstalled.length === 0) {
|
||||||
|
console.log('\x1b[32m所有语言服务器都已安装\x1b[0m');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n将安装 ${notInstalled.length} 个语言服务器:\n`);
|
||||||
|
notInstalled.forEach((s) => console.log(` - ${s.displayName}`));
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
let success = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
for (const server of notInstalled) {
|
||||||
|
const result = await installServer(server.id);
|
||||||
|
if (result) {
|
||||||
|
success++;
|
||||||
|
} else {
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n安装完成: ${success} 成功, ${failed} 失败`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示服务器详细信息
|
||||||
|
*/
|
||||||
|
export function showServerInfo(serverId: string): void {
|
||||||
|
const servers = listServers();
|
||||||
|
const server = servers.find(
|
||||||
|
(s) => s.id === serverId || s.displayName.toLowerCase() === serverId.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
console.error(`\x1b[31m错误: 未找到服务器 "${serverId}"\x1b[0m`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = server.installed ? '\x1b[32m已安装\x1b[0m' : '\x1b[31m未安装\x1b[0m';
|
||||||
|
|
||||||
|
console.log(`\n${server.displayName}`);
|
||||||
|
console.log('='.repeat(40));
|
||||||
|
console.log(`状态: ${status}`);
|
||||||
|
console.log(`命令: ${server.command}`);
|
||||||
|
console.log(`描述: ${server.description}`);
|
||||||
|
console.log(`支持语言: ${server.languages.join(', ')}`);
|
||||||
|
|
||||||
|
if (!server.installed) {
|
||||||
|
const installCmd = getInstallCommand(server.install);
|
||||||
|
if (installCmd) {
|
||||||
|
console.log(`\n安装命令 (${installCmd.description}):`);
|
||||||
|
console.log(` ${installCmd.command}`);
|
||||||
|
}
|
||||||
|
if (server.install.manual) {
|
||||||
|
console.log('\n手动安装说明:');
|
||||||
|
console.log(server.install.manual);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
@@ -0,0 +1,409 @@
|
|||||||
|
/**
|
||||||
|
* LSP 客户端
|
||||||
|
* 负责与语言服务器通信
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { spawn, ChildProcess } from 'child_process';
|
||||||
|
import {
|
||||||
|
createMessageConnection,
|
||||||
|
StreamMessageReader,
|
||||||
|
StreamMessageWriter,
|
||||||
|
MessageConnection,
|
||||||
|
} from 'vscode-jsonrpc/node.js';
|
||||||
|
import {
|
||||||
|
InitializeParams,
|
||||||
|
Diagnostic,
|
||||||
|
DiagnosticSeverity,
|
||||||
|
PublishDiagnosticsParams,
|
||||||
|
} from 'vscode-languageserver-protocol';
|
||||||
|
import * as fs from 'fs/promises';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { getLanguageId, type LanguageId } from './language.js';
|
||||||
|
import { getServerConfig, type ServerConfig } from './server.js';
|
||||||
|
|
||||||
|
// 诊断信息接口
|
||||||
|
export interface FileDiagnostic {
|
||||||
|
file: string;
|
||||||
|
line: number;
|
||||||
|
column: number;
|
||||||
|
endLine?: number;
|
||||||
|
endColumn?: number;
|
||||||
|
severity: 'error' | 'warning' | 'info' | 'hint';
|
||||||
|
message: string;
|
||||||
|
source?: string;
|
||||||
|
code?: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 客户端状态
|
||||||
|
interface ClientState {
|
||||||
|
process: ChildProcess;
|
||||||
|
connection: MessageConnection;
|
||||||
|
languageId: LanguageId;
|
||||||
|
config: ServerConfig;
|
||||||
|
initialized: boolean;
|
||||||
|
openDocuments: Set<string>;
|
||||||
|
documentVersions: Map<string, number>;
|
||||||
|
diagnostics: Map<string, Diagnostic[]>;
|
||||||
|
rootUri: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LSP 客户端管理器
|
||||||
|
* 管理多个语言服务器的生命周期
|
||||||
|
*/
|
||||||
|
export class LSPClientManager {
|
||||||
|
private clients: Map<LanguageId, ClientState> = new Map();
|
||||||
|
private rootPath: string;
|
||||||
|
|
||||||
|
constructor(rootPath?: string) {
|
||||||
|
this.rootPath = rootPath || process.cwd();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置工作区根目录
|
||||||
|
*/
|
||||||
|
setRootPath(rootPath: string): void {
|
||||||
|
this.rootPath = rootPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取或启动语言服务器
|
||||||
|
*/
|
||||||
|
async getClient(languageId: LanguageId): Promise<ClientState | undefined> {
|
||||||
|
// 如果已有客户端,直接返回
|
||||||
|
if (this.clients.has(languageId)) {
|
||||||
|
return this.clients.get(languageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取服务器配置
|
||||||
|
const config = getServerConfig(languageId);
|
||||||
|
if (!config) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动语言服务器
|
||||||
|
try {
|
||||||
|
const client = await this.startServer(languageId, config);
|
||||||
|
this.clients.set(languageId, client);
|
||||||
|
return client;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`启动语言服务器失败 (${languageId}):`, error);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动语言服务器
|
||||||
|
*/
|
||||||
|
private async startServer(languageId: LanguageId, config: ServerConfig): Promise<ClientState> {
|
||||||
|
// 检查命令是否可用
|
||||||
|
const commandExists = await this.checkCommand(config.command);
|
||||||
|
if (!commandExists) {
|
||||||
|
throw new Error(`语言服务器命令不存在: ${config.command}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动进程
|
||||||
|
const serverProcess = spawn(config.command, config.args, {
|
||||||
|
env: { ...process.env, ...config.env },
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!serverProcess.stdin || !serverProcess.stdout) {
|
||||||
|
throw new Error('无法创建进程管道');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 JSON-RPC 连接
|
||||||
|
const connection = createMessageConnection(
|
||||||
|
new StreamMessageReader(serverProcess.stdout),
|
||||||
|
new StreamMessageWriter(serverProcess.stdin)
|
||||||
|
);
|
||||||
|
|
||||||
|
const rootUri = `file://${this.rootPath}`;
|
||||||
|
|
||||||
|
const state: ClientState = {
|
||||||
|
process: serverProcess,
|
||||||
|
connection,
|
||||||
|
languageId,
|
||||||
|
config,
|
||||||
|
initialized: false,
|
||||||
|
openDocuments: new Set(),
|
||||||
|
documentVersions: new Map(),
|
||||||
|
diagnostics: new Map(),
|
||||||
|
rootUri,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听诊断通知(使用字符串方法名)
|
||||||
|
connection.onNotification('textDocument/publishDiagnostics', (params: PublishDiagnosticsParams) => {
|
||||||
|
const filePath = params.uri.replace('file://', '');
|
||||||
|
state.diagnostics.set(filePath, params.diagnostics);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听进程错误
|
||||||
|
serverProcess.on('error', (error) => {
|
||||||
|
console.error(`语言服务器错误 (${languageId}):`, error);
|
||||||
|
});
|
||||||
|
|
||||||
|
serverProcess.on('exit', () => {
|
||||||
|
this.clients.delete(languageId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 启动连接
|
||||||
|
connection.listen();
|
||||||
|
|
||||||
|
// 初始化服务器
|
||||||
|
await this.initializeServer(state, config);
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化语言服务器
|
||||||
|
*/
|
||||||
|
private async initializeServer(state: ClientState, config: ServerConfig): Promise<void> {
|
||||||
|
const initParams: InitializeParams = {
|
||||||
|
processId: process.pid,
|
||||||
|
rootUri: state.rootUri,
|
||||||
|
capabilities: {
|
||||||
|
textDocument: {
|
||||||
|
synchronization: {
|
||||||
|
dynamicRegistration: false,
|
||||||
|
willSave: false,
|
||||||
|
willSaveWaitUntil: false,
|
||||||
|
didSave: true,
|
||||||
|
},
|
||||||
|
publishDiagnostics: {
|
||||||
|
relatedInformation: true,
|
||||||
|
tagSupport: { valueSet: [1, 2] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
workspace: {
|
||||||
|
workspaceFolders: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
workspaceFolders: [
|
||||||
|
{
|
||||||
|
uri: state.rootUri,
|
||||||
|
name: path.basename(this.rootPath),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
initializationOptions: config.initializationOptions,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用字符串方法名发送请求
|
||||||
|
await state.connection.sendRequest('initialize', initParams);
|
||||||
|
await state.connection.sendNotification('initialized', {});
|
||||||
|
state.initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知文件变更(打开或更新)
|
||||||
|
* @returns 是否是首次启动服务器(需要更长等待时间)
|
||||||
|
*/
|
||||||
|
async touchFile(filePath: string, isNew: boolean = false): Promise<boolean> {
|
||||||
|
const absolutePath = path.isAbsolute(filePath)
|
||||||
|
? filePath
|
||||||
|
: path.join(this.rootPath, filePath);
|
||||||
|
|
||||||
|
const languageId = getLanguageId(absolutePath);
|
||||||
|
if (!languageId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是首次启动
|
||||||
|
const wasRunning = this.clients.has(languageId);
|
||||||
|
|
||||||
|
const client = await this.getClient(languageId);
|
||||||
|
if (!client || !client.initialized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFirstStart = !wasRunning;
|
||||||
|
|
||||||
|
const uri = `file://${absolutePath}`;
|
||||||
|
const content = await fs.readFile(absolutePath, 'utf-8');
|
||||||
|
|
||||||
|
if (!client.openDocuments.has(absolutePath)) {
|
||||||
|
// 打开文档(使用字符串方法名)
|
||||||
|
await client.connection.sendNotification('textDocument/didOpen', {
|
||||||
|
textDocument: {
|
||||||
|
uri,
|
||||||
|
languageId,
|
||||||
|
version: 1,
|
||||||
|
text: content,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
client.openDocuments.add(absolutePath);
|
||||||
|
client.documentVersions.set(absolutePath, 1);
|
||||||
|
} else {
|
||||||
|
// 更新文档
|
||||||
|
const version = (client.documentVersions.get(absolutePath) || 0) + 1;
|
||||||
|
client.documentVersions.set(absolutePath, version);
|
||||||
|
|
||||||
|
await client.connection.sendNotification('textDocument/didChange', {
|
||||||
|
textDocument: {
|
||||||
|
uri,
|
||||||
|
version,
|
||||||
|
},
|
||||||
|
contentChanges: [{ text: content }],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待诊断结果(给服务器一些时间处理)
|
||||||
|
await this.waitForDiagnostics(100);
|
||||||
|
|
||||||
|
return isFirstStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭文档
|
||||||
|
*/
|
||||||
|
async closeFile(filePath: string): Promise<void> {
|
||||||
|
const absolutePath = path.isAbsolute(filePath)
|
||||||
|
? filePath
|
||||||
|
: path.join(this.rootPath, filePath);
|
||||||
|
|
||||||
|
const languageId = getLanguageId(absolutePath);
|
||||||
|
if (!languageId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = this.clients.get(languageId);
|
||||||
|
if (!client || !client.openDocuments.has(absolutePath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uri = `file://${absolutePath}`;
|
||||||
|
await client.connection.sendNotification('textDocument/didClose', {
|
||||||
|
textDocument: { uri },
|
||||||
|
});
|
||||||
|
|
||||||
|
client.openDocuments.delete(absolutePath);
|
||||||
|
client.documentVersions.delete(absolutePath);
|
||||||
|
client.diagnostics.delete(absolutePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件诊断信息
|
||||||
|
*/
|
||||||
|
getDiagnostics(filePath?: string): Map<string, FileDiagnostic[]> {
|
||||||
|
const result = new Map<string, FileDiagnostic[]>();
|
||||||
|
|
||||||
|
for (const client of this.clients.values()) {
|
||||||
|
for (const [file, diagnostics] of client.diagnostics) {
|
||||||
|
if (filePath && file !== filePath) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileDiagnostics = diagnostics.map((d) => this.convertDiagnostic(file, d));
|
||||||
|
if (fileDiagnostics.length > 0) {
|
||||||
|
const existing = result.get(file) || [];
|
||||||
|
result.set(file, [...existing, ...fileDiagnostics]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单个文件的诊断信息
|
||||||
|
*/
|
||||||
|
getFileDiagnostics(filePath: string): FileDiagnostic[] {
|
||||||
|
const absolutePath = path.isAbsolute(filePath)
|
||||||
|
? filePath
|
||||||
|
: path.join(this.rootPath, filePath);
|
||||||
|
|
||||||
|
for (const client of this.clients.values()) {
|
||||||
|
const diagnostics = client.diagnostics.get(absolutePath);
|
||||||
|
if (diagnostics) {
|
||||||
|
return diagnostics.map((d) => this.convertDiagnostic(absolutePath, d));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换诊断信息格式
|
||||||
|
*/
|
||||||
|
private convertDiagnostic(file: string, diagnostic: Diagnostic): FileDiagnostic {
|
||||||
|
return {
|
||||||
|
file,
|
||||||
|
line: diagnostic.range.start.line + 1,
|
||||||
|
column: diagnostic.range.start.character + 1,
|
||||||
|
endLine: diagnostic.range.end.line + 1,
|
||||||
|
endColumn: diagnostic.range.end.character + 1,
|
||||||
|
severity: this.convertSeverity(diagnostic.severity),
|
||||||
|
message: diagnostic.message,
|
||||||
|
source: diagnostic.source,
|
||||||
|
code: diagnostic.code as string | number | undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换严重性
|
||||||
|
*/
|
||||||
|
private convertSeverity(severity?: DiagnosticSeverity): FileDiagnostic['severity'] {
|
||||||
|
switch (severity) {
|
||||||
|
case DiagnosticSeverity.Error:
|
||||||
|
return 'error';
|
||||||
|
case DiagnosticSeverity.Warning:
|
||||||
|
return 'warning';
|
||||||
|
case DiagnosticSeverity.Information:
|
||||||
|
return 'info';
|
||||||
|
case DiagnosticSeverity.Hint:
|
||||||
|
return 'hint';
|
||||||
|
default:
|
||||||
|
return 'error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查命令是否存在
|
||||||
|
*/
|
||||||
|
private async checkCommand(command: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const { execSync } = await import('child_process');
|
||||||
|
execSync(`which ${command}`, { stdio: 'ignore' });
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 等待诊断结果
|
||||||
|
*/
|
||||||
|
private waitForDiagnostics(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭所有客户端
|
||||||
|
*/
|
||||||
|
async shutdown(): Promise<void> {
|
||||||
|
for (const [languageId, client] of this.clients) {
|
||||||
|
try {
|
||||||
|
client.connection.dispose();
|
||||||
|
client.process.kill();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`关闭语言服务器失败 (${languageId}):`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.clients.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查服务器是否运行中
|
||||||
|
*/
|
||||||
|
isServerRunning(languageId: LanguageId): boolean {
|
||||||
|
return this.clients.has(languageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取运行中的服务器列表
|
||||||
|
*/
|
||||||
|
getRunningServers(): LanguageId[] {
|
||||||
|
return Array.from(this.clients.keys());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
/**
|
||||||
|
* LSP 模块入口
|
||||||
|
* 提供简化的 API 供其他模块使用
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { LSPClientManager, type FileDiagnostic } from './client.js';
|
||||||
|
import { getLanguageId, isLanguageSupported } from './language.js';
|
||||||
|
|
||||||
|
// 全局 LSP 管理器实例
|
||||||
|
let lspManager: LSPClientManager | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 LSP 系统
|
||||||
|
*/
|
||||||
|
export function initLSP(rootPath?: string): void {
|
||||||
|
if (lspManager) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lspManager = new LSPClientManager(rootPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 LSP 管理器实例
|
||||||
|
*/
|
||||||
|
export function getLSPManager(): LSPClientManager | null {
|
||||||
|
return lspManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知 LSP 文件已变更
|
||||||
|
* 用于编辑/写入文件后通知语言服务器
|
||||||
|
* @returns 是否是首次启动服务器(需要更长等待时间)
|
||||||
|
*/
|
||||||
|
export async function touchFile(filePath: string, isNew: boolean = false): Promise<boolean> {
|
||||||
|
if (!lspManager) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLanguageSupported(filePath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await lspManager.touchFile(filePath, isNew);
|
||||||
|
} catch (error) {
|
||||||
|
// 静默失败,LSP 是增强功能
|
||||||
|
console.error('LSP touchFile 错误:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有文件的诊断信息
|
||||||
|
*/
|
||||||
|
export function getDiagnostics(): Map<string, FileDiagnostic[]> {
|
||||||
|
if (!lspManager) {
|
||||||
|
return new Map();
|
||||||
|
}
|
||||||
|
return lspManager.getDiagnostics();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单个文件的诊断信息
|
||||||
|
*/
|
||||||
|
export function getFileDiagnostics(filePath: string): FileDiagnostic[] {
|
||||||
|
if (!lspManager) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return lspManager.getFileDiagnostics(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化诊断信息为字符串
|
||||||
|
* 用于返回给 AI
|
||||||
|
*/
|
||||||
|
export function formatDiagnostics(diagnostics: FileDiagnostic[]): string {
|
||||||
|
if (diagnostics.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines: string[] = [];
|
||||||
|
for (const d of diagnostics) {
|
||||||
|
const location = `${d.line}:${d.column}`;
|
||||||
|
const severity = d.severity.toUpperCase();
|
||||||
|
const code = d.code ? ` [${d.code}]` : '';
|
||||||
|
const source = d.source ? ` (${d.source})` : '';
|
||||||
|
lines.push(` ${location} ${severity}${code}: ${d.message}${source}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件诊断并格式化为 AI 可读的格式
|
||||||
|
*/
|
||||||
|
export async function getFormattedFileDiagnostics(filePath: string): Promise<string> {
|
||||||
|
const diagnostics = getFileDiagnostics(filePath);
|
||||||
|
|
||||||
|
if (diagnostics.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors = diagnostics.filter(d => d.severity === 'error');
|
||||||
|
const warnings = diagnostics.filter(d => d.severity === 'warning');
|
||||||
|
|
||||||
|
let result = `\n<file_diagnostics file="${filePath}">\n`;
|
||||||
|
result += `发现 ${errors.length} 个错误, ${warnings.length} 个警告:\n`;
|
||||||
|
result += formatDiagnostics(diagnostics);
|
||||||
|
result += '\n</file_diagnostics>';
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭 LSP 系统
|
||||||
|
*/
|
||||||
|
export async function shutdownLSP(): Promise<void> {
|
||||||
|
if (lspManager) {
|
||||||
|
await lspManager.shutdown();
|
||||||
|
lspManager = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出类型
|
||||||
|
export type { FileDiagnostic } from './client.js';
|
||||||
|
export { getLanguageId, isLanguageSupported, getSupportedExtensions } from './language.js';
|
||||||
|
export { getServerConfig, hasServerConfig, getSupportedLanguages } from './server.js';
|
||||||
|
export { LSPClientManager } from './client.js';
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* 文件扩展名到 LSP languageId 的映射
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 语言 ID 定义
|
||||||
|
export type LanguageId =
|
||||||
|
| 'typescript'
|
||||||
|
| 'javascript'
|
||||||
|
| 'typescriptreact'
|
||||||
|
| 'javascriptreact'
|
||||||
|
| 'python'
|
||||||
|
| 'go'
|
||||||
|
| 'rust'
|
||||||
|
| 'java'
|
||||||
|
| 'c'
|
||||||
|
| 'cpp'
|
||||||
|
| 'csharp'
|
||||||
|
| 'php'
|
||||||
|
| 'ruby'
|
||||||
|
| 'swift'
|
||||||
|
| 'kotlin'
|
||||||
|
| 'scala'
|
||||||
|
| 'html'
|
||||||
|
| 'css'
|
||||||
|
| 'scss'
|
||||||
|
| 'less'
|
||||||
|
| 'json'
|
||||||
|
| 'yaml'
|
||||||
|
| 'markdown'
|
||||||
|
| 'vue'
|
||||||
|
| 'svelte';
|
||||||
|
|
||||||
|
// 扩展名到语言 ID 的映射
|
||||||
|
const extensionToLanguageId: Record<string, LanguageId> = {
|
||||||
|
// TypeScript/JavaScript
|
||||||
|
'.ts': 'typescript',
|
||||||
|
'.tsx': 'typescriptreact',
|
||||||
|
'.js': 'javascript',
|
||||||
|
'.jsx': 'javascriptreact',
|
||||||
|
'.mjs': 'javascript',
|
||||||
|
'.cjs': 'javascript',
|
||||||
|
'.mts': 'typescript',
|
||||||
|
'.cts': 'typescript',
|
||||||
|
|
||||||
|
// Python
|
||||||
|
'.py': 'python',
|
||||||
|
'.pyi': 'python',
|
||||||
|
'.pyw': 'python',
|
||||||
|
|
||||||
|
// Go
|
||||||
|
'.go': 'go',
|
||||||
|
|
||||||
|
// Rust
|
||||||
|
'.rs': 'rust',
|
||||||
|
|
||||||
|
// Java
|
||||||
|
'.java': 'java',
|
||||||
|
|
||||||
|
// C/C++
|
||||||
|
'.c': 'c',
|
||||||
|
'.h': 'c',
|
||||||
|
'.cpp': 'cpp',
|
||||||
|
'.cc': 'cpp',
|
||||||
|
'.cxx': 'cpp',
|
||||||
|
'.hpp': 'cpp',
|
||||||
|
'.hh': 'cpp',
|
||||||
|
'.hxx': 'cpp',
|
||||||
|
|
||||||
|
// C#
|
||||||
|
'.cs': 'csharp',
|
||||||
|
|
||||||
|
// PHP
|
||||||
|
'.php': 'php',
|
||||||
|
|
||||||
|
// Ruby
|
||||||
|
'.rb': 'ruby',
|
||||||
|
'.rake': 'ruby',
|
||||||
|
|
||||||
|
// Swift
|
||||||
|
'.swift': 'swift',
|
||||||
|
|
||||||
|
// Kotlin
|
||||||
|
'.kt': 'kotlin',
|
||||||
|
'.kts': 'kotlin',
|
||||||
|
|
||||||
|
// Scala
|
||||||
|
'.scala': 'scala',
|
||||||
|
'.sc': 'scala',
|
||||||
|
|
||||||
|
// Web
|
||||||
|
'.html': 'html',
|
||||||
|
'.htm': 'html',
|
||||||
|
'.css': 'css',
|
||||||
|
'.scss': 'scss',
|
||||||
|
'.less': 'less',
|
||||||
|
'.vue': 'vue',
|
||||||
|
'.svelte': 'svelte',
|
||||||
|
|
||||||
|
// Data formats
|
||||||
|
'.json': 'json',
|
||||||
|
'.yaml': 'yaml',
|
||||||
|
'.yml': 'yaml',
|
||||||
|
|
||||||
|
// Markdown
|
||||||
|
'.md': 'markdown',
|
||||||
|
'.markdown': 'markdown',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据文件路径获取语言 ID
|
||||||
|
*/
|
||||||
|
export function getLanguageId(filePath: string): LanguageId | undefined {
|
||||||
|
const ext = getExtension(filePath);
|
||||||
|
return extensionToLanguageId[ext];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件扩展名(小写)
|
||||||
|
*/
|
||||||
|
function getExtension(filePath: string): string {
|
||||||
|
const lastDot = filePath.lastIndexOf('.');
|
||||||
|
if (lastDot === -1) return '';
|
||||||
|
return filePath.slice(lastDot).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查文件是否支持 LSP
|
||||||
|
*/
|
||||||
|
export function isLanguageSupported(filePath: string): boolean {
|
||||||
|
return getLanguageId(filePath) !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有支持的扩展名
|
||||||
|
*/
|
||||||
|
export function getSupportedExtensions(): string[] {
|
||||||
|
return Object.keys(extensionToLanguageId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,336 @@
|
|||||||
|
/**
|
||||||
|
* 语言服务器定义
|
||||||
|
* 定义各种语言的 LSP 服务器配置
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { LanguageId } from './language.js';
|
||||||
|
|
||||||
|
// 安装命令配置
|
||||||
|
export interface InstallConfig {
|
||||||
|
/** npm 全局安装包名 */
|
||||||
|
npm?: string;
|
||||||
|
/** pip 安装包名 */
|
||||||
|
pip?: string;
|
||||||
|
/** go install 路径 */
|
||||||
|
go?: string;
|
||||||
|
/** cargo install 包名 */
|
||||||
|
cargo?: string;
|
||||||
|
/** brew 安装包名 */
|
||||||
|
brew?: string;
|
||||||
|
/** gem 安装包名 */
|
||||||
|
gem?: string;
|
||||||
|
/** rustup component */
|
||||||
|
rustup?: string;
|
||||||
|
/** 自定义安装命令 */
|
||||||
|
custom?: string;
|
||||||
|
/** 安装说明(当无法自动安装时) */
|
||||||
|
manual?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 服务器配置接口
|
||||||
|
export interface ServerConfig {
|
||||||
|
/** 服务器命令 */
|
||||||
|
command: string;
|
||||||
|
/** 命令参数 */
|
||||||
|
args: string[];
|
||||||
|
/** 环境变量 */
|
||||||
|
env?: Record<string, string>;
|
||||||
|
/** 初始化选项 */
|
||||||
|
initializationOptions?: Record<string, unknown>;
|
||||||
|
/** 安装配置 */
|
||||||
|
install: InstallConfig;
|
||||||
|
/** 显示名称 */
|
||||||
|
displayName: string;
|
||||||
|
/** 描述 */
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 语言服务器定义
|
||||||
|
const serverConfigs: Partial<Record<LanguageId, ServerConfig>> = {
|
||||||
|
// TypeScript/JavaScript - 使用 typescript-language-server
|
||||||
|
typescript: {
|
||||||
|
command: 'typescript-language-server',
|
||||||
|
args: ['--stdio'],
|
||||||
|
initializationOptions: {
|
||||||
|
preferences: {
|
||||||
|
includeInlayParameterNameHints: 'all',
|
||||||
|
includeInlayPropertyDeclarationTypeHints: true,
|
||||||
|
includeInlayFunctionLikeReturnTypeHints: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
install: {
|
||||||
|
npm: 'typescript-language-server typescript',
|
||||||
|
},
|
||||||
|
displayName: 'TypeScript',
|
||||||
|
description: 'TypeScript/JavaScript 语言服务器',
|
||||||
|
},
|
||||||
|
javascript: {
|
||||||
|
command: 'typescript-language-server',
|
||||||
|
args: ['--stdio'],
|
||||||
|
install: {
|
||||||
|
npm: 'typescript-language-server typescript',
|
||||||
|
},
|
||||||
|
displayName: 'JavaScript',
|
||||||
|
description: 'JavaScript 语言服务器(共用 TypeScript)',
|
||||||
|
},
|
||||||
|
typescriptreact: {
|
||||||
|
command: 'typescript-language-server',
|
||||||
|
args: ['--stdio'],
|
||||||
|
install: {
|
||||||
|
npm: 'typescript-language-server typescript',
|
||||||
|
},
|
||||||
|
displayName: 'TypeScript React',
|
||||||
|
description: 'TSX 语言服务器(共用 TypeScript)',
|
||||||
|
},
|
||||||
|
javascriptreact: {
|
||||||
|
command: 'typescript-language-server',
|
||||||
|
args: ['--stdio'],
|
||||||
|
install: {
|
||||||
|
npm: 'typescript-language-server typescript',
|
||||||
|
},
|
||||||
|
displayName: 'JavaScript React',
|
||||||
|
description: 'JSX 语言服务器(共用 TypeScript)',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Python - 使用 pyright
|
||||||
|
python: {
|
||||||
|
command: 'pyright-langserver',
|
||||||
|
args: ['--stdio'],
|
||||||
|
install: {
|
||||||
|
npm: 'pyright',
|
||||||
|
pip: 'pyright',
|
||||||
|
},
|
||||||
|
displayName: 'Python',
|
||||||
|
description: 'Python 语言服务器 (Pyright)',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Go - 使用 gopls
|
||||||
|
go: {
|
||||||
|
command: 'gopls',
|
||||||
|
args: ['serve'],
|
||||||
|
install: {
|
||||||
|
go: 'golang.org/x/tools/gopls@latest',
|
||||||
|
},
|
||||||
|
displayName: 'Go',
|
||||||
|
description: 'Go 语言服务器 (gopls)',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Rust - 使用 rust-analyzer
|
||||||
|
rust: {
|
||||||
|
command: 'rust-analyzer',
|
||||||
|
args: [],
|
||||||
|
install: {
|
||||||
|
rustup: 'rust-analyzer',
|
||||||
|
brew: 'rust-analyzer',
|
||||||
|
},
|
||||||
|
displayName: 'Rust',
|
||||||
|
description: 'Rust 语言服务器 (rust-analyzer)',
|
||||||
|
},
|
||||||
|
|
||||||
|
// C/C++ - 使用 clangd
|
||||||
|
c: {
|
||||||
|
command: 'clangd',
|
||||||
|
args: ['--background-index'],
|
||||||
|
install: {
|
||||||
|
brew: 'llvm',
|
||||||
|
manual: 'Ubuntu: sudo apt install clangd\nmacOS: brew install llvm',
|
||||||
|
},
|
||||||
|
displayName: 'C',
|
||||||
|
description: 'C 语言服务器 (clangd)',
|
||||||
|
},
|
||||||
|
cpp: {
|
||||||
|
command: 'clangd',
|
||||||
|
args: ['--background-index'],
|
||||||
|
install: {
|
||||||
|
brew: 'llvm',
|
||||||
|
manual: 'Ubuntu: sudo apt install clangd\nmacOS: brew install llvm',
|
||||||
|
},
|
||||||
|
displayName: 'C++',
|
||||||
|
description: 'C++ 语言服务器 (clangd)',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Java - 使用 jdtls
|
||||||
|
java: {
|
||||||
|
command: 'jdtls',
|
||||||
|
args: [],
|
||||||
|
install: {
|
||||||
|
brew: 'jdtls',
|
||||||
|
manual: '请访问 https://github.com/eclipse/eclipse.jdt.ls 获取安装说明',
|
||||||
|
},
|
||||||
|
displayName: 'Java',
|
||||||
|
description: 'Java 语言服务器 (Eclipse JDT.LS)',
|
||||||
|
},
|
||||||
|
|
||||||
|
// C# - 使用 OmniSharp
|
||||||
|
csharp: {
|
||||||
|
command: 'omnisharp',
|
||||||
|
args: ['-lsp'],
|
||||||
|
install: {
|
||||||
|
brew: 'omnisharp/omnisharp-roslyn/omnisharp-mono',
|
||||||
|
manual: '请访问 https://github.com/OmniSharp/omnisharp-roslyn 获取安装说明',
|
||||||
|
},
|
||||||
|
displayName: 'C#',
|
||||||
|
description: 'C# 语言服务器 (OmniSharp)',
|
||||||
|
},
|
||||||
|
|
||||||
|
// PHP - 使用 intelephense
|
||||||
|
php: {
|
||||||
|
command: 'intelephense',
|
||||||
|
args: ['--stdio'],
|
||||||
|
install: {
|
||||||
|
npm: 'intelephense',
|
||||||
|
},
|
||||||
|
displayName: 'PHP',
|
||||||
|
description: 'PHP 语言服务器 (Intelephense)',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Ruby - 使用 solargraph
|
||||||
|
ruby: {
|
||||||
|
command: 'solargraph',
|
||||||
|
args: ['stdio'],
|
||||||
|
install: {
|
||||||
|
gem: 'solargraph',
|
||||||
|
},
|
||||||
|
displayName: 'Ruby',
|
||||||
|
description: 'Ruby 语言服务器 (Solargraph)',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Vue - 使用 vue-language-server
|
||||||
|
vue: {
|
||||||
|
command: 'vue-language-server',
|
||||||
|
args: ['--stdio'],
|
||||||
|
install: {
|
||||||
|
npm: '@vue/language-server',
|
||||||
|
},
|
||||||
|
displayName: 'Vue',
|
||||||
|
description: 'Vue 语言服务器',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Svelte - 使用 svelte-language-server
|
||||||
|
svelte: {
|
||||||
|
command: 'svelteserver',
|
||||||
|
args: ['--stdio'],
|
||||||
|
install: {
|
||||||
|
npm: 'svelte-language-server',
|
||||||
|
},
|
||||||
|
displayName: 'Svelte',
|
||||||
|
description: 'Svelte 语言服务器',
|
||||||
|
},
|
||||||
|
|
||||||
|
// HTML - 使用 vscode-html-language-server
|
||||||
|
html: {
|
||||||
|
command: 'vscode-html-language-server',
|
||||||
|
args: ['--stdio'],
|
||||||
|
install: {
|
||||||
|
npm: 'vscode-langservers-extracted',
|
||||||
|
},
|
||||||
|
displayName: 'HTML',
|
||||||
|
description: 'HTML 语言服务器',
|
||||||
|
},
|
||||||
|
|
||||||
|
// CSS/SCSS/Less - 使用 vscode-css-language-server
|
||||||
|
css: {
|
||||||
|
command: 'vscode-css-language-server',
|
||||||
|
args: ['--stdio'],
|
||||||
|
install: {
|
||||||
|
npm: 'vscode-langservers-extracted',
|
||||||
|
},
|
||||||
|
displayName: 'CSS',
|
||||||
|
description: 'CSS 语言服务器',
|
||||||
|
},
|
||||||
|
scss: {
|
||||||
|
command: 'vscode-css-language-server',
|
||||||
|
args: ['--stdio'],
|
||||||
|
install: {
|
||||||
|
npm: 'vscode-langservers-extracted',
|
||||||
|
},
|
||||||
|
displayName: 'SCSS',
|
||||||
|
description: 'SCSS 语言服务器(共用 CSS)',
|
||||||
|
},
|
||||||
|
less: {
|
||||||
|
command: 'vscode-css-language-server',
|
||||||
|
args: ['--stdio'],
|
||||||
|
install: {
|
||||||
|
npm: 'vscode-langservers-extracted',
|
||||||
|
},
|
||||||
|
displayName: 'Less',
|
||||||
|
description: 'Less 语言服务器(共用 CSS)',
|
||||||
|
},
|
||||||
|
|
||||||
|
// JSON - 使用 vscode-json-language-server
|
||||||
|
json: {
|
||||||
|
command: 'vscode-json-language-server',
|
||||||
|
args: ['--stdio'],
|
||||||
|
install: {
|
||||||
|
npm: 'vscode-langservers-extracted',
|
||||||
|
},
|
||||||
|
displayName: 'JSON',
|
||||||
|
description: 'JSON 语言服务器',
|
||||||
|
},
|
||||||
|
|
||||||
|
// YAML - 使用 yaml-language-server
|
||||||
|
yaml: {
|
||||||
|
command: 'yaml-language-server',
|
||||||
|
args: ['--stdio'],
|
||||||
|
install: {
|
||||||
|
npm: 'yaml-language-server',
|
||||||
|
},
|
||||||
|
displayName: 'YAML',
|
||||||
|
description: 'YAML 语言服务器',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取语言服务器配置
|
||||||
|
*/
|
||||||
|
export function getServerConfig(languageId: LanguageId): ServerConfig | undefined {
|
||||||
|
return serverConfigs[languageId];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查语言是否有可用的服务器配置
|
||||||
|
*/
|
||||||
|
export function hasServerConfig(languageId: LanguageId): boolean {
|
||||||
|
return languageId in serverConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有支持的语言 ID
|
||||||
|
*/
|
||||||
|
export function getSupportedLanguages(): LanguageId[] {
|
||||||
|
return Object.keys(serverConfigs) as LanguageId[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有服务器配置(用于 CLI)
|
||||||
|
*/
|
||||||
|
export function getAllServerConfigs(): Partial<Record<LanguageId, ServerConfig>> {
|
||||||
|
return serverConfigs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取唯一的服务器列表(去重相同命令的服务器)
|
||||||
|
*/
|
||||||
|
export function getUniqueServers(): Array<{ id: string; config: ServerConfig; languages: LanguageId[] }> {
|
||||||
|
const commandMap = new Map<string, { config: ServerConfig; languages: LanguageId[] }>();
|
||||||
|
|
||||||
|
for (const [langId, config] of Object.entries(serverConfigs)) {
|
||||||
|
if (!config) continue;
|
||||||
|
|
||||||
|
const existing = commandMap.get(config.command);
|
||||||
|
if (existing) {
|
||||||
|
existing.languages.push(langId as LanguageId);
|
||||||
|
} else {
|
||||||
|
commandMap.set(config.command, {
|
||||||
|
config,
|
||||||
|
languages: [langId as LanguageId],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(commandMap.entries()).map(([command, data]) => ({
|
||||||
|
id: command,
|
||||||
|
config: data.config,
|
||||||
|
languages: data.languages,
|
||||||
|
}));
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import type { ToolResult } from '../../types/index.js';
|
|||||||
import type { ToolWithMetadata } from '../types.js';
|
import type { ToolWithMetadata } from '../types.js';
|
||||||
import { loadDescription } from '../load_description.js';
|
import { loadDescription } from '../load_description.js';
|
||||||
import { getPermissionManager } from '../../permission/index.js';
|
import { getPermissionManager } from '../../permission/index.js';
|
||||||
|
import { touchFile, getFormattedFileDiagnostics, isLanguageSupported } from '../../lsp/index.js';
|
||||||
|
|
||||||
export const editFileTool: ToolWithMetadata = {
|
export const editFileTool: ToolWithMetadata = {
|
||||||
name: 'edit_file',
|
name: 'edit_file',
|
||||||
@@ -13,7 +14,7 @@ export const editFileTool: ToolWithMetadata = {
|
|||||||
category: 'filesystem',
|
category: 'filesystem',
|
||||||
description: '编辑文件内容(查找替换)',
|
description: '编辑文件内容(查找替换)',
|
||||||
keywords: ['edit', 'file', 'replace', 'modify', 'change', 'update', '编辑', '文件', '替换', '修改', '更新'],
|
keywords: ['edit', 'file', 'replace', 'modify', 'change', 'update', '编辑', '文件', '替换', '修改', '更新'],
|
||||||
deferLoading: true,
|
deferLoading: false, // 核心工具,始终可用
|
||||||
},
|
},
|
||||||
parameters: {
|
parameters: {
|
||||||
path: {
|
path: {
|
||||||
@@ -87,9 +88,27 @@ export const editFileTool: ToolWithMetadata = {
|
|||||||
const newContent = content.replace(oldString, newString);
|
const newContent = content.replace(oldString, newString);
|
||||||
await fs.writeFile(absolutePath, newContent, 'utf-8');
|
await fs.writeFile(absolutePath, newContent, 'utf-8');
|
||||||
|
|
||||||
|
let output = `文件已编辑: ${absolutePath}`;
|
||||||
|
|
||||||
|
// 如果支持 LSP,通知语言服务器并获取诊断
|
||||||
|
if (isLanguageSupported(absolutePath)) {
|
||||||
|
try {
|
||||||
|
const isFirstStart = await touchFile(absolutePath, false);
|
||||||
|
// 首次启动需要更长时间,后续只需短暂等待
|
||||||
|
const waitTime = isFirstStart ? 2000 : 300;
|
||||||
|
await new Promise(resolve => setTimeout(resolve, waitTime));
|
||||||
|
const diagnostics = await getFormattedFileDiagnostics(absolutePath);
|
||||||
|
if (diagnostics) {
|
||||||
|
output += `\n\n⚠️ 代码检查发现问题,请修复:${diagnostics}`;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// LSP 错误不影响主流程
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
output: `文件已编辑: ${absolutePath}`,
|
output,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const readFileTool: ToolWithMetadata = {
|
|||||||
category: 'filesystem',
|
category: 'filesystem',
|
||||||
description: '读取文件内容',
|
description: '读取文件内容',
|
||||||
keywords: ['read', 'file', 'content', 'cat', 'view', 'open', '读取', '文件', '内容', '查看', '打开'],
|
keywords: ['read', 'file', 'content', 'cat', 'view', 'open', '读取', '文件', '内容', '查看', '打开'],
|
||||||
deferLoading: true,
|
deferLoading: false, // 核心工具,始终可用
|
||||||
},
|
},
|
||||||
parameters: {
|
parameters: {
|
||||||
path: {
|
path: {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { ToolResult } from '../../types/index.js';
|
|||||||
import type { ToolWithMetadata } from '../types.js';
|
import type { ToolWithMetadata } from '../types.js';
|
||||||
import { loadDescription } from '../load_description.js';
|
import { loadDescription } from '../load_description.js';
|
||||||
import { getPermissionManager } from '../../permission/index.js';
|
import { getPermissionManager } from '../../permission/index.js';
|
||||||
|
import { touchFile, getFormattedFileDiagnostics, isLanguageSupported } from '../../lsp/index.js';
|
||||||
|
|
||||||
export const writeFileTool: ToolWithMetadata = {
|
export const writeFileTool: ToolWithMetadata = {
|
||||||
name: 'write_file',
|
name: 'write_file',
|
||||||
@@ -13,7 +14,7 @@ export const writeFileTool: ToolWithMetadata = {
|
|||||||
category: 'filesystem',
|
category: 'filesystem',
|
||||||
description: '写入文件内容',
|
description: '写入文件内容',
|
||||||
keywords: ['write', 'file', 'save', 'create', '写入', '文件', '保存', '创建', '新建'],
|
keywords: ['write', 'file', 'save', 'create', '写入', '文件', '保存', '创建', '新建'],
|
||||||
deferLoading: true,
|
deferLoading: false, // 核心工具,始终可用
|
||||||
},
|
},
|
||||||
parameters: {
|
parameters: {
|
||||||
path: {
|
path: {
|
||||||
@@ -61,9 +62,28 @@ export const writeFileTool: ToolWithMetadata = {
|
|||||||
try {
|
try {
|
||||||
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
||||||
await fs.writeFile(absolutePath, content, 'utf-8');
|
await fs.writeFile(absolutePath, content, 'utf-8');
|
||||||
|
|
||||||
|
let output = `文件已写入: ${absolutePath}`;
|
||||||
|
|
||||||
|
// 如果支持 LSP,通知语言服务器并获取诊断
|
||||||
|
if (isLanguageSupported(absolutePath)) {
|
||||||
|
try {
|
||||||
|
const isFirstStart = await touchFile(absolutePath, true);
|
||||||
|
// 首次启动需要更长时间,后续只需短暂等待
|
||||||
|
const waitTime = isFirstStart ? 2000 : 300;
|
||||||
|
await new Promise(resolve => setTimeout(resolve, waitTime));
|
||||||
|
const diagnostics = await getFormattedFileDiagnostics(absolutePath);
|
||||||
|
if (diagnostics) {
|
||||||
|
output += `\n\n⚠️ 代码检查发现问题,请修复:${diagnostics}`;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// LSP 错误不影响主流程
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
output: `文件已写入: ${absolutePath}`,
|
output,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ const DEFAULT_SYSTEM_PROMPT = `你是一个运行在终端中的 AI 编程助手
|
|||||||
2. 执行可能有风险的命令前,先向用户确认
|
2. 执行可能有风险的命令前,先向用户确认
|
||||||
3. 给出清晰、简洁的回答
|
3. 给出清晰、简洁的回答
|
||||||
|
|
||||||
|
重要的工具使用规则:
|
||||||
|
- 创建或修改文件时,必须使用 write_file 或 edit_file 工具,不要使用 bash 命令(如 cat、echo 等)
|
||||||
|
- write_file 和 edit_file 工具集成了代码诊断功能,可以在写入后自动检查代码错误
|
||||||
|
- bash 工具仅用于运行命令、安装依赖、执行脚本等操作,不要用于文件内容的创建和修改
|
||||||
|
|
||||||
当前工作目录: ${process.cwd()}
|
当前工作目录: ${process.cwd()}
|
||||||
操作系统: ${process.platform}`;
|
操作系统: ${process.platform}`;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user