feat: 新增客户端协议生成系统,支持8种主流下载工具

🚀 核心功能
- 新增完整的客户端下载链接生成器系统
- 支持ARIA2、Motrix、比特彗星、迅雷、wget、cURL、IDM、FDM、PowerShell等8种客户端
- 自动处理防盗链参数(User-Agent、Referer、Cookie等)
- 提供可扩展的生成器架构,支持自定义客户端

🔧 技术实现
- ClientLinkGeneratorFactory: 工厂模式管理生成器
- DownloadLinkMeta: 元数据存储下载信息
- ClientLinkUtils: 便捷工具类
- 线程安全的ConcurrentHashMap设计

🌐 前端集成
- 新增ClientLinks.vue界面,支持客户端链接展示
- Element Plus图标系统,混合图标显示
- 客户端检测逻辑优化,避免自动打开外部应用
- 移动端和PC端环境判断

📚 文档完善
- 完整的CLIENT_LINK_GENERATOR_GUIDE.md使用指南
- API文档和测试用例
- 输出示例和最佳实践

从单纯的网盘解析工具升级为完整的下载解决方案生态
This commit is contained in:
q
2025-10-24 09:25:57 +08:00
parent 231d5c3fb9
commit 42b721eabf
47 changed files with 3740 additions and 96 deletions

View File

