mirror of
https://github.com/timeshiftsauce/CeruMusic.git
synced 2025-11-25 11:29:42 +08:00
fix:main
This commit is contained in:
284
plugins/dee58cdd073b4b96a7263928543a967e-__API___js
Normal file
284
plugins/dee58cdd073b4b96a7263928543a967e-__API___js
Normal file
File diff suppressed because one or more lines are too long
81
plugins/e73b70f26c8144179b6eda7bd74fce8c-example-plugin_js
Normal file
81
plugins/e73b70f26c8144179b6eda7bd74fce8c-example-plugin_js
Normal 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) { ... },
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
await fsPromise.mkdir(path.dirname(filePath), { recursive: true })
|
||||
await fsPromise.writeFile(
|
||||
path.join(getAppDirPath(), 'plugins', `${pluginId}-${pluginName}`),
|
||||
ceruPluginManager.getPluginCode() as string
|
||||
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 || '选择插件文件失败' }
|
||||
}
|
||||
},
|
||||
|
||||
async addPlugin(pluginCode: string, pluginName: string) {
|
||||
try {
|
||||
// 首先解析插件信息
|
||||
const tempPluginManager = new CeruMusicPluginHost(pluginCode, new Logger('temp'))
|
||||
|
||||
// 验证插件信息
|
||||
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
|
||||
)
|
||||
const pluginInfo = ceruPluginManager.getPluginInfo()
|
||||
|
||||
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,
|
||||
pluginName: safePluginName,
|
||||
pluginInfo,
|
||||
supportedSources: ceruPluginManager.getSupportedSources(),
|
||||
plugin: ceruPluginManager
|
||||
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 })
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
throw new Error(`无法读取插件目录${err.message ? ': ' + err.message : ''}`)
|
||||
files = await fsPromise.readdir(pluginDirPath, { recursive: false, withFileTypes: true })
|
||||
|
||||
// 只处理文件,忽略目录
|
||||
files = files.filter((file) => file.isFile())
|
||||
|
||||
if (files.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
// 清空已加载的插件
|
||||
Object.keys(loadedPlugins).forEach((key) => delete loadedPlugins[key])
|
||||
|
||||
const results = await Promise.all(
|
||||
files.map(async (file) => {
|
||||
const pluginId = file.name.split('-')[0]
|
||||
const pluginName = file.name.split('-').slice(1).join('-')
|
||||
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)
|
||||
loadedPlugins[pluginId] = ceruPluginManager
|
||||
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)
|
||||
throw new Error(`无法读取插件目录${err.message ? ': ' + err.message : ''}`)
|
||||
}
|
||||
},
|
||||
|
||||
async getPluginsList() {
|
||||
// 如果没有已加载的插件,先尝试初始化
|
||||
if (Object.keys(loadedPlugins).length === 0) {
|
||||
await this.initializePlugins()
|
||||
}
|
||||
|
||||
// 返回已加载插件的信息
|
||||
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()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,18 +20,81 @@ 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;
|
||||
|
||||
@@ -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,7 +176,10 @@ function initializePlugin() {
|
||||
version: '1.0.0',
|
||||
currentScriptInfo: {
|
||||
rawScript: originalPluginCode,
|
||||
version: '1.0.0' // 添加版本信息
|
||||
name: '${pluginName}',
|
||||
version: '${pluginVersion}',
|
||||
author: '${author}',
|
||||
description: '${pluginDesc}'
|
||||
},
|
||||
env: 'nodejs' // 添加环境信息
|
||||
};
|
||||
@@ -145,28 +229,7 @@ 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) {
|
||||
// 确保插件已初始化
|
||||
@@ -231,4 +294,3 @@ module.exports = {
|
||||
musicUrl
|
||||
};`
|
||||
}
|
||||
export { convertEventDrivenPlugin }
|
||||
|
||||
2
src/preload/index.d.ts
vendored
2
src/preload/index.d.ts
vendored
@@ -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>
|
||||
|
||||
@@ -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: {
|
||||
|
||||
2
src/renderer/components.d.ts
vendored
2
src/renderer/components.d.ts
vendored
@@ -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']
|
||||
}
|
||||
|
||||
18
src/renderer/src/types/Sources.ts
Normal file
18
src/renderer/src/types/Sources.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
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)
|
||||
} catch (error) {
|
||||
console.error('获取插件列表失败:', error)
|
||||
} 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
7
temp/log.txt
Normal 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.
|
||||
Reference in New Issue
Block a user