fix(server): 改用 Express 5 + path-to-regexp v7 兼容的通配写法,避免路由启动崩溃

Express 5 内置的 path-to-regexp v7 不再支持裸 '*',也不支持
自定义正则 '.*'(会报 Missing parameter name)。Express 5
官方兼容的单段全通写是 ':name(*)',并且路由注册会在 app 启动
第一时间完成(出错会直接崩)。

同时为了兼容旧 URL 行为,保留 express.static 前置挂载 +
显式 app.get 后备两条路径,因为 Audio 模块可能需要带
Cache-Control / Range / Content-Type 等头的 custom sendBinaryFile
处理(例如 station 动态裁剪音频),所以不能直接用 static 替代。

修复 7 条路由:
  /media/audio/general/:path(*)
  /media/audio/beep/:path(*)
  /media/audio/music/:path(*)
  /media/audio/voice/:path(*)
  /media/audio/line/:path(*)
  /media/audio/station/:path(*)
  /media/temp/:path(*)

参数解码加 decodeURIComponent(),兼容中文/空格文件名。
客户端 bump v1.0.101 -> v1.0.102
This commit is contained in:
2026-08-17 15:41:36 +08:00
parent fc66e43379
commit 5c462a4d09
2 changed files with 23 additions and 17 deletions
+22 -16
View File
@@ -148,44 +148,50 @@ function safeRelativeToBase(baseDir, relativePath) {
return full;
}
app.get('/media/audio/general/:relPath(.*)', (req, res) => {
const rel = req.params.relPath || '';
app.use('/media/audio/general', express.static(getBeepAudioDirs(), { fallthrough: true }));
app.use('/media/audio/beep', express.static(getBeepAudioDirs(), { fallthrough: true }));
app.use('/media/audio/music', express.static(audioMusicDir, { fallthrough: true }));
app.use('/media/audio/voice', express.static(audioVoiceDir, { fallthrough: true }));
app.use('/media/audio/line', express.static(audioLineDir, { fallthrough: true }));
app.use('/media/audio/station', express.static(audioStationDir, { fallthrough: true }));
app.use('/media/temp', express.static(tempBroadcastDir, { fallthrough: true }));
app.get('/media/audio/general/:path(*)', (req, res) => {
const rel = decodeURIComponent(req.params.path || '');
const candidates = getBeepAudioDirs().map(dir => safeRelativeToBase(dir, rel)).filter(Boolean);
sendBinaryFileFromCandidates(res, candidates);
});
app.get('/media/audio/beep/:relPath(.*)', (req, res) => {
const rel = req.params.relPath || '';
app.get('/media/audio/beep/:path(*)', (req, res) => {
const rel = decodeURIComponent(req.params.path || '');
const candidates = getBeepAudioDirs().map(dir => safeRelativeToBase(dir, rel)).filter(Boolean);
sendBinaryFileFromCandidates(res, candidates);
});
app.get('/media/audio/music/:relPath(.*)', (req, res) => {
const rel = req.params.relPath || '';
app.get('/media/audio/music/:path(*)', (req, res) => {
const rel = decodeURIComponent(req.params.path || '');
const full = safeRelativeToBase(audioMusicDir, rel);
if (!full) return res.status(404).end();
sendBinaryFile(res, full);
});
app.get('/media/audio/voice/:relPath(.*)', (req, res) => {
const rel = req.params.relPath || '';
app.get('/media/audio/voice/:path(*)', (req, res) => {
const rel = decodeURIComponent(req.params.path || '');
const full = safeRelativeToBase(audioVoiceDir, rel);
if (!full) return res.status(404).end();
sendBinaryFile(res, full);
});
app.get('/media/audio/line/:relPath(.*)', (req, res) => {
const rel = req.params.relPath || '';
app.get('/media/audio/line/:path(*)', (req, res) => {
const rel = decodeURIComponent(req.params.path || '');
const full = safeRelativeToBase(audioLineDir, rel);
if (!full) return res.status(404).end();
sendBinaryFile(res, full);
});
app.get('/media/audio/station/:relPath(.*)', (req, res) => {
const rel = req.params.relPath || '';
app.get('/media/audio/station/:path(*)', (req, res) => {
const rel = decodeURIComponent(req.params.path || '');
const full = safeRelativeToBase(audioStationDir, rel);
if (!full) return res.status(404).end();
sendBinaryFile(res, full);
});
app.get('/media/temp/:relPath(.*)', (req, res) => {
const rel = req.params.relPath || '';
app.get('/media/temp/:path(*)', (req, res) => {
const rel = decodeURIComponent(req.params.path || '');
const full = safeRelativeToBase(tempBroadcastDir, rel);
if (!full) return res.status(404).end();
sendBinaryFile(res, full);