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",
|
||||
"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",
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
@@ -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 { 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 {
|
||||
|
||||
@@ -13,7 +13,7 @@ export const readFileTool: ToolWithMetadata = {
|
||||
category: 'filesystem',
|
||||
description: '读取文件内容',
|
||||
keywords: ['read', 'file', 'content', 'cat', 'view', 'open', '读取', '文件', '内容', '查看', '打开'],
|
||||
deferLoading: true,
|
||||
deferLoading: false, // 核心工具,始终可用
|
||||
},
|
||||
parameters: {
|
||||
path: {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user