Compare commits

...

9 Commits

Author SHA1 Message Date
qaiu 65d518373d Merge pull request #191 from yukaidi1220/lecloud-direct-download
feat: 乐云 directDownload 接口支持 & 缓存配置补充完善
2026-06-05 23:02:18 +08:00
yukaidi bca4da4b6c feat: 乐云 directDownload 接口支持 & 缓存配置补充完善
- 新增 directDownload (GET) 接口,比 packageDownloadWithFileIds 少一次请求
- 每次随机选择下载方式,失败自动 fallback 到另一种
- 统一所有下载方法的 Promise 参数传递
- 添加 HTTP 状态码日志便于调试
- 优化 app-dev.yml 缓存配置注释,补充所有缺失的网盘类型
2026-06-05 22:48:01 +08:00
qaiu 0fd78defcb feat(ct): 升级城通网盘接口并实现目录分享解析
- 移除token参数,新增url参数至API1请求
- API2新增start_time/wait_seconds/verifycode/share_id/acheck=1参数
- parse()中从API1响应提取FileInfo(file_name/file_id/file_size/file_time/username)
- 新增parseFileList()实现目录分享解析,通过getdir.php+file_list API获取文件列表
- 新增CTD枚举项匹配城通网盘目录分享链接(/d/路径)
2026-06-03 09:46:16 +00:00
qaiu 452fd0ea2c Merge pull request #190 from yukaidi1220/pr/native-package
CI: 新增原生环境打包 & 启动日志改进
2026-05-31 17:02:50 +08:00
yukaidi dd8f2efb37 ci: Release 自动生成更新说明(generate_release_notes) 2026-05-31 16:39:23 +08:00
yukaidi 0feb8e798a fix: PR#190 review 修复 — 配置查找顺序/页面日志/ZIP结构/注释
- 配置文件查找顺序与 Deploy 保持一致(先当前目录,再 resources/)
- 页面地址日志改用 onComplete,无论演练场加载成功失败均输出
- Windows ZIP 移除 /* 通配符,与 Linux 保持一致的顶层目录结构
- 修正注释:同步读文件会阻塞 event loop,不再声称'避免阻塞'
- YAML 正则和 jdeps 回退列表补充适用范围说明
2026-05-31 16:34:19 +08:00
yukaidi d55d8edd2f AppMain: 启动日志增加前端页面访问地址提示 2026-05-31 14:00:45 +08:00
yukaidi 451496f102 CI: 新增 Linux/Windows 原生环境打包 (jlink + 精简 JRE) 及 Docker 多平台构建 2026-05-31 14:00:45 +08:00
qaiu 6d6351bd58 更新 README.md 2026-05-31 07:02:09 +08:00
8 changed files with 587 additions and 94 deletions
+207 -37
View File
@@ -1,14 +1,5 @@
# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time name: Java CIMaven 构建 + Docker 镜像 + 原生环境打包)
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: Java CI with Maven
# The API requires write permission on the repository to submit dependencies
permissions: permissions:
contents: write contents: write
packages: write packages: write
@@ -16,9 +7,9 @@ permissions:
on: on:
push: push:
tags: tags:
- '*' # 只有推送tag时才会触发构建 - '*'
branches-ignore: branches-ignore:
- '*' # 排除所有分支的提交 - '*'
paths-ignore: paths-ignore:
- 'bin/**' - 'bin/**'
- '.github/**' - '.github/**'
@@ -32,71 +23,250 @@ on:
- "main" - "main"
jobs: jobs:
# ================================================================
# 阶段一:构建前端 + Maven 打包(只执行一次,产物共享)
# ================================================================
build: build:
name: 编译构建
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - name: 检出代码
uses: actions/checkout@v3
- uses: actions/setup-node@v4 - name: 设置 Node.js 18
uses: actions/setup-node@v4
with: with:
node-version: '18' node-version: '18'
- name: Set up JDK 17 - name: 设置 JDK 17
uses: actions/setup-java@v3 uses: actions/setup-java@v3
with: with:
java-version: '17' java-version: '17'
distribution: 'temurin' distribution: 'temurin'
cache: maven cache: maven
- name: Build Frontend - name: 构建前端
run: cd web-front && yarn install && yarn run build run: cd web-front && yarn install && yarn run build
- name: Build with Maven - name: Maven 编译打包
run: mvn -B package -DskipTests --file pom.xml run: mvn -B package -DskipTests --file pom.xml
# Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive - name: 更新依赖图谱
- name: Update dependency graph
uses: advanced-security/maven-dependency-submission-action@v3 uses: advanced-security/maven-dependency-submission-action@v3
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
continue-on-error: true continue-on-error: true
with: with:
ignore-maven-wrapper: true ignore-maven-wrapper: true
# - uses: release-drafter/release-drafter@v5 - name: 分享应用打包目录(供原生包和 Docker 复用)
# env: if: github.event_name != 'pull_request'
# GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
- name: Upload Artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: app-package
path: web-service/target/package/
- name: 分享 bin-zip(供 Docker 复用)
if: github.event_name != 'pull_request'
uses: actions/upload-artifact@v4
with:
name: app-bin-zip
path: web-service/target/netdisk-fast-download-bin.zip path: web-service/target/netdisk-fast-download-bin.zip
- name: Login to GitHub Container Registry # ================================================================
# 阶段二-A:Docker 镜像构建(并行)
# ================================================================
docker:
name: Docker 镜像
needs: build
if: github.event_name != 'pull_request' if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
steps:
- name: 检出代码
uses: actions/checkout@v3
- name: 下载 bin-zip 产物
uses: actions/download-artifact@v4
with:
name: app-bin-zip
path: web-service/target/
- name: 登录 GitHub 容器仓库
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx - name: 设置 QEMU(多平台构建支持)
uses: docker/setup-qemu-action@v3
- name: 设置 Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Extract git tag - name: 构建并推送 Docker 镜像
id: tag
run: |
GIT_TAG=$(git tag --points-at HEAD | head -n 1)
echo "tag=$GIT_TAG" >> $GITHUB_OUTPUT
- name: Build and push Docker image
if: github.event_name != 'pull_request'
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
context: . context: .
push: true push: true
platforms: linux/amd64,linux/arm64,linux/arm/v7 platforms: linux/amd64,linux/arm64,linux/arm/v7
tags: | tags: |
ghcr.io/${{ github.repository }}:${{ steps.tag.outputs.tag }} ghcr.io/${{ github.repository }}:${{ github.ref_name }}
ghcr.io/${{ github.repository }}:latest ghcr.io/${{ github.repository }}:latest
# ================================================================
# 阶段二-B:原生环境打包 Linux + Windows(并行)
# ================================================================
native-package:
name: 原生环境打包 → ${{ matrix.artifact-name }}
needs: build
if: github.event_name != 'pull_request'
strategy:
matrix:
include:
- os: ubuntu-latest
artifact-name: netdisk-fast-download-linux-amd64
- os: windows-latest
artifact-name: netdisk-fast-download-windows-amd64
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
steps:
- name: 设置 JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: 下载 Maven 构建产物
uses: actions/download-artifact@v4
with:
name: app-package
path: web-service/target/package
# ============================================================
# jdeps 分析 → 确定所需 JDK 模块
# ============================================================
- name: 分析所需 JDK 模块(jdeps
run: |
MAIN_JAR="web-service/target/package/netdisk-fast-download.jar"
LIB_DIR="web-service/target/package/lib"
CP=""
for jar in "$LIB_DIR"/*.jar; do
CP="$CP${CP:+:}$jar"
done
RAW_MODULES=$(jdeps --print-module-deps --ignore-missing-deps --multi-release 17 \
--class-path "$CP" "$MAIN_JAR" 2>/dev/null | head -n 1 | tr -d '\r\n' || true)
if [ -z "$RAW_MODULES" ] || [[ "$RAW_MODULES" == *"Missing"* ]] || [[ "$RAW_MODULES" == *"Error"* ]]; then
# ⚠️ 回退列表:若项目新增了需要 java.* / jdk.* 模块的依赖,需同步更新此处
RAW_MODULES="java.base,java.logging,java.sql,java.naming,java.management,java.xml,jdk.unsupported,java.net.http,java.instrument,java.security.jgss,java.security.sasl,java.desktop,jdk.crypto.ec"
echo "jdeps 分析失败,使用回退模块列表"
else
# 补上 jdeps 无法检测的反射/SPI依赖
RAW_MODULES="$RAW_MODULES,java.desktop,jdk.crypto.ec"
fi
echo "detected modules: $RAW_MODULES"
printf 'JDK_MODULES=%s\n' "$RAW_MODULES" >> $GITHUB_ENV
# ============================================================
# jlink 生成精简 JRE
# ============================================================
- name: 生成精简 JREjlink
run: |
jlink \
--module-path "$JAVA_HOME/jmods" \
--add-modules "$JDK_MODULES" \
--output "native-package/netdisk-fast-download/jre" \
--strip-debug \
--compress=2 \
--no-header-files \
--no-man-pages
echo "JRE size:"
du -sh native-package/netdisk-fast-download/jre || true
# Windows: 确保 MSVC 运行时 DLL 到位
if [[ "$RUNNER_OS" == "Windows" ]]; then
JRE_BIN="native-package/netdisk-fast-download/jre/bin"
for dll in vcruntime140.dll msvcp140.dll vcruntime140_1.dll; do
if [ ! -f "$JRE_BIN/$dll" ] && [ -f "$JAVA_HOME/bin/$dll" ]; then
echo "jlink 未包含 $dll,从 JDK 补拷"
cp "$JAVA_HOME/bin/$dll" "$JRE_BIN/"
fi
done
echo "=== JRE bin 目录 DLL 清单 ==="
ls -la "$JRE_BIN"/*.dll 2>/dev/null || echo "(无 .dll 文件)"
fi
# ============================================================
# 组装包目录
# ============================================================
- name: 组装包目录
run: |
PKG="native-package/netdisk-fast-download"
SRC="web-service/target/package"
cp "$SRC/netdisk-fast-download.jar" "$PKG/"
cp -r "$SRC/lib" "$PKG/"
cp -r "$SRC/resources" "$PKG/"
cp -r "$SRC/webroot" "$PKG/"
mkdir -p "$PKG/db"
mkdir -p "$PKG/logs"
# ============================================================
# 生成启动脚本
# ============================================================
- name: 生成启动脚本(Linux
run: |
PKG="native-package/netdisk-fast-download"
echo '#!/bin/bash' > "$PKG/run.sh"
echo 'DIR="$(cd "$(dirname "$0")" && pwd)"' >> "$PKG/run.sh"
echo 'cd "$DIR" || exit 1' >> "$PKG/run.sh"
echo 'exec "$DIR/jre/bin/java" -Xmx512M -Dfile.encoding=utf-8 -jar "$DIR/netdisk-fast-download.jar" "$@"' >> "$PKG/run.sh"
chmod +x "$PKG/run.sh"
- name: 生成启动脚本(Windows
run: |
PKG="native-package/netdisk-fast-download"
echo '@echo off' > "$PKG/run.bat"
echo 'chcp 65001 > nul' >> "$PKG/run.bat"
echo 'pushd %~dp0' >> "$PKG/run.bat"
echo '"%~dp0jre\bin\java.exe" -Xmx512M -Dfile.encoding=utf-8 -jar "%~dp0netdisk-fast-download.jar" %*' >> "$PKG/run.bat"
# ============================================================
# 打包为 zip
# ============================================================
- name: 打包 ZIPLinux
if: runner.os == 'Linux'
run: |
cd native-package
zip -r "../${{ matrix.artifact-name }}.zip" netdisk-fast-download/
- name: 打包 ZIPWindows
if: runner.os == 'Windows'
shell: pwsh
run: |
Compress-Archive -Path native-package/netdisk-fast-download -DestinationPath "${{ matrix.artifact-name }}.zip"
# ============================================================
# 上传产物
# ============================================================
- name: 上传原生安装包
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact-name }}
path: ${{ matrix.artifact-name }}.zip
- name: 上传到 Release
uses: softprops/action-gh-release@v2
with:
files: ${{ matrix.artifact-name }}.zip
tag_name: ${{ github.ref_name }}
generate_release_notes: true
+1
View File
@@ -92,3 +92,4 @@ yarn-error.log*
**/${project.build.directory}/ **/${project.build.directory}/
**/${project.basedir}/target/ **/${project.basedir}/target/
**/${basedir}/target/ **/${basedir}/target/
.spec-workflow/
+1 -1
View File
@@ -1,7 +1,7 @@
# 一款网盘分享链接云解析快速下载服务 # 一款网盘分享链接云解析快速下载服务
QQ交流群:1017480890 QQ交流群:1017480890
<p align="center"> <p align="center">
<a href="https://github.com/qaiu/netdisk-fast-download/actions/workflows/maven.yml"><img src="https://img.shields.io/github/actions/workflow/status/qaiu/netdisk-fast-download/maven.yml?style=flat"></a> <a href="https://github.com/qaiu/netdisk-fast-download/actions/workflows/maven.yml"><img src="https://img.shields.io/github/actions/workflow/status/qaiu/netdisk-fast-download/build.yml?branch=main&style=flat"></a>
<a href="https://www.oracle.com/cn/java/technologies/downloads"><img src="https://img.shields.io/badge/jdk-%3E%3D17-blue"></a> <a href="https://www.oracle.com/cn/java/technologies/downloads"><img src="https://img.shields.io/badge/jdk-%3E%3D17-blue"></a>
<a href="https://vertx-china.github.io"><img src="https://img.shields.io/badge/vert.x-4.5.27-blue?style=flat"></a> <a href="https://vertx-china.github.io"><img src="https://img.shields.io/badge/vert.x-4.5.27-blue?style=flat"></a>
<a href="https://raw.githubusercontent.com/qaiu/netdisk-fast-download/master/LICENSE"><img src="https://img.shields.io/github/license/qaiu/netdisk-fast-download?style=flat"></a> <a href="https://raw.githubusercontent.com/qaiu/netdisk-fast-download/master/LICENSE"><img src="https://img.shields.io/github/license/qaiu/netdisk-fast-download?style=flat"></a>
@@ -249,7 +249,13 @@ public enum PanDomainTemplate {
CT("城通网盘", CT("城通网盘",
compile("https://(?:[a-zA-Z\\d-]+\\.)?(ctfile|545c|u062|ghpym|474b)\\.com/f(ile)?/" + compile("https://(?:[a-zA-Z\\d-]+\\.)?(ctfile|545c|u062|ghpym|474b)\\.com/f(ile)?/" +
"(?<KEY>[0-9a-zA-Z_-]+)(\\?p=(?<PWD>\\w+))?"), "(?<KEY>[0-9a-zA-Z_-]+)(\\?p=(?<PWD>\\w+))?"),
"https://474b.com/file/{shareKey}", "https://ctfile.com/file/{shareKey}",
CtTool.class),
// https://url94.ctfile.com/d/64115194-164803691-48508c?p=7609&d=164803691&fk=decb36
CTD("城通网盘-目录",
compile("https://(?:[a-zA-Z\\d-]+\\.)?(ctfile|545c|u062|ghpym|474b)\\.com/d/" +
"(?<KEY>[0-9a-zA-Z_-]+)(\\?p=(?<PWD>\\w+))?"),
"https://ctfile.com/d/{shareKey}",
CtTool.class), CtTool.class),
// https://www.vyuyun.com/s/QMa6ie?password=I4KG7H // https://www.vyuyun.com/s/QMa6ie?password=I4KG7H
// https://www.vyuyun.com/s/QMa6ie/file?password=I4KG7H // https://www.vyuyun.com/s/QMa6ie/file?password=I4KG7H
@@ -1,16 +1,23 @@
package cn.qaiu.parser.impl; package cn.qaiu.parser.impl;
import cn.qaiu.entity.FileInfo;
import cn.qaiu.entity.ShareLinkInfo; import cn.qaiu.entity.ShareLinkInfo;
import cn.qaiu.parser.PanBase; import cn.qaiu.parser.PanBase;
import cn.qaiu.util.RandomStringGenerator; import cn.qaiu.util.FileSizeConverter;
import io.vertx.core.Future; import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.buffer.Buffer; import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject; import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.client.HttpRequest; import io.vertx.ext.web.client.HttpRequest;
import io.vertx.uritemplate.UriTemplate; import io.vertx.uritemplate.UriTemplate;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** /**
* <a href="https://www.ctfile.com">诚通网盘</a> * <a href="https://www.ctfile.com">诚通网盘</a>
@@ -18,19 +25,34 @@ import java.util.Map;
public class CtTool extends PanBase { public class CtTool extends PanBase {
private static final String API_URL_PREFIX = "https://webapi.ctfile.com"; private static final String API_URL_PREFIX = "https://webapi.ctfile.com";
// https://webapi.ctfile.com/getfile.php?path=f&f=55050874-1246660795-6464f6& // https://webapi.ctfile.com/getfile.php?path=f&f=64115194-17569800420720-06c697&
// passcode=7548&token=30wiijxs1fzhb6brw0p9m6&r=0.5885881231735761& // passcode=7609&r=0.6611183001986635&ref=&url=https%3A%2F%2Furl94.ctfile.com%2Ff%2F64115194-17569800420720-06c697%3Fp%3D7609
// ref=&url=https%3A%2F%2F474b.com%2Ff%2F55050874-1246660795-6464f6%3Fp%3D7548
private static final String API1 = API_URL_PREFIX + "/getfile.php?path={path}" + private static final String API1 = API_URL_PREFIX + "/getfile.php?path={path}" +
"&f={shareKey}&passcode={pwd}&token={token}&r={rand}&ref="; "&f={shareKey}&passcode={pwd}&r={rand}&ref=&url={url}";
//https://webapi.ctfile.com/get_file_url.php?uid=55050874&fid=1246660795&folder_id=0& // https://webapi.ctfile.com/get_file_url.php?uid=64115194&fid=17569800420720&folder_id=0&
// file_chk=054bc20461f5c63ff82015b9e69fb7fc&mb=1&token=30wiijxs1fzhb6brw0p9m6&app=0& // share_id=&file_chk=af5c8757a49cbc69a557eb3da59b246c&start_time=1780471868&wait_seconds=0&
// acheck=1&verifycode=&rd=0.965929071503574 // mb=0&app=0&acheck=1&verifycode=1780471868.2951fe63abedf36ec02f34ed5711ce70&rd=0.36350981353622636
private static final String API2 = API_URL_PREFIX + "/get_file_url.php?" + private static final String API2 = API_URL_PREFIX + "/get_file_url.php?" +
"uid={uid}&fid={fid}&folder_id=0&file_chk={file_chk}&mb=0&token={token}&app=0&acheck=0&verifycode=" + "uid={uid}&fid={fid}&folder_id=0&share_id=&file_chk={file_chk}" +
"&rd={rand}"; "&start_time={start_time}&wait_seconds={wait_seconds}&mb=0&app=0&acheck=1" +
"&verifycode={verifycode}&rd={rand}";
// https://webapi.ctfile.com/getdir.php?path=d&d=64115194-164803691-48508c&
// folder_id=164803691&fk=decb36&passcode=7609&r=0.23...&ref=&url=https://url94.ctfile.com/d/...
private static final String API_GETDIR = API_URL_PREFIX + "/getdir.php?path={path}" +
"&d={shareKey}&folder_id={folder_id}&fk={fk}&passcode={pwd}&r={rand}&ref=&url={url}";
// DataTables参数,用于获取目录文件列表
private static final String FILE_LIST_PARAMS = "&sEcho=1&iColumns=4&sColumns=%2C%2C%2C" +
"&iDisplayStart=0&iDisplayLength=500&mDataProp_0=0&mDataProp_1=1&mDataProp_2=2&mDataProp_3=3" +
"&iSortCol_0=3&sSortDir_0=desc&iSortingCols=1";
// 文件列表HTML解析正则
private static final Pattern FILE_ID_PATTERN = Pattern.compile("value=\"f(\\d+)\"");
private static final Pattern FILE_HREF_PATTERN = Pattern.compile("href=\"#/f/([^\"]+)\"");
private static final Pattern FILE_NAME_PATTERN = Pattern.compile(">([^<]+)</a>");
private static final Pattern FILE_ICON_PATTERN = Pattern.compile("alt=\"([^\"]+)\"");
/** /**
* 子类重写此构造方法不需要添加额外逻辑 * 子类重写此构造方法不需要添加额外逻辑
@@ -57,7 +79,6 @@ public class CtTool extends PanBase {
} }
String[] split = shareKey.split("-"); String[] split = shareKey.split("-");
String uid = split[0], fid = split[1]; String uid = split[0], fid = split[1];
String token = RandomStringGenerator.generateRandomString();
// 获取url path // 获取url path
int i1 = shareLinkInfo.getShareUrl().indexOf("com/"); int i1 = shareLinkInfo.getShareUrl().indexOf("com/");
int i2 = shareLinkInfo.getShareUrl().lastIndexOf("/"); int i2 = shareLinkInfo.getShareUrl().lastIndexOf("/");
@@ -67,8 +88,8 @@ public class CtTool extends PanBase {
.setTemplateParam("path", path) .setTemplateParam("path", path)
.setTemplateParam("shareKey", shareKey) .setTemplateParam("shareKey", shareKey)
.setTemplateParam("pwd", shareLinkInfo.getSharePassword()) .setTemplateParam("pwd", shareLinkInfo.getSharePassword())
.setTemplateParam("token", token) .setTemplateParam("rand", String.valueOf(Math.random()))
.setTemplateParam("r", Math.random() + ""); .setTemplateParam("url", shareLinkInfo.getShareUrl());
bufferHttpRequest1 bufferHttpRequest1
.send().onSuccess(res -> { .send().onSuccess(res -> {
@@ -77,12 +98,29 @@ public class CtTool extends PanBase {
var fileJson = resJson.getJsonObject("file"); var fileJson = resJson.getJsonObject("file");
if (fileJson.containsKey("file_chk")) { if (fileJson.containsKey("file_chk")) {
var file_chk = fileJson.getString("file_chk"); var file_chk = fileJson.getString("file_chk");
String startTime = fileJson.getValue("start_time").toString();
String waitSeconds = fileJson.getValue("wait_seconds").toString();
String verifycode = fileJson.getString("verifycode");
// 提取文件信息并存储
FileInfo fileInfo = new FileInfo()
.setFileName(fileJson.getString("file_name"))
.setFileId(String.valueOf(fileJson.getLong("file_id", 0L)))
.setSizeStr(fileJson.getString("file_size"))
.setCreateTime(fileJson.getString("file_time"))
.setCreateBy(fileJson.getString("username"))
.setFileType("file")
.setPanType(shareLinkInfo.getType());
shareLinkInfo.getOtherParam().put("fileInfo", fileInfo);
HttpRequest<Buffer> bufferHttpRequest2 = clientSession.getAbs(UriTemplate.of(API2)) HttpRequest<Buffer> bufferHttpRequest2 = clientSession.getAbs(UriTemplate.of(API2))
.setTemplateParam("uid", uid) .setTemplateParam("uid", uid)
.setTemplateParam("fid", fid) .setTemplateParam("fid", fid)
.setTemplateParam("file_chk", file_chk) .setTemplateParam("file_chk", file_chk)
.setTemplateParam("token", token) .setTemplateParam("start_time", startTime)
.setTemplateParam("rd", Math.random() + ""); .setTemplateParam("wait_seconds", waitSeconds)
.setTemplateParam("verifycode", verifycode)
.setTemplateParam("rand", String.valueOf(Math.random()));
bufferHttpRequest2 bufferHttpRequest2
.send().onSuccess(res2 -> { .send().onSuccess(res2 -> {
JsonObject resJson2 = asJson(res2); JsonObject resJson2 = asJson(res2);
@@ -109,4 +147,132 @@ public class CtTool extends PanBase {
}).onFailure(handleFail(bufferHttpRequest1.queryParams().toString())); }).onFailure(handleFail(bufferHttpRequest1.queryParams().toString()));
return promise.future(); return promise.future();
} }
@Override
public Future<List<FileInfo>> parseFileList() {
Promise<List<FileInfo>> listPromise = Promise.promise();
final String shareKey = shareLinkInfo.getShareKey();
final String shareUrl = shareLinkInfo.getShareUrl();
final String pwd = shareLinkInfo.getSharePassword();
// shareKey格式: uid-folder_id-hash (例如 64115194-164803691-48508c)
String[] split = shareKey.split("-");
if (split.length < 2) {
listPromise.fail(baseMsg() + " shareKey格式不正确: " + shareKey);
return listPromise.future();
}
String folderId = split[1];
// 从分享URL中提取fk参数
String fk = extractQueryParam(shareUrl, "fk");
// 从URL中提取path (例如从 "https://url94.ctfile.com/d/xxx?p=..." 中提取 "d")
int comIdx = shareUrl.indexOf("com/");
int qIdx = shareUrl.indexOf('?');
String pathAndKey = qIdx > 0 ? shareUrl.substring(comIdx + 4, qIdx) : shareUrl.substring(comIdx + 4);
int slashIdx = pathAndKey.indexOf('/');
String path = slashIdx > 0 ? pathAndKey.substring(0, slashIdx) : pathAndKey;
clientSession.getAbs(UriTemplate.of(API_GETDIR))
.setTemplateParam("path", path)
.setTemplateParam("shareKey", shareKey)
.setTemplateParam("folder_id", folderId)
.setTemplateParam("fk", fk != null ? fk : "")
.setTemplateParam("pwd", pwd != null ? pwd : "")
.setTemplateParam("rand", String.valueOf(Math.random()))
.setTemplateParam("url", shareUrl)
.send().onSuccess(res -> {
var resJson = asJson(res);
if (!resJson.containsKey("file")) {
listPromise.fail(baseMsg() + " 目录解析失败: " + resJson.encode());
return;
}
var dirInfo = resJson.getJsonObject("file");
String fileListRelUrl = dirInfo.getString("url");
if (fileListRelUrl == null) {
listPromise.fail(baseMsg() + " 文件列表URL为空");
return;
}
String fileListUrl = API_URL_PREFIX + fileListRelUrl + FILE_LIST_PARAMS;
clientSession.getAbs(fileListUrl)
.send().onSuccess(res2 -> {
var listJson = asJson(res2);
JsonArray aaData = listJson.getJsonArray("aaData");
if (aaData == null) {
listPromise.fail(baseMsg() + " 文件列表为空");
return;
}
List<FileInfo> fileList = new ArrayList<>();
String panType = shareLinkInfo.getType();
for (int i = 0; i < aaData.size(); i++) {
var row = aaData.getJsonArray(i);
try {
String checkboxHtml = row.getString(0);
String nameCellHtml = row.getString(1);
String sizeStr = row.getString(2).trim();
// 从checkbox HTML中提取文件ID
String fileId = null;
Matcher idMatcher = FILE_ID_PATTERN.matcher(checkboxHtml);
if (idMatcher.find()) fileId = idMatcher.group(1);
// 从文件名单元格HTML中提取临时分享key
String fileShareKey = null;
Matcher hrefMatcher = FILE_HREF_PATTERN.matcher(nameCellHtml);
if (hrefMatcher.find()) fileShareKey = hrefMatcher.group(1);
// 提取文件名
String fileName = null;
Matcher nameMatcher = FILE_NAME_PATTERN.matcher(nameCellHtml);
if (nameMatcher.find()) fileName = nameMatcher.group(1).trim();
// 提取文件图标/类型
String fileIcon = null;
Matcher iconMatcher = FILE_ICON_PATTERN.matcher(nameCellHtml);
if (iconMatcher.find()) fileIcon = iconMatcher.group(1);
if (fileName == null || fileShareKey == null) continue;
long sizeBytes = 0;
try {
sizeBytes = FileSizeConverter.convertToBytes(sizeStr);
} catch (Exception ignored) {}
FileInfo fileInfo = new FileInfo()
.setFileName(fileName)
.setFileId(fileId)
.setSizeStr(sizeStr)
.setSize(sizeBytes)
.setFileType(fileIcon)
.setFileIcon(fileIcon)
.setPanType(panType)
.setParserUrl(String.format("%s/v2/redirectUrl/%s/%s",
getDomainName(), panType, fileShareKey));
fileList.add(fileInfo);
} catch (Exception e) {
log.warn("解析文件行失败: {}", e.getMessage());
}
}
listPromise.complete(fileList);
}).onFailure(listPromise::fail);
}).onFailure(listPromise::fail);
return listPromise.future();
}
private String extractQueryParam(String url, String paramName) {
if (url == null) return null;
int qIdx = url.indexOf('?');
if (qIdx < 0) return null;
String query = url.substring(qIdx + 1);
for (String param : query.split("&")) {
int eqIdx = param.indexOf('=');
if (eqIdx > 0 && param.substring(0, eqIdx).equals(paramName)) {
return param.substring(eqIdx + 1);
}
}
return null;
}
} }
@@ -15,6 +15,7 @@ import java.net.URLEncoder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Base64; import java.util.Base64;
import java.util.List; import java.util.List;
import java.util.Random;
import java.util.UUID; import java.util.UUID;
/** /**
@@ -24,6 +25,7 @@ public class LeTool extends PanBase {
private static final String API_URL_PREFIX = "https://lecloud.lenovo.com/mshare/api/clouddiskapi/share/public/v1/"; private static final String API_URL_PREFIX = "https://lecloud.lenovo.com/mshare/api/clouddiskapi/share/public/v1/";
private static final String DEFAULT_FILE_TYPE = "file"; private static final String DEFAULT_FILE_TYPE = "file";
private static final int FILE_TYPE_DIRECTORY = 0; // 目录类型 private static final int FILE_TYPE_DIRECTORY = 0; // 目录类型
private static final Random RANDOM = new Random();
private static final MultiMap HEADERS; private static final MultiMap HEADERS;
@@ -100,8 +102,8 @@ public class LeTool extends PanBase {
} }
String fileId = fileInfoJson.getString("fileId"); String fileId = fileInfoJson.getString("fileId");
// 根据文件ID获取跳转链接 // 根据文件ID获取跳转链接(随机选择方式,失败自动fallback)
getDownURL(dataKey, fileId); getDownURLWithFallback(dataKey, fileId);
} }
} else { } else {
fail("{}: {}", resJson.getString("errcode"), resJson.getString("errmsg")); fail("{}: {}", resJson.getString("errcode"), resJson.getString("errmsg"));
@@ -260,8 +262,8 @@ public class LeTool extends PanBase {
String shareId = paramJson.getString("shareId"); String shareId = paramJson.getString("shareId");
String fileId = paramJson.getString("fileId"); String fileId = paramJson.getString("fileId");
// 调用获取下载链接 // 调用获取下载链接(随机选择方式,失败自动fallback)
getDownURLForById(shareId, fileId, parsePromise); getDownURLWithFallbackForById(shareId, fileId, parsePromise);
} catch (Exception e) { } catch (Exception e) {
parsePromise.fail("解析参数失败: " + e.getMessage()); parsePromise.fail("解析参数失败: " + e.getMessage());
@@ -304,14 +306,22 @@ public class LeTool extends PanBase {
}).onFailure(err -> promise.fail(err)); }).onFailure(err -> promise.fail(err));
} }
private void getDownURL(String key, String fileId) { /**
* 通过 packageDownloadWithFileIds 接口获取下载链接
* 需要两步:先获取 downloadUrl,再请求 302 跳转
*
* @param shareId 分享ID
* @param fileId 文件ID
* @param promise 完成时会写入此 promise
*/
private void getDownURL(String shareId, String fileId, Promise<String> promise) {
String uuid = UUID.randomUUID().toString(); String uuid = UUID.randomUUID().toString();
JsonArray fileIds = JsonArray.of(fileId); JsonArray fileIds = JsonArray.of(fileId);
String apiUrl2 = API_URL_PREFIX + "packageDownloadWithFileIds"; String apiUrl = API_URL_PREFIX + "packageDownloadWithFileIds";
// {"fileIds":[123],"shareId":"xxx","browserId":"uuid"} // {"fileIds":[123],"shareId":"xxx","browserId":"uuid"}
client.postAbs(apiUrl2) client.postAbs(apiUrl)
.putHeaders(HEADERS) .putHeaders(HEADERS)
.sendJsonObject(JsonObject.of("fileIds", fileIds, "shareId", key, "browserId", uuid)) .sendJsonObject(JsonObject.of("fileIds", fileIds, "shareId", shareId, "browserId", uuid))
.onSuccess(res -> { .onSuccess(res -> {
JsonObject resJson = asJson(res); JsonObject resJson = asJson(res);
if (resJson.containsKey("result")) { if (resJson.containsKey("result")) {
@@ -320,20 +330,107 @@ public class LeTool extends PanBase {
// 获取重定向链接跳转链接 // 获取重定向链接跳转链接
String downloadUrl = dataJson.getString("downloadUrl"); String downloadUrl = dataJson.getString("downloadUrl");
if (downloadUrl == null) { if (downloadUrl == null) {
fail("Result JSON数据异常: downloadUrl不存在"); promise.fail("Result JSON数据异常: downloadUrl不存在");
return; return;
} }
// 获取重定向链接跳转链接 // 获取重定向链接跳转链接
clientNoRedirects.getAbs(downloadUrl).send() clientNoRedirects.getAbs(downloadUrl).send()
.onSuccess(res2 -> promise.complete(res2.headers().get("Location"))) .onSuccess(res2 -> promise.complete(res2.headers().get("Location")))
.onFailure(handleFail(downloadUrl)); .onFailure(err -> promise.fail(err));
} else { } else {
fail("{}: {}", resJson.getString("errcode"), resJson.getString("errmsg")); promise.fail(resJson.getString("errcode") + ": " + resJson.getString("errmsg"));
} }
} else { } else {
fail("Result JSON数据异常: result字段不存在"); promise.fail("Result JSON数据异常: result字段不存在");
}
}).onFailure(err -> promise.fail(err));
}
/**
* 通过 directDownload 接口获取下载链接
* 相比 packageDownloadWithFileIds 少一次请求,直接返回302
*
* @param shareId 分享ID
* @param fileId 文件ID
* @param promise 完成时会写入此 promise
*/
private void getDownURLDirect(String shareId, String fileId, Promise<String> promise) {
String uuid = UUID.randomUUID().toString();
String apiUrl = API_URL_PREFIX + "directDownload"
+ "?shareId=" + shareId
+ "&fileId=" + fileId
+ "&browserId=" + uuid;
clientNoRedirects.getAbs(apiUrl)
.putHeaders(HEADERS)
.send()
.onSuccess(res -> {
String location = res.headers().get("Location");
if (location != null && !location.isEmpty()) {
promise.complete(location);
} else {
log.warn("directDownload 返回非302响应: shareId={}, fileId={}, statusCode={}", shareId, fileId, res.statusCode());
promise.fail("directDownload 未返回有效的 Location, statusCode=" + res.statusCode());
}
})
.onFailure(err -> {
log.warn("directDownload 请求失败: shareId={}, fileId={}, error={}", shareId, fileId, err.getMessage());
promise.fail(err);
});
}
/**
* 随机选择下载方式并带 fallback(用于 parse
* 先随机选择 directDownload 或 packageDownloadWithFileIds,失败则尝试另一个
*/
private void getDownURLWithFallback(String shareId, String fileId) {
boolean useDirect = RANDOM.nextBoolean();
log.info("乐云下载方式选择: shareId={}, fileId={}, method={}", shareId, fileId, useDirect ? "directDownload" : "packageDownloadWithFileIds");
Promise<String> fallbackPromise = Promise.promise();
fallbackPromise.future().onSuccess(url -> {
promise.complete(url);
}).onFailure(err -> {
log.warn("乐云第一种下载方式失败,尝试另一种: {}", err.getMessage());
if (useDirect) {
getDownURL(shareId, fileId, promise);
} else {
getDownURLDirect(shareId, fileId, promise);
}
});
if (useDirect) {
getDownURLDirect(shareId, fileId, fallbackPromise);
} else {
getDownURL(shareId, fileId, fallbackPromise);
}
}
/**
* 随机选择下载方式并带 fallback(用于 parseById
* 先随机选择 directDownload 或 packageDownloadWithFileIds,失败则尝试另一个
*/
private void getDownURLWithFallbackForById(String shareId, String fileId, Promise<String> promise) {
boolean useDirect = RANDOM.nextBoolean();
log.info("乐云下载方式选择(parseById): shareId={}, fileId={}, method={}", shareId, fileId, useDirect ? "directDownload" : "packageDownloadWithFileIds");
Promise<String> fallbackPromise = Promise.promise();
fallbackPromise.future().onSuccess(url -> {
promise.complete(url);
}).onFailure(err -> {
log.warn("乐云第一种下载方式失败,尝试另一种: {}", err.getMessage());
if (useDirect) {
getDownURLForById(shareId, fileId, promise);
} else {
getDownURLDirect(shareId, fileId, promise);
}
});
if (useDirect) {
getDownURLDirect(shareId, fileId, fallbackPromise);
} else {
getDownURLForById(shareId, fileId, fallbackPromise);
} }
}).onFailure(handleFail(apiUrl2));
} }
/** /**
@@ -21,6 +21,8 @@ import io.vertx.core.shareddata.LocalMap;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.DateFormatUtils; import org.apache.commons.lang3.time.DateFormatUtils;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Date; import java.util.Date;
import static cn.qaiu.vx.core.util.ConfigConstant.LOCAL; import static cn.qaiu.vx.core.util.ConfigConstant.LOCAL;
@@ -78,12 +80,35 @@ public class AppMain {
System.out.println("数据库连接成功"); System.out.println("数据库连接成功");
// 加载演练场解析器 // 加载演练场解析器
loadPlaygroundParsers();
String addr = jsonObject.getJsonObject(ConfigConstant.SERVER).getString("domainName"); String addr = jsonObject.getJsonObject(ConfigConstant.SERVER).getString("domainName");
if (addr == null || addr.isBlank()) { if (addr == null || addr.isBlank()) {
addr = "http://127.0.0.1:" + jsonObject.getJsonObject(ConfigConstant.SERVER).getInteger("port", 6400); addr = "http://127.0.0.1:" + jsonObject.getJsonObject(ConfigConstant.SERVER).getInteger("port", 6400);
} }
// 读取代理配置获取前端页面端口(同步读取小文件,仅启动时执行一次)
String proxyConfName = jsonObject.getString("proxyConf", "server-proxy");
String pageAddr = addr;
try {
String configFile = proxyConfName + ".yml";
// 与 Deploy 保持一致:优先当前目录,其次 resources/
Path configPath = Path.of(configFile);
if (!Files.exists(configPath)) {
configPath = Path.of("resources", configFile);
}
if (Files.exists(configPath)) {
String yamlContent = Files.readString(configPath);
// 匹配项目约定的 YAML 格式: "- listen: 8080"
java.util.regex.Matcher m = java.util.regex.Pattern
.compile("^\\s*-\\s+listen:\\s*(\\d+)", java.util.regex.Pattern.MULTILINE)
.matcher(yamlContent);
if (m.find()) {
int pagePort = Integer.parseInt(m.group(1));
pageAddr = "http://127.0.0.1:" + pagePort;
}
}
} catch (Exception e) {
log.warn("读取代理配置失败,使用默认页面地址: {}", e.getMessage());
}
loadPlaygroundParsers(pageAddr);
System.out.println("启动成功: \n本地服务地址: " + addr); System.out.println("启动成功: \n本地服务地址: " + addr);
}); });
}); });
@@ -125,7 +150,7 @@ public class AppMain {
/** /**
* 在启动时加载所有已发布的演练场解析器 * 在启动时加载所有已发布的演练场解析器
*/ */
private static void loadPlaygroundParsers() { private static void loadPlaygroundParsers(String accessAddr) {
DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class); DbService dbService = AsyncServiceUtil.getAsyncServiceInstance(DbService.class);
dbService.getPlaygroundParserList().onSuccess(result -> { dbService.getPlaygroundParserList().onSuccess(result -> {
@@ -165,6 +190,8 @@ public class AppMain {
} }
}).onFailure(e -> { }).onFailure(e -> {
log.error("加载演练场解析器列表失败", e); log.error("加载演练场解析器列表失败", e);
}).onComplete(ar -> {
log.info("服务已启动,可通过 {} 访问页面", accessAddr);
}); });
} }
} }
+47 -21
View File
@@ -74,28 +74,54 @@ cache:
type: h2db type: h2db
# 默认时长: 单位分钟,大部分网盘未严格验证,建议不要太大 # 默认时长: 单位分钟,大部分网盘未严格验证,建议不要太大
defaultDuration: 5 defaultDuration: 5
# 具体网盘的缓存配置,如果不加配置则不缓存,每次请求都会请求网盘API,格式:网盘标识: 时长 # 具体网盘的缓存配置(单位:分钟)
# - 配置 key 且有值(如 le: 2879):使用指定时长
# - 配置 key 但无值(如 fc:):使用上面的 defaultDuration
# - 未配置的 key:不缓存,每次都请求网盘API
# 格式:网盘标识: 时长
duration: duration:
ce: 5 # ---- 网盘类 ----
cow: 5 ce: 5 # Cloudreve
ec: 5 cow: 5 # 奶牛快传
fc: ct: 30 # 城通网盘
fj: 20 ec: 5 # 移动云空间
iz: 20 fc: # 亿方云
le: 2879 fj: 20 # 小飞机网盘
lz: 20 fs: # 飞书云盘
qq: 9999999 iz: 20 # 蓝奏云优享
qqw: 30 kd: # 可道云
ws: 10 le: 2879 # 联想乐云
ye: -1 lz: 20 # 蓝奏云
mne: 30 other: # 其他网盘
mqq: 30 p115: 30 # 115网盘
mkg: 30 pdb: # Dropbox
p115: 30 pcx: # 超星云盘(需要 referer 头)
ct: 30 pgd: # Google Drive
qishui_music: 5 pic: # iCloud
baidu_photo: 5 pod: # OneDrive
migu: 5 pvyy: # 微雨云存储
pwps: # WPS云文档
qk: # 夸克网盘
qq: 9999999 # QQ邮箱中转站 (iwx.mail.qq.com/ftn/download)
qqsc: # QQ闪传 (qfile.qq.com)
qqw: 30 # QQ邮箱云盘 (wx.mail.qq.com/s)
uc: # UC网盘
ws: 10 # 文叔叔
ye: -1 # 123网盘
# ---- 音乐类 ----
baidu_photo: 5 # 百度网盘相册
migu: 5 # 咪咕音乐
mkg: 30 # 酷狗音乐
mkgs: # 酷狗音乐分享短链
mkgs2: # 酷狗音乐分享2share/*.html
mkws: # 酷我音乐分享
mmgs: # 咪咕音乐分享短链
mne: 30 # 网易云音乐
mnes: # 网易云音乐分享短链
mqq: 30 # QQ音乐
mqqs: # QQ音乐分享短链
qishui_music: 5 # 汽水音乐
# httpClient静态代理服务器配置(外网代理) # httpClient静态代理服务器配置(外网代理)
proxy: proxy: