Compare commits

..

8 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
8582290db3 fix: restore copy of nfd-front to webroot/nfd-front in vue.config.js
Agent-Logs-Url: https://github.com/qaiu/netdisk-fast-download/sessions/e4e9323a-d8f5-48b1-9476-7efab611f978

Co-authored-by: qaiu <29825328+qaiu@users.noreply.github.com>
2026-04-22 04:40:03 +00:00
copilot-swe-agent[bot]
5ff33d7c58 Initial plan 2026-04-22 04:39:22 +00:00
q
0cfb69a240 fix frontend shortcut parsing and proxy static serving 2026-04-22 04:24:22 +08:00
q
110a9beda4 fix parser onedrive url decoding and bump vulnerable deps 2026-04-22 02:03:04 +08:00
qaiu
fd6a3f5929 更新 README.md 2026-04-21 23:22:52 +08:00
qaiu
82ad6ec427 更新 README.md 2026-04-20 07:51:57 +08:00
qaiu
1bfc7c960d 更新 README.md 2026-04-19 19:34:57 +08:00
qaiu
332f49f483 更新 README.md 2026-04-19 19:32:13 +08:00
9 changed files with 292 additions and 79 deletions

2
.gitignore vendored
View File

@@ -41,7 +41,9 @@ gradlew.bat
unused.txt
/web-service/src/main/generated/
/db
/netdisk-fast-download/
/webroot/nfd-front/
/netdisk-fast-download/webroot/nfd-front/
package-lock.json
# Maven generated files

View File

