feat: 重构解析器发布覆盖功能 - 添加forceOverwrite参数支持覆盖已存在解析器 - 前端添加覆盖确认对话框 - 修复lambda中Boolean类型转换错误

This commit is contained in:
q
2026-01-19 11:10:16 +08:00
parent 55c3387415
commit a925731f52
21 changed files with 5630 additions and 389 deletions

View File

@@ -667,6 +667,9 @@ public class PlaygroundApi {
String version = config.getMetadata().get("version");
String matchPattern = config.getMatchPattern() != null ? config.getMatchPattern().pattern() : null;
final boolean isPython = "python".equals(language);
// 在外部提取forceOverwrite参数避免lambda中类型转换问题
final boolean forceOverwrite = Boolean.TRUE.equals(body.getValue("forceOverwrite"));
// 检查数量限制
dbService.getPlaygroundParserCount().onSuccess(count -> {
@@ -678,23 +681,29 @@ public class PlaygroundApi {
// 检查type是否已存在
dbService.getPlaygroundParserList().onSuccess(listResult -> {
var list = listResult.getJsonArray("data");
boolean exists = false;
Long existingId = null;
if (list != null) {
for (int i = 0; i < list.size(); i++) {
var item = list.getJsonObject(i);
if (type.equals(item.getString("type"))) {
exists = true;
existingId = item.getLong("id");
break;
}
}
}
if (exists) {
promise.complete(JsonResult.error("解析器类型 " + type + " 已存在,请使用其他类型标识").toJsonObject());
if (existingId != null && !forceOverwrite) {
// type已存在且未强制覆盖返回错误信息和existingId
JsonObject errorResult = JsonResult.error("解析器类型 " + type + " 已存在,是否覆盖?").toJsonObject();
errorResult.put("existingId", existingId);
errorResult.put("existingType", type);
promise.complete(errorResult);
return;
}
final Long finalExistingId = existingId;
// 保存到数据
// 准备解析器数据
JsonObject parser = new JsonObject();
parser.put("name", name);
parser.put("type", type);
@@ -708,16 +717,35 @@ public class PlaygroundApi {
parser.put("ip", getClientIp(ctx.request()));
parser.put("enabled", true);
dbService.savePlaygroundParser(parser).onSuccess(result -> {
// 保存成功后,立即注册到解析器系统
// 根据是否覆盖选择不同的操作
Future<JsonObject> saveFuture;
if (finalExistingId != null) {
// 覆盖模式:更新现有解析器
saveFuture = dbService.updatePlaygroundParser(finalExistingId, parser);
log.info("覆盖现有解析器ID: {}, type: {}", finalExistingId, type);
} else {
// 新增模式
saveFuture = dbService.savePlaygroundParser(parser);
}
saveFuture.onSuccess(result -> {
// 保存成功后,注册/重新注册到解析器系统
try {
// 先注销旧的(覆盖模式需要)
if (finalExistingId != null) {
CustomParserRegistry.unregister(type);
}
// 注册新的
if (isPython) {
CustomParserRegistry.registerPy(config);
} else {
CustomParserRegistry.register(config);
}
log.info("已注册演练场{}解析器: {} ({})", isPython ? "Python" : "JavaScript", displayName, type);
promise.complete(JsonResult.success("保存并注册成功").toJsonObject());
String action = finalExistingId != null ? "覆盖并重新注册" : "保存并注册";
log.info("{}演练场{}解析器: {} ({})", action, isPython ? "Python" : "JavaScript", displayName, type);
promise.complete(JsonResult.success(action + "成功").toJsonObject());
} catch (Exception e) {
log.error("注册解析器失败", e);
// 虽然注册失败,但保存成功了,返回警告
@@ -924,6 +952,64 @@ public class PlaygroundApi {
return dbService.getPlaygroundParserById(id);
}
/**
* 获取示例解析器代码
* @param language 语言类型 (javascript 或 python)
*/
@RouteMapping(value = "/example/:language", method = RouteMethod.GET)
public void getExampleParser(HttpServerResponse response, String language) {
// 权限检查(示例代码也需要认证)
if (!checkEnabled()) {
ResponseUtil.fireJsonObjectResponse(response,
JsonResult.error("演练场功能已禁用").toJsonObject());
return;
}
try {
String resourcePath;
String contentType = "text/plain; charset=utf-8";
if ("python".equalsIgnoreCase(language)) {
resourcePath = "custom-parsers/py/example_parser.py";
} else if ("javascript".equalsIgnoreCase(language)) {
resourcePath = "custom-parsers/example-demo.js";
} else {
ResponseUtil.fireJsonObjectResponse(response,
JsonResult.error("不支持的语言类型: " + language).toJsonObject());
return;
}
// 从资源文件加载示例代码
InputStream inputStream = getClass().getClassLoader().getResourceAsStream(resourcePath);
if (inputStream == null) {
log.error("无法找到示例文件: {}", resourcePath);
ResponseUtil.fireJsonObjectResponse(response,
JsonResult.error("示例文件不存在").toJsonObject());
return;
}
StringBuilder content = new StringBuilder();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
content.append(line).append("\n");
}
}
// 返回示例代码
response.putHeader("Content-Type", contentType);
response.end(content.toString());
log.debug("返回{}示例代码,长度: {} 字节", language, content.length());
} catch (Exception e) {
log.error("加载示例文件失败", e);
ResponseUtil.fireJsonObjectResponse(response,
JsonResult.error("加载示例失败: " + e.getMessage()).toJsonObject());
}
}
/**
* 获取客户端IP
*/

View File

@@ -299,103 +299,199 @@ public class DbServiceImpl implements DbService {
// ==UserScript==
// @name 示例JS解析器
// @description 演示如何编写JavaScript解析器访问 https://httpbin.org/html 获取HTML内容
// @type example-js
// @type example_js
// @displayName JS示例
// @version 1.0.0
// @author System
// @matchPattern ^https?://httpbin\\.org/.*$
// @match https?://httpbin\\.org/s/(?<KEY>\\w+)
// ==/UserScript==
/**
* 解析入口函数
* @param {string} url 分享链接URL
* @param {string} pwd 提取码(可选)
* @returns {object} 包含下载链接的结果对象
* 解析单个文件下载链接
* @param {ShareLinkInfo} shareLinkInfo - 分享链接信息对象
* @param {JsHttpClient} http - HTTP客户端实例
* @param {JsLogger} logger - 日志记录器实例
* @returns {string} 下载链接
*/
function parse(url, pwd) {
log.info("开始解析: " + url);
function parse(shareLinkInfo, http, logger) {
logger.info("===== JS示例解析器 =====");
var shareUrl = shareLinkInfo.getShareUrl();
var shareKey = shareLinkInfo.getShareKey();
logger.info("分享链接: " + shareUrl);
logger.info("分享Key: " + shareKey);
// 使用内置HTTP客户端发送GET请求
var response = http.get("https://httpbin.org/html");
if (response.statusCode === 200) {
var body = response.body;
log.info("获取到HTML内容长度: " + body.length);
if (response.statusCode() === 200) {
var body = response.text();
logger.info("获取到HTML内容长度: " + body.length);
// 提取标题
var titleMatch = body.match(/<title>([^<]+)<\\/title>/i);
var title = titleMatch ? titleMatch[1] : "未知标题";
logger.info("页面标题: " + title);
// 返回结果
return {
downloadUrl: "https://httpbin.org/html",
fileName: title + ".html",
fileSize: body.length,
extra: {
title: title,
contentType: "text/html"
}
};
// 返回下载链接示例返回HTML页面URL
return "https://httpbin.org/html";
} else {
log.error("请求失败,状态码: " + response.statusCode);
throw new Error("请求失败: " + response.statusCode);
logger.error("请求失败,状态码: " + response.statusCode());
throw new Error("请求失败: " + response.statusCode());
}
}
/**
* 解析文件列表(可选)
* @param {ShareLinkInfo} shareLinkInfo - 分享链接信息对象
* @param {JsHttpClient} http - HTTP客户端实例
* @param {JsLogger} logger - 日志记录器实例
* @returns {FileInfo[]} 文件信息列表
*/
function parseFileList(shareLinkInfo, http, logger) {
logger.info("===== 解析文件列表 =====");
var response = http.get("https://httpbin.org/json");
var data = response.json();
// 返回文件列表
return [{
fileName: "example.html",
fileId: "1",
fileType: "file",
size: 1024,
sizeStr: "1 KB",
parserUrl: "https://httpbin.org/html"
}];
}
""";
// Python 示例解析器代码
String pyExampleCode = """
# ==UserScript==
# @name 示例Python解析器
# @description 演示如何编写Python解析器访问 https://httpbin.org/json 获取JSON数据
# @type example-py
# @type example_py
# @displayName Python示例
# @version 1.0.0
# @description 演示如何编写Python解析器使用requests库和正则表达式
# @match https?://httpbin\\.org/s/(?P<KEY>\\w+)
# @author System
# @matchPattern ^https?://httpbin\\.org/.*$
# @version 1.0.0
# ==/UserScript==
def parse(url: str, pwd: str = None) -> dict:
\"\"\"
Python解析器示例 - 使用GraalPy运行
可用模块:
- requests: HTTP请求库 (已内置,支持 get/post/put/delete 等)
- re: 正则表达式
- json: JSON处理
- base64: Base64编解码
- hashlib: 哈希算法
内置对象:
- share_link_info: 分享链接信息
- http: 底层HTTP客户端PyHttpClient
- logger: 日志记录器PyLogger
- crypto: 加密工具 (md5/sha1/sha256/aes/base64)
\"\"\"
import requests
import re
import json
def parse(share_link_info, http, logger):
\"\"\"
解析入口函数
解析单个文件下载链接
Args:
url: 分享链接URL
pwd: 提取码(可选)
share_link_info: 分享链接信息对象
http: HTTP客户端
logger: 日志记录器
Returns:
包含下载链接的结果字典
str: 直链下载地址
\"\"\"
log.info(f"开始解析: {url}")
url = share_link_info.get_share_url()
key = share_link_info.get_share_key()
pwd = share_link_info.get_share_password()
# 使用内置HTTP客户端发送GET请求
response = http.get("https://httpbin.org/json")
logger.info("===== Python示例解析器 =====")
logger.info(f"分享链接: {url}")
logger.info(f"分享Key: {key}")
if response['statusCode'] == 200:
body = response['body']
log.info(f"获取到JSON内容长度: {len(body)}")
# 解析JSON
import json
data = json.loads(body)
# 返回结果
return {
"downloadUrl": "https://httpbin.org/json",
"fileName": "data.json",
"fileSize": len(body),
"extra": {
"title": data.get("slideshow", {}).get("title", "未知"),
"contentType": "application/json"
}
# 方式1使用 requests 库发起请求(推荐)
response = requests.get('https://httpbin.org/html', headers={
"Referer": url,
"User-Agent": "Mozilla/5.0"
})
if response.status_code != 200:
logger.error(f"请求失败: {response.status_code}")
raise Exception(f"请求失败: {response.status_code}")
html = response.text
logger.info(f"获取到HTML内容长度: {len(html)}")
# 示例:使用正则表达式提取标题
match = re.search(r'<title>([^<]+)</title>', html, re.IGNORECASE)
if match:
title = match.group(1)
logger.info(f"页面标题: {title}")
# 方式2使用内置HTTP客户端适合简单场景
# json_response = http.get("https://httpbin.org/json")
# data = json_response.json()
# logger.info(f"JSON数据: {data.get('slideshow', {}).get('title', '未知')}")
# 返回下载链接
return "https://httpbin.org/html"
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}")
# 使用requests获取文件列表
response = requests.get('https://httpbin.org/json')
data = response.json()
# 构建文件列表
file_list = [
{
"fileName": "example.html",
"fileId": "1",
"fileType": "file",
"size": 2048,
"sizeStr": "2 KB",
"createTime": "2026-01-15 12:00:00",
"parserUrl": "https://httpbin.org/html"
},
{
"fileName": "subfolder",
"fileId": "2",
"fileType": "folder",
"size": 0,
"sizeStr": "-",
"parserUrl": ""
}
else:
log.error(f"请求失败,状态码: {response['statusCode']}")
raise Exception(f"请求失败: {response['statusCode']}")
]
logger.info(f"返回 {len(file_list)} 个文件/文件夹")
return file_list
""";
// 先检查JS示例是否存在
existsPlaygroundParserByType("example-js").compose(jsExists -> {
existsPlaygroundParserByType("example_js").compose(jsExists -> {
if (jsExists) {
log.info("JS示例解析器已存在跳过初始化");
return Future.succeededFuture();
@@ -403,12 +499,12 @@ public class DbServiceImpl implements DbService {
// 插入JS示例解析器
JsonObject jsParser = new JsonObject()
.put("name", "示例JS解析器")
.put("type", "example-js")
.put("type", "example_js")
.put("displayName", "JS示例")
.put("description", "演示如何编写JavaScript解析器")
.put("author", "System")
.put("version", "1.0.0")
.put("matchPattern", "^https?://httpbin\\.org/.*$")
.put("matchPattern", "https?://httpbin\\.org/s/(?<KEY>\\w+)")
.put("jsCode", jsExampleCode)
.put("language", "javascript")
.put("ip", "127.0.0.1")
@@ -416,7 +512,7 @@ public class DbServiceImpl implements DbService {
return savePlaygroundParser(jsParser);
}).compose(v -> {
// 检查Python示例是否存在
return existsPlaygroundParserByType("example-py");
return existsPlaygroundParserByType("example_py");
}).compose(pyExists -> {
if (pyExists) {
log.info("Python示例解析器已存在跳过初始化");
@@ -425,12 +521,12 @@ public class DbServiceImpl implements DbService {
// 插入Python示例解析器
JsonObject pyParser = new JsonObject()
.put("name", "示例Python解析器")
.put("type", "example-py")
.put("type", "example_py")
.put("displayName", "Python示例")
.put("description", "演示如何编写Python解析器")
.put("author", "System")
.put("version", "1.0.0")
.put("matchPattern", "^https?://httpbin\\.org/.*$")
.put("matchPattern", "https?://httpbin\\.org/s/(?P<KEY>\\w+)")
.put("jsCode", pyExampleCode)
.put("language", "python")
.put("ip", "127.0.0.1")