diff --git a/.gitignore b/.gitignore index db005ebe..4ed2e7aa 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,4 @@ thumbs.db # Editor-based Rest Client .idea/httpRequests /.idea/csv-plugin.xml +需求文档/整合需求文档.md diff --git a/package.json b/package.json index 537898bf..9ccfe737 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ }, "dependencies": { "archiver": "^7.0.1", + "better-sqlite3": "^12.2.0", "conf": "^14.0.0", "electron-data": "^2.1.2", "electron-store": "^10.1.0", diff --git a/packages/main/package.json b/packages/main/package.json index 627b0863..b8aadcf9 100644 --- a/packages/main/package.json +++ b/packages/main/package.json @@ -17,12 +17,12 @@ "@app/renderer": "*", "electron-updater": "6.6.2", "electron-data": "^2.1.2", - "obs-websocket-js": "^5.0.0" + "obs-websocket-js": "^5.0.6" }, "devDependencies": { "@app/electron-versions": "*", "electron-devtools-installer": "4.0.0", - "@types/conf": "^14.0.0",}]}}} + "@types/conf": "^14.0.0", "typescript": "5.8.3", "vite": "7.0.0" } diff --git a/packages/main/src/apis/electronApi.ts b/packages/main/src/apis/electronApi.ts index dbe205ad..824d87d5 100644 --- a/packages/main/src/apis/electronApi.ts +++ b/packages/main/src/apis/electronApi.ts @@ -1,9 +1,41 @@ import { ipcMain } from 'electron'; +import { authService } from '../utils/AuthService.js'; +import { errorHandlingService } from '../utils/ErrorHandlingService'; +import { notificationService } from '../utils/NotificationService'; + +// 用户认证相关API +ipcMain.handle('login', async (event, loginInfo) => { + try { + const session = await authService.login(loginInfo); + return { success: true, session }; + } catch (error) { + return { success: false, error: error.message }; + } +}); + +ipcMain.handle('logout', async () => { + try { + await authService.logout(); + return { success: true }; + } catch (error) { + return { success: false, error: error.message }; + } +}); + +ipcMain.handle('checkAuthStatus', async () => { + try { + const status = await authService.checkStatus(); + return { success: true, status }; + } catch (error) { + return { success: false, error: error.message }; + } +}); import { getLogManager } from '../utils/LogManager.js'; import { acfunDanmuModule } from '../modules/AcfunDanmuModule.js'; import settingsModule from '../modules/SettingsModule.js'; import UserModule from '../modules/UserModule.js'; import DashboardModule from '../modules/DashboardModule.js'; +import { appManager } from '../utils/AppManager'; // 初始化用户模块和仪表盘模块 const userModule = new UserModule(); @@ -149,6 +181,17 @@ export function initializeElectronApi() { } }); + // 同步开播(设置推流码并启动直播) + ipcMain.handle('live:syncStartBroadcast', async () => { + try { + await liveManagementModule.syncStartBroadcast(); + return { success: true }; + } catch (error) { + console.error('Error syncing and starting broadcast:', error); + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } + }); + ipcMain.handle('live:saveOBSConfig', async (_, config) => { try { const result = await liveManagementModule.saveOBSConfig(config); @@ -169,7 +212,60 @@ export function initializeElectronApi() { } }); - // Acfun弹幕模块相关API + // 直播管理相关API扩展 +ipcMain.handle('live:configureRoom', async (_, roomConfig) => { + try { + const result = await liveManagementModule.configureRoom(roomConfig); + return { success: true, data: result }; + } catch (error) { + console.error('Error configuring room:', error); + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } +}); + +ipcMain.handle('live:startStreaming', async (_, streamConfig) => { + try { + const streamInfo = await liveManagementModule.startStreaming(streamConfig); + return { success: true, data: streamInfo }; + } catch (error) { + console.error('Error starting streaming:', error); + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } +}); + +ipcMain.handle('live:stopStreaming', async () => { + try { + await liveManagementModule.stopStreaming(); + return { success: true }; + } catch (error) { + console.error('Error stopping streaming:', error); + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } +}); + +// 获取直播分类 +ipcMain.handle('live:getCategories', async () => { + try { + const categories = await liveService.getCategories(); + return { success: true, data: categories }; + } catch (error) { + console.error('Error fetching categories:', error); + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } +}); + +// 获取推流信息 +ipcMain.handle('live:getStreamInfo', async () => { + try { + const streamInfo = await liveService.getStreamInfo(); + return { success: true, data: streamInfo }; + } catch (error) { + console.error('Error fetching stream info:', error); + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } +}); + +// Acfun弹幕模块相关API // 发送弹幕 ipcMain.handle('acfunDanmu:sendDanmu', async (_, liveId: number, content: string) => { try { @@ -521,4 +617,282 @@ export function initializeElectronApi() { return { success: false, error: 'Failed to start app' }; } }); + + // 小程序管理相关API + ipcMain.handle('getInstalledMiniPrograms', async () => { + try { + const miniPrograms = await appManager.getMiniPrograms(); + return miniPrograms; + } catch (error) { + console.error('获取已安装小程序失败:', error); + return { + success: false, + error: error instanceof Error ? error.message : String(error) + }; + } + }); + + ipcMain.handle('getMarketplaceApps', async () => { + try { + const marketplaceApps = await appManager.getMarketplaceApps(); + return marketplaceApps; + } catch (error) { + console.error('获取小程序市场应用失败:', error); + return { + success: false, + error: error instanceof Error ? error.message : String(error) + }; + } + }); + + ipcMain.handle('installMiniProgram', async (_, name: string, source: string) => { + try { + await appManager.installMiniProgram(name, source); + return { success: true }; + } catch (error) { + console.error('安装小程序失败:', error); + return { + success: false, + error: error instanceof Error ? error.message : String(error) + }; + } + }); + + ipcMain.handle('startMiniProgram', async (_, id: string) => { + try { + await appManager.startApp(id); + return { success: true }; + } catch (error) { + console.error('启动小程序失败:', error); + return { + success: false, + error: error instanceof Error ? error.message : String(error) + }; + } + }); + + ipcMain.handle('stopMiniProgram', async (_, id: string) => { + try { + await appManager.closeApp(id); + return { success: true }; + } catch (error) { + console.error('停止小程序失败:', error); + return { + success: false, + error: error instanceof Error ? error.message : String(error) + }; + } + }); + + ipcMain.handle('updateMiniProgram', async (_, id: string) => { + try { + await appManager.updateMiniProgram(id); + return { success: true }; + } catch (error) { + console.error('更新小程序失败:', error); + return { + success: false, + error: error instanceof Error ? error.message : String(error) + }; + } + }); + + ipcMain.handle('removeMiniProgram', async (_, id: string) => { + try { + await appManager.removeMiniProgram(id); + return { success: true }; + } catch (error) { + console.error('移除小程序失败:', error); + return { + success: false, + error: error instanceof Error ? error.message : String(error) + }; + } + }); + + // 数据分析相关API +ipcMain.handle('getRealTimeStats', async () => { + try { + const stats = await analyticsService.getRealTimeStats(); + return { success: true, data: stats }; + } catch (error) { + return { success: false, error: error.message }; + } +}); + +ipcMain.handle('getAudienceAnalysis', async (event, params) => { + try { + const analysis = await analyticsService.getAudienceAnalysis(params); + return { success: true, data: analysis }; + } catch (error) { + return { success: false, error: error.message }; + } +}); + +ipcMain.handle('getGiftStats', async (event, timeRange) => { + try { + const stats = await analyticsService.getGiftStats(timeRange); + return { success: true, data: stats }; + } catch (error) { + return { success: false, error: error.message }; + } +}); + +ipcMain.handle('generateReport', async (event, reportType) => { + try { + const report = await analyticsService.generateReport(reportType); + return { success: true, data: report }; + } catch (error) { + return { success: false, error: error.message }; + } +}); + +// 小本本相关API +ipcMain.handle('notebook:getNotes', async () => { + try { + const notes = await notebookService.getNotes(); + return { success: true, data: notes }; + } catch (error) { + return { success: false, error: error.message }; + } +}); + +ipcMain.handle('notebook:saveNote', async (event, note) => { + try { + const savedNote = await notebookService.saveNote(note); + return { success: true, data: savedNote }; + } catch (error) { + return { success: false, error: error.message }; + } +}); + +ipcMain.handle('notebook:deleteNote', async (event, noteId) => { + try { + await notebookService.deleteNote(noteId); + return { success: true }; + } catch (error) { + return { success: false, error: error.message }; + } +}); + +ipcMain.handle('notebook:getNoteById', async (event, noteId) => { + try { + const note = await notebookService.getNoteById(noteId); + return { success: true, data: note }; + } catch (error) { + return { success: false, error: error.message }; + } +}); + +// 系统设置相关API +ipcMain.handle('systemSettings:getAll', async () => { + try { + const settings = await systemSettingsService.getAllSettings(); + return { success: true, data: settings }; + } catch (error) { + return { success: false, error: error.message }; + } +}); + +ipcMain.handle('systemSettings:getByCategory', async (event, category) => { + try { + const settings = await systemSettingsService.getSettingsByCategory(category); + return { success: true, data: settings }; + } catch (error) { + return { success: false, error: error.message }; + } +}); + +ipcMain.handle('systemSettings:updateByCategory', async (event, { category, settings }) => { + try { + const updatedSettings = await systemSettingsService.updateSettingsByCategory(category, settings); + return { success: true, data: updatedSettings }; + } catch (error) { + return { success: false, error: error.message }; + } +}); + +ipcMain.handle('systemSettings:resetToDefault', async (event, category) => { + try { + let result; + if (category) { + result = await systemSettingsService.resetCategoryToDefault(category); + } else { + result = await systemSettingsService.resetToDefault(); + } + return { success: true, data: result }; + } catch (error) { + return { success: false, error: error.message }; + } +}); + + // 错误处理相关API + ipcMain.handle('error:logError', async (event, errorInfo) => { + try { + const errorId = await errorHandlingService.logError(errorInfo); + return { success: true, data: { errorId } }; + } catch (error) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('error:getErrorHistory', async (event, params) => { + try { + const errors = await errorHandlingService.getErrorHistory(params); + return { success: true, data: errors }; + } catch (error) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('error:clearErrorHistory', async () => { + try { + await errorHandlingService.clearErrorHistory(); + return { success: true }; + } catch (error) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('error:reportError', async (event, errorId) => { + try { + const reportResult = await errorHandlingService.reportError(errorId); + return { success: true, data: reportResult }; + } catch (error) { + return { success: false, error: error.message }; + } + }); + + // 通知相关API + ipcMain.handle('notification:getPermissionStatus', async () => { + try { + const status = await notificationService.getPermissionStatus(); + return { success: true, data: status }; + } catch (error) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('notification:requestPermission', async () => { + try { + const status = await notificationService.requestPermission(); + return { success: true, data: status }; + } catch (error) { + return { success: false, error: error.message }; + } + }); + + ipcMain.handle('notification:sendTestNotification', async () => { + try { + const result = await notificationService.sendNotification({ + title: '测试通知', + body: '这是一条测试通知,用于验证通知功能是否正常工作', + type: 'test' + }); + return { success: result }; + } catch (error) { + return { success: false, error: error.message }; + } + }); + } \ No newline at end of file diff --git a/packages/main/src/apis/httpApi.ts b/packages/main/src/apis/httpApi.ts index f43aec86..f90ee3eb 100644 --- a/packages/main/src/apis/httpApi.ts +++ b/packages/main/src/apis/httpApi.ts @@ -57,12 +57,44 @@ export function initializeHttpApi() { // 健康检查接口 - router.get('/health', errorHandler(async (req: Request, res: Response) => { - res.json>({ - success: true, - data: { status: 'ok', timestamp: new Date().toISOString() } - }); - })); +router.get('/health', errorHandler(async (req: Request, res: Response) => { + res.json>({ + success: true, + data: { status: 'ok', timestamp: new Date().toISOString() } + }); +})); + +// 用户认证相关接口 +const validateLogin = (req: Request): ValidationError[] => { + const errors: ValidationError[] = []; + if (!req.body.username) errors.push({ field: 'username', message: '用户名必填' }); + if (!req.body.password) errors.push({ field: 'password', message: '密码必填' }); + return errors; +}; + +// 用户登录 +router.post('/auth/login', validateRequest([validateLogin]), errorHandler(async (req: Request, res: Response) => { + const { username, password } = req.body; + const token = await acfunDanmuModule.login(username, password); + res.json>({ success: true, data: { token } }); +})); + +// 用户注销 +router.post('/auth/logout', errorHandler(async (req: Request, res: Response) => { + const { token } = req.body; + await acfunDanmuModule.logout(token); + res.json>({ success: true, data: {} }); +})); + +// 验证用户令牌 +router.get('/auth/verify', errorHandler(async (req: Request, res: Response) => { + const token = req.headers.authorization?.split(' ')[1]; + if (!token) { + return res.status(401).json({ success: false, error: '未提供令牌' }); + } + const isValid = await acfunDanmuModule.verifyToken(token); + res.json>({ success: true, data: { valid: isValid } }); +})); // 窗口关闭验证函数 const validateWindowClose = (req: Request): ValidationError[] => { @@ -174,7 +206,302 @@ export function initializeHttpApi() { }); })); - // ====== 应用管理相关HTTP接口 ====== + // ====== 小程序管理相关HTTP接口 ====== + // 获取小程序列表 + router.get('/miniProgram/list', errorHandler(async (req: Request, res: Response) => { + const result = await globalThis.appManager.getMiniPrograms(); + res.json>({ success: true, data: result }); + })); + + // 添加小程序验证函数 + const validateAddMiniProgram = (req: Request): ValidationError[] => { + const errors: ValidationError[] = []; + if (!req.body.name) errors.push({ field: 'name', message: '小程序名称必填' }); + if (!req.body.path) errors.push({ field: 'path', message: '小程序路径必填' }); + return errors; + }; + + // 添加小程序 + router.post('/miniProgram/add', validateRequest([validateAddMiniProgram]), errorHandler(async (req: Request, res: Response) => { + const { name, path, config } = req.body; + const result = await globalThis.appManager.addMiniProgram(name, path, config); + res.json>({ success: true, data: { id: result } }); + })); + + // 更新小程序配置 + router.post('/miniProgram/update', errorHandler(async (req: Request, res: Response) => { + const { id, config } = req.body; + await globalThis.appManager.updateMiniProgramConfig(id, config); + res.json>({ success: true, data: {} }); + })); + + // 删除小程序 + router.delete('/miniProgram/:id', errorHandler(async (req: Request, res: Response) => { + await globalThis.appManager.removeMiniProgram(req.params.id); + res.json>({ success: true, data: {} }); + })); + + // 快捷键设置相关接口 +router.get('/settings/shortcuts', async (req, res) => { + try { + const shortcuts = await appManager.getShortcuts(); + res.json({ success: true, data: shortcuts }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.post('/settings/shortcuts', async (req, res) => { + try { + const { shortcuts } = req.body; + await appManager.setShortcuts(shortcuts); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.post('/settings/shortcuts/reset', async (req, res) => { + try { + await appManager.resetShortcuts(); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +// 小程序市场相关接口 +router.get('/mini-programs/marketplace', async (req, res) => { + try { + const marketplaceApps = await appManager.getMarketplaceApps(); + res.json({ success: true, data: marketplaceApps }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.post('/mini-programs/install', async (req, res) => { + try { + const { name, source } = req.body; + await appManager.installMiniProgram(name, source); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.post('/mini-programs/update', async (req, res) => { + try { + const { name } = req.body; + await appManager.updateMiniProgram(name); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +// 弹幕系统相关接口 +router.get('/danmu/send', async (req, res) => { + try { + const { roomId, content, userId, nickname } = req.query; + const result = await acfunDanmuModule.sendDanmu(Number(roomId), Number(userId), nickname as string, content as string); + res.json({ success: true, data: result }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.get('/danmu/history', async (req, res) => { + try { + const { roomId, page = 1, pageSize = 20 } = req.query; + const history = await acfunDanmuModule.getDanmuHistory(Number(roomId), Number(page), Number(pageSize)); + res.json({ success: true, data: history }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.post('/danmu/block', async (req, res) => { + try { + const { roomId, userId, duration = 3600 } = req.body; + await acfunDanmuModule.blockUser(Number(roomId), Number(userId), Number(duration)); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +// 房间管理相关接口 +router.get('/room/managers', async (req, res) => { + try { + const { uid, page = 1, pageSize = 20 } = req.query; + const managers = await acfunDanmuModule.getManagerList(Number(uid), Number(page), Number(pageSize)); + res.json({ success: true, data: managers }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.post('/room/managers/add', async (req, res) => { + try { + const { uid, targetId } = req.body; + await acfunDanmuModule.addManager(Number(uid), Number(targetId)); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.post('/room/managers/remove', async (req, res) => { + try { + const { uid, targetId } = req.body; + await acfunDanmuModule.removeManager(Number(uid), Number(targetId)); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.post('/room/kick', async (req, res) => { + try { + const { uid, targetId, reason, duration = 3600 } = req.body; + await acfunDanmuModule.managerKickUser(Number(uid), Number(targetId), reason, Number(duration)); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +// 推流管理相关接口 +router.post('/stream/start', async (req, res) => { + try { + const { roomId, streamKey, quality = 'medium' } = req.body; + const result = await acfunDanmuModule.startStream(Number(roomId), streamKey, quality); + res.json({ success: true, data: result }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.post('/stream/stop', async (req, res) => { + try { + const { roomId } = req.body; + await acfunDanmuModule.stopStream(Number(roomId)); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.get('/stream/status', async (req, res) => { + try { + const { roomId } = req.query; + const status = await acfunDanmuModule.getStreamStatus(Number(roomId)); + res.json({ success: true, data: status }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +// RTMP配置管理 +router.post('/stream/saveRtmpConfig', async (req, res) => { + try { + const { roomId, rtmpUrl, streamKey } = req.body; + const result = await acfunDanmuModule.saveRtmpConfig(Number(roomId), rtmpUrl, streamKey); + res.json({ success: true, data: result }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.get('/stream/getRtmpConfig', async (req, res) => { + try { + const { roomId } = req.query; + const config = await acfunDanmuModule.getRtmpConfig(Number(roomId)); + res.json({ success: true, data: config }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +// OBS连接状态监控 +router.get('/stream/obsStatus', async (req, res) => { + try { + const { roomId } = req.query; + const status = await acfunDanmuModule.getObsConnectionStatus(Number(roomId)); + res.json({ success: true, data: status }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +// 日志系统相关接口 +router.get('/logs', async (req, res) => { + try { + const { source, level, limit = 100 } = req.query; + const logs = getLogManager().getLogs(source as string, Number(limit)); + res.json({ success: true, data: logs }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.delete('/logs', async (req, res) => { + try { + const { source } = req.body; + if (source) { + getLogManager().clearLogs(source); + } else { + getLogManager().clearAllLogs(); + } + res.json({ success: true }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +// 系统功能相关接口 +router.get('/system/network-check', async (req, res) => { + try { + const status = await appManager.checkNetworkStatus(); + res.json({ success: true, data: { connected: status } }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.get('/mini-programs/status', async (req, res) => { + try { + const statuses = await appManager.getMiniProgramStatuses(); + res.json({ success: true, data: statuses }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +// 小程序安装接口 +router.post('/mini-program/install', async (req, res) => { + try { + const { packageUrl, version } = req.body; + const result = await appManager.installMiniProgram(packageUrl, version); + res.json({ success: result }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +// 小程序更新接口 +router.post('/mini-program/update', async (req, res) => { + try { + const { appId } = req.body; + const result = await appManager.updateMiniProgram(appId); + res.json({ success: result }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +// ====== 应用管理相关HTTP接口 ====== // 获取已安装应用 router.get('/app/getInstalledApps', errorHandler(async (req: Request, res: Response) => { const result = await globalThis.appManager.getInstalledApps(); @@ -278,6 +605,23 @@ router.post('/acfunDanmu/restart', errorHandler(async (req: Request, res: Respon }); })); +// 上传直播封面 +const validateUploadCover = (req: Request): ValidationError[] => { + const errors: ValidationError[] = []; + if (!req.body.liveId || req.body.liveId <= 0) errors.push({ field: 'liveId', message: '有效的liveId必填' }); + if (!req.body.imagePath) errors.push({ field: 'imagePath', message: '图片路径必填' }); + return errors; +}; + +router.post('/acfunDanmu/uploadCover', validateRequest([validateUploadCover]), errorHandler(async (req: Request, res: Response) => { + const { liveId, imagePath } = req.body; + const result = await acfunDanmuModule.uploadCover(Number(liveId), imagePath); + res.json>({ + success: true, + data: result + }); +})); + // 更新Acfun弹幕模块配置验证函数 const validateUpdateConfig = (req: Request): ValidationError[] => { const errors: ValidationError[] = []; diff --git a/packages/main/src/apis/index.ts b/packages/main/src/apis/index.ts new file mode 100644 index 00000000..fbcf3216 --- /dev/null +++ b/packages/main/src/apis/index.ts @@ -0,0 +1,62 @@ +import { ipcMain } from 'electron'; +import electron from 'electron'; +import DanmuModule from '../modules/DanmuModule'; +import DataReportService from '../services/DataReportService'; +import { logger } from '@app/utils/logger'; + +// API注册器 - 集中管理主进程API +class ApiRegistry { + private modules: Record = {}; + + constructor() { + this.registerModules(); + this.setupIpcHandlers(); + } + + // 注册所有功能模块 + private registerModules(): void { + // 注册弹幕模块 + const danmuModule = new DanmuModule(); + danmuModule.enable({ app: electron.app }); + this.modules.danmu = danmuModule.getApi(); + + logger.info('All API modules registered successfully'); + } + + // 设置IPC处理器 + private setupIpcHandlers(): void { + // 弹幕模块API + ipcMain.handle('danmu:connectToRoom', (_, roomId: number) => { + return this.modules.danmu.connectToRoom(roomId); + }); + + ipcMain.handle('danmu:disconnectFromRoom', (_, roomId: number) => { + this.modules.danmu.disconnectFromRoom(roomId); + return true; + }); + + ipcMain.handle('danmu:getActiveRooms', () => { + return this.modules.danmu.getActiveRooms(); + }); + + // 数据报表模块API + ipcMain.handle('report:getDailyReport', async (_, date?: Date) => { + return DataReportService.getInstance().getDailyDanmuReport(date); + }); + + ipcMain.handle('report:getAudienceAnalysis', async (_, roomId?: number, days?: number) => { + return DataReportService.getInstance().getAudienceBehaviorAnalysis(roomId, days); + }); + + ipcMain.handle('report:exportToCSV', async (_, reportData: any, reportType: string) => { + return DataReportService.getInstance().exportReportToCSV(reportData, reportType); + }); + + // 后续将添加更多模块的IPC处理... + } +} + +// 初始化API注册器 +export const apiRegistry = new ApiRegistry(); + +export default apiRegistry; \ No newline at end of file diff --git a/packages/main/src/bootstrap/.gitkeep b/packages/main/src/bootstrap/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/main/src/bootstrap/index.ts b/packages/main/src/bootstrap/index.ts new file mode 100644 index 00000000..5cee3136 --- /dev/null +++ b/packages/main/src/bootstrap/index.ts @@ -0,0 +1,83 @@ +import type { AppInitConfig } from './AppInitConfig.js'; +import { createModuleRunner } from './ModuleRunner.js'; +import { createAcfunDanmuModule } from './modules/AcfunDanmuModule.js'; +import { LiveManagementModule } from './modules/LiveManagementModule.js'; + +// 创建直播管理模块的工厂函数 +function createLiveManagementModule(config: { debug?: boolean } = {}): LiveManagementModule { + return new LiveManagementModule(config); +} +import { disallowMultipleAppInstance } from './modules/SingleInstanceApp.js'; +import { createWindowManagerModule } from './modules/WindowManager.js'; +import { terminateAppOnLastWindowClose } from './modules/ApplicationTerminatorOnLastWindowClose.js'; +import { hardwareAccelerationMode } from './modules/HardwareAccelerationModule.js'; +import { autoUpdater } from './modules/AutoUpdater.js'; +import { chromeDevToolsExtension } from './modules/ChromeDevToolsExtension.js'; +import { HttpManager } from './utils/HttpManager.js'; +import { ConfigManager } from './utils/ConfigManager.js'; +import { DataManager } from './utils/DataManager.js'; +import { AppManager } from './utils/AppManager.js'; +import { initializeElectronApi } from './apis/electronApi.js'; +import { initializeHttpApi } from './apis/httpApi.js'; +import { app } from "electron"; + +export async function initApp(initConfig: AppInitConfig) { + const moduleRunner = createModuleRunner() + .init(disallowMultipleAppInstance()) + .init(terminateAppOnLastWindowClose()) + .init(hardwareAccelerationMode({ enable: true })) + .init(autoUpdater()) + .init(createAcfunDanmuModule({ debug: process.env.NODE_ENV === 'development' })) + // Install DevTools extension if needed + // .init(chromeDevToolsExtension({extension: 'VUEJS3_DEVTOOLS'})) + // 初始化Electron API + initializeElectronApi(); + + globalThis.appName = app.getName(); + globalThis.appVersion = app.getVersion(); + + // 延迟初始化WindowManager,确保IPC事件处理器已注册 + const windowManager = createWindowManagerModule({ initConfig, openDevTools: process.env.NODE_ENV === 'development' }); + moduleRunner.init(windowManager); + + // 初始化全局变量 + globalThis.configManager = new ConfigManager(); + globalThis.dataManager = DataManager.getInstance(); + globalThis.httpManager = new HttpManager(); + // Initialize HTTP server to set APP_DIR before AppManager uses it + await globalThis.httpManager.initializeServer(); // <-- Add this line + + // 初始化HTTP API并挂载路由 + const apiRouter = initializeHttpApi(); + globalThis.httpManager.addApiRoutes('/api', apiRouter); + + // 初始化EventSource服务 + import('./initEventSource.js') + .then(({ initEventSourceServices }) => { + initEventSourceServices(); + }) + .catch(error => { + console.error('Failed to initialize EventSource services:', error); + }); + + // 初始化应用 + globalThis.appManager = new AppManager(); + await globalThis.appManager.init(); + globalThis.dataManager.setAppManager(globalThis.appManager); + + // 创建LiveManagementModule实例 + const liveManagementModule = createLiveManagementModule({ debug: process.env.NODE_ENV === 'development' }); + // 初始化模块 + moduleRunner.init(liveManagementModule); + // 注册到AppManager + globalThis.appManager.registerModule('LiveManagementModule', liveManagementModule); + + await moduleRunner; + + // 应用数据准备就绪后通过windowManager通知所有窗口 + windowManager.getWindows().forEach((window: Electron.BrowserWindow) => { + if (!window.isDestroyed()) { + window.webContents.send('apps-ready'); + } + }); +} \ No newline at end of file diff --git a/packages/main/src/initEventSource.ts b/packages/main/src/bootstrap/initEventSource.ts similarity index 100% rename from packages/main/src/initEventSource.ts rename to packages/main/src/bootstrap/initEventSource.ts diff --git a/packages/main/src/config/.gitkeep b/packages/main/src/config/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/main/src/AppInitConfig.ts b/packages/main/src/config/AppInitConfig.ts similarity index 97% rename from packages/main/src/AppInitConfig.ts rename to packages/main/src/config/AppInitConfig.ts index 105f76b8..324b4ea8 100644 --- a/packages/main/src/AppInitConfig.ts +++ b/packages/main/src/config/AppInitConfig.ts @@ -8,4 +8,4 @@ export type AppInitConfig = { path: string; } | URL; -}; +}; \ No newline at end of file diff --git a/packages/main/src/utils/config.ts b/packages/main/src/config/config.ts similarity index 100% rename from packages/main/src/utils/config.ts rename to packages/main/src/config/config.ts diff --git a/packages/main/src/core/.gitkeep b/packages/main/src/core/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/main/src/core/AppManager.ts b/packages/main/src/core/AppManager.ts new file mode 100644 index 00000000..53e311c4 --- /dev/null +++ b/packages/main/src/core/AppManager.ts @@ -0,0 +1,886 @@ +import fs from "fs"; +import path from "path"; +import { promisify } from "util"; +import { ConfigManager } from "../utils/ConfigManager.js"; +import { HttpManager } from "../utils/HttpManager.js"; +import { WindowManager, WindowConfig } from "../modules/WindowManager.js"; +import { getPackageJson } from "./Devars.js"; +import { app } from "electron"; +import { EventEmitter } from "events"; +import { BrowserWindow, ipcMain } from 'electron'; +import { DataManager } from './DataManager'; +import { AppModule } from '../AppModule.js'; +import { securityScanner } from '../modules/SecurityScanner.js'; + + +const readdir = promisify(fs.readdir); +const stat = promisify(fs.stat); + +interface AppConfig { + id: string; + name: string; + version: string; + info?: string; + settings?: Record; + windows: WindowConfig; + supportedDisplays?: ("main" | "obs" | "client")[]; + type?: string; + path?: string; +} + +interface MiniProgramInfo { + id: string; + name: string; + path: string; + status: 'running' | 'stopped' | 'error'; +} + +export class AppManager extends EventEmitter { + private apps: Map = new Map(); + private appWindows: Map = new Map(); + private httpManager: HttpManager = globalThis.httpManager; + private windowManager: WindowManager = globalThis.windowManager; + private appDir: string = ""; + private configManager: ConfigManager = globalThis.configManager; + private modules: Map = new Map(); + private installedApps: any[] = []; + private config: any = {}; + private appsDir: string = ''; + private logManager: any = { addLog: (type: string, message: string, level: string) => console.log(`[${level}] ${type}: ${message}`) }; + + constructor() { + super(); + this.initSecurityListeners(); + this.loadConfig(); + this.loadInstalledApps(); + + // 初始化应用目录 + this.appsDir = path.join(app.getPath('appData'), 'acfun-live-toolbox', 'applications'); + if (!fs.existsSync(this.appsDir)) { + fs.mkdirSync(this.appsDir, { recursive: true }); + } + } + + private initSecurityListeners(): void { + // 监听安全扫描完成事件 + securityScanner.on('scan-completed', (result) => { + console.log('安全扫描完成:', result); + }); + + // 监听安全扫描错误事件 + securityScanner.on('scan-error', (result) => { + console.error('安全扫描错误:', result); + }); + + // 监听异常行为事件 + securityScanner.on('abnormal-behavior', (appId, message) => { + console.warn(`小程序${appId}出现异常行为:`, message); + this.logManager.addLog('security', `小程序${appId}出现异常行为: ${message}`, 'warn'); + }); + } + + private loadConfig(): void { + try { + const configPath = path.join(app.getPath('appData'), 'acfun-live-toolbox', 'app-manager-config.json'); + if (fs.existsSync(configPath)) { + const content = fs.readFileSync(configPath, 'utf-8'); + this.config = JSON.parse(content); + } else { + this.config = { + shortcuts: this.getDefaultShortcuts(), + security: { + autoScan: true, + permissionAudit: true, + runtimeMonitoring: true + } + }; + this.saveConfig(); + } + } catch (error) { + console.error('加载配置失败:', error); + this.config = { + shortcuts: this.getDefaultShortcuts(), + security: { + autoScan: true, + permissionAudit: true, + runtimeMonitoring: true + } + }; + } + } + + private async saveConfig(): Promise { + try { + const configPath = path.join(app.getPath('appData'), 'acfun-live-toolbox', 'app-manager-config.json'); + fs.writeFileSync(configPath, JSON.stringify(this.config, null, 2)); + } catch (error) { + console.error('保存配置失败:', error); + } + } + + private loadInstalledApps(): void { + try { + const appsPath = path.join(app.getPath('appData'), 'acfun-live-toolbox', 'installed-apps.json'); + if (fs.existsSync(appsPath)) { + const content = fs.readFileSync(appsPath, 'utf-8'); + this.installedApps = JSON.parse(content); + } else { + this.installedApps = []; + this.saveInstalledApps(); + } + } catch (error) { + console.error('加载已安装应用失败:', error); + this.installedApps = []; + } + } + + private async saveInstalledApps(): Promise { + try { + const appsPath = path.join(app.getPath('appData'), 'acfun-live-toolbox', 'installed-apps.json'); + fs.writeFileSync(appsPath, JSON.stringify(this.installedApps, null, 2)); + } catch (error) { + console.error('保存已安装应用失败:', error); + } + } + + /** + * 注册模块 + * @param moduleId 模块ID + * @param module 模块实例 + */ + registerModule(moduleId: string, module: AppModule): void { + this.modules.set(moduleId, module); + this.emit('module-registered', moduleId); + console.log(`Module ${moduleId} registered successfully`); + } + + /** + * 获取模块实例 + * @param moduleId 模块ID + * @returns 模块实例或undefined + */ + getModule(moduleId: string): AppModule | undefined { + return this.modules.get(moduleId); + } + + /** + * 获取所有已注册的模块 + * @returns 模块ID和实例的映射 + */ + getAllModules(): Map { + return this.modules; + } + // 修正:直接使用 HttpManager 初始化的应用目录 + private async getAppDirectory(): Promise { + return globalThis.httpManager.getAppDir(); // 使用 HttpManager 暴露的路径 + } + + async init(): Promise { + this.appDir = await this.getAppDirectory(); // 从 HttpManager 获取已初始化的路径 + const appFolders = await this.getAppFolders(); + for (const folder of appFolders) { + const configPath = path.join(folder, "config.json"); + const configContent = await fs.promises.readFile(configPath, "utf-8"); + const config: AppConfig = JSON.parse(configContent); + this.apps.set(config.id, config); + + // 托管静态文件 + this.httpManager.serveStatic(`/application/${config.name}`, folder); + + // 加载API接口 + const apiPath = path.join(folder, "api.cjs"); + if (fs.existsSync(apiPath)) { + // Access default export for ES module compatibility + const apiModule = require(apiPath); + const apiRoutes = apiModule.default || apiModule; + this.httpManager.addApiRoutes( + `/api/application/${config.name}`, + apiRoutes + ); + } + } + } + + // 修正:重写 getAppFolders 方法,使用纯 Promise 风格 + private async getAppFolders(): Promise { + const appDir = this.appDir; // 已通过 init 初始化的有效路径 + if (!appDir) { + throw new Error("Application directory path is undefined"); + } + + const items = await readdir(appDir); // 使用 promisify 后的 readdir + const folders: string[] = []; + + for (const item of items) { + const itemPath = path.join(appDir, item); + const stats = await stat(itemPath); // 使用 promisify 后的 stat + if (stats.isDirectory()) { + folders.push(itemPath); + } + } + + return folders; + } + + // 小程序管理相关方法 + getMiniPrograms(): MiniProgramInfo[] { + return Array.from(this.apps.values()) + .filter(app => app.type === 'miniProgram') + .map(app => ({ id: app.id, name: app.name, path: app.path, status: 'running' })); + } + + async addMiniProgram(name: string, path: string, config?: Record): Promise { + const appId = `mini_${Date.now()}`; + const miniProgramConfig: AppConfig = { + id: appId, + name, + type: 'miniProgram', + path, + version: '1.0.0', + windows: { width: 400, height: 600, title: name } + }; + this.apps.set(appId, { ...miniProgramConfig, ...config }); + await this.saveAppConfig(appId, miniProgramConfig); + return appId; + } + + async updateMiniProgramConfig(appId: string, config: Record): Promise { + const app = this.getAppConfig(appId); + if (!app || app.type !== 'miniProgram') throw new Error('小程序不存在'); + const updatedConfig = { ...app, ...config }; + this.apps.set(appId, updatedConfig); + await this.saveAppConfig(appId, updatedConfig); + } + + async removeMiniProgram(appId: string): Promise { + if (!this.apps.has(appId)) throw new Error('小程序不存在'); + await this.closeApp(appId); + this.apps.delete(appId); + await this.configManager.deleteConfig(appId); + } + + getAppConfig(appId: string): AppConfig | undefined { + return this.apps.get(appId); + } + + async readAppConfig(appId: string): Promise { + const config = this.apps.get(appId); + if (!config) { + throw new Error(`App ${appId} not found`); + } + // 读取已保存的配置 + let savedConfig: any = await this.configManager.readConfig(config.name); + // 如果没有保存的配置,使用默认配置并保存 + if (!savedConfig) { + savedConfig = { ...config }; + await this.configManager.saveConfig(config.name, savedConfig); + } + return savedConfig; + } + + async saveAppConfig(appId: string, configData: AppConfig): Promise { + const app = this.apps.get(appId); + if (!app) { + throw new Error(`App ${appId} not found`); + } + await this.configManager.saveConfig(app.name, configData); + } + + // 数据分析相关方法 + async getLiveStatistics() { + // 实现直播统计数据获取逻辑 + try { + const response = await this.callAcfunApi('/live/statistics'); + return { + viewerCount: response.viewerCount || 0, + likeCount: response.likeCount || 0, + giftCount: response.giftCount || 0, + danmakuCount: response.danmakuCount || 0, + liveDuration: response.liveDuration || 0 + }; + } catch (error) { + this.logManager.addLog('analytics', `获取直播统计失败: ${error.message}`, 'error'); + // 返回缓存数据或默认值 + return this.liveStatsCache || { + viewerCount: 0, + likeCount: 0, + giftCount: 0, + danmakuCount: 0, + liveDuration: 0 + }; + } + } + + async getAudienceAnalysis() { + // 实现观众行为分析逻辑 + return { + sources: { direct: 0, share: 0, search: 0 }, + watchTime: { avg: 0, distribution: [] }, + interactionRate: 0 + }; + } + + async checkNetworkStatus() { + // 实现完整网络检测逻辑 + const results = { + acfunApi: false, + danmuServer: false, + pushServer: false, + cdn: false + }; + + // 检测ACFUN API连接性 + try { + const response = await fetch('https://api.acfun.cn/rest/app/version', { timeout: 5000 }); + results.acfunApi = response.ok; + } catch (error) { + this.logManager.addLog('network', `ACFUN API检测失败: ${error.message}`, 'error'); + } + + // 检测弹幕服务器连接性 + try { + const response = await fetch('https://danmu.acfun.cn/api/v2/status', { timeout:5000 }); + results.danmuServer = response.ok; + } catch (error) { + this.logManager.addLog('network', `弹幕服务器检测失败: ${error.message}`, 'error'); + } + + // 检测推流服务器连接性 + try { + const response = await fetch('https://push.acfun.cn/api/v1/status', { timeout:5000 }); + results.pushServer = response.ok; + } catch (error) { + this.logManager.addLog('network', `推流服务器检测失败: ${error.message}`, 'error'); + } + + // 检测CDN连接性 + try { + const response = await fetch('https://cdn.acfun.cn/health-check', { timeout:5000 }); + results.cdn = response.ok; + } catch (error) { + this.logManager.addLog('network', `CDN检测失败: ${error.message}`, 'error'); + } + + // 记录整体网络状态 + const allOk = Object.values(results).every(status => status); + this.logManager.addLog('network', `网络状态检测完成: ${allOk ? '正常' : '部分服务异常'}`, allOk ? 'info' : 'warn'); + + return results; + } + + async getMarketplaceApps() { + // 获取小程序市场应用列表 + // 实际实现应从服务器获取,此处为模拟数据 + return [ + { + name: '弹幕增强工具', + description: '高级弹幕过滤与管理功能', + version: '1.0.0', + author: 'ACFUN官方', + rating: 4.8, + downloads: 1200 + }, + { + name: '观众数据分析', + description: '详细的观众行为分析报表', + version: '2.1.0', + author: '第三方开发者', + rating: 4.5, + downloads: 850 + } + ]; + } + + async installMiniProgram(name: string, source: string) { + // 实现小程序安装逻辑 + console.log(`开始安装小程序: ${name} 从来源: ${source}`); + + // 1. 验证来源合法性 + if (!this.validateSource(source)) { + throw new Error('不合法的小程序来源'); + } + + // 2. 创建临时目录 + const tempDir = path.join(app.getPath('temp'), `acfun-mini-program-${Date.now()}`); + const installDir = path.join(this.appsDir, name); + + try { + // 确保临时目录不存在 + if (fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + fs.mkdirSync(tempDir, { recursive: true }); + + // 3. 下载小程序资源 + console.log(`正在下载小程序资源到临时目录: ${tempDir}`); + // 实际项目中应该实现真实的下载逻辑 + // 这里为了演示,创建一个简单的模拟小程序结构 + this.createMockMiniProgram(tempDir, name); + + // 4. 安全扫描 - 代码扫描 + if (this.config.security?.autoScan !== false) { + console.log('开始进行安全扫描...'); + const scanResult = await securityScanner.scanMiniProgram(tempDir); + + if (!scanResult.passed) { + console.error('小程序安全扫描失败:', scanResult.issues); + throw new Error(`小程序安全扫描失败: ${scanResult.issues.map(issue => issue.description).join(', ')}`); + } + console.log('安全扫描通过'); + } + + // 5. 权限审计 + if (this.config.security?.permissionAudit !== false) { + console.log('开始进行权限审计...'); + const permissionResult = await securityScanner.auditPermissions(tempDir); + + // 如果权限分数过低,拒绝安装 + if (permissionResult.analysis.permissionScore < 60) { + console.error('小程序权限审计失败:', permissionResult); + throw new Error(`小程序权限审计失败: 权限分数过低 (${permissionResult.analysis.permissionScore}/100)`); + } + console.log('权限审计通过,分数:', permissionResult.analysis.permissionScore); + } + + // 6. 安装到本地目录 + if (fs.existsSync(installDir)) { + fs.rmSync(installDir, { recursive: true, force: true }); + } + fs.cpSync(tempDir, installDir, { recursive: true }); + console.log(`小程序安装到: ${installDir}`); + + // 7. 更新installedApps列表 + const newApp = { + id: `mini_${Date.now()}`, + name, + type: 'miniProgram', + path: installDir, + source, + isRunning: false, + config: {}, + lastUpdated: new Date().toISOString(), + installDate: new Date().toISOString() + }; + + this.installedApps.push(newApp); + await this.saveInstalledApps(); + console.log('小程序安装完成:', newApp); + + return newApp.id; + } catch (error) { + console.error('小程序安装失败:', error); + throw error; + } finally { + // 清理临时目录 + if (fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + } + } + + private validateSource(source: string): boolean { + // 验证来源的合法性 + // 实际项目中应该实现更严格的验证逻辑 + const validDomains = ['https://mini-programs.acfun.cn', 'https://acfun-mini-programs.oss-cn-beijing.aliyuncs.com']; + + try { + const url = new URL(source); + return validDomains.some(domain => url.origin === domain); + } catch (error) { + // 如果不是URL,可能是本地路径或其他格式,需要进一步验证 + return source.startsWith('./') || source.startsWith('../') || path.isAbsolute(source); + } + } + + private createMockMiniProgram(dir: string, name: string): void { + // 创建模拟小程序结构,仅用于演示 + const indexHtml = ` + + + + + + ${name} + + + +
+

