opencode实现自定义LSP
·
1. opencode调试遇到的问题
opencode打开文件后发送initialize请求后,js返回数据,但是一直卡住
opencode打开文件后发送initialize请求后,js返回数据,但是一直卡住,是因为stdout污染,通过去掉所有的console.log解决
为什么TypeScript、Python、Rust等语言服务器可以被agent读取到诊断信息,而自定义的lsp不能,都是通过opencode.json形式在cli中运行?
1、OpenCode 等诊断有 3 秒超时
内置的TypeScript是直连的、已启动、已初始化。分析速度快,通常几百毫秒就返回诊断。
而arkts的lsp经过好几层传递,尝试修复三秒超时问题
还有一个重要原因就是:Agent 只有在写/编辑文件后才能看到LSP的诊断。用 Read 读文件时 Agent 看不到LSP 诊断。所以应该输入:帮我修复 xx文件出现的问题
2、要求返回的uri是 file:///D:/Projects/arkts/src/app.ts这种格式,而不是将:编码为%3A,如file:///D%3A/Projects/arkts/src/app.ts
2.实现代码
import { Utils } from './utils';
import { activate } from './extension';
import { LspClient } from './client/lspClient';
import { getFileExtension, normalizeEOL, readFile } from './util/util';
import { initLogger, logger } from './logger';
import { AnyLspMessage, LspMessageParser } from './client/lspParser';
class LSPWrapper {
private workspaceRoot: string | null = null;
private openFileUri: string = '';
constructor() {
}
private sendToOpenCode(message: any): void {
const messageStr = Utils.safeJsonStringify(message);
const contentLength = Buffer.byteLength(messageStr, 'utf8');
const header = Buffer.from(`Content-Length: ${contentLength}\r\n\r\n`, 'ascii');
const body = Buffer.from(messageStr, 'utf8');
const fullMessage = Buffer.concat([header, body]);
try {
process.stdout.write(fullMessage);
process.stdout.emit('drain');
logToFile(`[WRAPPER] Sent: ${messageStr}`);
} catch (err) {
logToFile(`[WRAPPER] Exception while writing to stdout: ${err}`);
}
}
private extractWorkspaceRoot(params: any): string | null {
if (params?.rootUri && typeof params.rootUri === 'string') {
try {
const url = new URL(params.rootUri);
let path = decodeURIComponent(url.pathname);
if (process.platform === 'win32' && path.startsWith('/')) path = path.substring(1);
return path;
} catch (e) {
logger.warn(`[WRAPPER] Failed to parse rootUri: ${params.rootUri}, error: ${e}`);
}
}
if (params?.workspaceFolders && Array.isArray(params.workspaceFolders) && params.workspaceFolders.length > 0) {
const folder = params.workspaceFolders[0];
if (folder?.uri && typeof folder.uri === 'string') {
try {
const url = new URL(folder.uri);
let path = decodeURIComponent(url.pathname);
if (process.platform === 'win32' && path.startsWith('/')) {
path = path.substring(1);
}
return path;
} catch (e) {
logger.warn(`[WRAPPER] Failed to parse workspaceFolder uri: ${folder.uri}, error: ${e}`);
}
}
}
return null;
}
private normalizeForComparison(u: string): string {
if (!u) return '';
let normalized = u;
if (normalized.startsWith('file:///')) {
normalized = normalized.substring(8); // 移除 "file:///"
} else if (normalized.startsWith('file://')) {
normalized = normalized.substring(7);
}
if (normalized.startsWith('/') && process.platform === 'win32') {
normalized = normalized.substring(1);
}
normalized = normalized.replace(/\\/g, '/');
if (process.platform === 'win32') {
normalized = normalized.toLowerCase();
}
return decodeURIComponent(normalized);
}
private fileUriToPath(uri: string): string {
if (!uri.startsWith('file://')) return uri;
try {
const url = new URL(uri);
let filePath = decodeURIComponent(url.pathname);
if (process.platform === 'win32' && filePath.startsWith('/')) {
filePath = filePath.substring(1);
}
return filePath;
} catch (e) {
return uri;
}
}
private pathToCleanFileUri(filePath: string): string {
const normalized = filePath.replace(/\\/g, '/');
return `file:///${normalized.replace(/^\/+/, '')}`;
}
private cleanFileUri(uri: string): string {
return this.pathToCleanFileUri(this.fileUriToPath(uri));
}
private sendPublishDiagnostic(): void {
const testUri = this.openFileUri;
const testDiagnostics = [
{
range: {
start: { line: 5, character: 4 },
end: { line: 5, character: 10 }
},
severity: 1, // Error
message: 'This is a hardcoded test error from mock diagnostics.',
source: 'mock-lsp',
code: 'TEST001'
},
{
range: {
start: { line: 8, character: 2 },
end: { line: 8, character: 15 }
},
severity: 2, // Warning
message: 'Hardcoded warning for testing purposes.',
source: 'mock-lsp',
code: 'TEST002'
}
];
const normalizedKey = this.normalizeForComparison(testUri);
const rawUri = testUri.startsWith('file://') ? testUri : `file:///${testUri.replace(/\\/g, '/')}`;
const targetUri = this.cleanFileUri(rawUri);
const diagnosticMessage = {
jsonrpc: '2.0',
method: 'textDocument/publishDiagnostics',
params: {
uri: targetUri,
diagnostics: testDiagnostics
}
};
// 立即发送测试诊断
this.sendToOpenCode(diagnosticMessage);
}
async processOpenCodeMessage(message: AnyLspMessage, args: string[]): Promise<void> {
if (!('method' in message)) {
if ('id' in message) logger.info(`[WRAPPER] Response id: ${(message as any).id}`);
return;
}
if ('method' in message) {
switch (message.method) {
case 'initialize': {
const id = (message as any).id;
const params = (message as any).params || {};
try {
const extractedWorkspaceRoot = this.extractWorkspaceRoot(params);
if (!extractedWorkspaceRoot) {
throw new Error(
'Failed to extract workspaceRoot from initialize params. ' +
`Available params keys: ${Object.keys(params).join(', ')}`
);
}
if (args.length < 2) {
throw new Error(
`Insufficient command line arguments. Expected 2 (sdkPath, arktsLangServer), got ${args.length}`
);
}
this.sdkPath = args[0];
this.arktsLangServer = args[1];
this.workspaceRoot = extractedWorkspaceRoot;
try {
await initLogger(this.arktsLangServer, this.workspaceRoot);
} catch (err: any) {
logToFile(`[WRAPPER] initLogger failed: ${err}`);
}
const result: any = {
capabilities: {
textDocumentSync: 1,
hoverProvider: true,
completionProvider: {
resolveProvider: true,
triggerCharacters: ['.']
},
definitionProvider: true,
referencesProvider: true,
codeActionProvider: true,
},
serverInfo: { name: 'arkts-languageserver', version: '1.0.0' }
};
const cleanResult = JSON.parse(JSON.stringify(result));
const response = {
jsonrpc: '2.0',
id,
result: cleanResult,
};
this.sendToOpenCode(response);
logToFile(`[WRAPPER] Initialize response sent`);
} catch (err: any) {
const errorMsg = `[WRAPPER] Initialize failed: ${err}`;
logToFile(errorMsg);
logger.error(errorMsg);
this.sendToOpenCode({
jsonrpc: '2.0',
id,
error: {
code: -32603,
message: `Server initialization failed: ${err?.message ?? String(err)}`,
},
});
}
break;
}
case 'initialized':
break;
case 'textDocument/didChange':
case 'textDocument/didOpen': {
const params: any = (message as any).params;
const textDocument = params?.textDocument;
const uri = textDocument?.uri;
if (!uri) break;
this.openFileUri = uri;
this.sendPublishDiagnostic();
break;
}
default:
logToFile(`[WRAPPER] Unhandled: ${message.method}`);
break;
}
}
}
}
let logToFile: (msg: string) => void;
async function main(): Promise<void> {
// 强制 stdout 为同步阻塞模式,确保 LSP 消息立即发出不被缓冲
if ((process.stdout as any)._handle && typeof (process.stdout as any)._handle.setBlocking === 'function') {
(process.stdout as any)._handle.setBlocking(true);
}
const args = process.argv.slice(2);
logToFile(`[WRAPPER] Command line args: ${args.join(', ')}`);
const wrapper = new LSPWrapper();
const stdinParser = new LspMessageParser();
process.stdin.on('data', (chunk: Buffer) => {
for (const msg of stdinParser.parseChunk(chunk)) {
wrapper.processOpenCodeMessage(msg, args).catch((err) => logToFile(`[WRAPPER] ${err}`));
}
});
process.stdin.on('end', () => {});
process.stdin.on('error', (err) => logToFile(`[WRAPPER] stdin error: ${err}`));
process.on('SIGINT', () => process.exit(0));
process.on('SIGTERM', () => process.exit(0));
setInterval(() => {}, 24 * 60 * 60 * 1000);
}
if (require.main === module) {
(() => {
const fs = require('fs') as typeof import('fs');
const path = require('path') as typeof import('path');
const logPath = path.join(process.cwd(), 'wrapper-debug.log');
let logFd: number | null = null;
try { logFd = fs.openSync(logPath, 'a'); } catch (_) {}
logToFile = (msg: string) => {
const line = `[${new Date().toISOString()}] ${msg}\n`;
process.stderr.write(line, () => {});
if (logFd !== null) {
try {
fs.writeSync(logFd, line, undefined, 'utf8');
fs.fsyncSync(logFd);
} catch (_) {}
}
};
})();
process.on('uncaughtException', (err) => { logToFile(`[WRAPPER] ${err}`); logToFile(err.stack ?? ''); });
process.on('unhandledRejection', (reason) => { logToFile(`[WRAPPER] rejection: ${reason}`); });
main().catch((err) => { logToFile(`[WRAPPER] ${err}`); logToFile(err.stack ?? ''); });
}
export { LSPWrapper };
更多推荐
所有评论(0)