@@ -29,7 +29,8 @@ QQ交流群1017480890
[API接入](https://nfdparser.apifox.cn/)
[公益解析lz站](https://lz.qaiu.top)
[公益解析lz0站](https://lz0.qaiu.top)
[专业版189站注册体验](https://189.qaiu.top)
[专业版](https://189.qaiu.top)
## 快速开始
命令行下载分享文件:
@@ -58,7 +59,7 @@ https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.
**注意⚠小飞机解析有IP限制多数云服务商的大陆IP会被拦截可以自行配置代理和本程序无关**
**注意⚠️收到很多用户反馈,小飞机近期封号频繁,请尽可能选择其他网盘分享**
**注意⚠️请不要过度依赖 lz.qaiu.top建议本地搭建或者云服务器自行搭建。请求量过多的话服务器可能会被云盘厂商限制遇到解析失败的分享链接不要着急提issues请先检查分享是否有效** [lz站](https://lz.qaiu.top) 和 [lz0](https://lz0.qaiu.top) 不支持大文件,请使用 [189站](https://189.qaiu.top) 注册体验。
**注意⚠️请不要过度依赖 lz.qaiu.top建议本地搭建或者云服务器自行搭建。请求量过多的话服务器可能会被云盘厂商限制遇到解析失败的分享链接不要着急提issues请先检查分享是否有效**
## 网盘支持情况:
> 20230905 奶牛云直链做了防盗链需加入请求头Referer: https://cowtransfer.com/
@@ -90,17 +91,10 @@ https://nfd-parser.github.io/nfd-preview/preview.html?src=https%3A%2F%2Flz.qaiu.
- [WPS云文档-pwps](https://www.kdocs.cn/)
- [汽水音乐-qishui_music](https://music.douyin.com/qishui/)
- [咪咕音乐-migu](https://music.migu.cn/)
- [一刻相册-baidu_photo](https://photo.baidu.com/)
- Google云盘-pgd
- Onedrive-pod
- Dropbox-pdp
- iCloud-pic
### 专属版提供
- [夸克云盘-qk](https://pan.quark.cn/)
- [UC云盘-uc](https://fast.uc.cn/)
- [移动云盘-p139](https://yun.139.com/)
- [联通云盘-pwo](https://pan.wo.cn/)
- [天翼云盘-p189](https://cloud.189.cn/)
## API接口
@@ -332,10 +326,9 @@ json返回数据格式示例:
| 网盘名称 | 免登陆下载分享 | 加密分享 | 初始网盘空间 | 单文件大小限制 |
|-------------|---------|----------|-----------|-----------------|
| 蓝奏云 | √ | √ | 不限空间 | 100M |
| 奶牛快传 | √ | X | 10G | 不限大小 |
| 移动云云空间(个人版) | √ | √(密码可忽略) | 5G(个人) | 不限大小 |
| 小飞机网盘 | √ | √(密码可忽略) | 10G | 不限大小 |
| 360亿方云 | √ | √(密码可忽略) | 100G(须实名) | 不限大小 |
| 小飞机网盘 | √ | √ | 10G | 不限大小 |
| 360亿方云 | √ | √ | 100G(须实名) | 不限大小 |
| 123云盘 | √ | √ | 2T | 100G>100M需要登录 |
| 文叔叔 | √ | √ | 10G | 5GB |
| WPS云文档 | √ | X | 5G(免费) | 10M(免费)/2G(会员) |
@@ -494,23 +487,6 @@ auths:
**注意:** 目前仅支持 123ye的认证配置。
## 开发计划
### v0.1.8~v0.1.9 ✓
- API添加文件信息(专属版/开源版)
- 目录解析(专属版/开源版)
- 文件预览功能(专属版/开源版)
- 文件夹预览功能(开源版)
- 友好的错误提示和一键反馈功能(开源版)
- 带cookie/token/username/pwd参数解析大文件(专属版)
### v0.2.x
- web后台管理--认证配置/分享链接管理(开源版/专属版)
- 123/小飞机/蓝奏优享等大文件解析(开源版)
- 直链分享(开源版/专属版)
- aria2/idm+/curl/wget链接生成(开源版/专属版)
- IP限流配置(开源版/专属版)
- refere防盗链API鉴权防盗链(专属版)
- 123/小飞机/蓝奏优享/蓝奏文件夹解析API天翼云盘/移动云盘文件夹解析API(专属版)
- 用户管理面板--营销推广系统(专属版)
**技术栈:**
Jdk17+Vert.x4
@@ -535,20 +511,6 @@ Core模块集成Vert.x实现类似spring的注解式路由API
</p>
### 关于赞助定制专属版
1. 专属版提供对小飞机,蓝奏优享大文件解析的支持, 提供天翼云盘/移动云盘/联通云盘的解析支持。
2. 可提供托管服务:包含部署服务和云服务器环境。
3. 可提供功能定制开发。
您可能需要提供一定的资金赞助支持定制专属版, 请添加以下任意一个联系方式详谈赞助模式:
<p>qq: 197575894</p>
<p>wechat: imcoding_</p>
<!--
![image](https://github.com/qaiu/netdisk-fast-download/assets/29825328/54276aee-cc3f-4ebd-8973-2e15f6295819)
[手机端支付宝打赏跳转链接](https://qr.alipay.com/fkx01882dnoxxtjenhlxt53)
-->

View File

@@ -22,6 +22,7 @@ import org.slf4j.LoggerFactory;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Path;
import java.util.Map;
/**
@@ -77,25 +78,13 @@ public class ReverseProxyVerticle extends AbstractVerticle {
* @param proxyConf 代理配置
*/
private void handleProxyConf(JsonObject proxyConf) {
// page404 path
if (proxyConf.containsKey(
"page404")) {
System.getProperty("user.dir");
String path = proxyConf.getString("page404");
if (StringUtils.isEmpty(path)) {
proxyConf.put("page404", DEFAULT_PATH_404);
} else {
if (!path.startsWith("/")) {
path = "/" + path;
}
if (!new File(System.getProperty("user.dir") + path).exists()) {
proxyConf.put("page404", DEFAULT_PATH_404);
}
}
} else {
proxyConf.put("page404", DEFAULT_PATH_404);
// page404 path: 兼容不同启动目录(根目录或子模块目录)
String configured404 = proxyConf.getString("page404");
String resolved404 = resolveExistingPath(configured404, false);
if (resolved404 == null) {
resolved404 = resolveExistingPath(DEFAULT_PATH_404, false);
}
proxyConf.put("page404", resolved404 == null ? DEFAULT_PATH_404 : resolved404);
final HttpClient httpClient = VertxHolder.getVertxInstance().createHttpClient();
Router proxyRouter = Router.router(vertx);
@@ -180,7 +169,14 @@ public class ReverseProxyVerticle extends AbstractVerticle {
StaticHandler staticHandler;
if (staticConf.containsKey("root")) {
staticHandler = StaticHandler.create(staticConf.getString("root"));
String configuredRoot = staticConf.getString("root");
String resolvedRoot = resolveStaticRoot(configuredRoot);
if (resolvedRoot != null) {
staticHandler = StaticHandler.create(resolvedRoot);
} else {
LOGGER.warn("static root not found, fallback to configured path: {}", configuredRoot);
staticHandler = StaticHandler.create(configuredRoot);
}
} else {
staticHandler = StaticHandler.create();
}
@@ -253,4 +249,77 @@ public class ReverseProxyVerticle extends AbstractVerticle {
});
}
/**
* 解析配置路径: 优先绝对路径, 否则尝试 user.dir 和 user.dir/..。
*/
private String resolveExistingPath(String path, boolean directory) {
if (StringUtils.isBlank(path)) {
return null;
}
File directFile = new File(path);
if (existsByType(directFile, directory)) {
return directFile.getAbsolutePath();
}
String userDir = System.getProperty("user.dir");
File inUserDir = new File(userDir, path);
if (existsByType(inUserDir, directory)) {
return inUserDir.getAbsolutePath();
}
File inParentDir = new File(new File(userDir).getParentFile(), path);
if (existsByType(inParentDir, directory)) {
return inParentDir.getAbsolutePath();
}
return null;
}
/**
* StaticHandler 只接受相对 web root不接受以 / 开头的绝对路径。
*/
private String resolveStaticRoot(String path) {
if (StringUtils.isBlank(path)) {
return null;
}
File directFile = new File(path);
if (existsByType(directFile, true)) {
return path;
}
String userDir = System.getProperty("user.dir");
File inUserDir = new File(userDir, path);
if (existsByType(inUserDir, true)) {
return relativizePath(new File(userDir), inUserDir);
}
File userDirFile = new File(userDir);
File parentDir = userDirFile.getParentFile();
File inParentDir = parentDir == null ? null : new File(parentDir, path);
if (existsByType(inParentDir, true)) {
return relativizePath(userDirFile, inParentDir);
}
return null;
}
private String relativizePath(File baseDir, File target) {
try {
Path basePath = baseDir.toPath().toAbsolutePath().normalize();
Path targetPath = target.toPath().toAbsolutePath().normalize();
return basePath.relativize(targetPath).toString().replace(File.separatorChar, '/');
} catch (IllegalArgumentException ignored) {
return target.getPath().replace(File.separatorChar, '/');
}
}
private boolean existsByType(File file, boolean directory) {
if (file == null || !file.exists()) {
return false;
}
return directory ? file.isDirectory() : file.isFile();
}
}

View File

@@ -59,12 +59,12 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- Versions -->
<vertx.version>4.5.22</vertx.version>
<vertx.version>4.5.24</vertx.version>
<org.reflections.version>0.10.2</org.reflections.version>
<lombok.version>1.18.38</lombok.version>
<slf4j.version>2.0.5</slf4j.version>
<slf4j.version>2.0.16</slf4j.version>
<commons-lang3.version>3.18.0</commons-lang3.version>
<jackson.version>2.14.2</jackson.version>
<jackson.version>2.18.6</jackson.version>
<logback.version>1.5.19</logback.version>
<junit.version>4.13.2</junit.version>
</properties>

View File

@@ -99,7 +99,8 @@ public class PodTool extends PanBase {
Matcher matcher1 =
Pattern.compile("\"downloadUrl\":\"(?<url>https?://[^\s\"]+)").matcher(body);
if (matcher1.find()) {
complete(matcher1.group("url"));
// 响应体是 JSON 文本URL 中的 '&' 被转义为 \u0026需要反转义
complete(unescapeJsonUnicode(matcher1.group("url")));
} else {
fail();
}
@@ -134,6 +135,34 @@ public class PodTool extends PanBase {
throw new RuntimeException("URL匹配失败");
}
/**
* 反转义 JSON 响应文本中残留的 Unicode 转义序列(主要是 \u0026 -> &)。
* 主分支通过正则直接从 JSON 原文抠 URL未经过 JSON 解析器,需要手动还原。
*/
private String unescapeJsonUnicode(String s) {
if (s == null || s.indexOf("\\u") < 0) {
return s;
}
StringBuilder sb = new StringBuilder(s.length());
int i = 0;
while (i < s.length()) {
char c = s.charAt(i);
if (c == '\\' && i + 5 < s.length() && s.charAt(i + 1) == 'u') {
try {
int cp = Integer.parseInt(s.substring(i + 2, i + 6), 16);
sb.append((char) cp);
i += 6;
continue;
} catch (NumberFormatException ignored) {
// 非法转义按原样保留
}
}
sb.append(c);
i++;
}
return sb.toString();
}
private String matcherToken(String html) {
// 正则表达式来匹配 inputElem.value 中的 Token
@@ -228,4 +257,4 @@ public class PodTool extends PanBase {
return promise.future();
}
}
}

View File

@@ -26,13 +26,13 @@
<packageDirectory>${project.basedir}/web-service/target/package</packageDirectory>
<!-- Vert.x 4.5.24 已包含安全修复,无需单独指定 Netty 版本 -->
<vertx.version>4.5.14</vertx.version>
<vertx.version>4.5.24</vertx.version>
<org.reflections.version>0.10.2</org.reflections.version>
<lombok.version>1.18.38</lombok.version>
<slf4j.version>2.0.16</slf4j.version>
<commons-lang3.version>3.18.0</commons-lang3.version>
<commons-beanutils2.version>2.0.0</commons-beanutils2.version>
<jackson.version>2.18.2</jackson.version>
<jackson.version>2.18.6</jackson.version>
<!-- Logback 最新稳定版 -->
<logback.version>1.5.18</logback.version>
<junit.version>4.13.2</junit.version>

View File

@@ -344,7 +344,7 @@
<script>
import axios from 'axios'
import { ElTree } from 'element-plus'
import { ElTree, ElMessageBox } from 'element-plus'
import { Splitpanes, Pane } from 'splitpanes'
import 'splitpanes/dist/splitpanes.css'
import fileTypeUtils from '@/utils/fileTypeUtils'
@@ -677,7 +677,31 @@ export default {
this.$message.success(`自动检测到 ${detected.type} ${detected.version}`)
return true
}
this.$message.error('下载器连接失败,请先在首页设置中配置下载器')
try {
await ElMessageBox.confirm(
'未检测到本地下载器,是否切换为迅雷下载?',
'下载器未检测到',
{
confirmButtonText: '使用迅雷',
cancelButtonText: '取消',
type: 'warning'
}
)
const thunderConfig = {
...config,
downloaderType: 'thunder',
rpcUrl: ''
}
saveConfig(thunderConfig)
const thunderResult = await testConnection()
if (thunderResult.connected) {
this.$message.success('已切换并保存为迅雷下载器配置')
return true
}
this.$message.error('已保存为迅雷配置,但未检测到迅雷客户端,请先启动迅雷')
} catch {
this.$message.error('下载器连接失败,请先在首页设置中配置下载器')
}
return false
},
async sendSingleToDownloader(file) {

View File

@@ -73,9 +73,9 @@
</div>
<!-- 项目简介移到卡片内 -->
<div class="project-intro">
<div class="intro-title">NFD网盘直链解析0.2.1b3</div>
<div class="intro-title">NFD网盘直链解析0.3.0</div>
<div class="intro-desc">
<div>支持网盘蓝奏云蓝奏云优享小飞机盘123云盘奶牛快传移动云空间QQ邮箱云盘QQ闪传等 <el-link style="color:#606cf5" href="https://github.com/qaiu/netdisk-fast-download?tab=readme-ov-file#%E7%BD%91%E7%9B%98%E6%94%AF%E6%8C%81%E6%83%85%E5%86%B5" target="_blank"> &gt;&gt; </el-link></div>
<div>支持网盘蓝奏云蓝奏云优享小飞机盘123云盘iCloud移动云空间联想乐云QQ闪传等 <el-link style="color:#606cf5" href="https://github.com/qaiu/netdisk-fast-download?tab=readme-ov-file#%E7%BD%91%E7%9B%98%E6%94%AF%E6%8C%81%E6%83%85%E5%86%B5" target="_blank"> &gt;&gt; </el-link></div>
<div>文件夹解析支持蓝奏云蓝奏云优享小飞机盘123云盘</div>
</div>
</div>
@@ -111,8 +111,8 @@
</el-input>
<p style="text-align: center">
<el-button style="margin-left: 40px" @click="parseFile">解析文件</el-button>
<el-button style="margin-left: 20px" @click="parseDirectory">解析目录</el-button>
<el-button class="parse-action-btn" type="success" style="margin-left: 40px" @click="parseFile">解析文件</el-button>
<el-button class="parse-action-btn" type="success" style="margin-left: 20px" @click="parseDirectory">解析目录</el-button>
<el-button style="margin-left: 20px" @click="generateMarkdown">生成Markdown</el-button>
<el-button style="margin-left: 20px" @click="generateQRCode">扫码下载</el-button>
<el-button style="margin-left: 20px" @click="getStatistics">分享统计</el-button>
@@ -592,7 +592,7 @@ import DirectoryTree from '@/components/DirectoryTree'
import DownloadDialog from '@/components/DownloadDialog'
import parserUrl from '../parserUrl1'
import fileTypeUtils from '@/utils/fileTypeUtils'
import { ElMessage } from 'element-plus'
import { ElMessage, ElMessageBox } from 'element-plus'
import { playgroundApi } from '@/utils/playgroundApi'
import { testConnection, autoDetect, addDownload, getConfig, saveConfig } from '@/utils/downloaderService'
@@ -708,7 +708,9 @@ export default {
downloadDialogVisible: false,
downloadDialogInfo: null,
// 目录解析支持的网盘列表
directoryParseSupportedPans: []
directoryParseSupportedPans: [],
// 后端支持网盘列表(用于短格式 type:key@pwd 展开)
panList: []
}
},
computed: {
@@ -1041,6 +1043,7 @@ export default {
// 验证输入
validateInput() {
this.normalizeShortcutInput()
this.clearResults()
if (!this.link.startsWith("https://") && !this.link.startsWith("http://")) {
@@ -1049,6 +1052,58 @@ export default {
}
},
// 获取后端支持网盘列表
async getPanList() {
try {
const response = await axios.get(`${this.baseAPI}/v2/getPanList`)
const payload = response?.data
const list = Array.isArray(payload)
? payload
: (Array.isArray(payload?.data) ? payload.data : [])
if (list.length > 0) {
this.panList = list
}
} catch (error) {
// 静默失败:短格式解析会自动回退
}
},
// 按后端网盘列表展开短格式type:key@pwd
expandShortFormat(text) {
const raw = (text || '').trim()
if (!raw) return null
const shortMatch = raw.match(/^([a-zA-Z][a-zA-Z0-9]{1,10}):([^@]+?)(?:@(.+))?$/)
if (!shortMatch) return null
const [, shortType, shortKey, shortPwd] = shortMatch
const pan = this.panList.find(p => (p.type || '').toLowerCase() === shortType.toLowerCase())
if (!pan || !pan.shareUrlFormat) return null
const link = pan.shareUrlFormat
.replace('{shareKey}', shortKey)
.replace(/\{pwd}/g, shortPwd || '')
return {
link,
pwd: shortPwd || '',
name: pan.name || pan.type || shortType
}
},
// 识别并转换短链输入(如 lz:shareKey@pwd
normalizeShortcutInput() {
const shortInfo = this.expandShortFormat(this.link)
if (!shortInfo) return
this.link = shortInfo.link
if (!this.password && shortInfo.pwd) {
this.password = shortInfo.pwd
}
this.$message.success(`已识别短格式并自动转换,网盘类型: ${shortInfo.name}`)
this.updateDirectLink()
},
// 清除结果
clearResults() {
this.parseResult = {}
@@ -1265,6 +1320,23 @@ export default {
try {
const text = await navigator.clipboard.readText()
console.log('获取到的文本内容是:', text)
const shortInfo = this.expandShortFormat(text)
if (shortInfo) {
if (shortInfo.link !== this.link || shortInfo.pwd !== this.password) {
this.password = shortInfo.pwd
this.link = shortInfo.link
this.updateDirectLink()
if (!this.hasClipboardSuccessTip) {
this.$message.success(`自动识别分享成功, 网盘类型: ${shortInfo.name}; 分享URL ${this.link}; 分享密码: ${this.password || '空'}`)
this.hasClipboardSuccessTip = true
}
} else {
this.$message.warning(`[${shortInfo.name}]分享信息无变化`)
}
this.hasWarnedNoLink = false
return
}
const linkInfo = parserUrl.parseLink(text)
const pwd = parserUrl.parsePwd(text) || ''
@@ -1375,6 +1447,7 @@ export default {
// 跳转到客户端链接页面
async goToClientLinks() {
this.normalizeShortcutInput()
// 验证输入
if (!this.link.trim()) {
this.$message.warning('请先输入分享链接')
@@ -1550,9 +1623,19 @@ export default {
aria2: 'http://localhost:6800/jsonrpc',
thunder: ''
}
// 切换类型时先清空旧连接状态,避免显示残留版本信息
this.aria2Connected = false
this.aria2Version = ''
if (defaults[this.aria2ConfigForm.downloaderType] !== undefined) {
this.aria2ConfigForm.rpcUrl = defaults[this.aria2ConfigForm.downloaderType]
}
// 非迅雷类型在切换后自动静默重测,刷新连接状态
if (this.aria2ConfigForm.downloaderType !== 'thunder') {
this.$nextTick(() => this.testAria2Connection(true))
}
},
async testAria2Connection(silent = false) {
this.aria2Testing = true
@@ -1599,7 +1682,25 @@ export default {
this.aria2Version = result.version || ''
this.$message.success(`检测到 ${this.downloaderTypeName} ${this.aria2Version}`)
} else {
this.$message.warning('未检测到本地下载器,请确认 Motrix/Gopeed/Aria2 正在运行')
try {
await ElMessageBox.confirm(
'未检测到本地下载器,是否切换为迅雷下载?',
'下载器未检测到',
{
confirmButtonText: '使用迅雷',
cancelButtonText: '取消',
type: 'warning'
}
)
this.aria2ConfigForm.downloaderType = 'thunder'
this.aria2ConfigForm.rpcUrl = ''
saveConfig(this.aria2ConfigForm)
this.$message.success('已切换并保存为迅雷下载器配置')
this.aria2DialogVisible = true
await this.testAria2Connection(true)
} catch {
this.$message.warning('未检测到本地下载器,请确认 Motrix/Gopeed/Aria2 正在运行')
}
}
} catch (e) {
this.$message.error('自动检测失败:' + e.message)
@@ -1676,6 +1777,9 @@ export default {
// 初始化下载器配置
this.getAria2Config()
// 拉取后端网盘支持列表(用于 type:key@pwd 短格式)
this.getPanList()
// 自动读取剪切板
if (this.autoReadClipboard) {
this.getPaste()
@@ -2094,4 +2198,26 @@ hr {
#app.dark-theme .el-form-item__label {
color: #ccc;
}
/* 解析按钮专用配色:亮色浅绿,暗色深绿 */
.parse-action-btn.el-button--success {
--el-button-bg-color: #7fcb96;
--el-button-border-color: #7fcb96;
--el-button-text-color: #f7fff9;
--el-button-hover-bg-color: #93d8a8;
--el-button-hover-border-color: #93d8a8;
--el-button-active-bg-color: #69b884;
--el-button-active-border-color: #69b884;
}
#app.dark-theme .parse-action-btn.el-button--success,
body.dark-theme .parse-action-btn.el-button--success {
--el-button-bg-color: #1f6b3a;
--el-button-border-color: #1f6b3a;
--el-button-text-color: #ecf9f0;
--el-button-hover-bg-color: #2b7d49;
--el-button-hover-border-color: #2b7d49;
--el-button-active-bg-color: #185731;
--el-button-active-border-color: #185731;
}
</style>

View File

@@ -85,7 +85,8 @@ module.exports = {
{
source: './node_modules/monaco-editor/min/vs',
destination: './nfd-front/js/vs'
}
},
{ source: './nfd-front', destination: '../webroot/nfd-front' }
],
archive: [ //然后我们选择dist文件夹将之打包成dist.zip并放在根目录
{