fix(local-music): 注册 local 协议为特权 scheme 修复本地音乐 CORS 报错

- main/index.ts 在 app.whenReady 之前调用 registerSchemesAsPrivileged,
  把 local 标记为 standard/secure/supportFetchAPI/stream/corsEnabled/bypassCSP,
  让 http 页面(dev server / 生产)可跨协议加载本地音频
- fileManager.ts 用 Electron 25 起推荐的 protocol.handle 替代已弃用的
  registerFileProtocol,内部转发到 file:// 自动支持 Range / 流式播放
- 修复 macOS/Linux 上去掉前导斜杠后丢失绝对路径前缀的问题
This commit is contained in:
chengww
2026-05-17 19:48:43 +08:00
parent ee98eb0266
commit 0c0189bcef
2 changed files with 39 additions and 17 deletions
+18 -1
View File
@@ -1,7 +1,24 @@
import { electronApp, optimizer } from '@electron-toolkit/utils';
import { app, ipcMain, nativeImage, session } from 'electron';
import { app, ipcMain, nativeImage, protocol, session } from 'electron';
import { join } from 'path';
// 必须在 app.whenReady() 之前注册自定义协议为特权协议,
// 否则 http(s) 页面(dev server、生产环境的 file://)无法把 local:// 当成
// 安全/可 fetch/可流式的资源加载,会触发 CORS 拦截或 net::ERR_UNKNOWN_URL_SCHEME
protocol.registerSchemesAsPrivileged([
{
scheme: 'local',
privileges: {
standard: true,
secure: true,
supportFetchAPI: true,
stream: true,
bypassCSP: true,
corsEnabled: true
}
}
]);
import type { Language } from '../i18n/main';
import i18n from '../i18n/main';
import { loadLyricWindow } from './lyric';
+21 -16
View File
@@ -1,7 +1,8 @@
import { app, dialog, ipcMain, protocol, shell } from 'electron';
import { app, dialog, ipcMain, net, protocol, shell } from 'electron';
import Store from 'electron-store';
import * as fs from 'fs';
import * as path from 'path';
import { pathToFileURL } from 'url';
import { getStore } from './config';
@@ -28,31 +29,35 @@ function sanitizeFilename(filename: string): string {
*/
export function initializeFileManager() {
// 注册本地文件协议
protocol.registerFileProtocol('local', (request, callback) => {
// Electron 25+ 起 registerFileProtocol 已弃用,改用 protocol.handle,并配合 main/index.ts
// 中的 registerSchemesAsPrivileged,让 audio 元素能从 http(s) 页面跨协议加载本地文件
protocol.handle('local', async (request) => {
try {
const url = request.url;
// local://C:/Users/xxx.mp3
let filePath = decodeURIComponent(url.replace('local:///', ''));
// local:///<absolute-path>
let filePath = decodeURIComponent(request.url.replace(/^local:\/\/\/?/, ''));
// 兼容 local:///C:/Users/xxx.mp3 这种情况
// Windows: 协议解析后可能是 /C:/...,去掉前导斜杠
if (/^\/[a-zA-Z]:\//.test(filePath)) {
filePath = filePath.slice(1);
}
// 还原为系统路径格式
filePath = path.normalize(filePath);
// 检查文件是否存在
if (!fs.existsSync(filePath)) {
console.error('File not found:', filePath);
callback({ error: -6 }); // net::ERR_FILE_NOT_FOUND
return;
// macOS/Linux 上去掉前导斜杠后会丢失绝对路径标识,这里补回
if (process.platform !== 'win32' && !filePath.startsWith('/')) {
filePath = '/' + filePath;
}
callback({ path: filePath });
filePath = path.normalize(filePath);
if (!fs.existsSync(filePath)) {
console.error('File not found:', filePath);
return new Response(null, { status: 404 });
}
// 用 net.fetch 走 file:// 加载,自动支持 Range / streaming,让媒体元素能正常 seek
return net.fetch(pathToFileURL(filePath).toString());
} catch (error) {
console.error('Error handling local protocol:', error);
callback({ error: -2 }); // net::FAILED
return new Response(null, { status: 500 });
}
});