${name}

+

这是一个模拟的小程序示例。

+
+ + + `.trim(); + + const manifestJson = JSON.stringify({ + name, + version: '1.0.0', + description: `${name}的描述`, + author: '未知', + entry: 'index.html' + }, null, 2); + + const configJson = JSON.stringify({ + id: `mini_${Date.now()}`, + name, + version: '1.0.0', + type: 'miniProgram', + windows: { + width: 800, + height: 600, + title: name + } + }, null, 2); + + const permissionsJson = JSON.stringify({ + network: { + required: true, + reason: '需要网络连接获取数据' + } + }, null, 2); + + // 创建文件 + fs.writeFileSync(path.join(dir, 'index.html'), indexHtml); + fs.writeFileSync(path.join(dir, 'manifest.json'), manifestJson); + fs.writeFileSync(path.join(dir, 'config.json'), configJson); + fs.writeFileSync(path.join(dir, 'permissions.json'), permissionsJson); + } + + async getShortcuts() { + // 获取当前快捷键配置 + return this.config.shortcuts || this.getDefaultShortcuts(); + } + + async setShortcuts(shortcuts: Record) { + // 保存快捷键配置 + this.config.shortcuts = shortcuts; + await this.saveConfig(); + } + + async resetShortcuts() { + // 重置快捷键为默认值 + this.config.shortcuts = this.getDefaultShortcuts(); + await this.saveConfig(); + } + + private getDefaultShortcuts() { + // 默认快捷键配置 + return { + 'startLive': 'Ctrl+Shift+L', + 'stopLive': 'Ctrl+Shift+S', + 'toggleDanmaku': 'Ctrl+D', + 'muteMic': 'Ctrl+M', + 'openSettings': 'Ctrl+,', + }; + } + + async updateMiniProgram(id: string) { + // 实现小程序更新逻辑 + const appIndex = this.installedApps.findIndex(app => app.id === id && app.type === 'miniProgram'); + if (appIndex === -1) { + throw new Error(`小程序 ${id} 未安装`); + } + + const miniProgram = this.installedApps[appIndex]; + console.log(`开始更新小程序: ${miniProgram.name} (${id})`); + + // 如果小程序正在运行,先停止 + if (miniProgram.isRunning) { + console.log('小程序正在运行,先停止...'); + await this.closeApp(id); + } + + // 创建临时目录 + const tempDir = path.join(app.getPath('temp'), `acfun-mini-program-update-${Date.now()}`); + + try { + // 确保临时目录不存在 + if (fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + fs.mkdirSync(tempDir, { recursive: true }); + + // 下载更新包 + console.log(`正在下载更新包到临时目录: ${tempDir}`); + // 实际项目中应该从服务器下载更新包 + // 这里为了演示,更新模拟小程序的内容 + this.createMockMiniProgram(tempDir, miniProgram.name); + + // 安全扫描 + if (this.config.security?.autoScan !== false) { + console.log('开始进行安全扫描...'); + const scanResult = await securityScanner.scanMiniProgram(tempDir); + + if (!scanResult.passed) { + console.error('更新包安全扫描失败:', scanResult.issues); + throw new Error(`更新包安全扫描失败: ${scanResult.issues.map(issue => issue.description).join(', ')}`); + } + console.log('安全扫描通过'); + } + + // 备份当前版本 + const backupDir = `${miniProgram.path}.bak.${Date.now()}`; + if (fs.existsSync(miniProgram.path)) { + fs.renameSync(miniProgram.path, backupDir); + } + + try { + // 安装更新包 + fs.cpSync(tempDir, miniProgram.path, { recursive: true }); + console.log(`小程序更新完成,安装到: ${miniProgram.path}`); + + // 更新信息 + miniProgram.lastUpdated = new Date().toISOString(); + await this.saveInstalledApps(); + + // 清理备份 + if (fs.existsSync(backupDir)) { + fs.rmSync(backupDir, { recursive: true, force: true }); + } + } catch (installError) { + console.error('安装更新包失败,回滚到备份版本:', installError); + // 回滚到备份版本 + if (fs.existsSync(backupDir)) { + if (fs.existsSync(miniProgram.path)) { + fs.rmSync(miniProgram.path, { recursive: true, force: true }); + } + fs.renameSync(backupDir, miniProgram.path); + } + throw installError; + } + } catch (error) { + console.error('小程序更新失败:', error); + throw error; + } finally { + // 清理临时目录 + if (fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + } + + console.log('小程序更新成功'); + } + + async getMiniProgramStatuses() { + // 实现小程序状态监控逻辑 + return this.installedApps + .filter(app => app.type === 'miniProgram') + .map(app => ({ + name: app.name, + status: app.isRunning ? 'running' : 'stopped', + cpuUsage: 0, + memoryUsage: 0, + lastActive: new Date().toISOString() + })); + } + + async getGiftStatistics() { + // 实现礼物数据统计逻辑 + return { + totalValue: 0, + topUsers: [], + typeDistribution: {} + }; + } + + getAppUrl(appName: string): string { + const port = this.httpManager.getPort(); + return `http://localhost:${port}/application/${appName}/index.html`; + } + + async startApp(appId: string): Promise { + const config = this.getAppConfig(appId); + if (!config) { + throw new Error(`App ${appId} not found`); + } + + // 获取小程序信息 + const miniProgram = this.installedApps.find(app => app.id === appId && app.type === 'miniProgram'); + + // 如果是小程序,进行安全检查 + if (miniProgram) { + // 启动前再次进行快速安全扫描 + if (this.config.security?.autoScan !== false) { + console.log(`启动前对小程序${appId}进行快速安全扫描...`); + const scanResult = await securityScanner.scanMiniProgram(miniProgram.path); + + if (!scanResult.passed) { + console.error('小程序安全扫描失败,拒绝启动:', scanResult.issues); + throw new Error(`小程序安全扫描失败,拒绝启动: ${scanResult.issues.map(issue => issue.description).join(', ')}`); + } + } + + // 启动运行时监控 + if (this.config.security?.runtimeMonitoring !== false) { + console.log(`启动小程序${appId}的运行时监控...`); + securityScanner.startRuntimeMonitoring(appId); + } + + // 更新小程序状态为运行中 + miniProgram.isRunning = true; + await this.saveInstalledApps(); + } + + const window = await this.windowManager.createWindow(config.windows); + window.loadURL(this.getAppUrl(config.name)); + + // 设置窗口事件监听以支持运行时监控 + if (miniProgram && this.config.security?.runtimeMonitoring !== false) { + this.setupRuntimeMonitoring(appId, window); + } + + if (!this.appWindows.has(appId)) { + this.appWindows.set(appId, []); + } + this.appWindows.get(appId)!.push(window); + + return window; + } + + private setupRuntimeMonitoring(appId: string, window: Electron.BrowserWindow): void { + // 设置窗口的事件监听以支持运行时监控 + let lastMemoryUsage = 0; + let lastCpuUsage = 0; + let networkRequests: any[] = []; + let fileAccesses: any[] = []; + let errors: any[] = []; + let warnings: any[] = []; + let eventCount = 0; + + // 定期收集性能数据 + const monitorInterval = setInterval(() => { + // 获取窗口的性能数据 + window.webContents.getProcessId().then(pid => { + // 模拟获取内存和CPU使用率 + // 实际项目中应该使用真实的系统API获取 + const memoryUsage = Math.random() * 100 + 50; // 50-150MB + const cpuUsage = Math.random() * 20; // 0-20% + + // 记录运行时数据 + securityScanner.recordRuntimeData(appId, { + memoryUsage, + cpuUsage, + networkRequests, + fileAccesses, + errors, + warnings, + eventCount + }); + + // 重置计数器 + networkRequests = []; + fileAccesses = []; + errors = []; + warnings = []; + eventCount = 0; + }); + }, 5000); // 每5秒收集一次数据 + + // 监听窗口关闭事件,清理监控 + window.on('closed', () => { + clearInterval(monitorInterval); + securityScanner.stopRuntimeMonitoring(appId); + + // 更新小程序状态 + const miniProgram = this.installedApps.find(app => app.id === appId && app.type === 'miniProgram'); + if (miniProgram) { + miniProgram.isRunning = false; + this.saveInstalledApps().catch(console.error); + } + }); + + // 监听IPC消息,收集网络请求、文件访问等数据 + ipcMain.on(`mini-program-${appId}-network-request`, (_, request) => { + networkRequests.push(request); + eventCount++; + }); + + ipcMain.on(`mini-program-${appId}-file-access`, (_, access) => { + fileAccesses.push(access); + eventCount++; + }); + + ipcMain.on(`mini-program-${appId}-error`, (_, error) => { + errors.push(error); + eventCount++; + }); + + ipcMain.on(`mini-program-${appId}-warning`, (_, warning) => { + warnings.push(warning); + eventCount++; + }); + } + + async closeApp(appId: string): Promise { + const windows = this.appWindows.get(appId) || []; + windows.forEach((window) => window.close()); + this.appWindows.delete(appId); + this.emit("app-closed", appId); + } + + async restartApp(appId: string): Promise { + await this.closeApp(appId); + await this.startApp(appId); + } + + async reloadApps(): Promise { + // 关闭所有窗口 + Array.from(this.appWindows.keys()).forEach( + async (id) => await this.closeApp(id) + ); + + // 清理HTTP托管 + this.apps.forEach((app) => { + this.httpManager.removeStatic(`/application/${app.name}`); + this.httpManager.removeApiRoutes(`/api/application/${app.id}`); + }); + + // 重新初始化 + this.apps.clear(); + await this.init(); + } + private createClientWindow(appId: string): BrowserWindow { + const window = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + preload: path.join(__dirname, "preload.js"), + }, + }); + + // 加载渲染器页面 + const indexPath = path.join(__dirname, '../../../renderer/index.html'); + window.loadFile(indexPath).catch(err => { + console.error('Failed to load window content:', err); + }); + + // 监听窗口关闭事件 + window.on("closed", () => { + DataManager.getInstance().handleClientClosed(appId); + this.emit("app-closed", appId); + }); + + return window; + } +} + diff --git a/packages/main/src/AppModule.ts b/packages/main/src/core/AppModule.ts similarity index 98% rename from packages/main/src/AppModule.ts rename to packages/main/src/core/AppModule.ts index 7833a69b..c32573d8 100644 --- a/packages/main/src/AppModule.ts +++ b/packages/main/src/core/AppModule.ts @@ -2,4 +2,4 @@ import type { ModuleContext } from './ModuleContext.js'; export interface AppModule { enable(context: ModuleContext): Promise | void; -} +} \ No newline at end of file diff --git a/packages/main/src/utils/ConfigManager.ts b/packages/main/src/core/ConfigManager.ts similarity index 99% rename from packages/main/src/utils/ConfigManager.ts rename to packages/main/src/core/ConfigManager.ts index bddd19ea..1ef2851b 100644 --- a/packages/main/src/utils/ConfigManager.ts +++ b/packages/main/src/core/ConfigManager.ts @@ -215,4 +215,4 @@ class ConfigManager { } } -export { ConfigManager }; +export { ConfigManager }; \ No newline at end of file diff --git a/packages/main/src/utils/DataManager.ts b/packages/main/src/core/DataManager.ts similarity index 100% rename from packages/main/src/utils/DataManager.ts rename to packages/main/src/core/DataManager.ts diff --git a/packages/main/src/utils/HttpManager.ts b/packages/main/src/core/HttpManager.ts similarity index 99% rename from packages/main/src/utils/HttpManager.ts rename to packages/main/src/core/HttpManager.ts index 6057a74d..e534c4fd 100644 --- a/packages/main/src/utils/HttpManager.ts +++ b/packages/main/src/core/HttpManager.ts @@ -286,4 +286,4 @@ export class HttpManager { public getAppDir(): string { return this.APP_DIR; } -} +} \ No newline at end of file diff --git a/packages/main/src/ModuleRunner.ts b/packages/main/src/core/ModuleRunner.ts similarity index 99% rename from packages/main/src/ModuleRunner.ts rename to packages/main/src/core/ModuleRunner.ts index 28db6a55..da1e646c 100644 --- a/packages/main/src/ModuleRunner.ts +++ b/packages/main/src/core/ModuleRunner.ts @@ -33,4 +33,4 @@ class ModuleRunner implements PromiseLike { export function createModuleRunner() { return new ModuleRunner(); -} +} \ No newline at end of file diff --git a/packages/main/src/modules/AcfunDanmuModule.ts b/packages/main/src/modules/AcfunDanmuModule.ts index ab12eb35..a9fdeb87 100644 --- a/packages/main/src/modules/AcfunDanmuModule.ts +++ b/packages/main/src/modules/AcfunDanmuModule.ts @@ -5,6 +5,7 @@ import path from 'path'; import { app } from 'electron'; import { getPackageJson } from '../utils/Devars.js'; import { getLogManager } from '../utils/LogManager.js'; +import WebSocket from 'ws'; // 定义配置接口 interface AcfunDanmuConfig { @@ -24,6 +25,8 @@ const DEFAULT_CONFIG: AcfunDanmuConfig = { export class AcfunDanmuModule implements AppModule { private process: ChildProcess | null = null; + private obsWebSocket: WebSocket | null = null; + private obsConnectionStatus: 'disconnected' | 'connecting' | 'connected' = 'disconnected'; private config: AcfunDanmuConfig; private logCallback: ((message: string, type: 'info' | 'error') => void) | null = null; private logManager: ReturnType; @@ -196,6 +199,31 @@ export class AcfunDanmuModule implements AppModule { } } + // 弹幕相关方法 + async sendDanmu(roomId: number, userId: number, nickname: string, content: string): Promise { + return this.callAcfunDanmuApi( + `/danmu/send`, + 'POST', + { roomId, userId, nickname, content } + ); + } + + async getDanmuHistory(roomId: number, page: number = 1, pageSize: number = 20): Promise { + return this.callAcfunDanmuApi( + `/danmu/history`, + 'GET', + { roomId, page, pageSize } + ); + } + + async blockUser(roomId: number, userId: number, duration: number = 3600): Promise { + return this.callAcfunDanmuApi( + `/danmu/block`, + 'POST', + { roomId, userId, duration } + ); + } + // 房管相关方法 async getManagerList(uid: number, page: number = 1, pageSize: number = 20): Promise { return this.callAcfunDanmuApi( @@ -294,6 +322,137 @@ export class AcfunDanmuModule implements AppModule { ); } + // RTMP地址管理 + async saveRtmpConfig(roomId: number, rtmpUrl: string, streamKey: string): Promise { + return this.callAcfunDanmuApi( + `/stream/saveRtmpConfig`, + 'POST', + { roomId, rtmpUrl, streamKey } + ); + } + + async getRtmpConfig(roomId: number): Promise { + return this.callAcfunDanmuApi( + `/stream/getRtmpConfig`, + 'GET', + { roomId } + ); + } + + // OBS连接状态监控 + async getObsConnectionStatus(roomId: number): Promise { + return this.callAcfunDanmuApi( + `/stream/obsStatus`, + 'GET', + { roomId } + ); + } + + // RTMP配置管理 + async saveRtmpConfig(roomId: number, rtmpUrl: string, streamKey: string): Promise { + try { + const rtmpConfigs = this.configManager.readConfig().rtmpConfigs || {}; + rtmpConfigs[roomId] = { rtmpUrl, streamKey, updatedAt: new Date().toISOString() }; + this.configManager.writeConfig({ rtmpConfigs }); + return true; + } catch (error) { + this.logManager.addLog('AcfunDanmuModule', `Failed to save RTMP config: ${error.message}`, 'error'); + return false; + } + } + + async getRtmpConfig(roomId: number): Promise<{rtmpUrl: string, streamKey: string} | null> { + try { + const rtmpConfigs = this.configManager.readConfig().rtmpConfigs || {}; + return rtmpConfigs[roomId] || null; + } catch (error) { + this.logManager.addLog('AcfunDanmuModule', `Failed to get RTMP config: ${error.message}`, 'error'); + return null; + } + } + + // OBS连接管理 + private setupOBSWebSocket(obsHost: string, obsPort: number, password: string): void { + const wsUrl = `ws://${obsHost}:${obsPort}/ws`; + this.obsWebSocket = new WebSocket(wsUrl); + this.obsConnectionStatus = 'connecting'; + + this.obsWebSocket.on('open', () => { + this.logManager.addLog('AcfunDanmuModule', 'OBS WebSocket connected', 'info'); + this.obsConnectionStatus = 'connected'; + // 发送认证请求 + this.obsWebSocket?.send(JSON.stringify({ + "op": 1, + "d": { + "rpcVersion": 1, + "authentication": password + } + })); + }); + + this.obsWebSocket.on('close', () => { + this.logManager.addLog('AcfunDanmuModule', 'OBS WebSocket disconnected', 'info'); + this.obsConnectionStatus = 'disconnected'; + // 自动重连逻辑 + setTimeout(() => this.setupOBSWebSocket(obsHost, obsPort, password), 5000); + }); + + this.obsWebSocket.on('error', (error) => { + this.logManager.addLog('AcfunDanmuModule', `OBS WebSocket error: ${error.message}`, 'error'); + this.obsConnectionStatus = 'disconnected'; + }); + + this.obsWebSocket.on('message', (data) => { + const message = JSON.parse(data.toString()); + // 处理OBS事件通知 + if (message.op === 5) { + this.logManager.addLog('AcfunDanmuModule', `OBS event: ${message.d.eventType}`, 'info'); + // 可扩展处理具体事件(如流状态变化) + } + }); + } + + connectToOBS(obsHost: string = 'localhost', obsPort: number = 4455, password: string = ''): void { + this.setupOBSWebSocket(obsHost, obsPort, password); + } + + disconnectFromOBS(): void { + if (this.obsWebSocket) { + this.obsWebSocket.close(); + this.obsWebSocket = null; + this.obsConnectionStatus = 'disconnected'; + } + } + + getOBSConnectionStatus(): 'disconnected' | 'connecting' | 'connected' { + return this.obsConnectionStatus; + } + + // 推流管理相关方法 + async startStream(roomId: number, streamKey: string, quality: string): Promise { + return this.callAcfunDanmuApi( + `/stream/start`, + 'POST', + { roomId, streamKey, quality } + ); + } + + async stopStream(roomId: number): Promise { + return this.callAcfunDanmuApi( + `/stream/stop`, + 'POST', + { roomId } + ); + } + + async getStreamStatus(roomId: number): Promise { + return this.callAcfunDanmuApi( + `/stream/status`, + 'GET', + { roomId } + ); + } + // 登录相关方法 async login(account: string, password: string): Promise { return this.callAcfunDanmuApi( @@ -419,6 +578,22 @@ export class AcfunDanmuModule implements AppModule { ); } + // 上传直播封面 + async uploadCover(liveId: number, imagePath: string): Promise { + if (!liveId || liveId <= 0) throw new Error('Invalid liveId'); + if (!imagePath) throw new Error('Image path is required'); + try { + return await this.callAcfunDanmuApi( + `/live/uploadCover`, + 'POST', + { liveId, imageFile: imagePath } + ); + } catch (error) { + this.logManager.addLog('AcfunDanmuModule', `Failed to upload cover: ${error.message}`, 'error'); + throw error; + } + } + // 直播预告相关方法 async getScheduleList(): Promise { return this.callAcfunDanmuApi( @@ -429,7 +604,7 @@ export class AcfunDanmuModule implements AppModule { // 弹幕相关方法 async sendDanmu(liveId: number, content: string): Promise { - if (!liveId || liveId <=0) throw new Error('Invalid liveId'); + if (!liveId || liveId <= 0) throw new Error('Invalid liveId'); if (!content || content.trim().length === 0 || content.length > 200) throw new Error('Invalid danmu content'); try { return await this.callAcfunDanmuApi( @@ -438,7 +613,7 @@ export class AcfunDanmuModule implements AppModule { { liveId, content } ); } catch (error) { - this.logger.error(`Failed to send danmu: ${error.message}`, { liveId, content }); + this.logManager.addLog('AcfunDanmuModule', `Failed to send danmu: ${error.message}`, 'error'); throw error; } } @@ -448,7 +623,7 @@ export class AcfunDanmuModule implements AppModule { return this.callAcfunDanmuApi( `/live/status`, 'GET', - { liveId: liveID } + { liveId } ); } diff --git a/packages/main/src/modules/AnalyticsModule.ts b/packages/main/src/modules/AnalyticsModule.ts new file mode 100644 index 00000000..b6a7ec7f --- /dev/null +++ b/packages/main/src/modules/AnalyticsModule.ts @@ -0,0 +1,116 @@ +import { EventEmitter } from 'events'; + +/** + * 数据分析模块 + * 负责实时统计、观众行为分析、礼物统计和数据报表生成 + */ +export class AnalyticsModule extends EventEmitter { + constructor() { + super(); + this.initialize(); + } + + /** + * 初始化数据分析模块 + */ + private async initialize(): Promise { + // 初始化逻辑 + this.emit('initialized'); + } + + /** + * 获取实时统计数据 + * @param roomId 直播间ID + * @returns 实时统计信息 + */ + async getRealTimeStats(roomId: number): Promise<{ + viewerCount: number; + likeCount: number; + giftCount: number; + popularity: number; + }> { + // 实现实时统计逻辑 + return { + viewerCount: 0, + likeCount: 0, + giftCount: 0, + popularity: 0 + }; + } + + /** + * 分析观众行为 + * @param roomId 直播间ID + * @returns 观众行为分析结果 + */ + async analyzeAudienceBehavior(roomId: number): Promise<{ + activeUsers: number; + newUsers: number; + userRetention: number; + topViewingTimes: Array<{ + hour: number; + count: number; + }> + }> { + // 实现观众行为分析逻辑 + return { + activeUsers: 0, + newUsers: 0, + userRetention: 0, + topViewingTimes: [] + }; + } + + /** + * 获取礼物统计数据 + * @param roomId 直播间ID + * @param period 时间周期(daily, weekly, monthly) + * @returns 礼物统计结果 + */ + async getGiftStatistics(roomId: number, period: 'daily' | 'weekly' | 'monthly'): Promise<{ + totalGifts: number; + totalValue: number; + topGifters: Array<{ + userId: number; + userName: string; + giftCount: number; + totalValue: number; + }>; + giftDistribution: Array<{ + giftId: number; + giftName: string; + count: number; + }> + }> { + // 实现礼物统计逻辑 + return { + totalGifts: 0, + totalValue: 0, + topGifters: [], + giftDistribution: [] + }; + } + + /** + * 生成数据报表 + * @param roomId 直播间ID + * @param period 时间周期 + * @returns 报表数据 + */ + async generateReport(roomId: number, period: 'daily' | 'weekly' | 'monthly'): Promise<{ + reportId: string; + generationTime: Date; + data: any; + downloadUrl: string; + }> { + // 实现报表生成逻辑 + return { + reportId: `report-${roomId}-${period}-${Date.now()}`, + generationTime: new Date(), + data: {}, + downloadUrl: '' + }; + } +} + +export default new AnalyticsModule(); \ No newline at end of file diff --git a/packages/main/src/modules/BlacklistManager.ts b/packages/main/src/modules/BlacklistManager.ts new file mode 100644 index 00000000..9729d779 --- /dev/null +++ b/packages/main/src/modules/BlacklistManager.ts @@ -0,0 +1,187 @@ +import { EventEmitter } from 'events'; +import { ConfigManager } from '../utils/ConfigManager'; +import { LogManager } from '../utils/LogManager'; + +// 黑名单用户接口 +export interface BlacklistUser { + userId: string; + username: string; + reason?: string; + addedAt: number; + expiresAt?: number; // 可选的过期时间,永久黑名单不设置此值 +} + +/** + * 黑名单管理模块 + * 负责处理用户黑名单的添加、移除、查询和持久化 + */ +export class BlacklistManager extends EventEmitter { + private blacklist: Map; + private configManager: ConfigManager; + private logManager: LogManager; + private static readonly CONFIG_KEY = 'blacklist'; + + constructor() { + super(); + this.configManager = globalThis.configManager; + this.logManager = globalThis.logManager; + this.blacklist = new Map(); + this.initialize(); + } + + /** + * 初始化黑名单模块 + * 从配置加载已保存的黑名单数据 + */ + private initialize(): void { + this.logManager.addLog('BlacklistManager', 'Initializing blacklist manager', 'info'); + this.loadBlacklist(); + this.cleanupExpiredEntries(); + } + + /** + * 从配置加载黑名单数据 + */ + private loadBlacklist(): void { + try { + const savedBlacklist = this.configManager.readConfig()[BlacklistManager.CONFIG_KEY] || []; + savedBlacklist.forEach((user: BlacklistUser) => { + this.blacklist.set(user.userId, user); + }); + this.logManager.addLog('BlacklistManager', `Loaded ${this.blacklist.size} blacklist entries`, 'info'); + } catch (error) { + this.logManager.addLog('BlacklistManager', `Failed to load blacklist: ${error.message}`, 'error'); + // 加载失败时初始化空黑名单 + this.blacklist = new Map(); + } + } + + /** + * 保存黑名单数据到配置 + */ + private saveBlacklist(): void { + try { + const blacklistArray = Array.from(this.blacklist.values()); + this.configManager.writeConfig({ + [BlacklistManager.CONFIG_KEY]: blacklistArray + }); + this.logManager.addLog('BlacklistManager', `Saved ${blacklistArray.length} blacklist entries`, 'info'); + } catch (error) { + this.logManager.addLog('BlacklistManager', `Failed to save blacklist: ${error.message}`, 'error'); + } + } + + /** + * 清理过期的黑名单条目 + */ + private cleanupExpiredEntries(): void { + const now = Date.now(); + let expiredCount = 0; + + this.blacklist.forEach((user, userId) => { + if (user.expiresAt && user.expiresAt < now) { + this.blacklist.delete(userId); + expiredCount++; + } + }); + + if (expiredCount > 0) { + this.logManager.addLog('BlacklistManager', `Removed ${expiredCount} expired blacklist entries`, 'info'); + this.saveBlacklist(); + this.emit('blacklistUpdated', this.getBlacklist()); + } + } + + /** + * 添加用户到黑名单 + * @param user - 要添加的黑名单用户信息 + * @returns 添加结果 + */ + public addToBlacklist(user: BlacklistUser): boolean { + if (this.blacklist.has(user.userId)) { + this.logManager.addLog('BlacklistManager', `User ${user.username} (${user.userId}) is already in blacklist`, 'warn'); + return false; + } + + // 设置添加时间(如果未提供) + const userToAdd = { + ...user, + addedAt: user.addedAt || Date.now() + }; + + this.blacklist.set(user.userId, userToAdd); + this.logManager.addLog('BlacklistManager', `Added user ${user.username} (${user.userId}) to blacklist. Reason: ${user.reason || 'Not specified'}`, 'info'); + this.saveBlacklist(); + this.emit('blacklistUpdated', this.getBlacklist()); + this.emit('userAdded', userToAdd); + return true; + } + + /** + * 从黑名单移除用户 + * @param userId - 要移除的用户ID + * @returns 移除结果 + */ + public removeFromBlacklist(userId: string): boolean { + const user = this.blacklist.get(userId); + if (!user) { + this.logManager.addLog('BlacklistManager', `User ${userId} is not in blacklist`, 'warn'); + return false; + } + + this.blacklist.delete(userId); + this.logManager.addLog('BlacklistManager', `Removed user ${user.username} (${userId}) from blacklist`, 'info'); + this.saveBlacklist(); + this.emit('blacklistUpdated', this.getBlacklist()); + this.emit('userRemoved', user); + return true; + } + + /** + * 检查用户是否在黑名单中 + * @param userId - 要检查的用户ID + * @returns 是否在黑名单中 + */ + public isBlacklisted(userId: string): boolean { + this.cleanupExpiredEntries(); // 检查前先清理过期条目 + return this.blacklist.has(userId); + } + + /** + * 获取黑名单中的用户信息 + * @param userId - 用户ID + * @returns 用户信息或undefined + */ + public getBlacklistUser(userId: string): BlacklistUser | undefined { + this.cleanupExpiredEntries(); + return this.blacklist.get(userId); + } + + /** + * 获取完整的黑名单列表 + * @returns 黑名单用户数组 + */ + public getBlacklist(): BlacklistUser[] { + this.cleanupExpiredEntries(); + return Array.from(this.blacklist.values()); + } + + /** + * 清空黑名单 + * @returns 操作结果 + */ + public clearBlacklist(): boolean { + if (this.blacklist.size === 0) { + this.logManager.addLog('BlacklistManager', 'Blacklist is already empty', 'warn'); + return false; + } + + const count = this.blacklist.size; + this.blacklist.clear(); + this.logManager.addLog('BlacklistManager', `Cleared ${count} entries from blacklist`, 'info'); + this.saveBlacklist(); + this.emit('blacklistUpdated', this.getBlacklist()); + this.emit('blacklistCleared'); + return true; + } +} \ No newline at end of file diff --git a/packages/main/src/modules/DanmuModule.ts b/packages/main/src/modules/DanmuModule.ts new file mode 100644 index 00000000..4fd449ac --- /dev/null +++ b/packages/main/src/modules/DanmuModule.ts @@ -0,0 +1,135 @@ +import { Module } from '@app/core/interfaces/Module'; +import { DanmuDatabaseService } from '../services/DanmuDatabaseService'; +import { AcfunDanmuModule } from '@app/acfundanmu'; +import { logger } from '@app/utils/logger'; +import { ModuleContext } from '@app/core/interfaces/ModuleContext'; + +// 弹幕连接管理器 - 处理多直播间弹幕流分发 +export class DanmuConnectionManager { + private static instance: DanmuConnectionManager; + private roomConnections: Map = new Map(); + private dbService: DanmuDatabaseService; + + private constructor() { + this.dbService = DanmuDatabaseService.getInstance(); + } + + public static getInstance(): DanmuConnectionManager { + if (!DanmuConnectionManager.instance) { + DanmuConnectionManager.instance = new DanmuConnectionManager(); + } + return DanmuConnectionManager.instance; + } + + // 连接到直播间弹幕流 + async connectToRoom(roomId: number): Promise { + if (this.roomConnections.has(roomId)) { + logger.warn(`Already connected to room ${roomId}`); + return true; + } + + try { + const danmuModule = new AcfunDanmuModule(); + await danmuModule.connect(roomId); + + // 监听弹幕事件并存储到数据库 + danmuModule.on('danmu', (danmu) => this.handleDanmu(roomId, danmu)); + danmuModule.on('error', (error) => this.handleConnectionError(roomId, error)); + danmuModule.on('close', () => this.handleConnectionClose(roomId)); + + this.roomConnections.set(roomId, danmuModule); + logger.info(`Successfully connected to room ${roomId} danmu stream`); + return true; + } catch (error) { + logger.error(`Failed to connect to room ${roomId}:`, error); + return false; + } + } + + // 断开直播间弹幕流连接 + disconnectFromRoom(roomId: number): void { + const connection = this.roomConnections.get(roomId); + if (connection) { + connection.removeAllListeners(); + connection.disconnect(); + this.roomConnections.delete(roomId); + logger.info(`Disconnected from room ${roomId} danmu stream`); + } + } + + // 断开所有直播间连接 + disconnectAll(): void { + this.roomConnections.forEach((_, roomId) => this.disconnectFromRoom(roomId)); + } + + // 处理接收到的弹幕 + private handleDanmu(roomId: number, danmu: any): void { + try { + // 存储弹幕到数据库 + this.dbService.insertDanmu({ + roomId, + userId: danmu.userId, + username: danmu.username, + content: danmu.content, + type: danmu.type, + color: danmu.color, + fontSize: danmu.fontSize, + isGift: danmu.isGift || false, + giftValue: danmu.giftValue || 0 + }); + + // TODO: 实现弹幕分发逻辑,发送到前端 + } catch (error) { + logger.error(`Failed to process danmu for room ${roomId}:`, error); + } + } + + // 处理连接错误 + private handleConnectionError(roomId: number, error: Error): void { + logger.error(`Danmu connection error for room ${roomId}:`, error); + // 自动重连逻辑 + setTimeout(() => this.connectToRoom(roomId), 5000); + } + + // 处理连接关闭 + private handleConnectionClose(roomId: number): void { + logger.warn(`Danmu connection closed for room ${roomId}`); + this.roomConnections.delete(roomId); + // 自动重连 + setTimeout(() => this.connectToRoom(roomId), 3000); + } + + // 获取活跃的直播间连接 + getActiveRooms(): number[] { + return Array.from(this.roomConnections.keys()); + } +} + +// 弹幕模块 - 实现多直播间弹幕流分发机制 +export default class DanmuModule implements Module { + private connectionManager: DanmuConnectionManager; + + enable(context: ModuleContext): void { + this.connectionManager = DanmuConnectionManager.getInstance(); + logger.info('DanmuModule enabled with multi-room support'); + + // 注册应用退出时的清理函数 + context.app.on('will-quit', () => { + this.connectionManager.disconnectAll(); + }); + } + + disable(): void { + this.connectionManager.disconnectAll(); + logger.info('DanmuModule disabled'); + } + + // 提供API方法供前端调用 + getApi() { + return { + connectToRoom: (roomId: number) => this.connectionManager.connectToRoom(roomId), + disconnectFromRoom: (roomId: number) => this.connectionManager.disconnectFromRoom(roomId), + getActiveRooms: () => this.connectionManager.getActiveRooms() + }; + } +} \ No newline at end of file diff --git a/packages/main/src/modules/DashboardModule.ts b/packages/main/src/modules/DashboardModule.ts index f1a2a458..23f58691 100644 --- a/packages/main/src/modules/DashboardModule.ts +++ b/packages/main/src/modules/DashboardModule.ts @@ -1,7 +1,6 @@ import { singleton } from 'tsyringe'; - -import { randomInt } from 'crypto'; -import { acfunLiveAPI } from 'acfundanmu'; +import { authService } from '../utils/AuthService'; +import fetch from 'node-fetch'; interface Stats { viewerCount: number; @@ -53,15 +52,30 @@ export default class DashboardModule { } try { - // 调用ACFUN直播API获取真实数据 - const liveData = await acfunLiveAPI.getLiveData(1); // 获取最近1天数据 - + // 获取认证令牌 + const token = await authService.getCurrentToken(); + if (!token) { + throw new Error('用户未登录,无法获取统计数据'); + } + + // 调用ACFUN直播API获取真实数据 + const response = await fetch('https://api.acfun.cn/v2/live/stats', { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (!response.ok) { + throw new Error(`获取统计数据失败: ${response.statusText}`); + } + + const liveData = await response.json(); const statsData = { viewerCount: liveData.viewerCount || 0, likeCount: liveData.interactionStats?.likeCount || 0, bananaCount: liveData.giftStats?.bananaCount || 0, acCoinCount: liveData.virtualCurrencyStats?.acCoinCount || 0, - // 添加额外的安全检查 totalIncome: liveData.incomeStats?.total || 0 }; @@ -103,29 +117,39 @@ export default class DashboardModule { } try { - // 模拟从服务器获取动态内容 - // 实际应用中应该调用真实的API + // 获取认证令牌 + const token = await authService.getCurrentToken(); + if (!token) { + throw new Error('用户未登录,无法获取动态内容'); + } + + // 调用API获取动态内容 + const response = await fetch('https://api.acfun.cn/v2/dashboard/blocks', { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}` + } + }); + + if (!response.ok) { + throw new Error(`获取动态内容失败: ${response.statusText}`); + } + + const data = await response.json(); const currentHour = new Date().getHours(); const systemNotice = this.getSystemNotice(currentHour); - // 模拟最近直播数据 - const recentStreams = [ - '直播标题: 今天我们聊聊前端技术 | 观众数: 1,280 | 时长: 2小时30分', - '直播标题: 我的游戏日常 | 观众数: 850 | 时长: 3小时15分', - '直播标题: 周末闲聊 | 观众数: 620 | 时长: 1小时45分' - ]; + // 处理API返回的最近直播数据 + const recentStreams = data.recentStreams.map((stream: any) => + `直播标题: ${stream.title} | 观众数: ${stream.viewerCount.toLocaleString()} | 时长: ${stream.duration}` + ); - // 随机决定是否显示活动通知 - const showActivity = Math.random() > 0.5; - let activityNotice: DynamicBlock[] = []; - - if (showActivity) { - activityNotice = [{ + // 处理平台活动通知 + const activityNotice = data.platformActivity ? [{ title: '平台活动', type: 'string', - content: '本周末有直播挑战赛活动,参与即可获得丰厚奖励!详情请查看活动中心。' - }]; - } + content: data.platformActivity + }] : []; const blocks = [ { diff --git a/packages/main/src/modules/ErrorHandlingModule.ts b/packages/main/src/modules/ErrorHandlingModule.ts new file mode 100644 index 00000000..2079ee5c --- /dev/null +++ b/packages/main/src/modules/ErrorHandlingModule.ts @@ -0,0 +1,122 @@ +import { EventEmitter } from 'events'; +import { BrowserWindow } from 'electron'; +import { join } from 'path'; + +/** + * 错误类型定义 + */ +type ErrorType = 'network' | 'authentication' | 'validation' | 'runtime' | 'unknown'; + +/** + * 错误信息接口 + */ +interface ErrorInfo { + type: ErrorType; + code: string; + message: string; + details?: any; + timestamp: Date; +} + +/** + * 错误处理模块 + * 负责全局错误捕获、错误展示和用户反馈 + */ +export class ErrorHandlingModule extends EventEmitter { + private mainWindow: BrowserWindow | null = null; + private errorHistory: ErrorInfo[] = []; + private maxHistorySize = 50; + + constructor() { + super(); + this.setupGlobalErrorHandlers(); + } + + /** + * 设置主窗口引用 + */ + setMainWindow(window: BrowserWindow): void { + this.mainWindow = window; + } + + /** + * 设置全局错误处理器 + */ + private setupGlobalErrorHandlers(): void { + // 捕获未处理的Promise拒绝 + process.on('unhandledRejection', (reason, promise) => { + this.handleError({ + type: 'runtime', + code: 'UNHANDLED_REJECTION', + message: reason instanceof Error ? reason.message : String(reason), + details: { promise } + }); + }); + + // 捕获未捕获的异常 + process.on('uncaughtException', (error) => { + this.handleError({ + type: 'runtime', + code: 'UNCAUGHT_EXCEPTION', + message: error.message, + details: { stack: error.stack } + }); + }); + } + + /** + * 处理错误 + */ + handleError(error: Omit): ErrorInfo { + const errorInfo: ErrorInfo = { + ...error, + timestamp: new Date() + }; + + // 添加到错误历史 + this.errorHistory.unshift(errorInfo); + if (this.errorHistory.length > this.maxHistorySize) { + this.errorHistory.pop(); + } + + // 触发错误事件 + this.emit('error', errorInfo); + + // 显示错误界面 + this.showErrorPage(errorInfo); + + return errorInfo; + } + + /** + * 显示错误页面 + */ + private showErrorPage(errorInfo: ErrorInfo): void { + if (!this.mainWindow) return; + + // 在主窗口中显示错误页面 + this.mainWindow.loadFile(join(__dirname, '../../renderer/error.html'), { + query: { + type: errorInfo.type, + code: errorInfo.code, + message: encodeURIComponent(errorInfo.message) + } + }); + } + + /** + * 获取错误历史 + */ + getErrorHistory(): ErrorInfo[] { + return [...this.errorHistory]; + } + + /** + * 清除错误历史 + */ + clearErrorHistory(): void { + this.errorHistory = []; + } +} + +export default new ErrorHandlingModule(); \ No newline at end of file diff --git a/packages/main/src/modules/LiveManagementModule.ts b/packages/main/src/modules/LiveManagementModule.ts index 50e3f10d..62a3440e 100644 --- a/packages/main/src/modules/LiveManagementModule.ts +++ b/packages/main/src/modules/LiveManagementModule.ts @@ -3,6 +3,7 @@ import { getLogManager } from '../utils/LogManager.js'; import { AppModule } from '../AppModule.js'; import { ModuleContext } from '../ModuleContext.js'; import OBSWebSocket from 'obs-websocket-js'; +import fetch from 'node-fetch'; // 直播管理模块配置接口 export interface LiveManagementConfig { @@ -157,8 +158,8 @@ private obsStatus: OBSStatus = 'offline'; // 实际应用中,这里应该调用API获取真实推流码 // 如果没有推流码,则生成一个模拟的 if (!this.config.streamKey) { - this.config.streamKey = this.generateMockStreamKey(); - } + this.config.streamKey = await this.fetchStreamKeyFromAPI(); + } return { server: this.config.rtmpServer || 'rtmp://push.acfun.cn/live', key: this.config.streamKey @@ -176,7 +177,7 @@ private obsStatus: OBSStatus = 'offline'; async refreshStreamKey(): Promise<{ server: string; streamKey: string }> { try { // 实际应用中,这里应该调用API刷新推流码 - this.config.streamKey = this.generateMockStreamKey(); + this.config.streamKey = await this.fetchStreamKeyFromAPI(); this.logger.info('Stream key refreshed'); return { server: this.config.rtmpServer || 'rtmp://push.acfun.cn/live', @@ -192,6 +193,39 @@ private obsStatus: OBSStatus = 'offline'; * 连接OBS * @returns 是否成功 */ + private async fetchStreamKeyFromAPI(attempt = 0): Promise { + try { + if (!this.config.roomId) { + throw new Error('Room ID is not configured'); + } + + // 调用真实API获取推流码 + const response = await fetch(`https://api.acfun.cn/v2/rooms/${this.config.roomId}/stream-key`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${global.authToken}` + } + }); + + if (!response.ok) { + throw new Error(`Failed to fetch stream key: ${response.statusText}`); + } + + const data = await response.json(); + this.reconnectAttempts = 0; // 重置重试计数器 + return data.streamKey; + } catch (error) { + if (attempt < this.maxReconnectAttempts) { + const delay = Math.min(1000 * Math.pow(2, attempt), this.maxReconnectDelay); + this.logger.warn(`Stream key fetch failed, retrying in ${delay}ms (attempt ${attempt + 1})`); + await new Promise(resolve => setTimeout(resolve, delay)); + return this.fetchStreamKeyFromAPI(attempt + 1); + } + this.logger.error('Max retries reached for stream key fetch'); + throw error; + } + } + async connectOBS(): Promise { try { this.obsStatus = 'connecting'; @@ -221,6 +255,114 @@ private obsStatus: OBSStatus = 'offline'; } } + /** + * 同步推流配置到OBS并开始直播 + */ + async syncStartBroadcast(): Promise { + try { + if (this.obsStatus !== 'online') { + await this.connectOBS(); + } + + // 获取推流码 + const { server, key } = await this.getStreamKey(); + if (!server || !key) { + throw new Error('推流码获取失败'); + } + + // 设置OBS推流配置 + await this.obsWebSocket.call('SetStreamServiceSettings', { + streamServiceType: 'rtmp_custom', + streamServiceSettings: { + server, + key, + useAuth: false + } + }); + + // 自动配置OBS场景和源 + await this.configureOBSScene(); + + // 开始推流 + await this.obsWebSocket.call('StartStream'); + this.streamStatus = 'live'; + this.logger.info('直播已同步开始'); + this.emit('streamStatusChanged', { status: this.streamStatus }); + return true; + } catch (error) { + this.logger.error('同步开播失败:', error); + throw error; + } + } + + /** + * 配置OBS场景和源 + */ + private async configureOBSScene(): Promise { + try { + // 检查场景是否存在,不存在则创建 + const sceneName = 'ACFun直播场景'; + const scenes = await this.obsWebSocket.call('GetSceneList'); + const sceneExists = scenes.scenes.some(s => s.sceneName === sceneName); + + if (!sceneExists) { + await this.obsWebSocket.call('CreateScene', { sceneName }); + this.logger.info(`创建OBS场景: ${sceneName}`); + } + + // 设置当前场景 + await this.obsWebSocket.call('SetCurrentScene', { sceneName }); + + // 添加或更新显示器捕获源 + const displaySourceName = '主显示器'; + const sources = await this.obsWebSocket.call('GetSourcesList'); + const displaySourceExists = sources.sources.some(s => s.sourceName === displaySourceName); + + if (!displaySourceExists) { + await this.obsWebSocket.call('CreateSource', { + sceneName, + sourceName: displaySourceName, + sourceKind: 'monitor_capture', + sourceSettings: { + monitor: 0 // 使用第一个显示器 + } + }); + this.logger.info(`添加显示器捕获源: ${displaySourceName}`); + } + + // 添加或更新文本源显示直播标题 + const titleSourceName = '直播标题'; + const titleSourceExists = sources.sources.some(s => s.sourceName === titleSourceName); + const roomInfo = await this.getRoomInfo(); + + if (!titleSourceExists) { + await this.obsWebSocket.call('CreateSource', { + sceneName, + sourceName: titleSourceName, + sourceKind: 'text_gdiplus', + sourceSettings: { + text: roomInfo.title || 'ACFun直播', + font: { face: 'Microsoft YaHei', size: 36 }, + color: 0xFFFFFF, + position: { x: 50, y: 50 } + } + }); + this.logger.info(`添加文本源: ${titleSourceName}`); + } else { + await this.obsWebSocket.call('SetSourceSettings', { + sourceName: titleSourceName, + sourceSettings: { + text: roomInfo.title || 'ACFun直播' + } + }); + } + + this.logger.info('OBS场景配置完成'); + } catch (error) { + this.logger.error('配置OBS场景失败:', error); + throw error; + } + /** * 获取OBS状态 * @returns OBS状态对象 diff --git a/packages/main/src/modules/MiniProgramModule.ts b/packages/main/src/modules/MiniProgramModule.ts new file mode 100644 index 00000000..e08e323a --- /dev/null +++ b/packages/main/src/modules/MiniProgramModule.ts @@ -0,0 +1,143 @@ +import { EventEmitter } from 'events'; +import { existsSync, readdirSync, readFile, writeFile } from 'fs/promises'; +import { join } from 'path'; + +/** + * 小程序元数据接口 + */ +interface MiniProgramMetadata { + appId: string; + name: string; + description: string; + version: string; + author: string; + entry: string; + icon: string; + permissions: string[]; +} + +/** + * 小程序实例接口 + */ +interface MiniProgramInstance { + appId: string; + metadata: MiniProgramMetadata; + isRunning: boolean; + pid?: number; + windowId?: number; +} + +/** + * 小程序系统模块 + * 负责小程序的管理、运行和市场功能 + */ +export class MiniProgramModule extends EventEmitter { + private miniProgramsPath: string; + private installedPrograms: Map; + private runningPrograms: Map; + + constructor() { + super(); + this.miniProgramsPath = join(__dirname, '../../mini-programs'); + this.installedPrograms = new Map(); + this.runningPrograms = new Map(); + this.initialize(); + } + + /** + * 初始化小程序系统 + */ + private async initialize(): Promise { + // 创建小程序目录 + await this.ensureDirectoryExists(this.miniProgramsPath); + // 加载已安装的小程序 + await this.loadInstalledPrograms(); + this.emit('initialized'); + } + + /** + * 确保目录存在 + */ + private async ensureDirectoryExists(path: string): Promise { + if (!await existsSync(path)) { + await fs.mkdir(path, { recursive: true }); + } + } + + /** + * 加载已安装的小程序 + */ + private async loadInstalledPrograms(): Promise { + const entries = await readdirSync(this.miniProgramsPath, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory()) { + const appId = entry.name; + const manifestPath = join(this.miniProgramsPath, appId, 'manifest.json'); + if (await existsSync(manifestPath)) { + try { + const manifestContent = await readFile(manifestPath, 'utf-8'); + const metadata: MiniProgramMetadata = JSON.parse(manifestContent); + this.installedPrograms.set(appId, metadata); + } catch (error) { + console.error(`Failed to load manifest for ${appId}:`, error); + } + } + } + } + } + + /** + * 获取已安装的小程序列表 + */ + public getInstalledPrograms(): MiniProgramMetadata[] { + return Array.from(this.installedPrograms.values()); + } + + /** + * 启动小程序 + */ + public async startProgram(appId: string): Promise { + if (this.runningPrograms.has(appId)) { + return this.runningPrograms.get(appId) || null; + } + + const metadata = this.installedPrograms.get(appId); + if (!metadata) { + throw new Error(`Program ${appId} not installed`); + } + + // 这里实现小程序启动逻辑 + const instance: MiniProgramInstance = { + appId, + metadata, + isRunning: true + }; + + this.runningPrograms.set(appId, instance); + this.emit('programStarted', instance); + return instance; + } + + /** + * 停止小程序 + */ + public async stopProgram(appId: string): Promise { + const instance = this.runningPrograms.get(appId); + if (!instance) return false; + + // 这里实现小程序停止逻辑 + instance.isRunning = false; + this.runningPrograms.delete(appId); + this.emit('programStopped', appId); + return true; + } + + /** + * 获取运行中的小程序 + */ + public getRunningPrograms(): MiniProgramInstance[] { + return Array.from(this.runningPrograms.values()); + } +} + +export default new MiniProgramModule(); \ No newline at end of file diff --git a/packages/main/src/modules/NotebookModule.ts b/packages/main/src/modules/NotebookModule.ts new file mode 100644 index 00000000..f2b64391 --- /dev/null +++ b/packages/main/src/modules/NotebookModule.ts @@ -0,0 +1,197 @@ +import { EventEmitter } from 'events'; +import { existsSync, readdirSync, readFile, writeFile, unlink, mkdir } from 'fs/promises'; +import { join } from 'path'; +import { v4 as uuidv4 } from 'uuid'; + +/** + * 笔记接口定义 + */ +interface Note { + id: string; + title: string; + content: string; + tags: string[]; + createdAt: Date; + updatedAt: Date; +} + +/** + * 小本本功能模块 + * 负责笔记的创建、编辑、查询和删除管理 + */ +export class NotebookModule extends EventEmitter { + private notesDirectory: string; + private notes: Map; + + constructor() { + super(); + this.notesDirectory = join(__dirname, '../../data/notebooks'); + this.notes = new Map(); + this.initialize(); + } + + /** + * 初始化小本本模块 + */ + private async initialize(): Promise { + // 确保笔记目录存在 + await this.ensureDirectoryExists(this.notesDirectory); + // 加载现有笔记 + await this.loadNotes(); + } + + /** + * 确保目录存在 + */ + private async ensureDirectoryExists(path: string): Promise { + if (!await existsSync(path)) { + await mkdir(path, { recursive: true }); + } + } + + /** + * 加载所有笔记 + */ + private async loadNotes(): Promise { + try { + const files = await readdirSync(this.notesDirectory); + const noteFiles = files.filter(file => file.endsWith('.json')); + + for (const file of noteFiles) { + const filePath = join(this.notesDirectory, file); + const fileContent = await readFile(filePath, 'utf-8'); + const note: Note = JSON.parse(fileContent); + + // 转换日期字符串为Date对象 + note.createdAt = new Date(note.createdAt); + note.updatedAt = new Date(note.updatedAt); + + this.notes.set(note.id, note); + } + + this.emit('notesLoaded', this.getNotes()); + } catch (error) { + console.error('Failed to load notes:', error); + } + } + + /** + * 创建新笔记 + */ + public async createNote(noteData: Omit): Promise { + const note: Note = { + id: uuidv4(), + ...noteData, + createdAt: new Date(), + updatedAt: new Date() + }; + + // 保存笔记到文件 + await this.saveNoteToFile(note); + + // 添加到内存存储 + this.notes.set(note.id, note); + + // 触发事件 + this.emit('noteCreated', note); + + return note; + } + + /** + * 保存笔记到文件 + */ + private async saveNoteToFile(note: Note): Promise { + const filePath = join(this.notesDirectory, `${note.id}.json`); + await writeFile(filePath, JSON.stringify(note, null, 2), 'utf-8'); + } + + /** + * 获取所有笔记 + */ + public getNotes(): Note[] { + return Array.from(this.notes.values()).sort((a, b) => + b.updatedAt.getTime() - a.updatedAt.getTime() + ); + } + + /** + * 根据ID获取笔记 + */ + public getNoteById(id: string): Note | undefined { + return this.notes.get(id); + } + + /** + * 更新笔记 + */ + public async updateNote(id: string, updates: Partial>): Promise { + const existingNote = this.notes.get(id); + if (!existingNote) { + return null; + } + + const updatedNote: Note = { + ...existingNote, + ...updates, + updatedAt: new Date() + }; + + // 保存更新后的笔记 + await this.saveNoteToFile(updatedNote); + + // 更新内存存储 + this.notes.set(id, updatedNote); + + // 触发事件 + this.emit('noteUpdated', updatedNote); + + return updatedNote; + } + + /** + * 删除笔记 + */ + public async deleteNote(id: string): Promise { + const existingNote = this.notes.get(id); + if (!existingNote) { + return false; + } + + // 删除文件 + const filePath = join(this.notesDirectory, `${id}.json`); + await unlink(filePath); + + // 从内存中移除 + this.notes.delete(id); + + // 触发事件 + this.emit('noteDeleted', id); + + return true; + } + + /** + * 根据标签搜索笔记 + */ + public searchNotesByTag(tag: string): Note[] { + return Array.from(this.notes.values()) + .filter(note => note.tags.includes(tag)) + .sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()); + } + + /** + * 根据关键词搜索笔记 + */ + public searchNotesByKeyword(keyword: string): Note[] { + const lowerKeyword = keyword.toLowerCase(); + return Array.from(this.notes.values()) + .filter(note => + note.title.toLowerCase().includes(lowerKeyword) || + note.content.toLowerCase().includes(lowerKeyword) + ) + .sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime()); + } +} + +export default new NotebookModule(); \ No newline at end of file diff --git a/packages/main/src/modules/NotificationModule.ts b/packages/main/src/modules/NotificationModule.ts new file mode 100644 index 00000000..1c2c0728 --- /dev/null +++ b/packages/main/src/modules/NotificationModule.ts @@ -0,0 +1,190 @@ +import { EventEmitter } from 'events'; +import { Notification } from 'electron'; +import { ConfigManager } from '../core/ConfigManager'; + +/** + * 通知类型枚举 + */ +enum NotificationType { + LIVE_STATUS = 'liveStatus', + DANMU_HIGHLIGHT = 'danmuHighlight', + GIFT_ALERT = 'giftAlert', + SYSTEM_UPDATE = 'systemUpdate', + REMINDER = 'reminder' +} + +/** + * 通知配置接口 + */ +interface NotificationConfig { + enabled: boolean; + sound: boolean; + displayDuration: number; + position: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'; +} + +/** + * 通知服务模块 + * 负责管理应用内所有通知的配置、触发和展示 + */ +export class NotificationModule extends EventEmitter { + private configManager: ConfigManager; + private notificationConfigs: Map; + private globalEnabled: boolean; + + constructor() { + super(); + this.configManager = new ConfigManager(); + this.notificationConfigs = new Map(); + this.globalEnabled = true; + this.initialize(); + } + + /** + * 初始化通知模块 + */ + private async initialize(): Promise { + await this.loadConfigurations(); + this.setupDefaultConfigs(); + } + + /** + * 加载通知配置 + */ + private async loadConfigurations(): Promise { + try { + const savedConfigs = await this.configManager.get('notificationSettings'); + if (savedConfigs) { + const defaultConfig: NotificationConfig = { + enabled: true, + sound: true, + displayDuration: 5000, + position: 'top-right' + }; + Object.entries(savedConfigs).forEach(([type, config]) => { + this.notificationConfigs.set(type as NotificationType, { ...defaultConfig, ...config }); + }); + } + + const globalSetting = await this.configManager.get('globalNotificationEnabled'); + this.globalEnabled = globalSetting !== false; + } catch (error) { + console.error('Failed to load notification configurations:', error); + } + } + + /** + * 设置默认配置 + */ + private setupDefaultConfigs(): void { + // 为未配置的通知类型设置默认值 + Object.values(NotificationType).forEach(type => { + if (!this.notificationConfigs.has(type)) { + this.notificationConfigs.set(type, { + enabled: true, + sound: true, + displayDuration: 5000, + position: 'top-right' + }); + } + }); + } + + /** + * 获取特定类型的通知配置 + */ + public getNotificationConfig(type: NotificationType): NotificationConfig { + return this.notificationConfigs.get(type) || { + enabled: true, + sound: true, + displayDuration: 5000, + position: 'top-right' + }; + } + + /** + * 更新通知配置 + */ + public async updateNotificationConfig( + type: NotificationType, + config: Partial + ): Promise { + const currentConfig = this.getNotificationConfig(type); + const updatedConfig = { ...currentConfig, ...config }; + + this.notificationConfigs.set(type, updatedConfig); + await this.saveConfigurations(); + + this.emit('configUpdated', type, updatedConfig); + return updatedConfig; + } + + /** + * 保存所有通知配置 + */ + private async saveConfigurations(): Promise { + const configObject: Record = {}; + this.notificationConfigs.forEach((config, type) => { + configObject[type] = config; + }); + + await this.configManager.set('notificationSettings', configObject); + await this.configManager.set('globalNotificationEnabled', this.globalEnabled); + } + + /** + * 触发通知 + */ + public showNotification( + type: NotificationType, + title: string, + body: string, + onClick?: () => void + ): boolean { + // 检查全局通知开关和类型开关 + if (!this.globalEnabled) return false; + + const config = this.getNotificationConfig(type); + if (!config.enabled) return false; + + // 创建通知 + const notification = new Notification({ + title, + body, + silent: !config.sound, + timeoutType: 'never' + }); + + // 设置点击事件 + if (onClick) { + notification.on('click', onClick); + } + + // 显示通知 + notification.show(); + + // 设置自动关闭 + setTimeout(() => notification.close(), config.displayDuration); + + this.emit('notificationShown', type, { title, body }); + return true; + } + + /** + * 启用/禁用所有通知 + */ + public async setGlobalNotificationEnabled(enabled: boolean): Promise { + this.globalEnabled = enabled; + await this.configManager.set('globalNotificationEnabled', enabled); + this.emit('globalStatusChanged', enabled); + } + + /** + * 获取全局通知状态 + */ + public isGlobalNotificationEnabled(): boolean { + return this.globalEnabled; + } +} + +export default new NotificationModule(); \ No newline at end of file diff --git a/packages/main/src/modules/StreamRecordingModule.ts b/packages/main/src/modules/StreamRecordingModule.ts new file mode 100644 index 00000000..82966a57 --- /dev/null +++ b/packages/main/src/modules/StreamRecordingModule.ts @@ -0,0 +1,191 @@ +import { singleton } from 'tsyringe'; +import { spawn } from 'child_process'; +import { existsSync, mkdirSync, writeFileSync, readdirSync } from 'fs'; +import { join } from 'path'; +import logger from '../utils/logger'; +import { ConfigManager } from '../utils/ConfigManager'; + +@singleton() +export class StreamRecordingModule { + private recordingsPath: string; + private isRecording: boolean = false; + private currentProcess: any = null; + private configManager: ConfigManager; + private currentRecordingFile: string = ''; + + constructor() { + this.configManager = new ConfigManager(); + this.recordingsPath = join(this.configManager.getConfigPath(), 'recordings'); + // 确保录制目录存在 + if (!existsSync(this.recordingsPath)) { + mkdirSync(this.recordingsPath, { recursive: true }); + } + } + + /** + * 开始录制直播 + * @param roomId 直播间ID + * @param quality 录制质量 (high, medium, low) + * @returns 录制文件名 + */ + startRecording(roomId: number, quality: 'high' | 'medium' | 'low' = 'high'): string { + if (this.isRecording) { + throw new Error('已有录制任务正在进行中'); + } + + try { + // 生成录制文件名 + const timestamp = new Date().toISOString().replace(/:/g, '-'); + const filename = `acfun_${roomId}_${timestamp}.flv`; + this.currentRecordingFile = filename; + const outputPath = join(this.recordingsPath, filename); + + // 获取直播流地址 (实际应用中需要从ACFUN API获取真实流地址) + const streamUrl = this.getStreamUrl(roomId, quality); + + // 使用ffmpeg录制流 (需要确保系统已安装ffmpeg) + this.currentProcess = spawn('ffmpeg', [ + '-i', streamUrl, + '-c:v', 'copy', + '-c:a', 'copy', + '-y', // 覆盖已存在文件 + outputPath + ]); + + this.isRecording = true; + + // 记录日志 + this.currentProcess.stdout.on('data', (data: Buffer) => { + logger.info(`录制输出: ${data.toString().trim()}`); + }); + + this.currentProcess.stderr.on('data', (data: Buffer) => { + logger.error(`录制错误: ${data.toString().trim()}`); + }); + + this.currentProcess.on('close', (code: number) => { + this.isRecording = false; + this.currentProcess = null; + this.updateRecordingMetadata(filename, 'completed'); + logger.info(`录制进程已退出,代码: ${code}`); + }); + + // 记录录制元数据 + this.saveRecordingMetadata(filename, roomId, quality); + + return filename; + } catch (error) { + logger.error('开始录制失败:', error); + this.isRecording = false; + throw error; + } + } + + /** + * 停止录制 + */ + stopRecording(): boolean { + if (!this.isRecording || !this.currentProcess) { + return false; + } + + try { + // 优雅地结束ffmpeg进程 + this.currentProcess.kill('SIGINT'); + this.isRecording = false; + this.updateRecordingMetadata(this.currentRecordingFile, 'stopped'); + return true; + } catch (error) { + logger.error('停止录制失败:', error); + return false; + } + } + + /** + * 获取录制状态 + */ + getRecordingStatus(): { isRecording: boolean; currentFile?: string; recordingsPath: string } { + return { + isRecording: this.isRecording, + currentFile: this.currentRecordingFile || undefined, + recordingsPath: this.recordingsPath + }; + } + + /** + * 获取录制文件列表 + */ + getRecordingList(): { name: string; size: number; date: Date }[] { + try { + const files = readdirSync(this.recordingsPath, { withFileTypes: true }) + .filter(dirent => dirent.isFile() && dirent.name.endsWith('.flv')) + .map(file => { + const stats = file.statSync(); + return { + name: file.name, + size: stats.size, + date: new Date(stats.mtime) + }; + }); + + // 按日期降序排序 + return files.sort((a, b) => b.date.getTime() - a.date.getTime()); + } catch (error) { + logger.error('获取录制列表失败:', error); + return []; + } + } + + /** + * 获取直播流地址 (模拟实现) + * @param roomId 直播间ID + * @param quality 质量 + */ + private getStreamUrl(roomId: number, quality: string): string { + // 实际应用中需要从ACFUN API获取真实的流地址 + const qualityParams = { + high: '1080p', + medium: '720p', + low: '480p' + }; + // 这里使用模拟地址,实际项目中需替换为真实API调用 + return `https://live.acfun.cn/api/stream/${roomId}?quality=${qualityParams[quality]}`; + } + + /** + * 保存录制元数据 + */ + private saveRecordingMetadata(filename: string, roomId: number, quality: string): void { + const metadata = { + filename, + roomId, + quality, + startTime: new Date().toISOString(), + status: 'recording', + fileSize: 0 + }; + + const metadataPath = join(this.recordingsPath, `${filename}.json`); + writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), 'utf8'); + } + + /** + * 更新录制元数据 + */ + private updateRecordingMetadata(filename: string, status: 'completed' | 'stopped' | 'failed'): void { + try { + const metadataPath = join(this.recordingsPath, `${filename}.json`); + if (existsSync(metadataPath)) { + // 实际实现中应读取并更新现有元数据 + const metadata = { + ...require(metadataPath), + status, + endTime: new Date().toISOString() + }; + writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), 'utf8'); + } + } catch (error) { + logger.error('更新录制元数据失败:', error); + } + } +} \ No newline at end of file diff --git a/packages/main/src/modules/UserAuthModule.ts b/packages/main/src/modules/UserAuthModule.ts new file mode 100644 index 00000000..a23946d7 --- /dev/null +++ b/packages/main/src/modules/UserAuthModule.ts @@ -0,0 +1,193 @@ +import { EventEmitter } from 'events'; +import { app } from 'electron'; +import { ConfigManager } from '../utils/ConfigManager'; +import { LogManager } from '../utils/LogManager'; + +// 用户认证状态枚举 +export enum AuthStatus { + LOGGED_OUT = 'logged_out', + LOGGING_IN = 'logging_in', + LOGGED_IN = 'logged_in', + GUEST = 'guest' +} + +// 用户信息接口 +export interface UserInfo { + userId: string; + username: string; + avatar?: string; + token?: string; + permissions: string[]; +} + +/** + * 用户认证模块 + * 负责处理登录、登出、会话管理和权限控制 + */ +export class UserAuthModule extends EventEmitter { + private status: AuthStatus = AuthStatus.LOGGED_OUT; + private currentUser: UserInfo | null = null; + private configManager: ConfigManager; + private logManager: LogManager; + private autoLoginTimer?: NodeJS.Timeout; + + constructor() { + super(); + this.configManager = globalThis.configManager; + this.logManager = globalThis.logManager; + this.initialize(); + } + + /** + * 初始化认证模块 + */ + private async initialize(): Promise { + this.logManager.addLog('UserAuthModule', 'Initializing user authentication module', 'info'); + await this.checkAutoLogin(); + } + + /** + * 检查是否需要自动登录 + */ + private async checkAutoLogin(): Promise { + const authConfig = this.configManager.readConfig().auth || {}; + + if (authConfig.autoLogin && authConfig.lastLoginUser) { + this.logManager.addLog('UserAuthModule', 'Attempting auto-login for user', 'info'); + this.status = AuthStatus.LOGGING_IN; + this.emit('statusChanged', this.status); + + try { + // 模拟自动登录过程 + this.autoLoginTimer = setTimeout(async () => { + // 在实际实现中,这里应该调用ACFUN API验证token有效性 + this.currentUser = authConfig.lastLoginUser; + this.status = AuthStatus.LOGGED_IN; + this.logManager.addLog('UserAuthModule', `Auto-login successful for user: ${this.currentUser.username}`, 'info'); + this.emit('statusChanged', this.status); + this.emit('userChanged', this.currentUser); + }, 1500); + } catch (error) { + this.logManager.addLog('UserAuthModule', `Auto-login failed: ${error.message}`, 'error'); + this.status = AuthStatus.LOGGED_OUT; + this.emit('statusChanged', this.status); + } + } + } + + /** + * 以游客模式登录 + */ + public enterGuestMode(): void { + this.logManager.addLog('UserAuthModule', 'Entering guest mode', 'info'); + this.status = AuthStatus.GUEST; + this.currentUser = { + userId: 'guest', + username: '游客', + permissions: ['guest'] + }; + this.emit('statusChanged', this.status); + this.emit('userChanged', this.currentUser); + } + + /** + * 用户登录 + * @param token - 认证token + * @param userInfo - 用户信息 + */ + public async login(token: string, userInfo: UserInfo): Promise { + if (this.status === AuthStatus.LOGGING_IN) { + this.logManager.addLog('UserAuthModule', 'Login process already in progress', 'warn'); + return false; + } + + this.status = AuthStatus.LOGGING_IN; + this.emit('statusChanged', this.status); + + try { + // 在实际实现中,这里应该验证token并获取完整用户信息 + this.currentUser = { + ...userInfo, + token + }; + + // 保存登录状态用于自动登录 + const authConfig = { + autoLogin: true, + lastLoginUser: this.currentUser + }; + this.configManager.writeConfig({ auth: authConfig }); + + this.status = AuthStatus.LOGGED_IN; + this.logManager.addLog('UserAuthModule', `Login successful for user: ${this.currentUser.username}`, 'info'); + this.emit('statusChanged', this.status); + this.emit('userChanged', this.currentUser); + return true; + } catch (error) { + this.logManager.addLog('UserAuthModule', `Login failed: ${error.message}`, 'error'); + this.status = AuthStatus.LOGGED_OUT; + this.emit('statusChanged', this.status); + return false; + } + } + + /** + * 用户登出 + * @param keepAutoLogin - 是否保持自动登录状态 + */ + public logout(keepAutoLogin: boolean = false): void { + this.logManager.addLog('UserAuthModule', 'User logging out', 'info'); + + // 清除自动登录状态(如果需要) + if (!keepAutoLogin) { + const authConfig = this.configManager.readConfig().auth || {}; + authConfig.autoLogin = false; + this.configManager.writeConfig({ auth: authConfig }); + } + + // 清除当前用户信息 + this.currentUser = null; + this.status = AuthStatus.LOGGED_OUT; + this.emit('statusChanged', this.status); + this.emit('userChanged', null); + + // 清除可能的自动登录定时器 + if (this.autoLoginTimer) { + clearTimeout(this.autoLoginTimer); + this.autoLoginTimer = undefined; + } + } + + /** + * 获取当前认证状态 + */ + public getStatus(): AuthStatus { + return this.status; + } + + /** + * 获取当前用户信息 + */ + public getUserInfo(): UserInfo | null { + return this.currentUser; + } + + /** + * 检查用户是否有权限执行某个操作 + * @param permission - 权限名称 + */ + public hasPermission(permission: string): boolean { + if (!this.currentUser) return false; + return this.currentUser.permissions.includes(permission) || this.currentUser.permissions.includes('admin'); + } + + /** + * 销毁模块资源 + */ + public destroy(): void { + if (this.autoLoginTimer) { + clearTimeout(this.autoLoginTimer); + } + this.removeAllListeners(); + } +} \ No newline at end of file diff --git a/packages/main/src/services/.gitkeep b/packages/main/src/services/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/main/src/services/AuthService.ts b/packages/main/src/services/AuthService.ts new file mode 100644 index 00000000..7f3337c6 --- /dev/null +++ b/packages/main/src/services/AuthService.ts @@ -0,0 +1,201 @@ +import { session } from 'electron'; +import { v4 as uuidv4 } from 'uuid'; +import fs from 'fs'; +import path from 'path'; + +// 定义会话接口 +interface AuthSession { + userId: string; + username: string; + token: string; + expiresAt: number; + roles: string[]; +} + +// 定义登录信息接口 +interface LoginInfo { + username: string; + password: string; + loginMethod: 'qr' | 'password' | 'guest'; + qrCodeId?: string; +} + +// 认证服务类 +export class AuthService { + private static instance: AuthService; + private sessionStore: Map = new Map(); + private sessionFilePath: string; + private guestSession: AuthSession = { + userId: 'guest', + username: '游客', + token: 'guest-token', + expiresAt: Date.now() + 30 * 24 * 60 * 60 * 1000, // 30天有效期 + roles: ['guest'] + }; + + private constructor() { + // 初始化会话存储路径 + this.sessionFilePath = path.join(__dirname, '../../sessions.json'); + this.loadSessions(); + } + + // 单例模式获取实例 + public static getInstance(): AuthService { + if (!AuthService.instance) { + AuthService.instance = new AuthService(); + } + return AuthService.instance; + } + + // 加载会话数据 + private loadSessions(): void { + try { + if (fs.existsSync(this.sessionFilePath)) { + const data = fs.readFileSync(this.sessionFilePath, 'utf8'); + const sessions = JSON.parse(data) as Record; + Object.entries(sessions).forEach(([token, session]) => { + if (session.expiresAt > Date.now()) { + this.sessionStore.set(token, session); + } + }); + } + } catch (error) { + console.error('Failed to load sessions:', error); + } + } + + // 保存会话数据 + private saveSessions(): void { + try { + const sessions: Record = {}; + this.sessionStore.forEach((session, token) => { + sessions[token] = session; + }); + fs.writeFileSync(this.sessionFilePath, JSON.stringify(sessions, null, 2)); + } catch (error) { + console.error('Failed to save sessions:', error); + } + } + + // 登录方法 + async login(loginInfo: LoginInfo): Promise { + // 游客登录处理 + if (loginInfo.loginMethod === 'guest') { + return this.guestSession; + } + + // 真实账号密码登录实现 + if (loginInfo.loginMethod === 'password') { + try { + const response = await fetch('https://api.acfun.cn/v2/auth/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + username: loginInfo.username, + password: loginInfo.password + }) + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.message || '登录失败: ' + response.statusText); + } + + const data = await response.json(); + const newSession: AuthSession = { + userId: data.userId, + username: data.username, + token: data.token, + expiresAt: Date.now() + data.expiresIn * 1000, + roles: data.roles || ['user'] + }; + + this.sessionStore.set(newSession.token, newSession); + this.saveSessions(); + return newSession; + } catch (error) { + console.error('登录API调用失败:', error); + throw new Error('登录失败: ' + (error instanceof Error ? error.message : String(error))); + } + } + + // 二维码登录处理(实际项目中需要实现二维码验证逻辑) + if (loginInfo.loginMethod === 'qr' && loginInfo.qrCodeId) { + throw new Error('二维码登录功能尚未实现'); + } + + throw new Error('无效的登录方式'); + } + + // 登出方法 + async logout(token?: string): Promise { + if (token) { + this.sessionStore.delete(token); + this.saveSessions(); + } else { + // 清除当前会话 + const currentSession = session.defaultSession; + await currentSession.clearStorageData(); + } + } + + // 检查认证状态 + async checkStatus(token?: string): Promise { + if (!token) { + return null; + } + + // 游客会话特殊处理 + if (token === 'guest-token') { + return this.guestSession; + } + + const session = this.sessionStore.get(token); + if (session && session.expiresAt > Date.now()) { + return session; + } + + // 会话过期或不存在 + if (session) { + this.sessionStore.delete(token); + this.saveSessions(); + } + return null; + } + + // 获取当前令牌 + async getCurrentToken(): Promise { + try { + const sessions = Array.from(this.sessionStore.values()); + const now = Date.now(); + // 查找未过期的最新会话 + const validSession = sessions + .filter(s => s.expiresAt > now) + .sort((a, b) => b.expiresAt - a.expiresAt)[0]; + return validSession?.token || null; + } catch (error) { + console.error('获取当前令牌失败:', error); + return null; + } + } + + // 验证权限 + hasPermission(token: string, requiredPermission: string): boolean { + const session = this.sessionStore.get(token); + if (!session) { + return false; + } + + // 管理员拥有所有权限 + if (session.roles.includes('admin')) { + return true; + } + + // 这里可以实现更复杂的权限验证逻辑 + return session.roles.includes(requiredPermission); + } +} + +export const authService = AuthService.getInstance(); \ No newline at end of file diff --git a/packages/main/src/services/DanmuDatabaseService.ts b/packages/main/src/services/DanmuDatabaseService.ts new file mode 100644 index 00000000..2068d118 --- /dev/null +++ b/packages/main/src/services/DanmuDatabaseService.ts @@ -0,0 +1,173 @@ +import Database from 'better-sqlite3'; +import { app } from 'electron'; +import path from 'path'; +import fs from 'fs'; + +// 弹幕数据库服务 - 处理弹幕的持久化存储 +export class DanmuDatabaseService { + private static instance: DanmuDatabaseService; + private db: Database.Database; + private dbPath: string; + + private constructor() { + // 初始化数据库路径 + const userDataPath = app.getPath('userData'); + this.dbPath = path.join(userDataPath, 'danmu.db'); + this.db = this.initDatabase(); + } + + // 单例模式获取实例 + public static getInstance(): DanmuDatabaseService { + if (!DanmuDatabaseService.instance) { + DanmuDatabaseService.instance = new DanmuDatabaseService(); + } + return DanmuDatabaseService.instance; + } + + // 初始化数据库连接并创建表 + private initDatabase(): Database.Database { + try { + // 确保目录存在 + const dir = path.dirname(this.dbPath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + // 连接数据库 + const db = new Database(this.dbPath); + + // 创建弹幕表 + db.exec(` + CREATE TABLE IF NOT EXISTS danmu ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + roomId INTEGER NOT NULL, + userId TEXT NOT NULL, + username TEXT NOT NULL, + content TEXT NOT NULL, + timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + type TEXT NOT NULL, + color TEXT, + fontSize INTEGER, + isGift BOOLEAN DEFAULT FALSE, + giftValue INTEGER DEFAULT 0 + ); + + // 创建索引提升查询性能 + CREATE INDEX IF NOT EXISTS idx_danmu_roomId ON danmu(roomId); + CREATE INDEX IF NOT EXISTS idx_danmu_timestamp ON danmu(timestamp); + `); + + return db; + } catch (error) { + console.error('Failed to initialize danmu database:', error); + throw new Error('弹幕数据库初始化失败'); + } + } + + // 插入新弹幕 + public insertDanmu(danmu: { + roomId: number; + userId: string; + username: string; + content: string; + type: string; + color?: string; + fontSize?: number; + isGift?: boolean; + giftValue?: number; + }): void { + try { + const stmt = this.db.prepare(` + INSERT INTO danmu ( + roomId, userId, username, content, type, color, fontSize, isGift, giftValue + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + `); + + stmt.run( + danmu.roomId, + danmu.userId, + danmu.username, + danmu.content, + danmu.type, + danmu.color || null, + danmu.fontSize || null, + danmu.isGift ? 1 : 0, + danmu.giftValue || 0 + ); + } catch (error) { + console.error('Failed to insert danmu:', error); + throw new Error('保存弹幕失败'); + } + } + + // 批量插入弹幕 + public bulkInsertDanmu(danmus: Array<{ + roomId: number; + userId: string; + username: string; + content: string; + type: string; + color?: string; + fontSize?: number; + isGift?: boolean; + giftValue?: number; + }>): void { + const transaction = this.db.transaction((items) => { + for (const item of items) { + this.insertDanmu(item); + } + }); + + try { + transaction(danmus); + } catch (error) { + console.error('Failed to bulk insert danmu:', error); + throw new Error('批量保存弹幕失败'); + } + } + + // 查询指定房间的弹幕 + public getDanmuByRoomId(roomId: number, limit: number = 100, offset: number = 0): Array { + try { + const stmt = this.db.prepare(` + SELECT * FROM danmu + WHERE roomId = ? + ORDER BY timestamp DESC + LIMIT ? OFFSET ? + `); + + return stmt.all(roomId, limit, offset); + } catch (error) { + console.error('Failed to query danmu by roomId:', error); + throw new Error('查询弹幕失败'); + } + } + + // 查询指定时间范围内的弹幕 + public getDanmuByTimeRange( + roomId: number, + startTime: Date, + endTime: Date + ): Array { + try { + const stmt = this.db.prepare(` + SELECT * FROM danmu + WHERE roomId = ? + AND timestamp BETWEEN ? AND ? + ORDER BY timestamp ASC + `); + + return stmt.all(roomId, startTime.toISOString(), endTime.toISOString()); + } catch (error) { + console.error('Failed to query danmu by time range:', error); + throw new Error('查询时间范围内弹幕失败'); + } + } + + // 关闭数据库连接 + public close(): void { + if (this.db) { + this.db.close(); + } + } +} \ No newline at end of file diff --git a/packages/main/src/services/DataReportService.ts b/packages/main/src/services/DataReportService.ts new file mode 100644 index 00000000..3b641af1 --- /dev/null +++ b/packages/main/src/services/DataReportService.ts @@ -0,0 +1,204 @@ +import { DanmuDatabaseService } from './DanmuDatabaseService'; +import { logger } from '@app/utils/logger'; +import fs from 'fs'; +import path from 'path'; +import { app } from 'electron'; + +// 数据报表服务 - 处理数据统计与分析功能 +export class DataReportService { + private static instance: DataReportService; + private dbService: DanmuDatabaseService; + private exportPath: string; + + private constructor() { + this.dbService = DanmuDatabaseService.getInstance(); + this.exportPath = path.join(app.getPath('documents'), 'AcfunLiveToolbox', 'Reports'); + this.ensureExportDirectory(); + } + + public static getInstance(): DataReportService { + if (!DataReportService.instance) { + DataReportService.instance = new DataReportService(); + } + return DataReportService.instance; + } + + // 确保导出目录存在 + private ensureExportDirectory(): void { + if (!fs.existsSync(this.exportPath)) { + fs.mkdirSync(this.exportPath, { recursive: true }); + } + } + + // 获取每日弹幕统计报表 + async getDailyDanmuReport(date: Date = new Date()): Promise { + try { + // 设置日期范围为当天 + const startDate = new Date(date); + startDate.setHours(0, 0, 0, 0); + const endDate = new Date(date); + endDate.setHours(23, 59, 59, 999); + + // 查询当天弹幕总量 + const totalDanmuStmt = this.dbService.db.prepare(` + SELECT COUNT(*) as total FROM danmu + WHERE timestamp BETWEEN ? AND ? + `); + const totalDanmu = totalDanmuStmt.get(startDate.toISOString(), endDate.toISOString()); + + // 查询各房间弹幕分布 + const roomDistributionStmt = this.dbService.db.prepare(` + SELECT roomId, COUNT(*) as count + FROM danmu + WHERE timestamp BETWEEN ? AND ? + GROUP BY roomId + ORDER BY count DESC + `); + const roomDistribution = roomDistributionStmt.all(startDate.toISOString(), endDate.toISOString()); + + // 查询弹幕高峰时段 + const hourlyDistributionStmt = this.dbService.db.prepare(` + SELECT strftime('%H', timestamp) as hour, COUNT(*) as count + FROM danmu + WHERE timestamp BETWEEN ? AND ? + GROUP BY hour + ORDER BY hour + `); + const hourlyDistribution = hourlyDistributionStmt.all(startDate.toISOString(), endDate.toISOString()); + + // 查询礼物统计 + const giftStatsStmt = this.dbService.db.prepare(` + SELECT SUM(giftValue) as totalGiftValue, COUNT(*) as giftCount + FROM danmu + WHERE timestamp BETWEEN ? AND ? AND isGift = 1 + `); + const giftStats = giftStatsStmt.get(startDate.toISOString(), endDate.toISOString()); + + return { + date: startDate.toISOString().split('T')[0], + totalDanmu: totalDanmu.total, + roomDistribution, + hourlyDistribution, + giftStats: { + totalGiftValue: giftStats.totalGiftValue || 0, + giftCount: giftStats.giftCount || 0 + } + }; + } catch (error) { + logger.error('Failed to generate daily danmu report:', error); + throw new Error('生成数据报表失败'); + } + } + + // 获取观众行为分析 + async getAudienceBehaviorAnalysis(roomId?: number, days: number = 7): Promise { + try { + const endDate = new Date(); + const startDate = new Date(); + startDate.setDate(startDate.getDate() - days); + + // 构建查询条件 + const roomCondition = roomId ? 'AND roomId = ?' : ''; + const params: any[] = [startDate.toISOString(), endDate.toISOString()]; + if (roomId) params.push(roomId); + + // 查询活跃用户统计 + const activeUsersStmt = this.dbService.db.prepare(` + SELECT userId, username, COUNT(*) as danmuCount + FROM danmu + WHERE timestamp BETWEEN ? AND ? ${roomCondition} + GROUP BY userId + ORDER BY danmuCount DESC + LIMIT 20 + `); + const activeUsers = activeUsersStmt.all(...params); + + // 查询观众活跃度趋势 + const activityTrendStmt = this.dbService.db.prepare(` + SELECT strftime('%Y-%m-%d', timestamp) as date, COUNT(DISTINCT userId) as activeUsers + FROM danmu + WHERE timestamp BETWEEN ? AND ? ${roomCondition} + GROUP BY date + ORDER BY date + `); + const activityTrend = activityTrendStmt.all(...params); + + // 查询常用弹幕关键词 (简单版本) + const keywordsStmt = this.dbService.db.prepare(` + SELECT content, COUNT(*) as count + FROM danmu + WHERE timestamp BETWEEN ? AND ? ${roomCondition} AND LENGTH(content) > 2 + GROUP BY content + ORDER BY count DESC + LIMIT 50 + `); + const keywords = keywordsStmt.all(...params); + + return { + timeRange: { + start: startDate.toISOString(), + end: endDate.toISOString() + }, + roomId, + activeUsers, + activityTrend, + topKeywords: keywords + }; + } catch (error) { + logger.error('Failed to generate audience behavior analysis:', error); + throw new Error('生成观众行为分析失败'); + } + } + + // 导出报表为CSV + async exportReportToCSV(reportData: any, reportType: string): Promise { + try { + const dateStr = new Date().toISOString().split('T')[0]; + const fileName = `${reportType}_report_${dateStr}.csv`; + const filePath = path.join(this.exportPath, fileName); + + // 根据报表类型生成CSV内容 + let csvContent = ''; + if (reportType === 'daily') { + csvContent = this.generateDailyReportCSV(reportData); + } else if (reportType === 'audience') { + csvContent = this.generateAudienceReportCSV(reportData); + } + + // 写入文件 + fs.writeFileSync(filePath, csvContent, 'utf-8'); + return filePath; + } catch (error) { + logger.error('Failed to export report to CSV:', error); + throw new Error('导出报表失败'); + } + } + + // 生成日报CSV内容 + private generateDailyReportCSV(data: any): string { + // 实现CSV生成逻辑 + let csv = '日期,总弹幕数,礼物总数,礼物总价值\n'; + csv += `${data.date},${data.totalDanmu},${data.giftStats.giftCount},${data.giftStats.totalGiftValue}\n\n`; + + csv += '房间ID,弹幕数量\n'; + data.roomDistribution.forEach((item: any) => { + csv += `${item.roomId},${item.count}\n`; + }); + + return csv; + } + + // 生成观众分析CSV内容 + private generateAudienceReportCSV(data: any): string { + // 实现CSV生成逻辑 + let csv = `时间范围: ${data.timeRange.start} 至 ${data.timeRange.end}\n`; + if (data.roomId) csv += `房间ID: ${data.roomId}\n`; + csv += '\n活跃用户,发送弹幕数\n'; + + data.activeUsers.forEach((user: any) => { + csv += `${user.username} (${user.userId}),${user.danmuCount}\n`; + }); + + return csv; + } +} \ No newline at end of file diff --git a/packages/main/src/services/ErrorHandlingService.ts b/packages/main/src/services/ErrorHandlingService.ts new file mode 100644 index 00000000..d89ef265 --- /dev/null +++ b/packages/main/src/services/ErrorHandlingService.ts @@ -0,0 +1,162 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { app } from 'electron'; +import { v4 as uuidv4 } from 'uuid'; + +interface ErrorInfo { + message: string; + stack?: string; + context?: Record; + timestamp: Date; +} + +interface StoredError extends ErrorInfo { + id: string; +} + +/** + * 错误处理服务,负责应用程序的错误日志记录、查询和管理 + */ +export class ErrorHandlingService { + private static instance: ErrorHandlingService; + private errorLogPath: string; + private maxErrorHistory = 1000; // 最大错误历史记录数 + + private constructor() { + // 初始化错误日志文件路径 + const userDataPath = app.getPath('userData'); + this.errorLogPath = path.join(userDataPath, 'error-logs.json'); + this.initializeErrorLogFile(); + } + + /** + * 获取单例实例 + */ + public static getInstance(): ErrorHandlingService { + if (!ErrorHandlingService.instance) { + ErrorHandlingService.instance = new ErrorHandlingService(); + } + return ErrorHandlingService.instance; + } + + /** + * 初始化错误日志文件(如果不存在则创建) + */ + private initializeErrorLogFile(): void { + if (!fs.existsSync(this.errorLogPath)) { + fs.writeFileSync(this.errorLogPath, JSON.stringify([]), 'utf8'); + } + } + + /** + * 记录错误信息 + * @param errorInfo 错误信息对象 + * @returns 错误ID + */ + public async logError(errorInfo: Omit): Promise { + try { + const error: StoredError = { + id: uuidv4(), + ...errorInfo, + timestamp: new Date() + }; + + // 读取现有错误日志 + const errors = await this.getErrorHistory(); + // 添加新错误并限制历史记录数量 + const updatedErrors = [error, ...errors].slice(0, this.maxErrorHistory); + + // 写入更新后的错误日志 + await fs.promises.writeFile( + this.errorLogPath, + JSON.stringify(updatedErrors, null, 2), + 'utf8' + ); + + return error.id; + } catch (error) { + console.error('Failed to log error:', error); + throw new Error(`Error logging failed: ${(error as Error).message}`); + } + } + + /** + * 获取错误历史记录 + * @param params 查询参数(可选) + * @returns 错误历史记录数组 + */ + public async getErrorHistory(params: { limit?: number; startDate?: Date; endDate?: Date } = {}): Promise { + try { + // 读取错误日志文件 + const data = await fs.promises.readFile(this.errorLogPath, 'utf8'); + let errors: StoredError[] = JSON.parse(data); + + // 解析日期字符串为Date对象 + errors = errors.map(error => ({ + ...error, + timestamp: new Date(error.timestamp) + })); + + // 应用日期范围过滤 + if (params.startDate) { + errors = errors.filter(error => error.timestamp >= params.startDate); + } + if (params.endDate) { + errors = errors.filter(error => error.timestamp <= params.endDate); + } + + // 应用数量限制 + if (params.limit) { + errors = errors.slice(0, params.limit); + } + + return errors; + } catch (error) { + console.error('Failed to get error history:', error); + throw new Error(`Error history retrieval failed: ${(error as Error).message}`); + } + } + + /** + * 清除错误历史记录 + */ + public async clearErrorHistory(): Promise { + try { + await fs.promises.writeFile(this.errorLogPath, JSON.stringify([]), 'utf8'); + } catch (error) { + console.error('Failed to clear error history:', error); + throw new Error(`Error history clearing failed: ${(error as Error).message}`); + } + } + + /** + * 报告错误(可以扩展为发送到远程服务器) + * @param errorId 错误ID + * @returns 报告结果 + */ + public async reportError(errorId: string): Promise<{ success: boolean; reportId?: string }> { + try { + const errors = await this.getErrorHistory(); + const errorToReport = errors.find(e => e.id === errorId); + + if (!errorToReport) { + throw new Error(`Error with ID ${errorId} not found`); + } + + // 在实际应用中,这里可以实现将错误发送到远程服务器的逻辑 + // 目前仅模拟报告成功 + const reportId = `report-${uuidv4()}`; + console.log(`Error reported successfully. Report ID: ${reportId}`, errorToReport); + + return { success: true, reportId }; + } catch (error) { + console.error('Failed to report error:', error); + throw new Error(`Error reporting failed: ${(error as Error).message}`); + } + } +} + +/** + * 错误处理服务单例实例 + */ +export const errorHandlingService = ErrorHandlingService.getInstance(); \ No newline at end of file diff --git a/packages/main/src/utils/LogManager.ts b/packages/main/src/services/LogManager.ts similarity index 100% rename from packages/main/src/utils/LogManager.ts rename to packages/main/src/services/LogManager.ts diff --git a/packages/main/src/services/NotebookService.ts b/packages/main/src/services/NotebookService.ts new file mode 100644 index 00000000..2a8cecc1 --- /dev/null +++ b/packages/main/src/services/NotebookService.ts @@ -0,0 +1,152 @@ +import { app } from 'electron'; +import * as fs from 'fs'; +import * as path from 'path'; +import { v4 as uuidv4 } from 'uuid'; + +interface Note { + id: string; + title: string; + content: string; + createdAt: string; + updatedAt: string; + tags?: string[]; +} + +export class NotebookService { + private notesPath: string; + private notes: Note[]; + + constructor() { + // 初始化笔记存储路径 + const userDataPath = app.getPath('userData'); + this.notesPath = path.join(userDataPath, 'notes.json'); + this.notes = this.loadNotes(); + } + + /** + * 从文件加载笔记数据 + */ + private loadNotes(): Note[] { + try { + if (fs.existsSync(this.notesPath)) { + const data = fs.readFileSync(this.notesPath, 'utf8'); + return JSON.parse(data) as Note[]; + } + return []; + } catch (error) { + console.error('Failed to load notes:', error); + return []; + } + } + + /** + * 保存笔记数据到文件 + */ + private saveNotes(): void { + try { + // 确保目录存在 + const dir = path.dirname(this.notesPath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + fs.writeFileSync(this.notesPath, JSON.stringify(this.notes, null, 2), 'utf8'); + } catch (error) { + console.error('Failed to save notes:', error); + throw new Error('无法保存笔记数据'); + } + } + + /** + * 获取所有笔记 + */ + getNotes(): Note[] { + return [...this.notes].sort((a, b) => + new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() + ); + } + + /** + * 根据ID获取笔记 + * @param noteId 笔记ID + */ + getNoteById(noteId: string): Note | undefined { + return this.notes.find(note => note.id === noteId); + } + + /** + * 保存笔记(新建或更新) + * @param note 笔记对象 + */ + saveNote(note: Partial): Note { + const now = new Date().toISOString(); + + if (note.id) { + // 更新现有笔记 + const index = this.notes.findIndex(n => n.id === note.id); + if (index === -1) { + throw new Error('笔记不存在'); + } + + const updatedNote: Note = { + ...this.notes[index], + ...note, + updatedAt: now + }; + + this.notes[index] = updatedNote; + this.saveNotes(); + return updatedNote; + } else { + // 创建新笔记 + const newNote: Note = { + id: uuidv4(), + title: note.title || '无标题笔记', + content: note.content || '', + createdAt: now, + updatedAt: now, + tags: note.tags || [] + }; + + this.notes.unshift(newNote); // 添加到数组开头 + this.saveNotes(); + return newNote; + } + } + + /** + * 删除笔记 + * @param noteId 笔记ID + */ + deleteNote(noteId: string): void { + const initialLength = this.notes.length; + this.notes = this.notes.filter(note => note.id !== noteId); + + if (this.notes.length < initialLength) { + this.saveNotes(); + } else { + throw new Error('笔记不存在'); + } + } + + /** + * 根据标签筛选笔记 + * @param tag 标签名称 + */ + getNotesByTag(tag: string): Note[] { + return this.notes.filter(note => note.tags?.includes(tag)); + } + + /** + * 获取所有标签 + */ + getAllTags(): string[] { + const tagsSet = new Set(); + this.notes.forEach(note => { + note.tags?.forEach(tag => tagsSet.add(tag)); + }); + return Array.from(tagsSet); + } +} + +export const notebookService = new NotebookService(); \ No newline at end of file diff --git a/packages/main/src/services/NotificationService.ts b/packages/main/src/services/NotificationService.ts new file mode 100644 index 00000000..20f27400 --- /dev/null +++ b/packages/main/src/services/NotificationService.ts @@ -0,0 +1,209 @@ +import { Notification, ipcMain } from 'electron'; +import { systemSettingsService } from './SystemSettingsService'; +import * as path from 'path'; + +/** + * 通知服务,负责管理和发送系统通知 + */ +export class NotificationService { + private static instance: NotificationService; + private notificationSounds: Record = {}; + private permissionStatus: 'granted' | 'denied' | 'default' = 'default'; + + private constructor() { + this.initialize(); + } + + /** + * 获取单例实例 + */ + public static getInstance(): NotificationService { + if (!NotificationService.instance) { + NotificationService.instance = new NotificationService(); + } + return NotificationService.instance; + } + + /** + * 初始化通知服务 + */ + private async initialize(): Promise { + // 初始化通知声音路径 + this.initializeSoundPaths(); + // 注册IPC事件处理程序 + this.registerIpcHandlers(); + // 加载通知设置 + await this.loadNotificationSettings(); + } + + /** + * 初始化通知声音路径 + */ + private initializeSoundPaths(): void { + // 在实际应用中,这里应该指向应用内的声音文件 + this.notificationSounds = { + default: path.join(__dirname, '../../assets/sounds/default.wav'), + soft: path.join(__dirname, '../../assets/sounds/soft.wav'), + loud: path.join(__dirname, '../../assets/sounds/loud.wav') + }; + } + + /** + * 注册IPC事件处理程序 + */ + private registerIpcHandlers(): void { + // 获取通知权限状态 + ipcMain.handle('notification:getPermissionStatus', async () => { + return { success: true, data: this.permissionStatus }; + }); + + // 请求通知权限 + ipcMain.handle('notification:requestPermission', async () => { + try { + const permission = await Notification.requestPermission(); + this.permissionStatus = permission; + return { success: true, data: permission }; + } catch (error) { + return { success: false, error: error.message }; + } + }); + + // 发送测试通知 + ipcMain.handle('notification:sendTestNotification', async () => { + try { + await this.sendNotification({ + title: '测试通知', + body: '这是一条测试通知,用于验证通知功能是否正常工作', + type: 'systemUpdate' + }); + return { success: true }; + } catch (error) { + return { success: false, error: error.message }; + } + }); + } + + /** + * 加载通知设置 + */ + private async loadNotificationSettings(): Promise { + try { + const settings = await systemSettingsService.getSettingsByCategory('notifications'); + // 应用通知设置 + if (settings && settings.enableNotifications === false) { + // 通知已被禁用 + console.log('Notifications are disabled in settings'); + } + } catch (error) { + console.error('Failed to load notification settings:', error); + } + } + + /** + * 检查通知是否应该被发送 + */ + private async shouldSendNotification(type: string): Promise { + try { + // 检查全局通知设置 + const settings = await systemSettingsService.getSettingsByCategory('notifications'); + + // 如果全局禁用通知,直接返回false + if (!settings || !settings.enableNotifications) { + return false; + } + + // 检查是否启用了特定类型的通知 + if (settings.notificationTypes && Array.isArray(settings.notificationTypes)) { + return settings.notificationTypes.includes(type); + } + + // 默认允许所有类型通知 + return true; + } catch (error) { + console.error('Error checking notification permissions:', error); + // 出错时默认不发送通知 + return false; + } + } + + /** + * 获取通知声音文件路径 + */ + private async getNotificationSoundPath(): Promise { + try { + const settings = await systemSettingsService.getSettingsByCategory('notifications'); + + // 如果禁用了通知声音,返回undefined + if (!settings || !settings.notificationSound) { + return undefined; + } + + // 获取配置的声音类型 + const soundType = settings.soundType || 'default'; + return this.notificationSounds[soundType]; + } catch (error) { + console.error('Error getting notification sound:', error); + return undefined; + } + } + + /** + * 发送系统通知 + * @param options 通知选项 + */ + public async sendNotification(options: { + title: string; + body: string; + type: string; + icon?: string; + data?: any; + urgency?: 'low' | 'normal' | 'critical'; + }): Promise { + try { + // 检查权限 + if (this.permissionStatus !== 'granted') { + console.log('Notification permission not granted'); + return false; + } + + // 检查通知设置是否允许该类型通知 + const shouldSend = await this.shouldSendNotification(options.type); + if (!shouldSend) { + console.log(`Notification type ${options.type} is disabled`); + return false; + } + + // 获取通知声音 + const soundPath = await this.getNotificationSoundPath(); + + // 创建通知选项 + const notificationOptions: Electron.NotificationConstructorOptions = { + title: options.title, + body: options.body, + silent: !soundPath, + icon: options.icon || path.join(__dirname, '../../assets/icons/notification-icon.png'), + urgency: options.urgency || 'normal', + data: options.data + }; + + // 添加声音(仅Windows支持) + if (soundPath && process.platform === 'win32') { + notificationOptions.sound = soundPath; + } + + // 发送通知 + const notification = new Notification(notificationOptions); + notification.show(); + + return true; + } catch (error) { + console.error('Failed to send notification:', error); + return false; + } + } +} + +/** + * 通知服务单例实例 + */ +export const notificationService = NotificationService.getInstance(); \ No newline at end of file diff --git a/packages/main/src/utils/ProcessManager.ts b/packages/main/src/services/ProcessManager.ts similarity index 100% rename from packages/main/src/utils/ProcessManager.ts rename to packages/main/src/services/ProcessManager.ts diff --git a/packages/main/src/services/SystemSettingsService.ts b/packages/main/src/services/SystemSettingsService.ts new file mode 100644 index 00000000..a00b32df --- /dev/null +++ b/packages/main/src/services/SystemSettingsService.ts @@ -0,0 +1,224 @@ +import { app } from 'electron'; +import * as fs from 'fs'; +import * as path from 'path'; + +interface GeneralSettings { + autoStart: boolean; + minimizeToTray: boolean; + showNotifications: boolean; + checkUpdates: boolean; +} + +interface NetworkSettings { + proxyEnabled: boolean; + proxyServer: string; + proxyPort: number; + timeout: number; + maxRetries: number; +} + +interface ShortcutSettings { + global: Record; + inApp: Record; +} + +interface ServerSettings { + httpPort: number; + wsPort: number; + enableCors: boolean; + allowedOrigins: string[]; +} + +interface AppearanceSettings { + theme: 'light' | 'dark' | 'system'; + language: string; + fontSize: number; + accentColor: string; + compactMode: boolean; +} + +interface NotificationSettings { + newDanmu: boolean; + giftReceived: boolean; + followerJoined: boolean; + systemMessages: boolean; + soundEnabled: boolean; +} + +interface SystemSettings { + general: GeneralSettings; + network: NetworkSettings; + shortcuts: ShortcutSettings; + server: ServerSettings; + appearance: AppearanceSettings; + notifications: NotificationSettings; + lastUpdated: string; +} + +const DEFAULT_SETTINGS: SystemSettings = { + general: { + autoStart: false, + minimizeToTray: true, + showNotifications: true, + checkUpdates: true + }, + network: { + proxyEnabled: false, + proxyServer: '', + proxyPort: 8080, + timeout: 30000, + maxRetries: 3 + }, + shortcuts: { + global: { + 'show-app': 'Ctrl+Shift+A', + 'toggle-mute': 'Ctrl+M', + 'take-screenshot': 'Ctrl+Shift+S' + }, + inApp: { + 'save-note': 'Ctrl+S', + 'search': 'Ctrl+F', + 'new-note': 'Ctrl+N' + } + }, + server: { + httpPort: 8088, + wsPort: 8089, + enableCors: true, + allowedOrigins: ['http://localhost:3000'] + }, + appearance: { + theme: 'system', + language: 'zh-CN', + fontSize: 14, + accentColor: '#2d8cf0', + compactMode: false + }, + notifications: { + newDanmu: true, + giftReceived: true, + followerJoined: true, + systemMessages: true, + soundEnabled: true + }, + lastUpdated: new Date().toISOString() +}; + +export class SystemSettingsService { + private settingsPath: string; + private settings: SystemSettings; + + constructor() { + // 初始化设置存储路径 + const userDataPath = app.getPath('userData'); + this.settingsPath = path.join(userDataPath, 'settings.json'); + this.settings = this.loadSettings(); + } + + /** + * 从文件加载设置 + */ + private loadSettings(): SystemSettings { + try { + if (fs.existsSync(this.settingsPath)) { + const data = fs.readFileSync(this.settingsPath, 'utf8'); + const savedSettings = JSON.parse(data) as Partial; + // 合并保存的设置与默认设置,确保所有字段都存在 + return this.mergeSettings(DEFAULT_SETTINGS, savedSettings); + } + return { ...DEFAULT_SETTINGS }; + } catch (error) { + console.error('Failed to load settings:', error); + // 加载失败时使用默认设置 + return { ...DEFAULT_SETTINGS }; + } + } + + /** + * 合并设置对象,确保所有默认字段都存在 + */ + private mergeSettings(defaults: any, custom: any): any { + const merged = { ...defaults }; + for (const key in custom) { + if (Object.prototype.hasOwnProperty.call(custom, key)) { + if (typeof custom[key] === 'object' && custom[key] !== null && !Array.isArray(custom[key])) { + merged[key] = this.mergeSettings(defaults[key], custom[key]); + } else { + merged[key] = custom[key]; + } + } + } + return merged; + } + + /** + * 保存设置到文件 + */ + private saveSettings(): void { + try { + // 确保目录存在 + const dir = path.dirname(this.settingsPath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + + // 更新最后修改时间 + this.settings.lastUpdated = new Date().toISOString(); + + fs.writeFileSync(this.settingsPath, JSON.stringify(this.settings, null, 2), 'utf8'); + } catch (error) { + console.error('Failed to save settings:', error); + throw new Error('无法保存应用设置'); + } + } + + /** + * 获取所有设置 + */ + getAllSettings(): SystemSettings { + return { ...this.settings }; + } + + /** + * 获取特定类别的设置 + */ + getSettingsByCategory(category: T): SystemSettings[T] { + return { ...this.settings[category] }; + } + + /** + * 更新特定类别的设置 + */ + updateSettingsByCategory( + category: T, + newSettings: Partial + ): SystemSettings[T] { + this.settings[category] = { + ...this.settings[category], + ...newSettings + }; + + this.saveSettings(); + return { ...this.settings[category] }; + } + + /** + * 重置所有设置为默认值 + */ + resetToDefault(): SystemSettings { + this.settings = { ...DEFAULT_SETTINGS }; + this.saveSettings(); + return { ...this.settings }; + } + + /** + * 重置特定类别的设置为默认值 + */ + resetCategoryToDefault(category: T): SystemSettings[T] { + this.settings[category] = { ...DEFAULT_SETTINGS[category] }; + this.saveSettings(); + return { ...this.settings[category] }; + } +} + +export const systemSettingsService = new SystemSettingsService(); \ No newline at end of file diff --git a/packages/main/src/types/.gitkeep b/packages/main/src/types/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/main/src/ModuleContext.ts b/packages/main/src/types/ModuleContext.ts similarity index 96% rename from packages/main/src/ModuleContext.ts rename to packages/main/src/types/ModuleContext.ts index a3643126..d83c8d3c 100644 --- a/packages/main/src/ModuleContext.ts +++ b/packages/main/src/types/ModuleContext.ts @@ -1,3 +1,3 @@ export type ModuleContext = { readonly app: Electron.App; -} +} \ No newline at end of file diff --git a/packages/main/src/utils/AppManager.ts b/packages/main/src/utils/AppManager.ts deleted file mode 100644 index e06753c6..00000000 --- a/packages/main/src/utils/AppManager.ts +++ /dev/null @@ -1,221 +0,0 @@ -import fs from "fs"; -import path from "path"; -import { promisify } from "util"; -import { ConfigManager } from "./ConfigManager.js"; -import { HttpManager } from "./HttpManager.js"; -import { WindowManager, WindowConfig } from "../modules/WindowManager.js"; -import { getPackageJson } from "./Devars.js"; -import { app } from "electron"; -import { EventEmitter } from "events"; -import { BrowserWindow, ipcMain } from 'electron'; -import { DataManager } from './DataManager'; -import { AppModule } from '../AppModule.js'; - -const readdir = promisify(fs.readdir); -const stat = promisify(fs.stat); - -interface AppConfig { - id: string; - name: string; - version: string; - info?: string; - settings?: Record; - windows: WindowConfig; - supportedDisplays?: ("main" | "obs" | "client")[]; -} - -export class AppManager extends EventEmitter { - private apps: Map = new Map(); - private appWindows: Map = new Map(); - private httpManager: HttpManager = globalThis.httpManager; - private windowManager: WindowManager = globalThis.windowManager; - private appDir: string = ""; - private configManager: ConfigManager = globalThis.configManager; - private modules: Map = new Map(); - - constructor() { - super(); - } - - /** - * 注册模块 - * @param moduleId 模块ID - * @param module 模块实例 - */ - registerModule(moduleId: string, module: AppModule): void { - this.modules.set(moduleId, module); - this.emit('module-registered', moduleId); - console.log(`Module ${moduleId} registered successfully`); - } - - /** - * 获取模块实例 - * @param moduleId 模块ID - * @returns 模块实例或undefined - */ - getModule(moduleId: string): AppModule | undefined { - return this.modules.get(moduleId); - } - - /** - * 获取所有已注册的模块 - * @returns 模块ID和实例的映射 - */ - getAllModules(): Map { - return this.modules; - } - // 修正:直接使用 HttpManager 初始化的应用目录 - private async getAppDirectory(): Promise { - return globalThis.httpManager.getAppDir(); // 使用 HttpManager 暴露的路径 - } - - async init(): Promise { - this.appDir = await this.getAppDirectory(); // 从 HttpManager 获取已初始化的路径 - const appFolders = await this.getAppFolders(); - for (const folder of appFolders) { - const configPath = path.join(folder, "config.json"); - const configContent = await fs.promises.readFile(configPath, "utf-8"); - const config: AppConfig = JSON.parse(configContent); - this.apps.set(config.id, config); - - // 托管静态文件 - this.httpManager.serveStatic(`/application/${config.name}`, folder); - - // 加载API接口 - const apiPath = path.join(folder, "api.cjs"); - if (fs.existsSync(apiPath)) { - // Access default export for ES module compatibility - const apiModule = require(apiPath); - const apiRoutes = apiModule.default || apiModule; - this.httpManager.addApiRoutes( - `/api/application/${config.name}`, - apiRoutes - ); - } - } - } - - // 修正:重写 getAppFolders 方法,使用纯 Promise 风格 - private async getAppFolders(): Promise { - const appDir = this.appDir; // 已通过 init 初始化的有效路径 - if (!appDir) { - throw new Error("Application directory path is undefined"); - } - - const items = await readdir(appDir); // 使用 promisify 后的 readdir - const folders: string[] = []; - - for (const item of items) { - const itemPath = path.join(appDir, item); - const stats = await stat(itemPath); // 使用 promisify 后的 stat - if (stats.isDirectory()) { - folders.push(itemPath); - } - } - - return folders; - } - - getAppConfig(appId: string): AppConfig | undefined { - return this.apps.get(appId); - } - - async readAppConfig(appId: string): Promise { - const config = this.apps.get(appId); - if (!config) { - throw new Error(`App ${appId} not found`); - } - // 读取已保存的配置 - let savedConfig: any = await this.configManager.readConfig(config.name); - // 如果没有保存的配置,使用默认配置并保存 - if (!savedConfig) { - savedConfig = { ...config }; - await this.configManager.saveConfig(config.name, savedConfig); - } - return savedConfig; - } - - async saveAppConfig(appId: string, configData: AppConfig): Promise { - const app = this.apps.get(appId); - if (!app) { - throw new Error(`App ${appId} not found`); - } - await this.configManager.saveConfig(app.name, configData); - } - - getAppUrl(appName: string): string { - const port = this.httpManager.getPort(); - return `http://localhost:${port}/application/${appName}/index.html`; - } - - async startApp(appId: string): Promise { - const config = this.getAppConfig(appId); - if (!config) { - throw new Error(`App ${appId} not found`); - } - const window = await this.windowManager.createWindow(config.windows); - window.loadURL(this.getAppUrl(config.name)); - - if (!this.appWindows.has(appId)) { - this.appWindows.set(appId, []); - } - this.appWindows.get(appId)!.push(window); - - return window; - } - - async closeApp(appId: string): Promise { - const windows = this.appWindows.get(appId) || []; - windows.forEach((window) => window.close()); - this.appWindows.delete(appId); - this.emit("app-closed", appId); - } - - async restartApp(appId: string): Promise { - await this.closeApp(appId); - await this.startApp(appId); - } - - async reloadApps(): Promise { - // 关闭所有窗口 - Array.from(this.appWindows.keys()).forEach( - async (id) => await this.closeApp(id) - ); - - // 清理HTTP托管 - this.apps.forEach((app) => { - this.httpManager.removeStatic(`/application/${app.name}`); - this.httpManager.removeApiRoutes(`/api/application/${app.id}`); - }); - - // 重新初始化 - this.apps.clear(); - await this.init(); - } - private createClientWindow(appId: string): BrowserWindow { - const window = new BrowserWindow({ - width: 800, - height: 600, - webPreferences: { - nodeIntegration: false, - contextIsolation: true, - preload: path.join(__dirname, "preload.js"), - }, - }); - - // 加载渲染器页面 - const indexPath = path.join(__dirname, '../../../renderer/index.html'); - window.loadFile(indexPath).catch(err => { - console.error('Failed to load window content:', err); - }); - - // 监听窗口关闭事件 - window.on("closed", () => { - DataManager.getInstance().handleClientClosed(appId); - this.emit("app-closed", appId); - }); - - return window; - } -} - diff --git a/packages/renderer/src/App.vue b/packages/renderer/src/App.vue index ce958dc5..b9dcf4be 100644 --- a/packages/renderer/src/App.vue +++ b/packages/renderer/src/App.vue @@ -1,10 +1,12 @@