mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-04-03 14:20:50 +08:00
feat:优化lx音源问题
This commit is contained in:
@@ -35,6 +35,7 @@
|
||||
"@electron-toolkit/utils": "^4.0.0",
|
||||
"@unblockneteasemusic/server": "^0.27.10",
|
||||
"cors": "^2.8.5",
|
||||
"crypto-js": "^4.2.0",
|
||||
"electron-store": "^8.2.0",
|
||||
"electron-updater": "^6.6.2",
|
||||
"electron-window-state": "^5.0.3",
|
||||
@@ -42,9 +43,12 @@
|
||||
"file-type": "^21.1.1",
|
||||
"flac-tagger": "^1.0.7",
|
||||
"font-list": "^1.6.0",
|
||||
"form-data": "^4.0.5",
|
||||
"husky": "^9.1.7",
|
||||
"jsencrypt": "^3.5.4",
|
||||
"music-metadata": "^11.10.3",
|
||||
"netease-cloud-music-api-alger": "^4.26.1",
|
||||
"node-fetch": "^2.7.0",
|
||||
"node-id3": "^0.2.9",
|
||||
"node-machine-id": "^1.1.12",
|
||||
"pinia-plugin-persistedstate": "^4.7.1",
|
||||
@@ -59,6 +63,7 @@
|
||||
"@rushstack/eslint-patch": "^1.15.0",
|
||||
"@types/howler": "^2.2.12",
|
||||
"@types/node": "^20.19.26",
|
||||
"@types/node-fetch": "^2.6.13",
|
||||
"@types/tinycolor2": "^1.4.6",
|
||||
"@typescript-eslint/eslint-plugin": "^8.49.0",
|
||||
"@typescript-eslint/parser": "^8.49.0",
|
||||
|
||||
@@ -17,6 +17,7 @@ import { setupUpdateHandlers } from './modules/update';
|
||||
import { createMainWindow, initializeWindowManager, setAppQuitting } from './modules/window';
|
||||
import { initWindowSizeManager } from './modules/window-size';
|
||||
import { startMusicApi } from './server';
|
||||
import { initLxMusicHttp } from './modules/lxMusicHttp';
|
||||
|
||||
// 导入所有图标
|
||||
const iconPath = join(__dirname, '../../resources');
|
||||
@@ -57,6 +58,9 @@ function initialize(configStore: any) {
|
||||
// 启动音乐API
|
||||
startMusicApi();
|
||||
|
||||
// 初始化落雪音乐 HTTP 请求处理
|
||||
initLxMusicHttp();
|
||||
|
||||
// 加载歌词窗口
|
||||
loadLyricWindow(ipcMain, mainWindow);
|
||||
|
||||
|
||||
153
src/main/modules/lxMusicHttp.ts
Normal file
153
src/main/modules/lxMusicHttp.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
/**
|
||||
* 落雪音乐 HTTP 请求处理(主进程)
|
||||
* 绕过渲染进程的 CORS 限制
|
||||
*/
|
||||
|
||||
import { ipcMain } from 'electron';
|
||||
import fetch, { type RequestInit } from 'node-fetch';
|
||||
|
||||
interface LxHttpRequest {
|
||||
url: string;
|
||||
options: {
|
||||
method?: string;
|
||||
headers?: Record<string, string>;
|
||||
body?: string;
|
||||
form?: Record<string, string>;
|
||||
formData?: Record<string, string>;
|
||||
timeout?: number;
|
||||
};
|
||||
requestId: string;
|
||||
}
|
||||
|
||||
interface LxHttpResponse {
|
||||
statusCode: number;
|
||||
headers: Record<string, string | string[]>;
|
||||
body: any;
|
||||
}
|
||||
|
||||
// 取消控制器映射
|
||||
const abortControllers = new Map<string, AbortController>();
|
||||
|
||||
/**
|
||||
* 初始化 HTTP 请求处理
|
||||
*/
|
||||
export const initLxMusicHttp = () => {
|
||||
// 处理 HTTP 请求
|
||||
ipcMain.handle(
|
||||
'lx-music-http-request',
|
||||
async (_, request: LxHttpRequest): Promise<LxHttpResponse> => {
|
||||
const { url, options, requestId } = request;
|
||||
const controller = new AbortController();
|
||||
|
||||
// 保存取消控制器
|
||||
abortControllers.set(requestId, controller);
|
||||
|
||||
try {
|
||||
console.log(`[LxMusicHttp] 请求: ${options.method || 'GET'} ${url}`);
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
method: options.method || 'GET',
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
...(options.headers || {})
|
||||
},
|
||||
signal: controller.signal
|
||||
};
|
||||
|
||||
// 处理请求体
|
||||
if (options.body) {
|
||||
fetchOptions.body = options.body;
|
||||
} else if (options.form) {
|
||||
const formData = new URLSearchParams(options.form);
|
||||
fetchOptions.body = formData.toString();
|
||||
fetchOptions.headers = {
|
||||
...fetchOptions.headers,
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
};
|
||||
} else if (options.formData) {
|
||||
// node-fetch 的 FormData 需要特殊处理
|
||||
const FormData = (await import('form-data')).default;
|
||||
const formData = new FormData();
|
||||
for (const [key, value] of Object.entries(options.formData)) {
|
||||
formData.append(key, value);
|
||||
}
|
||||
fetchOptions.body = formData as any;
|
||||
// FormData 会自动设置 Content-Type
|
||||
}
|
||||
|
||||
// 设置超时
|
||||
const timeout = options.timeout || 30000;
|
||||
const timeoutId = setTimeout(() => {
|
||||
console.warn(`[LxMusicHttp] 请求超时: ${url}`);
|
||||
controller.abort();
|
||||
}, timeout);
|
||||
|
||||
const response = await fetch(url, fetchOptions);
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
console.log(`[LxMusicHttp] 响应: ${response.status} ${url}`);
|
||||
|
||||
// 读取响应体
|
||||
const rawBody = await response.text();
|
||||
|
||||
// 尝试解析 JSON
|
||||
let parsedBody: any = rawBody;
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
if (
|
||||
contentType.includes('application/json') ||
|
||||
rawBody.startsWith('{') ||
|
||||
rawBody.startsWith('[')
|
||||
) {
|
||||
try {
|
||||
parsedBody = JSON.parse(rawBody);
|
||||
} catch {
|
||||
// 解析失败则使用原始字符串
|
||||
}
|
||||
}
|
||||
|
||||
// 转换 headers 为普通对象
|
||||
const headers: Record<string, string | string[]> = {};
|
||||
response.headers.forEach((value, key) => {
|
||||
headers[key] = value;
|
||||
});
|
||||
|
||||
const result: LxHttpResponse = {
|
||||
statusCode: response.status,
|
||||
headers,
|
||||
body: parsedBody
|
||||
};
|
||||
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
console.error(`[LxMusicHttp] 请求失败: ${url}`, error.message);
|
||||
throw error;
|
||||
} finally {
|
||||
// 清理取消控制器
|
||||
abortControllers.delete(requestId);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 处理请求取消
|
||||
ipcMain.handle('lx-music-http-cancel', (_, requestId: string) => {
|
||||
const controller = abortControllers.get(requestId);
|
||||
if (controller) {
|
||||
console.log(`[LxMusicHttp] 取消请求: ${requestId}`);
|
||||
controller.abort();
|
||||
abortControllers.delete(requestId);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[LxMusicHttp] HTTP 请求处理已初始化');
|
||||
};
|
||||
|
||||
/**
|
||||
* 清理所有正在进行的请求
|
||||
*/
|
||||
export const cleanupLxMusicHttp = () => {
|
||||
for (const [requestId, controller] of abortControllers.entries()) {
|
||||
console.log(`[LxMusicHttp] 清理请求: ${requestId}`);
|
||||
controller.abort();
|
||||
}
|
||||
abortControllers.clear();
|
||||
};
|
||||
6
src/preload/index.d.ts
vendored
6
src/preload/index.d.ts
vendored
@@ -25,6 +25,12 @@ interface API {
|
||||
importLxMusicScript: () => Promise<{ name: string; content: string } | null>;
|
||||
invoke: (channel: string, ...args: any[]) => Promise<any>;
|
||||
getSearchSuggestions: (keyword: string) => Promise<any>;
|
||||
lxMusicHttpRequest: (request: {
|
||||
url: string;
|
||||
options: any;
|
||||
requestId: string;
|
||||
}) => Promise<any>;
|
||||
lxMusicHttpCancel: (requestId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
// 自定义IPC渲染进程通信接口
|
||||
|
||||
@@ -58,7 +58,16 @@ const api = {
|
||||
return Promise.reject(new Error(`未授权的 IPC 通道: ${channel}`));
|
||||
},
|
||||
// 搜索建议
|
||||
getSearchSuggestions: (keyword: string) => ipcRenderer.invoke('get-search-suggestions', keyword)
|
||||
getSearchSuggestions: (keyword: string) => ipcRenderer.invoke('get-search-suggestions', keyword),
|
||||
|
||||
// 落雪音乐 HTTP 请求(绕过 CORS)
|
||||
lxMusicHttpRequest: (request: {
|
||||
url: string;
|
||||
options: any;
|
||||
requestId: string;
|
||||
}) => ipcRenderer.invoke('lx-music-http-request', request),
|
||||
|
||||
lxMusicHttpCancel: (requestId: string) => ipcRenderer.invoke('lx-music-http-cancel', requestId)
|
||||
};
|
||||
|
||||
// 创建带类型的ipcRenderer对象,暴露给渲染进程
|
||||
|
||||
@@ -27,6 +27,7 @@ import { checkLoginStatus } from '@/utils/auth';
|
||||
|
||||
import { initAudioListeners, initMusicHook } from './hooks/MusicHook';
|
||||
import { audioService } from './services/audioService';
|
||||
import { initLxMusicRunner } from './services/LxMusicSourceRunner';
|
||||
import { isMobile } from './utils';
|
||||
import { useAppShortcuts } from './utils/appShortcuts';
|
||||
|
||||
@@ -125,6 +126,21 @@ onMounted(async () => {
|
||||
// 初始化播放状态
|
||||
await playerStore.initializePlayState();
|
||||
|
||||
// 初始化落雪音源(如果有激活的音源)
|
||||
const activeLxApiId = settingsStore.setData?.activeLxMusicApiId;
|
||||
if (activeLxApiId) {
|
||||
const lxMusicScripts = settingsStore.setData?.lxMusicScripts || [];
|
||||
const activeScript = lxMusicScripts.find((script: any) => script.id === activeLxApiId);
|
||||
if (activeScript && activeScript.script) {
|
||||
try {
|
||||
console.log('[App] 初始化激活的落雪音源:', activeScript.name);
|
||||
await initLxMusicRunner(activeScript.script);
|
||||
} catch (error) {
|
||||
console.error('[App] 初始化落雪音源失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有正在播放的音乐,则初始化音频监听器
|
||||
if (playerStore.playMusic && playerStore.playMusic.id) {
|
||||
// 使用 nextTick 确保 DOM 更新后再初始化
|
||||
|
||||
@@ -13,6 +13,74 @@ import type { SongResult } from '@/types/music';
|
||||
import type { MusicParseResult } from './musicParser';
|
||||
import { CacheManager } from './musicParser';
|
||||
|
||||
/**
|
||||
* 解析可能是 API 端点的 URL,获取真实音频 URL
|
||||
* 一些音源脚本返回的是 API 端点,需要额外请求才能获取真实音频 URL
|
||||
*/
|
||||
const resolveAudioUrl = async (url: string): Promise<string> => {
|
||||
try {
|
||||
// 检查是否看起来像 API 端点(包含 /api/ 且有查询参数)
|
||||
const isApiEndpoint = url.includes('/api/') || (url.includes('?') && url.includes('type=url'));
|
||||
|
||||
if (!isApiEndpoint) {
|
||||
// 看起来像直接的音频 URL,直接返回
|
||||
return url;
|
||||
}
|
||||
|
||||
console.log('[LxMusicStrategy] 检测到 API 端点,尝试解析真实 URL:', url);
|
||||
|
||||
// 尝试获取真实 URL
|
||||
const response = await fetch(url, {
|
||||
method: 'HEAD',
|
||||
redirect: 'manual' // 不自动跟随重定向
|
||||
});
|
||||
|
||||
// 检查是否是重定向
|
||||
if (response.status >= 300 && response.status < 400) {
|
||||
const location = response.headers.get('Location');
|
||||
if (location) {
|
||||
console.log('[LxMusicStrategy] API 返回重定向 URL:', location);
|
||||
return location;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果 HEAD 请求没有重定向,尝试 GET 请求
|
||||
const getResponse = await fetch(url, {
|
||||
redirect: 'follow'
|
||||
});
|
||||
|
||||
// 检查 Content-Type
|
||||
const contentType = getResponse.headers.get('Content-Type') || '';
|
||||
|
||||
// 如果是音频类型,返回最终 URL
|
||||
if (contentType.includes('audio/') || contentType.includes('application/octet-stream')) {
|
||||
console.log('[LxMusicStrategy] 解析到音频 URL:', getResponse.url);
|
||||
return getResponse.url;
|
||||
}
|
||||
|
||||
// 如果是 JSON,尝试解析
|
||||
if (contentType.includes('application/json') || contentType.includes('text/json')) {
|
||||
const json = await getResponse.json();
|
||||
console.log('[LxMusicStrategy] API 返回 JSON:', json);
|
||||
|
||||
// 尝试从 JSON 中提取 URL(常见字段)
|
||||
const audioUrl = json.url || json.data?.url || json.audio_url || json.link || json.src;
|
||||
if (audioUrl && typeof audioUrl === 'string') {
|
||||
console.log('[LxMusicStrategy] 从 JSON 中提取音频 URL:', audioUrl);
|
||||
return audioUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果都不是,返回原始 URL(可能直接可用)
|
||||
console.warn('[LxMusicStrategy] 无法解析 API 端点,返回原始 URL');
|
||||
return url;
|
||||
} catch (error) {
|
||||
console.error('[LxMusicStrategy] URL 解析失败:', error);
|
||||
// 解析失败时返回原始 URL
|
||||
return url;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 将 SongResult 转换为 LxMusicInfo 格式
|
||||
*/
|
||||
@@ -82,9 +150,17 @@ export class LxMusicStrategy {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否导入了脚本
|
||||
const script = settingsStore?.setData?.lxMusicScript;
|
||||
return Boolean(script);
|
||||
// 检查是否有激活的音源
|
||||
const activeLxApiId = settingsStore?.setData?.activeLxMusicApiId;
|
||||
if (!activeLxApiId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查音源列表中是否存在该 ID
|
||||
const lxMusicScripts = settingsStore?.setData?.lxMusicScripts || [];
|
||||
const activeScript = lxMusicScripts.find((script: any) => script.id === activeLxApiId);
|
||||
|
||||
return Boolean(activeScript && activeScript.script);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,18 +179,30 @@ export class LxMusicStrategy {
|
||||
|
||||
try {
|
||||
const settingsStore = useSettingsStore();
|
||||
const script = settingsStore.setData?.lxMusicScript;
|
||||
|
||||
if (!script) {
|
||||
console.log('[LxMusicStrategy] 未导入落雪音源脚本');
|
||||
// 获取激活的音源 ID
|
||||
const activeLxApiId = settingsStore.setData?.activeLxMusicApiId;
|
||||
if (!activeLxApiId) {
|
||||
console.log('[LxMusicStrategy] 未选择激活的落雪音源');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 从音源列表中获取激活的脚本
|
||||
const lxMusicScripts = settingsStore.setData?.lxMusicScripts || [];
|
||||
const activeScript = lxMusicScripts.find((script: any) => script.id === activeLxApiId);
|
||||
|
||||
if (!activeScript || !activeScript.script) {
|
||||
console.log('[LxMusicStrategy] 未找到激活的落雪音源脚本');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`[LxMusicStrategy] 使用激活的音源: ${activeScript.name} (ID: ${activeScript.id})`);
|
||||
|
||||
// 获取或初始化执行器
|
||||
let runner = getLxMusicRunner();
|
||||
if (!runner || !runner.isInitialized()) {
|
||||
console.log('[LxMusicStrategy] 初始化落雪音源执行器...');
|
||||
runner = await initLxMusicRunner(script);
|
||||
runner = await initLxMusicRunner(activeScript.script);
|
||||
}
|
||||
|
||||
// 获取可用音源
|
||||
@@ -144,22 +232,33 @@ export class LxMusicStrategy {
|
||||
const lxQuality: LxQuality = QUALITY_TO_LX[quality || 'higher'] || '320k';
|
||||
|
||||
// 获取音乐 URL
|
||||
const url = await runner.getMusicUrl(bestSource, lxMusicInfo, lxQuality);
|
||||
const rawUrl = await runner.getMusicUrl(bestSource, lxMusicInfo, lxQuality);
|
||||
|
||||
if (!url) {
|
||||
if (!rawUrl) {
|
||||
console.log('[LxMusicStrategy] 获取 URL 失败');
|
||||
CacheManager.addFailedCache(id, this.name);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('[LxMusicStrategy] 解析成功:', url.substring(0, 50) + '...');
|
||||
console.log('[LxMusicStrategy] 脚本返回 URL:', rawUrl.substring(0, 80) + '...');
|
||||
|
||||
// 解析可能是 API 端点的 URL
|
||||
const resolvedUrl = await resolveAudioUrl(rawUrl);
|
||||
|
||||
if (!resolvedUrl) {
|
||||
console.log('[LxMusicStrategy] URL 解析失败');
|
||||
CacheManager.addFailedCache(id, this.name);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('[LxMusicStrategy] 最终音频 URL:', resolvedUrl.substring(0, 80) + '...');
|
||||
|
||||
return {
|
||||
data: {
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: {
|
||||
url,
|
||||
url: resolvedUrl,
|
||||
source: `lx-${bestSource}`,
|
||||
quality: lxQuality
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
class="source-card source-card--lxmusic"
|
||||
:class="{
|
||||
'source-card--selected': isSourceSelected('lxMusic'),
|
||||
'source-card--disabled': !settingsStore.setData.lxMusicScript
|
||||
'source-card--disabled': !activeLxApiId || lxMusicApis.length === 0
|
||||
}"
|
||||
style="--source-color: #10b981"
|
||||
@click="toggleSource('lxMusic')"
|
||||
@@ -70,9 +70,9 @@
|
||||
</div>
|
||||
<p class="source-card__description">
|
||||
{{
|
||||
settingsStore.setData.lxMusicScript
|
||||
? lxMusicScriptInfo?.name || '已导入'
|
||||
: '未导入 (请去落雪音源Tab配置)'
|
||||
activeLxApiId && lxMusicScriptInfo
|
||||
? lxMusicScriptInfo.name
|
||||
: '未配置 (请去落雪音源Tab配置)'
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
@@ -321,8 +321,19 @@ const activeLxApiId = computed<string | null>({
|
||||
// 落雪音源脚本信息(保持向后兼容)
|
||||
const lxMusicScriptInfo = computed<LxScriptInfo | null>(() => {
|
||||
const activeId = activeLxApiId.value;
|
||||
if (!activeId) return null;
|
||||
if (!activeId) {
|
||||
console.log('[lxMusicScriptInfo] 没有激活的音源 ID');
|
||||
return null;
|
||||
}
|
||||
|
||||
const activeApi = lxMusicApis.value.find((api) => api.id === activeId);
|
||||
console.log('[lxMusicScriptInfo] 查找激活的音源:', {
|
||||
activeId,
|
||||
found: !!activeApi,
|
||||
name: activeApi?.name,
|
||||
infoName: activeApi?.info?.name
|
||||
});
|
||||
|
||||
return activeApi?.info || null;
|
||||
});
|
||||
|
||||
@@ -351,10 +362,16 @@ const toggleSource = (sourceKey: string) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否是落雪音源且未导入
|
||||
if (sourceKey === 'lxMusic' && !settingsStore.setData.lxMusicScript) {
|
||||
message.warning('请先导入落雪音源脚本');
|
||||
return;
|
||||
// 检查是否是落雪音源且未配置
|
||||
if (sourceKey === 'lxMusic') {
|
||||
if (lxMusicApis.value.length === 0) {
|
||||
message.warning('请先导入落雪音源脚本');
|
||||
return;
|
||||
}
|
||||
if (!activeLxApiId.value) {
|
||||
message.warning('请先选择一个落雪音源');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const index = selectedSources.value.indexOf(sourceKey as ExtendedPlatform);
|
||||
@@ -411,6 +428,7 @@ const importLxMusicScript = async () => {
|
||||
const addLxMusicScript = async (scriptContent: string) => {
|
||||
// 解析脚本信息
|
||||
const scriptInfo = parseScriptInfo(scriptContent);
|
||||
console.log('[MusicSourceSettings] 解析到的脚本信息:', scriptInfo);
|
||||
|
||||
// 尝试初始化执行器以验证脚本
|
||||
try {
|
||||
@@ -418,6 +436,8 @@ const addLxMusicScript = async (scriptContent: string) => {
|
||||
const sources = runner.getSources();
|
||||
const sourceKeys = Object.keys(sources) as LxSourceKey[];
|
||||
|
||||
console.log('[MusicSourceSettings] 脚本支持的音源:', sourceKeys);
|
||||
|
||||
// 生成唯一 ID
|
||||
const id = `lx_api_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
@@ -432,6 +452,13 @@ const addLxMusicScript = async (scriptContent: string) => {
|
||||
createdAt: Date.now()
|
||||
};
|
||||
|
||||
console.log('[MusicSourceSettings] 创建的音源配置:', {
|
||||
id: newApiConfig.id,
|
||||
name: newApiConfig.name,
|
||||
infoName: newApiConfig.info.name,
|
||||
sources: newApiConfig.sources
|
||||
});
|
||||
|
||||
// 添加到列表
|
||||
const scripts = [...(settingsStore.setData.lxMusicScripts || []), newApiConfig];
|
||||
|
||||
@@ -440,6 +467,12 @@ const addLxMusicScript = async (scriptContent: string) => {
|
||||
activeLxMusicApiId: id // 自动激活新添加的音源
|
||||
});
|
||||
|
||||
console.log('[MusicSourceSettings] 保存后的 store 数据:', {
|
||||
scriptsCount: scripts.length,
|
||||
activeId: id,
|
||||
firstScript: scripts[0] ? { id: scripts[0].id, name: scripts[0].name } : null
|
||||
});
|
||||
|
||||
message.success(`音源脚本导入成功:${scriptInfo.name},支持 ${sourceKeys.length} 个音源`);
|
||||
|
||||
// 导入成功后自动勾选
|
||||
@@ -447,7 +480,7 @@ const addLxMusicScript = async (scriptContent: string) => {
|
||||
selectedSources.value.push('lxMusic');
|
||||
}
|
||||
} catch (initError: any) {
|
||||
console.error('落雪音源脚本初始化失败:', initError);
|
||||
console.error('[MusicSourceSettings] 落雪音源脚本初始化失败:', initError);
|
||||
message.error(`脚本初始化失败:${initError.message}`);
|
||||
}
|
||||
};
|
||||
@@ -463,8 +496,19 @@ const setActiveLxApi = async (apiId: string) => {
|
||||
}
|
||||
|
||||
try {
|
||||
// 初始化选中的脚本
|
||||
await initLxMusicRunner(api.script);
|
||||
console.log('[MusicSourceSettings] 切换音源:', {
|
||||
id: api.id,
|
||||
name: api.name,
|
||||
version: api.info?.version,
|
||||
sources: api.sources
|
||||
});
|
||||
|
||||
// 清除旧的 runner
|
||||
setLxMusicRunner(null);
|
||||
|
||||
// 初始化新选中的脚本
|
||||
const runner = await initLxMusicRunner(api.script);
|
||||
console.log('[MusicSourceSettings] 音源初始化成功,支持的音源:', runner.getSources());
|
||||
|
||||
// 更新激活的音源 ID
|
||||
activeLxApiId.value = apiId;
|
||||
@@ -476,7 +520,7 @@ const setActiveLxApi = async (apiId: string) => {
|
||||
|
||||
message.success(`已切换到音源: ${api.name}`);
|
||||
} catch (error: any) {
|
||||
console.error('切换落雪音源失败:', error);
|
||||
console.error('[MusicSourceSettings] 切换落雪音源失败:', error);
|
||||
message.error(`切换失败:${error.message}`);
|
||||
}
|
||||
};
|
||||
@@ -641,6 +685,21 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
// 监听落雪音源列表变化
|
||||
watch(
|
||||
() => [lxMusicApis.value.length, activeLxApiId.value],
|
||||
([apiCount, activeId]) => {
|
||||
// 如果没有音源或没有激活的音源,自动从已选音源中移除 lxMusic
|
||||
if (apiCount === 0 || !activeId) {
|
||||
const index = selectedSources.value.indexOf('lxMusic');
|
||||
if (index > -1) {
|
||||
selectedSources.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// 同步外部show属性变化
|
||||
watch(
|
||||
() => props.show,
|
||||
|
||||
@@ -17,6 +17,7 @@ import type {
|
||||
LxSourceConfig,
|
||||
LxSourceKey
|
||||
} from '@/types/lxMusic';
|
||||
import * as lxCrypto from '@/utils/lxCrypto';
|
||||
|
||||
/**
|
||||
* 解析脚本头部注释中的元信息
|
||||
@@ -27,27 +28,46 @@ export const parseScriptInfo = (script: string): LxScriptInfo => {
|
||||
rawScript: script
|
||||
};
|
||||
|
||||
// 匹配头部注释块
|
||||
const headerMatch = script.match(/^\/\*\*[\s\S]*?\*\//);
|
||||
if (!headerMatch) return info;
|
||||
// 尝试匹配不同格式的头部注释块
|
||||
// 支持 /** ... */ 和 /* ... */ 格式
|
||||
const headerMatch = script.match(/^\/\*+[\s\S]*?\*\//);
|
||||
if (!headerMatch) {
|
||||
console.warn('[parseScriptInfo] 未找到脚本头部注释块');
|
||||
return info;
|
||||
}
|
||||
|
||||
const header = headerMatch[0];
|
||||
console.log('[parseScriptInfo] 解析脚本头部:', header.substring(0, 200));
|
||||
|
||||
// 解析各个字段
|
||||
// 解析各个字段(支持 * 前缀和无前缀两种格式)
|
||||
const nameMatch = header.match(/@name\s+(.+?)(?:\r?\n|\*\/)/);
|
||||
if (nameMatch) info.name = nameMatch[1].trim();
|
||||
if (nameMatch) {
|
||||
info.name = nameMatch[1].trim().replace(/^\*\s*/, '');
|
||||
console.log('[parseScriptInfo] 解析到名称:', info.name);
|
||||
} else {
|
||||
console.warn('[parseScriptInfo] 未找到 @name 标签');
|
||||
}
|
||||
|
||||
const descMatch = header.match(/@description\s+(.+?)(?:\r?\n|\*\/)/);
|
||||
if (descMatch) info.description = descMatch[1].trim();
|
||||
if (descMatch) {
|
||||
info.description = descMatch[1].trim().replace(/^\*\s*/, '');
|
||||
}
|
||||
|
||||
const versionMatch = header.match(/@version\s+(.+?)(?:\r?\n|\*\/)/);
|
||||
if (versionMatch) info.version = versionMatch[1].trim();
|
||||
if (versionMatch) {
|
||||
info.version = versionMatch[1].trim().replace(/^\*\s*/, '');
|
||||
console.log('[parseScriptInfo] 解析到版本:', info.version);
|
||||
}
|
||||
|
||||
const authorMatch = header.match(/@author\s+(.+?)(?:\r?\n|\*\/)/);
|
||||
if (authorMatch) info.author = authorMatch[1].trim();
|
||||
if (authorMatch) {
|
||||
info.author = authorMatch[1].trim().replace(/^\*\s*/, '');
|
||||
}
|
||||
|
||||
const homepageMatch = header.match(/@homepage\s+(.+?)(?:\r?\n|\*\/)/);
|
||||
if (homepageMatch) info.homepage = homepageMatch[1].trim();
|
||||
if (homepageMatch) {
|
||||
info.homepage = homepageMatch[1].trim().replace(/^\*\s*/, '');
|
||||
}
|
||||
|
||||
return info;
|
||||
};
|
||||
@@ -209,26 +229,16 @@ export class LxMusicSourceRunner {
|
||||
}
|
||||
},
|
||||
crypto: {
|
||||
md5: (str: string) => {
|
||||
// 简化的 MD5 实现,实际使用可能需要引入完整库
|
||||
console.warn('[LxMusicRunner] MD5 暂未完整实现');
|
||||
return str;
|
||||
},
|
||||
randomBytes: (size: number) => {
|
||||
const array = new Uint8Array(size);
|
||||
crypto.getRandomValues(array);
|
||||
return Array.from(array)
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
},
|
||||
aesEncrypt: (buffer: any, _mode: string, _key: any, _iv: any) => {
|
||||
console.warn('[LxMusicRunner] AES 加密暂未实现');
|
||||
return buffer;
|
||||
},
|
||||
rsaEncrypt: (buffer: any, _key: string) => {
|
||||
console.warn('[LxMusicRunner] RSA 加密暂未实现');
|
||||
return buffer;
|
||||
}
|
||||
md5: lxCrypto.md5,
|
||||
sha1: lxCrypto.sha1,
|
||||
sha256: lxCrypto.sha256,
|
||||
randomBytes: lxCrypto.randomBytes,
|
||||
aesEncrypt: lxCrypto.aesEncrypt,
|
||||
aesDecrypt: lxCrypto.aesDecrypt,
|
||||
rsaEncrypt: lxCrypto.rsaEncrypt,
|
||||
rsaDecrypt: lxCrypto.rsaDecrypt,
|
||||
base64Encode: lxCrypto.base64Encode,
|
||||
base64Decode: lxCrypto.base64Decode
|
||||
},
|
||||
zlib: {
|
||||
inflate: async (buffer: ArrayBuffer) => {
|
||||
@@ -337,96 +347,135 @@ export class LxMusicSourceRunner {
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 HTTP 请求
|
||||
* 处理 HTTP 请求(优先使用主进程,绕过 CORS 限制)
|
||||
*/
|
||||
private handleHttpRequest(
|
||||
url: string,
|
||||
options: any,
|
||||
callback: (err: Error | null, resp: any, body: any) => void
|
||||
): () => void {
|
||||
const controller = new AbortController();
|
||||
|
||||
console.log(`[LxMusicRunner] HTTP 请求: ${options.method || 'GET'} ${url}`);
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
method: options.method || 'GET',
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
...options.headers
|
||||
},
|
||||
signal: controller.signal,
|
||||
// 使用 cors 模式尝试跨域请求
|
||||
mode: 'cors',
|
||||
// 不发送凭据
|
||||
credentials: 'omit'
|
||||
};
|
||||
|
||||
if (options.body) {
|
||||
fetchOptions.body = options.body;
|
||||
} else if (options.form) {
|
||||
fetchOptions.body = new URLSearchParams(options.form);
|
||||
fetchOptions.headers = {
|
||||
...fetchOptions.headers,
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
};
|
||||
} else if (options.formData) {
|
||||
const formData = new FormData();
|
||||
for (const [key, value] of Object.entries(options.formData)) {
|
||||
formData.append(key, value as string);
|
||||
}
|
||||
fetchOptions.body = formData;
|
||||
}
|
||||
|
||||
const timeout = options.timeout || 30000;
|
||||
const timeoutId = setTimeout(() => {
|
||||
console.warn(`[LxMusicRunner] HTTP 请求超时: ${url}`);
|
||||
controller.abort();
|
||||
}, timeout);
|
||||
const requestId = `lx_http_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
||||
|
||||
fetch(url, fetchOptions)
|
||||
.then(async (response) => {
|
||||
clearTimeout(timeoutId);
|
||||
console.log(`[LxMusicRunner] HTTP 响应: ${response.status} ${url}`);
|
||||
// 尝试使用主进程 HTTP 请求(如果可用)
|
||||
const hasMainProcessHttp = typeof window.api?.lxMusicHttpRequest === 'function';
|
||||
|
||||
const rawBody = await response.text();
|
||||
if (hasMainProcessHttp) {
|
||||
// 使用主进程 HTTP 请求(绕过 CORS)
|
||||
console.log(`[LxMusicRunner] 使用主进程 HTTP 请求`);
|
||||
|
||||
// 尝试解析 JSON,如果是 JSON 则返回解析后的对象
|
||||
let parsedBody: any = rawBody;
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
if (
|
||||
contentType.includes('application/json') ||
|
||||
rawBody.startsWith('{') ||
|
||||
rawBody.startsWith('[')
|
||||
) {
|
||||
try {
|
||||
parsedBody = JSON.parse(rawBody);
|
||||
// 如果响应中包含 URL,缓存下来以备后用
|
||||
if (parsedBody && parsedBody.url && typeof parsedBody.url === 'string') {
|
||||
this.lastMusicUrl = parsedBody.url;
|
||||
}
|
||||
} catch {
|
||||
// 解析失败则使用原始字符串
|
||||
}
|
||||
}
|
||||
|
||||
callback(
|
||||
null,
|
||||
{
|
||||
statusCode: response.status,
|
||||
headers: Object.fromEntries(response.headers.entries()),
|
||||
body: parsedBody
|
||||
window.api
|
||||
.lxMusicHttpRequest({
|
||||
url,
|
||||
options: {
|
||||
...options,
|
||||
timeout
|
||||
},
|
||||
parsedBody
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
clearTimeout(timeoutId);
|
||||
console.error(`[LxMusicRunner] HTTP 请求失败: ${url}`, error.message);
|
||||
callback(error, null, null);
|
||||
});
|
||||
requestId
|
||||
})
|
||||
.then((response: any) => {
|
||||
console.log(`[LxMusicRunner] HTTP 响应: ${response.statusCode} ${url}`);
|
||||
|
||||
// 返回取消函数
|
||||
return () => controller.abort();
|
||||
// 如果响应中包含 URL,缓存下来以备后用
|
||||
if (response.body && response.body.url && typeof response.body.url === 'string') {
|
||||
this.lastMusicUrl = response.body.url;
|
||||
}
|
||||
|
||||
callback(null, response, response.body);
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
console.error(`[LxMusicRunner] HTTP 请求失败: ${url}`, error.message);
|
||||
callback(error, null, null);
|
||||
});
|
||||
|
||||
// 返回取消函数
|
||||
return () => {
|
||||
void window.api?.lxMusicHttpCancel?.(requestId);
|
||||
};
|
||||
} else {
|
||||
// 回退到渲染进程 fetch(可能受 CORS 限制)
|
||||
console.log(`[LxMusicRunner] 主进程 HTTP 不可用,使用渲染进程 fetch`);
|
||||
|
||||
const controller = new AbortController();
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
method: options.method || 'GET',
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
...options.headers
|
||||
},
|
||||
signal: controller.signal,
|
||||
mode: 'cors',
|
||||
credentials: 'omit'
|
||||
};
|
||||
|
||||
if (options.body) {
|
||||
fetchOptions.body = options.body;
|
||||
} else if (options.form) {
|
||||
fetchOptions.body = new URLSearchParams(options.form);
|
||||
fetchOptions.headers = {
|
||||
...fetchOptions.headers,
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
};
|
||||
} else if (options.formData) {
|
||||
const formData = new FormData();
|
||||
for (const [key, value] of Object.entries(options.formData)) {
|
||||
formData.append(key, value as string);
|
||||
}
|
||||
fetchOptions.body = formData;
|
||||
}
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
console.warn(`[LxMusicRunner] HTTP 请求超时: ${url}`);
|
||||
controller.abort();
|
||||
}, timeout);
|
||||
|
||||
fetch(url, fetchOptions)
|
||||
.then(async (response) => {
|
||||
clearTimeout(timeoutId);
|
||||
console.log(`[LxMusicRunner] HTTP 响应: ${response.status} ${url}`);
|
||||
|
||||
const rawBody = await response.text();
|
||||
|
||||
// 尝试解析 JSON
|
||||
let parsedBody: any = rawBody;
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
if (
|
||||
contentType.includes('application/json') ||
|
||||
rawBody.startsWith('{') ||
|
||||
rawBody.startsWith('[')
|
||||
) {
|
||||
try {
|
||||
parsedBody = JSON.parse(rawBody);
|
||||
if (parsedBody && parsedBody.url && typeof parsedBody.url === 'string') {
|
||||
this.lastMusicUrl = parsedBody.url;
|
||||
}
|
||||
} catch {
|
||||
// 解析失败则使用原始字符串
|
||||
}
|
||||
}
|
||||
|
||||
callback(
|
||||
null,
|
||||
{
|
||||
statusCode: response.status,
|
||||
headers: Object.fromEntries(response.headers.entries()),
|
||||
body: parsedBody
|
||||
},
|
||||
parsedBody
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
clearTimeout(timeoutId);
|
||||
console.error(`[LxMusicRunner] HTTP 请求失败: ${url}`, error.message);
|
||||
callback(error, null, null);
|
||||
});
|
||||
|
||||
// 返回取消函数
|
||||
return () => controller.abort();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
284
src/renderer/utils/lxCrypto.ts
Normal file
284
src/renderer/utils/lxCrypto.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
/**
|
||||
* 落雪音乐加密工具
|
||||
* 实现 lx.utils.crypto API
|
||||
*
|
||||
* 提供 MD5、AES、RSA 等加密功能
|
||||
*/
|
||||
|
||||
import CryptoJS from 'crypto-js';
|
||||
import { JSEncrypt } from 'jsencrypt';
|
||||
|
||||
/**
|
||||
* MD5 哈希
|
||||
*/
|
||||
export const md5 = (str: string): string => {
|
||||
return CryptoJS.MD5(str).toString();
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成随机字节(返回16进制字符串)
|
||||
*/
|
||||
export const randomBytes = (size: number): string => {
|
||||
const array = new Uint8Array(size);
|
||||
crypto.getRandomValues(array);
|
||||
return Array.from(array)
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
};
|
||||
|
||||
/**
|
||||
* AES 加密
|
||||
*
|
||||
* @param buffer - 要加密的数据(字符串或 Buffer)
|
||||
* @param mode - 加密模式(如 'cbc')
|
||||
* @param key - 密钥(字符串或 WordArray)
|
||||
* @param iv - 初始化向量(字符串或 WordArray)
|
||||
* @returns 加密后的 Buffer(Uint8Array)
|
||||
*/
|
||||
export const aesEncrypt = (
|
||||
buffer: string | Uint8Array,
|
||||
mode: string,
|
||||
key: string | CryptoJS.lib.WordArray,
|
||||
iv: string | CryptoJS.lib.WordArray
|
||||
): Uint8Array => {
|
||||
try {
|
||||
// 将输入转换为 WordArray
|
||||
let wordArray: CryptoJS.lib.WordArray;
|
||||
if (typeof buffer === 'string') {
|
||||
wordArray = CryptoJS.enc.Utf8.parse(buffer);
|
||||
} else {
|
||||
// Uint8Array 转 WordArray
|
||||
const words: number[] = [];
|
||||
for (let i = 0; i < buffer.length; i += 4) {
|
||||
words.push(
|
||||
((buffer[i] || 0) << 24) |
|
||||
((buffer[i + 1] || 0) << 16) |
|
||||
((buffer[i + 2] || 0) << 8) |
|
||||
(buffer[i + 3] || 0)
|
||||
);
|
||||
}
|
||||
wordArray = CryptoJS.lib.WordArray.create(words, buffer.length);
|
||||
}
|
||||
|
||||
// 处理密钥和 IV
|
||||
const keyWordArray = typeof key === 'string' ? CryptoJS.enc.Utf8.parse(key) : key;
|
||||
const ivWordArray = typeof iv === 'string' ? CryptoJS.enc.Utf8.parse(iv) : iv;
|
||||
|
||||
// 根据模式选择加密方式
|
||||
const modeObj = getModeFromString(mode);
|
||||
|
||||
// 执行加密
|
||||
const encrypted = CryptoJS.AES.encrypt(wordArray, keyWordArray, {
|
||||
iv: ivWordArray,
|
||||
mode: modeObj,
|
||||
padding: CryptoJS.pad.Pkcs7
|
||||
});
|
||||
|
||||
// 将结果转换为 Uint8Array
|
||||
const ciphertext = encrypted.ciphertext;
|
||||
const result = new Uint8Array(ciphertext.words.length * 4);
|
||||
for (let i = 0; i < ciphertext.words.length; i++) {
|
||||
const word = ciphertext.words[i];
|
||||
result[i * 4] = (word >>> 24) & 0xff;
|
||||
result[i * 4 + 1] = (word >>> 16) & 0xff;
|
||||
result[i * 4 + 2] = (word >>> 8) & 0xff;
|
||||
result[i * 4 + 3] = word & 0xff;
|
||||
}
|
||||
|
||||
return result.slice(0, ciphertext.sigBytes);
|
||||
} catch (error) {
|
||||
console.error('[lxCrypto] AES 加密失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* AES 解密
|
||||
*/
|
||||
export const aesDecrypt = (
|
||||
buffer: Uint8Array,
|
||||
mode: string,
|
||||
key: string | CryptoJS.lib.WordArray,
|
||||
iv: string | CryptoJS.lib.WordArray
|
||||
): Uint8Array => {
|
||||
try {
|
||||
// Uint8Array 转 WordArray
|
||||
const words: number[] = [];
|
||||
for (let i = 0; i < buffer.length; i += 4) {
|
||||
words.push(
|
||||
((buffer[i] || 0) << 24) |
|
||||
((buffer[i + 1] || 0) << 16) |
|
||||
((buffer[i + 2] || 0) << 8) |
|
||||
(buffer[i + 3] || 0)
|
||||
);
|
||||
}
|
||||
const ciphertext = CryptoJS.lib.WordArray.create(words, buffer.length);
|
||||
|
||||
// 处理密钥和 IV
|
||||
const keyWordArray = typeof key === 'string' ? CryptoJS.enc.Utf8.parse(key) : key;
|
||||
const ivWordArray = typeof iv === 'string' ? CryptoJS.enc.Utf8.parse(iv) : iv;
|
||||
|
||||
// 根据模式选择解密方式
|
||||
const modeObj = getModeFromString(mode);
|
||||
|
||||
// 构造加密对象
|
||||
const cipherParams = CryptoJS.lib.CipherParams.create({
|
||||
ciphertext
|
||||
});
|
||||
|
||||
// 执行解密
|
||||
const decrypted = CryptoJS.AES.decrypt(cipherParams, keyWordArray, {
|
||||
iv: ivWordArray,
|
||||
mode: modeObj,
|
||||
padding: CryptoJS.pad.Pkcs7
|
||||
});
|
||||
|
||||
// 转换为 Uint8Array
|
||||
const result = new Uint8Array(decrypted.words.length * 4);
|
||||
for (let i = 0; i < decrypted.words.length; i++) {
|
||||
const word = decrypted.words[i];
|
||||
result[i * 4] = (word >>> 24) & 0xff;
|
||||
result[i * 4 + 1] = (word >>> 16) & 0xff;
|
||||
result[i * 4 + 2] = (word >>> 8) & 0xff;
|
||||
result[i * 4 + 3] = word & 0xff;
|
||||
}
|
||||
|
||||
return result.slice(0, decrypted.sigBytes);
|
||||
} catch (error) {
|
||||
console.error('[lxCrypto] AES 解密失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* RSA 加密
|
||||
*
|
||||
* @param buffer - 要加密的数据
|
||||
* @param publicKey - RSA 公钥(PEM 格式)
|
||||
* @returns 加密后的数据(Uint8Array)
|
||||
*/
|
||||
export const rsaEncrypt = (buffer: string | Uint8Array, publicKey: string): Uint8Array => {
|
||||
try {
|
||||
const encrypt = new JSEncrypt();
|
||||
encrypt.setPublicKey(publicKey);
|
||||
|
||||
// 转换输入为字符串
|
||||
let input: string;
|
||||
if (typeof buffer === 'string') {
|
||||
input = buffer;
|
||||
} else {
|
||||
// Uint8Array 转字符串
|
||||
input = new TextDecoder().decode(buffer);
|
||||
}
|
||||
|
||||
// 执行加密(返回 base64)
|
||||
const encrypted = encrypt.encrypt(input);
|
||||
if (!encrypted) {
|
||||
throw new Error('RSA encryption failed');
|
||||
}
|
||||
|
||||
// Base64 解码为 Uint8Array
|
||||
const binaryString = atob(encrypted);
|
||||
const result = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
result[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('[lxCrypto] RSA 加密失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* RSA 解密
|
||||
*/
|
||||
export const rsaDecrypt = (buffer: Uint8Array, privateKey: string): Uint8Array => {
|
||||
try {
|
||||
const decrypt = new JSEncrypt();
|
||||
decrypt.setPrivateKey(privateKey);
|
||||
|
||||
// Uint8Array 转 Base64
|
||||
let binaryString = '';
|
||||
for (let i = 0; i < buffer.length; i++) {
|
||||
binaryString += String.fromCharCode(buffer[i]);
|
||||
}
|
||||
const base64 = btoa(binaryString);
|
||||
|
||||
// 执行解密
|
||||
const decrypted = decrypt.decrypt(base64);
|
||||
if (!decrypted) {
|
||||
throw new Error('RSA decryption failed');
|
||||
}
|
||||
|
||||
// 字符串转 Uint8Array
|
||||
return new TextEncoder().encode(decrypted);
|
||||
} catch (error) {
|
||||
console.error('[lxCrypto] RSA 解密失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 从字符串获取加密模式
|
||||
*/
|
||||
const getModeFromString = (mode: string): CryptoJS.lib.Mode => {
|
||||
const modeStr = mode.toLowerCase();
|
||||
switch (modeStr) {
|
||||
case 'cbc':
|
||||
return CryptoJS.mode.CBC;
|
||||
case 'cfb':
|
||||
return CryptoJS.mode.CFB;
|
||||
case 'ctr':
|
||||
return CryptoJS.mode.CTR;
|
||||
case 'ofb':
|
||||
return CryptoJS.mode.OFB;
|
||||
case 'ecb':
|
||||
return CryptoJS.mode.ECB;
|
||||
default:
|
||||
console.warn(`[lxCrypto] 未知的加密模式: ${mode}, 使用 CBC`);
|
||||
return CryptoJS.mode.CBC;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* SHA1 哈希
|
||||
*/
|
||||
export const sha1 = (str: string): string => {
|
||||
return CryptoJS.SHA1(str).toString();
|
||||
};
|
||||
|
||||
/**
|
||||
* SHA256 哈希
|
||||
*/
|
||||
export const sha256 = (str: string): string => {
|
||||
return CryptoJS.SHA256(str).toString();
|
||||
};
|
||||
|
||||
/**
|
||||
* Base64 编码
|
||||
*/
|
||||
export const base64Encode = (data: string | Uint8Array): string => {
|
||||
if (typeof data === 'string') {
|
||||
return btoa(data);
|
||||
} else {
|
||||
let binary = '';
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
binary += String.fromCharCode(data[i]);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Base64 解码
|
||||
*/
|
||||
export const base64Decode = (str: string): Uint8Array => {
|
||||
const binaryString = atob(str);
|
||||
const result = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
result[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
Reference in New Issue
Block a user