docs: 更新文档导航和解析器指南

- 添加演练场(Playground)文档导航区到主 README
- 新增 Python 解析器文档链接(开发指南、测试报告、LSP集成)
- 更新前端版本号至 0.1.9b19p
- 补充 Python 解析器 requests 库使用章节和官方文档链接
- 添加 JavaScript 和 Python 解析器的语言版本和官方文档
- 优化文档结构,分类为项目文档和外部资源
This commit is contained in:
q
2026-01-11 22:35:45 +08:00
parent b8eee2b8a7
commit 2fcf9cfab1
60 changed files with 10132 additions and 436 deletions

View File

@@ -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",

View File

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

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

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

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

View File

@@ -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内容并配置
*/

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

View File

@@ -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"> &gt;&gt; </el-link></div>
<div>文件夹解析支持蓝奏云蓝奏云优享小飞机盘123云盘</div>

File diff suppressed because it is too large Load Diff