diff --git a/parser/.flattened-pom.xml b/parser/.flattened-pom.xml
index 79176ba..660bf59 100644
--- a/parser/.flattened-pom.xml
+++ b/parser/.flattened-pom.xml
@@ -4,7 +4,7 @@
4.0.0
cn.qaiu
parser
- 10.1.17
+ 10.2.1
cn.qaiu:parser
NFD parser module
https://qaiu.top
diff --git a/parser/doc/CHANGELOG_CUSTOM_PARSER.md b/parser/doc/CHANGELOG_CUSTOM_PARSER.md
index 8c589f0..6b6ca33 100644
--- a/parser/doc/CHANGELOG_CUSTOM_PARSER.md
+++ b/parser/doc/CHANGELOG_CUSTOM_PARSER.md
@@ -15,7 +15,7 @@
#### 1. 新增类
##### CustomParserConfig.java
-- **位置:** `cn.qaiu.parser.CustomParserConfig`
+- **位置:** `cn.qaiu.parser.custom.CustomParserConfig`
- **功能:** 自定义解析器配置类
- **主要字段:**
- `type`: 解析器类型标识(唯一,必填)
@@ -30,7 +30,7 @@
- 验证必填字段是否为空
##### CustomParserRegistry.java
-- **位置:** `cn.qaiu.parser.CustomParserRegistry`
+- **位置:** `cn.qaiu.parser.custom.CustomParserRegistry`
- **功能:** 自定义解析器注册中心
- **主要方法:**
- `register(CustomParserConfig)`: 注册解析器
@@ -71,7 +71,7 @@
#### 3. 测试类
##### CustomParserTest.java
-- **位置:** `cn.qaiu.parser.CustomParserTest`
+- **位置:** `cn.qaiu.parser.custom.CustomParserTest`
- **测试覆盖:**
- ✅ 注册自定义解析器
- ✅ 重复注册检测
diff --git a/parser/doc/CUSTOM_PARSER_GUIDE.md b/parser/doc/CUSTOM_PARSER_GUIDE.md
index e8ed36a..3992cd3 100644
--- a/parser/doc/CUSTOM_PARSER_GUIDE.md
+++ b/parser/doc/CUSTOM_PARSER_GUIDE.md
@@ -185,8 +185,8 @@ WebClient 是基于 Vert.x 的异步 HTTP 客户端,其请求流程如下:
在应用启动时注册你的解析器:
```java
-import cn.qaiu.parser.CustomParserConfig;
-import cn.qaiu.parser.CustomParserRegistry;
+import cn.qaiu.parser.custom.CustomParserConfig;
+import cn.qaiu.parser.custom.CustomParserRegistry;
import com.example.parser.MyCustomPanTool;
public class Application {
@@ -356,8 +356,8 @@ public MyCustomPanTool(ShareLinkInfo shareLinkInfo) {
```java
import cn.qaiu.entity.ShareLinkInfo;
-import cn.qaiu.parser.CustomParserConfig;
-import cn.qaiu.parser.CustomParserRegistry;
+import cn.qaiu.parser.custom.CustomParserConfig;
+import cn.qaiu.parser.custom.CustomParserRegistry;
import cn.qaiu.parser.IPanTool;
import cn.qaiu.parser.ParserCreate;
import cn.qaiu.parser.PanBase;
diff --git a/parser/doc/CUSTOM_PARSER_QUICKSTART.md b/parser/doc/CUSTOM_PARSER_QUICKSTART.md
index 6a0a140..75f1ca8 100644
--- a/parser/doc/CUSTOM_PARSER_QUICKSTART.md
+++ b/parser/doc/CUSTOM_PARSER_QUICKSTART.md
@@ -53,8 +53,8 @@ public class MyPanTool implements IPanTool {
```java
package com.example.myapp.config;
-import cn.qaiu.parser.CustomParserConfig;
-import cn.qaiu.parser.CustomParserRegistry;
+import cn.qaiu.parser.custom.CustomParserConfig;
+import cn.qaiu.parser.custom.CustomParserRegistry;
import com.example.myapp.parser.MyPanTool;
public class ParserRegistry {
@@ -130,8 +130,8 @@ public class DownloadService {
package com.example;
import cn.qaiu.entity.ShareLinkInfo;
-import cn.qaiu.parser.CustomParserConfig;
-import cn.qaiu.parser.CustomParserRegistry;
+import cn.qaiu.parser.custom.CustomParserConfig;
+import cn.qaiu.parser.custom.CustomParserRegistry;
import cn.qaiu.parser.IPanTool;
import cn.qaiu.parser.ParserCreate;
import cn.qaiu.WebClientVertxInit;
diff --git a/parser/doc/JAVASCRIPT_PARSER_GUIDE.md b/parser/doc/JAVASCRIPT_PARSER_GUIDE.md
index 8bdc4cf..da790aa 100644
--- a/parser/doc/JAVASCRIPT_PARSER_GUIDE.md
+++ b/parser/doc/JAVASCRIPT_PARSER_GUIDE.md
@@ -4,6 +4,26 @@
本指南介绍如何使用JavaScript编写自定义网盘解析器,支持通过JavaScript代码实现网盘解析逻辑,无需编写Java代码。
+## 目录
+
+- [快速开始](#快速开始)
+- [API参考](#api参考)
+ - [ShareLinkInfo对象](#sharelinkinfo对象)
+ - [JsHttpClient对象](#jshttpclient对象)
+ - [JsHttpResponse对象](#jshttpresponse对象)
+ - [JsLogger对象](#jslogger对象)
+- [重定向处理](#重定向处理)
+- [代理支持](#代理支持)
+- [文件上传支持](#文件上传支持)
+- [实现方法](#实现方法)
+ - [parse方法(必填)](#parse方法必填)
+ - [parseFileList方法(可选)](#parsefilelist方法可选)
+ - [parseById方法(可选)](#parsebyid方法可选)
+- [错误处理](#错误处理)
+- [调试技巧](#调试技巧)
+- [最佳实践](#最佳实践)
+- [示例解析器](#示例解析器)
+
## 快速开始
### 1. 创建JavaScript脚本
@@ -40,9 +60,6 @@ function parse(shareLinkInfo, http, logger) {
var response = http.get(url);
return response.body();
}
-
-// 导出函数
-exports.parse = parse;
```
### 2. 重启应用
@@ -119,6 +136,16 @@ if (shareLinkInfo.hasOtherParam("customParam")) {
// GET请求
var response = http.get("https://api.example.com/data");
+// GET请求并跟随重定向
+var redirectResponse = http.getWithRedirect("https://api.example.com/redirect");
+
+// GET请求但不跟随重定向(用于获取Location头)
+var noRedirectResponse = http.getNoRedirect("https://api.example.com/redirect");
+if (noRedirectResponse.statusCode() >= 300 && noRedirectResponse.statusCode() < 400) {
+ var location = noRedirectResponse.header("Location");
+ console.log("重定向到: " + location);
+}
+
// POST请求
var response = http.post("https://api.example.com/submit", {
key: "value",
@@ -129,12 +156,19 @@ var response = http.post("https://api.example.com/submit", {
http.putHeader("User-Agent", "MyBot/1.0")
.putHeader("Authorization", "Bearer token");
-// 发送表单数据
+// 发送简单表单数据
var formResponse = http.sendForm({
username: "user",
password: "pass"
});
+// 发送multipart表单数据(支持文件上传)
+var multipartResponse = http.sendMultipartForm("https://api.example.com/upload", {
+ textField: "value",
+ fileField: fileBuffer, // Buffer或byte[]类型
+ binaryData: binaryArray // byte[]类型
+});
+
// 发送JSON数据
var jsonResponse = http.sendJson({
name: "test",
@@ -190,6 +224,168 @@ if (logger.isDebugEnabled()) {
}
```
+## 重定向处理
+
+当网盘服务返回302重定向时,可以使用`getNoRedirect`方法获取真实的下载链接:
+
+```javascript
+/**
+ * 获取真实的下载链接(处理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;
+ }
+}
+
+// 在parse方法中使用
+function parse(shareLinkInfo, http, logger) {
+ // ... 获取原始下载链接的代码 ...
+ var originalUrl = "https://example.com/download?id=123";
+
+ // 获取真实的下载链接
+ var realUrl = getRealDownloadUrl(originalUrl, http, logger);
+ return realUrl;
+}
+```
+
+## 代理支持
+
+JavaScript解析器支持HTTP代理配置,代理信息通过`ShareLinkInfo`的`otherParam`传递:
+
+```javascript
+function parse(shareLinkInfo, http, logger) {
+ // 检查是否有代理配置
+ var proxyConfig = shareLinkInfo.getOtherParam("proxy");
+ if (proxyConfig) {
+ logger.info("使用代理: " + proxyConfig.host + ":" + proxyConfig.port);
+ }
+
+ // HTTP客户端会自动使用代理配置
+ var response = http.get("https://api.example.com/data");
+ return response.body();
+}
+```
+
+代理配置格式:
+```json
+{
+ "type": "HTTP", // 代理类型: HTTP, SOCKS4, SOCKS5
+ "host": "proxy.example.com",
+ "port": 8080,
+ "username": "user", // 可选,代理认证用户名
+ "password": "pass" // 可选,代理认证密码
+}
+```
+
+## 文件上传支持
+
+JavaScript解析器支持通过`sendMultipartForm`方法上传文件:
+
+### 1. 简单文件上传
+
+```javascript
+function uploadFile(shareLinkInfo, http, logger) {
+ // 模拟文件数据(实际使用中可能是从其他地方获取)
+ var fileData = new java.lang.String("Hello, World!").getBytes();
+
+ // 使用sendMultipartForm上传文件
+ var response = http.sendMultipartForm("https://api.example.com/upload", {
+ file: fileData,
+ filename: "test.txt",
+ description: "测试文件"
+ });
+
+ return response.body();
+}
+```
+
+### 2. 混合表单上传
+
+```javascript
+function uploadMixedForm(shareLinkInfo, http, logger) {
+ var fileData = getFileData();
+
+ // 同时上传文本字段和文件
+ var response = http.sendMultipartForm("https://api.example.com/upload", {
+ username: "user123",
+ email: "user@example.com",
+ file: fileData,
+ description: "用户上传的文件"
+ });
+
+ if (response.isSuccess()) {
+ var result = response.json();
+ return result.downloadUrl;
+ } else {
+ throw new Error("文件上传失败: " + response.statusCode());
+ }
+}
+```
+
+### 3. 多文件上传
+
+```javascript
+function uploadMultipleFiles(shareLinkInfo, http, logger) {
+ var files = [
+ { name: "file1.txt", data: getFileData1() },
+ { name: "file2.jpg", data: getFileData2() }
+ ];
+
+ var uploadResults = [];
+
+ for (var i = 0; i < files.length; i++) {
+ var file = files[i];
+ var response = http.sendMultipartForm("https://api.example.com/upload", {
+ file: file.data,
+ filename: file.name,
+ uploadIndex: i.toString()
+ });
+
+ if (response.isSuccess()) {
+ uploadResults.push({
+ fileName: file.name,
+ success: true,
+ url: response.json().url
+ });
+ } else {
+ uploadResults.push({
+ fileName: file.name,
+ success: false,
+ error: response.statusCode()
+ });
+ }
+ }
+
+ return uploadResults;
+}
+```
+
## 实现方法
JavaScript解析器支持三种方法,对应Java接口的三种同步方法:
@@ -301,43 +497,49 @@ String fileUrl = tool.parseByIdSync(); // 调用 parseById() 函数
- 如果JavaScript方法抛出异常,Java同步方法会抛出相应的异常
- 建议在JavaScript方法中添加适当的错误处理和日志记录
-## 导出方式
+## 函数定义方式
-支持三种导出方式:
-
-### 方式1:分别导出(推荐)
+JavaScript解析器使用全局函数定义,不需要`exports`对象:
```javascript
-function parse(shareLinkInfo, http, logger) { }
-function parseFileList(shareLinkInfo, http, logger) { }
-function parseById(shareLinkInfo, http, logger) { }
+/**
+ * 解析单个文件下载链接(必填)
+ * @param {ShareLinkInfo} shareLinkInfo - 分享链接信息
+ * @param {JsHttpClient} http - HTTP客户端
+ * @param {JsLogger} logger - 日志对象
+ * @returns {string} 下载链接
+ */
+function parse(shareLinkInfo, http, logger) {
+ // 实现解析逻辑
+ return "https://example.com/download";
+}
-exports.parse = parse;
-exports.parseFileList = parseFileList;
-exports.parseById = parseById;
+/**
+ * 解析文件列表(可选)
+ * @param {ShareLinkInfo} shareLinkInfo - 分享链接信息
+ * @param {JsHttpClient} http - HTTP客户端
+ * @param {JsLogger} logger - 日志对象
+ * @returns {Array} 文件信息数组
+ */
+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";
+}
```
-### 方式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
-};
-```
+**注意**:JavaScript解析器通过`engine.eval()`执行,函数必须定义为全局函数,不需要使用`exports`或`module.exports`。
## VSCode配置
diff --git a/parser/doc/README.md b/parser/doc/README.md
index 723a6c7..1bd379a 100644
--- a/parser/doc/README.md
+++ b/parser/doc/README.md
@@ -5,6 +5,7 @@
- 语言/构建:Java 17 / Maven
- 关键接口:cn.qaiu.parser.IPanTool(返回 Future>),各站点位于 parser/src/main/java/cn/qaiu/parser/impl
- 数据模型:cn.qaiu.entity.FileInfo(统一对外文件项)
+- JavaScript解析器:支持使用JavaScript编写自定义解析器,位于 parser/src/main/resources/custom-parsers/
---
@@ -75,6 +76,51 @@ List files = tool.parseFileListSync();
- 异步方法仍可用:parse()、parseFileList()、parseById() 返回 Future 对象
- 生成短链 path:ParserCreate.genPathSuffix()(用于页面/服务端聚合)。
+## JavaScript解析器快速开始
+
+除了Java解析器,还支持使用JavaScript编写自定义解析器:
+
+### 1. 创建JavaScript解析器
+
+在 `parser/src/main/resources/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==
+
+/**
+ * 解析单个文件下载链接
+ * @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();
+}
+```
+
+### 2. JavaScript解析器特性
+
+- **重定向处理**:支持`getNoRedirect()`方法获取302重定向的真实链接
+- **代理支持**:自动支持HTTP/SOCKS代理配置
+- **类型提示**:提供完整的JSDoc类型定义
+- **热加载**:修改后重启应用即可生效
+
+### 3. 详细文档
+
+- [JavaScript解析器开发指南](JAVASCRIPT_PARSER_GUIDE.md)
+- [自定义解析器开发指南](CUSTOM_PARSER_GUIDE.md)
+
---
## 1. 解析器约定
@@ -97,196 +143,125 @@ FileInfo 关键字段(节选):
---
-## 2. 文件列表解析规范(按给定 JSON)
-目标 JSON(摘要):
-- 列表路径:data.data[]
-- 每项结构:item.data(含 attributes、id、type、relationships)
-- type:"file" 或 "folder"
+## 2. 文件列表解析规范
-字段映射建议:
-- 通用
- - fileId ← data.id
- - createTime ← data.attributes.created_at(若格式不一致,上层再统一格式化)
- - updateTime ← data.attributes.updated_at
- - fileType:
- - 对文件用 data.attributes.mimetype 或固定 "file"
- - 对目录固定 "folder"
-- 文件(type="file")
- - fileName ← 优先 attributes.basename(示例:"GBT+28448-2019.pdf"),无则用 attributes.name
- - sizeStr ← attributes.filesize(示例:"18MB")
- - size ← 尝试用 FileSizeConverter.convertToBytes(sizeStr),失败则置空
- - parserUrl ← attributes.file_url(示例:BilPan://downLoad?id=...)
- - filePath/parentId ← relationships.parent.data.id(可放到 extParameters.parentId)
- - previewUrl/thumbnail ← attributes.thumbnail(可选)
-- 目录(type="folder")
- - fileName ← attributes.name
- - size/sizeStr ← 置空
- - 统计字段(如 items/trashed_items)可入 extParameters
+### 通用解析原则
-边界与兼容:
-- attributes.filesize 可能为空或为非标准字符串;转换失败时保留 sizeStr,忽略 size。
-- attributes.file_url 可能为占位协议(BilPan://),直链转换在下载阶段处理。
-- relationships.* 可能为空,读取前需判空。
+1. **数据结构识别**:根据网盘API响应结构确定文件列表的路径
+2. **字段映射**:将网盘特定字段映射到统一的`FileInfo`对象
+3. **类型区分**:正确识别文件和文件夹类型
+4. **数据转换**:处理时间格式、文件大小等数据格式转换
+
+### FileInfo字段映射指南
+
+| FileInfo字段 | 说明 | 映射建议 |
+|-------------|------|----------|
+| `fileName` | 文件名 | 优先使用文件名字段,无则使用标题字段 |
+| `fileId` | 文件ID | 使用网盘提供的唯一标识符 |
+| `fileType` | 文件类型 | "file"或"folder" |
+| `size` | 文件大小(字节) | 转换为字节数,文件夹可为0 |
+| `sizeStr` | 文件大小(可读) | 保持网盘原始格式或转换 |
+| `createTime` | 创建时间 | 统一时间格式 |
+| `updateTime` | 更新时间 | 统一时间格式 |
+| `parserUrl` | 下载链接 | 网盘提供的下载URL |
+| `previewUrl` | 预览链接 | 可选,网盘提供的预览URL |
+
+### 常见数据转换
+
+- **文件大小**:使用`FileSizeConverter`进行字符串与字节数转换
+- **时间格式**:统一转换为标准时间格式
+- **文件类型**:根据网盘API判断文件/文件夹类型
+
+### 解析注意事项
+
+- **数据验证**:检查必要字段是否存在,避免空指针异常
+- **格式兼容**:处理不同网盘的数据格式差异
+- **错误处理**:转换失败时提供合理的默认值
+- **扩展字段**:额外信息可存储在`extParameters`中
+
+### 解析示例
-伪代码(parseFileList 核心片段):
```java
-// 仅示意,按项目 Json 工具替换
-JsonObject root = ...; // 接口返回
-JsonArray arr = root.getJsonObject("data").getJsonArray("data");
-List list = new ArrayList<>();
-for (JsonObject wrap : arr) {
- JsonObject d = wrap.getJsonObject("data");
- String type = d.getString("type");
- JsonObject attrs = d.getJsonObject("attributes");
- FileInfo fi = new FileInfo();
- fi.setFileId(d.getString("id"));
- fi.setCreateTime(attrs.getString("created_at"));
- fi.setUpdateTime(attrs.getString("updated_at"));
- if ("file".equals(type)) {
- String basename = attrs.getString("basename");
- fi.setFileName(basename != null ? basename : attrs.getString("name"));
- fi.setFileType(attrs.getString("mimetype", "file"));
- String sizeStr = attrs.getString("filesize");
- fi.setSizeStr(sizeStr);
- try { if (sizeStr != null) fi.setSize(FileSizeConverter.convertToBytes(sizeStr)); } catch (Exception ignore) {}
- fi.setParserUrl(attrs.getString("file_url"));
- // parentId(可选)
- JsonObject rel = d.getJsonObject("relationships");
- if (rel != null) {
- JsonObject p = rel.getJsonObject("parent");
- if (p != null && p.getJsonObject("data") != null) {
- String pid = p.getJsonObject("data").getString("id");
- Map ext = new HashMap<>();
- ext.put("parentId", pid);
- fi.setExtParameters(ext);
- }
+// 通用解析模式示例
+JsonObject root = response.json(); // 获取API响应
+JsonArray fileList = root.getJsonArray("files"); // 根据实际API调整路径
+List result = new ArrayList<>();
+
+for (JsonObject item : fileList) {
+ FileInfo fileInfo = new FileInfo();
+
+ // 基本字段映射
+ fileInfo.setFileName(item.getString("name"));
+ fileInfo.setFileId(item.getString("id"));
+ fileInfo.setFileType(item.getString("type").equals("file") ? "file" : "folder");
+
+ // 文件大小处理
+ String sizeStr = item.getString("size");
+ if (sizeStr != null) {
+ fileInfo.setSizeStr(sizeStr);
+ try {
+ fileInfo.setSize(FileSizeConverter.convertToBytes(sizeStr));
+ } catch (Exception e) {
+ // 转换失败时保持sizeStr,size为0
+ }
}
- } else {
- fi.setFileName(attrs.getString("name"));
- fi.setFileType("folder");
- }
- list.add(fi);
+
+ // 时间处理
+ fileInfo.setCreateTime(formatTime(item.getString("createTime")));
+ fileInfo.setUpdateTime(formatTime(item.getString("updateTime")));
+
+ // 下载链接
+ fileInfo.setParserUrl(item.getString("downloadUrl"));
+
+ result.add(fileInfo);
}
-return Future.succeededFuture(list);
```
----
+### JavaScript解析器示例
-## 3. curl 转 Java 11 HttpClient 示例
-以 GET 为例(来源:developer-oss.lanrar.com):
-```java
-HttpClient client = HttpClient.newHttpClient();
-String q = "<替换为长查询串>";
-String url = "https://developer-oss.lanrar.com/file/?" + URLEncoder.encode(q, StandardCharsets.UTF_8);
-HttpRequest req = HttpRequest.newBuilder(URI.create(url))
- .header("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7")
- .header("accept-language", "zh-CN,zh;q=0.9")
- .header("cache-control", "max-age=0")
- .header("dnt", "1")
- .header("priority", "u=0, i")
- .header("referer", "https://developer-oss.lanrar.com/file/?" + q)
- .header("sec-ch-ua", "\"Chromium\";v=\"140\", \"Not=A?Brand\";v=\"24\", \"Microsoft Edge\";v=\"140\"")
- .header("sec-ch-ua-mobile", "?0")
- .header("sec-ch-ua-platform", "\"macOS\"")
- .header("sec-fetch-dest", "document")
- .header("sec-fetch-mode", "navigate")
- .header("sec-fetch-site", "same-origin")
- .header("upgrade-insecure-requests", "1")
- .header("user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0")
- .header("Cookie", "acw_tc=; cdn_sec_tc=; acw_sc__v2=")
- .GET()
- .build();
-HttpResponse resp = client.send(req, HttpResponse.BodyHandlers.ofString());
-System.out.println(resp.statusCode());
-System.out.println(resp.body());
-```
-
-POST 示例(来源:Weiyun Share BatchDownload,使用 JSON):
-```java
-HttpClient client = HttpClient.newHttpClient();
-String url = "https://share.weiyun.com/webapp/json/weiyunShare/WeiyunShareBatchDownload?refer=chrome_mac&g_tk=1399845656&r=0.3925692266635241";
-String json = "{...与 curl/requests 等价 JSON 负载,使用占位参数...}";
-HttpRequest req = HttpRequest.newBuilder(URI.create(url))
- .header("accept", "application/json, text/plain, */*")
- .header("content-type", "application/json;charset=UTF-8")
- .header("origin", "https://share.weiyun.com")
- .header("referer", "https://share.weiyun.com/")
- .header("user-agent", "Mozilla/5.0 ...")
- .header("Cookie", "uin=; skey=; p_skey=; ...")
- .POST(HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8))
- .build();
-HttpResponse resp = client.send(req, HttpResponse.BodyHandlers.ofString());
-```
-提示:
-- Cookie/Token 使用占位并从外部注入,避免硬编码与泄露。
-- r/g_tk 等参数如需计算,请在实现类中封装。
-
----
-
-## 4. IntelliJ IDEA `.http` 调试样例
-保存为 `requests.http`,可配合环境变量使用。
-
-GET:
-```http
-### 开发者资源 GET 示例
-GET https://developer-oss.lanrar.com/file/?{{q}}
-accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
-accept-language: zh-CN,zh;q=0.9
-cache-control: max-age=0
-dnt: 1
-priority: u=0, i
-referer: https://developer-oss.lanrar.com/file/?{{q}}
-sec-ch-ua: "Chromium";v="140", "Not=A?Brand";v="24", "Microsoft Edge";v="140"
-sec-ch-ua-mobile: ?0
-sec-ch-ua-platform: "macOS"
-sec-fetch-dest: document
-sec-fetch-mode: navigate
-sec-fetch-site: same-origin
-upgrade-insecure-requests: 1
-user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0
-Cookie: acw_tc={{acw_tc}}; cdn_sec_tc={{cdn_sec_tc}}; acw_sc__v2={{acw_sc_v2}}
-
-> {% client.log("status: " + response.status); %}
-
-### 环境变量(可在 HTTP Client Environment 中配置)
-@q=替换为实际长查询串
-@acw_tc=your_acw_tc
-@cdn_sec_tc=your_cdn_sec_tc
-@acw_sc_v2=your_acw_sc__v2
-```
-
-POST:
-```http
-### Weiyun 批量下载 POST 示例
-POST https://share.weiyun.com/webapp/json/weiyunShare/WeiyunShareBatchDownload?refer=chrome_mac&g_tk={{g_tk}}&r={{r}}
-accept: application/json, text/plain, */*
-content-type: application/json;charset=UTF-8
-origin: https://share.weiyun.com
-referer: https://share.weiyun.com/{{share_key}}
-user-agent: Mozilla/5.0 ...
-Cookie: uin={{uin}}; skey={{skey}}; p_skey={{p_skey}}; p_uin={{p_uin}}; wyctoken={{wyctoken}}
-
-{
- "req_header": "{...}",
- "req_body": "{...}"
+```javascript
+function parseFileList(shareLinkInfo, http, logger) {
+ var response = http.get(shareLinkInfo.getShareUrl());
+ var data = response.json();
+
+ var fileList = [];
+ var files = data.files || data.data || data.items; // 根据实际API调整
+
+ for (var i = 0; i < files.length; i++) {
+ var file = files[i];
+ var fileInfo = {
+ fileName: file.name || file.title,
+ fileId: file.id,
+ fileType: file.type === "file" ? "file" : "folder",
+ size: file.size || 0,
+ sizeStr: file.sizeStr || formatSize(file.size),
+ createTime: file.createTime,
+ updateTime: file.updateTime,
+ parserUrl: file.downloadUrl || file.url
+ };
+
+ fileList.push(fileInfo);
+ }
+
+ return fileList;
}
```
---
-## 5. 开发流程建议
+## 3. 开发流程建议
- 新增站点:在 impl 下新增 Tool,实现 IPanTool,复用 PanBase/模板类;补充单测。
- 字段不全:尽量回填 sizeStr/createTime 等便于前端展示;不可用字段置空。
- 单测:放置于 parser/src/test/java,尽量添加 1-2 个 happy path + 1 个边界用例。
-## 6. 常见问题
+## 4. 常见问题
- 容量解析失败:保留 sizeStr,并忽略 size;避免抛出异常影响整体列表。
- 协议占位下载链接:统一放至 parserUrl,直链转换由下载阶段处理。
- 鉴权:Cookie/Token 过期问题由上层刷新或外部注入处理;解析器保持无状态最佳。
---
-## 7. 参考
+## 5. 参考
- FileInfo:parser/src/main/java/cn/qaiu/entity/FileInfo.java
- IPanTool:parser/src/main/java/cn/qaiu/parser/IPanTool.java
- FileSizeConverter:parser/src/main/java/cn/qaiu/util/FileSizeConverter.java
diff --git a/parser/pom.xml b/parser/pom.xml
index 577ab6f..674a65d 100644
--- a/parser/pom.xml
+++ b/parser/pom.xml
@@ -12,7 +12,7 @@
cn.qaiu
parser
- 10.1.17
+ 10.2.1
jar
cn.qaiu:parser
diff --git a/parser/src/main/java/cn/qaiu/WebClientVertxInit.java b/parser/src/main/java/cn/qaiu/WebClientVertxInit.java
index 61449ae..2dd0560 100644
--- a/parser/src/main/java/cn/qaiu/WebClientVertxInit.java
+++ b/parser/src/main/java/cn/qaiu/WebClientVertxInit.java
@@ -1,10 +1,11 @@
package cn.qaiu;
-import cn.qaiu.parser.CustomParserRegistry;
import io.vertx.core.Vertx;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import cn.qaiu.parser.custom.CustomParserRegistry;
+
public class WebClientVertxInit {
private Vertx vertx = null;
private static final WebClientVertxInit INSTANCE = new WebClientVertxInit();
@@ -36,4 +37,4 @@ public class WebClientVertxInit {
}
return INSTANCE.vertx;
}
-}
+}
\ No newline at end of file
diff --git a/parser/src/main/java/cn/qaiu/parser/ParserCreate.java b/parser/src/main/java/cn/qaiu/parser/ParserCreate.java
index 454811c..e99dd2d 100644
--- a/parser/src/main/java/cn/qaiu/parser/ParserCreate.java
+++ b/parser/src/main/java/cn/qaiu/parser/ParserCreate.java
@@ -1,6 +1,10 @@
package cn.qaiu.parser;
import cn.qaiu.entity.ShareLinkInfo;
+import cn.qaiu.parser.custom.CustomParserConfig;
+import cn.qaiu.parser.custom.CustomParserRegistry;
+import cn.qaiu.parser.customjs.JsParserExecutor;
+
import org.apache.commons.lang3.StringUtils;
import java.net.URLEncoder;
diff --git a/parser/src/main/java/cn/qaiu/parser/CustomParserConfig.java b/parser/src/main/java/cn/qaiu/parser/custom/CustomParserConfig.java
similarity index 99%
rename from parser/src/main/java/cn/qaiu/parser/CustomParserConfig.java
rename to parser/src/main/java/cn/qaiu/parser/custom/CustomParserConfig.java
index 5ee2d24..54ec474 100644
--- a/parser/src/main/java/cn/qaiu/parser/CustomParserConfig.java
+++ b/parser/src/main/java/cn/qaiu/parser/custom/CustomParserConfig.java
@@ -1,6 +1,7 @@
-package cn.qaiu.parser;
+package cn.qaiu.parser.custom;
import cn.qaiu.entity.ShareLinkInfo;
+import cn.qaiu.parser.IPanTool;
import java.util.Map;
import java.util.regex.Pattern;
diff --git a/parser/src/main/java/cn/qaiu/parser/CustomParserRegistry.java b/parser/src/main/java/cn/qaiu/parser/custom/CustomParserRegistry.java
similarity index 97%
rename from parser/src/main/java/cn/qaiu/parser/CustomParserRegistry.java
rename to parser/src/main/java/cn/qaiu/parser/custom/CustomParserRegistry.java
index b224540..a978ed3 100644
--- a/parser/src/main/java/cn/qaiu/parser/CustomParserRegistry.java
+++ b/parser/src/main/java/cn/qaiu/parser/custom/CustomParserRegistry.java
@@ -1,8 +1,12 @@
-package cn.qaiu.parser;
+package cn.qaiu.parser.custom;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import cn.qaiu.parser.PanDomainTemplate;
+import cn.qaiu.parser.customjs.JsScriptLoader;
+import cn.qaiu.parser.customjs.JsScriptMetadataParser;
+
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
diff --git a/parser/src/main/java/cn/qaiu/parser/JsHttpClient.java b/parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java
similarity index 64%
rename from parser/src/main/java/cn/qaiu/parser/JsHttpClient.java
rename to parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java
index d78c962..ab5fd78 100644
--- a/parser/src/main/java/cn/qaiu/parser/JsHttpClient.java
+++ b/parser/src/main/java/cn/qaiu/parser/customjs/JsHttpClient.java
@@ -1,4 +1,4 @@
-package cn.qaiu.parser;
+package cn.qaiu.parser.customjs;
import cn.qaiu.WebClientVertxInit;
import cn.qaiu.util.HttpResponseHelper;
@@ -6,11 +6,17 @@ 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.JsonArray;
import io.vertx.core.json.JsonObject;
+import io.vertx.core.net.ProxyOptions;
+import io.vertx.core.net.ProxyType;
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.WebClientOptions;
import io.vertx.ext.web.client.WebClientSession;
+import io.vertx.ext.web.multipart.MultipartForm;
+import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -36,6 +42,48 @@ public class JsHttpClient {
this.client = WebClient.create(WebClientVertxInit.get());
this.clientSession = WebClientSession.create(client);
this.headers = MultiMap.caseInsensitiveMultiMap();
+ // 设置默认的Accept-Encoding头以支持压缩响应
+ this.headers.set("Accept-Encoding", "gzip, deflate, br, zstd");
+ // 设置默认的User-Agent头
+ this.headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0");
+ // 设置默认的Accept-Language头
+ this.headers.set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6");
+ }
+
+ /**
+ * 带代理配置的构造函数
+ * @param proxyConfig 代理配置JsonObject,包含type、host、port、username、password
+ */
+ public JsHttpClient(JsonObject proxyConfig) {
+ if (proxyConfig != null && proxyConfig.containsKey("type")) {
+ ProxyOptions proxyOptions = new ProxyOptions()
+ .setType(ProxyType.valueOf(proxyConfig.getString("type").toUpperCase()))
+ .setHost(proxyConfig.getString("host"))
+ .setPort(proxyConfig.getInteger("port"));
+
+ if (StringUtils.isNotEmpty(proxyConfig.getString("username"))) {
+ proxyOptions.setUsername(proxyConfig.getString("username"));
+ }
+ if (StringUtils.isNotEmpty(proxyConfig.getString("password"))) {
+ proxyOptions.setPassword(proxyConfig.getString("password"));
+ }
+
+ this.client = WebClient.create(WebClientVertxInit.get(),
+ new WebClientOptions()
+ .setUserAgentEnabled(false)
+ .setProxyOptions(proxyOptions));
+ this.clientSession = WebClientSession.create(client);
+ } else {
+ this.client = WebClient.create(WebClientVertxInit.get());
+ this.clientSession = WebClientSession.create(client);
+ }
+ this.headers = MultiMap.caseInsensitiveMultiMap();
+ // 设置默认的Accept-Encoding头以支持压缩响应
+ this.headers.set("Accept-Encoding", "gzip, deflate, br, zstd");
+ // 设置默认的User-Agent头
+ this.headers.set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0");
+ // 设置默认的Accept-Language头
+ this.headers.set("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6");
}
/**
@@ -132,7 +180,7 @@ public class JsHttpClient {
}
/**
- * 发送表单数据
+ * 发送表单数据(简单键值对)
* @param data 表单数据
* @return HTTP响应
*/
@@ -152,6 +200,45 @@ public class JsHttpClient {
});
}
+ /**
+ * 发送multipart表单数据(支持文件上传)
+ * @param url 请求URL
+ * @param data 表单数据,支持:
+ * - Map: 文本字段
+ * - Map: 混合字段,Object可以是String、byte[]或Buffer
+ * @return HTTP响应
+ */
+ public JsHttpResponse sendMultipartForm(String url, Map data) {
+ return executeRequest(() -> {
+ HttpRequest request = client.postAbs(url);
+ if (!headers.isEmpty()) {
+ request.putHeaders(headers);
+ }
+
+ MultipartForm form = MultipartForm.create();
+
+ if (data != null) {
+ for (Map.Entry entry : data.entrySet()) {
+ String key = entry.getKey();
+ Object value = entry.getValue();
+
+ if (value instanceof String) {
+ form.attribute(key, (String) value);
+ } else if (value instanceof byte[]) {
+ form.binaryFileUpload(key, key, Buffer.buffer((byte[]) value), "application/octet-stream");
+ } else if (value instanceof Buffer) {
+ form.binaryFileUpload(key, key, (Buffer) value, "application/octet-stream");
+ } else if (value != null) {
+ // 其他类型转换为字符串
+ form.attribute(key, value.toString());
+ }
+ }
+ }
+
+ return request.sendMultipartForm(form);
+ });
+ }
+
/**
* 发送JSON数据
* @param data JSON数据
@@ -224,26 +311,13 @@ public class JsHttpClient {
*/
public Object json() {
try {
- String body = response.bodyAsString();
- if (body == null || body.trim().isEmpty()) {
+ JsonObject jsonObject = HttpResponseHelper.asJson(response);
+ if (jsonObject == null || jsonObject.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;
- }
- }
+ // 将JsonObject转换为Map,这样JavaScript可以正确访问
+ return jsonObject.getMap();
} catch (Exception e) {
log.error("解析JSON响应失败", e);
throw new RuntimeException("解析JSON响应失败: " + e.getMessage(), e);
diff --git a/parser/src/main/java/cn/qaiu/parser/JsLogger.java b/parser/src/main/java/cn/qaiu/parser/customjs/JsLogger.java
similarity index 99%
rename from parser/src/main/java/cn/qaiu/parser/JsLogger.java
rename to parser/src/main/java/cn/qaiu/parser/customjs/JsLogger.java
index 969e6a8..c30e9bb 100644
--- a/parser/src/main/java/cn/qaiu/parser/JsLogger.java
+++ b/parser/src/main/java/cn/qaiu/parser/customjs/JsLogger.java
@@ -1,4 +1,4 @@
-package cn.qaiu.parser;
+package cn.qaiu.parser.customjs;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
diff --git a/parser/src/main/java/cn/qaiu/parser/JsParserExecutor.java b/parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java
similarity index 96%
rename from parser/src/main/java/cn/qaiu/parser/JsParserExecutor.java
rename to parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java
index 1cc0e7c..81715f1 100644
--- a/parser/src/main/java/cn/qaiu/parser/JsParserExecutor.java
+++ b/parser/src/main/java/cn/qaiu/parser/customjs/JsParserExecutor.java
@@ -1,9 +1,12 @@
-package cn.qaiu.parser;
+package cn.qaiu.parser.customjs;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo;
+import cn.qaiu.parser.IPanTool;
+import cn.qaiu.parser.custom.CustomParserConfig;
import io.vertx.core.Future;
import io.vertx.core.Promise;
+import io.vertx.core.json.JsonObject;
import org.openjdk.nashorn.api.scripting.ScriptObjectMirror;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -39,7 +42,14 @@ public class JsParserExecutor implements IPanTool {
this.config = config;
this.shareLinkInfo = shareLinkInfo;
this.engine = initEngine();
- this.httpClient = new JsHttpClient();
+
+ // 检查是否有代理配置
+ JsonObject proxyConfig = null;
+ if (shareLinkInfo.getOtherParam().containsKey("proxy")) {
+ proxyConfig = (JsonObject) shareLinkInfo.getOtherParam().get("proxy");
+ }
+
+ this.httpClient = new JsHttpClient(proxyConfig);
this.jsLogger = new JsLogger("JsParser-" + config.getType());
this.shareLinkInfoWrapper = new JsShareLinkInfoWrapper(shareLinkInfo);
}
diff --git a/parser/src/main/java/cn/qaiu/parser/JsScriptLoader.java b/parser/src/main/java/cn/qaiu/parser/customjs/JsScriptLoader.java
similarity index 65%
rename from parser/src/main/java/cn/qaiu/parser/JsScriptLoader.java
rename to parser/src/main/java/cn/qaiu/parser/customjs/JsScriptLoader.java
index 785a633..8354918 100644
--- a/parser/src/main/java/cn/qaiu/parser/JsScriptLoader.java
+++ b/parser/src/main/java/cn/qaiu/parser/customjs/JsScriptLoader.java
@@ -1,8 +1,10 @@
-package cn.qaiu.parser;
+package cn.qaiu.parser.customjs;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import cn.qaiu.parser.custom.CustomParserConfig;
+
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
@@ -66,33 +68,28 @@ public class JsScriptLoader {
List configs = new ArrayList<>();
try {
- // 获取资源目录的输入流
- InputStream resourceStream = JsScriptLoader.class.getClassLoader()
- .getResourceAsStream(RESOURCE_PATH);
+ // 尝试使用反射方式获取JAR包内的资源文件列表
+ List resourceFiles = getResourceFileList();
- if (resourceStream == null) {
- log.debug("资源目录 {} 不存在", RESOURCE_PATH);
- return configs;
- }
+ // 按文件名排序,确保加载顺序一致
+ resourceFiles.sort(String::compareTo);
- // 读取资源目录下的所有文件
- 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);
- }
- });
+ for (String resourceFile : resourceFiles) {
+ try {
+ InputStream inputStream = JsScriptLoader.class.getClassLoader()
+ .getResourceAsStream(resourceFile);
+
+ if (inputStream != null) {
+ String jsCode = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
+ CustomParserConfig config = JsScriptMetadataParser.parseScript(jsCode);
+ configs.add(config);
+
+ String fileName = resourceFile.substring(resourceFile.lastIndexOf('/') + 1);
+ log.debug("从资源目录加载脚本: {}", fileName);
+ }
+ } catch (Exception e) {
+ log.warn("加载资源脚本失败: {}", resourceFile, e);
+ }
}
} catch (Exception e) {
@@ -102,6 +99,92 @@ public class JsScriptLoader {
return configs;
}
+ /**
+ * 尝试使用反射方式获取JAR包内的资源文件列表
+ */
+ private static List getResourceFileList() {
+ List resourceFiles = new ArrayList<>();
+
+ try {
+ // 尝试获取资源目录的URL
+ java.net.URL resourceUrl = JsScriptLoader.class.getClassLoader()
+ .getResource(RESOURCE_PATH);
+
+ if (resourceUrl != null) {
+ String protocol = resourceUrl.getProtocol();
+
+ if ("jar".equals(protocol)) {
+ // JAR包内的资源
+ resourceFiles = getJarResourceFiles(resourceUrl);
+ } else if ("file".equals(protocol)) {
+ // 文件系统中的资源(开发环境)
+ resourceFiles = getFileSystemResourceFiles(resourceUrl);
+ }
+ }
+ } catch (Exception e) {
+ log.debug("使用反射方式获取资源文件列表失败,将使用预定义列表", e);
+ }
+
+ return resourceFiles;
+ }
+
+ /**
+ * 获取JAR包内的资源文件列表
+ */
+ private static List getJarResourceFiles(java.net.URL jarUrl) {
+ List resourceFiles = new ArrayList<>();
+
+ try {
+ String jarPath = jarUrl.getPath().substring(5, jarUrl.getPath().indexOf("!"));
+ java.util.jar.JarFile jarFile = new java.util.jar.JarFile(jarPath);
+
+ java.util.Enumeration entries = jarFile.entries();
+ while (entries.hasMoreElements()) {
+ java.util.jar.JarEntry entry = entries.nextElement();
+ String entryName = entry.getName();
+
+ if (entryName.startsWith(RESOURCE_PATH + "/") &&
+ entryName.endsWith(".js") &&
+ !isExcludedFile(entryName.substring(entryName.lastIndexOf('/') + 1))) {
+ resourceFiles.add(entryName);
+ }
+ }
+
+ jarFile.close();
+ } catch (Exception e) {
+ log.debug("解析JAR包资源文件失败", e);
+ }
+
+ return resourceFiles;
+ }
+
+ /**
+ * 获取文件系统中的资源文件列表
+ */
+ private static List getFileSystemResourceFiles(java.net.URL fileUrl) {
+ List resourceFiles = new ArrayList<>();
+
+ try {
+ java.io.File resourceDir = new java.io.File(fileUrl.getPath());
+ if (resourceDir.exists() && resourceDir.isDirectory()) {
+ java.io.File[] files = resourceDir.listFiles();
+ if (files != null) {
+ for (java.io.File file : files) {
+ if (file.isFile() && file.getName().endsWith(".js") &&
+ !isExcludedFile(file.getName())) {
+ resourceFiles.add(RESOURCE_PATH + "/" + file.getName());
+ }
+ }
+ }
+ }
+ } catch (Exception e) {
+ log.debug("解析文件系统资源文件失败", e);
+ }
+
+ return resourceFiles;
+ }
+
+
/**
* 从外部目录加载JavaScript脚本
*/
diff --git a/parser/src/main/java/cn/qaiu/parser/JsScriptMetadataParser.java b/parser/src/main/java/cn/qaiu/parser/customjs/JsScriptMetadataParser.java
similarity index 98%
rename from parser/src/main/java/cn/qaiu/parser/JsScriptMetadataParser.java
rename to parser/src/main/java/cn/qaiu/parser/customjs/JsScriptMetadataParser.java
index 87d08b3..a90bf05 100644
--- a/parser/src/main/java/cn/qaiu/parser/JsScriptMetadataParser.java
+++ b/parser/src/main/java/cn/qaiu/parser/customjs/JsScriptMetadataParser.java
@@ -1,9 +1,11 @@
-package cn.qaiu.parser;
+package cn.qaiu.parser.customjs;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import cn.qaiu.parser.custom.CustomParserConfig;
+
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
diff --git a/parser/src/main/java/cn/qaiu/parser/JsShareLinkInfoWrapper.java b/parser/src/main/java/cn/qaiu/parser/customjs/JsShareLinkInfoWrapper.java
similarity index 99%
rename from parser/src/main/java/cn/qaiu/parser/JsShareLinkInfoWrapper.java
rename to parser/src/main/java/cn/qaiu/parser/customjs/JsShareLinkInfoWrapper.java
index a5b6b1d..c8c4ac2 100644
--- a/parser/src/main/java/cn/qaiu/parser/JsShareLinkInfoWrapper.java
+++ b/parser/src/main/java/cn/qaiu/parser/customjs/JsShareLinkInfoWrapper.java
@@ -1,4 +1,4 @@
-package cn.qaiu.parser;
+package cn.qaiu.parser.customjs;
import cn.qaiu.entity.ShareLinkInfo;
import org.slf4j.Logger;
diff --git a/parser/src/main/java/cn/qaiu/util/HttpResponseHelper.java b/parser/src/main/java/cn/qaiu/util/HttpResponseHelper.java
index 145ea53..73f784c 100644
--- a/parser/src/main/java/cn/qaiu/util/HttpResponseHelper.java
+++ b/parser/src/main/java/cn/qaiu/util/HttpResponseHelper.java
@@ -59,7 +59,7 @@ public class HttpResponseHelper {
case "gzip" -> decompressGzip(compressed);
case "deflate" -> decompressDeflate(compressed);
case "br" -> decompressBrotli(compressed);
- //case "zstd" -> decompressZstd(compressed);
+ case "zstd" -> compressed.toString(StandardCharsets.UTF_8); // 暂时返回原始内容
default -> throw new UnsupportedOperationException("不支持的 Content-Encoding: " + encoding);
};
}
diff --git a/parser/src/main/resources/custom-parsers/types.js b/parser/src/main/resources/custom-parsers/types.js
index 8c32c12..bfd23b7 100644
--- a/parser/src/main/resources/custom-parsers/types.js
+++ b/parser/src/main/resources/custom-parsers/types.js
@@ -29,9 +29,12 @@
/**
* @typedef {Object} JsHttpClient
* @property {function(string): JsHttpResponse} get - 发起GET请求
+ * @property {function(string): JsHttpResponse} getWithRedirect - 发起GET请求并跟随重定向
+ * @property {function(string): JsHttpResponse} getNoRedirect - 发起GET请求但不跟随重定向(用于获取Location头)
* @property {function(string, any=): JsHttpResponse} post - 发起POST请求
* @property {function(string, string): JsHttpClient} putHeader - 设置请求头
- * @property {function(Object): JsHttpResponse} sendForm - 发送表单数据
+ * @property {function(Object): JsHttpResponse} sendForm - 发送简单表单数据
+ * @property {function(string, Object): JsHttpResponse} sendMultipartForm - 发送multipart表单数据(支持文件上传)
* @property {function(any): JsHttpResponse} sendJson - 发送JSON数据
*/
diff --git a/parser/src/test/java/cn/qaiu/parser/BaiduPhotoParserTest.java b/parser/src/test/java/cn/qaiu/parser/BaiduPhotoParserTest.java
index 2c49509..ee6bde0 100644
--- a/parser/src/test/java/cn/qaiu/parser/BaiduPhotoParserTest.java
+++ b/parser/src/test/java/cn/qaiu/parser/BaiduPhotoParserTest.java
@@ -2,8 +2,12 @@ package cn.qaiu.parser;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo;
+import cn.qaiu.parser.custom.CustomParserConfig;
+import cn.qaiu.parser.custom.CustomParserRegistry;
+import cn.qaiu.parser.customjs.JsParserExecutor;
import cn.qaiu.WebClientVertxInit;
import io.vertx.core.Vertx;
+import io.vertx.core.json.JsonObject;
import org.junit.Test;
import java.util.HashMap;
diff --git a/parser/src/test/java/cn/qaiu/parser/CustomParserTest.java b/parser/src/test/java/cn/qaiu/parser/CustomParserTest.java
index 22a4e82..b11cb97 100644
--- a/parser/src/test/java/cn/qaiu/parser/CustomParserTest.java
+++ b/parser/src/test/java/cn/qaiu/parser/CustomParserTest.java
@@ -1,6 +1,8 @@
package cn.qaiu.parser;
import cn.qaiu.entity.ShareLinkInfo;
+import cn.qaiu.parser.custom.CustomParserConfig;
+import cn.qaiu.parser.custom.CustomParserRegistry;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import org.junit.After;
diff --git a/parser/src/test/java/cn/qaiu/parser/Demo.java b/parser/src/test/java/cn/qaiu/parser/Demo.java
index b583d22..adc3936 100644
--- a/parser/src/test/java/cn/qaiu/parser/Demo.java
+++ b/parser/src/test/java/cn/qaiu/parser/Demo.java
@@ -1,6 +1,8 @@
package cn.qaiu.parser;
import cn.qaiu.entity.ShareLinkInfo;
+import cn.qaiu.parser.custom.CustomParserConfig;
+import cn.qaiu.parser.custom.CustomParserRegistry;
import io.vertx.core.Future;
import io.vertx.core.Promise;
diff --git a/parser/src/test/java/cn/qaiu/parser/JsParserTest.java b/parser/src/test/java/cn/qaiu/parser/JsParserTest.java
index 163bb2b..88f2a43 100644
--- a/parser/src/test/java/cn/qaiu/parser/JsParserTest.java
+++ b/parser/src/test/java/cn/qaiu/parser/JsParserTest.java
@@ -2,6 +2,9 @@ package cn.qaiu.parser;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo;
+import cn.qaiu.parser.custom.CustomParserConfig;
+import cn.qaiu.parser.custom.CustomParserRegistry;
+import cn.qaiu.parser.customjs.JsParserExecutor;
import cn.qaiu.WebClientVertxInit;
import io.vertx.core.Vertx;
import org.junit.Test;
diff --git a/parser/src/test/java/cn/qaiu/parser/JsScriptLoaderTest.java b/parser/src/test/java/cn/qaiu/parser/JsScriptLoaderTest.java
index ed4c2da..cfecc73 100644
--- a/parser/src/test/java/cn/qaiu/parser/JsScriptLoaderTest.java
+++ b/parser/src/test/java/cn/qaiu/parser/JsScriptLoaderTest.java
@@ -2,6 +2,9 @@ package cn.qaiu.parser;
import org.junit.Test;
+import cn.qaiu.parser.custom.CustomParserConfig;
+import cn.qaiu.parser.customjs.JsScriptLoader;
+
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
diff --git a/pom.xml b/pom.xml
index 4517c93..d06b29d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -60,7 +60,7 @@
cn.qaiu
parser
- 10.1.17
+ 10.2.1