mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2026-01-12 09:24:14 +00:00
feat: 添加getNoRedirect方法支持302重定向处理
- 在JsHttpClient中添加getNoRedirect方法,支持不自动跟随重定向的HTTP请求 - 修改baidu-photo.js解析器,使用getNoRedirect获取真实的下载链接 - 更新测试用例断言,验证重定向处理功能正常工作 - 修复百度一刻相册解析器302重定向问题,现在能正确获取真实下载链接
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package cn.qaiu;
|
||||
|
||||
import cn.qaiu.parser.CustomParserRegistry;
|
||||
import io.vertx.core.Vertx;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -12,12 +13,26 @@ public class WebClientVertxInit {
|
||||
|
||||
public static void init(Vertx vx) {
|
||||
INSTANCE.vertx = vx;
|
||||
|
||||
// 自动加载JavaScript解析器脚本
|
||||
try {
|
||||
CustomParserRegistry.autoLoadJsScripts();
|
||||
} catch (Exception e) {
|
||||
log.warn("自动加载JavaScript解析器脚本失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static Vertx get() {
|
||||
if (INSTANCE.vertx == null) {
|
||||
log.info("getVertx: Vertx实例不存在, 创建Vertx实例.");
|
||||
INSTANCE.vertx = Vertx.vertx();
|
||||
|
||||
// 如果Vertx实例是新创建的,也尝试加载JavaScript脚本
|
||||
try {
|
||||
CustomParserRegistry.autoLoadJsScripts();
|
||||
} catch (Exception e) {
|
||||
log.warn("自动加载JavaScript解析器脚本失败", e);
|
||||
}
|
||||
}
|
||||
return INSTANCE.vertx;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package cn.qaiu.parser;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
@@ -46,6 +47,21 @@ public class CustomParserConfig {
|
||||
*/
|
||||
private final Pattern matchPattern;
|
||||
|
||||
/**
|
||||
* JavaScript代码(用于JavaScript解析器)
|
||||
*/
|
||||
private final String jsCode;
|
||||
|
||||
/**
|
||||
* 是否为JavaScript解析器
|
||||
*/
|
||||
private final boolean isJsParser;
|
||||
|
||||
/**
|
||||
* 元数据信息(从脚本注释中解析)
|
||||
*/
|
||||
private final Map<String, String> metadata;
|
||||
|
||||
private CustomParserConfig(Builder builder) {
|
||||
this.type = builder.type;
|
||||
this.displayName = builder.displayName;
|
||||
@@ -53,6 +69,9 @@ public class CustomParserConfig {
|
||||
this.standardUrlTemplate = builder.standardUrlTemplate;
|
||||
this.panDomain = builder.panDomain;
|
||||
this.matchPattern = builder.matchPattern;
|
||||
this.jsCode = builder.jsCode;
|
||||
this.isJsParser = builder.isJsParser;
|
||||
this.metadata = builder.metadata;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
@@ -79,6 +98,18 @@ public class CustomParserConfig {
|
||||
return matchPattern;
|
||||
}
|
||||
|
||||
public String getJsCode() {
|
||||
return jsCode;
|
||||
}
|
||||
|
||||
public boolean isJsParser() {
|
||||
return isJsParser;
|
||||
}
|
||||
|
||||
public Map<String, String> getMetadata() {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否支持从分享链接自动识别
|
||||
* @return true表示支持,false表示不支持
|
||||
@@ -101,6 +132,9 @@ public class CustomParserConfig {
|
||||
private String standardUrlTemplate;
|
||||
private String panDomain;
|
||||
private Pattern matchPattern;
|
||||
private String jsCode;
|
||||
private boolean isJsParser;
|
||||
private Map<String, String> metadata;
|
||||
|
||||
/**
|
||||
* 设置解析器类型标识(必填,唯一)
|
||||
@@ -167,6 +201,33 @@ public class CustomParserConfig {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置JavaScript代码(用于JavaScript解析器)
|
||||
* @param jsCode JavaScript代码
|
||||
*/
|
||||
public Builder jsCode(String jsCode) {
|
||||
this.jsCode = jsCode;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否为JavaScript解析器
|
||||
* @param isJsParser 是否为JavaScript解析器
|
||||
*/
|
||||
public Builder isJsParser(boolean isJsParser) {
|
||||
this.isJsParser = isJsParser;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置元数据信息
|
||||
* @param metadata 元数据信息
|
||||
*/
|
||||
public Builder metadata(Map<String, String> metadata) {
|
||||
this.metadata = metadata;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建配置对象
|
||||
* @return CustomParserConfig
|
||||
@@ -178,20 +239,29 @@ public class CustomParserConfig {
|
||||
if (displayName == null || displayName.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("displayName不能为空");
|
||||
}
|
||||
if (toolClass == null) {
|
||||
throw new IllegalArgumentException("toolClass不能为空");
|
||||
}
|
||||
|
||||
// 验证toolClass是否实现了IPanTool接口
|
||||
if (!IPanTool.class.isAssignableFrom(toolClass)) {
|
||||
throw new IllegalArgumentException("toolClass必须实现IPanTool接口");
|
||||
}
|
||||
|
||||
// 验证toolClass是否有ShareLinkInfo单参构造器
|
||||
try {
|
||||
toolClass.getDeclaredConstructor(ShareLinkInfo.class);
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new IllegalArgumentException("toolClass必须有ShareLinkInfo单参构造器", e);
|
||||
// 如果是JavaScript解析器,验证jsCode
|
||||
if (isJsParser) {
|
||||
if (jsCode == null || jsCode.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("JavaScript解析器的jsCode不能为空");
|
||||
}
|
||||
} else {
|
||||
// 如果是Java解析器,验证toolClass
|
||||
if (toolClass == null) {
|
||||
throw new IllegalArgumentException("Java解析器的toolClass不能为空");
|
||||
}
|
||||
|
||||
// 验证toolClass是否实现了IPanTool接口
|
||||
if (!IPanTool.class.isAssignableFrom(toolClass)) {
|
||||
throw new IllegalArgumentException("toolClass必须实现IPanTool接口");
|
||||
}
|
||||
|
||||
// 验证toolClass是否有ShareLinkInfo单参构造器
|
||||
try {
|
||||
toolClass.getDeclaredConstructor(ShareLinkInfo.class);
|
||||
} catch (NoSuchMethodException e) {
|
||||
throw new IllegalArgumentException("toolClass必须有ShareLinkInfo单参构造器", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 验证正则表达式(如果提供)
|
||||
@@ -212,10 +282,13 @@ public class CustomParserConfig {
|
||||
return "CustomParserConfig{" +
|
||||
"type='" + type + '\'' +
|
||||
", displayName='" + displayName + '\'' +
|
||||
", toolClass=" + toolClass.getName() +
|
||||
", toolClass=" + (toolClass != null ? toolClass.getName() : "null") +
|
||||
", standardUrlTemplate='" + standardUrlTemplate + '\'' +
|
||||
", panDomain='" + panDomain + '\'' +
|
||||
", matchPattern=" + (matchPattern != null ? matchPattern.pattern() : "null") +
|
||||
", jsCode=" + (jsCode != null ? "[JavaScript代码]" : "null") +
|
||||
", isJsParser=" + isJsParser +
|
||||
", metadata=" + metadata +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package cn.qaiu.parser;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@@ -12,6 +16,8 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
*/
|
||||
public class CustomParserRegistry {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(CustomParserRegistry.class);
|
||||
|
||||
/**
|
||||
* 存储自定义解析器配置的Map,key为类型标识,value为配置对象
|
||||
*/
|
||||
@@ -51,6 +57,108 @@ public class CustomParserRegistry {
|
||||
}
|
||||
|
||||
CUSTOM_PARSERS.put(type, config);
|
||||
log.info("注册自定义解析器成功: {} ({})", config.getDisplayName(), type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册JavaScript解析器
|
||||
*
|
||||
* @param config JavaScript解析器配置
|
||||
* @throws IllegalArgumentException 如果type已存在或与内置解析器冲突
|
||||
*/
|
||||
public static void registerJs(CustomParserConfig config) {
|
||||
if (config == null) {
|
||||
throw new IllegalArgumentException("config不能为空");
|
||||
}
|
||||
|
||||
if (!config.isJsParser()) {
|
||||
throw new IllegalArgumentException("config必须是JavaScript解析器配置");
|
||||
}
|
||||
|
||||
register(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从JavaScript代码字符串注册解析器
|
||||
*
|
||||
* @param jsCode JavaScript代码
|
||||
* @throws IllegalArgumentException 如果解析失败
|
||||
*/
|
||||
public static void registerJsFromCode(String jsCode) {
|
||||
if (jsCode == null || jsCode.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("JavaScript代码不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
CustomParserConfig config = JsScriptMetadataParser.parseScript(jsCode);
|
||||
registerJs(config);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("解析JavaScript代码失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件注册JavaScript解析器
|
||||
*
|
||||
* @param filePath 文件路径
|
||||
* @throws IllegalArgumentException 如果文件不存在或解析失败
|
||||
*/
|
||||
public static void registerJsFromFile(String filePath) {
|
||||
if (filePath == null || filePath.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("文件路径不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
CustomParserConfig config = JsScriptLoader.loadFromFile(filePath);
|
||||
registerJs(config);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("从文件加载JavaScript解析器失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从资源文件注册JavaScript解析器
|
||||
*
|
||||
* @param resourcePath 资源路径
|
||||
* @throws IllegalArgumentException 如果资源不存在或解析失败
|
||||
*/
|
||||
public static void registerJsFromResource(String resourcePath) {
|
||||
if (resourcePath == null || resourcePath.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("资源路径不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
CustomParserConfig config = JsScriptLoader.loadFromResource(resourcePath);
|
||||
registerJs(config);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalArgumentException("从资源加载JavaScript解析器失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动加载所有JavaScript脚本
|
||||
*/
|
||||
public static void autoLoadJsScripts() {
|
||||
try {
|
||||
List<CustomParserConfig> configs = JsScriptLoader.loadAllScripts();
|
||||
int successCount = 0;
|
||||
int failCount = 0;
|
||||
|
||||
for (CustomParserConfig config : configs) {
|
||||
try {
|
||||
registerJs(config);
|
||||
successCount++;
|
||||
} catch (Exception e) {
|
||||
log.error("加载JavaScript脚本失败: {}", config.getType(), e);
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
|
||||
log.info("自动加载JavaScript脚本完成: 成功 {} 个,失败 {} 个", successCount, failCount);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("自动加载JavaScript脚本时发生异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,7 +171,13 @@ public class CustomParserRegistry {
|
||||
if (type == null || type.trim().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return CUSTOM_PARSERS.remove(type.toLowerCase()) != null;
|
||||
|
||||
CustomParserConfig removed = CUSTOM_PARSERS.remove(type.toLowerCase());
|
||||
if (removed != null) {
|
||||
log.info("注销自定义解析器: {} ({})", removed.getDisplayName(), type);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,11 @@ import io.vertx.core.Promise;
|
||||
import java.util.List;
|
||||
|
||||
public interface IPanTool {
|
||||
|
||||
/**
|
||||
* 解析文件
|
||||
* @return 文件内容
|
||||
*/
|
||||
Future<String> parse();
|
||||
|
||||
default String parseSync() {
|
||||
@@ -23,6 +28,10 @@ public interface IPanTool {
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
default List<FileInfo> parseFileListSync() {
|
||||
return parseFileList().toCompletionStage().toCompletableFuture().join();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件ID获取下载链接
|
||||
* @return url
|
||||
@@ -32,4 +41,8 @@ public interface IPanTool {
|
||||
promise.complete("Not implemented yet");
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
default String parseByIdSync() {
|
||||
return parseById().toCompletionStage().toCompletableFuture().join();
|
||||
}
|
||||
}
|
||||
|
||||
300
parser/src/main/java/cn/qaiu/parser/JsHttpClient.java
Normal file
300
parser/src/main/java/cn/qaiu/parser/JsHttpClient.java
Normal file
@@ -0,0 +1,300 @@
|
||||
package cn.qaiu.parser;
|
||||
|
||||
import cn.qaiu.WebClientVertxInit;
|
||||
import cn.qaiu.util.HttpResponseHelper;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.MultiMap;
|
||||
import io.vertx.core.Promise;
|
||||
import io.vertx.core.buffer.Buffer;
|
||||
import io.vertx.core.json.JsonObject;
|
||||
import io.vertx.ext.web.client.HttpRequest;
|
||||
import io.vertx.ext.web.client.HttpResponse;
|
||||
import io.vertx.ext.web.client.WebClient;
|
||||
import io.vertx.ext.web.client.WebClientSession;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* JavaScript HTTP客户端封装
|
||||
* 为JavaScript提供同步API风格的HTTP请求功能
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2025/10/17
|
||||
*/
|
||||
public class JsHttpClient {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(JsHttpClient.class);
|
||||
|
||||
private final WebClient client;
|
||||
private final WebClientSession clientSession;
|
||||
private MultiMap headers;
|
||||
|
||||
public JsHttpClient() {
|
||||
this.client = WebClient.create(WebClientVertxInit.get());
|
||||
this.clientSession = WebClientSession.create(client);
|
||||
this.headers = MultiMap.caseInsensitiveMultiMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起GET请求
|
||||
* @param url 请求URL
|
||||
* @return HTTP响应
|
||||
*/
|
||||
public JsHttpResponse get(String url) {
|
||||
return executeRequest(() -> {
|
||||
HttpRequest<Buffer> request = client.getAbs(url);
|
||||
if (!headers.isEmpty()) {
|
||||
request.putHeaders(headers);
|
||||
}
|
||||
return request.send();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起GET请求并跟随重定向
|
||||
* @param url 请求URL
|
||||
* @return HTTP响应
|
||||
*/
|
||||
public JsHttpResponse getWithRedirect(String url) {
|
||||
return executeRequest(() -> {
|
||||
HttpRequest<Buffer> request = client.getAbs(url);
|
||||
if (!headers.isEmpty()) {
|
||||
request.putHeaders(headers);
|
||||
}
|
||||
// 设置跟随重定向
|
||||
request.followRedirects(true);
|
||||
return request.send();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起GET请求但不跟随重定向(用于获取Location头)
|
||||
* @param url 请求URL
|
||||
* @return HTTP响应
|
||||
*/
|
||||
public JsHttpResponse getNoRedirect(String url) {
|
||||
return executeRequest(() -> {
|
||||
HttpRequest<Buffer> request = client.getAbs(url);
|
||||
if (!headers.isEmpty()) {
|
||||
request.putHeaders(headers);
|
||||
}
|
||||
// 设置不跟随重定向
|
||||
request.followRedirects(false);
|
||||
return request.send();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发起POST请求
|
||||
* @param url 请求URL
|
||||
* @param data 请求数据
|
||||
* @return HTTP响应
|
||||
*/
|
||||
public JsHttpResponse post(String url, Object data) {
|
||||
return executeRequest(() -> {
|
||||
HttpRequest<Buffer> request = client.postAbs(url);
|
||||
if (!headers.isEmpty()) {
|
||||
request.putHeaders(headers);
|
||||
}
|
||||
|
||||
if (data != null) {
|
||||
if (data instanceof String) {
|
||||
request.sendBuffer(Buffer.buffer((String) data));
|
||||
} else if (data instanceof Map) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, String> mapData = (Map<String, String>) data;
|
||||
request.sendForm(MultiMap.caseInsensitiveMultiMap().addAll(mapData));
|
||||
} else {
|
||||
request.sendJson(data);
|
||||
}
|
||||
} else {
|
||||
request.send();
|
||||
}
|
||||
|
||||
return request.send();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置请求头
|
||||
* @param name 头名称
|
||||
* @param value 头值
|
||||
* @return 当前客户端实例(支持链式调用)
|
||||
*/
|
||||
public JsHttpClient putHeader(String name, String value) {
|
||||
if (name != null && value != null) {
|
||||
headers.set(name, value);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送表单数据
|
||||
* @param data 表单数据
|
||||
* @return HTTP响应
|
||||
*/
|
||||
public JsHttpResponse sendForm(Map<String, String> data) {
|
||||
return executeRequest(() -> {
|
||||
HttpRequest<Buffer> request = client.postAbs("");
|
||||
if (!headers.isEmpty()) {
|
||||
request.putHeaders(headers);
|
||||
}
|
||||
|
||||
MultiMap formData = MultiMap.caseInsensitiveMultiMap();
|
||||
if (data != null) {
|
||||
formData.addAll(data);
|
||||
}
|
||||
|
||||
return request.sendForm(formData);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送JSON数据
|
||||
* @param data JSON数据
|
||||
* @return HTTP响应
|
||||
*/
|
||||
public JsHttpResponse sendJson(Object data) {
|
||||
return executeRequest(() -> {
|
||||
HttpRequest<Buffer> request = client.postAbs("");
|
||||
if (!headers.isEmpty()) {
|
||||
request.putHeaders(headers);
|
||||
}
|
||||
|
||||
return request.sendJson(data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行HTTP请求(同步)
|
||||
*/
|
||||
private JsHttpResponse executeRequest(RequestExecutor executor) {
|
||||
try {
|
||||
Promise<HttpResponse<Buffer>> promise = Promise.promise();
|
||||
Future<HttpResponse<Buffer>> future = executor.execute();
|
||||
|
||||
future.onComplete(promise);
|
||||
|
||||
// 等待响应完成(最多30秒)
|
||||
HttpResponse<Buffer> response = promise.future().toCompletionStage()
|
||||
.toCompletableFuture()
|
||||
.get(30, TimeUnit.SECONDS);
|
||||
|
||||
return new JsHttpResponse(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("HTTP请求执行失败", e);
|
||||
throw new RuntimeException("HTTP请求执行失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求执行器接口
|
||||
*/
|
||||
@FunctionalInterface
|
||||
private interface RequestExecutor {
|
||||
Future<HttpResponse<Buffer>> execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* JavaScript HTTP响应封装
|
||||
*/
|
||||
public static class JsHttpResponse {
|
||||
|
||||
private final HttpResponse<Buffer> response;
|
||||
|
||||
public JsHttpResponse(HttpResponse<Buffer> response) {
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取响应体(字符串)
|
||||
* @return 响应体字符串
|
||||
*/
|
||||
public String body() {
|
||||
return HttpResponseHelper.asText(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析JSON响应
|
||||
* @return JSON对象或数组
|
||||
*/
|
||||
public Object json() {
|
||||
try {
|
||||
String body = response.bodyAsString();
|
||||
if (body == null || body.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 尝试解析为JSON对象
|
||||
try {
|
||||
JsonObject jsonObject = response.bodyAsJsonObject();
|
||||
// 将JsonObject转换为Map,这样JavaScript可以正确访问
|
||||
return jsonObject.getMap();
|
||||
} catch (Exception e) {
|
||||
// 如果解析为对象失败,尝试解析为数组
|
||||
try {
|
||||
return response.bodyAsJsonArray().getList();
|
||||
} catch (Exception e2) {
|
||||
// 如果都失败了,返回原始字符串
|
||||
log.warn("无法解析为JSON,返回原始字符串: {}", body);
|
||||
return body;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("解析JSON响应失败", e);
|
||||
throw new RuntimeException("解析JSON响应失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取HTTP状态码
|
||||
* @return 状态码
|
||||
*/
|
||||
public int statusCode() {
|
||||
return response.statusCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取响应头
|
||||
* @param name 头名称
|
||||
* @return 头值
|
||||
*/
|
||||
public String header(String name) {
|
||||
return response.getHeader(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有响应头
|
||||
* @return 响应头Map
|
||||
*/
|
||||
public Map<String, String> headers() {
|
||||
MultiMap responseHeaders = response.headers();
|
||||
Map<String, String> result = new java.util.HashMap<>();
|
||||
for (String name : responseHeaders.names()) {
|
||||
result.put(name, responseHeaders.get(name));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查请求是否成功
|
||||
* @return true表示成功(2xx状态码),false表示失败
|
||||
*/
|
||||
public boolean isSuccess() {
|
||||
int status = statusCode();
|
||||
return status >= 200 && status < 300;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取原始响应对象
|
||||
* @return HttpResponse对象
|
||||
*/
|
||||
public HttpResponse<Buffer> getOriginalResponse() {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
144
parser/src/main/java/cn/qaiu/parser/JsLogger.java
Normal file
144
parser/src/main/java/cn/qaiu/parser/JsLogger.java
Normal file
@@ -0,0 +1,144 @@
|
||||
package cn.qaiu.parser;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* JavaScript日志封装
|
||||
* 为JavaScript提供日志功能
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2025/10/17
|
||||
*/
|
||||
public class JsLogger {
|
||||
|
||||
private final Logger logger;
|
||||
private final String prefix;
|
||||
|
||||
public JsLogger(String name) {
|
||||
this.logger = LoggerFactory.getLogger(name);
|
||||
this.prefix = "[" + name + "] ";
|
||||
}
|
||||
|
||||
public JsLogger(Class<?> clazz) {
|
||||
this.logger = LoggerFactory.getLogger(clazz);
|
||||
this.prefix = "[" + clazz.getSimpleName() + "] ";
|
||||
}
|
||||
|
||||
/**
|
||||
* 调试日志
|
||||
* @param message 日志消息
|
||||
*/
|
||||
public void debug(String message) {
|
||||
logger.debug(prefix + message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调试日志(带参数)
|
||||
* @param message 日志消息模板
|
||||
* @param args 参数
|
||||
*/
|
||||
public void debug(String message, Object... args) {
|
||||
logger.debug(prefix + message, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 信息日志
|
||||
* @param message 日志消息
|
||||
*/
|
||||
public void info(String message) {
|
||||
logger.info(prefix + message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 信息日志(带参数)
|
||||
* @param message 日志消息模板
|
||||
* @param args 参数
|
||||
*/
|
||||
public void info(String message, Object... args) {
|
||||
logger.info(prefix + message, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 警告日志
|
||||
* @param message 日志消息
|
||||
*/
|
||||
public void warn(String message) {
|
||||
logger.warn(prefix + message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 警告日志(带参数)
|
||||
* @param message 日志消息模板
|
||||
* @param args 参数
|
||||
*/
|
||||
public void warn(String message, Object... args) {
|
||||
logger.warn(prefix + message, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误日志
|
||||
* @param message 日志消息
|
||||
*/
|
||||
public void error(String message) {
|
||||
logger.error(prefix + message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误日志(带参数)
|
||||
* @param message 日志消息模板
|
||||
* @param args 参数
|
||||
*/
|
||||
public void error(String message, Object... args) {
|
||||
logger.error(prefix + message, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误日志(带异常)
|
||||
* @param message 日志消息
|
||||
* @param throwable 异常对象
|
||||
*/
|
||||
public void error(String message, Throwable throwable) {
|
||||
logger.error(prefix + message, throwable);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否启用调试级别日志
|
||||
* @return true表示启用,false表示不启用
|
||||
*/
|
||||
public boolean isDebugEnabled() {
|
||||
return logger.isDebugEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否启用信息级别日志
|
||||
* @return true表示启用,false表示不启用
|
||||
*/
|
||||
public boolean isInfoEnabled() {
|
||||
return logger.isInfoEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否启用警告级别日志
|
||||
* @return true表示启用,false表示不启用
|
||||
*/
|
||||
public boolean isWarnEnabled() {
|
||||
return logger.isWarnEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否启用错误级别日志
|
||||
* @return true表示启用,false表示不启用
|
||||
*/
|
||||
public boolean isErrorEnabled() {
|
||||
return logger.isErrorEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取原始Logger对象
|
||||
* @return Logger对象
|
||||
*/
|
||||
public Logger getOriginalLogger() {
|
||||
return logger;
|
||||
}
|
||||
}
|
||||
283
parser/src/main/java/cn/qaiu/parser/JsParserExecutor.java
Normal file
283
parser/src/main/java/cn/qaiu/parser/JsParserExecutor.java
Normal file
@@ -0,0 +1,283 @@
|
||||
package cn.qaiu.parser;
|
||||
|
||||
import cn.qaiu.entity.FileInfo;
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.Promise;
|
||||
import org.openjdk.nashorn.api.scripting.ScriptObjectMirror;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.script.Invocable;
|
||||
import javax.script.ScriptEngine;
|
||||
import javax.script.ScriptEngineManager;
|
||||
import javax.script.ScriptException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* JavaScript解析器执行器
|
||||
* 实现IPanTool接口,执行JavaScript解析器逻辑
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2025/10/17
|
||||
*/
|
||||
public class JsParserExecutor implements IPanTool {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(JsParserExecutor.class);
|
||||
|
||||
private final CustomParserConfig config;
|
||||
private final ShareLinkInfo shareLinkInfo;
|
||||
private final ScriptEngine engine;
|
||||
private final JsHttpClient httpClient;
|
||||
private final JsLogger jsLogger;
|
||||
private final JsShareLinkInfoWrapper shareLinkInfoWrapper;
|
||||
private final Promise<String> promise = Promise.promise();
|
||||
|
||||
public JsParserExecutor(ShareLinkInfo shareLinkInfo, CustomParserConfig config) {
|
||||
this.config = config;
|
||||
this.shareLinkInfo = shareLinkInfo;
|
||||
this.engine = initEngine();
|
||||
this.httpClient = new JsHttpClient();
|
||||
this.jsLogger = new JsLogger("JsParser-" + config.getType());
|
||||
this.shareLinkInfoWrapper = new JsShareLinkInfoWrapper(shareLinkInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取ShareLinkInfo对象
|
||||
* @return ShareLinkInfo对象
|
||||
*/
|
||||
public ShareLinkInfo getShareLinkInfo() {
|
||||
return shareLinkInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化JavaScript引擎
|
||||
*/
|
||||
private ScriptEngine initEngine() {
|
||||
try {
|
||||
ScriptEngineManager engineManager = new ScriptEngineManager();
|
||||
ScriptEngine engine = engineManager.getEngineByName("JavaScript");
|
||||
|
||||
if (engine == null) {
|
||||
throw new RuntimeException("无法创建JavaScript引擎,请确保Nashorn可用");
|
||||
}
|
||||
|
||||
// 注入Java对象到JavaScript环境
|
||||
engine.put("http", httpClient);
|
||||
engine.put("logger", jsLogger);
|
||||
engine.put("shareLinkInfo", shareLinkInfoWrapper);
|
||||
|
||||
// 执行JavaScript代码
|
||||
engine.eval(config.getJsCode());
|
||||
|
||||
log.debug("JavaScript引擎初始化成功,解析器类型: {}", config.getType());
|
||||
return engine;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("JavaScript引擎初始化失败", e);
|
||||
throw new RuntimeException("JavaScript引擎初始化失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<String> parse() {
|
||||
try {
|
||||
jsLogger.info("开始执行JavaScript解析器: {}", config.getType());
|
||||
|
||||
// 直接调用全局parse函数
|
||||
Object parseFunction = engine.get("parse");
|
||||
if (parseFunction == null) {
|
||||
throw new RuntimeException("JavaScript代码中未找到parse函数");
|
||||
}
|
||||
|
||||
if (parseFunction instanceof ScriptObjectMirror) {
|
||||
ScriptObjectMirror parseMirror = (ScriptObjectMirror) parseFunction;
|
||||
|
||||
Object result = parseMirror.call(null, shareLinkInfoWrapper, httpClient, jsLogger);
|
||||
|
||||
if (result instanceof String) {
|
||||
jsLogger.info("解析成功: {}", result);
|
||||
promise.complete((String) result);
|
||||
} else {
|
||||
jsLogger.error("parse方法返回值类型错误,期望String,实际: {}",
|
||||
result != null ? result.getClass().getSimpleName() : "null");
|
||||
promise.fail("parse方法返回值类型错误");
|
||||
}
|
||||
} else {
|
||||
throw new RuntimeException("parse函数类型错误");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
jsLogger.error("JavaScript解析器执行失败", e);
|
||||
promise.fail("JavaScript解析器执行失败: " + e.getMessage());
|
||||
}
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<List<FileInfo>> parseFileList() {
|
||||
Promise<List<FileInfo>> promise = Promise.promise();
|
||||
|
||||
try {
|
||||
jsLogger.info("开始执行JavaScript文件列表解析: {}", config.getType());
|
||||
|
||||
// 直接调用全局parseFileList函数
|
||||
Object parseFileListFunction = engine.get("parseFileList");
|
||||
if (parseFileListFunction == null) {
|
||||
throw new RuntimeException("JavaScript代码中未找到parseFileList函数");
|
||||
}
|
||||
|
||||
// 调用parseFileList方法
|
||||
if (parseFileListFunction instanceof ScriptObjectMirror) {
|
||||
ScriptObjectMirror parseFileListMirror = (ScriptObjectMirror) parseFileListFunction;
|
||||
|
||||
Object result = parseFileListMirror.call(null, shareLinkInfoWrapper, httpClient, jsLogger);
|
||||
|
||||
if (result instanceof ScriptObjectMirror) {
|
||||
ScriptObjectMirror resultMirror = (ScriptObjectMirror) result;
|
||||
List<FileInfo> fileList = convertToFileInfoList(resultMirror);
|
||||
|
||||
jsLogger.info("文件列表解析成功,共 {} 个文件", fileList.size());
|
||||
promise.complete(fileList);
|
||||
} else {
|
||||
jsLogger.error("parseFileList方法返回值类型错误,期望数组,实际: {}",
|
||||
result != null ? result.getClass().getSimpleName() : "null");
|
||||
promise.fail("parseFileList方法返回值类型错误");
|
||||
}
|
||||
} else {
|
||||
throw new RuntimeException("parseFileList函数类型错误");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
jsLogger.error("JavaScript文件列表解析失败", e);
|
||||
promise.fail("JavaScript文件列表解析失败: " + e.getMessage());
|
||||
}
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<String> parseById() {
|
||||
Promise<String> promise = Promise.promise();
|
||||
|
||||
try {
|
||||
jsLogger.info("开始执行JavaScript按ID解析: {}", config.getType());
|
||||
|
||||
// 直接调用全局parseById函数
|
||||
Object parseByIdFunction = engine.get("parseById");
|
||||
if (parseByIdFunction == null) {
|
||||
throw new RuntimeException("JavaScript代码中未找到parseById函数");
|
||||
}
|
||||
|
||||
// 调用parseById方法
|
||||
if (parseByIdFunction instanceof ScriptObjectMirror) {
|
||||
ScriptObjectMirror parseByIdMirror = (ScriptObjectMirror) parseByIdFunction;
|
||||
|
||||
Object result = parseByIdMirror.call(null, shareLinkInfoWrapper, httpClient, jsLogger);
|
||||
|
||||
if (result instanceof String) {
|
||||
jsLogger.info("按ID解析成功: {}", result);
|
||||
promise.complete((String) result);
|
||||
} else {
|
||||
jsLogger.error("parseById方法返回值类型错误,期望String,实际: {}",
|
||||
result != null ? result.getClass().getSimpleName() : "null");
|
||||
promise.fail("parseById方法返回值类型错误");
|
||||
}
|
||||
} else {
|
||||
throw new RuntimeException("parseById函数类型错误");
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
jsLogger.error("JavaScript按ID解析失败", e);
|
||||
promise.fail("JavaScript按ID解析失败: " + e.getMessage());
|
||||
}
|
||||
|
||||
return promise.future();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将JavaScript对象数组转换为FileInfo列表
|
||||
*/
|
||||
private List<FileInfo> convertToFileInfoList(ScriptObjectMirror resultMirror) {
|
||||
List<FileInfo> fileList = new ArrayList<>();
|
||||
|
||||
if (resultMirror.isArray()) {
|
||||
for (int i = 0; i < resultMirror.size(); i++) {
|
||||
Object item = resultMirror.get(String.valueOf(i));
|
||||
if (item instanceof ScriptObjectMirror) {
|
||||
FileInfo fileInfo = convertToFileInfo((ScriptObjectMirror) item);
|
||||
if (fileInfo != null) {
|
||||
fileList.add(fileInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fileList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将JavaScript对象转换为FileInfo
|
||||
*/
|
||||
private FileInfo convertToFileInfo(ScriptObjectMirror itemMirror) {
|
||||
try {
|
||||
FileInfo fileInfo = new FileInfo();
|
||||
|
||||
// 设置基本字段
|
||||
if (itemMirror.hasMember("fileName")) {
|
||||
fileInfo.setFileName(itemMirror.getMember("fileName").toString());
|
||||
}
|
||||
if (itemMirror.hasMember("fileId")) {
|
||||
fileInfo.setFileId(itemMirror.getMember("fileId").toString());
|
||||
}
|
||||
if (itemMirror.hasMember("fileType")) {
|
||||
fileInfo.setFileType(itemMirror.getMember("fileType").toString());
|
||||
}
|
||||
if (itemMirror.hasMember("size")) {
|
||||
Object size = itemMirror.getMember("size");
|
||||
if (size instanceof Number) {
|
||||
fileInfo.setSize(((Number) size).longValue());
|
||||
}
|
||||
}
|
||||
if (itemMirror.hasMember("sizeStr")) {
|
||||
fileInfo.setSizeStr(itemMirror.getMember("sizeStr").toString());
|
||||
}
|
||||
if (itemMirror.hasMember("createTime")) {
|
||||
fileInfo.setCreateTime(itemMirror.getMember("createTime").toString());
|
||||
}
|
||||
if (itemMirror.hasMember("updateTime")) {
|
||||
fileInfo.setUpdateTime(itemMirror.getMember("updateTime").toString());
|
||||
}
|
||||
if (itemMirror.hasMember("createBy")) {
|
||||
fileInfo.setCreateBy(itemMirror.getMember("createBy").toString());
|
||||
}
|
||||
if (itemMirror.hasMember("downloadCount")) {
|
||||
Object downloadCount = itemMirror.getMember("downloadCount");
|
||||
if (downloadCount instanceof Number) {
|
||||
fileInfo.setDownloadCount(((Number) downloadCount).intValue());
|
||||
}
|
||||
}
|
||||
if (itemMirror.hasMember("fileIcon")) {
|
||||
fileInfo.setFileIcon(itemMirror.getMember("fileIcon").toString());
|
||||
}
|
||||
if (itemMirror.hasMember("panType")) {
|
||||
fileInfo.setPanType(itemMirror.getMember("panType").toString());
|
||||
}
|
||||
if (itemMirror.hasMember("parserUrl")) {
|
||||
fileInfo.setParserUrl(itemMirror.getMember("parserUrl").toString());
|
||||
}
|
||||
if (itemMirror.hasMember("previewUrl")) {
|
||||
fileInfo.setPreviewUrl(itemMirror.getMember("previewUrl").toString());
|
||||
}
|
||||
|
||||
return fileInfo;
|
||||
|
||||
} catch (Exception e) {
|
||||
jsLogger.error("转换FileInfo对象失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
264
parser/src/main/java/cn/qaiu/parser/JsScriptLoader.java
Normal file
264
parser/src/main/java/cn/qaiu/parser/JsScriptLoader.java
Normal file
@@ -0,0 +1,264 @@
|
||||
package cn.qaiu.parser;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* JavaScript脚本加载器
|
||||
* 自动加载资源目录和外部目录的JavaScript脚本文件
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2025/10/17
|
||||
*/
|
||||
public class JsScriptLoader {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(JsScriptLoader.class);
|
||||
|
||||
private static final String RESOURCE_PATH = "custom-parsers";
|
||||
private static final String EXTERNAL_PATH = "./custom-parsers";
|
||||
|
||||
// 系统属性配置的外部目录路径
|
||||
private static final String EXTERNAL_PATH_PROPERTY = "parser.custom-parsers.path";
|
||||
|
||||
/**
|
||||
* 加载所有JavaScript脚本
|
||||
* @return 解析器配置列表
|
||||
*/
|
||||
public static List<CustomParserConfig> loadAllScripts() {
|
||||
List<CustomParserConfig> configs = new ArrayList<>();
|
||||
|
||||
// 1. 加载资源目录下的JS文件
|
||||
try {
|
||||
List<CustomParserConfig> resourceConfigs = loadFromResources();
|
||||
configs.addAll(resourceConfigs);
|
||||
log.info("从资源目录加载了 {} 个JavaScript解析器", resourceConfigs.size());
|
||||
} catch (Exception e) {
|
||||
log.warn("从资源目录加载JavaScript脚本失败", e);
|
||||
}
|
||||
|
||||
// 2. 加载外部目录下的JS文件
|
||||
try {
|
||||
List<CustomParserConfig> externalConfigs = loadFromExternal();
|
||||
configs.addAll(externalConfigs);
|
||||
log.info("从外部目录加载了 {} 个JavaScript解析器", externalConfigs.size());
|
||||
} catch (Exception e) {
|
||||
log.warn("从外部目录加载JavaScript脚本失败", e);
|
||||
}
|
||||
|
||||
log.info("总共加载了 {} 个JavaScript解析器", configs.size());
|
||||
return configs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从资源目录加载JavaScript脚本
|
||||
*/
|
||||
private static List<CustomParserConfig> loadFromResources() {
|
||||
List<CustomParserConfig> configs = new ArrayList<>();
|
||||
|
||||
try {
|
||||
// 获取资源目录的输入流
|
||||
InputStream resourceStream = JsScriptLoader.class.getClassLoader()
|
||||
.getResourceAsStream(RESOURCE_PATH);
|
||||
|
||||
if (resourceStream == null) {
|
||||
log.debug("资源目录 {} 不存在", RESOURCE_PATH);
|
||||
return configs;
|
||||
}
|
||||
|
||||
// 读取资源目录下的所有文件
|
||||
String resourcePath = JsScriptLoader.class.getClassLoader()
|
||||
.getResource(RESOURCE_PATH).getPath();
|
||||
|
||||
try (Stream<Path> paths = Files.walk(Paths.get(resourcePath))) {
|
||||
paths.filter(Files::isRegularFile)
|
||||
.filter(path -> path.toString().endsWith(".js"))
|
||||
.filter(path -> !isExcludedFile(path.getFileName().toString()))
|
||||
.forEach(path -> {
|
||||
try {
|
||||
String jsCode = Files.readString(path, StandardCharsets.UTF_8);
|
||||
CustomParserConfig config = JsScriptMetadataParser.parseScript(jsCode);
|
||||
configs.add(config);
|
||||
log.debug("从资源目录加载脚本: {}", path.getFileName());
|
||||
} catch (Exception e) {
|
||||
log.warn("加载资源脚本失败: {}", path.getFileName(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("从资源目录加载脚本时发生异常", e);
|
||||
}
|
||||
|
||||
return configs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从外部目录加载JavaScript脚本
|
||||
*/
|
||||
private static List<CustomParserConfig> loadFromExternal() {
|
||||
List<CustomParserConfig> configs = new ArrayList<>();
|
||||
|
||||
try {
|
||||
// 获取外部目录路径,支持系统属性配置
|
||||
String externalPath = getExternalPath();
|
||||
Path externalDir = Paths.get(externalPath);
|
||||
|
||||
if (!Files.exists(externalDir) || !Files.isDirectory(externalDir)) {
|
||||
log.debug("外部目录 {} 不存在或不是目录", externalPath);
|
||||
return configs;
|
||||
}
|
||||
|
||||
try (Stream<Path> paths = Files.walk(externalDir)) {
|
||||
paths.filter(Files::isRegularFile)
|
||||
.filter(path -> path.toString().endsWith(".js"))
|
||||
.filter(path -> !isExcludedFile(path.getFileName().toString()))
|
||||
.forEach(path -> {
|
||||
try {
|
||||
String jsCode = Files.readString(path, StandardCharsets.UTF_8);
|
||||
CustomParserConfig config = JsScriptMetadataParser.parseScript(jsCode);
|
||||
configs.add(config);
|
||||
log.debug("从外部目录加载脚本: {}", path.getFileName());
|
||||
} catch (Exception e) {
|
||||
log.warn("加载外部脚本失败: {}", path.getFileName(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("从外部目录加载脚本时发生异常", e);
|
||||
}
|
||||
|
||||
return configs;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取外部目录路径
|
||||
* 优先级:系统属性 > 环境变量 > 默认路径
|
||||
*/
|
||||
private static String getExternalPath() {
|
||||
// 1. 检查系统属性
|
||||
String systemProperty = System.getProperty(EXTERNAL_PATH_PROPERTY);
|
||||
if (systemProperty != null && !systemProperty.trim().isEmpty()) {
|
||||
log.debug("使用系统属性配置的外部目录: {}", systemProperty);
|
||||
return systemProperty;
|
||||
}
|
||||
|
||||
// 2. 检查环境变量
|
||||
String envVariable = System.getenv("PARSER_CUSTOM_PARSERS_PATH");
|
||||
if (envVariable != null && !envVariable.trim().isEmpty()) {
|
||||
log.debug("使用环境变量配置的外部目录: {}", envVariable);
|
||||
return envVariable;
|
||||
}
|
||||
|
||||
// 3. 使用默认路径
|
||||
log.debug("使用默认外部目录: {}", EXTERNAL_PATH);
|
||||
return EXTERNAL_PATH;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从指定文件加载JavaScript脚本
|
||||
* @param filePath 文件路径
|
||||
* @return 解析器配置
|
||||
*/
|
||||
public static CustomParserConfig loadFromFile(String filePath) {
|
||||
try {
|
||||
Path path = Paths.get(filePath);
|
||||
if (!Files.exists(path)) {
|
||||
throw new IllegalArgumentException("文件不存在: " + filePath);
|
||||
}
|
||||
|
||||
String jsCode = Files.readString(path, StandardCharsets.UTF_8);
|
||||
return JsScriptMetadataParser.parseScript(jsCode);
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("读取文件失败: " + filePath, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从指定文件加载JavaScript脚本(资源路径)
|
||||
* @param resourcePath 资源路径
|
||||
* @return 解析器配置
|
||||
*/
|
||||
public static CustomParserConfig loadFromResource(String resourcePath) {
|
||||
try {
|
||||
InputStream inputStream = JsScriptLoader.class.getClassLoader()
|
||||
.getResourceAsStream(resourcePath);
|
||||
|
||||
if (inputStream == null) {
|
||||
throw new IllegalArgumentException("资源文件不存在: " + resourcePath);
|
||||
}
|
||||
|
||||
String jsCode = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
return JsScriptMetadataParser.parseScript(jsCode);
|
||||
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("读取资源文件失败: " + resourcePath, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查外部目录是否存在
|
||||
* @return true表示存在,false表示不存在
|
||||
*/
|
||||
public static boolean isExternalDirectoryExists() {
|
||||
Path externalDir = Paths.get(EXTERNAL_PATH);
|
||||
return Files.exists(externalDir) && Files.isDirectory(externalDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建外部目录
|
||||
* @return true表示创建成功,false表示创建失败
|
||||
*/
|
||||
public static boolean createExternalDirectory() {
|
||||
try {
|
||||
Path externalDir = Paths.get(EXTERNAL_PATH);
|
||||
Files.createDirectories(externalDir);
|
||||
log.info("创建外部目录成功: {}", EXTERNAL_PATH);
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
log.error("创建外部目录失败: {}", EXTERNAL_PATH, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取外部目录路径
|
||||
* @return 外部目录路径
|
||||
*/
|
||||
public static String getExternalDirectoryPath() {
|
||||
return EXTERNAL_PATH;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资源目录路径
|
||||
* @return 资源目录路径
|
||||
*/
|
||||
public static String getResourceDirectoryPath() {
|
||||
return RESOURCE_PATH;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查文件是否应该被排除
|
||||
* @param fileName 文件名
|
||||
* @return true表示应该排除,false表示应该加载
|
||||
*/
|
||||
private static boolean isExcludedFile(String fileName) {
|
||||
// 排除类型定义文件和其他非解析器文件
|
||||
return fileName.equals("types.js") ||
|
||||
fileName.equals("jsconfig.json") ||
|
||||
fileName.equals("README.md") ||
|
||||
fileName.contains(".test.") ||
|
||||
fileName.contains(".spec.");
|
||||
}
|
||||
}
|
||||
195
parser/src/main/java/cn/qaiu/parser/JsScriptMetadataParser.java
Normal file
195
parser/src/main/java/cn/qaiu/parser/JsScriptMetadataParser.java
Normal file
@@ -0,0 +1,195 @@
|
||||
package cn.qaiu.parser;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* JavaScript脚本元数据解析器
|
||||
* 解析类油猴格式的元数据注释
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2025/10/17
|
||||
*/
|
||||
public class JsScriptMetadataParser {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(JsScriptMetadataParser.class);
|
||||
|
||||
// 元数据块匹配正则
|
||||
private static final Pattern METADATA_BLOCK_PATTERN = Pattern.compile(
|
||||
"//\\s*==UserScript==\\s*(.*?)\\s*//\\s*==/UserScript==",
|
||||
Pattern.DOTALL
|
||||
);
|
||||
|
||||
// 元数据行匹配正则
|
||||
private static final Pattern METADATA_LINE_PATTERN = Pattern.compile(
|
||||
"//\\s*@(\\w+)\\s+(.*)"
|
||||
);
|
||||
|
||||
/**
|
||||
* 解析JavaScript脚本,提取元数据并构建CustomParserConfig
|
||||
*
|
||||
* @param jsCode JavaScript代码
|
||||
* @return CustomParserConfig配置对象
|
||||
* @throws IllegalArgumentException 如果解析失败或缺少必填字段
|
||||
*/
|
||||
public static CustomParserConfig parseScript(String jsCode) {
|
||||
if (StringUtils.isBlank(jsCode)) {
|
||||
throw new IllegalArgumentException("JavaScript代码不能为空");
|
||||
}
|
||||
|
||||
// 1. 提取元数据块
|
||||
Map<String, String> metadata = extractMetadata(jsCode);
|
||||
|
||||
// 2. 验证必填字段
|
||||
validateRequiredFields(metadata);
|
||||
|
||||
// 3. 构建CustomParserConfig
|
||||
return buildConfig(metadata, jsCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取元数据
|
||||
*/
|
||||
private static Map<String, String> extractMetadata(String jsCode) {
|
||||
Map<String, String> metadata = new HashMap<>();
|
||||
|
||||
Matcher blockMatcher = METADATA_BLOCK_PATTERN.matcher(jsCode);
|
||||
if (!blockMatcher.find()) {
|
||||
throw new IllegalArgumentException("未找到元数据块,请确保包含 // ==UserScript== ... // /UserScript== 格式的注释");
|
||||
}
|
||||
|
||||
String metadataBlock = blockMatcher.group(1);
|
||||
Matcher lineMatcher = METADATA_LINE_PATTERN.matcher(metadataBlock);
|
||||
|
||||
while (lineMatcher.find()) {
|
||||
String key = lineMatcher.group(1).toLowerCase();
|
||||
String value = lineMatcher.group(2).trim();
|
||||
metadata.put(key, value);
|
||||
}
|
||||
|
||||
log.debug("解析到元数据: {}", metadata);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证必填字段
|
||||
*/
|
||||
private static void validateRequiredFields(Map<String, String> metadata) {
|
||||
if (!metadata.containsKey("name")) {
|
||||
throw new IllegalArgumentException("缺少必填字段 @name");
|
||||
}
|
||||
if (!metadata.containsKey("type")) {
|
||||
throw new IllegalArgumentException("缺少必填字段 @type");
|
||||
}
|
||||
if (!metadata.containsKey("displayname")) {
|
||||
throw new IllegalArgumentException("缺少必填字段 @displayName");
|
||||
}
|
||||
if (!metadata.containsKey("match")) {
|
||||
throw new IllegalArgumentException("缺少必填字段 @match");
|
||||
}
|
||||
|
||||
// 验证match字段包含KEY命名捕获组
|
||||
String matchPattern = metadata.get("match");
|
||||
if (!matchPattern.contains("(?<KEY>")) {
|
||||
throw new IllegalArgumentException("@match 正则表达式必须包含命名捕获组 KEY,用于提取分享键");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建CustomParserConfig
|
||||
*/
|
||||
private static CustomParserConfig buildConfig(Map<String, String> metadata, String jsCode) {
|
||||
CustomParserConfig.Builder builder = CustomParserConfig.builder()
|
||||
.type(metadata.get("type"))
|
||||
.displayName(metadata.get("displayname"))
|
||||
.isJsParser(true)
|
||||
.jsCode(jsCode)
|
||||
.metadata(metadata);
|
||||
|
||||
// 设置匹配正则
|
||||
String matchPattern = metadata.get("match");
|
||||
if (StringUtils.isNotBlank(matchPattern)) {
|
||||
builder.matchPattern(matchPattern);
|
||||
}
|
||||
|
||||
// 设置可选字段
|
||||
if (metadata.containsKey("description")) {
|
||||
// description字段可以用于其他用途,暂时不存储到config中
|
||||
}
|
||||
|
||||
if (metadata.containsKey("author")) {
|
||||
// author字段可以用于其他用途,暂时不存储到config中
|
||||
}
|
||||
|
||||
if (metadata.containsKey("version")) {
|
||||
// version字段可以用于其他用途,暂时不存储到config中
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查JavaScript代码是否包含有效的元数据块
|
||||
*
|
||||
* @param jsCode JavaScript代码
|
||||
* @return true表示包含有效元数据,false表示不包含
|
||||
*/
|
||||
public static boolean hasValidMetadata(String jsCode) {
|
||||
if (StringUtils.isBlank(jsCode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
Map<String, String> metadata = extractMetadata(jsCode);
|
||||
validateRequiredFields(metadata);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.debug("JavaScript代码不包含有效元数据: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从JavaScript代码中提取脚本名称
|
||||
*
|
||||
* @param jsCode JavaScript代码
|
||||
* @return 脚本名称,如果未找到则返回null
|
||||
*/
|
||||
public static String extractScriptName(String jsCode) {
|
||||
if (StringUtils.isBlank(jsCode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
Map<String, String> metadata = extractMetadata(jsCode);
|
||||
return metadata.get("name");
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从JavaScript代码中提取脚本类型
|
||||
*
|
||||
* @param jsCode JavaScript代码
|
||||
* @return 脚本类型,如果未找到则返回null
|
||||
*/
|
||||
public static String extractScriptType(String jsCode) {
|
||||
if (StringUtils.isBlank(jsCode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
Map<String, String> metadata = extractMetadata(jsCode);
|
||||
return metadata.get("type");
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
163
parser/src/main/java/cn/qaiu/parser/JsShareLinkInfoWrapper.java
Normal file
163
parser/src/main/java/cn/qaiu/parser/JsShareLinkInfoWrapper.java
Normal file
@@ -0,0 +1,163 @@
|
||||
package cn.qaiu.parser;
|
||||
|
||||
import cn.qaiu.entity.ShareLinkInfo;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* ShareLinkInfo的JavaScript包装器
|
||||
* 为JavaScript提供ShareLinkInfo对象的访问接口
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2025/10/17
|
||||
*/
|
||||
public class JsShareLinkInfoWrapper {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(JsShareLinkInfoWrapper.class);
|
||||
|
||||
private final ShareLinkInfo shareLinkInfo;
|
||||
|
||||
public JsShareLinkInfoWrapper(ShareLinkInfo shareLinkInfo) {
|
||||
this.shareLinkInfo = shareLinkInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分享URL
|
||||
* @return 分享URL
|
||||
*/
|
||||
public String getShareUrl() {
|
||||
return shareLinkInfo.getShareUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分享Key
|
||||
* @return 分享Key
|
||||
*/
|
||||
public String getShareKey() {
|
||||
return shareLinkInfo.getShareKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分享密码
|
||||
* @return 分享密码
|
||||
*/
|
||||
public String getSharePassword() {
|
||||
return shareLinkInfo.getSharePassword();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网盘类型
|
||||
* @return 网盘类型
|
||||
*/
|
||||
public String getType() {
|
||||
return shareLinkInfo.getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网盘名称
|
||||
* @return 网盘名称
|
||||
*/
|
||||
public String getPanName() {
|
||||
return shareLinkInfo.getPanName();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取其他参数
|
||||
* @param key 参数键
|
||||
* @return 参数值
|
||||
*/
|
||||
public Object getOtherParam(String key) {
|
||||
if (key == null) {
|
||||
return null;
|
||||
}
|
||||
return shareLinkInfo.getOtherParam().get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有其他参数
|
||||
* @return 参数Map
|
||||
*/
|
||||
public Map<String, Object> getAllOtherParams() {
|
||||
return shareLinkInfo.getOtherParam();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否包含指定参数
|
||||
* @param key 参数键
|
||||
* @return true表示包含,false表示不包含
|
||||
*/
|
||||
public boolean hasOtherParam(String key) {
|
||||
if (key == null) {
|
||||
return false;
|
||||
}
|
||||
return shareLinkInfo.getOtherParam().containsKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取其他参数的字符串值
|
||||
* @param key 参数键
|
||||
* @return 参数值(字符串形式)
|
||||
*/
|
||||
public String getOtherParamAsString(String key) {
|
||||
Object value = getOtherParam(key);
|
||||
return value != null ? value.toString() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取其他参数的整数值
|
||||
* @param key 参数键
|
||||
* @return 参数值(整数形式)
|
||||
*/
|
||||
public Integer getOtherParamAsInteger(String key) {
|
||||
Object value = getOtherParam(key);
|
||||
if (value instanceof Integer) {
|
||||
return (Integer) value;
|
||||
} else if (value instanceof Number) {
|
||||
return ((Number) value).intValue();
|
||||
} else if (value instanceof String) {
|
||||
try {
|
||||
return Integer.parseInt((String) value);
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("无法将参数 {} 转换为整数: {}", key, value);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取其他参数的布尔值
|
||||
* @param key 参数键
|
||||
* @return 参数值(布尔形式)
|
||||
*/
|
||||
public Boolean getOtherParamAsBoolean(String key) {
|
||||
Object value = getOtherParam(key);
|
||||
if (value instanceof Boolean) {
|
||||
return (Boolean) value;
|
||||
} else if (value instanceof String) {
|
||||
return Boolean.parseBoolean((String) value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取原始的ShareLinkInfo对象
|
||||
* @return ShareLinkInfo对象
|
||||
*/
|
||||
public ShareLinkInfo getOriginalShareLinkInfo() {
|
||||
return shareLinkInfo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "JsShareLinkInfoWrapper{" +
|
||||
"shareUrl='" + getShareUrl() + '\'' +
|
||||
", shareKey='" + getShareKey() + '\'' +
|
||||
", sharePassword='" + getSharePassword() + '\'' +
|
||||
", type='" + getType() + '\'' +
|
||||
", panName='" + getPanName() + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -148,13 +148,19 @@ public class ParserCreate {
|
||||
|
||||
// 自定义解析器处理
|
||||
if (isCustomParser) {
|
||||
try {
|
||||
return this.customParserConfig.getToolClass()
|
||||
.getDeclaredConstructor(ShareLinkInfo.class)
|
||||
.newInstance(shareLinkInfo);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("无法创建自定义工具实例: " +
|
||||
customParserConfig.getToolClass().getName(), e);
|
||||
// 检查是否为JavaScript解析器
|
||||
if (customParserConfig.isJsParser()) {
|
||||
return new JsParserExecutor(shareLinkInfo, customParserConfig);
|
||||
} else {
|
||||
// Java实现的解析器
|
||||
try {
|
||||
return this.customParserConfig.getToolClass()
|
||||
.getDeclaredConstructor(ShareLinkInfo.class)
|
||||
.newInstance(shareLinkInfo);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("无法创建自定义工具实例: " +
|
||||
customParserConfig.getToolClass().getName(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,10 +232,12 @@ public class ParserCreate {
|
||||
public ParserCreate setShareLinkInfoPwd(String pwd) {
|
||||
if (pwd != null) {
|
||||
shareLinkInfo.setSharePassword(pwd);
|
||||
standardUrl = standardUrl.replace("{pwd}", pwd);
|
||||
shareLinkInfo.setStandardUrl(standardUrl);
|
||||
if (shareLinkInfo.getShareUrl().contains("{pwd}")) {
|
||||
shareLinkInfo.setShareUrl(standardUrl);
|
||||
if (standardUrl != null) {
|
||||
standardUrl = standardUrl.replace("{pwd}", pwd);
|
||||
shareLinkInfo.setStandardUrl(standardUrl);
|
||||
if (shareLinkInfo.getShareUrl().contains("{pwd}")) {
|
||||
shareLinkInfo.setShareUrl(standardUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
|
||||
Reference in New Issue
Block a user