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:
2025-12-11 00:01:58 +08:00
parent 1e0ecc2de7
commit 929f6f7850
12 changed files with 1430 additions and 5 deletions
+36
View File
@@ -17,6 +17,8 @@
"inquirer": "^12.0.0",
"ora": "^8.1.0",
"tree-sitter-bash": "^0.25.1",
"vscode-jsonrpc": "^8.2.1",
"vscode-languageserver-protocol": "^3.17.5",
"web-tree-sitter": "^0.25.10",
"zod": "^4.1.13"
},
@@ -1500,6 +1502,40 @@
"devOptional": true,
"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": {
"version": "0.25.10",
"resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.25.10.tgz",
+2
View File
@@ -31,6 +31,8 @@
"inquirer": "^12.0.0",
"ora": "^8.1.0",
"tree-sitter-bash": "^0.25.1",
"vscode-jsonrpc": "^8.2.1",
"vscode-languageserver-protocol": "^3.17.5",
"web-tree-sitter": "^0.25.10",
"zod": "^4.1.13"
},
+48
View File
@@ -7,6 +7,13 @@ import { loadConfig, initConfig } from './utils/config.js';
import { toolRegistry, todoManager } from './tools/index.js';
import { getPermissionManager, promptPermission } from './permission/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();
@@ -23,6 +30,43 @@ program
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 {
const permissionManager = getPermissionManager();
@@ -61,6 +105,9 @@ program.action(async () => {
const config = loadConfig();
const agent = new Agent(config);
// 初始化 LSP 系统
initLSP(process.cwd());
// 设置工具注册表(支持动态工具发现)
agent.setRegistry(toolRegistry);
@@ -84,6 +131,7 @@ program.action(async () => {
// 优雅退出
process.on('SIGINT', async () => {
console.log('\n\n👋 再见!');
await shutdownLSP();
await sessionManager.close();
ui.close();
process.exit(0);
+284
View File
@@ -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('');
}
+409
View File
@@ -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());
}
}
+128
View File
@@ -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';
+138
View File
@@ -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);
}
+336
View File
@@ -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,
}));
}
+21 -2
View File
@@ -4,6 +4,7 @@ import type { ToolResult } from '../../types/index.js';
import type { ToolWithMetadata } from '../types.js';
import { loadDescription } from '../load_description.js';
import { getPermissionManager } from '../../permission/index.js';
import { touchFile, getFormattedFileDiagnostics, isLanguageSupported } from '../../lsp/index.js';
export const editFileTool: ToolWithMetadata = {
name: 'edit_file',
@@ -13,7 +14,7 @@ export const editFileTool: ToolWithMetadata = {
category: 'filesystem',
description: '编辑文件内容(查找替换)',
keywords: ['edit', 'file', 'replace', 'modify', 'change', 'update', '编辑', '文件', '替换', '修改', '更新'],
deferLoading: true,
deferLoading: false, // 核心工具,始终可用
},
parameters: {
path: {
@@ -87,9 +88,27 @@ export const editFileTool: ToolWithMetadata = {
const newContent = content.replace(oldString, newString);
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 {
success: true,
output: `文件已编辑: ${absolutePath}`,
output,
};
} catch (error) {
return {
+1 -1
View File
@@ -13,7 +13,7 @@ export const readFileTool: ToolWithMetadata = {
category: 'filesystem',
description: '读取文件内容',
keywords: ['read', 'file', 'content', 'cat', 'view', 'open', '读取', '文件', '内容', '查看', '打开'],
deferLoading: true,
deferLoading: false, // 核心工具,始终可用
},
parameters: {
path: {
+22 -2
View File
@@ -4,6 +4,7 @@ import type { ToolResult } from '../../types/index.js';
import type { ToolWithMetadata } from '../types.js';
import { loadDescription } from '../load_description.js';
import { getPermissionManager } from '../../permission/index.js';
import { touchFile, getFormattedFileDiagnostics, isLanguageSupported } from '../../lsp/index.js';
export const writeFileTool: ToolWithMetadata = {
name: 'write_file',
@@ -13,7 +14,7 @@ export const writeFileTool: ToolWithMetadata = {
category: 'filesystem',
description: '写入文件内容',
keywords: ['write', 'file', 'save', 'create', '写入', '文件', '保存', '创建', '新建'],
deferLoading: true,
deferLoading: false, // 核心工具,始终可用
},
parameters: {
path: {
@@ -61,9 +62,28 @@ export const writeFileTool: ToolWithMetadata = {
try {
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
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 {
success: true,
output: `文件已写入: ${absolutePath}`,
output,
};
} catch (error) {
return {
+5
View File
@@ -32,6 +32,11 @@ const DEFAULT_SYSTEM_PROMPT = `你是一个运行在终端中的 AI 编程助手
2. 执行可能有风险的命令前,先向用户确认
3. 给出清晰、简洁的回答
重要的工具使用规则:
- 创建或修改文件时,必须使用 write_file 或 edit_file 工具,不要使用 bash 命令(如 cat、echo 等)
- write_file 和 edit_file 工具集成了代码诊断功能,可以在写入后自动检查代码错误
- bash 工具仅用于运行命令、安装依赖、执行脚本等操作,不要用于文件内容的创建和修改
当前工作目录: ${process.cwd()}
操作系统: ${process.platform}`;