From f1dd9fc0ee43e5f57a2ba0431ca2cf341dddce42 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 10 Nov 2025 09:57:41 +0000
Subject: [PATCH] Add Ce4Tool for Cloudreve 4.x API support and update CeTool
with version detection
Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
---
.../java/cn/qaiu/parser/impl/Ce4Tool.java | 195 ++++++++++++++++++
.../main/java/cn/qaiu/parser/impl/CeTool.java | 145 +++++++++++--
2 files changed, 320 insertions(+), 20 deletions(-)
create mode 100644 parser/src/main/java/cn/qaiu/parser/impl/Ce4Tool.java
diff --git a/parser/src/main/java/cn/qaiu/parser/impl/Ce4Tool.java b/parser/src/main/java/cn/qaiu/parser/impl/Ce4Tool.java
new file mode 100644
index 0000000..a9cdae5
--- /dev/null
+++ b/parser/src/main/java/cn/qaiu/parser/impl/Ce4Tool.java
@@ -0,0 +1,195 @@
+package cn.qaiu.parser.impl;
+
+import cn.qaiu.entity.ShareLinkInfo;
+import cn.qaiu.parser.PanBase;
+import io.vertx.core.Future;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.client.HttpRequest;
+
+import java.net.URL;
+
+/**
+ * Cloudreve 4.x 自建网盘解析
+ * Cloudreve 4.x API 版本解析器
+ */
+public class Ce4Tool extends PanBase {
+
+ // Cloudreve 4.x uses /api/v3/ prefix for most APIs
+ private static final String PING_API_V3_PATH = "/api/v3/site/ping";
+ private static final String PING_API_V4_PATH = "/api/v4/site/ping";
+ private static final String FILE_URL_API_PATH = "/api/v3/file/url";
+ private static final String SHARE_API_PATH = "/api/v3/share/info/";
+
+ public Ce4Tool(ShareLinkInfo shareLinkInfo) {
+ super(shareLinkInfo);
+ }
+
+ public Future parse() {
+ String key = shareLinkInfo.getShareKey();
+ String pwd = shareLinkInfo.getSharePassword();
+
+ try {
+ URL url = new URL(shareLinkInfo.getShareUrl());
+ String baseUrl = url.getProtocol() + "://" + url.getHost();
+
+ // First, detect API version by pinging
+ detectVersion(baseUrl, key, pwd);
+ } catch (Exception e) {
+ fail(e, "URL解析错误");
+ }
+ return promise.future();
+ }
+
+ /**
+ * Detect Cloudreve version by pinging /api/v3/site/ping or /api/v4/site/ping
+ */
+ private void detectVersion(String baseUrl, String key, String pwd) {
+ String pingUrlV3 = baseUrl + PING_API_V3_PATH;
+
+ // Try v3 ping first (which also works for 4.x)
+ clientSession.getAbs(pingUrlV3).send().onSuccess(res -> {
+ if (res.statusCode() == 200) {
+ try {
+ JsonObject pingResponse = asJson(res);
+ // If we get a valid JSON response, this is a Cloudreve instance
+ // Check if it's 4.x by trying the share API
+ getShareInfo(baseUrl, key, pwd);
+ } catch (Exception e) {
+ // Not a valid JSON response, try v4 ping
+ tryV4Ping(baseUrl, key, pwd);
+ }
+ } else if (res.statusCode() == 404) {
+ // Try v4 ping
+ tryV4Ping(baseUrl, key, pwd);
+ } else {
+ // Not a Cloudreve instance, try next parser
+ nextParser();
+ }
+ }).onFailure(t -> {
+ // Network error or not accessible, try next parser
+ nextParser();
+ });
+ }
+
+ private void tryV4Ping(String baseUrl, String key, String pwd) {
+ String pingUrlV4 = baseUrl + PING_API_V4_PATH;
+
+ clientSession.getAbs(pingUrlV4).send().onSuccess(res -> {
+ if (res.statusCode() == 200) {
+ try {
+ JsonObject pingResponse = asJson(res);
+ // Valid v4 response
+ getShareInfo(baseUrl, key, pwd);
+ } catch (Exception e) {
+ // Not a Cloudreve instance
+ nextParser();
+ }
+ } else {
+ // Not a Cloudreve instance
+ nextParser();
+ }
+ }).onFailure(t -> {
+ // Not accessible, try next parser
+ nextParser();
+ });
+ }
+
+ /**
+ * Get share information from Cloudreve 4.x
+ */
+ private void getShareInfo(String baseUrl, String key, String pwd) {
+ String shareApiUrl = baseUrl + SHARE_API_PATH + key;
+
+ HttpRequest httpRequest = clientSession.getAbs(shareApiUrl);
+ if (pwd != null && !pwd.isEmpty()) {
+ httpRequest.addQueryParam("password", pwd);
+ }
+
+ httpRequest.send().onSuccess(res -> {
+ try {
+ if (res.statusCode() == 200) {
+ JsonObject jsonObject = asJson(res);
+ if (jsonObject.containsKey("code")) {
+ int code = jsonObject.getInteger("code");
+ if (code == 0) {
+ // Success, get file info and download URL
+ JsonObject data = jsonObject.getJsonObject("data");
+ if (data != null) {
+ // Get file path or use default
+ String filePath = "/";
+ if (data.containsKey("path")) {
+ filePath = data.getString("path");
+ }
+ // For 4.x, we need to get the download URL via POST /api/v3/file/url
+ getDownloadUrl(baseUrl, key, filePath);
+ } else {
+ fail("分享信息获取失败: data字段为空");
+ }
+ } else {
+ // Error code, might be wrong password or invalid share
+ String msg = jsonObject.getString("msg", "未知错误");
+ fail("分享验证失败: {}", msg);
+ }
+ } else {
+ // Not a Cloudreve 4.x response, try next parser
+ nextParser();
+ }
+ } else {
+ // HTTP error, not a valid Cloudreve instance
+ nextParser();
+ }
+ } catch (Exception e) {
+ // JSON parsing error, not a Cloudreve instance
+ nextParser();
+ }
+ }).onFailure(t -> {
+ // Network error, try next parser
+ nextParser();
+ });
+ }
+
+ /**
+ * Get download URL via POST /api/v3/file/url (Cloudreve 4.x API)
+ */
+ private void getDownloadUrl(String baseUrl, String key, String filePath) {
+ String fileUrlApi = baseUrl + FILE_URL_API_PATH;
+
+ // Prepare request body for Cloudreve 4.x
+ JsonObject requestBody = new JsonObject()
+ .put("uris", new JsonArray().add(filePath))
+ .put("download", true);
+
+ clientSession.postAbs(fileUrlApi)
+ .putHeader("Content-Type", "application/json")
+ .sendJsonObject(requestBody)
+ .onSuccess(res -> {
+ try {
+ if (res.statusCode() == 200) {
+ JsonObject jsonObject = asJson(res);
+ if (jsonObject.containsKey("urls")) {
+ JsonArray urls = jsonObject.getJsonArray("urls");
+ if (urls != null && urls.size() > 0) {
+ JsonObject urlObj = urls.getJsonObject(0);
+ String downloadUrl = urlObj.getString("url");
+ if (downloadUrl != null && !downloadUrl.isEmpty()) {
+ promise.complete(downloadUrl);
+ } else {
+ fail("下载链接为空");
+ }
+ } else {
+ fail("下载链接列表为空");
+ }
+ } else {
+ fail("响应中不包含urls字段");
+ }
+ } else {
+ fail("获取下载链接失败: HTTP {}", res.statusCode());
+ }
+ } catch (Exception e) {
+ fail(e, "解析下载链接响应失败");
+ }
+ }).onFailure(handleFail(fileUrlApi));
+ }
+}
diff --git a/parser/src/main/java/cn/qaiu/parser/impl/CeTool.java b/parser/src/main/java/cn/qaiu/parser/impl/CeTool.java
index 968f6cc..460db22 100644
--- a/parser/src/main/java/cn/qaiu/parser/impl/CeTool.java
+++ b/parser/src/main/java/cn/qaiu/parser/impl/CeTool.java
@@ -15,6 +15,7 @@ import java.net.URL;
* huang1111
* 看见存储
* 亿安云盘
+ * Cloudreve 3.x 解析器,会自动检测版本并在4.x时转发到Ce4Tool
*/
public class CeTool extends PanBase {
@@ -22,6 +23,9 @@ public class CeTool extends PanBase {
// api/v3/share/info/g31PcQ?password=qaiu
private static final String SHARE_API_PATH = "/api/v3/share/info/";
+
+ private static final String PING_API_V3_PATH = "/api/v3/site/ping";
+ private static final String PING_API_V4_PATH = "/api/v4/site/ping";
public CeTool(ShareLinkInfo shareLinkInfo) {
super(shareLinkInfo);
@@ -39,31 +43,132 @@ public class CeTool extends PanBase {
try {
// // 处理URL
URL url = new URL(shareLinkInfo.getShareUrl());
- String downloadApiUrl = url.getProtocol() + "://" + url.getHost() + DOWNLOAD_API_PATH + key + "?path" +
- "=undefined/undefined;";
- String shareApiUrl = url.getProtocol() + "://" + url.getHost() + SHARE_API_PATH + key;
- // 设置cookie
- HttpRequest httpRequest = clientSession.getAbs(shareApiUrl);
- if (pwd != null) {
- httpRequest.addQueryParam("password", pwd);
- }
- // 获取下载链接
- httpRequest.send().onSuccess(res -> {
- try {
- if (res.statusCode() == 200 && res.bodyAsJsonObject().containsKey("code")) {
- getDownURL(downloadApiUrl);
- } else {
- nextParser();
- }
- } catch (Exception e) {
- nextParser();
- }
- }).onFailure(handleFail(shareApiUrl));
+ String baseUrl = url.getProtocol() + "://" + url.getHost();
+
+ // 先检测API版本
+ detectVersionAndParse(baseUrl, key, pwd);
} catch (Exception e) {
fail(e, "URL解析错误");
}
return promise.future();
}
+
+ /**
+ * 检测Cloudreve版本并选择合适的解析器
+ * 先调用 /api/v3/site/ping 或 /api/v4/site/ping 判断是哪个版本
+ * 如果都返回404说明不是Cloudreve盘,则调用nextParser
+ */
+ private void detectVersionAndParse(String baseUrl, String key, String pwd) {
+ String pingUrlV3 = baseUrl + PING_API_V3_PATH;
+
+ // 先尝试v3 ping
+ clientSession.getAbs(pingUrlV3).send().onSuccess(res -> {
+ if (res.statusCode() == 200) {
+ try {
+ JsonObject pingResponse = asJson(res);
+ // 获取到JSON响应,检查是否是4.x版本
+ // 4.x的ping响应可能有不同的结构,我们通过share API来判断
+ checkVersionByShareApi(baseUrl, key, pwd);
+ } catch (Exception e) {
+ // JSON解析失败,尝试v4 ping
+ tryV4PingAndParse(baseUrl, key, pwd);
+ }
+ } else if (res.statusCode() == 404) {
+ // v3 ping不存在,尝试v4
+ tryV4PingAndParse(baseUrl, key, pwd);
+ } else {
+ // 其他错误,不是Cloudreve盘
+ nextParser();
+ }
+ }).onFailure(t -> {
+ // 网络错误,尝试下一个解析器
+ nextParser();
+ });
+ }
+
+ private void tryV4PingAndParse(String baseUrl, String key, String pwd) {
+ String pingUrlV4 = baseUrl + PING_API_V4_PATH;
+
+ clientSession.getAbs(pingUrlV4).send().onSuccess(res -> {
+ if (res.statusCode() == 200) {
+ try {
+ JsonObject pingResponse = asJson(res);
+ // v4 ping成功,使用Ce4Tool
+ delegateToCe4Tool();
+ } catch (Exception e) {
+ // 不是Cloudreve盘
+ nextParser();
+ }
+ } else {
+ // 不是Cloudreve盘
+ nextParser();
+ }
+ }).onFailure(t -> {
+ // 网络错误,尝试下一个解析器
+ nextParser();
+ });
+ }
+
+ /**
+ * 通过Share API的响应来判断版本
+ * 3.x和4.x的share API响应格式可能不同
+ */
+ private void checkVersionByShareApi(String baseUrl, String key, String pwd) {
+ String shareApiUrl = baseUrl + SHARE_API_PATH + key;
+ HttpRequest httpRequest = clientSession.getAbs(shareApiUrl);
+ if (pwd != null) {
+ httpRequest.addQueryParam("password", pwd);
+ }
+
+ httpRequest.send().onSuccess(res -> {
+ try {
+ if (res.statusCode() == 200 && res.bodyAsJsonObject().containsKey("code")) {
+ JsonObject jsonObject = asJson(res);
+ // 检查响应结构来判断版本
+ // 如果share API成功,但download API返回404,说明是4.x
+ // 这里我们先尝试3.x的download API
+ String downloadApiUrl = baseUrl + DOWNLOAD_API_PATH + key + "?path=undefined/undefined;";
+ checkDownloadApi(downloadApiUrl, baseUrl, key, pwd);
+ } else {
+ nextParser();
+ }
+ } catch (Exception e) {
+ nextParser();
+ }
+ }).onFailure(t -> {
+ nextParser();
+ });
+ }
+
+ /**
+ * 检查3.x的download API是否存在
+ * 如果不存在,说明是4.x版本
+ */
+ private void checkDownloadApi(String downloadApiUrl, String baseUrl, String key, String pwd) {
+ clientSession.putAbs(downloadApiUrl).send().onSuccess(res -> {
+ if (res.statusCode() == 404 || res.statusCode() == 405) {
+ // download API不存在或方法不允许,说明是4.x
+ delegateToCe4Tool();
+ } else if (res.statusCode() == 200) {
+ // 3.x版本,继续使用当前逻辑
+ getDownURL(downloadApiUrl);
+ } else {
+ // 其他错误
+ fail("无法确定Cloudreve版本或接口调用失败");
+ }
+ }).onFailure(t -> {
+ // 尝试使用4.x
+ delegateToCe4Tool();
+ });
+ }
+
+ /**
+ * 转发到Ce4Tool处理4.x版本
+ */
+ private void delegateToCe4Tool() {
+ log.debug("检测到Cloudreve 4.x,转发到Ce4Tool处理");
+ new Ce4Tool(shareLinkInfo).parse().onComplete(promise);
+ }
private void getDownURL(String shareApiUrl) {