feat(v0.2.1): 添加认证参数支持和客户端下载命令生成

主要更新:
- 新增 auth 参数加密传递支持 (QK/UC Cookie认证)
- 实现下载命令自动生成 (curl/aria2c/迅雷)
- aria2c 命令支持 8 线程 8 片段下载
- 修复 cookie 字段映射问题
- 优化前端 clientLinks 页面
- 添加认证参数文档和测试用例
- 更新 .gitignore 忽略编译目录
This commit is contained in:
q
2026-02-05 20:35:47 +08:00
parent 7fc6367b9e
commit 3a25e5f2ae
53 changed files with 6881 additions and 1470 deletions
@@ -0,0 +1,185 @@
package cn.qaiu.util;
import java.util.*;
/**
* Cookie 工具类
* 用于过滤和处理 Cookie 字符串
*/
public class CookieUtils {
/**
* UC/夸克网盘常用的 Cookie 字段
*/
public static final List<String> UC_QUARK_COOKIE_KEYS = Arrays.asList(
"__pus", // 主要的用户会话标识(最重要)
"__kp", // 用户标识
"__kps", // 会话密钥
"__ktd", // 会话令牌
"__uid", // 用户ID
"__puus" // 用户会话签名
);
/**
* 根据指定的 key 列表过滤 cookie
*
* @param cookieStr 原始 cookie 字符串,格式如 "key1=value1; key2=value2"
* @param keys 需要保留的 cookie key 列表
* @return 过滤后的 cookie 字符串,只包含指定的 key
*/
public static String filterCookie(String cookieStr, List<String> keys) {
if (cookieStr == null || cookieStr.isEmpty()) {
return "";
}
if (keys == null || keys.isEmpty()) {
return cookieStr;
}
// 将 keys 转为 Set 以提高查找效率
Set<String> keySet = new HashSet<>(keys);
StringBuilder result = new StringBuilder();
String[] cookies = cookieStr.split(";\\s*");
for (String cookie : cookies) {
if (cookie.isEmpty()) {
continue;
}
// 提取 cookie 的 key
int equalIndex = cookie.indexOf('=');
if (equalIndex > 0) {
String key = cookie.substring(0, equalIndex).trim();
// 如果 key 在需要的列表中,保留这个 cookie
if (keySet.contains(key)) {
if (result.length() > 0) {
result.append("; ");
}
result.append(cookie);
}
}
}
return result.toString();
}
/**
* 使用 UC/夸克网盘默认的 cookie 字段过滤
*
* @param cookieStr 原始 cookie 字符串
* @return 过滤后的 cookie 字符串
*/
public static String filterUcQuarkCookie(String cookieStr) {
return filterCookie(cookieStr, UC_QUARK_COOKIE_KEYS);
}
/**
* 从 cookie 字符串中提取指定 key 的值
*
* @param cookieStr cookie 字符串
* @param key 要提取的 cookie key
* @return cookie 值,如果不存在返回 null
*/
public static String getCookieValue(String cookieStr, String key) {
if (cookieStr == null || cookieStr.isEmpty() || key == null) {
return null;
}
String[] cookies = cookieStr.split(";\\s*");
for (String cookie : cookies) {
if (cookie.startsWith(key + "=")) {
int equalIndex = cookie.indexOf('=');
if (equalIndex > 0 && equalIndex < cookie.length() - 1) {
return cookie.substring(equalIndex + 1);
}
}
}
return null;
}
/**
* 检查 cookie 字符串中是否包含指定的 key
*
* @param cookieStr cookie 字符串
* @param key 要检查的 cookie key
* @return true 表示包含该 key
*/
public static boolean containsKey(String cookieStr, String key) {
return getCookieValue(cookieStr, key) != null;
}
/**
* 更新 cookie 字符串中的指定 cookie 值
*
* @param cookieStr 原始 cookie 字符串
* @param cookieName cookie 名称
* @param newValue 新的完整 cookie 值(格式:cookieName=value)
* @return 更新后的 cookie 字符串
*/
public static String updateCookieValue(String cookieStr, String cookieName, String newValue) {
if (cookieStr == null || cookieStr.isEmpty()) {
return newValue;
}
StringBuilder result = new StringBuilder();
String[] cookies = cookieStr.split(";\\s*");
boolean found = false;
for (String cookie : cookies) {
if (cookie.startsWith(cookieName + "=")) {
// 替换为新值
if (result.length() > 0) result.append("; ");
result.append(newValue);
found = true;
} else if (!cookie.isEmpty()) {
if (result.length() > 0) result.append("; ");
result.append(cookie);
}
}
// 如果原来没有这个 cookie,添加它
if (!found) {
if (result.length() > 0) result.append("; ");
result.append(newValue);
}
return result.toString();
}
/**
* 合并多个 cookie 字符串,后面的会覆盖前面的同名 cookie
*
* @param cookieStrings cookie 字符串数组
* @return 合并后的 cookie 字符串
*/
public static String mergeCookies(String... cookieStrings) {
if (cookieStrings == null || cookieStrings.length == 0) {
return "";
}
Map<String, String> cookieMap = new LinkedHashMap<>();
for (String cookieStr : cookieStrings) {
if (cookieStr == null || cookieStr.isEmpty()) {
continue;
}
String[] cookies = cookieStr.split(";\\s*");
for (String cookie : cookies) {
if (cookie.isEmpty()) {
continue;
}
int equalIndex = cookie.indexOf('=');
if (equalIndex > 0) {
String key = cookie.substring(0, equalIndex).trim();
cookieMap.put(key, cookie);
}
}
}
return String.join("; ", cookieMap.values());
}
}
@@ -0,0 +1,111 @@
package cn.qaiu.util;
import java.time.*;
import java.time.format.DateTimeFormatter;
/**
* 日期时间工具类,用于转换各种时间戳格式为可读的日期字符串
*/
public class DateTimeUtils {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter ISO_FORMATTER = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
/**
* 将毫秒时间戳转换为 yyyy-MM-dd HH:mm:ss 格式
* @param millis 毫秒时间戳
* @return 格式化后的日期字符串
*/
public static String formatMillisToDateTime(long millis) {
return FORMATTER.format(Instant.ofEpochMilli(millis).atZone(ZoneId.systemDefault()).toLocalDateTime());
}
/**
* 将毫秒时间戳字符串转换为 yyyy-MM-dd HH:mm:ss 格式
* @param millisStr 毫秒时间戳字符串(如 "1684737715067")
* @return 格式化后的日期字符串
*/
public static String formatMillisStringToDateTime(String millisStr) {
if (millisStr == null || millisStr.trim().isEmpty()) {
return "";
}
try {
long millis = Long.parseLong(millisStr.trim());
return formatMillisToDateTime(millis);
} catch (NumberFormatException e) {
// 如果解析失败,返回原始值
return millisStr;
}
}
/**
* 将秒级时间戳转换为 yyyy-MM-dd HH:mm:ss 格式
* @param seconds 秒级时间戳
* @return 格式化后的日期字符串
*/
public static String formatSecondsToDateTime(long seconds) {
return FORMATTER.format(Instant.ofEpochSecond(seconds).atZone(ZoneId.systemDefault()).toLocalDateTime());
}
/**
* 将秒级时间戳字符串转换为 yyyy-MM-dd HH:mm:ss 格式
* @param secondsStr 秒级时间戳字符串
* @return 格式化后的日期字符串
*/
public static String formatSecondsStringToDateTime(String secondsStr) {
if (secondsStr == null || secondsStr.trim().isEmpty()) {
return "";
}
try {
long seconds = Long.parseLong(secondsStr.trim());
return formatSecondsToDateTime(seconds);
} catch (NumberFormatException e) {
// 如果解析失败,返回原始值
return secondsStr;
}
}
/**
* 智能转换时间戳:自动判断是毫秒还是秒级
* 根据值的大小判断:如果大于等于 10000000000(即 2286-11-20),视为毫秒;否则视为秒级
* @param timestamp 时间戳字符串
* @return 格式化后的日期字符串
*/
public static String formatTimestampToDateTime(String timestamp) {
if (timestamp == null || timestamp.trim().isEmpty()) {
return "";
}
try {
long value = Long.parseLong(timestamp.trim());
// 10000000000 对应 2286-11-20(毫秒)或 1970-04-26(秒级)
// 使用 10^10 作为分界线
if (value >= 10_000_000_000L) {
return formatMillisToDateTime(value);
} else {
return formatSecondsToDateTime(value);
}
} catch (NumberFormatException e) {
// 如果是 ISO 8601 格式,尝试解析
return formatISODateTime(timestamp);
}
}
/**
* 解析并格式化 ISO 8601 格式的日期时间字符串
* @param isoDateTime ISO 8601 格式的日期时间字符串
* @return 格式化后的日期字符串
*/
public static String formatISODateTime(String isoDateTime) {
if (isoDateTime == null || isoDateTime.trim().isEmpty()) {
return "";
}
try {
OffsetDateTime offsetDateTime = OffsetDateTime.parse(isoDateTime, ISO_FORMATTER);
return FORMATTER.format(offsetDateTime.toLocalDateTime());
} catch (Exception e) {
// 如果格式化失败,直接返回原始值
return isoDateTime;
}
}
}