Files
netdisk-fast-download/web-front/src/utils/pylspClient.js
T
q 19d83fa267 docs: 更新文档导航和解析器指南
- 添加演练场(Playground)文档导航区到主 README
- 新增 Python 解析器文档链接(开发指南、测试报告、LSP集成)
- 更新前端版本号至 0.1.9b19p
- 补充 Python 解析器 requests 库使用章节和官方文档链接
- 添加 JavaScript 和 Python 解析器的语言版本和官方文档
- 优化文档结构,分类为项目文档和外部资源
2026-01-11 22:35:45 +08:00

411 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Python LSP (pylsp/jedi) WebSocket 客户端
*
* 通过 WebSocket 连接到后端 pylsp 桥接服务,
* 提供实时代码检查、自动完成、悬停提示等功能。
*
* @author QAIU
*/
import SockJS from 'sockjs-client';
// LSP 消息类型
const LSP_METHODS = {
INITIALIZE: 'initialize',
INITIALIZED: 'initialized',
TEXT_DOCUMENT_DID_OPEN: 'textDocument/didOpen',
TEXT_DOCUMENT_DID_CHANGE: 'textDocument/didChange',
TEXT_DOCUMENT_DID_CLOSE: 'textDocument/didClose',
TEXT_DOCUMENT_COMPLETION: 'textDocument/completion',
TEXT_DOCUMENT_HOVER: 'textDocument/hover',
TEXT_DOCUMENT_DIAGNOSTICS: 'textDocument/publishDiagnostics',
SHUTDOWN: 'shutdown',
EXIT: 'exit'
};
// 诊断严重程度
const DiagnosticSeverity = {
Error: 1,
Warning: 2,
Information: 3,
Hint: 4
};
/**
* pylsp WebSocket 客户端类
*/
class PylspClient {
constructor(options = {}) {
this.wsUrl = options.wsUrl || this._getDefaultWsUrl();
this.ws = null;
this.requestId = 1;
this.pendingRequests = new Map();
this.documentUri = 'file:///playground.py';
this.documentVersion = 0;
// 回调函数
this.onDiagnostics = options.onDiagnostics || (() => {});
this.onConnected = options.onConnected || (() => {});
this.onDisconnected = options.onDisconnected || (() => {});
this.onError = options.onError || (() => {});
// 状态
this.connected = false;
this.initialized = false;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 3;
this.reconnectDelay = 2000;
}
/**
* 获取默认 WebSocket URL
* SockJS 客户端需要使用 HTTP/HTTPS URL,而不是 WS/WSS
*/
_getDefaultWsUrl() {
const protocol = window.location.protocol; // http: 或 https:
const host = window.location.host;
return `${protocol}//${host}/v2/ws/pylsp`;
}
/**
* 连接到 pylsp 服务
*/
async connect() {
if (this.connected) {
console.log('[PylspClient] 已经连接');
return true;
}
return new Promise((resolve, reject) => {
try {
console.log('[PylspClient] 正在连接:', this.wsUrl);
// 使用 SockJS 连接(支持 WebSocket 和 fallback
this.ws = new SockJS(this.wsUrl);
this.ws.onopen = () => {
console.log('[PylspClient] WebSocket 连接成功');
this.connected = true;
this.reconnectAttempts = 0;
this._initialize().then(() => {
this.onConnected();
resolve(true);
}).catch(err => {
console.error('[PylspClient] 初始化失败:', err);
reject(err);
});
};
this.ws.onmessage = (event) => {
this._handleMessage(event.data);
};
this.ws.onerror = (error) => {
console.error('[PylspClient] WebSocket 错误:', error);
this.onError(error);
};
this.ws.onclose = () => {
console.log('[PylspClient] WebSocket 连接关闭');
this.connected = false;
this.initialized = false;
this.onDisconnected();
// 尝试重连
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
console.log(`[PylspClient] 尝试重连 (${this.reconnectAttempts}/${this.maxReconnectAttempts})...`);
setTimeout(() => this.connect(), this.reconnectDelay);
}
};
// 设置超时
setTimeout(() => {
if (!this.connected) {
reject(new Error('连接超时'));
}
}, 10000);
} catch (error) {
console.error('[PylspClient] 连接失败:', error);
reject(error);
}
});
}
/**
* 断开连接
*/
disconnect() {
if (this.ws) {
this._sendRequest(LSP_METHODS.SHUTDOWN).then(() => {
this._sendNotification(LSP_METHODS.EXIT);
this.ws.close();
}).catch(() => {
this.ws.close();
});
}
this.connected = false;
this.initialized = false;
}
/**
* 初始化 LSP
*/
async _initialize() {
const params = {
processId: null,
rootUri: null,
capabilities: {
textDocument: {
synchronization: {
dynamicRegistration: false,
willSave: false,
willSaveWaitUntil: false,
didSave: true
},
completion: {
dynamicRegistration: false,
completionItem: {
snippetSupport: true,
commitCharactersSupport: true,
documentationFormat: ['markdown', 'plaintext'],
deprecatedSupport: true
}
},
hover: {
dynamicRegistration: false,
contentFormat: ['markdown', 'plaintext']
},
publishDiagnostics: {
relatedInformation: true
}
}
}
};
await this._sendRequest(LSP_METHODS.INITIALIZE, params);
this._sendNotification(LSP_METHODS.INITIALIZED, {});
this.initialized = true;
console.log('[PylspClient] LSP 初始化完成');
}
/**
* 打开文档
*/
openDocument(content, uri = this.documentUri) {
if (!this.initialized) {
console.warn('[PylspClient] LSP 未初始化');
return;
}
this.documentUri = uri;
this.documentVersion = 1;
this._sendNotification(LSP_METHODS.TEXT_DOCUMENT_DID_OPEN, {
textDocument: {
uri: uri,
languageId: 'python',
version: this.documentVersion,
text: content
}
});
}
/**
* 更新文档内容
*/
updateDocument(content, uri = this.documentUri) {
if (!this.initialized) {
return;
}
this.documentVersion++;
this._sendNotification(LSP_METHODS.TEXT_DOCUMENT_DID_CHANGE, {
textDocument: {
uri: uri,
version: this.documentVersion
},
contentChanges: [{ text: content }]
});
}
/**
* 关闭文档
*/
closeDocument(uri = this.documentUri) {
if (!this.initialized) {
return;
}
this._sendNotification(LSP_METHODS.TEXT_DOCUMENT_DID_CLOSE, {
textDocument: { uri: uri }
});
}
/**
* 获取补全建议
*/
async getCompletions(line, character, uri = this.documentUri) {
if (!this.initialized) {
return [];
}
try {
const result = await this._sendRequest(LSP_METHODS.TEXT_DOCUMENT_COMPLETION, {
textDocument: { uri: uri },
position: { line, character }
});
return result?.items || result || [];
} catch (error) {
console.error('[PylspClient] 获取补全失败:', error);
return [];
}
}
/**
* 获取悬停信息
*/
async getHover(line, character, uri = this.documentUri) {
if (!this.initialized) {
return null;
}
try {
return await this._sendRequest(LSP_METHODS.TEXT_DOCUMENT_HOVER, {
textDocument: { uri: uri },
position: { line, character }
});
} catch (error) {
console.error('[PylspClient] 获取悬停信息失败:', error);
return null;
}
}
/**
* 发送 LSP 请求
*/
_sendRequest(method, params = {}) {
return new Promise((resolve, reject) => {
// SockJS readyState: 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED
if (!this.ws || this.ws.readyState !== 1) {
reject(new Error('WebSocket 未连接'));
return;
}
const id = this.requestId++;
const message = {
jsonrpc: '2.0',
id: id,
method: method,
params: params
};
this.pendingRequests.set(id, { resolve, reject });
this.ws.send(JSON.stringify(message));
// 设置超时
setTimeout(() => {
if (this.pendingRequests.has(id)) {
this.pendingRequests.delete(id);
reject(new Error(`请求超时: ${method}`));
}
}, 30000);
});
}
/**
* 发送 LSP 通知(无需响应)
*/
_sendNotification(method, params = {}) {
// SockJS readyState: 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED
if (!this.ws || this.ws.readyState !== 1) {
return;
}
const message = {
jsonrpc: '2.0',
method: method,
params: params
};
this.ws.send(JSON.stringify(message));
}
/**
* 处理接收到的消息
*/
_handleMessage(data) {
try {
const message = JSON.parse(data);
// 响应消息
if (message.id !== undefined) {
const pending = this.pendingRequests.get(message.id);
if (pending) {
this.pendingRequests.delete(message.id);
if (message.error) {
pending.reject(new Error(message.error.message || '未知错误'));
} else {
pending.resolve(message.result);
}
}
return;
}
// 通知消息
if (message.method === LSP_METHODS.TEXT_DOCUMENT_DIAGNOSTICS) {
this._handleDiagnostics(message.params);
}
} catch (error) {
console.error('[PylspClient] 解析消息失败:', error);
}
}
/**
* 处理诊断信息
*/
_handleDiagnostics(params) {
const { uri, diagnostics } = params;
// 转换为 Monaco Editor 格式
const monacoMarkers = diagnostics.map(d => ({
severity: this._convertSeverity(d.severity),
startLineNumber: d.range.start.line + 1,
startColumn: d.range.start.character + 1,
endLineNumber: d.range.end.line + 1,
endColumn: d.range.end.character + 1,
message: d.message,
source: d.source || 'pylsp'
}));
this.onDiagnostics(uri, monacoMarkers);
}
/**
* 转换诊断严重程度到 Monaco 格式
*/
_convertSeverity(lspSeverity) {
// Monaco MarkerSeverity: Error = 8, Warning = 4, Info = 2, Hint = 1
switch (lspSeverity) {
case DiagnosticSeverity.Error: return 8;
case DiagnosticSeverity.Warning: return 4;
case DiagnosticSeverity.Information: return 2;
case DiagnosticSeverity.Hint: return 1;
default: return 4;
}
}
}
// 导出
export {
PylspClient,
LSP_METHODS,
DiagnosticSeverity
};
export default PylspClient;