fix frontend shortcut parsing and proxy static serving

This commit is contained in:
q
2026-04-22 04:24:22 +08:00
parent 110a9beda4
commit 0cfb69a240
4 changed files with 249 additions and 28 deletions

2
.gitignore vendored
View File

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

View File

@@ -22,6 +22,7 @@ import org.slf4j.LoggerFactory;
import java.io.File; import java.io.File;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.nio.file.Path;
import java.util.Map; import java.util.Map;
/** /**
@@ -77,25 +78,13 @@ public class ReverseProxyVerticle extends AbstractVerticle {
* @param proxyConf 代理配置 * @param proxyConf 代理配置
*/ */
private void handleProxyConf(JsonObject proxyConf) { private void handleProxyConf(JsonObject proxyConf) {
// page404 path // page404 path: 兼容不同启动目录(根目录或子模块目录)
if (proxyConf.containsKey( String configured404 = proxyConf.getString("page404");
String resolved404 = resolveExistingPath(configured404, false);
"page404")) { if (resolved404 == null) {
System.getProperty("user.dir"); resolved404 = resolveExistingPath(DEFAULT_PATH_404, false);
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);
} }
proxyConf.put("page404", resolved404 == null ? DEFAULT_PATH_404 : resolved404);
final HttpClient httpClient = VertxHolder.getVertxInstance().createHttpClient(); final HttpClient httpClient = VertxHolder.getVertxInstance().createHttpClient();
Router proxyRouter = Router.router(vertx); Router proxyRouter = Router.router(vertx);
@@ -180,7 +169,14 @@ public class ReverseProxyVerticle extends AbstractVerticle {
StaticHandler staticHandler; StaticHandler staticHandler;
if (staticConf.containsKey("root")) { 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 { } else {
staticHandler = StaticHandler.create(); 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

@@ -344,7 +344,7 @@
<script> <script>
import axios from 'axios' import axios from 'axios'
import { ElTree } from 'element-plus' import { ElTree, ElMessageBox } from 'element-plus'
import { Splitpanes, Pane } from 'splitpanes' import { Splitpanes, Pane } from 'splitpanes'
import 'splitpanes/dist/splitpanes.css' import 'splitpanes/dist/splitpanes.css'
import fileTypeUtils from '@/utils/fileTypeUtils' import fileTypeUtils from '@/utils/fileTypeUtils'
@@ -677,7 +677,31 @@ export default {
this.$message.success(`自动检测到 ${detected.type} ${detected.version}`) this.$message.success(`自动检测到 ${detected.type} ${detected.version}`)
return true return true
} }
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('下载器连接失败,请先在首页设置中配置下载器') this.$message.error('下载器连接失败,请先在首页设置中配置下载器')
}
return false return false
}, },
async sendSingleToDownloader(file) { async sendSingleToDownloader(file) {

View File

@@ -73,9 +73,9 @@
</div> </div>
<!-- 项目简介移到卡片内 --> <!-- 项目简介移到卡片内 -->
<div class="project-intro"> <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 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>文件夹解析支持蓝奏云蓝奏云优享小飞机盘123云盘</div>
</div> </div>
</div> </div>
@@ -111,8 +111,8 @@
</el-input> </el-input>
<p style="text-align: center"> <p style="text-align: center">
<el-button style="margin-left: 40px" @click="parseFile">解析文件</el-button> <el-button class="parse-action-btn" type="success" 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: 20px" @click="parseDirectory">解析目录</el-button>
<el-button style="margin-left: 20px" @click="generateMarkdown">生成Markdown</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="generateQRCode">扫码下载</el-button>
<el-button style="margin-left: 20px" @click="getStatistics">分享统计</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 DownloadDialog from '@/components/DownloadDialog'
import parserUrl from '../parserUrl1' import parserUrl from '../parserUrl1'
import fileTypeUtils from '@/utils/fileTypeUtils' import fileTypeUtils from '@/utils/fileTypeUtils'
import { ElMessage } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { playgroundApi } from '@/utils/playgroundApi' import { playgroundApi } from '@/utils/playgroundApi'
import { testConnection, autoDetect, addDownload, getConfig, saveConfig } from '@/utils/downloaderService' import { testConnection, autoDetect, addDownload, getConfig, saveConfig } from '@/utils/downloaderService'
@@ -708,7 +708,9 @@ export default {
downloadDialogVisible: false, downloadDialogVisible: false,
downloadDialogInfo: null, downloadDialogInfo: null,
// 目录解析支持的网盘列表 // 目录解析支持的网盘列表
directoryParseSupportedPans: [] directoryParseSupportedPans: [],
// 后端支持网盘列表(用于短格式 type:key@pwd 展开)
panList: []
} }
}, },
computed: { computed: {
@@ -1041,6 +1043,7 @@ export default {
// 验证输入 // 验证输入
validateInput() { validateInput() {
this.normalizeShortcutInput()
this.clearResults() this.clearResults()
if (!this.link.startsWith("https://") && !this.link.startsWith("http://")) { 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() { clearResults() {
this.parseResult = {} this.parseResult = {}
@@ -1266,6 +1321,23 @@ export default {
const text = await navigator.clipboard.readText() const text = await navigator.clipboard.readText()
console.log('获取到的文本内容是:', text) 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 linkInfo = parserUrl.parseLink(text)
const pwd = parserUrl.parsePwd(text) || '' const pwd = parserUrl.parsePwd(text) || ''
@@ -1375,6 +1447,7 @@ export default {
// 跳转到客户端链接页面 // 跳转到客户端链接页面
async goToClientLinks() { async goToClientLinks() {
this.normalizeShortcutInput()
// 验证输入 // 验证输入
if (!this.link.trim()) { if (!this.link.trim()) {
this.$message.warning('请先输入分享链接') this.$message.warning('请先输入分享链接')
@@ -1550,9 +1623,19 @@ export default {
aria2: 'http://localhost:6800/jsonrpc', aria2: 'http://localhost:6800/jsonrpc',
thunder: '' thunder: ''
} }
// 切换类型时先清空旧连接状态,避免显示残留版本信息
this.aria2Connected = false
this.aria2Version = ''
if (defaults[this.aria2ConfigForm.downloaderType] !== undefined) { if (defaults[this.aria2ConfigForm.downloaderType] !== undefined) {
this.aria2ConfigForm.rpcUrl = defaults[this.aria2ConfigForm.downloaderType] this.aria2ConfigForm.rpcUrl = defaults[this.aria2ConfigForm.downloaderType]
} }
// 非迅雷类型在切换后自动静默重测,刷新连接状态
if (this.aria2ConfigForm.downloaderType !== 'thunder') {
this.$nextTick(() => this.testAria2Connection(true))
}
}, },
async testAria2Connection(silent = false) { async testAria2Connection(silent = false) {
this.aria2Testing = true this.aria2Testing = true
@@ -1599,8 +1682,26 @@ export default {
this.aria2Version = result.version || '' this.aria2Version = result.version || ''
this.$message.success(`检测到 ${this.downloaderTypeName} ${this.aria2Version}`) this.$message.success(`检测到 ${this.downloaderTypeName} ${this.aria2Version}`)
} else { } else {
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 正在运行') this.$message.warning('未检测到本地下载器,请确认 Motrix/Gopeed/Aria2 正在运行')
} }
}
} catch (e) { } catch (e) {
this.$message.error('自动检测失败:' + e.message) this.$message.error('自动检测失败:' + e.message)
} finally { } finally {
@@ -1676,6 +1777,9 @@ export default {
// 初始化下载器配置 // 初始化下载器配置
this.getAria2Config() this.getAria2Config()
// 拉取后端网盘支持列表(用于 type:key@pwd 短格式)
this.getPanList()
// 自动读取剪切板 // 自动读取剪切板
if (this.autoReadClipboard) { if (this.autoReadClipboard) {
this.getPaste() this.getPaste()
@@ -2094,4 +2198,26 @@ hr {
#app.dark-theme .el-form-item__label { #app.dark-theme .el-form-item__label {
color: #ccc; 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> </style>