This commit is contained in:
sqj
2025-08-20 18:38:23 +08:00
parent 9eaab3a512
commit c86fd7b1e4
13 changed files with 1045 additions and 145 deletions

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,81 @@
/**
* ikun 音源插件 for CeruMusic
* @author ikunshare
* @version v1.0
*/
// 插件元信息
const pluginInfo = {
name: 'ikun音源',
version: '1.0.0',
author: 'ikunshare',
description: '基于 ikunshare API 的音源插件',
};
// API 配置
const API_URL = "https://api.ikunshare.com";
const API_KEY = ``; // 如果需要请填入你的API Key
// 支持的音源和音质
const sources = {
kw: {
name: '酷我音乐',
type: 'music',
qualitys: ["128k", "320k", "flac", "flac24bit", "hires"],
},
wy: {
name: '网易云音乐',
type: 'music',
qualitys: ["128k", "320k", "flac", "flac24bit", "hires", "atmos", "master"],
},
mg: {
name: '咪咕音乐',
type: 'music',
qualitys: ["128k", "320k", "flac", "flac24bit", "hires"],
},
};
/**
* 获取音乐URL的核心函数
* @param {string} source - 音源标识 (e.g., "kw", "wy")
* @param {object} musicInfo - 歌曲信息
* @param {string} quality - 音质
* @returns {Promise<string>} 歌曲的URL
*/
async function musicUrl(source, musicInfo, quality) {
// cerumusic 对象由插件宿主提供
const { request, env, version } = cerumusic;
const songId = musicInfo.hash ?? musicInfo.songmid;
const url = `${API_URL}/url?source=${source}&songId=${songId}&quality=${quality}`;
console.log(`[${pluginInfo.name}] Requesting URL: ${url}`);
const { body, statusCode } = await request(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'User-Agent': `cerumusic-${env}/${version}`,
'X-Request-Key': API_KEY,
},
});
if (statusCode !== 200 || body.code !== 200) {
const errorMessage = body.msg || `接口错误 (HTTP: ${statusCode}, Body: ${body.code})`;
console.error(`[${pluginInfo.name}] Error: ${errorMessage}`);
throw new Error(errorMessage);
}
console.log(`[${pluginInfo.name}] Got URL: ${body.url}`);
return body.url;
}
// 导出插件模块
module.exports = {
pluginInfo,
sources,
musicUrl,
// 如果需要,可以继续实现 getPic, getLyric 等方法
// getPic: async function(source, musicInfo) { ... },
// getLyric: async function(source, musicInfo) { ... },
};

View File