@@ -1,10 +1,14 @@
package cn.qaiu.parser;//package cn.qaiu.lz.common.parser;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.clientlink.ClientLinkGeneratorFactory;
import cn.qaiu.parser.clientlink.ClientLinkType;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import java.util.List;
import java.util.Map;
public interface IPanTool {
@@ -45,4 +49,92 @@ public interface IPanTool {
default String parseByIdSync() {
return parseById().toCompletionStage().toCompletableFuture().join();
}
/**
* 解析文件并生成客户端下载链接
* @return Future<Map<ClientLinkType, String>> 客户端下载链接集合
*/
default Future<Map<ClientLinkType, String>> parseWithClientLinks() {
Promise<Map<ClientLinkType, String>> promise = Promise.promise();
// 首先尝试获取 ShareLinkInfo
ShareLinkInfo shareLinkInfo = getShareLinkInfo();
if (shareLinkInfo == null) {
promise.fail("无法获取 ShareLinkInfo");
return promise.future();
}
// 检查是否已经有下载链接元数据
String existingDownloadUrl = (String) shareLinkInfo.getOtherParam().get("downloadUrl");
if (existingDownloadUrl != null && !existingDownloadUrl.trim().isEmpty()) {
// 如果已经有下载链接,直接生成客户端链接
try {
Map<ClientLinkType, String> clientLinks =
ClientLinkGeneratorFactory.generateAll(shareLinkInfo);
promise.complete(clientLinks);
return promise.future();
} catch (Exception e) {
// 如果生成失败,继续尝试解析
}
}
// 尝试解析获取下载链接
parse().onComplete(result -> {
if (result.succeeded()) {
try {
String downloadUrl = result.result();
if (downloadUrl != null && !downloadUrl.trim().isEmpty()) {
// 确保下载链接已存储到 otherParam 中
shareLinkInfo.getOtherParam().put("downloadUrl", downloadUrl);
// 生成客户端链接
Map<ClientLinkType, String> clientLinks =
ClientLinkGeneratorFactory.generateAll(shareLinkInfo);
promise.complete(clientLinks);
} else {
promise.fail("解析结果为空,无法生成客户端链接");
}
} catch (Exception e) {
promise.fail("生成客户端链接失败: " + e.getMessage());
}
} else {
// 解析失败时,尝试使用分享链接作为默认下载链接
try {
String fallbackUrl = shareLinkInfo.getShareUrl();
if (fallbackUrl != null && !fallbackUrl.trim().isEmpty()) {
// 使用分享链接作为默认下载链接
shareLinkInfo.getOtherParam().put("downloadUrl", fallbackUrl);
// 尝试生成客户端链接
Map<ClientLinkType, String> clientLinks =
ClientLinkGeneratorFactory.generateAll(shareLinkInfo);
promise.complete(clientLinks);
} else {
promise.fail("解析失败且无法使用分享链接作为默认下载链接: " + result.cause().getMessage());
}
} catch (Exception e) {
promise.fail("解析失败且生成默认客户端链接失败: " + result.cause().getMessage());
}
}
});
return promise.future();
}
/**
* 解析文件并生成客户端下载链接(同步版本)
* @return Map<ClientLinkType, String> 客户端下载链接集合
*/
default Map<ClientLinkType, String> parseWithClientLinksSync() {
return parseWithClientLinks().toCompletionStage().toCompletableFuture().join();
}
/**
* 获取 ShareLinkInfo 对象
* 子类需要实现此方法来提供 ShareLinkInfo
* @return ShareLinkInfo 对象
*/
default ShareLinkInfo getShareLinkInfo() {
return null;
}
}

View File

@@ -5,6 +5,7 @@ import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.util.HttpResponseHelper;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.MultiMap;
import io.vertx.core.Promise;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonObject;
@@ -21,7 +22,9 @@ import org.slf4j.LoggerFactory;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.zip.GZIPInputStream;
/**
@@ -225,9 +228,39 @@ public abstract class PanBase implements IPanTool {
}
protected void complete(String url) {
// 自动将直链存储到 otherParam 中,以便客户端链接生成器使用
shareLinkInfo.getOtherParam().put("downloadUrl", url);
promise.complete(url);
}
/**
* 完成解析并存储下载元数据
*
* @param url 下载直链
* @param headers 请求头Map
*/
protected void completeWithMeta(String url, Map<String, String> headers) {
shareLinkInfo.getOtherParam().put("downloadUrl", url);
if (headers != null && !headers.isEmpty()) {
shareLinkInfo.getOtherParam().put("downloadHeaders", headers);
}
promise.complete(url);
}
/**
* 完成解析并存储下载元数据MultiMap版本
*
* @param url 下载直链
* @param headers MultiMap格式的请求头
*/
protected void completeWithMeta(String url, MultiMap headers) {
Map<String, String> headerMap = new HashMap<>();
if (headers != null) {
headers.forEach(entry -> headerMap.put(entry.getKey(), entry.getValue()));
}
completeWithMeta(url, headerMap);
}
protected Future<String> future() {
return promise.future();
}
@@ -279,4 +312,9 @@ public abstract class PanBase implements IPanTool {
protected String getDomainName(){
return shareLinkInfo.getOtherParam().getOrDefault("domainName", "").toString();
}
@Override
public ShareLinkInfo getShareLinkInfo() {
return shareLinkInfo;
}
}

View File

@@ -73,7 +73,7 @@ public class ParserCreate {
throw new IllegalArgumentException("ShareLinkInfo shareUrl is empty");
}
java.util.regex.Matcher matcher = customParserConfig.getMatchPattern().matcher(shareUrl);
Matcher matcher = customParserConfig.getMatchPattern().matcher(shareUrl);
if (matcher.matches()) {
// 提取分享键
try {
@@ -252,7 +252,7 @@ public class ParserCreate {
// 优先查找支持正则匹配的自定义解析器
for (CustomParserConfig customConfig : CustomParserRegistry.getAll().values()) {
if (customConfig.supportsFromShareUrl()) {
java.util.regex.Matcher matcher = customConfig.getMatchPattern().matcher(shareUrl);
Matcher matcher = customConfig.getMatchPattern().matcher(shareUrl);
if (matcher.matches()) {
ShareLinkInfo shareLinkInfo = ShareLinkInfo.newBuilder()
.type(customConfig.getType())

View File

@@ -0,0 +1,36 @@
package cn.qaiu.parser.clientlink;
/**
* 客户端下载链接生成器接口
*
* @author <a href="https://qaiu.top">QAIU</a>
* Create at 2025/01/21
*/
public interface ClientLinkGenerator {
/**
* 生成客户端下载链接
*
* @param meta 下载链接元数据
* @return 生成的客户端下载链接字符串
*/
String generate(DownloadLinkMeta meta);
/**
* 获取生成器对应的客户端类型
*
* @return ClientLinkType 枚举值
*/
ClientLinkType getType();
/**
* 检查是否支持生成该类型的链接
* 默认实现检查元数据是否有有效的URL
*
* @param meta 下载链接元数据
* @return true 表示支持false 表示不支持
*/
default boolean supports(DownloadLinkMeta meta) {
return meta != null && meta.hasValidUrl();
}
}

View File

@@ -0,0 +1,180 @@
package cn.qaiu.parser.clientlink;
import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.clientlink.impl.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 客户端下载链接生成器工厂类
*
* @author <a href="https://qaiu.top">QAIU</a>
* Create at 2025/01/21
*/
public class ClientLinkGeneratorFactory {
private static final Logger log = LoggerFactory.getLogger(ClientLinkGeneratorFactory.class);
// 存储所有注册的生成器
private static final Map<ClientLinkType, ClientLinkGenerator> generators = new ConcurrentHashMap<>();
// 静态初始化块,注册默认的生成器
static {
try {
// 注册默认生成器 - 按指定顺序注册
register(new Aria2LinkGenerator());
register(new MotrixLinkGenerator());
register(new BitCometLinkGenerator());
register(new ThunderLinkGenerator());
register(new WgetLinkGenerator());
register(new CurlLinkGenerator());
register(new IdmLinkGenerator());
register(new FdmLinkGenerator());
register(new PowerShellLinkGenerator());
log.info("客户端链接生成器工厂初始化完成,已注册 {} 个生成器", generators.size());
} catch (Exception e) {
log.error("初始化客户端链接生成器失败", e);
}
}
/**
* 生成所有类型的客户端链接
*
* @param info ShareLinkInfo 对象
* @return Map<ClientLinkType, String> 格式的客户端链接集合
*/
public static Map<ClientLinkType, String> generateAll(ShareLinkInfo info) {
Map<ClientLinkType, String> result = new LinkedHashMap<>();
if (info == null) {
log.warn("ShareLinkInfo 为空,无法生成客户端链接");
return result;
}
DownloadLinkMeta meta = DownloadLinkMeta.fromShareLinkInfo(info);
if (!meta.hasValidUrl()) {
log.warn("下载链接元数据无效,无法生成客户端链接: {}", meta);
return result;
}
// 按照枚举顺序遍历,保证顺序
for (ClientLinkType type : ClientLinkType.values()) {
ClientLinkGenerator generator = generators.get(type);
if (generator != null) {
try {
if (generator.supports(meta)) {
String link = generator.generate(meta);
if (link != null && !link.trim().isEmpty()) {
result.put(type, link);
}
}
} catch (Exception e) {
log.warn("生成 {} 客户端链接失败: {}", type.getDisplayName(), e.getMessage());
}
}
}
log.debug("成功生成 {} 个客户端链接", result.size());
return result;
}
/**
* 生成指定类型的客户端链接
*
* @param info ShareLinkInfo 对象
* @param type 客户端类型
* @return 生成的客户端链接字符串,失败时返回 null
*/
public static String generate(ShareLinkInfo info, ClientLinkType type) {
if (info == null || type == null) {
log.warn("参数为空,无法生成客户端链接: info={}, type={}", info, type);
return null;
}
ClientLinkGenerator generator = generators.get(type);
if (generator == null) {
log.warn("未找到类型为 {} 的生成器", type.getDisplayName());
return null;
}
try {
DownloadLinkMeta meta = DownloadLinkMeta.fromShareLinkInfo(info);
if (!generator.supports(meta)) {
log.warn("生成器 {} 不支持该元数据", type.getDisplayName());
return null;
}
return generator.generate(meta);
} catch (Exception e) {
log.error("生成 {} 客户端链接失败", type.getDisplayName(), e);
return null;
}
}
/**
* 注册自定义生成器(扩展点)
*
* @param generator 客户端链接生成器
*/
public static void register(ClientLinkGenerator generator) {
if (generator == null) {
log.warn("尝试注册空的生成器");
return;
}
ClientLinkType type = generator.getType();
if (type == null) {
log.warn("生成器的类型为空,无法注册");
return;
}
generators.put(type, generator);
log.info("成功注册客户端链接生成器: {}", type.getDisplayName());
}
/**
* 注销生成器
*
* @param type 客户端类型
* @return 被注销的生成器,如果不存在则返回 null
*/
public static ClientLinkGenerator unregister(ClientLinkType type) {
ClientLinkGenerator removed = generators.remove(type);
if (removed != null) {
log.info("成功注销客户端链接生成器: {}", type.getDisplayName());
}
return removed;
}
/**
* 获取所有已注册的生成器类型
*
* @return 已注册的客户端类型集合
*/
public static Map<ClientLinkType, ClientLinkGenerator> getAllGenerators() {
Map<ClientLinkType, ClientLinkGenerator> result = new LinkedHashMap<>();
// 按照枚举顺序添加,保证顺序
for (ClientLinkType type : ClientLinkType.values()) {
ClientLinkGenerator generator = generators.get(type);
if (generator != null) {
result.put(type, generator);
}
}
return result;
}
/**
* 检查是否已注册指定类型的生成器
*
* @param type 客户端类型
* @return true 表示已注册false 表示未注册
*/
public static boolean isRegistered(ClientLinkType type) {
return generators.containsKey(type);
}
}

View File

@@ -0,0 +1,40 @@
package cn.qaiu.parser.clientlink;
/**
* 客户端下载工具类型枚举
*
* @author <a href="https://qaiu.top">QAIU</a>
* Create at 2025/01/21
*/
public enum ClientLinkType {
ARIA2("aria2", "Aria2"),
MOTRIX("motrix", "Motrix"),
BITCOMET("bitcomet", "比特彗星"),
THUNDER("thunder", "迅雷"),
WGET("wget", "wget 命令"),
CURL("curl", "cURL 命令"),
IDM("idm", "IDM"),
FDM("fdm", "Free Download Manager"),
POWERSHELL("powershell", "PowerShell");
private final String code;
private final String displayName;
ClientLinkType(String code, String displayName) {
this.code = code;
this.displayName = displayName;
}
public String getCode() {
return code;
}
public String getDisplayName() {
return displayName;
}
@Override
public String toString() {
return displayName;
}
}

View File

@@ -0,0 +1,141 @@
package cn.qaiu.parser.clientlink;
import cn.qaiu.entity.ShareLinkInfo;
import java.util.Map;
/**
* 客户端下载链接生成工具类
* 提供便捷的静态方法来生成各种客户端下载链接
*
* @author <a href="https://qaiu.top">QAIU</a>
* Create at 2025/01/21
*/
public class ClientLinkUtils {
/**
* 为 ShareLinkInfo 生成所有类型的客户端下载链接
*
* @param info ShareLinkInfo 对象
* @return Map<ClientLinkType, String> 格式的客户端链接集合
*/
public static Map<ClientLinkType, String> generateAllClientLinks(ShareLinkInfo info) {
return ClientLinkGeneratorFactory.generateAll(info);
}
/**
* 生成指定类型的客户端下载链接
*
* @param info ShareLinkInfo 对象
* @param type 客户端类型
* @return 生成的客户端链接字符串
*/
public static String generateClientLink(ShareLinkInfo info, ClientLinkType type) {
return ClientLinkGeneratorFactory.generate(info, type);
}
/**
* 生成 curl 命令
*
* @param info ShareLinkInfo 对象
* @return curl 命令字符串
*/
public static String generateCurlCommand(ShareLinkInfo info) {
return generateClientLink(info, ClientLinkType.CURL);
}
/**
* 生成 wget 命令
*
* @param info ShareLinkInfo 对象
* @return wget 命令字符串
*/
public static String generateWgetCommand(ShareLinkInfo info) {
return generateClientLink(info, ClientLinkType.WGET);
}
/**
* 生成 aria2 命令
*
* @param info ShareLinkInfo 对象
* @return aria2 命令字符串
*/
public static String generateAria2Command(ShareLinkInfo info) {
return generateClientLink(info, ClientLinkType.ARIA2);
}
/**
* 生成迅雷链接
*
* @param info ShareLinkInfo 对象
* @return 迅雷协议链接
*/
public static String generateThunderLink(ShareLinkInfo info) {
return generateClientLink(info, ClientLinkType.THUNDER);
}
/**
* 生成 IDM 链接
*
* @param info ShareLinkInfo 对象
* @return IDM 协议链接
*/
public static String generateIdmLink(ShareLinkInfo info) {
return generateClientLink(info, ClientLinkType.IDM);
}
/**
* 生成比特彗星链接
*
* @param info ShareLinkInfo 对象
* @return 比特彗星协议链接
*/
public static String generateBitCometLink(ShareLinkInfo info) {
return generateClientLink(info, ClientLinkType.BITCOMET);
}
/**
* 生成 Motrix 导入格式
*
* @param info ShareLinkInfo 对象
* @return Motrix JSON 格式字符串
*/
public static String generateMotrixFormat(ShareLinkInfo info) {
return generateClientLink(info, ClientLinkType.MOTRIX);
}
/**
* 生成 FDM 导入格式
*
* @param info ShareLinkInfo 对象
* @return FDM 格式字符串
*/
public static String generateFdmFormat(ShareLinkInfo info) {
return generateClientLink(info, ClientLinkType.FDM);
}
/**
* 生成 PowerShell 命令
*
* @param info ShareLinkInfo 对象
* @return PowerShell 命令字符串
*/
public static String generatePowerShellCommand(ShareLinkInfo info) {
return generateClientLink(info, ClientLinkType.POWERSHELL);
}
/**
* 检查 ShareLinkInfo 是否包含有效的下载元数据
*
* @param info ShareLinkInfo 对象
* @return true 表示包含有效元数据false 表示不包含
*/
public static boolean hasValidDownloadMeta(ShareLinkInfo info) {
if (info == null || info.getOtherParam() == null) {
return false;
}
Object downloadUrl = info.getOtherParam().get("downloadUrl");
return downloadUrl instanceof String && !((String) downloadUrl).trim().isEmpty();
}
}

View File

@@ -0,0 +1,233 @@
package cn.qaiu.parser.clientlink;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo;
import org.apache.commons.lang3.StringUtils;
import java.util.HashMap;
import java.util.Map;
/**
* 下载链接元数据封装类
* 包含生成客户端下载链接所需的所有信息
*
* @author <a href="https://qaiu.top">QAIU</a>
* Create at 2025/01/21
*/
public class DownloadLinkMeta {
private String url; // 直链
private Map<String, String> headers; // 请求头
private String referer; // Referer
private String userAgent; // User-Agent
private String fileName; // 文件名(可选)
private Map<String, Object> extParams; // 扩展参数
public DownloadLinkMeta() {
this.headers = new HashMap<>();
this.extParams = new HashMap<>();
}
public DownloadLinkMeta(String url) {
this();
this.url = url;
}
/**
* 从 ShareLinkInfo.otherParam 构建 DownloadLinkMeta
*
* @param info ShareLinkInfo 对象
* @return DownloadLinkMeta 实例
*/
public static DownloadLinkMeta fromShareLinkInfo(ShareLinkInfo info) {
DownloadLinkMeta meta = new DownloadLinkMeta();
// 从 otherParam 中提取元数据
Map<String, Object> otherParam = info.getOtherParam();
// 获取直链 - 优先从 downloadUrl 获取,如果没有则尝试从解析结果获取
Object downloadUrl = otherParam.get("downloadUrl");
if (downloadUrl instanceof String && StringUtils.isNotEmpty((String) downloadUrl)) {
meta.setUrl((String) downloadUrl);
} else {
// 如果没有存储的 downloadUrl尝试从解析结果中获取
// 这里假设解析器会将直链存储在 otherParam 的某个字段中
// 或者我们可以从 ShareLinkInfo 的其他字段中获取
String directLink = extractDirectLinkFromInfo(info);
if (StringUtils.isNotEmpty(directLink)) {
meta.setUrl(directLink);
} else {
// 如果仍然没有找到直链,使用分享链接作为默认下载链接
String shareUrl = info.getShareUrl();
if (StringUtils.isNotEmpty(shareUrl)) {
meta.setUrl(shareUrl);
}
}
}
// 获取请求头
Object downloadHeaders = otherParam.get("downloadHeaders");
if (downloadHeaders instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, String> headerMap = (Map<String, String>) downloadHeaders;
meta.setHeaders(headerMap);
}
// 获取 Referer
Object downloadReferer = otherParam.get("downloadReferer");
if (downloadReferer instanceof String) {
meta.setReferer((String) downloadReferer);
}
// 获取文件名(从 fileInfo 中提取)
Object fileInfo = otherParam.get("fileInfo");
if (fileInfo instanceof FileInfo) {
FileInfo fi = (FileInfo) fileInfo;
if (StringUtils.isNotEmpty(fi.getFileName())) {
meta.setFileName(fi.getFileName());
}
}
// 从请求头中提取 User-Agent 和 Referer如果单独存储的话
if (meta.getHeaders() != null) {
String ua = meta.getHeaders().get("User-Agent");
if (StringUtils.isNotEmpty(ua)) {
meta.setUserAgent(ua);
}
String ref = meta.getHeaders().get("Referer");
if (StringUtils.isNotEmpty(ref) && StringUtils.isEmpty(meta.getReferer())) {
meta.setReferer(ref);
}
}
// 如果没有 User-Agent设置默认的 User-Agent
if (StringUtils.isEmpty(meta.getUserAgent())) {
meta.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
}
return meta;
}
/**
* 从 ShareLinkInfo 中提取直链
* 尝试从各种可能的字段中获取直链
*
* @param info ShareLinkInfo 对象
* @return 直链URL如果找不到则返回 null
*/
private static String extractDirectLinkFromInfo(ShareLinkInfo info) {
Map<String, Object> otherParam = info.getOtherParam();
// 尝试从各种可能的字段中获取直链
String[] possibleKeys = {
"directLink", "downloadUrl", "url", "link",
"download_link", "direct_link", "fileUrl", "file_url"
};
for (String key : possibleKeys) {
Object value = otherParam.get(key);
if (value instanceof String && StringUtils.isNotEmpty((String) value)) {
return (String) value;
}
}
return null;
}
// Getter 和 Setter 方法
public String getUrl() {
return url;
}
public DownloadLinkMeta setUrl(String url) {
this.url = url;
return this;
}
public Map<String, String> getHeaders() {
return headers;
}
public DownloadLinkMeta setHeaders(Map<String, String> headers) {
this.headers = headers != null ? headers : new HashMap<>();
return this;
}
public String getReferer() {
return referer;
}
public DownloadLinkMeta setReferer(String referer) {
this.referer = referer;
return this;
}
public String getUserAgent() {
return userAgent;
}
public DownloadLinkMeta setUserAgent(String userAgent) {
this.userAgent = userAgent;
return this;
}
public String getFileName() {
return fileName;
}
public DownloadLinkMeta setFileName(String fileName) {
this.fileName = fileName;
return this;
}
public Map<String, Object> getExtParams() {
return extParams;
}
public DownloadLinkMeta setExtParams(Map<String, Object> extParams) {
this.extParams = extParams != null ? extParams : new HashMap<>();
return this;
}
/**
* 添加请求头
*/
public DownloadLinkMeta addHeader(String name, String value) {
if (this.headers == null) {
this.headers = new HashMap<>();
}
this.headers.put(name, value);
return this;
}
/**
* 添加扩展参数
*/
public DownloadLinkMeta addExtParam(String key, Object value) {
if (this.extParams == null) {
this.extParams = new HashMap<>();
}
this.extParams.put(key, value);
return this;
}
/**
* 检查是否有有效的下载链接
*/
public boolean hasValidUrl() {
return StringUtils.isNotEmpty(url);
}
@Override
public String toString() {
return "DownloadLinkMeta{" +
"url='" + url + '\'' +
", fileName='" + fileName + '\'' +
", headers=" + headers +
", referer='" + referer + '\'' +
", userAgent='" + userAgent + '\'' +
'}';
}
}

View File

@@ -0,0 +1,55 @@
package cn.qaiu.parser.clientlink.impl;
import cn.qaiu.parser.clientlink.ClientLinkGenerator;
import cn.qaiu.parser.clientlink.ClientLinkType;
import cn.qaiu.parser.clientlink.DownloadLinkMeta;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Aria2 命令生成器
*
* @author <a href="https://qaiu.top">QAIU</a>
* Create at 2025/01/21
*/
public class Aria2LinkGenerator implements ClientLinkGenerator {
@Override
public String generate(DownloadLinkMeta meta) {
if (!supports(meta)) {
return null;
}
List<String> parts = new ArrayList<>();
parts.add("aria2c");
// 添加请求头
if (meta.getHeaders() != null && !meta.getHeaders().isEmpty()) {
for (Map.Entry<String, String> entry : meta.getHeaders().entrySet()) {
parts.add("--header=\"" + entry.getKey() + ": " + entry.getValue() + "\"");
}
}
// 设置输出文件名
if (meta.getFileName() != null && !meta.getFileName().trim().isEmpty()) {
parts.add("--out=\"" + meta.getFileName() + "\"");
}
// 添加其他常用参数
parts.add("--continue"); // 支持断点续传
parts.add("--max-tries=3"); // 最大重试次数
parts.add("--retry-wait=5"); // 重试等待时间
// 添加URL
parts.add("\"" + meta.getUrl() + "\"");
return String.join(" \\\n ", parts);
}
@Override
public ClientLinkType getType() {
return ClientLinkType.ARIA2;
}
}

View File

@@ -0,0 +1,69 @@
package cn.qaiu.parser.clientlink.impl;
import cn.qaiu.parser.clientlink.ClientLinkGenerator;
import cn.qaiu.parser.clientlink.ClientLinkType;
import cn.qaiu.parser.clientlink.DownloadLinkMeta;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Map;
/**
* 比特彗星协议链接生成器
*
* @author <a href="https://qaiu.top">QAIU</a>
* Create at 2025/01/21
*/
public class BitCometLinkGenerator implements ClientLinkGenerator {
@Override
public String generate(DownloadLinkMeta meta) {
if (!supports(meta)) {
return null;
}
try {
// 比特彗星支持 HTTP 下载,格式类似 IDM
String encodedUrl = Base64.getEncoder().encodeToString(
meta.getUrl().getBytes(StandardCharsets.UTF_8)
);
StringBuilder link = new StringBuilder("bitcomet:///?url=").append(encodedUrl);
// 添加请求头
if (meta.getHeaders() != null && !meta.getHeaders().isEmpty()) {
StringBuilder headerStr = new StringBuilder();
for (Map.Entry<String, String> entry : meta.getHeaders().entrySet()) {
if (headerStr.length() > 0) {
headerStr.append("\\r\\n");
}
headerStr.append(entry.getKey()).append(": ").append(entry.getValue());
}
String encodedHeaders = Base64.getEncoder().encodeToString(
headerStr.toString().getBytes(StandardCharsets.UTF_8)
);
link.append("&header=").append(encodedHeaders);
}
// 添加文件名
if (meta.getFileName() != null && !meta.getFileName().trim().isEmpty()) {
String encodedFileName = Base64.getEncoder().encodeToString(
meta.getFileName().getBytes(StandardCharsets.UTF_8)
);
link.append("&filename=").append(encodedFileName);
}
return link.toString();
} catch (Exception e) {
// 如果编码失败返回简单的URL
return "bitcomet:///?url=" + meta.getUrl();
}
}
@Override
public ClientLinkType getType() {
return ClientLinkType.BITCOMET;
}
}

View File

@@ -0,0 +1,53 @@
package cn.qaiu.parser.clientlink.impl;
import cn.qaiu.parser.clientlink.ClientLinkGenerator;
import cn.qaiu.parser.clientlink.ClientLinkType;
import cn.qaiu.parser.clientlink.DownloadLinkMeta;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* cURL 命令生成器
*
* @author <a href="https://qaiu.top">QAIU</a>
* Create at 2025/01/21
*/
public class CurlLinkGenerator implements ClientLinkGenerator {
@Override
public String generate(DownloadLinkMeta meta) {
if (!supports(meta)) {
return null;
}
List<String> parts = new ArrayList<>();
parts.add("curl");
parts.add("-L"); // 跟随重定向
// 添加请求头
if (meta.getHeaders() != null && !meta.getHeaders().isEmpty()) {
for (Map.Entry<String, String> entry : meta.getHeaders().entrySet()) {
parts.add("-H");
parts.add("\"" + entry.getKey() + ": " + entry.getValue() + "\"");
}
}
// 设置输出文件名
if (meta.getFileName() != null && !meta.getFileName().trim().isEmpty()) {
parts.add("-o");
parts.add("\"" + meta.getFileName() + "\"");
}
// 添加URL
parts.add("\"" + meta.getUrl() + "\"");
return String.join(" \\\n ", parts);
}
@Override
public ClientLinkType getType() {
return ClientLinkType.CURL;
}
}

View File

@@ -0,0 +1,56 @@
package cn.qaiu.parser.clientlink.impl;
import cn.qaiu.parser.clientlink.ClientLinkGenerator;
import cn.qaiu.parser.clientlink.ClientLinkType;
import cn.qaiu.parser.clientlink.DownloadLinkMeta;
import java.util.Map;
/**
* Free Download Manager 导入格式生成器
*
* @author <a href="https://qaiu.top">QAIU</a>
* Create at 2025/01/21
*/
public class FdmLinkGenerator implements ClientLinkGenerator {
@Override
public String generate(DownloadLinkMeta meta) {
if (!supports(meta)) {
return null;
}
// FDM 支持简单的文本格式导入
StringBuilder result = new StringBuilder();
result.append("URL=").append(meta.getUrl()).append("\n");
// 添加文件名
if (meta.getFileName() != null && !meta.getFileName().trim().isEmpty()) {
result.append("Filename=").append(meta.getFileName()).append("\n");
}
// 添加请求头
if (meta.getHeaders() != null && !meta.getHeaders().isEmpty()) {
result.append("Headers=");
boolean first = true;
for (Map.Entry<String, String> entry : meta.getHeaders().entrySet()) {
if (!first) {
result.append("; ");
}
result.append(entry.getKey()).append(": ").append(entry.getValue());
first = false;
}
result.append("\n");
}
result.append("Referer=").append(meta.getReferer() != null ? meta.getReferer() : "").append("\n");
result.append("User-Agent=").append(meta.getUserAgent() != null ? meta.getUserAgent() : "").append("\n");
return result.toString();
}
@Override
public ClientLinkType getType() {
return ClientLinkType.FDM;
}
}

View File

@@ -0,0 +1,69 @@
package cn.qaiu.parser.clientlink.impl;
import cn.qaiu.parser.clientlink.ClientLinkGenerator;
import cn.qaiu.parser.clientlink.ClientLinkType;
import cn.qaiu.parser.clientlink.DownloadLinkMeta;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Map;
/**
* IDM 协议链接生成器
*
* @author <a href="https://qaiu.top">QAIU</a>
* Create at 2025/01/21
*/
public class IdmLinkGenerator implements ClientLinkGenerator {
@Override
public String generate(DownloadLinkMeta meta) {
if (!supports(meta)) {
return null;
}
try {
// 对URL进行Base64编码
String encodedUrl = Base64.getEncoder().encodeToString(
meta.getUrl().getBytes(StandardCharsets.UTF_8)
);
StringBuilder link = new StringBuilder("idm:///?url=").append(encodedUrl);
// 添加请求头
if (meta.getHeaders() != null && !meta.getHeaders().isEmpty()) {
StringBuilder headerStr = new StringBuilder();
for (Map.Entry<String, String> entry : meta.getHeaders().entrySet()) {
if (headerStr.length() > 0) {
headerStr.append("\\r\\n");
}
headerStr.append(entry.getKey()).append(": ").append(entry.getValue());
}
String encodedHeaders = Base64.getEncoder().encodeToString(
headerStr.toString().getBytes(StandardCharsets.UTF_8)
);
link.append("&header=").append(encodedHeaders);
}
// 添加文件名
if (meta.getFileName() != null && !meta.getFileName().trim().isEmpty()) {
String encodedFileName = Base64.getEncoder().encodeToString(
meta.getFileName().getBytes(StandardCharsets.UTF_8)
);
link.append("&filename=").append(encodedFileName);
}
return link.toString();
} catch (Exception e) {
// 如果编码失败返回简单的URL
return "idm:///?url=" + meta.getUrl();
}
}
@Override
public ClientLinkType getType() {
return ClientLinkType.IDM;
}
}

View File

@@ -0,0 +1,53 @@
package cn.qaiu.parser.clientlink.impl;
import cn.qaiu.parser.clientlink.ClientLinkGenerator;
import cn.qaiu.parser.clientlink.ClientLinkType;
import cn.qaiu.parser.clientlink.DownloadLinkMeta;
import io.vertx.core.json.JsonObject;
import java.util.Map;
/**
* Motrix 导入格式生成器
*
* @author <a href="https://qaiu.top">QAIU</a>
* Create at 2025/01/21
*/
public class MotrixLinkGenerator implements ClientLinkGenerator {
@Override
public String generate(DownloadLinkMeta meta) {
if (!supports(meta)) {
return null;
}
// 使用 Vert.x JsonObject 构建 JSON
JsonObject taskJson = new JsonObject();
taskJson.put("url", meta.getUrl());
// 添加文件名
if (meta.getFileName() != null && !meta.getFileName().trim().isEmpty()) {
taskJson.put("filename", meta.getFileName());
}
// 添加请求头
if (meta.getHeaders() != null && !meta.getHeaders().isEmpty()) {
JsonObject headersJson = new JsonObject();
for (Map.Entry<String, String> entry : meta.getHeaders().entrySet()) {
headersJson.put(entry.getKey(), entry.getValue());
}
taskJson.put("headers", headersJson);
}
// 设置输出文件名
String outputFile = meta.getFileName() != null ? meta.getFileName() : "";
taskJson.put("out", outputFile);
return taskJson.encodePrettily();
}
@Override
public ClientLinkType getType() {
return ClientLinkType.MOTRIX;
}
}

View File

@@ -0,0 +1,98 @@
package cn.qaiu.parser.clientlink.impl;
import cn.qaiu.parser.clientlink.ClientLinkGenerator;
import cn.qaiu.parser.clientlink.ClientLinkType;
import cn.qaiu.parser.clientlink.DownloadLinkMeta;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* PowerShell 命令生成器
*
* @author <a href="https://qaiu.top">QAIU</a>
* Create at 2025/01/21
*/
public class PowerShellLinkGenerator implements ClientLinkGenerator {
@Override
public String generate(DownloadLinkMeta meta) {
if (!supports(meta)) {
return null;
}
List<String> lines = new ArrayList<>();
// 创建 WebRequestSession
lines.add("$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession");
// 设置 User-Agent如果存在
String userAgent = meta.getUserAgent();
if (userAgent == null && meta.getHeaders() != null) {
userAgent = meta.getHeaders().get("User-Agent");
}
if (userAgent != null && !userAgent.trim().isEmpty()) {
lines.add("$session.UserAgent = \"" + escapePowerShellString(userAgent) + "\"");
}
// 构建 Invoke-WebRequest 命令
List<String> invokeParams = new ArrayList<>();
invokeParams.add("Invoke-WebRequest");
invokeParams.add("-UseBasicParsing");
invokeParams.add("-Uri \"" + escapePowerShellString(meta.getUrl()) + "\"");
// 添加 WebSession
invokeParams.add("-WebSession $session");
// 添加请求头
if (meta.getHeaders() != null && !meta.getHeaders().isEmpty()) {
List<String> headerLines = new ArrayList<>();
headerLines.add("-Headers @{");
boolean first = true;
for (Map.Entry<String, String> entry : meta.getHeaders().entrySet()) {
if (!first) {
headerLines.add("");
}
headerLines.add(" \"" + escapePowerShellString(entry.getKey()) + "\"=\"" +
escapePowerShellString(entry.getValue()) + "\"");
first = false;
}
headerLines.add("}");
// 将头部参数添加到主命令中
invokeParams.add(String.join("`\n", headerLines));
}
// 设置输出文件(如果指定了文件名)
if (meta.getFileName() != null && !meta.getFileName().trim().isEmpty()) {
invokeParams.add("-OutFile \"" + escapePowerShellString(meta.getFileName()) + "\"");
}
// 将所有参数连接起来
String invokeCommand = String.join(" `\n", invokeParams);
lines.add(invokeCommand);
return String.join("\n", lines);
}
/**
* 转义 PowerShell 字符串中的特殊字符
*/
private String escapePowerShellString(String str) {
if (str == null) {
return "";
}
return str.replace("`", "``")
.replace("\"", "`\"")
.replace("$", "`$");
}
@Override
public ClientLinkType getType() {
return ClientLinkType.POWERSHELL;
}
}

View File

@@ -0,0 +1,46 @@
package cn.qaiu.parser.clientlink.impl;
import cn.qaiu.parser.clientlink.ClientLinkGenerator;
import cn.qaiu.parser.clientlink.ClientLinkType;
import cn.qaiu.parser.clientlink.DownloadLinkMeta;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
/**
* 迅雷协议链接生成器
*
* @author <a href="https://qaiu.top">QAIU</a>
* Create at 2025/01/21
*/
public class ThunderLinkGenerator implements ClientLinkGenerator {
@Override
public String generate(DownloadLinkMeta meta) {
if (!supports(meta)) {
return null;
}
try {
// 迅雷链接格式thunder://Base64(AA + 原URL + ZZ)
String originalUrl = meta.getUrl();
String thunderUrl = "AA" + originalUrl + "ZZ";
// Base64编码
String encodedUrl = Base64.getEncoder().encodeToString(
thunderUrl.getBytes(StandardCharsets.UTF_8)
);
return "thunder://" + encodedUrl;
} catch (Exception e) {
// 如果编码失败返回null
return null;
}
}
@Override
public ClientLinkType getType() {
return ClientLinkType.THUNDER;
}
}

View File

@@ -0,0 +1,51 @@
package cn.qaiu.parser.clientlink.impl;
import cn.qaiu.parser.clientlink.ClientLinkGenerator;
import cn.qaiu.parser.clientlink.ClientLinkType;
import cn.qaiu.parser.clientlink.DownloadLinkMeta;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* wget 命令生成器
*
* @author <a href="https://qaiu.top">QAIU</a>
* Create at 2025/01/21
*/
public class WgetLinkGenerator implements ClientLinkGenerator {
@Override
public String generate(DownloadLinkMeta meta) {
if (!supports(meta)) {
return null;
}
List<String> parts = new ArrayList<>();
parts.add("wget");
// 添加请求头
if (meta.getHeaders() != null && !meta.getHeaders().isEmpty()) {
for (Map.Entry<String, String> entry : meta.getHeaders().entrySet()) {
parts.add("--header=\"" + entry.getKey() + ": " + entry.getValue() + "\"");
}
}
// 设置输出文件名
if (meta.getFileName() != null && !meta.getFileName().trim().isEmpty()) {
parts.add("-O");
parts.add("\"" + meta.getFileName() + "\"");
}
// 添加URL
parts.add("\"" + meta.getUrl() + "\"");
return String.join(" \\\n ", parts);
}
@Override
public ClientLinkType getType() {
return ClientLinkType.WGET;
}
}

View File

@@ -0,0 +1,145 @@
package cn.qaiu.parser.clientlink.util;
import java.util.Map;
/**
* 请求头格式化工具类
*
* @author <a href="https://qaiu.top">QAIU</a>
* Create at 2025/01/21
*/
public class HeaderFormatter {
/**
* 将请求头格式化为 curl 格式
*
* @param headers 请求头Map
* @return curl 格式的请求头字符串
*/
public static String formatForCurl(Map<String, String> headers) {
if (headers == null || headers.isEmpty()) {
return "";
}
StringBuilder result = new StringBuilder();
for (Map.Entry<String, String> entry : headers.entrySet()) {
if (result.length() > 0) {
result.append(" \\\n ");
}
result.append("-H \"").append(entry.getKey()).append(": ").append(entry.getValue()).append("\"");
}
return result.toString();
}
/**
* 将请求头格式化为 wget 格式
*
* @param headers 请求头Map
* @return wget 格式的请求头字符串
*/
public static String formatForWget(Map<String, String> headers) {
if (headers == null || headers.isEmpty()) {
return "";
}
StringBuilder result = new StringBuilder();
for (Map.Entry<String, String> entry : headers.entrySet()) {
if (result.length() > 0) {
result.append(" \\\n ");
}
result.append("--header=\"").append(entry.getKey()).append(": ").append(entry.getValue()).append("\"");
}
return result.toString();
}
/**
* 将请求头格式化为 aria2 格式
*
* @param headers 请求头Map
* @return aria2 格式的请求头字符串
*/
public static String formatForAria2(Map<String, String> headers) {
if (headers == null || headers.isEmpty()) {
return "";
}
StringBuilder result = new StringBuilder();
for (Map.Entry<String, String> entry : headers.entrySet()) {
if (result.length() > 0) {
result.append(" \\\n ");
}
result.append("--header=\"").append(entry.getKey()).append(": ").append(entry.getValue()).append("\"");
}
return result.toString();
}
/**
* 将请求头格式化为 HTTP 头格式(用于 Base64 编码)
*
* @param headers 请求头Map
* @return HTTP 头格式的字符串
*/
public static String formatForHttpHeaders(Map<String, String> headers) {
if (headers == null || headers.isEmpty()) {
return "";
}
StringBuilder result = new StringBuilder();
for (Map.Entry<String, String> entry : headers.entrySet()) {
if (result.length() > 0) {
result.append("\\r\\n");
}
result.append(entry.getKey()).append(": ").append(entry.getValue());
}
return result.toString();
}
/**
* 将请求头格式化为 JSON 格式
*
* @param headers 请求头Map
* @return JSON 格式的请求头字符串
*/
public static String formatForJson(Map<String, String> headers) {
if (headers == null || headers.isEmpty()) {
return "{}";
}
StringBuilder result = new StringBuilder();
result.append("{\n");
boolean first = true;
for (Map.Entry<String, String> entry : headers.entrySet()) {
if (!first) {
result.append(",\n");
}
result.append(" \"").append(entry.getKey()).append("\": \"")
.append(entry.getValue()).append("\"");
first = false;
}
result.append("\n }");
return result.toString();
}
/**
* 将请求头格式化为简单键值对格式(用于 FDM
*
* @param headers 请求头Map
* @return 简单键值对格式的字符串
*/
public static String formatForSimple(Map<String, String> headers) {
if (headers == null || headers.isEmpty()) {
return "";
}
StringBuilder result = new StringBuilder();
for (Map.Entry<String, String> entry : headers.entrySet()) {
if (result.length() > 0) {
result.append("; ");
}
result.append(entry.getKey()).append(": ").append(entry.getValue());
}
return result.toString();
}
}

View File

@@ -20,6 +20,7 @@ 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.concurrent.TimeUnit;
@@ -347,7 +348,7 @@ public class JsHttpClient {
*/
public Map<String, String> headers() {
MultiMap responseHeaders = response.headers();
Map<String, String> result = new java.util.HashMap<>();
Map<String, String> result = new HashMap<>();
for (String name : responseHeaders.names()) {
result.put(name, responseHeaders.get(name));
}

View File

@@ -12,7 +12,10 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Stream;
/**
@@ -136,11 +139,11 @@ public class JsScriptLoader {
try {
String jarPath = jarUrl.getPath().substring(5, jarUrl.getPath().indexOf("!"));
java.util.jar.JarFile jarFile = new java.util.jar.JarFile(jarPath);
JarFile jarFile = new JarFile(jarPath);
java.util.Enumeration<java.util.jar.JarEntry> entries = jarFile.entries();
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
java.util.jar.JarEntry entry = entries.nextElement();
JarEntry entry = entries.nextElement();
String entryName = entry.getName();
if (entryName.startsWith(RESOURCE_PATH + "/") &&

View File

@@ -6,6 +6,9 @@ import io.vertx.core.Future;
import io.vertx.core.json.JsonObject;
import org.apache.commons.lang3.StringUtils;
import java.util.HashMap;
import java.util.Map;
/**
* 奶牛快传解析工具
*
@@ -46,7 +49,14 @@ public class CowTool extends PanBase {
String downloadUrl = data2.getString("downloadUrl");
if (StringUtils.isNotEmpty(downloadUrl)) {
log.info("cow parse success: {}", downloadUrl);
promise.complete(downloadUrl);
// 存储下载元数据,包括必要的请求头
Map<String, String> headers = new HashMap<>();
headers.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
headers.put("Referer", shareLinkInfo.getShareUrl());
// 使用新的 completeWithMeta 方法存储元数据
completeWithMeta(downloadUrl, headers);
return;
}
fail("cow parse fail: {}; downloadUrl is empty", url2);

View File

@@ -9,9 +9,8 @@ import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.client.HttpRequest;
import io.vertx.uritemplate.UriTemplate;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.regex.Pattern;
import java.util.HashMap;
import java.util.Map;
/**
* <a href="https://www.ctfile.com">诚通网盘</a>
@@ -88,7 +87,15 @@ public class CtTool extends PanBase {
.send().onSuccess(res2 -> {
JsonObject resJson2 = asJson(res2);
if (resJson2.containsKey("downurl")) {
promise.complete(resJson2.getString("downurl"));
String downloadUrl = resJson2.getString("downurl");
// 存储下载元数据,包括必要的请求头
Map<String, String> headers = new HashMap<>();
headers.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
headers.put("Referer", shareLinkInfo.getShareUrl());
// 使用新的 completeWithMeta 方法
completeWithMeta(downloadUrl, headers);
} else {
fail("解析失败, 可能分享已失效: json: {} 字段 {} 不存在", resJson2, "downurl");
}

View File

@@ -150,7 +150,7 @@ public class PvyyTool extends PanBase {
// var arr = asJson(res2).getJsonObject("data").getJsonArray("data");
// List<FileInfo> list = arr.stream().map(o -> {
// FileInfo fileInfo = new FileInfo();
// var jo = ((io.vertx.core.json.JsonObject) o).getJsonObject("data");
// var jo = ((JsonObject) o).getJsonObject("data");
// String fileType = jo.getString("type");
// fileInfo.setFileId(jo.getString("id"));
// fileInfo.setFileName(jo.getJsonObject("attributes").getString("name"));

View File

@@ -5,6 +5,9 @@ import cn.qaiu.parser.PanBase;
import io.vertx.core.Future;
import io.vertx.core.json.JsonObject;
import java.util.HashMap;
import java.util.Map;
/**
* <a href="https://www.kdocs.cn/">WPS云文档</a>
* 分享格式https://www.kdocs.cn/l/ck0azivLlDi3
@@ -38,7 +41,15 @@ public class PwpsTool extends PanBase {
if (downloadUrl != null && !downloadUrl.isEmpty()) {
log.info("WPS云文档解析成功: shareKey={}, downloadUrl={}", shareKey, downloadUrl);
promise.complete(downloadUrl);
// 存储下载元数据,包括必要的请求头
Map<String, String> headers = new HashMap<>();
headers.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
headers.put("Referer", shareLinkInfo.getShareUrl());
// 使用新的 completeWithMeta 方法存储元数据
completeWithMeta(downloadUrl, headers);
return;
} else {
fail("download_url字段为空");
}