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 };

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