@@ -120,6 +120,15 @@ function createWindow(): void {
}
}
ipcMain.handle('service-plugin-selectAndAddPlugin', async (_, type): Promise<any> => {
try {
return await pluginService.selectAndAddPlugin(type)
} catch (error: any) {
console.error('Error selecting and adding plugin:', error)
return { error: error.message }
}
})
ipcMain.handle('service-plugin-addPlugin', async (_, pluginCode, pluginName): Promise<any> => {
try {
return await pluginService.addPlugin(pluginCode, pluginName)
@@ -140,13 +149,23 @@ ipcMain.handle('service-plugin-getPluginById', async (_, id): Promise<any> => {
ipcMain.handle('service-plugin-loadAllPlugins', async (): Promise<any> => {
try {
return await pluginService.loadAllPlugins()
// 使用新的 getPluginsList 方法,但保持 API 兼容性
return await pluginService.getPluginsList()
} catch (error: any) {
console.error('Error loading all plugins:', error)
return { error: error.message }
}
})
ipcMain.handle('service-plugin-uninstallPlugin', async (_, pluginId): Promise<any> => {
try {
return await pluginService.uninstallPlugin(pluginId)
} catch (error: any) {
console.error('Error uninstalling plugin:', error)
return { error: error.message }
}
})
ipcMain.handle('service-music-request', async (_, api, args) => {
return await musicService.request(api, args)
})
@@ -156,10 +175,18 @@ aiEvents(mainWindow)
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
app.whenReady().then(async () => {
// Set app user model id for windows
electronApp.setAppUserModelId('com.cerulean.music')
// 初始化插件系统
try {
await pluginService.initializePlugins()
console.log('插件系统初始化完成')
} catch (error) {
console.error('插件系统初始化失败:', error)
}
// Default open or close DevTools by F12 in development
// and ignore CommandOrControl + R in production.
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils

View File

@@ -2,38 +2,102 @@ import fs, { Dirent } from 'fs'
import path from 'path'
import fsPromise from 'fs/promises'
import { randomUUID } from 'crypto'
import { dialog } from 'electron'
import { getAppDirPath } from '../../utils/path'
import CeruMusicPluginHost from './manager/CeruMusicPluginHost'
import convertEventDrivenPlugin from './manager/converter-event-driven'
import Logger from './logger'
// 导出类型以解决TypeScript错误
// 存储已加载的插件实例
const loadedPlugins = {}
const pluginService = {
async addPlugin(pluginCode: string, pluginName: string) {
const pluginId = randomUUID().replace(/-/g, '')
const ceruPluginManager = new CeruMusicPluginHost(pluginCode, new Logger(pluginId))
async selectAndAddPlugin(type: 'lx' | 'cr') {
try {
// 打开文件选择对话框
const result = await dialog.showOpenDialog({
title: `请选择你的 ${type == 'lx' ? '洛雪' : '澜音'} js插件`,
filters: [
{ name: 'JavaScript 文件', extensions: ['js'] },
{ name: '所有文件', extensions: ['*'] }
],
properties: ['openFile']
})
const filePath = path.join(getAppDirPath(), 'plugins', `${pluginId}-${pluginName}`)
if (fs.existsSync(filePath)) {
throw new Error('插件已存在')
if (result.canceled || !result.filePaths.length) {
return { canceled: true }
}
const filePath = result.filePaths[0]
const fileName = path.basename(filePath)
// 读取文件内容
let pluginCode = await fsPromise.readFile(filePath, 'utf-8')
if (type == 'lx') {
pluginCode = convertEventDrivenPlugin(pluginCode)
}
// 调用现有的添加插件方法
return await this.addPlugin(pluginCode, fileName)
} catch (error: any) {
console.error('选择并添加插件失败:', error)
return { error: error.message || '选择插件文件失败' }
}
},
await fsPromise.mkdir(path.dirname(filePath), { recursive: true })
await fsPromise.writeFile(
path.join(getAppDirPath(), 'plugins', `${pluginId}-${pluginName}`),
ceruPluginManager.getPluginCode() as string
)
const pluginInfo = ceruPluginManager.getPluginInfo()
async addPlugin(pluginCode: string, pluginName: string) {
try {
// 首先解析插件信息
const tempPluginManager = new CeruMusicPluginHost(pluginCode, new Logger('temp'))
return {
pluginId,
pluginName,
pluginInfo,
supportedSources: ceruPluginManager.getSupportedSources(),
plugin: ceruPluginManager
// 验证插件信息
const pluginInfo = tempPluginManager.getPluginInfo()
if (!pluginInfo || !pluginInfo.name || !pluginInfo.version || !pluginInfo.author) {
throw new Error('插件信息不完整,必须包含名称、版本和作者信息')
}
// 确保插件目录存在
const pluginsDir = path.join(getAppDirPath(), 'plugins')
await fsPromise.mkdir(pluginsDir, { recursive: true })
// 检查是否已存在相同名称和版本的插件
const existingPlugins = (await this.getPluginsList()) || []
const duplicatePlugin = existingPlugins.find(
(plugin) =>
plugin.pluginInfo.name === pluginInfo.name &&
plugin.pluginInfo.version === pluginInfo.version
)
if (duplicatePlugin) {
throw new Error(`插件 "${pluginInfo.name} v${pluginInfo.version}" 已存在,不能重复添加`)
}
// 生成插件ID和安全的文件名
const pluginId = randomUUID().replace(/-/g, '')
const safePluginName = (pluginName || pluginInfo.name).replace(/[^\w\d-]/g, '_')
const filePath = path.join(pluginsDir, `${pluginId}-${safePluginName}`)
// 写入插件文件
await fsPromise.writeFile(filePath, tempPluginManager.getPluginCode() as string)
// 重新加载插件以确保正确初始化
const ceruPluginManager = new CeruMusicPluginHost()
await ceruPluginManager.loadPlugin(filePath, new Logger('log/' + pluginId))
// 将插件添加到已加载插件列表
loadedPlugins[pluginId] = ceruPluginManager
return {
pluginId,
pluginName: safePluginName,
pluginInfo,
supportedSources: ceruPluginManager.getSupportedSources()
}
} catch (error: any) {
console.error('添加插件失败:', error)
throw new Error(`添加插件失败: ${error.message}`)
}
},
@@ -45,38 +109,118 @@ const pluginService = {
return loadedPlugins[pluginId]
},
async loadAllPlugins() {
async uninstallPlugin(pluginId: string) {
try {
const pluginsDir = path.join(getAppDirPath(), 'plugins')
const files = await fsPromise.readdir(pluginsDir)
// 查找匹配的插件文件
const pluginFile = files.find((file) => file.startsWith(`${pluginId}-`))
if (!pluginFile) {
throw new Error(`未找到插件ID为 ${pluginId} 的插件文件`)
}
// 删除插件文件
const pluginPath = path.join(pluginsDir, pluginFile)
await fsPromise.unlink(pluginPath)
// 从已加载插件中移除
if (loadedPlugins[pluginId]) {
delete loadedPlugins[pluginId]
}
return { success: true, message: '插件卸载成功' }
} catch (error: any) {
console.error('卸载插件失败:', error)
throw new Error(`卸载插件失败: ${error.message}`)
}
},
async initializePlugins() {
const pluginDirPath = path.join(getAppDirPath(), 'plugins')
// 确保插件目录存在
if (!fs.existsSync(pluginDirPath)) {
return
await fsPromise.mkdir(pluginDirPath, { recursive: true })
return []
}
let files: Dirent<string>[] = []
try {
files = await fsPromise.readdir(pluginDirPath, { recursive: true, withFileTypes: true })
files = await fsPromise.readdir(pluginDirPath, { recursive: false, withFileTypes: true })
// 只处理文件,忽略目录
files = files.filter((file) => file.isFile())
if (files.length === 0) {
return []
}
// 清空已加载的插件
Object.keys(loadedPlugins).forEach((key) => delete loadedPlugins[key])
const results = await Promise.all(
files.map(async (file) => {
try {
// 解析插件ID和名称
const parts = file.name.split('-')
if (parts.length < 2) {
console.warn(`跳过无效的插件文件名: ${file.name}`)
return null
}
const pluginId = parts[0]
const pluginName = parts.slice(1).join('-')
const fullPath = path.join(pluginDirPath, file.name)
// 加载插件
const ceruPluginManager = new CeruMusicPluginHost()
await ceruPluginManager.loadPlugin(fullPath, new Logger(pluginId))
// 获取插件信息
const pluginInfo = ceruPluginManager.getPluginInfo()
// 存储到已加载插件列表
loadedPlugins[pluginId] = ceruPluginManager
return {
pluginId,
pluginName,
pluginInfo,
supportedSources: ceruPluginManager.getSupportedSources()
}
} catch (error: any) {
console.error(`加载插件 ${file.name} 失败:`, error)
return null
}
})
)
// 过滤掉加载失败的插件
return results.filter((result) => result !== null)
} catch (err: any) {
console.error(err)
console.error('读取插件目录失败:', err)
throw new Error(`无法读取插件目录${err.message ? ': ' + err.message : ''}`)
}
},
return Promise.all(
files.map(async (file) => {
const pluginId = file.name.split('-')[0]
const pluginName = file.name.split('-').slice(1).join('-')
const fullPath = path.join(pluginDirPath, file.name)
async getPluginsList() {
// 如果没有已加载的插件,先尝试初始化
if (Object.keys(loadedPlugins).length === 0) {
await this.initializePlugins()
}
const ceruPluginManager = new CeruMusicPluginHost()
await ceruPluginManager.loadPlugin(fullPath)
loadedPlugins[pluginId] = ceruPluginManager
const pluginInfo = ceruPluginManager.getPluginInfo()
return {
pluginId,
pluginName,
pluginInfo,
supportedSources: ceruPluginManager.getSupportedSources()
}
})
)
// 返回已加载插件的信息
return Object.entries(loadedPlugins).map(([pluginId, manager]) => {
const ceruPluginManager = manager as CeruMusicPluginHost
return {
pluginId,
pluginName: pluginId.split('-')[1] || pluginId,
pluginInfo: ceruPluginManager.getPluginInfo(),
supportedSources: ceruPluginManager.getSupportedSources()
}
})
}
}

View File

@@ -1,11 +1,10 @@
function convertEventDrivenPlugin(originalCode: string): string {
console.log('检测到事件驱动插件,使用事件包装器转换...')
export default function convertEventDrivenPlugin(originalCode: string): string {
// 提取插件信息
const nameMatch = originalCode.match(/@name\s+(.+)/)
const versionMatch = originalCode.match(/@version\s+(.+)/)
const authorMatch = originalCode.match(/@author\s+(.+)/)
const descMatch = originalCode.match(/@description\s+(.+)/)
const author = authorMatch ? authorMatch[1].trim() : 'Unknown'
const pluginName = nameMatch ? nameMatch[1].trim() : '未知插件'
const pluginVersion = versionMatch ? versionMatch[1].trim() : '1.0.0'
const pluginDesc = descMatch ? descMatch[1].trim() : '从事件驱动插件转换而来'
@@ -13,6 +12,7 @@ function convertEventDrivenPlugin(originalCode: string): string {
return `/**
* 由 CeruMusic 插件转换器转换 - @author sqj
* @name ${pluginName}
* @author ${author}
* @version ${pluginVersion}
* @description ${pluginDesc}
*/
@@ -20,23 +20,86 @@ function convertEventDrivenPlugin(originalCode: string): string {
const pluginInfo = {
name: "${pluginName}",
version: "${pluginVersion}",
author: "Unknown",
author: "${author}",
description: "${pluginDesc}"
};
// 原始插件代码
const originalPluginCode = ${JSON.stringify(originalCode)};
// 音源信息将通过插件的 send 调用动态获取
let sources = {};
function getSourceName(sourceId) {
const nameMap = {
'kw': '酷我音乐',
'kg': '酷狗音乐',
'tx': 'QQ音乐',
'wy': '网易云音乐',
'mg': '咪咕音乐'
};
return nameMap[sourceId] || sourceId.toUpperCase() + '音乐';
}
// 提取默认音源配置作为备用
function extractDefaultSources() {
// 尝试从 MUSIC_QUALITY 常量中提取音源信息
const qualityMatch = originalPluginCode.match(/const\\s+MUSIC_QUALITY\\s*=\\s*JSON\\.parse\\(([^)]+)\\)/);
if (qualityMatch) {
try {
// 处理字符串,移除外层引号并正确解析
let qualityStr = qualityMatch[1].trim();
if (qualityStr.startsWith("'") && qualityStr.endsWith("'")) {
qualityStr = qualityStr.slice(1, -1);
} else if (qualityStr.startsWith('"') && qualityStr.endsWith('"')) {
qualityStr = qualityStr.slice(1, -1);
}
console.log('提取到的 MUSIC_QUALITY 字符串:', qualityStr);
const qualityData = JSON.parse(qualityStr);
console.log('解析后的 MUSIC_QUALITY 数据:', qualityData);
const extractedSources = {};
Object.keys(qualityData).forEach(sourceId => {
extractedSources[sourceId] = {
name: getSourceName(sourceId),
type: 'music',
qualitys: qualityData[sourceId] || ['128k', '320k']
};
});
console.log('提取的音源配置:', extractedSources);
return extractedSources;
} catch (e) {
console.log('解析 MUSIC_QUALITY 失败:', e.message);
}
}
// 默认音源配置
return {
kw: { name: "酷我音乐", type: "music", qualitys: ['128k', '320k', 'flac', 'flac24bit', 'hires', 'atmos', 'master'] },
kg: { name: "酷狗音乐", type: "music", qualitys: ['128k', '320k', 'flac', 'flac24bit', 'hires', 'atmos', 'master'] },
tx: { name: "QQ音乐", type: "music", qualitys: ['128k', '320k', 'flac', 'flac24bit', 'hires', 'atmos', 'master'] },
wy: { name: "网易云音乐", type: "music", qualitys: ['128k', '320k', 'flac', 'flac24bit', 'hires', 'atmos', 'master'] },
mg: { name: "咪咕音乐", type: "music", qualitys: ['128k', '320k', 'flac', 'flac24bit', 'hires', 'atmos', 'master'] }
};
}
// 初始化默认音源
sources = extractDefaultSources();
// 插件状态
let isInitialized = false;
let pluginSources = {};
let requestHandler = null;
initializePlugin()
function initializePlugin() {
if (isInitialized) return;
const { request, utils } = cerumusic;
// 创建完整的 lx 模拟环境
const mockLx = {
EVENT_NAMES: {
@@ -53,8 +116,26 @@ function initializePlugin() {
send: (event, data) => {
console.log(\`[${pluginName + ' by Ceru插件' || 'ceru插件'}] 发送事件: \${event}\`, data);
if (event === 'inited' && data.sources) {
// 动态更新音源信息,保持原始的音质配置
pluginSources = data.sources;
// 将插件发送的音源信息转换为正确格式并同步到导出的 sources
Object.keys(pluginSources).forEach(sourceId => {
const sourceInfo = pluginSources[sourceId];
// 保留原始音质配置,如果存在的话
const originalQualitys = sources[sourceId] && sources[sourceId].qualitys;
sources[sourceId] = {
name: getSourceName(sourceId),
type: sourceInfo.type || 'music',
// 优先使用插件发送的音质配置,其次使用原始解析的配置,最后使用默认配置
qualitys: sourceInfo.qualitys || originalQualitys || ['128k', '320k']
};
});
console.log('[${pluginName + ' by Ceru插件' || 'ceru插件'}] 音源注册完成:', Object.keys(pluginSources));
console.log('[${pluginName + ' by Ceru插件' || 'ceru插件'}] 动态音源信息已更新:', sources);
}
},
request: request,
@@ -95,16 +176,19 @@ function initializePlugin() {
version: '1.0.0',
currentScriptInfo: {
rawScript: originalPluginCode,
version: '1.0.0' // 添加版本信息
name: '${pluginName}',
version: '${pluginVersion}',
author: '${author}',
description: '${pluginDesc}'
},
env: 'nodejs' // 添加环境信息
};
// 创建全局环境
const globalThis = {
lx: mockLx
};
// 创建沙箱环境
const sandbox = {
globalThis: globalThis,
@@ -121,22 +205,22 @@ function initializePlugin() {
exports: {},
process: { env: { NODE_ENV: 'production' } }
};
try {
// 使用 Function 构造器执行插件代码
const pluginFunction = new Function(
'globalThis', 'lx', 'console', 'setTimeout', 'clearTimeout',
'setInterval', 'clearInterval', 'Buffer', 'JSON', 'require',
'globalThis', 'lx', 'console', 'setTimeout', 'clearTimeout',
'setInterval', 'clearInterval', 'Buffer', 'JSON', 'require',
'module', 'exports', 'process',
originalPluginCode
);
pluginFunction(
globalThis, mockLx, console, setTimeout, clearTimeout,
setInterval, clearInterval, Buffer, JSON, () => ({}),
{ exports: {} }, {}, { env: { NODE_ENV: 'production' } }
);
isInitialized = true;
console.log(\`[CeruMusic] 事件驱动插件初始化成功\`);
} catch (error) {
@@ -145,44 +229,23 @@ function initializePlugin() {
}
}
// 从插件源码中提取音源信息作为备用
const sources = {};
// 尝试从代码中提取音源信息
const sourceMatches = originalPluginCode.match(/sources\\[['"]([^'"]+)['"]\\]\\s*=\\s*apiInfo\\.info/g);
if (sourceMatches) {
sourceMatches.forEach(match => {
const sourceId = match.match(/['"]([^'"]+)['"]/)[1];
sources[sourceId] = {
name: sourceId.toUpperCase() + '音乐',
type: 'music',
qualitys: ['128k', '320k']
};
});
} else {
// 默认音源配置
sources.kw = { name: "酷我音乐", type: "music", qualitys: ["128k", "320k"] };
sources.kg = { name: "酷狗音乐", type: "music", qualitys: ["128k"] };
sources.tx = { name: "QQ音乐", type: "music", qualitys: ["128k"] };
sources.wy = { name: "网易云音乐", type: "music", qualitys: ["128k", "320k"] };
sources.mg = { name: "咪咕音乐", type: "music", qualitys: ["128k"] };
}
async function musicUrl(source, musicInfo, quality) {
// 确保插件已初始化
initializePlugin();
// 等待一小段时间让插件完全初始化
await new Promise(resolve => setTimeout(resolve, 100));
if (!requestHandler) {
const errorMessage = '插件请求处理器未初始化';
console.error(\`[\${pluginInfo.name}] Error: \${errorMessage}\`);
throw new Error(errorMessage);
}
console.log(\`[\${pluginInfo.name}] 使用事件驱动方式获取 \${source} 音源链接\`);
try {
// 调用插件的请求处理器
const result = await requestHandler({
@@ -193,28 +256,28 @@ async function musicUrl(source, musicInfo, quality) {
type: quality
}
});
// 检查结果是否有效
if (!result) {
const errorMessage = \`获取 \${source} 音源链接失败: 返回结果为空\`;
console.error(\`[\${pluginInfo.name}] Error: \${errorMessage}\`);
throw new Error(errorMessage);
}
// 如果结果是对象且包含错误信息
if (typeof result === 'object' && result.error) {
const errorMessage = result.error || \`获取 \${source} 音源链接失败\`;
console.error(\`[\${pluginInfo.name}] Error: \${errorMessage}\`);
throw new Error(errorMessage);
}
// 如果结果是对象且包含状态码
if (typeof result === 'object' && result.code && result.code !== 200) {
const errorMessage = result.msg || \`接口错误 (Code: \${result.code})\`;
console.error(\`[\${pluginInfo.name}] Error: \${errorMessage}\`);
throw new Error(errorMessage);
}
console.log(\`[\${pluginInfo.name}] Got URL: \${typeof result === 'string' ? result : result.url || result}\`);
return result;
} catch (error) {
@@ -231,4 +294,3 @@ module.exports = {
musicUrl
};`
}
export { convertEventDrivenPlugin }

View File

@@ -9,4 +9,4 @@ function getAppDirPath() {
return dirPath
}
export { getAppDirPath }
export { getAppDirPath }

View File

@@ -24,6 +24,8 @@ interface CustomAPI {
// 插件管理API
plugins: {
selectAndAddPlugin: (type: 'lx' | 'cr') => Promise<any>
uninstallPlugin(pluginId: string): ApiResult | PromiseLike<ApiResult>
addPlugin: (pluginCode: string, pluginName: string) => Promise<any>
getPluginById: (id: string) => Promise<any>
loadAllPlugins: () => Promise<any>

View File

@@ -28,10 +28,14 @@ const api = {
request: (api: string, args: any) => ipcRenderer.invoke('service-music-request', api, args)
},
plugins: {
selectAndAddPlugin: (type: 'lx' | 'cr') =>
ipcRenderer.invoke('service-plugin-selectAndAddPlugin', type),
addPlugin: (pluginCode: string, pluginName: string) =>
ipcRenderer.invoke('service-plugin-addPlugin', pluginCode, pluginName),
getPluginById: (id: string) => ipcRenderer.invoke('service-plugin-getPluginById', id),
loadAllPlugins: () => ipcRenderer.invoke('service-plugin-loadAllPlugins')
loadAllPlugins: () => ipcRenderer.invoke('service-plugin-loadAllPlugins'),
uninstallPlugin: (pluginId: string) =>
ipcRenderer.invoke('service-plugin-uninstallPlugin', pluginId)
},
ai: {

View File

@@ -29,6 +29,8 @@ declare module 'vue' {
TitleBarControls: typeof import('./src/components/TitleBarControls.vue')['default']
TLayout: typeof import('tdesign-vue-next')['Layout']
TLoading: typeof import('tdesign-vue-next')['Loading']
TRadioButton: typeof import('tdesign-vue-next')['RadioButton']
TRadioGroup: typeof import('tdesign-vue-next')['RadioGroup']
TTooltip: typeof import('tdesign-vue-next')['Tooltip']
Versions: typeof import('./src/components/Versions.vue')['default']
}

View File

@@ -0,0 +1,18 @@
export interface PluginInfo {
name: string
version: string
author: string
description: string
}
export interface SourceDetail {
name: string
type: string
qualitys: string[]
}
export interface Sources {
supportedSources: {
[key: string]: SourceDetail
}
}

View File

@@ -1,4 +1,5 @@
import { PlayMode } from './audio'
import { Sources } from './Sources'
export interface UserInfo {
lastPlaySongId?: number | null
@@ -8,5 +9,8 @@ export interface UserInfo {
mainColor?: string
playMode?: PlayMode
deepseekAPIkey?: string
musicSource?: string
pluginId?: string
supportedSources?: Sources['supportedSources']
selectSources?: string
selectQuality?: string
}

View File

@@ -4,12 +4,28 @@
<h2>插件管理</h2>
<div class="plugin-actions">
<button class="btn-primary" @click="openPluginFile">
<i class="iconfont icon-add"></i> 添加插件
</button>
<button class="btn-secondary" @click="refreshPlugins">
<i class="iconfont icon-refresh"></i> 刷新
</button>
<t-button theme="primary" @click="plugTypeDialog = true">
<template #icon><t-icon name="add" /></template> 添加插件
</t-button>
<t-dialog
:visible="plugTypeDialog"
:close-btn="true"
confirm-btn="确定"
cancel-btn="取消"
:on-confirm="addPlug"
:on-close="() => (plugTypeDialog = false)"
>
<template #header>请选择你的插件类别</template>
<template #body>
<t-radio-group v-model="type" variant="primary-filled" default-value="cr">
<t-radio-button value="cr">澜音插件</t-radio-button>
<t-radio-button value="lx">洛雪插件</t-radio-button>
</t-radio-group>
</template>
</t-dialog>
<t-button theme="default" @click="refreshPlugins">
<template #icon><t-icon name="refresh" /></template> 刷新
</t-button>
</div>
<div v-if="loading" class="loading">
@@ -17,25 +33,62 @@
<span>加载中...</span>
</div>
<div v-else-if="error" class="error-state">
<t-icon name="error-circle" style="font-size: 48px; color: #dc3545" />
<p>加载插件时出错</p>
<p class="error-message">{{ error }}</p>
<t-button theme="default" @click="refreshPlugins">
<template #icon><t-icon name="refresh" /></template> 重试
</t-button>
</div>
<div v-else-if="plugins.length === 0" class="empty-state">
<i class="iconfont icon-plugin" style="font-size: 48px"></i>
<t-icon name="app" style="font-size: 48px" />
<p>暂无已安装的插件</p>
<p class="hint">点击"添加插件"按钮来安装新插件</p>
</div>
<div v-else class="plugin-list">
<div v-for="plugin in plugins" :key="plugin.name" class="plugin-item">
<div
v-for="plugin in plugins"
:key="plugin.pluginId"
class="plugin-item"
:class="{ selected: isPluginSelected(plugin.pluginId) }"
>
<div class="plugin-info">
<h3>
{{ plugin.name }} <span class="version">v{{ plugin.version }}</span>
{{ plugin.pluginInfo.name }}
<span class="version">{{ plugin.pluginInfo.version }}</span>
<span v-if="isPluginSelected(plugin.pluginId)" class="current-tag">当前使用</span>
</h3>
<p class="author">作者: {{ plugin.author }}</p>
<p class="description">{{ plugin.description || '无描述' }}</p>
<p class="author">作者: {{ plugin.pluginInfo.author }}</p>
<p class="description">{{ plugin.pluginInfo.description || '无描述' }}</p>
<div
v-if="plugin.supportedSources && Object.keys(plugin.supportedSources).length > 0"
class="plugin-sources"
>
<span class="source-label">支持的音源:</span>
<span v-for="source in plugin.supportedSources" :key="source.name" class="source-tag">
{{ source.name }}
</span>
</div>
</div>
<div class="plugin-actions">
<button class="btn-danger" @click="uninstallPlugin(plugin.name)">
<i class="iconfont icon-delete"></i> 卸载
</button>
<t-button
v-if="!isPluginSelected(plugin.pluginId)"
theme="primary"
size="small"
@click="selectPlugin(plugin)"
>
<template #icon><t-icon name="check" /></template> 使用
</t-button>
<t-button
theme="danger"
size="small"
@click="uninstallPlugin(plugin.pluginId, plugin.pluginInfo.name)"
>
<template #icon><t-icon name="delete" /></template> 卸载
</t-button>
</div>
</div>
</div>
@@ -45,44 +98,217 @@
<script setup lang="ts">
import TitleBarControls from '@renderer/components/TitleBarControls.vue'
import { ref, onMounted } from 'vue'
import { MessagePlugin, DialogPlugin } from 'tdesign-vue-next'
import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
interface Plugin {
interface PluginSource {
name: string
type: string
qualitys: string[]
}
interface PluginInfo {
name: string
version: string
author: string
description?: string
}
interface Plugin {
pluginId: string
pluginName: string
pluginInfo: PluginInfo
supportedSources: { [key: string]: PluginSource }
}
// 定义API返回结果的接口
interface ApiResult {
error?: string
pluginInfo?: PluginInfo
[key: string]: any
}
const plugins = ref<Plugin[]>([])
const loading = ref(true)
const error = ref<string | null>(null)
const plugTypeDialog = ref(false)
let type = ref<'lx' | 'cr'>('cr')
// 获取store实例
const localUserStore = LocalUserDetailStore()
// 检查插件是否被选中
function isPluginSelected(pluginId: string): boolean {
return localUserStore.userInfo.pluginId === pluginId
}
// 选择插件
function selectPlugin(plugin: Plugin) {
try {
// 确保store已初始化
if (!localUserStore.initialization) {
localUserStore.init()
}
const { pluginId, pluginInfo, supportedSources: sources } = plugin
// 检查插件是否提供音源
if (!sources || Object.keys(sources).length === 0) {
MessagePlugin.warning(`插件 "${pluginInfo.name}" 没有提供可用的音源。`)
// 即使没有音源,也可能需要选择该插件(如果插件有其他功能)
// 这里我们只更新ID清空音源相关信息
localUserStore.userInfo.pluginId = pluginId
localUserStore.userInfo.supportedSources = {}
localUserStore.userInfo.selectSources = ''
localUserStore.userInfo.selectQuality = ''
MessagePlugin.success(`已选择插件: ${pluginInfo.name}`)
return
}
// 转换supportedSources格式以匹配UserInfo类型并添加 `type` 字段
const supportedSourcesForStore = sources
let selectSources: string
// 获取第一个音源作为默认选择
if (
!(typeof localUserStore.userInfo.selectSources === 'string') ||
!sources[localUserStore.userInfo.selectSources as unknown as string]
) {
selectSources = Object.keys(sources)[0]
} else {
selectSources = localUserStore.userInfo.selectSources
}
let selectQuality: string
if (
!(typeof localUserStore.userInfo.selectQuality === 'string') ||
!sources[localUserStore.userInfo.selectSources as unknown as string] ||
!sources[localUserStore.userInfo.selectSources as unknown as string][
localUserStore.userInfo.selectQuality as unknown as string
]
) {
const qualitys = sources[selectSources].qualitys
selectQuality = qualitys[qualitys.length - 1]
} else {
selectQuality = localUserStore.userInfo.selectQuality
}
// 更新userInfo
localUserStore.userInfo.pluginId = pluginId
localUserStore.userInfo.supportedSources = supportedSourcesForStore
localUserStore.userInfo.selectSources = selectSources
localUserStore.userInfo.selectQuality = selectQuality
MessagePlugin.success(`已选择插件: ${pluginInfo.name}`)
} catch (err: any) {
console.error('选择插件失败:', err)
MessagePlugin.error(`选择插件失败: ${err.message || '未知错误'}`)
}
}
// 获取已安装的插件列表
async function getPlugins() {
loading.value = true
error.value = null
try {
const result = await window.api.plugins.loadAllPlugins()
plugins.value = result
console.log('插件列表加载完成', result)
} catch (error) {
console.error('获取插件列表失败:', error)
console.log(result)
// 检查返回结果是否有错误
if (result && typeof result === 'object' && 'error' in result) {
console.error('获取插件列表失败:', result.error)
error.value = `加载插件失败: ${result.error}`
plugins.value = []
} else if (Array.isArray(result)) {
plugins.value = result
console.log('插件列表加载完成', result)
} else {
// 处理意外的返回格式
console.error('插件列表格式不正确:', result)
plugins.value = []
error.value = '插件数据格式不正确'
}
} catch (err: any) {
console.error('获取插件列表失败:', err)
error.value = err?.message || '未知错误'
plugins.value = []
} finally {
loading.value = false
}
}
// 打开文件选择器安装插件
async function openPluginFile() {
async function addPlug() {
try {
console.log(111)
} catch (error) {
console.error('安装插件失败:', error)
// 调用主进程的文件选择和添加插件API
plugTypeDialog.value = false
console.log(type.value)
const result = (await window.api.plugins.selectAndAddPlugin(type.value)) as ApiResult
// 检查用户是否取消了文件选择
if (result && result.canceled) {
return
}
// 检查结果是否包含错误
if (result && typeof result === 'object' && 'error' in result) {
MessagePlugin.error(`安装插件失败: ${result.error}`)
console.error('安装插件失败:', result.error)
} else {
// 安装成功才刷新插件列表
await getPlugins()
// 显示成功消息
if (result && result.pluginInfo) {
MessagePlugin.success(`插件 "${result.pluginInfo.name}" 安装成功!`)
} else {
MessagePlugin.success('插件安装成功!')
}
}
} catch (err: any) {
console.error('安装插件失败:', err)
MessagePlugin.error(`安装插件失败: ${err.message || '未知错误'}`)
}
}
// 卸载插件
async function uninstallPlugin(pluginName: string) {
if (!confirm(`确定要卸载插件 "${pluginName}" 吗?`)) {
return
async function uninstallPlugin(pluginId: string, pluginName: string) {
try {
// 使用TDesign对话框替代confirm
const dialog = DialogPlugin.confirm({
header: '确认卸载',
body: `确定要卸载插件 "${pluginName}" 吗?`,
confirmBtn: '确认卸载',
cancelBtn: '取消',
onConfirm: async () => {
// 用户确认后,开始卸载操作
loading.value = true
const result = (await window.api.plugins.uninstallPlugin(pluginId)) as ApiResult
// 检查结果是否包含错误
if (result && typeof result === 'object' && 'error' in result) {
// 使用TDesign消息提示替代alert
MessagePlugin.error(`卸载插件失败: ${result.error}`)
console.error('卸载插件失败:', result.error)
} else {
// 卸载成功才刷新插件列表
await getPlugins()
// 显示成功消息
if (pluginId == localUserStore.userInfo.pluginId) {
localUserStore.userInfo.pluginId = ''
localUserStore.userInfo.supportedSources = {}
localUserStore.userInfo.selectSources = ''
localUserStore.userInfo.selectQuality = ''
}
MessagePlugin.success(`插件 "${pluginName}" 卸载成功`)
}
dialog.destroy()
}
})
} catch (err: any) {
// 使用TDesign消息提示替代alert
console.error('卸载插件失败:', err)
MessagePlugin.error(`卸载插件失败: ${err.message || '未知错误'}`)
} finally {
loading.value = false
}
}
@@ -92,6 +318,11 @@ async function refreshPlugins() {
}
onMounted(async () => {
// 确保store已初始化
if (!localUserStore.initialization) {
console.log('组件挂载时初始化store')
localUserStore.init()
}
await getPlugins()
})
</script>
@@ -112,6 +343,7 @@ onMounted(async () => {
.plugins-container {
padding: 20px;
box-sizing: border-box;
}
.plugin-actions {
@@ -120,33 +352,6 @@ onMounted(async () => {
margin-bottom: 20px;
}
.btn-primary,
.btn-secondary,
.btn-danger {
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
display: flex;
align-items: center;
gap: 5px;
}
.btn-primary {
background-color: var(--color-primary, #007bff);
color: white;
}
.btn-secondary {
background-color: var(--color-secondary, #6c757d);
color: white;
}
.btn-danger {
background-color: var(--color-danger, #dc3545);
color: white;
}
.loading {
display: flex;
flex-direction: column;
@@ -165,6 +370,22 @@ onMounted(async () => {
margin-bottom: 10px;
}
.error-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
color: #666;
}
.error-message {
color: #dc3545;
margin-bottom: 15px;
text-align: center;
max-width: 80%;
}
@keyframes spin {
to {
transform: rotate(360deg);
@@ -199,6 +420,12 @@ onMounted(async () => {
border-radius: 8px;
background-color: var(--color-background-soft, #f8f9fa);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.plugin-item.selected {
background-color: #e8f5e8;
border: 2px solid #28a745;
}
.plugin-info {
@@ -208,6 +435,9 @@ onMounted(async () => {
.plugin-info h3 {
margin: 0 0 5px 0;
font-size: 1.1em;
display: flex;
align-items: center;
gap: 8px;
}
.version {
@@ -216,6 +446,15 @@ onMounted(async () => {
font-weight: normal;
}
.current-tag {
background-color: #28a745;
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.75em;
font-weight: normal;
}
.author {
margin: 0 0 5px 0;
font-size: 0.9em;
@@ -223,7 +462,33 @@ onMounted(async () => {
}
.description {
margin: 0;
margin: 0 0 8px 0;
font-size: 0.9em;
}
.plugin-sources {
display: flex;
flex-wrap: wrap;
gap: 5px;
align-items: center;
margin-top: 5px;
}
.source-label {
font-size: 0.85em;
color: #666;
}
.source-tag {
background-color: var(--color-primary, #007bff);
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.8em;
}
.plugin-actions {
display: flex;
gap: 8px;
}
</style>

7
temp/log.txt Normal file
View File

@@ -0,0 +1,7 @@
log [CeruMusic] Plugin "ikun音源" loaded successfully.
log [聚合API接口 (by lerd) by Ceru插件] 注册事件监听器: request
log [聚合API接口 (by lerd) by Ceru插件] 发送事件: inited [object Object]
log [聚合API接口 (by lerd) by Ceru插件] 音源注册完成: tx,wy,kg,kw,mg
log [聚合API接口 (by lerd) by Ceru插件] 动态音源信息已更新: [object Object]
log [CeruMusic] 事件驱动插件初始化成功
log [CeruMusic] Plugin "聚合API接口 (by lerd)" loaded successfully.