mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2026-01-12 17:34:12 +00:00
docs: 更新文档导航和解析器指南
- 添加演练场(Playground)文档导航区到主 README - 新增 Python 解析器文档链接(开发指南、测试报告、LSP集成) - 更新前端版本号至 0.1.9b19p - 补充 Python 解析器 requests 库使用章节和官方文档链接 - 添加 JavaScript 和 Python 解析器的语言版本和官方文档 - 优化文档结构,分类为项目文档和外部资源
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
"element-plus": "2.11.3",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"sockjs-client": "^1.6.1",
|
||||
"splitpanes": "^4.0.4",
|
||||
"vue": "^3.5.12",
|
||||
"vue-clipboard3": "^2.0.0",
|
||||
|
||||
@@ -206,6 +206,17 @@ export default {
|
||||
updateTheme(newTheme);
|
||||
});
|
||||
|
||||
// 监听语言变化
|
||||
watch(() => props.language, (newLanguage) => {
|
||||
if (editor && monaco) {
|
||||
const model = editor.getModel();
|
||||
if (model) {
|
||||
monaco.editor.setModelLanguage(model, newLanguage);
|
||||
console.log('[MonacoEditor] 语言已切换为:', newLanguage);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => props.height, (newHeight) => {
|
||||
if (editorContainer.value) {
|
||||
editorContainer.value.style.height = newHeight;
|
||||
|
||||
104
web-front/src/templates/index.js
Normal file
104
web-front/src/templates/index.js
Normal file
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* 解析器模板统一导出
|
||||
* 提供 JavaScript 和 Python 解析器模板的统一接口
|
||||
*/
|
||||
|
||||
import {
|
||||
generateJsTemplate,
|
||||
JS_EMPTY_TEMPLATE,
|
||||
JS_HTTP_EXAMPLE,
|
||||
JS_REGEX_EXAMPLE
|
||||
} from './jsParserTemplate';
|
||||
|
||||
import {
|
||||
generatePyTemplate,
|
||||
PY_EMPTY_TEMPLATE,
|
||||
PY_HTTP_EXAMPLE,
|
||||
PY_REGEX_EXAMPLE,
|
||||
PY_SECURITY_NOTICE
|
||||
} from './pyParserTemplate';
|
||||
|
||||
/**
|
||||
* 根据语言生成模板代码
|
||||
* @param {string} name - 解析器名称
|
||||
* @param {string} identifier - 标识符
|
||||
* @param {string} author - 作者
|
||||
* @param {string} match - URL匹配模式
|
||||
* @param {string} language - 语言类型 ('javascript' | 'python')
|
||||
* @returns {string} 模板代码
|
||||
*/
|
||||
export const generateTemplate = (name, identifier, author, match, language = 'javascript') => {
|
||||
if (language === 'python') {
|
||||
return generatePyTemplate(name, identifier, author, match);
|
||||
}
|
||||
return generateJsTemplate(name, identifier, author, match);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取默认空白模板
|
||||
* @param {string} language - 语言类型
|
||||
* @returns {string} 空白模板代码
|
||||
*/
|
||||
export const getEmptyTemplate = (language = 'javascript') => {
|
||||
if (language === 'python') {
|
||||
return PY_EMPTY_TEMPLATE;
|
||||
}
|
||||
return JS_EMPTY_TEMPLATE;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取 HTTP 请求示例
|
||||
* @param {string} language - 语言类型
|
||||
* @returns {string} HTTP 示例代码
|
||||
*/
|
||||
export const getHttpExample = (language = 'javascript') => {
|
||||
if (language === 'python') {
|
||||
return PY_HTTP_EXAMPLE;
|
||||
}
|
||||
return JS_HTTP_EXAMPLE;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取正则表达式示例
|
||||
* @param {string} language - 语言类型
|
||||
* @returns {string} 正则表达式示例代码
|
||||
*/
|
||||
export const getRegexExample = (language = 'javascript') => {
|
||||
if (language === 'python') {
|
||||
return PY_REGEX_EXAMPLE;
|
||||
}
|
||||
return JS_REGEX_EXAMPLE;
|
||||
};
|
||||
|
||||
// 导出所有模板
|
||||
export {
|
||||
// JavaScript
|
||||
generateJsTemplate,
|
||||
JS_EMPTY_TEMPLATE,
|
||||
JS_HTTP_EXAMPLE,
|
||||
JS_REGEX_EXAMPLE,
|
||||
// Python
|
||||
generatePyTemplate,
|
||||
PY_EMPTY_TEMPLATE,
|
||||
PY_HTTP_EXAMPLE,
|
||||
PY_REGEX_EXAMPLE,
|
||||
PY_SECURITY_NOTICE
|
||||
};
|
||||
|
||||
export default {
|
||||
generateTemplate,
|
||||
getEmptyTemplate,
|
||||
getHttpExample,
|
||||
getRegexExample,
|
||||
// JavaScript
|
||||
generateJsTemplate,
|
||||
JS_EMPTY_TEMPLATE,
|
||||
JS_HTTP_EXAMPLE,
|
||||
JS_REGEX_EXAMPLE,
|
||||
// Python
|
||||
generatePyTemplate,
|
||||
PY_EMPTY_TEMPLATE,
|
||||
PY_HTTP_EXAMPLE,
|
||||
PY_REGEX_EXAMPLE,
|
||||
PY_SECURITY_NOTICE
|
||||
};
|
||||
153
web-front/src/templates/jsParserTemplate.js
Normal file
153
web-front/src/templates/jsParserTemplate.js
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* JavaScript 解析器模板
|
||||
* 包含解析器的基础模板代码
|
||||
*/
|
||||
|
||||
/**
|
||||
* 生成 JavaScript 解析器模板代码
|
||||
* @param {string} name - 解析器名称
|
||||
* @param {string} identifier - 标识符
|
||||
* @param {string} author - 作者
|
||||
* @param {string} match - URL匹配模式
|
||||
* @returns {string} JavaScript模板代码
|
||||
*/
|
||||
export const generateJsTemplate = (name, identifier, author, match) => {
|
||||
const type = identifier.toLowerCase().replace(/[^a-z0-9]/g, '_');
|
||||
const displayName = name;
|
||||
const description = `使用JavaScript实现的${name}解析器`;
|
||||
|
||||
return `// ==UserScript==
|
||||
// @name ${name}
|
||||
// @type ${type}
|
||||
// @displayName ${displayName}
|
||||
// @description ${description}
|
||||
// @match ${match || 'https?://example.com/s/(?<KEY>\\\\w+)'}
|
||||
// @author ${author || 'yourname'}
|
||||
// @version 1.0.0
|
||||
// ==/UserScript==
|
||||
|
||||
/**
|
||||
* 解析单个文件下载链接
|
||||
* @param {ShareLinkInfo} shareLinkInfo - 分享链接信息
|
||||
* @param {JsHttpClient} http - HTTP客户端
|
||||
* @param {JsLogger} logger - 日志对象
|
||||
* @returns {string} 下载链接
|
||||
*/
|
||||
function parse(shareLinkInfo, http, logger) {
|
||||
var url = shareLinkInfo.getShareUrl();
|
||||
logger.info("开始解析: " + url);
|
||||
|
||||
var response = http.get(url);
|
||||
if (!response.isSuccess()) {
|
||||
throw new Error("请求失败: " + response.statusCode());
|
||||
}
|
||||
|
||||
var html = response.body();
|
||||
// 这里添加你的解析逻辑
|
||||
// 例如:使用正则表达式提取下载链接
|
||||
|
||||
return "https://example.com/download/file.zip";
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析文件列表(可选)
|
||||
* @param {ShareLinkInfo} shareLinkInfo - 分享链接信息
|
||||
* @param {JsHttpClient} http - HTTP客户端
|
||||
* @param {JsLogger} logger - 日志对象
|
||||
* @returns {Array} 文件信息数组
|
||||
*/
|
||||
function parseFileList(shareLinkInfo, http, logger) {
|
||||
var dirId = shareLinkInfo.getOtherParam("dirId") || "0";
|
||||
logger.info("解析文件列表,目录ID: " + dirId);
|
||||
|
||||
// 这里添加你的文件列表解析逻辑
|
||||
var fileList = [];
|
||||
|
||||
return fileList;
|
||||
}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* JavaScript 解析器的默认空白模板
|
||||
*/
|
||||
export const JS_EMPTY_TEMPLATE = `// ==UserScript==
|
||||
// @name 新解析器
|
||||
// @type new_parser
|
||||
// @displayName 新解析器
|
||||
// @description 解析器描述
|
||||
// @match https?://example.com/s/(?<KEY>\\w+)
|
||||
// @author yourname
|
||||
// @version 1.0.0
|
||||
// ==/UserScript==
|
||||
|
||||
function parse(shareLinkInfo, http, logger) {
|
||||
var url = shareLinkInfo.getShareUrl();
|
||||
logger.info("开始解析: " + url);
|
||||
|
||||
// 在这里编写你的解析逻辑
|
||||
|
||||
return "";
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* JavaScript HTTP 请求示例模板
|
||||
*/
|
||||
export const JS_HTTP_EXAMPLE = `// HTTP 请求示例
|
||||
|
||||
// GET 请求
|
||||
var response = http.get("https://api.example.com/data");
|
||||
if (response.isSuccess()) {
|
||||
var json = JSON.parse(response.body());
|
||||
logger.info("获取数据成功");
|
||||
}
|
||||
|
||||
// POST 请求(表单数据)
|
||||
var formData = {
|
||||
"key": "value",
|
||||
"name": "test"
|
||||
};
|
||||
var postResponse = http.post("https://api.example.com/submit", formData);
|
||||
|
||||
// POST 请求(JSON数据)
|
||||
var jsonData = JSON.stringify({ id: 1, name: "test" });
|
||||
var headers = { "Content-Type": "application/json" };
|
||||
var jsonResponse = http.postJson("https://api.example.com/api", jsonData, headers);
|
||||
|
||||
// 自定义请求头
|
||||
var customHeaders = {
|
||||
"User-Agent": "Mozilla/5.0",
|
||||
"Referer": "https://example.com"
|
||||
};
|
||||
var customResponse = http.getWithHeaders("https://api.example.com/data", customHeaders);
|
||||
`;
|
||||
|
||||
/**
|
||||
* JavaScript 正则表达式示例
|
||||
*/
|
||||
export const JS_REGEX_EXAMPLE = `// 正则表达式示例
|
||||
|
||||
var html = response.body();
|
||||
|
||||
// 匹配下载链接
|
||||
var downloadMatch = html.match(/href=["']([^"']*\\.zip)["']/);
|
||||
if (downloadMatch) {
|
||||
var downloadUrl = downloadMatch[1];
|
||||
}
|
||||
|
||||
// 匹配JSON数据
|
||||
var jsonMatch = html.match(/var data = (\\{[^}]+\\})/);
|
||||
if (jsonMatch) {
|
||||
var data = JSON.parse(jsonMatch[1]);
|
||||
}
|
||||
|
||||
// 全局匹配
|
||||
var allLinks = html.match(/href=["']([^"']+)["']/g);
|
||||
`;
|
||||
|
||||
export default {
|
||||
generateJsTemplate,
|
||||
JS_EMPTY_TEMPLATE,
|
||||
JS_HTTP_EXAMPLE,
|
||||
JS_REGEX_EXAMPLE
|
||||
};
|
||||
234
web-front/src/templates/pyParserTemplate.js
Normal file
234
web-front/src/templates/pyParserTemplate.js
Normal file
@@ -0,0 +1,234 @@
|
||||
/**
|
||||
* Python 解析器模板
|
||||
* 包含解析器的基础模板代码
|
||||
*/
|
||||
|
||||
/**
|
||||
* 生成 Python 解析器模板代码
|
||||
* @param {string} name - 解析器名称
|
||||
* @param {string} identifier - 标识符
|
||||
* @param {string} author - 作者
|
||||
* @param {string} match - URL匹配模式
|
||||
* @returns {string} Python模板代码
|
||||
*/
|
||||
export const generatePyTemplate = (name, identifier, author, match) => {
|
||||
const type = identifier.toLowerCase().replace(/[^a-z0-9]/g, '_');
|
||||
const displayName = name;
|
||||
const description = `使用Python实现的${name}解析器`;
|
||||
|
||||
return `# ==UserScript==
|
||||
# @name ${name}
|
||||
# @type ${type}
|
||||
# @displayName ${displayName}
|
||||
# @description ${description}
|
||||
# @match ${match || 'https?://example.com/s/(?<KEY>\\\\w+)'}
|
||||
# @author ${author || 'yourname'}
|
||||
# @version 1.0.0
|
||||
# ==/UserScript==
|
||||
|
||||
"""
|
||||
${name}解析器 - Python实现
|
||||
使用GraalPy运行,提供与JavaScript解析器相同的功能
|
||||
|
||||
可用模块:
|
||||
- requests: HTTP请求库 (已内置,支持 get/post/put/delete 等)
|
||||
- re: 正则表达式
|
||||
- json: JSON处理
|
||||
- base64: Base64编解码
|
||||
- hashlib: 哈希算法
|
||||
|
||||
内置对象:
|
||||
- share_link_info: 分享链接信息
|
||||
- http: 底层HTTP客户端
|
||||
- logger: 日志记录器
|
||||
- crypto: 加密工具 (md5/sha1/sha256/aes/base64)
|
||||
"""
|
||||
|
||||
import requests
|
||||
import re
|
||||
import json
|
||||
|
||||
|
||||
def parse(share_link_info, http, logger):
|
||||
"""
|
||||
解析单个文件下载链接
|
||||
|
||||
Args:
|
||||
share_link_info: 分享链接信息对象
|
||||
http: HTTP客户端
|
||||
logger: 日志记录器
|
||||
|
||||
Returns:
|
||||
str: 直链下载地址
|
||||
"""
|
||||
url = share_link_info.get_share_url()
|
||||
logger.info(f"开始解析: {url}")
|
||||
|
||||
# 使用 requests 库发起请求(推荐)
|
||||
response = requests.get(url, headers={
|
||||
"Referer": url
|
||||
})
|
||||
|
||||
if not response.ok:
|
||||
raise Exception(f"请求失败: {response.status_code}")
|
||||
|
||||
html = response.text
|
||||
|
||||
# 示例:使用正则表达式提取下载链接
|
||||
# match = re.search(r'download_url["\\':]\s*["\\']([^"\\'>]+)', html)
|
||||
# if match:
|
||||
# return match.group(1)
|
||||
|
||||
return "https://example.com/download/file.zip"
|
||||
|
||||
|
||||
def parse_file_list(share_link_info, http, logger):
|
||||
"""
|
||||
解析文件列表(可选)
|
||||
|
||||
Args:
|
||||
share_link_info: 分享链接信息对象
|
||||
http: HTTP客户端
|
||||
logger: 日志记录器
|
||||
|
||||
Returns:
|
||||
list: 文件信息列表
|
||||
"""
|
||||
dir_id = share_link_info.get_other_param("dirId") or "0"
|
||||
logger.info(f"解析文件列表,目录ID: {dir_id}")
|
||||
|
||||
file_list = []
|
||||
|
||||
return file_list
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Python 解析器的默认空白模板
|
||||
*/
|
||||
export const PY_EMPTY_TEMPLATE = `# ==UserScript==
|
||||
# @name 新解析器
|
||||
# @type new_parser
|
||||
# @displayName 新解析器
|
||||
# @description 解析器描述
|
||||
# @match https?://example.com/s/(?<KEY>\\w+)
|
||||
# @author yourname
|
||||
# @version 1.0.0
|
||||
# ==/UserScript==
|
||||
|
||||
import requests
|
||||
import re
|
||||
import json
|
||||
|
||||
|
||||
def parse(share_link_info, http, logger):
|
||||
"""解析单个文件下载链接"""
|
||||
url = share_link_info.get_share_url()
|
||||
logger.info(f"开始解析: {url}")
|
||||
|
||||
# 在这里编写你的解析逻辑
|
||||
|
||||
return ""
|
||||
`;
|
||||
|
||||
/**
|
||||
* Python HTTP 请求示例模板
|
||||
*/
|
||||
export const PY_HTTP_EXAMPLE = `# HTTP 请求示例
|
||||
|
||||
import requests
|
||||
import json
|
||||
|
||||
# GET 请求
|
||||
response = requests.get("https://api.example.com/data")
|
||||
if response.ok:
|
||||
data = response.json()
|
||||
logger.info("获取数据成功")
|
||||
|
||||
# POST 请求(表单数据)
|
||||
form_data = {
|
||||
"key": "value",
|
||||
"name": "test"
|
||||
}
|
||||
post_response = requests.post("https://api.example.com/submit", data=form_data)
|
||||
|
||||
# POST 请求(JSON数据)
|
||||
json_data = {"id": 1, "name": "test"}
|
||||
json_response = requests.post(
|
||||
"https://api.example.com/api",
|
||||
json=json_data,
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
# 自定义请求头
|
||||
custom_headers = {
|
||||
"User-Agent": "Mozilla/5.0",
|
||||
"Referer": "https://example.com"
|
||||
}
|
||||
custom_response = requests.get("https://api.example.com/data", headers=custom_headers)
|
||||
|
||||
# 会话保持 Cookie
|
||||
session = requests.Session()
|
||||
session.get("https://example.com/login") # 获取 Cookie
|
||||
session.post("https://example.com/api") # 自动带上 Cookie
|
||||
`;
|
||||
|
||||
/**
|
||||
* Python 正则表达式示例
|
||||
*/
|
||||
export const PY_REGEX_EXAMPLE = `# 正则表达式示例
|
||||
|
||||
import re
|
||||
|
||||
html = response.text
|
||||
|
||||
# 匹配下载链接
|
||||
download_match = re.search(r'href=["\\']([^"\\']*.zip)["\\'\\']', html)
|
||||
if download_match:
|
||||
download_url = download_match.group(1)
|
||||
|
||||
# 匹配JSON数据
|
||||
json_match = re.search(r'var data = (\\{[^}]+\\})', html)
|
||||
if json_match:
|
||||
data = json.loads(json_match.group(1))
|
||||
|
||||
# 查找所有匹配项
|
||||
all_links = re.findall(r'href=["\\']([^"\\']]+)["\\'\\']', html)
|
||||
|
||||
# 使用命名分组
|
||||
pattern = r'<a href="(?P<url>[^"]+)">(?P<text>[^<]+)</a>'
|
||||
for match in re.finditer(pattern, html):
|
||||
url = match.group('url')
|
||||
text = match.group('text')
|
||||
`;
|
||||
|
||||
/**
|
||||
* Python 安全提示
|
||||
*/
|
||||
export const PY_SECURITY_NOTICE = `# ⚠️ Python 安全限制说明
|
||||
#
|
||||
# 以下操作被禁止(安全策略限制):
|
||||
# - os.system() 系统命令执行
|
||||
# - os.popen() 进程创建
|
||||
# - os.remove() 删除文件
|
||||
# - os.rmdir() 删除目录
|
||||
# - subprocess.* 子进程操作
|
||||
# - open() 文件写入 (read模式允许)
|
||||
#
|
||||
# 允许的操作:
|
||||
# - requests.* 网络请求
|
||||
# - re.* 正则表达式
|
||||
# - json.* JSON处理
|
||||
# - base64.* Base64编解码
|
||||
# - hashlib.* 哈希算法
|
||||
# - os.getcwd() 获取当前目录
|
||||
# - os.path.* 路径操作
|
||||
`;
|
||||
|
||||
export default {
|
||||
generatePyTemplate,
|
||||
PY_EMPTY_TEMPLATE,
|
||||
PY_HTTP_EXAMPLE,
|
||||
PY_REGEX_EXAMPLE,
|
||||
PY_SECURITY_NOTICE
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Monaco Editor 代码补全配置工具
|
||||
* 基于 types.js 提供完整的代码补全支持
|
||||
* 支持 JavaScript 和 Python 两种语言
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -45,6 +46,9 @@ export async function configureMonacoTypes(monaco) {
|
||||
|
||||
// 注册代码补全提供者
|
||||
registerCompletionProvider(monaco);
|
||||
|
||||
// 注册Python语言补全提供者
|
||||
registerPythonCompletionProvider(monaco);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -299,6 +303,613 @@ function registerCompletionProvider(monaco) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册Python语言补全提供者
|
||||
* 提供 requests 库、内置对象和常用模块的代码补全
|
||||
*/
|
||||
function registerPythonCompletionProvider(monaco) {
|
||||
monaco.languages.registerCompletionItemProvider('python', {
|
||||
triggerCharacters: ['.', '(', '"', "'"],
|
||||
provideCompletionItems: (model, position) => {
|
||||
const word = model.getWordUntilPosition(position);
|
||||
const range = {
|
||||
startLineNumber: position.lineNumber,
|
||||
endLineNumber: position.lineNumber,
|
||||
startColumn: word.startColumn,
|
||||
endColumn: word.endColumn
|
||||
};
|
||||
|
||||
// 获取当前行内容以判断上下文
|
||||
const lineContent = model.getLineContent(position.lineNumber);
|
||||
const textBeforeCursor = lineContent.substring(0, position.column - 1);
|
||||
|
||||
const suggestions = [];
|
||||
|
||||
// ===== requests 库补全 =====
|
||||
if (textBeforeCursor.endsWith('requests.') || textBeforeCursor.match(/requests\s*\.\s*$/)) {
|
||||
suggestions.push(
|
||||
{
|
||||
label: 'get',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'get(${1:url}, params=${2:None}, headers=${3:None})',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: '发起 GET 请求\n\n参数:\n- url: 请求URL\n- params: URL参数字典\n- headers: 请求头字典\n\n返回: Response 对象',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'post',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'post(${1:url}, data=${2:None}, json=${3:None}, headers=${4:None})',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: '发起 POST 请求\n\n参数:\n- url: 请求URL\n- data: 表单数据\n- json: JSON数据\n- headers: 请求头字典\n\n返回: Response 对象',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'put',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'put(${1:url}, data=${2:None})',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: '发起 PUT 请求',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'delete',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'delete(${1:url})',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: '发起 DELETE 请求',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'patch',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'patch(${1:url}, data=${2:None})',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: '发起 PATCH 请求',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'head',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'head(${1:url})',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: '发起 HEAD 请求',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'Session',
|
||||
kind: monaco.languages.CompletionItemKind.Class,
|
||||
insertText: 'Session()',
|
||||
documentation: '创建一个会话对象,可以跨请求保持 cookies 和 headers',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'url_encode',
|
||||
kind: monaco.languages.CompletionItemKind.Function,
|
||||
insertText: 'url_encode(${1:text})',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'URL 编码',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'url_decode',
|
||||
kind: monaco.languages.CompletionItemKind.Function,
|
||||
insertText: 'url_decode(${1:text})',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'URL 解码',
|
||||
range
|
||||
}
|
||||
);
|
||||
}
|
||||
// Response 对象补全
|
||||
else if (textBeforeCursor.match(/\.\s*$/) && (
|
||||
textBeforeCursor.includes('response') ||
|
||||
textBeforeCursor.includes('resp') ||
|
||||
textBeforeCursor.includes('res') ||
|
||||
textBeforeCursor.match(/requests\.(get|post|put|delete|patch|head)\([^)]*\)\s*\./)
|
||||
)) {
|
||||
suggestions.push(
|
||||
{
|
||||
label: 'text',
|
||||
kind: monaco.languages.CompletionItemKind.Property,
|
||||
insertText: 'text',
|
||||
documentation: '响应的文本内容',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'content',
|
||||
kind: monaco.languages.CompletionItemKind.Property,
|
||||
insertText: 'content',
|
||||
documentation: '响应的二进制内容',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'json',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'json()',
|
||||
documentation: '解析响应为 JSON 对象',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'status_code',
|
||||
kind: monaco.languages.CompletionItemKind.Property,
|
||||
insertText: 'status_code',
|
||||
documentation: 'HTTP 状态码',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'ok',
|
||||
kind: monaco.languages.CompletionItemKind.Property,
|
||||
insertText: 'ok',
|
||||
documentation: '请求是否成功 (status_code < 400)',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'headers',
|
||||
kind: monaco.languages.CompletionItemKind.Property,
|
||||
insertText: 'headers',
|
||||
documentation: '响应头字典',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'raise_for_status',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'raise_for_status()',
|
||||
documentation: '如果响应状态码表示错误,则抛出异常',
|
||||
range
|
||||
}
|
||||
);
|
||||
}
|
||||
// share_link_info 对象补全
|
||||
else if (textBeforeCursor.endsWith('share_link_info.')) {
|
||||
suggestions.push(
|
||||
{
|
||||
label: 'get_share_url',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'get_share_url()',
|
||||
documentation: '获取分享URL',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'get_share_key',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'get_share_key()',
|
||||
documentation: '获取分享Key',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'get_share_password',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'get_share_password()',
|
||||
documentation: '获取分享密码',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'get_type',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'get_type()',
|
||||
documentation: '获取网盘类型',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'get_pan_name',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'get_pan_name()',
|
||||
documentation: '获取网盘名称',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'get_other_param',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'get_other_param(${1:key})',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: '获取其他参数',
|
||||
range
|
||||
}
|
||||
);
|
||||
}
|
||||
// http 对象补全(Python 风格下划线命名)
|
||||
else if (textBeforeCursor.endsWith('http.')) {
|
||||
suggestions.push(
|
||||
{
|
||||
label: 'get',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'get(${1:url})',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: '发起 GET 请求',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'get_with_redirect',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'get_with_redirect(${1:url})',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: '发起 GET 请求并跟随重定向',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'get_no_redirect',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'get_no_redirect(${1:url})',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: '发起 GET 请求但不跟随重定向',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'post',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'post(${1:url}, ${2:data})',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: '发起 POST 请求',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'post_json',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'post_json(${1:url}, ${2:json_data})',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: '发起 POST 请求(JSON 数据)',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'put_header',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'put_header(${1:name}, ${2:value})',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: '设置请求头',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'put_headers',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'put_headers(${1:headers_dict})',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: '批量设置请求头',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'set_timeout',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'set_timeout(${1:seconds})',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: '设置请求超时时间(秒)',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'url_encode',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'url_encode(${1:text})',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'URL 编码',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'url_decode',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'url_decode(${1:text})',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'URL 解码',
|
||||
range
|
||||
}
|
||||
);
|
||||
}
|
||||
// logger 对象补全
|
||||
else if (textBeforeCursor.endsWith('logger.')) {
|
||||
suggestions.push(
|
||||
{
|
||||
label: 'info',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'info(${1:message})',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: '记录信息日志',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'debug',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'debug(${1:message})',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: '记录调试日志',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'warn',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'warn(${1:message})',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: '记录警告日志',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'error',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'error(${1:message})',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: '记录错误日志',
|
||||
range
|
||||
}
|
||||
);
|
||||
}
|
||||
// crypto 加密工具补全
|
||||
else if (textBeforeCursor.endsWith('crypto.')) {
|
||||
suggestions.push(
|
||||
{
|
||||
label: 'md5',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'md5(${1:data})',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'MD5 加密(返回32位小写)',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'md5_16',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'md5_16(${1:data})',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'MD5 加密(返回16位小写)',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'sha1',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'sha1(${1:data})',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'SHA-1 加密',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'sha256',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'sha256(${1:data})',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'SHA-256 加密',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'base64_encode',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'base64_encode(${1:data})',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'Base64 编码',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'base64_decode',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'base64_decode(${1:data})',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'Base64 解码',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'aes_encrypt',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'aes_encrypt(${1:data}, ${2:key}, ${3:iv})',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'AES 加密',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'aes_decrypt',
|
||||
kind: monaco.languages.CompletionItemKind.Method,
|
||||
insertText: 'aes_decrypt(${1:data}, ${2:key}, ${3:iv})',
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'AES 解密',
|
||||
range
|
||||
}
|
||||
);
|
||||
}
|
||||
// 全局补全
|
||||
else {
|
||||
// import 语句补全
|
||||
suggestions.push(
|
||||
{
|
||||
label: 'import requests',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: 'import requests',
|
||||
documentation: '导入 requests HTTP 库',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'import re',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: 'import re',
|
||||
documentation: '导入正则表达式模块',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'import json',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: 'import json',
|
||||
documentation: '导入 JSON 模块',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'import base64',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: 'import base64',
|
||||
documentation: '导入 Base64 编码模块',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'import hashlib',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: 'import hashlib',
|
||||
documentation: '导入哈希算法模块',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'from urllib.parse import urlencode, quote, unquote',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: 'from urllib.parse import urlencode, quote, unquote',
|
||||
documentation: '导入 URL 处理函数',
|
||||
range
|
||||
}
|
||||
);
|
||||
|
||||
// 全局变量补全
|
||||
suggestions.push(
|
||||
{
|
||||
label: 'requests',
|
||||
kind: monaco.languages.CompletionItemKind.Module,
|
||||
insertText: 'requests',
|
||||
documentation: 'HTTP 请求库,支持 get, post, put, delete 等方法',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'share_link_info',
|
||||
kind: monaco.languages.CompletionItemKind.Variable,
|
||||
insertText: 'share_link_info',
|
||||
documentation: '分享链接信息对象,包含 URL、密码等信息',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'http',
|
||||
kind: monaco.languages.CompletionItemKind.Variable,
|
||||
insertText: 'http',
|
||||
documentation: 'HTTP 客户端对象(底层 Java 实现)',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'logger',
|
||||
kind: monaco.languages.CompletionItemKind.Variable,
|
||||
insertText: 'logger',
|
||||
documentation: '日志记录器',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'crypto',
|
||||
kind: monaco.languages.CompletionItemKind.Variable,
|
||||
insertText: 'crypto',
|
||||
documentation: '加密工具对象,提供 MD5、SHA、AES、Base64 等功能',
|
||||
range
|
||||
}
|
||||
);
|
||||
|
||||
// 函数模板补全
|
||||
suggestions.push(
|
||||
{
|
||||
label: 'def parse',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: [
|
||||
'def parse(share_link_info, http, logger):',
|
||||
' """',
|
||||
' 解析单个文件下载链接',
|
||||
' ',
|
||||
' Args:',
|
||||
' share_link_info: 分享链接信息对象',
|
||||
' http: HTTP 客户端',
|
||||
' logger: 日志记录器',
|
||||
' ',
|
||||
' Returns:',
|
||||
' str: 直链下载地址',
|
||||
' """',
|
||||
' url = share_link_info.get_share_url()',
|
||||
' logger.info(f"开始解析: {url}")',
|
||||
' ',
|
||||
' ${0}',
|
||||
' ',
|
||||
' return ""'
|
||||
].join('\n'),
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: '创建 parse 函数模板',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'def parse_file_list',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: [
|
||||
'def parse_file_list(share_link_info, http, logger):',
|
||||
' """',
|
||||
' 解析文件列表',
|
||||
' ',
|
||||
' Args:',
|
||||
' share_link_info: 分享链接信息对象',
|
||||
' http: HTTP 客户端',
|
||||
' logger: 日志记录器',
|
||||
' ',
|
||||
' Returns:',
|
||||
' list: 文件信息列表',
|
||||
' """',
|
||||
' dir_id = share_link_info.get_other_param("dirId") or "0"',
|
||||
' logger.info(f"解析文件列表,目录ID: {dir_id}")',
|
||||
' ',
|
||||
' file_list = []',
|
||||
' ${0}',
|
||||
' ',
|
||||
' return file_list'
|
||||
].join('\n'),
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: '创建 parse_file_list 函数模板',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'requests.get example',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: [
|
||||
'response = requests.get(${1:url}, headers={',
|
||||
' "User-Agent": "Mozilla/5.0"',
|
||||
'})',
|
||||
'if response.ok:',
|
||||
' data = response.json()',
|
||||
' ${0}'
|
||||
].join('\n'),
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'requests.get 请求示例',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 'requests.post example',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: [
|
||||
'response = requests.post(${1:url}, json={',
|
||||
' ${2:"key": "value"}',
|
||||
'}, headers={',
|
||||
' "Content-Type": "application/json"',
|
||||
'})',
|
||||
'if response.ok:',
|
||||
' result = response.json()',
|
||||
' ${0}'
|
||||
].join('\n'),
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: 'requests.post 请求示例',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 're.search example',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: [
|
||||
'import re',
|
||||
'match = re.search(r\'${1:pattern}\', ${2:text})',
|
||||
'if match:',
|
||||
' result = match.group(${3:1})',
|
||||
' ${0}'
|
||||
].join('\n'),
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: '正则表达式搜索示例',
|
||||
range
|
||||
},
|
||||
{
|
||||
label: 're.findall example',
|
||||
kind: monaco.languages.CompletionItemKind.Snippet,
|
||||
insertText: [
|
||||
'import re',
|
||||
'matches = re.findall(r\'${1:pattern}\', ${2:text})',
|
||||
'for match in matches:',
|
||||
' ${0}'
|
||||
].join('\n'),
|
||||
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
||||
documentation: '正则表达式查找所有匹配示例',
|
||||
range
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return { suggestions };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 从API获取types.js内容并配置
|
||||
*/
|
||||
|
||||
410
web-front/src/utils/pylspClient.js
Normal file
410
web-front/src/utils/pylspClient.js
Normal file
@@ -0,0 +1,410 @@
|
||||
/**
|
||||
* 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;
|
||||
@@ -48,7 +48,7 @@
|
||||
</div>
|
||||
<!-- 项目简介移到卡片内 -->
|
||||
<div class="project-intro">
|
||||
<div class="intro-title">NFD网盘直链解析0.1.9_b15</div>
|
||||
<div class="intro-title">NFD网盘直链解析0.1.9b19p</div>
|
||||
<div class="intro-desc">
|
||||
<div>支持网盘:蓝奏云、蓝奏云优享、小飞机盘、123云盘、奶牛快传、移动云空间、QQ邮箱云盘、QQ闪传等 <el-link style="color:#606cf5" href="https://github.com/qaiu/netdisk-fast-download?tab=readme-ov-file#%E7%BD%91%E7%9B%98%E6%94%AF%E6%8C%81%E6%83%85%E5%86%B5" target="_blank"> >> </el-link></div>
|
||||
<div>文件夹解析支持:蓝奏云、蓝奏云优享、小飞机盘、123云盘</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user