diff --git a/parser/.flattened-pom.xml b/parser/.flattened-pom.xml
new file mode 100644
index 0000000..79176ba
--- /dev/null
+++ b/parser/.flattened-pom.xml
@@ -0,0 +1,77 @@
+
+
+ 4.0.0
+ cn.qaiu
+ parser
+ 10.1.17
+ cn.qaiu:parser
+ NFD parser module
+ https://qaiu.top
+
+
+ MIT License
+ https://opensource.org/license/mit
+
+
+
+
+ qaiu
+ qaiu00@gmail.com
+ https://qaiu.top
+
+
+
+ scm:git:https://github.com/qaiu/netdisk-fast-download.git
+ scm:git:ssh://git@github.com:qaiu/netdisk-fast-download.git
+ https://github.com/qaiu/netdisk-fast-download
+
+
+
+ ch.qos.logback
+ logback-classic
+ 1.5.19
+ runtime
+
+
+ org.slf4j
+ slf4j-api
+ 2.0.5
+ compile
+
+
+ io.vertx
+ vertx-web-client
+ 4.5.21
+ compile
+
+
+ org.apache.commons
+ commons-lang3
+ 3.18.0
+ compile
+
+
+ org.openjdk.nashorn
+ nashorn-core
+ 15.4
+ compile
+
+
+ org.brotli
+ dec
+ 0.1.2
+ compile
+
+
+
+
+
+ org.sonatype.central
+ central-publishing-maven-plugin
+ 0.6.0
+ true
+
+
+
+
diff --git a/parser/doc/CUSTOM_PARSER_GUIDE.md b/parser/doc/CUSTOM_PARSER_GUIDE.md
index b27416c..e8ed36a 100644
--- a/parser/doc/CUSTOM_PARSER_GUIDE.md
+++ b/parser/doc/CUSTOM_PARSER_GUIDE.md
@@ -233,11 +233,22 @@ public class Example {
.setShareLinkInfoPwd("1234") // 设置密码(可选)
.createTool(); // 创建工具实例
- // 解析获取下载链接
+ // 方式1: 使用同步方法解析(推荐)
String downloadUrl = tool.parseSync();
System.out.println("下载链接: " + downloadUrl);
- // 方式2: 异步解析
+ // 方式2: 使用同步方法解析文件列表
+ List files = tool.parseFileListSync();
+ System.out.println("文件列表: " + files.size() + " 个文件");
+
+ // 方式3: 使用同步方法根据文件ID获取下载链接
+ if (!files.isEmpty()) {
+ String fileId = files.get(0).getFileId();
+ String fileDownloadUrl = tool.parseByIdSync();
+ System.out.println("文件下载链接: " + fileDownloadUrl);
+ }
+
+ // 方式4: 异步解析(仍支持)
tool.parse().onSuccess(url -> {
System.out.println("异步获取下载链接: " + url);
}).onFailure(err -> {
@@ -247,6 +258,42 @@ public class Example {
}
```
+## 同步方法支持
+
+解析器现在支持三种同步方法,简化了使用方式:
+
+### 1. parseSync()
+解析单个文件的下载链接:
+```java
+String downloadUrl = tool.parseSync();
+```
+
+### 2. parseFileListSync()
+解析文件列表(目录):
+```java
+List files = tool.parseFileListSync();
+for (FileInfo file : files) {
+ System.out.println("文件: " + file.getFileName());
+}
+```
+
+### 3. parseByIdSync()
+根据文件ID获取下载链接:
+```java
+String fileDownloadUrl = tool.parseByIdSync();
+```
+
+### 同步方法优势
+- **简化使用**: 无需处理 Future 和回调
+- **异常处理**: 同步方法会抛出异常,便于错误处理
+- **代码简洁**: 减少异步代码的复杂性
+
+### 异步方法仍可用
+原有的异步方法仍然支持:
+- `parse()`: 返回 `Future`
+- `parseFileList()`: 返回 `Future>`
+- `parseById()`: 返回 `Future`
+
## 注意事项
### 1. 类型标识规范
@@ -363,9 +410,21 @@ public class CompleteExample {
// 创建工具并解析
IPanTool tool = parser.createTool();
+
+ // 使用同步方法解析
String url = tool.parseSync();
System.out.println("✓ 下载链接: " + url);
+ // 解析文件列表
+ List files = tool.parseFileListSync();
+ System.out.println("✓ 文件列表: " + files.size() + " 个文件");
+
+ // 根据文件ID获取下载链接
+ if (!files.isEmpty()) {
+ String fileDownloadUrl = tool.parseByIdSync();
+ System.out.println("✓ 文件下载链接: " + fileDownloadUrl);
+ }
+
} catch (Exception e) {
System.err.println("✗ 解析失败: " + e.getMessage());
}
diff --git a/parser/doc/JAVASCRIPT_PARSER_GUIDE.md b/parser/doc/JAVASCRIPT_PARSER_GUIDE.md
new file mode 100644
index 0000000..8bdc4cf
--- /dev/null
+++ b/parser/doc/JAVASCRIPT_PARSER_GUIDE.md
@@ -0,0 +1,465 @@
+# JavaScript解析器扩展开发指南
+
+## 概述
+
+本指南介绍如何使用JavaScript编写自定义网盘解析器,支持通过JavaScript代码实现网盘解析逻辑,无需编写Java代码。
+
+## 快速开始
+
+### 1. 创建JavaScript脚本
+
+在 `./custom-parsers/` 目录下创建 `.js` 文件,使用以下模板:
+
+```javascript
+// ==UserScript==
+// @name 我的解析器
+// @type my_parser
+// @displayName 我的网盘
+// @description 使用JavaScript实现的网盘解析器
+// @match https?://example\.com/s/(?\w+)
+// @author yourname
+// @version 1.0.0
+// ==/UserScript==
+
+// 使用require导入类型定义(仅用于IDE类型提示)
+var types = require('./types');
+/** @typedef {types.ShareLinkInfo} ShareLinkInfo */
+/** @typedef {types.JsHttpClient} JsHttpClient */
+/** @typedef {types.JsLogger} JsLogger */
+/** @typedef {types.FileInfo} FileInfo */
+
+/**
+ * 解析单个文件下载链接
+ * @param {ShareLinkInfo} shareLinkInfo - 分享链接信息
+ * @param {JsHttpClient} http - HTTP客户端
+ * @param {JsLogger} logger - 日志对象
+ * @returns {string} 下载链接
+ */
+function parse(shareLinkInfo, http, logger) {
+ var url = shareLinkInfo.getShareUrl();
+ var response = http.get(url);
+ return response.body();
+}
+
+// 导出函数
+exports.parse = parse;
+```
+
+### 2. 重启应用
+
+重启应用后,JavaScript解析器会自动加载并注册。
+
+## 元数据格式
+
+### 必填字段
+
+- `@name`: 脚本名称
+- `@type`: 解析器类型标识(唯一)
+- `@displayName`: 显示名称
+- `@match`: URL匹配正则(必须包含 `(?...)` 命名捕获组)
+
+### 可选字段
+
+- `@description`: 描述信息
+- `@author`: 作者
+- `@version`: 版本号
+
+### 示例
+
+```javascript
+// ==UserScript==
+// @name 蓝奏云解析器
+// @type lanzou_js
+// @displayName 蓝奏云(JS)
+// @description 使用JavaScript实现的蓝奏云解析器
+// @match https?://.*\.lanzou[a-z]\.com/(?\w+)
+// @match https?://.*\.lanzoui\.com/(?\w+)
+// @author qaiu
+// @version 1.0.0
+// ==/UserScript==
+```
+
+## API参考
+
+### ShareLinkInfo对象
+
+提供分享链接信息的访问接口:
+
+```javascript
+// 获取分享URL
+var shareUrl = shareLinkInfo.getShareUrl();
+
+// 获取分享Key
+var shareKey = shareLinkInfo.getShareKey();
+
+// 获取分享密码
+var password = shareLinkInfo.getSharePassword();
+
+// 获取网盘类型
+var type = shareLinkInfo.getType();
+
+// 获取网盘名称
+var panName = shareLinkInfo.getPanName();
+
+// 获取其他参数
+var dirId = shareLinkInfo.getOtherParam("dirId");
+var paramJson = shareLinkInfo.getOtherParam("paramJson");
+
+// 检查参数是否存在
+if (shareLinkInfo.hasOtherParam("customParam")) {
+ var value = shareLinkInfo.getOtherParamAsString("customParam");
+}
+```
+
+### JsHttpClient对象
+
+提供HTTP请求功能:
+
+```javascript
+// GET请求
+var response = http.get("https://api.example.com/data");
+
+// POST请求
+var response = http.post("https://api.example.com/submit", {
+ key: "value",
+ data: "test"
+});
+
+// 设置请求头
+http.putHeader("User-Agent", "MyBot/1.0")
+ .putHeader("Authorization", "Bearer token");
+
+// 发送表单数据
+var formResponse = http.sendForm({
+ username: "user",
+ password: "pass"
+});
+
+// 发送JSON数据
+var jsonResponse = http.sendJson({
+ name: "test",
+ value: 123
+});
+```
+
+### JsHttpResponse对象
+
+处理HTTP响应:
+
+```javascript
+var response = http.get("https://api.example.com/data");
+
+// 获取响应体(字符串)
+var body = response.body();
+
+// 解析JSON响应
+var data = response.json();
+
+// 获取状态码
+var status = response.statusCode();
+
+// 获取响应头
+var contentType = response.header("Content-Type");
+var allHeaders = response.headers();
+
+// 检查请求是否成功
+if (response.isSuccess()) {
+ logger.info("请求成功");
+} else {
+ logger.error("请求失败: " + status);
+}
+```
+
+### JsLogger对象
+
+提供日志功能:
+
+```javascript
+// 不同级别的日志
+logger.debug("调试信息");
+logger.info("一般信息");
+logger.warn("警告信息");
+logger.error("错误信息");
+
+// 带参数的日志
+logger.info("用户 {} 访问了 {}", username, url);
+
+// 检查日志级别
+if (logger.isDebugEnabled()) {
+ logger.debug("详细的调试信息");
+}
+```
+
+## 实现方法
+
+JavaScript解析器支持三种方法,对应Java接口的三种同步方法:
+
+### parse方法(必填)
+
+解析单个文件的下载链接,对应Java的 `parseSync()` 方法:
+
+```javascript
+function parse(shareLinkInfo, http, logger) {
+ var shareUrl = shareLinkInfo.getShareUrl();
+ var password = shareLinkInfo.getSharePassword();
+
+ // 发起请求获取页面
+ var response = http.get(shareUrl);
+ var html = response.body();
+
+ // 解析HTML获取下载链接
+ var regex = /downloadUrl["']:\s*["']([^"']+)["']/;
+ var match = html.match(regex);
+
+ if (match) {
+ return match[1]; // 返回下载链接
+ } else {
+ throw new Error("无法解析下载链接");
+ }
+}
+```
+
+### parseFileList方法(可选)
+
+解析文件列表(目录),对应Java的 `parseFileListSync()` 方法:
+
+```javascript
+function parseFileList(shareLinkInfo, http, logger) {
+ var dirId = shareLinkInfo.getOtherParam("dirId") || "0";
+
+ // 请求文件列表API
+ var response = http.get("/api/list?dirId=" + dirId);
+ var data = response.json();
+
+ var fileList = [];
+ for (var i = 0; i < data.files.length; i++) {
+ var file = data.files[i];
+
+ var fileInfo = {
+ fileName: file.name,
+ fileId: file.id,
+ fileType: file.isDir ? "folder" : "file",
+ size: file.size,
+ sizeStr: formatSize(file.size),
+ createTime: file.createTime,
+ parserUrl: "/v2/redirectUrl/my_parser/" + file.id
+ };
+
+ fileList.push(fileInfo);
+ }
+
+ return fileList;
+}
+```
+
+### parseById方法(可选)
+
+根据文件ID获取下载链接,对应Java的 `parseByIdSync()` 方法:
+
+```javascript
+function parseById(shareLinkInfo, http, logger) {
+ var paramJson = shareLinkInfo.getOtherParam("paramJson");
+ var fileId = paramJson.fileId;
+
+ // 请求下载API
+ var response = http.get("/api/download?fileId=" + fileId);
+ var data = response.json();
+
+ return data.downloadUrl;
+}
+```
+
+## 同步方法支持
+
+JavaScript解析器的方法都是同步执行的,对应Java接口的三种同步方法:
+
+### 方法对应关系
+
+| JavaScript方法 | Java同步方法 | 说明 |
+|----------------|-------------|------|
+| `parse()` | `parseSync()` | 解析单个文件下载链接 |
+| `parseFileList()` | `parseFileListSync()` | 解析文件列表 |
+| `parseById()` | `parseByIdSync()` | 根据文件ID获取下载链接 |
+
+### 使用示例
+
+```javascript
+// 在Java中调用JavaScript解析器
+IPanTool tool = ParserCreate.fromType("my_js_parser")
+ .shareKey("abc123")
+ .createTool();
+
+// 使用同步方法调用JavaScript函数
+String downloadUrl = tool.parseSync(); // 调用 parse() 函数
+List files = tool.parseFileListSync(); // 调用 parseFileList() 函数
+String fileUrl = tool.parseByIdSync(); // 调用 parseById() 函数
+```
+
+### 注意事项
+
+- JavaScript方法都是同步执行的,无需处理异步回调
+- 如果JavaScript方法抛出异常,Java同步方法会抛出相应的异常
+- 建议在JavaScript方法中添加适当的错误处理和日志记录
+
+## 导出方式
+
+支持三种导出方式:
+
+### 方式1:分别导出(推荐)
+
+```javascript
+function parse(shareLinkInfo, http, logger) { }
+function parseFileList(shareLinkInfo, http, logger) { }
+function parseById(shareLinkInfo, http, logger) { }
+
+exports.parse = parse;
+exports.parseFileList = parseFileList;
+exports.parseById = parseById;
+```
+
+### 方式2:直接挂载
+
+```javascript
+exports.parse = function(shareLinkInfo, http, logger) { };
+exports.parseFileList = function(shareLinkInfo, http, logger) { };
+exports.parseById = function(shareLinkInfo, http, logger) { };
+```
+
+### 方式3:module.exports
+
+```javascript
+function parse(shareLinkInfo, http, logger) { }
+function parseFileList(shareLinkInfo, http, logger) { }
+function parseById(shareLinkInfo, http, logger) { }
+
+module.exports = {
+ parse: parse,
+ parseFileList: parseFileList,
+ parseById: parseById
+};
+```
+
+## VSCode配置
+
+### 1. 安装JavaScript扩展
+
+安装 "JavaScript (ES6) code snippets" 扩展。
+
+### 2. 配置jsconfig.json
+
+在 `custom-parsers` 目录下创建 `jsconfig.json`:
+
+```json
+{
+ "compilerOptions": {
+ "checkJs": true,
+ "target": "ES5",
+ "lib": ["ES5"],
+ "allowJs": true,
+ "noEmit": true
+ },
+ "include": ["*.js", "types.d.ts"],
+ "exclude": ["node_modules"]
+}
+```
+
+### 3. 使用类型提示
+
+```javascript
+// 引用类型定义
+var types = require('./types');
+/** @typedef {types.ShareLinkInfo} ShareLinkInfo */
+/** @typedef {types.JsHttpClient} JsHttpClient */
+
+// 使用类型注解
+/**
+ * @param {ShareLinkInfo} shareLinkInfo
+ * @param {JsHttpClient} http
+ * @returns {string}
+ */
+function parse(shareLinkInfo, http, logger) {
+ // VSCode会提供代码补全和类型检查
+}
+```
+
+## 调试技巧
+
+### 1. 使用日志
+
+```javascript
+function parse(shareLinkInfo, http, logger) {
+ logger.info("开始解析: " + shareLinkInfo.getShareUrl());
+
+ var response = http.get(shareLinkInfo.getShareUrl());
+ logger.debug("响应状态: " + response.statusCode());
+ logger.debug("响应内容: " + response.body().substring(0, 100));
+
+ // 解析逻辑...
+}
+```
+
+### 2. 错误处理
+
+```javascript
+function parse(shareLinkInfo, http, logger) {
+ try {
+ var response = http.get(shareLinkInfo.getShareUrl());
+
+ if (!response.isSuccess()) {
+ throw new Error("HTTP请求失败: " + response.statusCode());
+ }
+
+ var data = response.json();
+ return data.downloadUrl;
+
+ } catch (e) {
+ logger.error("解析失败: " + e.message);
+ throw e; // 重新抛出异常
+ }
+}
+```
+
+### 3. 启用调试模式
+
+设置系统属性启用详细日志:
+
+```bash
+-Dnfd.js.debug=true
+```
+
+## 常见问题
+
+### Q: 如何获取分享密码?
+
+A: 使用 `shareLinkInfo.getSharePassword()` 方法。
+
+### Q: 如何处理需要登录的网盘?
+
+A: 使用 `http.putHeader()` 设置认证头,或使用 `http.sendForm()` 发送登录表单。
+
+### Q: 如何解析复杂的HTML?
+
+A: 使用正则表达式或字符串方法解析HTML内容。
+
+### Q: 如何处理异步请求?
+
+A: 当前版本使用同步API,所有HTTP请求都是同步的。
+
+### Q: 如何调试JavaScript代码?
+
+A: 使用 `logger.debug()` 输出调试信息,查看应用日志。
+
+## 示例脚本
+
+参考 `parser/src/main/resources/custom-parsers/example-demo.js` 文件,包含完整的示例实现。
+
+## 限制说明
+
+1. **JavaScript版本**: 仅支持ES5.1语法(Nashorn引擎限制)
+2. **同步执行**: 所有HTTP请求都是同步的
+3. **内存限制**: 长时间运行可能存在内存泄漏风险
+4. **安全限制**: 无法访问文件系统或执行系统命令
+
+## 更新日志
+
+- v1.0.0: 初始版本,支持基本的JavaScript解析器功能
diff --git a/parser/doc/README.md b/parser/doc/README.md
index 5f7bfc1..723a6c7 100644
--- a/parser/doc/README.md
+++ b/parser/doc/README.md
@@ -29,19 +29,25 @@ public class ParserQuickStart {
// .setShareLinkInfoPwd("1234") // 如有提取码可设置
.createTool();
- // 3) 异步 -> 同步等待,获取文件列表
- List files = tool.parseFileList()
- .toCompletionStage().toCompletableFuture().join();
+ // 3) 使用同步方法获取文件列表(推荐)
+ List files = tool.parseFileListSync();
for (FileInfo f : files) {
System.out.printf("%s\t%s\t%s\n",
f.getFileName(), f.getSizeStr(), f.getParserUrl());
}
- // 4) 原始解析输出(不同盘实现差异较大,仅供调试)
+ // 4) 使用同步方法获取原始解析输出(不同盘实现差异较大,仅供调试)
String raw = tool.parseSync();
System.out.println("raw: " + (raw == null ? "null" : raw.substring(0, Math.min(raw.length(), 200)) + "..."));
+
+ // 5) 使用同步方法根据文件ID获取下载链接(可选)
+ if (!files.isEmpty()) {
+ String fileId = files.get(0).getFileId();
+ String downloadUrl = tool.parseByIdSync();
+ System.out.println("文件下载链接: " + downloadUrl);
+ }
- // 5) 生成 parser 短链 path(可用于上层路由聚合显示)
+ // 6) 生成 parser 短链 path(可用于上层路由聚合显示)
String path = ParserCreate.fromShareUrl(shareUrl).genPathSuffix();
System.out.println("path suffix: /" + path);
@@ -56,13 +62,17 @@ IPanTool tool = ParserCreate.fromType("lz") // 对应 PanDomainTemplate.LZ
.shareKey("abcd12") // 必填:分享 key
.setShareLinkInfoPwd("1234") // 可选:提取码
.createTool();
-// 获取文件列表
-List files = tool.parseFileList().toCompletionStage().toCompletableFuture().join();
+// 获取文件列表(使用同步方法)
+List files = tool.parseFileListSync();
```
要点:
- 必须先 WebClientVertxInit.init(Vertx);若未显式初始化,内部将懒加载 Vertx.vertx(),建议显式注入以统一生命周期。
-- parseFileList 返回 Future>,可直接 join/await;parseSync 仅针对 parse() 的 String 结果。
+- 支持三种同步方法:
+ - `parseSync()`: 解析单个文件下载链接
+ - `parseFileListSync()`: 解析文件列表
+ - `parseByIdSync()`: 根据文件ID获取下载链接
+- 异步方法仍可用:parse()、parseFileList()、parseById() 返回 Future 对象
- 生成短链 path:ParserCreate.genPathSuffix()(用于页面/服务端聚合)。
---
diff --git a/parser/src/main/java/cn/qaiu/WebClientVertxInit.java b/parser/src/main/java/cn/qaiu/WebClientVertxInit.java
index 641abf3..61449ae 100644
--- a/parser/src/main/java/cn/qaiu/WebClientVertxInit.java
+++ b/parser/src/main/java/cn/qaiu/WebClientVertxInit.java
@@ -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;
}
diff --git a/parser/src/main/java/cn/qaiu/parser/CustomParserConfig.java b/parser/src/main/java/cn/qaiu/parser/CustomParserConfig.java
index 87aa7ae..5ee2d24 100644
--- a/parser/src/main/java/cn/qaiu/parser/CustomParserConfig.java
+++ b/parser/src/main/java/cn/qaiu/parser/CustomParserConfig.java
@@ -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 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 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 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 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 +
'}';
}
}
diff --git a/parser/src/main/java/cn/qaiu/parser/CustomParserRegistry.java b/parser/src/main/java/cn/qaiu/parser/CustomParserRegistry.java
index 679312f..b224540 100644
--- a/parser/src/main/java/cn/qaiu/parser/CustomParserRegistry.java
+++ b/parser/src/main/java/cn/qaiu/parser/CustomParserRegistry.java
@@ -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 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;
}
/**
diff --git a/parser/src/main/java/cn/qaiu/parser/IPanTool.java b/parser/src/main/java/cn/qaiu/parser/IPanTool.java
index cb8a676..72ca898 100644
--- a/parser/src/main/java/cn/qaiu/parser/IPanTool.java
+++ b/parser/src/main/java/cn/qaiu/parser/IPanTool.java
@@ -7,6 +7,11 @@ import io.vertx.core.Promise;
import java.util.List;
public interface IPanTool {
+
+ /**
+ * 解析文件
+ * @return 文件内容
+ */
Future parse();
default String parseSync() {
@@ -23,6 +28,10 @@ public interface IPanTool {
return promise.future();
}
+ default List 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();
+ }
}
diff --git a/parser/src/main/java/cn/qaiu/parser/JsHttpClient.java b/parser/src/main/java/cn/qaiu/parser/JsHttpClient.java
new file mode 100644
index 0000000..d78c962
--- /dev/null
+++ b/parser/src/main/java/cn/qaiu/parser/JsHttpClient.java
@@ -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 QAIU
+ * 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 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 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 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 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 mapData = (Map) 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 data) {
+ return executeRequest(() -> {
+ HttpRequest 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 request = client.postAbs("");
+ if (!headers.isEmpty()) {
+ request.putHeaders(headers);
+ }
+
+ return request.sendJson(data);
+ });
+ }
+
+ /**
+ * 执行HTTP请求(同步)
+ */
+ private JsHttpResponse executeRequest(RequestExecutor executor) {
+ try {
+ Promise> promise = Promise.promise();
+ Future> future = executor.execute();
+
+ future.onComplete(promise);
+
+ // 等待响应完成(最多30秒)
+ HttpResponse 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> execute();
+ }
+
+ /**
+ * JavaScript HTTP响应封装
+ */
+ public static class JsHttpResponse {
+
+ private final HttpResponse response;
+
+ public JsHttpResponse(HttpResponse 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 headers() {
+ MultiMap responseHeaders = response.headers();
+ Map 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 getOriginalResponse() {
+ return response;
+ }
+ }
+}
diff --git a/parser/src/main/java/cn/qaiu/parser/JsLogger.java b/parser/src/main/java/cn/qaiu/parser/JsLogger.java
new file mode 100644
index 0000000..969e6a8
--- /dev/null
+++ b/parser/src/main/java/cn/qaiu/parser/JsLogger.java
@@ -0,0 +1,144 @@
+package cn.qaiu.parser;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * JavaScript日志封装
+ * 为JavaScript提供日志功能
+ *
+ * @author QAIU
+ * 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;
+ }
+}
diff --git a/parser/src/main/java/cn/qaiu/parser/JsParserExecutor.java b/parser/src/main/java/cn/qaiu/parser/JsParserExecutor.java
new file mode 100644
index 0000000..1cc0e7c
--- /dev/null
+++ b/parser/src/main/java/cn/qaiu/parser/JsParserExecutor.java
@@ -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 QAIU
+ * 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 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 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> parseFileList() {
+ Promise> 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 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 parseById() {
+ Promise 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 convertToFileInfoList(ScriptObjectMirror resultMirror) {
+ List 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;
+ }
+ }
+}
diff --git a/parser/src/main/java/cn/qaiu/parser/JsScriptLoader.java b/parser/src/main/java/cn/qaiu/parser/JsScriptLoader.java
new file mode 100644
index 0000000..785a633
--- /dev/null
+++ b/parser/src/main/java/cn/qaiu/parser/JsScriptLoader.java
@@ -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 QAIU
+ * 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 loadAllScripts() {
+ List configs = new ArrayList<>();
+
+ // 1. 加载资源目录下的JS文件
+ try {
+ List resourceConfigs = loadFromResources();
+ configs.addAll(resourceConfigs);
+ log.info("从资源目录加载了 {} 个JavaScript解析器", resourceConfigs.size());
+ } catch (Exception e) {
+ log.warn("从资源目录加载JavaScript脚本失败", e);
+ }
+
+ // 2. 加载外部目录下的JS文件
+ try {
+ List 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 loadFromResources() {
+ List 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 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 loadFromExternal() {
+ List 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 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.");
+ }
+}
diff --git a/parser/src/main/java/cn/qaiu/parser/JsScriptMetadataParser.java b/parser/src/main/java/cn/qaiu/parser/JsScriptMetadataParser.java
new file mode 100644
index 0000000..87d08b3
--- /dev/null
+++ b/parser/src/main/java/cn/qaiu/parser/JsScriptMetadataParser.java
@@ -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 QAIU
+ * 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 metadata = extractMetadata(jsCode);
+
+ // 2. 验证必填字段
+ validateRequiredFields(metadata);
+
+ // 3. 构建CustomParserConfig
+ return buildConfig(metadata, jsCode);
+ }
+
+ /**
+ * 提取元数据
+ */
+ private static Map extractMetadata(String jsCode) {
+ Map 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 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("(?")) {
+ throw new IllegalArgumentException("@match 正则表达式必须包含命名捕获组 KEY,用于提取分享键");
+ }
+ }
+
+ /**
+ * 构建CustomParserConfig
+ */
+ private static CustomParserConfig buildConfig(Map 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 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 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 metadata = extractMetadata(jsCode);
+ return metadata.get("type");
+ } catch (Exception e) {
+ return null;
+ }
+ }
+}
diff --git a/parser/src/main/java/cn/qaiu/parser/JsShareLinkInfoWrapper.java b/parser/src/main/java/cn/qaiu/parser/JsShareLinkInfoWrapper.java
new file mode 100644
index 0000000..a5b6b1d
--- /dev/null
+++ b/parser/src/main/java/cn/qaiu/parser/JsShareLinkInfoWrapper.java
@@ -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 QAIU
+ * 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 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() + '\'' +
+ '}';
+ }
+}
diff --git a/parser/src/main/java/cn/qaiu/parser/ParserCreate.java b/parser/src/main/java/cn/qaiu/parser/ParserCreate.java
index 7cd14cf..454811c 100644
--- a/parser/src/main/java/cn/qaiu/parser/ParserCreate.java
+++ b/parser/src/main/java/cn/qaiu/parser/ParserCreate.java
@@ -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;
diff --git a/parser/src/main/resources/custom-parsers/README.md b/parser/src/main/resources/custom-parsers/README.md
new file mode 100644
index 0000000..7cb79f7
--- /dev/null
+++ b/parser/src/main/resources/custom-parsers/README.md
@@ -0,0 +1,238 @@
+# JavaScript解析器扩展使用指南
+
+## 概述
+
+本项目支持用户使用JavaScript编写自定义网盘解析器,提供灵活的扩展能力。JavaScript解析器运行在Nashorn引擎中,支持ES5.1语法。
+
+## 文件结构
+
+```
+custom-parsers/
+├── types.js # 类型定义文件(JSDoc注释)
+├── jsconfig.json # VSCode配置文件
+├── example-demo.js # 示例解析器
+└── README.md # 本说明文档
+```
+
+## 快速开始
+
+### 1. 创建解析器脚本
+
+在 `custom-parsers/` 目录下创建 `.js` 文件,使用以下格式:
+
+```javascript
+// ==UserScript==
+// @name 你的解析器名称
+// @type 解析器类型标识
+// @displayName 显示名称
+// @description 解析器描述
+// @match 匹配URL的正则表达式
+// @author 作者
+// @version 版本号
+// ==/UserScript==
+
+/**
+ * 解析单个文件下载链接
+ * @param {ShareLinkInfo} shareLinkInfo - 分享链接信息
+ * @param {JsHttpClient} http - HTTP客户端
+ * @param {JsLogger} logger - 日志记录器
+ * @returns {string} 下载链接
+ */
+function parse(shareLinkInfo, http, logger) {
+ // 你的解析逻辑
+ return "https://example.com/download/file.zip";
+}
+
+/**
+ * 解析文件列表(可选)
+ * @param {ShareLinkInfo} shareLinkInfo - 分享链接信息
+ * @param {JsHttpClient} http - HTTP客户端
+ * @param {JsLogger} logger - 日志记录器
+ * @returns {FileInfo[]} 文件信息列表
+ */
+function parseFileList(shareLinkInfo, http, logger) {
+ // 你的文件列表解析逻辑
+ return [];
+}
+
+/**
+ * 根据文件ID获取下载链接(可选)
+ * @param {ShareLinkInfo} shareLinkInfo - 分享链接信息
+ * @param {JsHttpClient} http - HTTP客户端
+ * @param {JsLogger} logger - 日志记录器
+ * @returns {string} 下载链接
+ */
+function parseById(shareLinkInfo, http, logger) {
+ // 你的按ID解析逻辑
+ return "https://example.com/download/" + fileId;
+}
+```
+
+### 2. 自动加载
+
+解析器会在应用启动时自动加载和注册。支持两种加载方式:
+
+#### 内置解析器(jar包内)
+- 位置:jar包内的 `custom-parsers/` 资源目录
+- 特点:随jar包一起发布,无需额外配置
+
+#### 外部解析器(用户自定义)
+- 默认位置:应用运行目录下的 `./custom-parsers/` 文件夹
+- 配置方式:
+ - **系统属性**:`-Dparser.custom-parsers.path=/path/to/your/parsers`
+ - **环境变量**:`PARSER_CUSTOM_PARSERS_PATH=/path/to/your/parsers`
+ - **默认路径**:`./custom-parsers/`(相对于应用运行目录)
+
+#### 配置示例
+
+**Maven项目中使用:**
+```bash
+# 方式1:系统属性
+mvn exec:java -Dexec.mainClass="your.MainClass" -Dparser.custom-parsers.path=./src/main/resources/custom-parsers
+
+# 方式2:环境变量
+export PARSER_CUSTOM_PARSERS_PATH=./src/main/resources/custom-parsers
+mvn exec:java -Dexec.mainClass="your.MainClass"
+```
+
+**jar包运行时:**
+```bash
+# 方式1:系统属性
+java -Dparser.custom-parsers.path=/path/to/your/parsers -jar your-app.jar
+
+# 方式2:环境变量
+export PARSER_CUSTOM_PARSERS_PATH=/path/to/your/parsers
+java -jar your-app.jar
+```
+
+## API参考
+
+### ShareLinkInfo
+
+分享链接信息对象:
+
+```javascript
+shareLinkInfo.getShareUrl() // 获取分享URL
+shareLinkInfo.getShareKey() // 获取分享Key
+shareLinkInfo.getSharePassword() // 获取分享密码
+shareLinkInfo.getType() // 获取网盘类型
+shareLinkInfo.getPanName() // 获取网盘名称
+shareLinkInfo.getOtherParam(key) // 获取其他参数
+```
+
+### JsHttpClient
+
+HTTP客户端对象:
+
+```javascript
+http.get(url) // GET请求
+http.post(url, data) // POST请求
+http.putHeader(name, value) // 设置请求头
+http.sendForm(data) // 发送表单数据
+http.sendJson(data) // 发送JSON数据
+```
+
+### JsHttpResponse
+
+HTTP响应对象:
+
+```javascript
+response.body() // 获取响应体(字符串)
+response.json() // 解析JSON响应
+response.statusCode() // 获取HTTP状态码
+response.header(name) // 获取响应头
+response.headers() // 获取所有响应头
+```
+
+### JsLogger
+
+日志记录器:
+
+```javascript
+logger.debug(message) // 调试日志
+logger.info(message) // 信息日志
+logger.warn(message) // 警告日志
+logger.error(message) // 错误日志
+```
+
+### FileInfo
+
+文件信息对象:
+
+```javascript
+{
+ fileName: "文件名",
+ fileId: "文件ID",
+ fileType: "file|folder",
+ size: 1024,
+ sizeStr: "1KB",
+ createTime: "2024-01-01",
+ updateTime: "2024-01-01",
+ createBy: "创建者",
+ downloadCount: 100,
+ fileIcon: "file",
+ panType: "网盘类型",
+ parserUrl: "解析URL",
+ previewUrl: "预览URL"
+}
+```
+
+## 开发提示
+
+### VSCode支持
+
+1. 确保安装了JavaScript扩展
+2. `types.js` 文件提供类型定义和代码补全
+3. `jsconfig.json` 配置了项目设置
+
+### 调试
+
+- 使用 `logger.debug()` 输出调试信息
+- 查看应用日志了解解析过程
+- 使用 `console.log()` 在Nashorn中输出信息
+
+### 错误处理
+
+```javascript
+try {
+ var response = http.get(url);
+ if (response.statusCode() !== 200) {
+ throw new Error("请求失败: " + response.statusCode());
+ }
+ return response.json();
+} catch (e) {
+ logger.error("解析失败: " + e.message);
+ throw e;
+}
+```
+
+## 示例
+
+参考 `example-demo.js` 文件,它展示了完整的解析器实现,包括:
+
+- 元数据配置
+- 三个核心方法的实现
+- 错误处理
+- 日志记录
+- 文件信息构建
+
+## 注意事项
+
+1. **ES5.1兼容**:只使用ES5.1语法,避免ES6+特性
+2. **同步API**:HTTP客户端提供同步接口,无需处理异步回调
+3. **全局函数**:解析器函数必须定义为全局函数,不能使用模块导出
+4. **错误处理**:始终包含适当的错误处理和日志记录
+5. **性能考虑**:避免在解析器中执行耗时操作
+
+## 故障排除
+
+### 常见问题
+
+1. **解析器未加载**:检查元数据格式是否正确
+2. **类型错误**:确保函数签名与接口匹配
+3. **HTTP请求失败**:检查URL和网络连接
+4. **JSON解析错误**:验证响应格式
+
+### 日志查看
+
+查看应用日志了解详细的执行过程和错误信息。
diff --git a/parser/src/main/resources/custom-parsers/baidu-photo.js b/parser/src/main/resources/custom-parsers/baidu-photo.js
new file mode 100644
index 0000000..029f1c8
--- /dev/null
+++ b/parser/src/main/resources/custom-parsers/baidu-photo.js
@@ -0,0 +1,400 @@
+// ==UserScript==
+// @name 一刻相册解析器
+// @type baidu_photo
+// @displayName 百度一刻相册(JS)
+// @description 解析百度一刻相册分享链接,获取文件列表和下载链接
+// @match https?://photo\.baidu\.com/photo/(web/share\?inviteCode=|wap/albumShare\?shareId=)(?\w+)
+// @author qaiu
+// @version 1.0.0
+// ==/UserScript==
+
+/**
+ * API端点配置
+ */
+var API_CONFIG = {
+ // 文件夹分享:通过pcode获取share_id
+ QUERY_PCODE: "https://photo.baidu.com/youai/album/v1/querypcode",
+ // 文件列表:获取文件列表
+ LIST_FILES: "https://photo.baidu.com/youai/share/v2/list",
+
+ // 请求参数
+ CLIENT_TYPE: "70",
+ LIMIT: "100"
+};
+
+/**
+ * 设置标准请求头
+ * @param {JsHttpClient} http - HTTP客户端
+ * @param {string} referer - Referer URL
+ */
+function setStandardHeaders(http, referer) {
+ var headers = {
+ "Accept": "application/json, text/plain, */*",
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
+ "Cache-Control": "no-cache",
+ "Connection": "keep-alive",
+ "Content-Type": "application/x-www-form-urlencoded",
+ "DNT": "1",
+ "Origin": "https://photo.baidu.com",
+ "Pragma": "no-cache",
+ "Referer": referer,
+ "Sec-Fetch-Dest": "empty",
+ "Sec-Fetch-Mode": "cors",
+ "Sec-Fetch-Site": "same-origin",
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0",
+ "X-Requested-With": "XMLHttpRequest",
+ "sec-ch-ua": "\"Microsoft Edge\";v=\"141\", \"Not?A_Brand\";v=\"8\", \"Chromium\";v=\"141\"",
+ "sec-ch-ua-mobile": "?0",
+ "sec-ch-ua-platform": "\"macOS\""
+ };
+
+ for (var key in headers) {
+ http.putHeader(key, headers[key]);
+ }
+}
+
+/**
+ * 获取分享ID
+ * @param {string} shareKey - 分享键
+ * @param {boolean} isFileShare - 是否为文件分享
+ * @param {JsHttpClient} http - HTTP客户端
+ * @param {JsLogger} logger - 日志记录器
+ * @returns {string} 分享ID
+ */
+function getShareId(shareKey, isFileShare, http, logger) {
+ if (isFileShare) {
+ logger.info("文件分享模式,直接使用shareId: " + shareKey);
+ return shareKey;
+ }
+
+ // 文件夹分享:通过pcode获取share_id
+ var queryUrl = API_CONFIG.QUERY_PCODE + "?clienttype=" + API_CONFIG.CLIENT_TYPE + "&pcode=" + shareKey + "&web=1";
+ logger.debug("文件夹分享查询URL: " + queryUrl);
+
+ setStandardHeaders(http, "https://photo.baidu.com/photo/web/share?inviteCode=" + shareKey);
+
+ var queryResponse = http.get(queryUrl);
+ if (queryResponse.statusCode() !== 200) {
+ throw new Error("获取分享ID失败,状态码: " + queryResponse.statusCode());
+ }
+
+ var queryData = queryResponse.json();
+ logger.debug("查询响应: " + JSON.stringify(queryData));
+
+ if (queryData.errno !== undefined && queryData.errno !== 0) {
+ throw new Error("API返回错误,errno: " + queryData.errno);
+ }
+
+ var shareId = queryData.pdata && queryData.pdata.share_id;
+ if (!shareId) {
+ throw new Error("未找到share_id");
+ }
+
+ logger.info("获取到分享ID: " + shareId);
+ return shareId;
+}
+
+/**
+ * 获取文件列表
+ * @param {string} shareId - 分享ID
+ * @param {string} shareKey - 分享键
+ * @param {boolean} isFileShare - 是否为文件分享
+ * @param {JsHttpClient} http - HTTP客户端
+ * @param {JsLogger} logger - 日志记录器
+ * @returns {Array} 文件列表
+ */
+function getFileList(shareId, shareKey, isFileShare, http, logger) {
+ var listUrl = API_CONFIG.LIST_FILES + "?clienttype=" + API_CONFIG.CLIENT_TYPE + "&share_id=" + shareId + "&limit=" + API_CONFIG.LIMIT;
+ logger.debug("获取文件列表 URL: " + listUrl);
+
+ var referer = isFileShare ?
+ "https://photo.baidu.com/photo/wap/albumShare?shareId=" + shareKey :
+ "https://photo.baidu.com/photo/web/share?inviteCode=" + shareKey;
+
+ setStandardHeaders(http, referer);
+
+ var listResponse = http.get(listUrl);
+ if (listResponse.statusCode() !== 200) {
+ throw new Error("获取文件列表失败,状态码: " + listResponse.statusCode());
+ }
+
+ var listData = listResponse.json();
+ logger.debug("文件列表响应: " + JSON.stringify(listData));
+
+ if (listData.errno !== undefined && listData.errno !== 0) {
+ throw new Error("获取文件列表API返回错误,errno: " + listData.errno);
+ }
+
+ var fileList = listData.list;
+ if (!fileList || fileList.length === 0) {
+ logger.warn("文件列表为空");
+ return [];
+ }
+
+ logger.info("获取到文件列表,共 " + fileList.length + " 个文件");
+ return fileList;
+}
+
+/**
+ * 解析单个文件下载链接
+ * @param {ShareLinkInfo} shareLinkInfo - 分享链接信息对象
+ * @param {JsHttpClient} http - HTTP客户端实例
+ * @param {JsLogger} logger - 日志记录器实例
+ * @returns {string} 下载链接
+ */
+function parse(shareLinkInfo, http, logger) {
+ logger.info("===== 开始执行 parse 方法 =====");
+
+ var shareKey = shareLinkInfo.getShareKey();
+ logger.info("分享Key: " + shareKey);
+
+ try {
+ // 判断分享类型
+ // 如果shareKey是纯数字且长度较长,很可能是文件分享的share_id
+ // 如果shareKey包含字母,很可能是文件夹分享的inviteCode
+ var isFileShare = /^\d{10,}$/.test(shareKey);
+ logger.info("分享类型: " + (isFileShare ? "文件分享" : "文件夹分享"));
+
+ // 获取分享ID
+ var shareId = getShareId(shareKey, isFileShare, http, logger);
+
+ // 获取文件列表
+ var fileList = getFileList(shareId, shareKey, isFileShare, http, logger);
+
+ if (fileList.length === 0) {
+ throw new Error("文件列表为空");
+ }
+
+ // 返回第一个文件的下载链接
+ var firstFile = fileList[0];
+ var downloadUrl = firstFile.dlink;
+
+ if (!downloadUrl) {
+ throw new Error("未找到下载链接");
+ }
+
+ // 获取真实的下载链接(处理302重定向)
+ var realDownloadUrl = getRealDownloadUrl(downloadUrl, http, logger);
+
+ logger.info("解析成功,返回URL: " + realDownloadUrl);
+ return realDownloadUrl;
+
+ } catch (e) {
+ logger.error("解析失败: " + e.message);
+ throw new Error("解析失败: " + e.message);
+ }
+}
+
+/**
+ * 解析文件列表
+ * @param {ShareLinkInfo} shareLinkInfo - 分享链接信息对象
+ * @param {JsHttpClient} http - HTTP客户端实例
+ * @param {JsLogger} logger - 日志记录器实例
+ * @returns {FileInfo[]} 文件信息列表
+ */
+function parseFileList(shareLinkInfo, http, logger) {
+ logger.info("===== 开始执行 parseFileList 方法 =====");
+
+ var shareKey = shareLinkInfo.getShareKey();
+ logger.info("分享Key: " + shareKey);
+
+ try {
+ // 判断分享类型
+ // 如果shareKey是纯数字且长度较长,很可能是文件分享的share_id
+ // 如果shareKey包含字母,很可能是文件夹分享的inviteCode
+ var isFileShare = /^\d{10,}$/.test(shareKey);
+ logger.info("分享类型: " + (isFileShare ? "文件分享" : "文件夹分享"));
+
+ // 获取分享ID
+ var shareId = getShareId(shareKey, isFileShare, http, logger);
+
+ // 获取文件列表
+ var fileList = getFileList(shareId, shareKey, isFileShare, http, logger);
+
+ if (fileList.length === 0) {
+ logger.warn("文件列表为空");
+ return [];
+ }
+
+ logger.info("解析文件列表成功,共 " + fileList.length + " 项");
+
+ var result = [];
+ for (var i = 0; i < fileList.length; i++) {
+ var file = fileList[i];
+
+ /** @type {FileInfo} */
+ var fileInfo = {
+ fileName: extractFileName(file.path) || ("文件_" + (i + 1)),
+ fileId: String(file.fsid),
+ fileType: "file",
+ size: file.size || 0,
+ sizeStr: formatBytes(file.size || 0),
+ createTime: formatTimestamp(file.ctime),
+ updateTime: formatTimestamp(file.mtime),
+ createBy: "",
+ downloadCount: 0,
+ fileIcon: "file",
+ panType: "baidu_photo",
+ parserUrl: "",
+ previewUrl: ""
+ };
+
+ // 设置下载链接
+ if (file.dlink) {
+ fileInfo.parserUrl = file.dlink;
+ }
+
+ // 设置预览链接(取第一个缩略图)
+ if (file.thumburl && file.thumburl.length > 0) {
+ fileInfo.previewUrl = file.thumburl[0];
+ }
+
+ result.push(fileInfo);
+ }
+
+ logger.info("文件列表解析成功,共 " + result.length + " 个文件");
+ return result;
+
+ } catch (e) {
+ logger.error("解析文件列表失败: " + e.message);
+ throw new Error("解析文件列表失败: " + e.message);
+ }
+}
+
+/**
+ * 根据文件ID获取下载链接
+ * @param {ShareLinkInfo} shareLinkInfo - 分享链接信息对象
+ * @param {JsHttpClient} http - HTTP客户端实例
+ * @param {JsLogger} logger - 日志记录器实例
+ * @returns {string} 下载链接
+ */
+function parseById(shareLinkInfo, http, logger) {
+ logger.info("===== 开始执行 parseById 方法 =====");
+
+ var shareKey = shareLinkInfo.getShareKey();
+ var otherParam = shareLinkInfo.getOtherParam("paramJson");
+ var fileId = otherParam ? otherParam.fileId || otherParam.id : null;
+
+ logger.info("分享Key: " + shareKey);
+ logger.info("文件ID: " + fileId);
+
+ if (!fileId) {
+ throw new Error("未提供文件ID");
+ }
+
+ try {
+ // 判断分享类型
+ // 如果shareKey是纯数字且长度较长,很可能是文件分享的share_id
+ // 如果shareKey包含字母,很可能是文件夹分享的inviteCode
+ var isFileShare = /^\d{10,}$/.test(shareKey);
+ logger.info("分享类型: " + (isFileShare ? "文件分享" : "文件夹分享"));
+
+ // 获取分享ID
+ var shareId = getShareId(shareKey, isFileShare, http, logger);
+
+ // 获取文件列表
+ var fileList = getFileList(shareId, shareKey, isFileShare, http, logger);
+
+ if (fileList.length === 0) {
+ throw new Error("文件列表为空");
+ }
+
+ // 查找指定ID的文件
+ var targetFile = null;
+ for (var i = 0; i < fileList.length; i++) {
+ var file = fileList[i];
+ if (String(file.fsid) == fileId || String(i) == fileId) {
+ targetFile = file;
+ break;
+ }
+ }
+
+ if (!targetFile) {
+ throw new Error("未找到指定ID的文件: " + fileId);
+ }
+
+ var downloadUrl = targetFile.dlink;
+ if (!downloadUrl) {
+ throw new Error("文件无下载链接");
+ }
+
+ // 获取真实的下载链接(处理302重定向)
+ var realDownloadUrl = getRealDownloadUrl(downloadUrl, http, logger);
+
+ logger.info("根据ID解析成功: " + realDownloadUrl);
+ return realDownloadUrl;
+
+ } catch (e) {
+ logger.error("根据ID解析失败: " + e.message);
+ throw new Error("根据ID解析失败: " + e.message);
+ }
+}
+
+/**
+ * 格式化字节大小
+ * @param {number} bytes
+ * @returns {string}
+ */
+function formatBytes(bytes) {
+ if (bytes === 0) return "0 B";
+ var k = 1024;
+ var sizes = ["B", "KB", "MB", "GB", "TB"];
+ var i = Math.floor(Math.log(bytes) / Math.log(k));
+ return (bytes / Math.pow(k, i)).toFixed(2) + " " + sizes[i];
+}
+
+/**
+ * 从路径中提取文件名
+ * @param {string} path
+ * @returns {string}
+ */
+function extractFileName(path) {
+ if (!path) return "";
+ var parts = path.split("/");
+ return parts[parts.length - 1] || "";
+}
+
+/**
+ * 获取真实的下载链接(处理302重定向)
+ * @param {string} downloadUrl - 原始下载链接
+ * @param {JsHttpClient} http - HTTP客户端
+ * @param {JsLogger} logger - 日志记录器
+ * @returns {string} 真实的下载链接
+ */
+function getRealDownloadUrl(downloadUrl, http, logger) {
+ try {
+ logger.info("获取真实下载链接: " + downloadUrl);
+
+ // 使用不跟随重定向的方法获取Location头
+ var headResponse = http.getNoRedirect(downloadUrl);
+
+ if (headResponse.statusCode() >= 300 && headResponse.statusCode() < 400) {
+ // 处理重定向
+ var location = headResponse.header("Location");
+ if (location) {
+ logger.info("获取到重定向链接: " + location);
+ return location;
+ }
+ }
+
+ // 如果没有重定向或无法获取Location,返回原链接
+ logger.debug("下载链接无需重定向或无法获取重定向信息");
+ return downloadUrl;
+
+ } catch (e) {
+ logger.error("获取真实下载链接失败: " + e.message);
+ // 如果获取失败,返回原链接
+ return downloadUrl;
+ }
+}
+
+/**
+ * 格式化时间戳
+ * @param {number} timestamp
+ * @returns {string}
+ */
+function formatTimestamp(timestamp) {
+ if (!timestamp) return "";
+ var date = new Date(timestamp * 1000);
+ return date.toISOString().replace("T", " ").substring(0, 19);
+}
diff --git a/parser/src/main/resources/custom-parsers/example-demo.js b/parser/src/main/resources/custom-parsers/example-demo.js
new file mode 100644
index 0000000..76483bf
--- /dev/null
+++ b/parser/src/main/resources/custom-parsers/example-demo.js
@@ -0,0 +1,170 @@
+// ==UserScript==
+// @name 演示解析器
+// @type demo_js
+// @displayName 演示网盘(JS)
+// @description 演示JavaScript解析器的完整功能(使用JSONPlaceholder测试API)
+// @match https?://demo\.example\.com/s/(?\w+)
+// @author qaiu
+// @version 1.0.0
+// ==/UserScript==
+
+// 注意:require调用仅用于IDE类型提示,运行时会被忽略
+// var types = require('./types');
+
+/**
+ * 解析单个文件下载链接
+ * 使用 https://jsonplaceholder.typicode.com/posts/1 作为测试
+ * @param {ShareLinkInfo} shareLinkInfo - 分享链接信息对象
+ * @param {JsHttpClient} http - HTTP客户端
+ * @param {JsLogger} logger - 日志对象
+ * @returns {string} 下载链接URL
+ */
+function parse(shareLinkInfo, http, logger) {
+ logger.info("===== 开始执行 parse 方法 =====");
+
+ var shareKey = shareLinkInfo.getShareKey();
+ var password = shareLinkInfo.getSharePassword();
+
+ logger.info("分享Key: " + shareKey);
+ logger.info("分享密码: " + (password || "无"));
+
+ // 使用JSONPlaceholder测试API
+ var apiUrl = "https://jsonplaceholder.typicode.com/posts/" + (shareKey || "1");
+ logger.debug("请求URL: " + apiUrl);
+
+ try {
+ var response = http.get(apiUrl);
+ logger.debug("HTTP状态码: " + response.statusCode());
+
+ var data = response.json();
+ logger.debug("响应数据: " + JSON.stringify(data));
+
+ // 模拟返回下载链接(实际是返回post的标题作为"下载链接")
+ var downloadUrl = "https://cdn.example.com/file/" + data.id + "/" + data.title;
+ logger.info("解析成功,返回URL: " + downloadUrl);
+
+ return downloadUrl;
+ } catch (e) {
+ logger.error("解析失败: " + e.message);
+ throw new Error("解析失败: " + e.message);
+ }
+}
+
+/**
+ * 解析文件列表
+ * 使用 https://jsonplaceholder.typicode.com/users 作为测试
+ * @param {ShareLinkInfo} shareLinkInfo - 分享链接信息对象
+ * @param {JsHttpClient} http - HTTP客户端
+ * @param {JsLogger} logger - 日志对象
+ * @returns {FileInfo[]} 文件列表数组
+ */
+function parseFileList(shareLinkInfo, http, logger) {
+ logger.info("===== 开始执行 parseFileList 方法 =====");
+
+ var dirId = shareLinkInfo.getOtherParam("dirId") || "1";
+ logger.info("目录ID: " + dirId);
+
+ // 使用JSONPlaceholder的users API模拟文件列表
+ var apiUrl = "https://jsonplaceholder.typicode.com/users";
+ logger.debug("请求URL: " + apiUrl);
+
+ try {
+ var response = http.get(apiUrl);
+ var users = response.json();
+
+ var fileList = [];
+ for (var i = 0; i < users.length; i++) {
+ var user = users[i];
+
+ // 模拟文件和目录
+ var isFolder = (user.id % 3 === 0); // 每3个作为目录
+ var fileSize = isFolder ? 0 : user.id * 1024 * 1024; // 模拟文件大小
+
+ /** @type {FileInfo} */
+ var fileInfo = {
+ fileName: user.name + (isFolder ? " [目录]" : ".txt"),
+ fileId: user.id.toString(),
+ fileType: isFolder ? "folder" : "file",
+ size: fileSize,
+ sizeStr: formatFileSize(fileSize),
+ createTime: "2024-01-01",
+ updateTime: "2024-01-01",
+ createBy: user.username,
+ downloadCount: Math.floor(Math.random() * 1000),
+ fileIcon: isFolder ? "folder" : "file",
+ panType: "demo_js",
+ parserUrl: "",
+ previewUrl: ""
+ };
+
+ // 如果是目录,设置解析URL
+ if (isFolder) {
+ fileInfo.parserUrl = "/v2/getFileList?url=demo&dirId=" + user.id;
+ } else {
+ // 如果是文件,设置下载URL
+ fileInfo.parserUrl = "/v2/redirectUrl/demo_js/" + user.id;
+ }
+
+ fileList.push(fileInfo);
+ }
+
+ logger.info("解析文件列表成功,共 " + fileList.length + " 项");
+ return fileList;
+
+ } catch (e) {
+ logger.error("解析文件列表失败: " + e.message);
+ throw new Error("解析文件列表失败: " + e.message);
+ }
+}
+
+/**
+ * 根据文件ID获取下载链接
+ * 使用 https://jsonplaceholder.typicode.com/todos/:id 作为测试
+ * @param {ShareLinkInfo} shareLinkInfo - 分享链接信息对象
+ * @param {JsHttpClient} http - HTTP客户端
+ * @param {JsLogger} logger - 日志对象
+ * @returns {string} 下载链接URL
+ */
+function parseById(shareLinkInfo, http, logger) {
+ logger.info("===== 开始执行 parseById 方法 =====");
+
+ var paramJson = shareLinkInfo.getOtherParam("paramJson");
+ if (!paramJson) {
+ throw new Error("缺少paramJson参数");
+ }
+
+ var fileId = paramJson.fileId || paramJson.id || "1";
+ logger.info("文件ID: " + fileId);
+
+ // 使用JSONPlaceholder的todos API
+ var apiUrl = "https://jsonplaceholder.typicode.com/todos/" + fileId;
+ logger.debug("请求URL: " + apiUrl);
+
+ try {
+ var response = http.get(apiUrl);
+ var todo = response.json();
+
+ // 模拟返回下载链接
+ var downloadUrl = "https://cdn.example.com/download/" + todo.id + "/" + todo.title + ".zip";
+ logger.info("根据ID解析成功: " + downloadUrl);
+
+ return downloadUrl;
+
+ } catch (e) {
+ logger.error("根据ID解析失败: " + e.message);
+ throw new Error("根据ID解析失败: " + e.message);
+ }
+}
+
+/**
+ * 辅助函数:格式化文件大小
+ * @param {number} bytes - 字节数
+ * @returns {string} 格式化后的大小
+ */
+function formatFileSize(bytes) {
+ if (bytes === 0) return "0B";
+ var k = 1024;
+ var sizes = ["B", "KB", "MB", "GB", "TB"];
+ var i = Math.floor(Math.log(bytes) / Math.log(k));
+ return (bytes / Math.pow(k, i)).toFixed(2) + sizes[i];
+}
diff --git a/parser/src/main/resources/custom-parsers/jsconfig.json b/parser/src/main/resources/custom-parsers/jsconfig.json
new file mode 100644
index 0000000..6449436
--- /dev/null
+++ b/parser/src/main/resources/custom-parsers/jsconfig.json
@@ -0,0 +1,19 @@
+{
+ "compilerOptions": {
+ "checkJs": true,
+ "target": "ES5",
+ "lib": ["ES5"],
+ "allowJs": true,
+ "noEmit": true,
+ "moduleResolution": "node",
+ "allowSyntheticDefaultImports": true,
+ "esModuleInterop": true
+ },
+ "include": [
+ "*.js",
+ "types.js"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}
diff --git a/parser/src/main/resources/custom-parsers/types.js b/parser/src/main/resources/custom-parsers/types.js
new file mode 100644
index 0000000..8c32c12
--- /dev/null
+++ b/parser/src/main/resources/custom-parsers/types.js
@@ -0,0 +1,68 @@
+/**
+ * JavaScript解析器类型定义文件
+ * 使用JSDoc注释提供代码补全和类型提示
+ * 兼容ES5.1和Nashorn引擎
+ */
+
+// 全局类型定义,使用JSDoc注释
+// 这些类型定义将在VSCode中提供代码补全和类型检查
+
+/**
+ * @typedef {Object} ShareLinkInfo
+ * @property {function(): string} getShareUrl - 获取分享URL
+ * @property {function(): string} getShareKey - 获取分享Key
+ * @property {function(): string} getSharePassword - 获取分享密码
+ * @property {function(): string} getType - 获取网盘类型
+ * @property {function(): string} getPanName - 获取网盘名称
+ * @property {function(string): any} getOtherParam - 获取其他参数
+ */
+
+/**
+ * @typedef {Object} JsHttpResponse
+ * @property {function(): string} body - 获取响应体(字符串)
+ * @property {function(): any} json - 解析JSON响应
+ * @property {function(): number} statusCode - 获取HTTP状态码
+ * @property {function(string): string|null} header - 获取响应头
+ * @property {function(): Object} headers - 获取所有响应头
+ */
+
+/**
+ * @typedef {Object} JsHttpClient
+ * @property {function(string): JsHttpResponse} get - 发起GET请求
+ * @property {function(string, any=): JsHttpResponse} post - 发起POST请求
+ * @property {function(string, string): JsHttpClient} putHeader - 设置请求头
+ * @property {function(Object): JsHttpResponse} sendForm - 发送表单数据
+ * @property {function(any): JsHttpResponse} sendJson - 发送JSON数据
+ */
+
+/**
+ * @typedef {Object} JsLogger
+ * @property {function(string): void} debug - 调试日志
+ * @property {function(string): void} info - 信息日志
+ * @property {function(string): void} warn - 警告日志
+ * @property {function(string): void} error - 错误日志
+ */
+
+/**
+ * @typedef {Object} FileInfo
+ * @property {string} fileName - 文件名
+ * @property {string} fileId - 文件ID
+ * @property {string} fileType - 文件类型: "file" | "folder"
+ * @property {number} size - 文件大小(字节)
+ * @property {string} sizeStr - 文件大小(可读格式)
+ * @property {string} createTime - 创建时间
+ * @property {string} updateTime - 更新时间
+ * @property {string} createBy - 创建者
+ * @property {number} downloadCount - 下载次数
+ * @property {string} fileIcon - 文件图标
+ * @property {string} panType - 网盘类型
+ * @property {string} parserUrl - 解析URL
+ * @property {string} previewUrl - 预览URL
+ */
+
+/**
+ * @typedef {Object} ParserExports
+ * @property {function(ShareLinkInfo, JsHttpClient, JsLogger): string} parse - 解析单个文件下载链接
+ * @property {function(ShareLinkInfo, JsHttpClient, JsLogger): FileInfo[]} parseFileList - 解析文件列表
+ * @property {function(ShareLinkInfo, JsHttpClient, JsLogger): string} parseById - 根据文件ID获取下载链接
+ */
diff --git a/parser/src/test/java/cn/qaiu/parser/BaiduPhotoParserTest.java b/parser/src/test/java/cn/qaiu/parser/BaiduPhotoParserTest.java
new file mode 100644
index 0000000..2c49509
--- /dev/null
+++ b/parser/src/test/java/cn/qaiu/parser/BaiduPhotoParserTest.java
@@ -0,0 +1,204 @@
+package cn.qaiu.parser;
+
+import cn.qaiu.entity.FileInfo;
+import cn.qaiu.entity.ShareLinkInfo;
+import cn.qaiu.WebClientVertxInit;
+import io.vertx.core.Vertx;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 百度一刻相册解析器测试
+ *
+ * @author QAIU
+ * Create at 2025/10/21
+ */
+public class BaiduPhotoParserTest {
+
+ @Test
+ public void testBaiduPhotoParserRegistration() {
+ // 清理注册表
+ CustomParserRegistry.clear();
+
+ // 初始化Vertx
+ Vertx vertx = Vertx.vertx();
+ WebClientVertxInit.init(vertx);
+
+ // 检查是否加载了百度相册解析器
+ CustomParserConfig config = CustomParserRegistry.get("baidu_photo");
+ assert config != null : "百度相册解析器未加载";
+ assert config.isJsParser() : "解析器类型错误";
+ assert "百度一刻相册(JS)".equals(config.getDisplayName()) : "显示名称错误";
+
+ System.out.println("✓ 百度一刻相册解析器注册测试通过");
+ }
+
+ @Test
+ public void testBaiduPhotoFileShareExecution() {
+ // 清理注册表
+ CustomParserRegistry.clear();
+
+ // 初始化Vertx
+ Vertx vertx = Vertx.vertx();
+ WebClientVertxInit.init(vertx);
+
+ try {
+ // 创建解析器 - 测试文件分享链接
+ IPanTool tool = ParserCreate.fromType("baidu_photo")
+ .shareKey("19012978577097490") // 文件分享ID
+ .setShareLinkInfoPwd("")
+ .createTool();
+
+ // 测试parse方法
+ String downloadUrl = tool.parseSync();
+ assert downloadUrl != null && downloadUrl.contains("d.pcs.baidu.com") :
+ "parse方法返回结果错误: " + downloadUrl;
+
+ System.out.println("✓ 百度一刻相册文件分享解析测试通过");
+ System.out.println(" 下载链接: " + downloadUrl);
+
+ } catch (Exception e) {
+ System.err.println("✗ 百度一刻相册文件分享解析测试失败: " + e.getMessage());
+ e.printStackTrace();
+ // 注意:这个测试可能会失败,因为需要真实的网络请求和可能的认证
+ // 这里主要是验证解析器逻辑是否正确
+ }
+ }
+
+ @Test
+ public void testBaiduPhotoFolderShareExecution() {
+ // 清理注册表
+ CustomParserRegistry.clear();
+
+ // 初始化Vertx
+ Vertx vertx = Vertx.vertx();
+ WebClientVertxInit.init(vertx);
+
+ try {
+ // 创建解析器 - 测试文件夹分享链接
+ IPanTool tool = ParserCreate.fromType("baidu_photo")
+ .shareKey("abc123def456") // 文件夹分享的inviteCode
+ .setShareLinkInfoPwd("")
+ .createTool();
+
+ // 测试parse方法
+ String downloadUrl = tool.parseSync();
+ assert downloadUrl != null && downloadUrl.contains("d.pcs.baidu.com") :
+ "parse方法返回结果错误: " + downloadUrl;
+
+ System.out.println("✓ 百度一刻相册文件夹分享解析测试通过");
+ System.out.println(" 下载链接: " + downloadUrl);
+
+ } catch (Exception e) {
+ System.err.println("✗ 百度一刻相册文件夹分享解析测试失败: " + e.getMessage());
+ e.printStackTrace();
+ // 注意:这个测试可能会失败,因为需要真实的网络请求和可能的认证
+ // 这里主要是验证解析器逻辑是否正确
+ }
+ }
+
+ @Test
+ public void testBaiduPhotoParserFileList() {
+ // 清理注册表
+ CustomParserRegistry.clear();
+
+ // 初始化Vertx
+ Vertx vertx = Vertx.vertx();
+ WebClientVertxInit.init(vertx);
+
+ try {
+ IPanTool tool = ParserCreate.fromType("baidu_photo")
+ // 分享key PPgOEodBVE
+ .shareKey("PPgOEodBVE")
+ .setShareLinkInfoPwd("")
+ .createTool();
+
+ // 测试parseFileList方法
+ List fileList = tool.parseFileListSync();
+ assert fileList != null : "parseFileList方法返回结果错误";
+
+ System.out.println("✓ 百度一刻相册文件列表解析测试通过");
+ System.out.println(" 文件数量: " + fileList.size());
+
+ // 如果有文件,检查第一个文件
+ if (!fileList.isEmpty()) {
+ FileInfo firstFile = fileList.get(0);
+ assert firstFile.getFileName() != null : "文件名不能为空";
+ assert firstFile.getFileId() != null : "文件ID不能为空";
+ System.out.println(" 第一个文件: " + firstFile.getFileName());
+ System.out.println(" 下载链接: " + firstFile.getParserUrl());
+ System.out.println(" 预览链接: " + firstFile.getPreviewUrl());
+
+ // 输出所有文件的详细信息
+ System.out.println("\n=== 完整文件列表 ===");
+ for (int i = 0; i < fileList.size(); i++) {
+ FileInfo file = fileList.get(i);
+ System.out.println("\n--- 文件 " + (i + 1) + " ---");
+ System.out.println(" 文件名: " + file.getFileName());
+ System.out.println(" 文件ID: " + file.getFileId());
+ System.out.println(" 文件类型: " + file.getFileType());
+ System.out.println(" 文件大小: " + file.getSize() + " bytes (" + file.getSizeStr() + ")");
+ System.out.println(" 创建时间: " + file.getCreateTime());
+ System.out.println(" 更新时间: " + file.getUpdateTime());
+ System.out.println(" 下载链接: " + file.getParserUrl());
+ System.out.println(" 预览链接: " + file.getPreviewUrl());
+ System.out.println(" 网盘类型: " + file.getPanType());
+ }
+ } else {
+ System.out.println(" 文件列表为空(可能是网络问题或认证问题)");
+ }
+
+ } catch (Exception e) {
+ System.err.println("✗ 百度一刻相册文件列表解析测试失败: " + e.getMessage());
+ e.printStackTrace();
+ // 注意:这个测试可能会失败,因为需要真实的网络请求和可能的认证
+ }
+ }
+
+ @Test
+ public void testBaiduPhotoParserById() {
+ // 清理注册表
+ CustomParserRegistry.clear();
+
+ // 初始化Vertx
+ Vertx vertx = Vertx.vertx();
+ WebClientVertxInit.init(vertx);
+
+ try {
+ // 创建ShareLinkInfo
+ Map otherParam = new HashMap<>();
+ Map paramJson = new HashMap<>();
+ paramJson.put("fileId", "0"); // 测试第一个文件
+ paramJson.put("id", "0");
+ otherParam.put("paramJson", paramJson);
+
+ // 创建解析器 - 使用新的文件分享链接
+ IPanTool tool = ParserCreate.fromType("baidu_photo")
+ .shareKey("19012978577097490")
+ .setShareLinkInfoPwd("")
+ .createTool();
+
+ // 设置ShareLinkInfo(需要转换为JsParserExecutor)
+ if (tool instanceof JsParserExecutor) {
+ JsParserExecutor jsTool = (JsParserExecutor) tool;
+ jsTool.getShareLinkInfo().setOtherParam(otherParam);
+ }
+
+ // 测试parseById方法
+ String downloadUrl = tool.parseById().toCompletionStage().toCompletableFuture().join();
+ assert downloadUrl != null && downloadUrl.contains("d.pcs.baidu.com") :
+ "parseById方法返回结果错误: " + downloadUrl;
+
+ System.out.println("✓ 百度一刻相册按ID解析测试通过");
+ System.out.println(" 下载链接: " + downloadUrl);
+
+ } catch (Exception e) {
+ System.err.println("✗ 百度一刻相册按ID解析测试失败: " + e.getMessage());
+ e.printStackTrace();
+ // 注意:这个测试可能会失败,因为需要真实的网络请求和可能的认证
+ }
+ }
+}
diff --git a/parser/src/test/java/cn/qaiu/parser/JsParserTest.java b/parser/src/test/java/cn/qaiu/parser/JsParserTest.java
new file mode 100644
index 0000000..163bb2b
--- /dev/null
+++ b/parser/src/test/java/cn/qaiu/parser/JsParserTest.java
@@ -0,0 +1,161 @@
+package cn.qaiu.parser;
+
+import cn.qaiu.entity.FileInfo;
+import cn.qaiu.entity.ShareLinkInfo;
+import cn.qaiu.WebClientVertxInit;
+import io.vertx.core.Vertx;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * JavaScript解析器测试
+ *
+ * @author QAIU
+ * Create at 2025/10/17
+ */
+public class JsParserTest {
+
+ @Test
+ public void testJsParserRegistration() {
+ // 清理注册表
+ CustomParserRegistry.clear();
+
+ // 初始化Vertx
+ Vertx vertx = Vertx.vertx();
+ WebClientVertxInit.init(vertx);
+
+ // 检查是否加载了JavaScript解析器
+ CustomParserConfig config = CustomParserRegistry.get("demo_js");
+ assert config != null : "JavaScript解析器未加载";
+ assert config.isJsParser() : "解析器类型错误";
+ assert "演示网盘(JS)".equals(config.getDisplayName()) : "显示名称错误";
+
+ System.out.println("✓ JavaScript解析器注册测试通过");
+ }
+
+ @Test
+ public void testJsParserExecution() {
+ // 清理注册表
+ CustomParserRegistry.clear();
+
+ // 初始化Vertx
+ Vertx vertx = Vertx.vertx();
+ WebClientVertxInit.init(vertx);
+
+ try {
+ // 创建解析器
+ IPanTool tool = ParserCreate.fromType("demo_js")
+ .shareKey("1")
+ .setShareLinkInfoPwd("test")
+ .createTool();
+
+ // 测试parse方法
+ String downloadUrl = tool.parseSync();
+ assert downloadUrl != null && downloadUrl.contains("cdn.example.com") :
+ "parse方法返回结果错误: " + downloadUrl;
+
+ System.out.println("✓ JavaScript解析器执行测试通过");
+ System.out.println(" 下载链接: " + downloadUrl);
+
+ } catch (Exception e) {
+ System.err.println("✗ JavaScript解析器执行测试失败: " + e.getMessage());
+ e.printStackTrace();
+ throw e;
+ }
+ }
+
+ @Test
+ public void testJsParserFileList() {
+ // 清理注册表
+ CustomParserRegistry.clear();
+
+ // 初始化Vertx
+ Vertx vertx = Vertx.vertx();
+ WebClientVertxInit.init(vertx);
+
+ try {
+ // 创建解析器
+ IPanTool tool = ParserCreate.fromType("demo_js")
+ .shareKey("1")
+ .setShareLinkInfoPwd("test")
+ .createTool();
+
+ // 测试parseFileList方法
+ List fileList = tool.parseFileList().toCompletionStage().toCompletableFuture().join();
+ assert fileList != null : "parseFileList方法返回结果错误";
+
+ System.out.println("✓ JavaScript文件列表解析测试通过");
+ System.out.println(" 文件数量: " + fileList.size());
+
+ // 如果有文件,检查第一个文件
+ if (!fileList.isEmpty()) {
+ FileInfo firstFile = fileList.get(0);
+ assert firstFile.getFileName() != null : "文件名不能为空";
+ assert firstFile.getFileId() != null : "文件ID不能为空";
+ System.out.println(" 第一个文件: " + firstFile.getFileName());
+ } else {
+ System.out.println(" 文件列表为空(这是正常的,因为使用的是测试API)");
+ }
+
+ } catch (Exception e) {
+ System.err.println("✗ JavaScript文件列表解析测试失败: " + e.getMessage());
+ e.printStackTrace();
+ throw e;
+ }
+ }
+
+ @Test
+ public void testJsParserById() {
+ // 清理注册表
+ CustomParserRegistry.clear();
+
+ // 初始化Vertx
+ Vertx vertx = Vertx.vertx();
+ WebClientVertxInit.init(vertx);
+
+ try {
+ // 创建ShareLinkInfo
+ Map otherParam = new HashMap<>();
+ Map paramJson = new HashMap<>();
+ paramJson.put("fileId", "1");
+ paramJson.put("id", "1");
+ otherParam.put("paramJson", paramJson);
+
+ ShareLinkInfo shareLinkInfo = ShareLinkInfo.newBuilder()
+ .type("demo_js")
+ .panName("演示网盘(JS)")
+ .shareKey("1")
+ .sharePassword("test")
+ .otherParam(otherParam)
+ .build();
+
+ // 创建解析器
+ IPanTool tool = ParserCreate.fromType("demo_js")
+ .shareKey("1")
+ .setShareLinkInfoPwd("test")
+ .createTool();
+
+ // 设置ShareLinkInfo(需要转换为JsParserExecutor)
+ if (tool instanceof JsParserExecutor) {
+ JsParserExecutor jsTool = (JsParserExecutor) tool;
+ jsTool.getShareLinkInfo().setOtherParam(otherParam);
+ }
+
+ // 测试parseById方法
+ String downloadUrl = tool.parseById().toCompletionStage().toCompletableFuture().join();
+ assert downloadUrl != null && downloadUrl.contains("cdn.example.com") :
+ "parseById方法返回结果错误: " + downloadUrl;
+
+ System.out.println("✓ JavaScript按ID解析测试通过");
+ System.out.println(" 下载链接: " + downloadUrl);
+
+ } catch (Exception e) {
+ System.err.println("✗ JavaScript按ID解析测试失败: " + e.getMessage());
+ e.printStackTrace();
+ throw e;
+ }
+ }
+}
diff --git a/parser/src/test/java/cn/qaiu/parser/JsScriptLoaderTest.java b/parser/src/test/java/cn/qaiu/parser/JsScriptLoaderTest.java
new file mode 100644
index 0000000..ed4c2da
--- /dev/null
+++ b/parser/src/test/java/cn/qaiu/parser/JsScriptLoaderTest.java
@@ -0,0 +1,132 @@
+package cn.qaiu.parser;
+
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+
+/**
+ * JavaScript脚本加载器测试
+ *
+ * @author QAIU
+ * Create at 2025/10/21
+ */
+public class JsScriptLoaderTest {
+
+ @Test
+ public void testSystemPropertyConfiguration() throws IOException {
+ // 创建临时目录
+ Path tempDir = Files.createTempDirectory("test-parsers");
+ try {
+ // 创建测试脚本文件
+ String testScript = "// ==UserScript==\n" +
+ "// @name 测试解析器\n" +
+ "// @type test_js\n" +
+ "// @displayName 测试网盘(JS)\n" +
+ "// @description 测试JavaScript解析器\n" +
+ "// @match https?://test\\.example\\.com/s/(?\\w+)\n" +
+ "// @author test\n" +
+ "// @version 1.0.0\n" +
+ "// ==/UserScript==\n" +
+ "\n" +
+ "function parse(shareLinkInfo, http, logger) {\n" +
+ " return 'https://test.example.com/download/test.zip';\n" +
+ "}";
+
+ Path testFile = tempDir.resolve("test-parser.js");
+ Files.write(testFile, testScript.getBytes());
+
+ // 设置系统属性
+ String originalProperty = System.getProperty("parser.custom-parsers.path");
+ try {
+ System.setProperty("parser.custom-parsers.path", tempDir.toString());
+
+ // 测试加载
+ List configs = JsScriptLoader.loadAllScripts();
+
+ // 验证结果
+ boolean foundTestParser = configs.stream()
+ .anyMatch(config -> "test_js".equals(config.getType()));
+
+ assert foundTestParser : "未找到测试解析器";
+ System.out.println("✓ 系统属性配置测试通过");
+
+ } finally {
+ // 恢复原始系统属性
+ if (originalProperty != null) {
+ System.setProperty("parser.custom-parsers.path", originalProperty);
+ } else {
+ System.clearProperty("parser.custom-parsers.path");
+ }
+ }
+
+ } finally {
+ // 清理临时目录
+ deleteDirectory(tempDir.toFile());
+ }
+ }
+
+ @Test
+ public void testEnvironmentVariableConfiguration() throws IOException {
+ // 创建临时目录
+ Path tempDir = Files.createTempDirectory("test-parsers-env");
+ try {
+ // 创建测试脚本文件
+ String testScript = "// ==UserScript==\n" +
+ "// @name 环境变量测试解析器\n" +
+ "// @type env_test_js\n" +
+ "// @displayName 环境变量测试网盘(JS)\n" +
+ "// @description 测试环境变量配置\n" +
+ "// @match https?://env\\.example\\.com/s/(?\\w+)\n" +
+ "// @author test\n" +
+ "// @version 1.0.0\n" +
+ "// ==/UserScript==\n" +
+ "\n" +
+ "function parse(shareLinkInfo, http, logger) {\n" +
+ " return 'https://env.example.com/download/test.zip';\n" +
+ "}";
+
+ Path testFile = tempDir.resolve("env-test-parser.js");
+ Files.write(testFile, testScript.getBytes());
+
+ // 设置环境变量
+ String originalEnv = System.getenv("PARSER_CUSTOM_PARSERS_PATH");
+ try {
+ // 注意:Java中无法直接修改环境变量,这里只是测试逻辑
+ // 实际使用时用户需要手动设置环境变量
+ System.out.println("✓ 环境变量配置逻辑测试通过");
+ System.out.println(" 注意:实际使用时需要手动设置环境变量 PARSER_CUSTOM_PARSERS_PATH=" + tempDir.toString());
+
+ } finally {
+ // 环境变量无法在测试中动态修改,这里只是演示
+ }
+
+ } finally {
+ // 清理临时目录
+ deleteDirectory(tempDir.toFile());
+ }
+ }
+
+ /**
+ * 递归删除目录
+ */
+ private void deleteDirectory(File directory) {
+ if (directory.exists()) {
+ File[] files = directory.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (file.isDirectory()) {
+ deleteDirectory(file);
+ } else {
+ file.delete();
+ }
+ }
+ }
+ directory.delete();
+ }
+ }
+}
diff --git a/pom.xml b/pom.xml
index 921a305..4517c93 100644
--- a/pom.xml
+++ b/pom.xml
@@ -82,7 +82,7 @@
maven-surefire-plugin
2.22.2
- true
+
diff --git a/test-filelist.java b/test-filelist.java
new file mode 100644
index 0000000..0519ecb
--- /dev/null
+++ b/test-filelist.java
@@ -0,0 +1 @@
+
\ No newline at end of file