diff --git a/.github/workflows/compile-and-test.yml b/.github/workflows/compile-and-test.yml index f15623be..318bb747 100644 --- a/.github/workflows/compile-and-test.yml +++ b/.github/workflows/compile-and-test.yml @@ -68,7 +68,7 @@ jobs: - run: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test --if-present if: matrix.os == 'ubuntu-latest' - - uses: actions/attest-build-provenance@v2 + - uses: actions/attest-build-provenance@v3 with: subject-path: "dist/root*, dist/latest*.yml" 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..9ba074f4 100644 --- a/packages/main/package.json +++ b/packages/main/package.json @@ -22,7 +22,7 @@ "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..5fad6d20 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(); @@ -16,7 +48,7 @@ let liveManagementModule: any; ipcMain.on('apps-ready', () => { liveManagementModule = globalThis.appManager.getModule('LiveManagementModule'); if (!liveManagementModule) { - console.error('Failed to get LiveManagementModule instance from AppManager'); + logger.error('Failed to get LiveManagementModule instance from AppManager'); } }); @@ -27,7 +59,7 @@ ipcMain.on('apps-ready', () => { // 注册应用模块 ipcMain.handle('app:registerModule', async (_, moduleName: string, modulePath: string) => { try { - const appManager = globalThis.appManager as any; + const appManager = globalThis.appManager as AppManager; await appManager.registerModule(moduleName, modulePath); return { success: true }; } catch (error) { @@ -39,7 +71,7 @@ ipcMain.on('apps-ready', () => { // 启用应用模块 ipcMain.handle('app:enableModule', async (_, moduleName: string) => { try { - const appManager = globalThis.appManager as any; + const appManager = globalThis.appManager as AppManager; await appManager.enableModule(moduleName); return { success: true }; } catch (error) { @@ -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/eventSourceApi.ts b/packages/main/src/apis/eventSourceApi.ts index 8ecfe61d..998d9b26 100644 --- a/packages/main/src/apis/eventSourceApi.ts +++ b/packages/main/src/apis/eventSourceApi.ts @@ -2,6 +2,7 @@ import { EventEmitter } from 'events'; import { IncomingMessage } from 'http'; import { ServerResponse } from 'http'; import { Readable } from 'stream'; +import { getLogManager } from '../utils/logger'; /** * EventSource连接管理器 @@ -10,6 +11,7 @@ import { Readable } from 'stream'; class EventSourceManager { private connections: Map; private eventEmitter: EventEmitter; + private logger = getLogManager().getLogger('EventSourceManager'); constructor() { this.connections = new Map(); @@ -41,10 +43,10 @@ class EventSourceManager { req.on('close', () => { this.connections.delete(clientId); this.eventEmitter.emit('disconnect', clientId); - console.log(`Client ${clientId} disconnected`); + this.logger.info(`Client ${clientId} disconnected`); }); - console.log(`New EventSource connection from client ${clientId}`); + this.logger.info(`New EventSource connection from client ${clientId}`); } /** @@ -125,13 +127,18 @@ const eventSourceManager = new EventSourceManager(); export default eventSourceManager; -export function setupEventSourceRoutes(app: any) { +export function setupEventSourceRoutes(app: { get: (path: string, handler: (req: IncomingMessage, res: ServerResponse) => void) => void }) { /** * 弹幕流EventSource连接 */ app.get('/api/events/danmaku', (req: IncomingMessage, res: ServerResponse) => { - const clientId = req.headers['client-id'] || `client_${Date.now()}`; - eventSourceManager.createConnection(req, res, clientId as string); + const clientId = req.headers['client-id'] as string || `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + if (typeof clientId !== 'string' || clientId.trim().length === 0) { + res.writeHead(400); + res.end('Invalid client-id header'); + return; + } + eventSourceManager.createConnection(req, res, clientId); }); /** diff --git a/packages/main/src/apis/httpApi.ts b/packages/main/src/apis/httpApi.ts index f43aec86..264a5d84 100644 --- a/packages/main/src/apis/httpApi.ts +++ b/packages/main/src/apis/httpApi.ts @@ -13,11 +13,52 @@ interface ValidationError { message: string; } +// 缺少的接口定义 +interface WindowInfo { + id: number; + title: string; + isFocused: boolean; + bounds: { x: number; y: number; width: number; height: number }; + alwaysOnTop: boolean; +} + +interface MiniProgramInfo { + id: string; + name: string; + path: string; + config: Record; + lastUsed: Date; +} + +// 请求验证辅助函数 +const validateRequest = (validations: ((req: Request) => ValidationError[])[]) => { + return (req: Request, res: Response, next: NextFunction) => { + const errors: ValidationError[] = []; + validations.forEach(validation => { + errors.push(...validation(req)); + }); + if (errors.length > 0) { + return res.status(400).json>({ + success: false, + error: 'Validation failed', + data: errors + }); + } + next(); + }; +} +import { initializeElectronApi } from './electronApi.js'; +import { acfunDanmuModule } from '../modules/AcfunDanmuModule.js'; +import { getLogManager } from '../utils/logger'; +import { AppManager } from '../core/AppManager'; +import { WindowManager } from '../modules/WindowManager'; + // 统一错误处理中间件 const errorHandler = (fn: (req: Request, res: Response, next: NextFunction) => Promise) => (req: Request, res: Response, next: NextFunction) => { Promise.resolve(fn(req, res, next)).catch((error) => { - console.error('API Error:', error); + const logger = getLogManager().getLogger('httpApi'); + logger.error('API Error:', error); res.status(500).json({ success: false, error: error.message || 'An unexpected error occurred' @@ -57,12 +98,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[] => { @@ -76,12 +149,20 @@ export function initializeHttpApi() { // ====== 窗口管理相关HTTP接口 ====== // 关闭窗口 router.post('/window/close', validateRequest([validateWindowClose]), errorHandler(async (req: Request, res: Response) => { + const logger = getLogManager().getLogger('httpApi'); const { windowId } = req.body; - const result = await globalThis.windowManager.closeWindow(windowId); - res.json>({ - success: true, - data: result - }); + try { + const windowManager = globalThis.windowManager as WindowManager; + const result = await windowManager.closeWindow(windowId); + logger.info(`Closed window ${windowId}: ${result}`); + res.json>({ + success: true, + data: result + }); + } catch (error) { + logger.error(`Failed to close window ${windowId}:`, error); + throw error; + } })); // 窗口最小化验证函数 @@ -174,7 +255,293 @@ 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', errorHandler(async (req: Request, res: Response) => { + const appManager = globalThis.appManager as AppManager; + const shortcuts = await appManager.getShortcuts(); + res.json>({ success: true, data: shortcuts }); +})); + +router.post('/settings/shortcuts', errorHandler(async (req: Request, res: Response) => { + const { shortcuts } = req.body; + const appManager = globalThis.appManager as AppManager; + await appManager.setShortcuts(shortcuts); + res.json>({ success: true, data: {} }); +})); + +router.post('/settings/shortcuts/reset', errorHandler(async (req: Request, res: Response) => { + const appManager = globalThis.appManager as AppManager; + await appManager.resetShortcuts(); + res.json>({ success: true, data: {} }); +})); + +// 小程序市场相关接口 +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 +645,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..faaf9fc6 --- /dev/null +++ b/packages/main/src/apis/index.ts @@ -0,0 +1,84 @@ +import { ipcMain } from 'electron'; +import electron from 'electron'; +import DanmuModule from '../modules/DanmuModule'; +import DataReportService from '../services/DataReportService'; +import { logger } from '@app/utils/logger'; +import { ModuleContext } from '../core/ModuleContext'; + +// API注册器 - 集中管理主进程API +class ApiRegistry { + private modules: Record = {}; + + constructor() { + this.registerModules(); + this.setupIpcHandlers(); + } + + // 注册所有功能模块 + private registerModules(): void { + // 注册弹幕模块 + const danmuModule = new DanmuModule(); + const moduleContext: ModuleContext = { + mainWindow: electron.BrowserWindow.getFocusedWindow() || null, + appDataPath: electron.app.getPath('userData'), + appVersion: electron.app.getVersion(), + configStore: new Map() + }; + danmuModule.enable(moduleContext); + 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) => { + try { + return await DataReportService.getInstance().getDailyDanmuReport(date); + } catch (error) { + logger.error('Failed to get daily report:', error); + throw new Error('获取日报表失败'); + } + }); + + ipcMain.handle('report:getAudienceAnalysis', async (_, roomId?: number, days?: number) => { + try { + return await DataReportService.getInstance().getAudienceBehaviorAnalysis(roomId, days); + } catch (error) { + logger.error('Failed to get audience analysis:', error); + throw new Error('获取观众分析失败'); + } + }); + + ipcMain.handle('report:exportToCSV', async (_, reportData: any, reportType: string) => { + try { + return await DataReportService.getInstance().exportReportToCSV(reportData, reportType); + } catch (error) { + logger.error('Failed to export report to CSV:', error); + throw new Error('导出报表失败'); + } + }); + + // 后续将添加更多模块的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/config/config.ts b/packages/main/src/config/config.ts new file mode 100644 index 00000000..05a6cbed --- /dev/null +++ b/packages/main/src/config/config.ts @@ -0,0 +1,36 @@ +export default { + port: 12590, + applications: [], + // 弹幕服务配置 + debug: false, + connectionMode: 'tcp', + logLevel: 'info', + // 直播管理配置 + rtmpServer: 'rtmp://push.acfun.cn/live', + defaultCoverUrl: '', + roomId: '', + streamKey: '', + // OBS连接配置 + obsIp: 'localhost', + obsPort: 4455, + obsPassword: '', + // 安全配置 + security: { + autoScan: true, + permissionAudit: true, + runtimeMonitoring: true + }, + // 快捷键配置 + shortcuts: {}, + // 通知配置 + notification: { + enabled: true, + sound: true, + displayDuration: 5000 + }, + // 认证配置 + auth: { + autoLogin: false, + lastLoginUser: null + } +} \ No newline at end of file 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 78% rename from packages/main/src/AppModule.ts rename to packages/main/src/core/AppModule.ts index 7833a69b..0656ecff 100644 --- a/packages/main/src/AppModule.ts +++ b/packages/main/src/core/AppModule.ts @@ -2,4 +2,5 @@ import type { ModuleContext } from './ModuleContext.js'; export interface AppModule { enable(context: ModuleContext): Promise | void; -} + disable?(): 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 77% rename from packages/main/src/utils/ConfigManager.ts rename to packages/main/src/core/ConfigManager.ts index bddd19ea..7dcd5942 100644 --- a/packages/main/src/utils/ConfigManager.ts +++ b/packages/main/src/core/ConfigManager.ts @@ -16,20 +16,67 @@ import archiver from "archiver"; // 新增解压相关模块 import { createReadStream } from "fs"; import { Extract } from "unzipper"; -import config from "./config.js"; +import config from "./config"; +import { logger } from '@app/utils/logger'; // 新增:定义配置模式接口(扩展Record允许任意属性) - +interface ConfigSchema extends Record { + appName: string; + windowSize: { width: number; height: number }; + // 优化通知设置接口定义 + notificationSettings: { + enabled: boolean; + sound: boolean; + showToast: boolean; + toastDuration: number; + categories: Record; + }; + globalNotificationEnabled: boolean; + // 添加RTMP配置属性定义 + rtmpConfigs?: Record; + // 添加黑名单配置属性定义 + blacklist?: Array<{ + userId: number; + username?: string; + reason?: string; + addedAt: string; + expiresAt?: string; + }>; + // 添加直播设置 + liveSettings?: { + defaultQuality: string; + autoRecord: boolean; + showGiftEffects: boolean; + }; + // 添加录制配置 + recordingSettings?: { + savePath: string; + format: string; + quality: string; + maxDuration?: number; + }; + // 添加自动更新设置 + autoUpdate?: { + enabled: boolean; + checkInterval: number; + ignoreVersions: string[]; + }; +} class ConfigManager { private configPath: string; private DEFAULT_CONFIG_PATH: string; constructor(customPath?: string) { - this.DEFAULT_CONFIG_PATH = join(homedir(), globalThis.appName); + const appName = globalThis.appName || 'acfun-live-toolbox'; + this.DEFAULT_CONFIG_PATH = join(homedir(), appName); // 初始化electron-data配置 electron_data.config({ - filename: globalThis.appName, + filename: appName, path: customPath || this.DEFAULT_CONFIG_PATH, autosave: true, prettysave: true @@ -40,7 +87,7 @@ class ConfigManager { this.configPath = customPath || this.DEFAULT_CONFIG_PATH; this.ensureConfigDirectoryExists(); // 异步初始化配置 - this.initializeConfig().catch(err => console.error('配置初始化失败:', err)); + this.initializeConfig().catch(err => logger.error('配置初始化失败:', err)); } // 新增异步初始化方法 @@ -158,7 +205,8 @@ class ConfigManager { if (!existsSync(originalPath)) { return null; } - const backupFileName = `${globalThis.appName}_backup_${Date.now()}.zip`; + const appName = globalThis.appName || 'acfun-live-toolbox'; + const backupFileName = `${appName}_backup_${Date.now()}.zip`; const backupPath = join(this.configPath, backupFileName); const output = createWriteStream(backupPath); @@ -194,7 +242,7 @@ class ConfigManager { // 备份当前配置 const backupPath = await this.backupConfig(); if (!backupPath) { - console.warn("备份当前配置失败,可能无配置文件需要备份"); + logger.warn("备份当前配置失败,可能无配置文件需要备份"); } // 确保配置目录存在 @@ -215,4 +263,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 97% rename from packages/main/src/utils/HttpManager.ts rename to packages/main/src/core/HttpManager.ts index 6057a74d..804dd09c 100644 --- a/packages/main/src/utils/HttpManager.ts +++ b/packages/main/src/core/HttpManager.ts @@ -5,6 +5,7 @@ import url from "url"; import { app } from "electron"; import { getPackageJson } from "./Devars.js"; import express from "express"; +import { logger } from '@app/utils/logger'; // 新增:事件源连接接口 export interface EventSourceConnection { @@ -41,7 +42,7 @@ export class HttpManager { // 修复找不到 ExpressRouter 名称的问题,推测应该使用 express.Router 类型 addApiRoutes(prefix: string, router: express.Router): void { - console.log(`Mounting API routes at prefix: ${prefix}`); + logger.info(`Mounting API routes at prefix: ${prefix}`); this.apiRoutes.set(prefix, router); this.expressApp.use(prefix, router); } @@ -116,7 +117,7 @@ export class HttpManager { await fs.access(this.APP_DIR); } catch { await fs.mkdir(this.APP_DIR, { recursive: true }); - console.log(`创建应用目录: ${this.APP_DIR}`); + logger.info(`创建应用目录: ${this.APP_DIR}`); } } @@ -161,7 +162,7 @@ export class HttpManager { } this.serverRunning = false; - console.log("服务器已关闭,准备重新启动..."); + logger.info("服务器已关闭,准备重新启动..."); const result = await this.initializeServer(); resolve(result); }); @@ -249,7 +250,7 @@ export class HttpManager { `); } catch (error) { - console.error('Failed to send SSE event:', error); + logger.error('Failed to send SSE event:', error); } }); @@ -286,4 +287,4 @@ export class HttpManager { public getAppDir(): string { return this.APP_DIR; } -} +} \ No newline at end of file diff --git a/packages/main/src/core/ModuleContext.ts b/packages/main/src/core/ModuleContext.ts new file mode 100644 index 00000000..c8a614f3 --- /dev/null +++ b/packages/main/src/core/ModuleContext.ts @@ -0,0 +1,16 @@ +import { BrowserWindow } from 'electron'; + +/** + * 模块上下文接口 + * 提供模块所需的应用全局资源 + */ +export interface ModuleContext { + /** 主窗口实例 */ + mainWindow: BrowserWindow; + /** 应用数据目录路径 */ + appDataPath: string; + /** 应用版本号 */ + appVersion: string; + /** 模块配置存储 */ + configStore: Map; +} \ 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/core/config.ts b/packages/main/src/core/config.ts new file mode 100644 index 00000000..c0247cbc --- /dev/null +++ b/packages/main/src/core/config.ts @@ -0,0 +1,92 @@ +/** + * 默认应用配置 + * 此文件由系统自动生成,用于提供基础配置模板 + */ +const defaultConfig = { + // 应用基本信息 + appName: 'AcfunLiveToolbox', + appVersion: '2.0.0', + copyright: 'Copyright © 2023 AcfunLiveToolbox', + + // 窗口配置 + windowSize: { + width: 1024, + height: 768, + minWidth: 800, + minHeight: 600 + }, + windowPosition: 'center', + resizable: true, + maximizable: true, + + // 通知配置 + notificationSettings: { + enabled: true, + sound: true, + showToast: true, + toastDuration: 5000, + categories: { + liveStatus: true, + danmuHighlight: true, + giftAlert: true, + systemUpdate: true, + reminder: true + } + }, + globalNotificationEnabled: true, + + // 网络配置 + network: { + requestTimeout: 10000, + retryCount: 3, + proxyEnabled: false, + proxyServer: '' + }, + + // 数据存储配置 + storage: { + maxDanmuHistory: 10000, + autoCleanup: true, + cleanupInterval: '7d' + }, + + // 外观配置 + appearance: { + theme: 'light', + fontSize: 14, + compactMode: false, + language: 'zh-CN' + }, + + // 新增:RTMP配置 + rtmpConfigs: {}, + // 新增:黑名单配置 + blacklist: [], + // 新增:直播设置 + liveSettings: { + defaultQuality: '720p', + autoRecord: false, + showGiftEffects: true + }, + // 新增:录制配置 + recordingSettings: { + savePath: '', + format: 'mp4', + quality: 'medium' + }, + // 新增:自动更新设置 + autoUpdate: { + enabled: true, + checkInterval: 86400000, + ignoreVersions: [] + }, + + // 开发者选项 + developer: { + debugMode: false, + logLevel: 'info', + enablePerformanceMonitor: false + } +}; + +export default defaultConfig; \ No newline at end of file diff --git a/packages/main/src/main.ts b/packages/main/src/main.ts new file mode 100644 index 00000000..745c9dad --- /dev/null +++ b/packages/main/src/main.ts @@ -0,0 +1,124 @@ +import { app, BrowserWindow, ipcMain } from 'electron'; +import path from 'path'; +import { logger } from './utils/logger'; +import { ApiRegistry } from './apis'; +import { ConfigManager } from './core/ConfigManager'; +import notificationModule from './modules/NotificationModule'; +import errorHandlingModule from './modules/ErrorHandlingModule'; +import analyticsModule from './modules/AnalyticsModule'; +import { ModuleContext } from './core/ModuleContext'; + +// 声明全局应用名称 +declare global { + var appName: string; +} + +globalThis.appName = 'AcfunLiveToolbox'; + +// 初始化核心服务 +const configManager = new ConfigManager(); + +// 确保单实例运行 +const gotTheLock = app.requestSingleInstanceLock(); +if (!gotTheLock) { + app.quit(); +} + +let mainWindow: BrowserWindow | null = null; + +// 创建主窗口 +function createWindow() { + // 从配置获取窗口尺寸 + configManager.get('windowSize').then(windowSize => { + mainWindow = new BrowserWindow({ + width: windowSize?.width || 1024, + height: windowSize?.height || 768, + title: globalThis.appName, + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + preload: path.join(__dirname, 'preload.js'), + webSecurity: true + }, + show: false, + frame: true, + titleBarStyle: 'default' + }); + + // 创建模块上下文 + const moduleContext: ModuleContext = { + mainWindow, + appDataPath: app.getPath('userData'), + appVersion: app.getVersion(), + configStore: new Map() + }; + + // 初始化所有模块 + notificationModule.enable(moduleContext); + errorHandlingModule.enable(moduleContext); + analyticsModule.enable(moduleContext); + + // 加载应用页面 + if (process.env.NODE_ENV === 'development') { + mainWindow.loadURL('http://localhost:3000'); + mainWindow.webContents.openDevTools(); + } else { + mainWindow.loadFile(path.join(__dirname, '../renderer/index.html')); + } + + // 窗口准备就绪后显示 + mainWindow.on('ready-to-show', () => { + mainWindow?.show(); + }); + + // 窗口关闭事件 + mainWindow.on('closed', () => { + // 保存窗口尺寸 + if (mainWindow) { + const { width, height } = mainWindow.getBounds(); + configManager.set('windowSize', { width, height }); + } + mainWindow = null; + }); + + // 注册API接口 + ApiRegistry.register(mainWindow); + }); +} + +// 应用就绪事件 +app.on('ready', () => { + logger.info('应用启动'); + createWindow(); +}); + +// 所有窗口关闭事件 +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit(); + } +}); + +// 应用激活事件 +app.on('activate', () => { + if (mainWindow === null) { + createWindow(); + } +}); + +// 第二实例启动事件 +app.on('second-instance', () => { + if (mainWindow) { + if (mainWindow.isMinimized()) mainWindow.restore(); + mainWindow.focus(); + } +}); + +// 错误捕获 +process.on('uncaughtException', (error) => { + logger.error('未捕获的异常:', error); +}); + +process.on('unhandledRejection', (reason, promise) => { + logger.error('未处理的Promise拒绝:', reason); +}); \ No newline at end of file diff --git a/packages/main/src/modules/AcfunDanmuModule.ts b/packages/main/src/modules/AcfunDanmuModule.ts index ab12eb35..239dbc86 100644 --- a/packages/main/src/modules/AcfunDanmuModule.ts +++ b/packages/main/src/modules/AcfunDanmuModule.ts @@ -5,6 +5,8 @@ import path from 'path'; import { app } from 'electron'; import { getPackageJson } from '../utils/Devars.js'; import { getLogManager } from '../utils/LogManager.js'; +import WebSocket from 'ws'; +import { ConfigManager } from '../core/ConfigManager.js'; // 定义配置接口 interface AcfunDanmuConfig { @@ -22,15 +24,56 @@ const DEFAULT_CONFIG: AcfunDanmuConfig = { logLevel: 'info' }; +// 定义RTMP配置接口 +interface RtmpConfig { + rtmpUrl: string; + streamKey: string; + updatedAt: string; +} + +// 弹幕发送响应接口 +interface DanmuSendSuccessResponse { + success: true; + data: { + danmuId: string; + timestamp: number; + }; +} + +interface DanmuSendErrorResponse { + success: false; + message: string; + data?: { + danmuId?: string; + timestamp?: number; + }; +} + +type DanmuSendResponse = DanmuSendSuccessResponse | DanmuSendErrorResponse; + +// 直播状态响应接口 +interface LiveStatusResponse { + liveId: number; + status: 'live' | 'offline' | 'reconnecting'; + viewerCount?: number; + startTime?: string; + title?: string; + coverUrl?: string; +} + 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; + private configManager: ConfigManager; constructor(config: Partial = {}) { this.config = { ...DEFAULT_CONFIG, ...config }; this.logManager = getLogManager(); + this.configManager = new ConfigManager(); // 初始化ConfigManager } // 设置日志回调函数 @@ -176,25 +219,61 @@ export class AcfunDanmuModule implements AppModule { } // 实现AppModule接口 - async enable({ app }: ModuleContext): Promise { + async enable(context: ModuleContext): Promise { // 设置日志回调 - this.setLogCallback((message, type) => { - this.logManager.addLog('acfunDanmu', message, type as any); + this.setLogCallback((message: string, type: string) => { + this.logManager.addLog('acfunDanmu', message, type); }); - + // 延迟启动,确保应用已就绪 - app.on('ready', () => { - setTimeout(() => { - this.start(); - }, 1000); - }); - + if (context.app.isReady()) { + setTimeout(() => this.start(), 1000); + } else { + context.app.on('ready', () => { + setTimeout(() => this.start(), 1000); + }); + } + // 主进程退出时停止服务 - app.on('will-quit', () => { + context.app.on('will-quit', () => { this.stop(); }); } -} + + // 新增:实现disable方法 + async disable(): Promise { + this.stop(); + // 清理WebSocket连接 + if (this.obsWebSocket) { + this.obsWebSocket.close(); + this.obsWebSocket = null; + } + } + + // 弹幕相关方法 + async sendDanmu(roomId: number, userId: number, nickname: string, content: string): Promise { + return this.callAcfunDanmuApi( + `/live/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 { @@ -294,6 +373,112 @@ export class AcfunDanmuModule implements AppModule { ); } + // RTMP配置管理 + async saveRtmpConfig(roomId: number, rtmpUrl: string, streamKey: string): Promise { + try { + const config = this.configManager.readConfig(); + const rtmpConfigs: Record = config.rtmpConfigs || {}; + rtmpConfigs[roomId] = { rtmpUrl, streamKey, updatedAt: new Date().toISOString() }; + this.configManager.writeConfig({ ...config, rtmpConfigs }); + return true; + } catch (error) { + this.logManager.addLog('AcfunDanmuModule', `Failed to save RTMP config: ${error.message}`, 'error'); + return false; + } + } + + async getRtmpConfig(roomId: number): Promise { + try { + const rtmpConfigs: Record = 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( @@ -311,7 +496,7 @@ export class AcfunDanmuModule implements AppModule { } // 观看列表相关方法 - async getWatchingList(liveID: number): Promise { + async getWatchingList(liveId: number): Promise { return this.callAcfunDanmuApi( `/live/watchingList`, 'GET', @@ -344,11 +529,11 @@ export class AcfunDanmuModule implements AppModule { } // 回放信息相关方法 - async getPlayback(liveID: number): Promise { + async getPlayback(liveId: number): Promise { return this.callAcfunDanmuApi( `/live/playback`, 'GET', - { liveId: liveID } + { liveId } ); } @@ -360,11 +545,11 @@ export class AcfunDanmuModule implements AppModule { ); } - async getGiftList(liveID: number): Promise { + async getGiftList(liveId: number): Promise { return this.callAcfunDanmuApi( - `/gift/list`, + `/live/giftList`, 'GET', - { liveId: liveID } + { liveId } ); } @@ -402,11 +587,11 @@ export class AcfunDanmuModule implements AppModule { } // 用户信息相关方法 - async getUserInfo(userID?: number): Promise { + async getUserInfo(userId?: number): Promise { return this.callAcfunDanmuApi( `/user/info`, 'GET', - userID ? { userId: userID } : {} + userId ? { userId } : {} ); } @@ -419,6 +604,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( @@ -428,17 +629,17 @@ export class AcfunDanmuModule implements AppModule { } // 弹幕相关方法 - async sendDanmu(liveId: number, content: string): Promise { - if (!liveId || liveId <=0) throw new Error('Invalid liveId'); - if (!content || content.trim().length === 0 || content.length > 200) throw new Error('Invalid danmu content'); + async sendDanmuV2(liveId: number, content: string): Promise { + if (!liveId || liveId <= 0) throw new Error('Invalid liveId'); + if (!content || content.trim().length === 0 || content.length > 200) throw new Error('Invalid danmu content: must be 1-200 characters'); try { return await this.callAcfunDanmuApi( - `/danmu/send`, + `/live/danmu/send`, 'POST', { 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 +649,7 @@ export class AcfunDanmuModule implements AppModule { return this.callAcfunDanmuApi( `/live/status`, 'GET', - { liveId: liveID } + { liveId } ); } @@ -485,27 +686,27 @@ export class AcfunDanmuModule implements AppModule { } // 直播剪辑相关方法 - async checkCanCut(liveID: number): Promise { + async checkCanCut(liveId: number): Promise { return this.callAcfunDanmuApi( `/live/checkCanCut`, 'GET', - { liveId: liveID } + { liveId } ); } - async setCanCut(liveID: number, canCut: boolean): Promise { + async setCanCut(liveId: number, canCut: boolean): Promise { return this.callAcfunDanmuApi( `/live/setCanCut`, 'POST', - { liveId: liveID, canCut } + { liveId, canCut } ); } // HTTP客户端工具方法 - private async callAcfunDanmuApi(path: string, method: 'GET' | 'POST' = 'GET', data: any = null): Promise { + private async callAcfunDanmuApi(path: string, method: 'GET' | 'POST' = 'GET', data: any = null): Promise { try { let url = `http://localhost:${this.config.port}/api${path}`; - const options = { + const options: RequestInit = { method, headers: { 'Content-Type': 'application/json', @@ -524,7 +725,7 @@ export class AcfunDanmuModule implements AppModule { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } - return await response.json(); + return await response.json() as T; } catch (error) { console.error(`[AcfunDanmu] API调用失败: ${error instanceof Error ? error.message : String(error)}`); throw error; diff --git a/packages/main/src/modules/AnalyticsModule.ts b/packages/main/src/modules/AnalyticsModule.ts new file mode 100644 index 00000000..2f7ff12c --- /dev/null +++ b/packages/main/src/modules/AnalyticsModule.ts @@ -0,0 +1,139 @@ +import { EventEmitter } from 'events'; +import { AppModule } from '../core/AppModule'; +import { ModuleContext } from '../core/ModuleContext'; + +/** + * 数据分析模块 + * 负责实时统计、观众行为分析、礼物统计和数据报表生成 + */ +export class AnalyticsModule extends EventEmitter implements AppModule { + private isEnabled = false; + + constructor() { + super(); + } + + /** + * 启用数据分析模块 + */ + enable(context: ModuleContext): void { + if (this.isEnabled) return; + this.isEnabled = true; + this.initialize(context); + } + + /** + * 禁用数据分析模块 + */ + disable(): void { + if (!this.isEnabled) return; + this.isEnabled = false; + this.removeAllListeners(); + this.emit('disabled'); + } + + /** + * 初始化数据分析模块 + */ + private async initialize(context: ModuleContext): Promise { + // 使用上下文信息进行初始化 + console.log(`Analytics initialized with app version: ${context.appVersion}`); + 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/ApplicationTerminatorOnLastWindowClose.ts b/packages/main/src/modules/ApplicationTerminatorOnLastWindowClose.ts index e66fccd3..db37840a 100644 --- a/packages/main/src/modules/ApplicationTerminatorOnLastWindowClose.ts +++ b/packages/main/src/modules/ApplicationTerminatorOnLastWindowClose.ts @@ -1,13 +1,24 @@ -import {AppModule} from '../AppModule.js'; -import {ModuleContext} from '../ModuleContext.js'; +import { AppModule } from '../core/AppModule'; +import { ModuleContext } from '../core/ModuleContext'; +import { app } from 'electron'; class ApplicationTerminatorOnLastWindowClose implements AppModule { - enable({app}: ModuleContext): Promise | void { - app.on('window-all-closed', () => app.quit()); + private windowAllClosedListener?: () => void; + + enable(context: ModuleContext): Promise | void { + this.windowAllClosedListener = () => app.quit(); + app.on('window-all-closed', this.windowAllClosedListener); + } + + disable(): Promise | void { + if (this.windowAllClosedListener) { + app.off('window-all-closed', this.windowAllClosedListener); + this.windowAllClosedListener = undefined; + } } } -export function terminateAppOnLastWindowClose(...args: ConstructorParameters) { - return new ApplicationTerminatorOnLastWindowClose(...args); +export function terminateAppOnLastWindowClose() { + return new ApplicationTerminatorOnLastWindowClose(); } diff --git a/packages/main/src/modules/AutoUpdater.ts b/packages/main/src/modules/AutoUpdater.ts index d8b8f8ec..9e98bc10 100644 --- a/packages/main/src/modules/AutoUpdater.ts +++ b/packages/main/src/modules/AutoUpdater.ts @@ -1,35 +1,40 @@ -import {AppModule} from '../AppModule.js'; -import electronUpdater, {type AppUpdater, type Logger} from 'electron-updater'; +import { AppModule } from '../core/AppModule'; +import { autoUpdater, type AppUpdater, type Logger } from 'electron-updater'; +import { ModuleContext } from '../core/ModuleContext'; type DownloadNotification = Parameters[0]; export class AutoUpdater implements AppModule { readonly #logger: Logger | null; - readonly #notification: DownloadNotification; + readonly #notification: DownloadNotification | undefined; + private context: ModuleContext | undefined; constructor( { logger = null, - downloadNotification = undefined, - }: - { - logger?: Logger | null | undefined, - downloadNotification?: DownloadNotification - } = {}, + downloadNotification, + }: { + logger?: Logger | null; + downloadNotification?: DownloadNotification; + } = {}, ) { this.#logger = logger; this.#notification = downloadNotification; } - async enable(): Promise { + async enable(context: ModuleContext): Promise { + this.context = context; await this.runAutoUpdater(); + return true; + } + + async disable(): Promise { + // 实现禁用逻辑 + return true; } getAutoUpdater(): AppUpdater { - // Using destructuring to access autoUpdater due to the CommonJS module of 'electron-updater'. - // It is a workaround for ESM compatibility issues, see https://github.com/electron-userland/electron-builder/issues/7976. - const {autoUpdater} = electronUpdater; return autoUpdater; } @@ -39,24 +44,21 @@ export class AutoUpdater implements AppModule { updater.logger = this.#logger || null; updater.fullChangelog = true; - if (import.meta.env.VITE_DISTRIBUTION_CHANNEL) { - updater.channel = import.meta.env.VITE_DISTRIBUTION_CHANNEL; + if (process.env.VITE_DISTRIBUTION_CHANNEL) { + updater.channel = process.env.VITE_DISTRIBUTION_CHANNEL; } return await updater.checkForUpdatesAndNotify(this.#notification); } catch (error) { - if (error instanceof Error) { - if (error.message.includes('No published versions')) { - return null; - } + if (error instanceof Error && error.message.includes('No published versions')) { + return null; } - throw error; } } } -export function autoUpdater(...args: ConstructorParameters) { +export function createAutoUpdater(...args: ConstructorParameters) { return new AutoUpdater(...args); } diff --git a/packages/main/src/modules/BlacklistManager.ts b/packages/main/src/modules/BlacklistManager.ts new file mode 100644 index 00000000..90574523 --- /dev/null +++ b/packages/main/src/modules/BlacklistManager.ts @@ -0,0 +1,208 @@ +import { EventEmitter } from 'events'; +import { AppModule } from '../core/AppModule'; +import { ModuleContext } from '../core/ModuleContext'; +import { ConfigManager } from '../core/ConfigManager'; +import { LogManager } from '../utils/LogManager'; + +// 黑名单用户接口 +export interface BlacklistUser { + userId: string; + username: string; + reason?: string; + addedAt: number; + expiresAt?: number; // 可选的过期时间,永久黑名单不设置此值 +} + +/** + * 黑名单管理模块 + * 负责处理用户黑名单的添加、移除、查询和持久化 + */ +export class BlacklistManager extends EventEmitter implements AppModule { + private blacklist: Map; + private configManager!: ConfigManager; + private logManager!: LogManager; + private static readonly CONFIG_KEY = 'blacklist'; + private isEnabled = false; + + constructor() { + super(); + this.blacklist = new Map(); + } + + /** + * 启用黑名单模块 + */ + enable(context: ModuleContext): void { + if (this.isEnabled) return; + this.isEnabled = true; + this.configManager = new ConfigManager(); + this.logManager = new LogManager(context.appDataPath); + this.initialize(); + } + + /** + * 禁用黑名单模块 + */ + disable(): void { + if (!this.isEnabled) return; + this.isEnabled = false; + this.removeAllListeners(); + this.blacklist.clear(); + } + + /** + * 初始化黑名单模块 + * 从配置加载已保存的黑名单数据 + */ + 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/ChromeDevToolsExtension.ts b/packages/main/src/modules/ChromeDevToolsExtension.ts index ef14fd6a..26d05916 100644 --- a/packages/main/src/modules/ChromeDevToolsExtension.ts +++ b/packages/main/src/modules/ChromeDevToolsExtension.ts @@ -1,5 +1,6 @@ -import {AppModule} from '../AppModule.js'; -import {ModuleContext} from '../ModuleContext.js'; +import { AppModule } from '../core/AppModule'; +import { ModuleContext } from '../core/ModuleContext'; +import { app } from 'electron'; import installer from 'electron-devtools-installer'; const { @@ -38,9 +39,15 @@ export class ChromeDevToolsExtension implements AppModule { this.#extension = extension; } - async enable({app}: ModuleContext): Promise { + async enable(context: ModuleContext): Promise { await app.whenReady(); await installExtension(extensionsDictionary[this.#extension]); + return true; + } + + async disable(): Promise { + // Chrome扩展无法通过编程方式禁用,返回true表示操作成功 + return true; } } diff --git a/packages/main/src/modules/DanmuModule.ts b/packages/main/src/modules/DanmuModule.ts new file mode 100644 index 00000000..2e5dc957 --- /dev/null +++ b/packages/main/src/modules/DanmuModule.ts @@ -0,0 +1,175 @@ +import { AppModule } from '../core/AppModule'; +import { DanmuDatabaseService } from '../services/DanmuDatabaseService'; +import { AcfunDanmuModule } from '../../acfundanmu'; +import { logger } from '../utils/logger'; +import { ModuleContext } from '../core/ModuleContext'; + +// 定义弹幕数据接口 +interface Danmu { + userId: number; + username: string; + content: string; + type: string; + color?: string; + fontSize?: number; + isGift?: boolean; + giftValue?: number; + timestamp?: number; +} + +// 弹幕连接管理器 - 处理多直播间弹幕流分发 +export class DanmuConnectionManager implements AppModule { + private static instance: DanmuConnectionManager; + private roomConnections: Map = new Map(); + private dbService: DanmuDatabaseService; + private isEnabled = false; + + private constructor() { + this.dbService = DanmuDatabaseService.getInstance(); + } + + public static getInstance(): DanmuConnectionManager { + if (!DanmuConnectionManager.instance) { + DanmuConnectionManager.instance = new DanmuConnectionManager(); + } + return DanmuConnectionManager.instance; + } + + /** + * 启用弹幕模块 + */ + async enable(context: ModuleContext): Promise { + if (this.isEnabled) return; + this.isEnabled = true; + this.initialize(context); + } + + /** + * 禁用弹幕模块 + */ + async disable(): Promise { + if (!this.isEnabled) return; + this.isEnabled = false; + this.disconnectAll(); + } + + /** + * 初始化弹幕模块 + */ + private initialize(context: ModuleContext): void { + logger.info(`Danmu module initialized with app data path: ${context.appDataPath}`); + // 使用上下文信息进行初始化 + } + + // 连接到直播间弹幕流 + 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.start(); + + // 监听弹幕事件并存储到数据库 + 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}`); + 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 AppModule { + private connectionManager: DanmuConnectionManager; + + async enable(context: ModuleContext): Promise { + this.connectionManager = DanmuConnectionManager.getInstance(); + logger.info('DanmuModule enabled with multi-room support'); + + // 注册应用退出时的清理函数 + context.app.on('will-quit', () => { + this.connectionManager.disconnectAll(); + }); + } + + async disable(): Promise { + 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..be89d599 100644 --- a/packages/main/src/modules/DashboardModule.ts +++ b/packages/main/src/modules/DashboardModule.ts @@ -1,7 +1,9 @@ import { singleton } from 'tsyringe'; - -import { randomInt } from 'crypto'; -import { acfunLiveAPI } from 'acfundanmu'; +import { authService } from '../utils/AuthService'; +import fetch from 'node-fetch'; +import { AppModule } from '../core/AppModule'; +import { ModuleContext } from '../core/ModuleContext'; +import logger from '../utils/logger'; interface Stats { viewerCount: number; @@ -23,9 +25,19 @@ const CACHE_DURATION_MS = 5 * 60 * 1000; // 5分钟缓存有效期 * 仪表盘模块 - 处理仪表盘相关数据和功能 */ @singleton() -export default class DashboardModule { +export default class DashboardModule implements AppModule { private lastStatsUpdate: number = 0; private statsCache: Stats | null = null; + private lastBlocksUpdate: number = 0; + private blocksCache: DynamicBlock[] | null = null; + + enable(context: ModuleContext): Promise | void { + // 初始化逻辑(如果需要) + } + + disable(): Promise | void { + // 清理逻辑(如果需要) + } /** * 根据当前时间生成系统通知 @@ -53,15 +65,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 }; @@ -71,7 +98,7 @@ export default class DashboardModule { return statsData; } catch (error) { - console.error('获取直播统计数据失败:', error); + logger.error('获取直播统计数据失败:', error); // 缓存失败时使用缓存数据或返回默认值 if (this.statsCache) { @@ -83,7 +110,8 @@ export default class DashboardModule { viewerCount: 0, likeCount: 0, bananaCount: 0, - acCoinCount: 0 + acCoinCount: 0, + totalIncome: 0 }; } } @@ -92,9 +120,6 @@ export default class DashboardModule { * 获取动态内容块 * @returns 动态内容块数组 */ - private lastBlocksUpdate: number = 0; - private blocksCache: DynamicBlock[] | null = null; - async getDynamicBlocks(): Promise { // 检查缓存是否有效(10分钟内) const now = Date.now(); @@ -103,29 +128,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分' - ]; - - // 随机决定是否显示活动通知 - const showActivity = Math.random() > 0.5; - let activityNotice: DynamicBlock[] = []; + // 处理API返回的最近直播数据 + const recentStreams = data.recentStreams.map((stream: any) => + `直播标题: ${stream.title} | 观众数: ${stream.viewerCount.toLocaleString()} | 时长: ${stream.duration}` + ); - if (showActivity) { - activityNotice = [{ + // 处理平台活动通知 + const activityNotice = data.platformActivity ? [{ title: '平台活动', type: 'string', - content: '本周末有直播挑战赛活动,参与即可获得丰厚奖励!详情请查看活动中心。' - }]; - } + content: data.platformActivity + }] : []; const blocks = [ { @@ -147,7 +182,7 @@ export default class DashboardModule { return blocks; } catch (error) { - console.error('获取动态内容块失败:', error); + logger.error('获取动态内容块失败:', error); // 返回缓存数据或默认内容 if (this.blocksCache) { return this.blocksCache; @@ -165,12 +200,7 @@ export default class DashboardModule { * 刷新统计数据 * @returns 新的统计数据 */ - refreshStats(): { - viewerCount: number; - likeCount: number; - bananaCount: number; - acCoinCount: number; - } { + async refreshStats(): Promise { // 强制刷新数据 this.statsCache = null; return this.getStats(); diff --git a/packages/main/src/modules/ErrorHandlingModule.ts b/packages/main/src/modules/ErrorHandlingModule.ts new file mode 100644 index 00000000..8ada5d09 --- /dev/null +++ b/packages/main/src/modules/ErrorHandlingModule.ts @@ -0,0 +1,156 @@ +import { EventEmitter } from 'events'; +import { BrowserWindow, ipcMain } from 'electron'; +import { join } from 'path'; +import { AppModule } from '../core/AppModule'; +import { ModuleContext } from '../core/ModuleContext'; + +/** + * 错误类型定义 + */ +type ErrorType = 'network' | 'authentication' | 'validation' | 'runtime' | 'unknown'; + +/** + * 错误信息接口 + */ +interface ErrorInfo { + type: ErrorType; + code: string; + message: string; + details?: any; + timestamp: Date; +} + +/** + * 错误处理模块 + * 负责全局错误捕获、错误展示和用户反馈 + */ +export class ErrorHandlingModule extends EventEmitter implements AppModule { + private mainWindow: BrowserWindow | null = null; + private errorHistory: ErrorInfo[] = []; + private maxHistorySize = 50; + private isEnabled = false; + + constructor() { + super(); + } + + /** + * 启用错误处理模块 + */ + async enable(context: ModuleContext): Promise { + if (this.isEnabled) return; + this.isEnabled = true; + this.mainWindow = context.mainWindow; + if (!this.mainWindow) { + throw new Error('主窗口未初始化,无法启用错误处理模块'); + } + this.setupGlobalErrorHandlers(); + this.setupIpcListeners(); + } + + /** + * 禁用错误处理模块 + */ + async disable(): Promise { + if (!this.isEnabled) return; + this.isEnabled = false; + this.removeAllListeners(); + ipcMain.removeHandler('get-error-history'); + ipcMain.removeHandler('clear-error-history'); + // 移除全局错误监听器 + process.removeListener('unhandledRejection', this.handleUnhandledRejection.bind(this)); + process.removeListener('uncaughtException', this.handleUncaughtException.bind(this)); + this.errorHistory = []; + } + + /** + * 设置IPC监听器 + */ + private setupIpcListeners(): void { + ipcMain.handle('get-error-history', () => this.getErrorHistory()); + ipcMain.handle('clear-error-history', () => this.clearErrorHistory()); + } + + /** + * 设置全局错误处理器 + */ + private setupGlobalErrorHandlers(): void { + // 捕获未处理的Promise拒绝 + process.on('unhandledRejection', this.handleUnhandledRejection.bind(this)); + + // 捕获未捕获的异常 + process.on('uncaughtException', this.handleUncaughtException.bind(this)); + } + + /** + * 处理未处理的Promise拒绝 + */ + private handleUnhandledRejection(reason: unknown): void { + this.handleError({ + type: 'runtime', + code: 'UNHANDLED_REJECTION', + message: reason instanceof Error ? reason.message : String(reason), + details: { reason } + }); + } + + /** + * 处理未捕获的异常 + */ + private handleUncaughtException(error: Error): void { + 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.sendErrorToRenderer(errorInfo); + + return errorInfo; + } + + /** + * 发送错误信息到渲染进程 + */ + private sendErrorToRenderer(errorInfo: ErrorInfo): void { + if (this.mainWindow?.isDestroyed()) return; + this.mainWindow?.webContents.send('error-occurred', errorInfo); + } + + /** + * 获取错误历史 + */ + 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/HardwareAccelerationModule.ts b/packages/main/src/modules/HardwareAccelerationModule.ts index dc488dab..64e24de8 100644 --- a/packages/main/src/modules/HardwareAccelerationModule.ts +++ b/packages/main/src/modules/HardwareAccelerationModule.ts @@ -1,5 +1,5 @@ -import {AppModule} from '../AppModule.js'; -import {ModuleContext} from '../ModuleContext.js'; +import { AppModule } from '../core/AppModule'; +import { ModuleContext } from '../core/ModuleContext'; export class HardwareAccelerationModule implements AppModule { readonly #shouldBeDisabled: boolean; @@ -9,10 +9,16 @@ export class HardwareAccelerationModule implements AppModule { this.#shouldBeDisabled = !enable; } - enable({app}: ModuleContext): Promise | void { + async enable({app}: ModuleContext): Promise { if (this.#shouldBeDisabled) { app.disableHardwareAcceleration(); } + return true; + } + + async disable(): Promise { + // 硬件加速设置无法在运行时动态启用,返回true表示操作成功 + return true; } } diff --git a/packages/main/src/modules/LiveManagementModule.ts b/packages/main/src/modules/LiveManagementModule.ts index 50e3f10d..b2d0fcf3 100644 --- a/packages/main/src/modules/LiveManagementModule.ts +++ b/packages/main/src/modules/LiveManagementModule.ts @@ -1,8 +1,9 @@ import { EventEmitter } from 'events'; import { getLogManager } from '../utils/LogManager.js'; -import { AppModule } from '../AppModule.js'; -import { ModuleContext } from '../ModuleContext.js'; +import { AppModule } from '../core/AppModule'; +import { ModuleContext } from '../core/ModuleContext'; import OBSWebSocket from 'obs-websocket-js'; +import fetch from 'node-fetch'; // 直播管理模块配置接口 export interface LiveManagementConfig { @@ -35,8 +36,8 @@ export type StreamStatus = 'live' | 'waiting' | 'offline'; export class LiveManagementModule extends EventEmitter implements AppModule { private config: LiveManagementConfig; private logger = getLogManager().getLogger('LiveManagementModule'); -private obsWebSocket = new OBSWebSocket(); -private obsStatus: OBSStatus = 'offline'; + private obsWebSocket = new OBSWebSocket(); + private obsStatus: OBSStatus = 'offline'; private streamStatus: StreamStatus = 'offline'; private roomInfo: RoomInfo = { title: '未设置标题', @@ -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状态对象 @@ -278,30 +420,26 @@ private obsStatus: OBSStatus = 'offline'; // 实现AppModule接口的enable方法 - enable(context: ModuleContext): void { + async enable(context: ModuleContext): Promise { this.app = context.app; this.logger.info('LiveManagementModule enabled'); - - // 注册应用退出时的清理函数 - this.app.on('will-quit', () => { - this.cleanup(); - }); + return true; } - // 清理资源 - private cleanup(): void { - if (this.reconnectTimeout) { - clearTimeout(this.reconnectTimeout); - } - - // 断开OBS连接 - if (this.obsWebSocket && this.obsStatus === 'online') { - this.obsWebSocket.disconnect().catch(error => { - this.logger.error('Error disconnecting OBS:', error); - }); + async disable(): Promise { + if (this.obsStatus === 'online') { + await this.obsWebSocket.disconnect(); + } + this.streamStatus = 'offline'; + this.obsStatus = 'offline'; + this.logger.info('LiveManagementModule disabled'); + return true; } - - this.logger.info('LiveManagementModule cleanup done'); + + // 注册应用退出时的清理函数 + this.app.on('will-quit', () => { + this.cleanup(); + }); } async stopStream(): Promise { diff --git a/packages/main/src/modules/MiniProgramModule.ts b/packages/main/src/modules/MiniProgramModule.ts new file mode 100644 index 00000000..883e9ac6 --- /dev/null +++ b/packages/main/src/modules/MiniProgramModule.ts @@ -0,0 +1,162 @@ +import { EventEmitter } from 'events'; +import { existsSync, readdirSync } from 'fs'; +import { readFile, writeFile, mkdir } from 'fs/promises'; +import { join } from 'path'; +import { AppModule } from '../core/AppModule'; +import { ModuleContext } from '../core/ModuleContext'; +import { getLogManager } from '../utils/LogManager'; + +/** + * 小程序元数据接口 + */ +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 implements AppModule { + private miniProgramsPath: string; + private installedPrograms: Map; + private runningPrograms: Map; + private logger = getLogManager().getLogger('MiniProgramModule'); + + constructor() { + super(); + this.miniProgramsPath = join(__dirname, '../../mini-programs'); + this.installedPrograms = new Map(); + this.runningPrograms = new Map(); + } + + /** + * 初始化小程序系统 + */ + private async initialize(): Promise { + // 创建小程序目录 + await this.ensureDirectoryExists(this.miniProgramsPath); + // 加载已安装的小程序 + await this.loadInstalledPrograms(); + this.emit('initialized'); + } + + /** + * 确保目录存在 + */ + private async ensureDirectoryExists(path: string): Promise { + if (!existsSync(path)) { + await 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) { + this.logger.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()); + } + + async enable(context: ModuleContext): Promise { + this.logger.info('Enabling MiniProgramModule'); + await this.initialize(); + return true; + } + + async disable(): Promise { + this.logger.info('Disabling MiniProgramModule'); + // 停止所有运行中的小程序 + for (const appId of Array.from(this.runningPrograms.keys())) { + await this.stopProgram(appId); + } + return true; + } +} + +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..736fb641 --- /dev/null +++ b/packages/main/src/modules/NotebookModule.ts @@ -0,0 +1,212 @@ +import { EventEmitter } from 'events'; +import { existsSync, readdirSync } from 'fs'; +import { readFile, writeFile, unlink, mkdir } from 'fs/promises'; +import { join } from 'path'; +import { v4 as uuidv4 } from 'uuid'; +import { AppModule } from '../core/AppModule'; +import { ModuleContext } from '../core/ModuleContext'; +import { getLogManager } from '../utils/LogManager'; + +/** + * 笔记接口定义 + */ +interface Note { + id: string; + title: string; + content: string; + tags: string[]; + createdAt: Date; + updatedAt: Date; +} + +/** + * 小本本功能模块 + * 负责笔记的创建、编辑、查询和删除管理 + */ +export class NotebookModule extends EventEmitter implements AppModule { + private notesDirectory: string; + private notes: Map; + private logger = getLogManager().getLogger('NotebookModule'); + + constructor() { + super(); + this.notesDirectory = join(__dirname, '../../data/notebooks'); + this.notes = new Map(); + } + + /** + * 初始化小本本模块 + */ + private async initialize(): Promise { + // 确保笔记目录存在 + await this.ensureDirectoryExists(this.notesDirectory); + // 加载现有笔记 + await this.loadNotes(); + } + + /** + * 确保目录存在 + */ + private async ensureDirectoryExists(path: string): Promise { + if (!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()); + } + + async enable(context: ModuleContext): Promise { + this.logger.info('Enabling NotebookModule'); + await this.initialize(); + return true; + } + + async disable(): Promise { + this.logger.info('Disabling NotebookModule'); + return true; + } +} + +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..3e76aa26 --- /dev/null +++ b/packages/main/src/modules/NotificationModule.ts @@ -0,0 +1,231 @@ +import { EventEmitter } from 'events'; +import { Notification } from 'electron'; +import { ConfigManager } from '../core/ConfigManager'; +import { AppModule } from '../core/AppModule'; +import type { ModuleContext } from '../core/ModuleContext'; + +/** + * 通知类型枚举 + */ +enum NotificationType { + LIVE_STATUS = 'liveStatus', + DANMU_HIGHLIGHT = 'danmuHighlight', + GIFT_ALERT = 'giftAlert', + SYSTEM_UPDATE = 'systemUpdate', + REMINDER = 'reminder' +} + +/** + * 通知配置接口 + */ +interface NotificationConfig { + enabled: boolean; + sound: boolean; + toastDuration: number; + showToast: boolean; +} + +/** + * 通知服务模块 + * 负责管理应用内所有通知的配置、触发和展示 + */ +export class NotificationModule extends EventEmitter implements AppModule { + private configManager: ConfigManager; + private notificationConfigs: Map; + private globalEnabled: boolean; + private globalSound: boolean; + private globalShowToast: boolean; + private globalToastDuration: number; + + constructor() { + super(); + this.configManager = new ConfigManager(); + this.notificationConfigs = new Map(); + this.globalEnabled = true; + this.globalSound = true; + this.globalShowToast = true; + this.globalToastDuration = 5000; + this.initialize(); + } + + /** + * 初始化通知模块 + */ + private async initialize(): Promise { + await this.initializeNotificationConfig(); + await this.loadConfigurations(); + this.setupDefaultConfigs(); + } + + /** + * 初始化通知配置 + */ + private async initializeNotificationConfig() { + const config = await this.configManager.readConfig(); + const notificationSettings = config.notificationSettings || {}; + + // 加载全局通知设置 + this.globalEnabled = notificationSettings.enabled !== false; + this.globalSound = notificationSettings.sound !== false; + this.globalShowToast = notificationSettings.showToast !== false; + this.globalToastDuration = notificationSettings.toastDuration || 5000; + + // 加载分类通知设置 + const categories = notificationSettings.categories || {}; + Object.values(NotificationType).forEach(type => { + const enabled = categories[type] !== false; + this.notificationConfigs.set(type as NotificationType, { + enabled, + sound: this.globalSound, + showToast: this.globalShowToast, + toastDuration: this.globalToastDuration + }); + }); + } + + /** + * 设置默认配置 + */ + 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' + }); + } + }); + } + + /** + * 加载通知配置 + */ + private async loadConfigurations(): Promise { + try { + const notificationSettings = await this.configManager.get('notificationSettings') || {}; + const categories = notificationSettings.categories || {}; + + // 加载全局设置 + this.globalEnabled = notificationSettings.enabled !== false; + this.globalSound = notificationSettings.sound !== false; + this.globalShowToast = notificationSettings.showToast !== false; + this.globalToastDuration = notificationSettings.toastDuration || 5000; + + // 加载分类设置 + Object.values(NotificationType).forEach(type => { + this.notificationConfigs.set(type as NotificationType, { + enabled: categories[type] !== false, + sound: this.globalSound, + showToast: this.globalShowToast, + toastDuration: this.globalToastDuration + }); + }); + } catch (error) { + console.error('Failed to load notification configurations:', error); + } + } + + /** + * 获取特定类型的通知配置 + */ + 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 || !config.showToast) return false; + + // 创建通知 + const notification = new Notification({ + title, + body, + silent: !config.sound, + timeoutType: 'default' + }); + + // 设置点击事件 + if (onClick) { + notification.on('click', onClick); + } + + // 显示通知 + notification.show(); + + // 设置自动关闭(由系统默认处理) + + 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; + } + + public enable(context: ModuleContext): void { + this.initialize(); + } +} + +export default new NotificationModule(); \ No newline at end of file diff --git a/packages/main/src/modules/SettingsModule.ts b/packages/main/src/modules/SettingsModule.ts index 83514694..f68096be 100644 --- a/packages/main/src/modules/SettingsModule.ts +++ b/packages/main/src/modules/SettingsModule.ts @@ -1,4 +1,5 @@ -import { ConfigManager } from '../utils/ConfigManager'; +import { AppModule } from '../core/AppModule'; +import { ConfigManager } from '../core/ConfigManager'; import { existsSync, unlinkSync, readdirSync, rmdirSync } from 'fs'; import { join } from 'path'; import { homedir } from 'os'; @@ -25,7 +26,7 @@ const DEFAULT_SETTINGS: AppSettings = { }; @singleton() -export class SettingsModule { +export class SettingsModule implements AppModule { private configManager: ConfigManager; private constructor() { this.configManager = new ConfigManager(); @@ -149,6 +150,14 @@ export class SettingsModule { return false; } } + + public async enable(): Promise { + return true; + } + + public async disable(): Promise { + return true; + } } export default SettingsModule.getInstance(); \ 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..6ba63f6f --- /dev/null +++ b/packages/main/src/modules/StreamRecordingModule.ts @@ -0,0 +1,192 @@ +import { singleton } from 'tsyringe'; +import { spawn } from 'child_process'; +import { existsSync, mkdirSync, writeFileSync, readdirSync, statSync } from 'fs'; +import { join } from 'path'; +import logger from '../utils/logger'; +import { ConfigManager } from '../core/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 filePath = join(this.recordingsPath, file.name); + const stats = existsSync(filePath) ? statSync(filePath) : { size: 0, mtime: new Date() }; + 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/modules/UserModule.ts b/packages/main/src/modules/UserModule.ts index adb18685..1be3b76c 100644 --- a/packages/main/src/modules/UserModule.ts +++ b/packages/main/src/modules/UserModule.ts @@ -1,5 +1,6 @@ import { singleton } from 'tsyringe'; import { ConfigManager } from '../utils/ConfigManager'; +import logger from '../utils/logger'; interface UserInfo { name: string; @@ -40,7 +41,7 @@ export default class UserModule { this.saveUserInfo(); } } catch (error) { - console.error('Failed to load user info:', error); + logger.error('Failed to load user info:', error); // 设置默认用户信息 this.userInfo = DEFAULT_USER_INFO; } @@ -55,7 +56,7 @@ export default class UserModule { this.configManager.set('userInfo', this.userInfo); } } catch (error) { - console.error('Failed to save user info:', error); + logger.error('Failed to save user info:', error); } } @@ -88,7 +89,7 @@ export default class UserModule { } return false; } catch (error) { - console.error('Failed to update user info:', error); + logger.error('Failed to update user info:', error); return false; } } @@ -102,7 +103,7 @@ export default class UserModule { try { // 模拟登录过程 // 实际应用中应该调用真实的登录API - console.log('Login attempt with:', credentials.username); + logger.info('Login attempt with:', credentials.username); // 模拟登录成功 this.userInfo = { @@ -114,7 +115,7 @@ export default class UserModule { this.saveUserInfo(); return true; } catch (error) { - console.error('Login failed:', error); + logger.error('Login failed:', error); return false; } } @@ -129,7 +130,7 @@ export default class UserModule { this.configManager.delete('userInfo'); return true; } catch (error) { - console.error('Logout failed:', error); + logger.error('Logout failed:', error); return false; } } 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..ab28b703 --- /dev/null +++ b/packages/main/src/services/AuthService.ts @@ -0,0 +1,206 @@ +import { session, app } from 'electron'; +import { v4 as uuidv4 } from 'uuid'; +import fs from 'fs'; +import path from 'path'; +import { logger } from '@app/utils/logger'; + +// 定义会话接口 +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(app.getPath('userData'), '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) { + logger.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) { + logger.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(); + // 验证响应数据结构 + if (!data.userId || !data.token || !data.expiresIn) { + throw new Error('登录响应数据格式无效'); + } + 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) { + logger.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(); + } + // 清除当前会话存储 + const currentSession = session.defaultSession; + await currentSession.clearStorageData(); + await currentSession.cookies.remove({}); + } + + // 检查认证状态 + 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) { + logger.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..1ae63cd4 --- /dev/null +++ b/packages/main/src/services/DanmuDatabaseService.ts @@ -0,0 +1,196 @@ +import Database from 'better-sqlite3'; +import { app } from 'electron'; +import path from 'path'; +import fs from 'fs'; +import { logger } from '@app/utils/logger'; + +// 弹幕数据库服务 - 处理弹幕的持久化存储 +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) { + logger.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) { + logger.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) { + logger.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) { + logger.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) { + logger.error('Failed to query danmu by time range:', error); + throw new Error('查询时间范围内弹幕失败'); + } + } + + // 关闭数据库连接 + public close(): void { + if (this.db) { + this.db.close(); + } + } + + // 执行查询并返回结果 + public executeQuery(sql: string, params: any[] = []): any[] { + try { + const stmt = this.db.prepare(sql); + return stmt.all(...params); + } catch (error) { + console.error('Failed to execute query:', error); + throw new Error('执行数据库查询失败'); + } + } + + // 执行单行查询并返回结果 + public getSingleResult(sql: string, params: any[] = []): any { + try { + const stmt = this.db.prepare(sql); + return stmt.get(...params); + } catch (error) { + console.error('Failed to get single result:', error); + throw new Error('获取单行查询结果失败'); + } + } +} \ 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..3cf894bf --- /dev/null +++ b/packages/main/src/services/DataReportService.ts @@ -0,0 +1,214 @@ +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 totalDanmu = this.dbService.getSingleResult(` + SELECT COUNT(*) as total FROM danmu + WHERE timestamp BETWEEN ? AND ? + `, [startDate.toISOString(), endDate.toISOString()]); + + // 查询各房间弹幕分布 + const roomDistribution = this.dbService.executeQuery(` + SELECT roomId, COUNT(*) as count + FROM danmu + WHERE timestamp BETWEEN ? AND ? + GROUP BY roomId + ORDER BY count DESC + `, [startDate.toISOString(), endDate.toISOString()]); + + // 查询弹幕高峰时段 + const hourlyDistribution = this.dbService.executeQuery(` + SELECT strftime('%H', timestamp) as hour, COUNT(*) as count + FROM danmu + WHERE timestamp BETWEEN ? AND ? + GROUP BY hour + ORDER BY hour + `, [startDate.toISOString(), endDate.toISOString()]); + + // 查询礼物统计 + const giftStats = this.dbService.getSingleResult(` + SELECT SUM(giftValue) as totalGiftValue, COUNT(*) as giftCount + FROM danmu + WHERE timestamp BETWEEN ? AND ? AND isGift = 1 + `, [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 activeUsers = this.dbService.executeQuery(` + SELECT userId, username, COUNT(*) as danmuCount + FROM danmu + WHERE timestamp BETWEEN ? AND ? ${roomCondition} + GROUP BY userId + ORDER BY danmuCount DESC + LIMIT 20 + `, [...params]); + + // 查询观众活跃度趋势 + const activityTrend = this.dbService.executeQuery(` + 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 + `, [...params]); + + // 查询常用弹幕关键词 (简单版本) + const keywords = this.dbService.executeQuery(` + 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 + `, [...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 escapeCSVField(field: string): string { + if (typeof field !== 'string') { + field = String(field); + } + if (field.includes(',') || field.includes('"') || field.includes('\n')) { + return `"${field.replace(/"/g, '""')}"`; + } + return field; + } + + // 生成日报CSV内容 + private generateDailyReportCSV(data: any): string { + // 实现CSV生成逻辑 + let csv = '日期,总弹幕数,礼物总数,礼物总价值\n'; + csv += `${this.escapeCSVField(data.date)},${this.escapeCSVField(data.totalDanmu.toString())},${this.escapeCSVField(data.giftStats.giftCount.toString())},${this.escapeCSVField(data.giftStats.totalGiftValue.toString())}\n\n`; + + csv += '房间ID,弹幕数量\n'; + if (data.roomDistribution && Array.isArray(data.roomDistribution)) { + data.roomDistribution.forEach((item: any) => { + csv += `${this.escapeCSVField(item.roomId.toString())},${this.escapeCSVField(item.count.toString())}\n`; + }); + } + + return csv; + } + + // 生成观众分析CSV内容 + private generateAudienceReportCSV(data: any): string { + // 实现CSV生成逻辑 + let csv = `时间范围: ${this.escapeCSVField(data.timeRange.start)} 至 ${this.escapeCSVField(data.timeRange.end)} +`; + if (data.roomId) csv += `房间ID: ${this.escapeCSVField(data.roomId.toString())} +`; + csv += ' +活跃用户,发送弹幕数 +'; + + data.activeUsers.forEach((user: any) => { + csv += `${this.escapeCSVField(user.username)} (${this.escapeCSVField(user.userId.toString())}),${this.escapeCSVField(user.danmuCount.toString())} +`; + }); + + 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..d41e9d10 --- /dev/null +++ b/packages/main/src/services/ErrorHandlingService.ts @@ -0,0 +1,163 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { app } from 'electron'; +import { v4 as uuidv4 } from 'uuid'; +import { logger } from '@app/utils/logger'; + +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) { + logger.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) { + logger.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) { + logger.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) { + logger.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 81% rename from packages/main/src/utils/LogManager.ts rename to packages/main/src/services/LogManager.ts index b880eb7d..e7c8facd 100644 --- a/packages/main/src/utils/LogManager.ts +++ b/packages/main/src/services/LogManager.ts @@ -2,6 +2,7 @@ import { EventEmitter } from 'events'; import fs from 'fs'; import path from 'path'; import { app } from 'electron'; +import { logger } from '@app/utils/logger'; // 日志级别 export type LogLevel = 'info' | 'debug' | 'error' | 'warn'; @@ -35,13 +36,15 @@ export class LogManager extends EventEmitter { } // 获取当前日志文件大小 - private getCurrentFileSize(): void { - try { - const stats = fs.statSync(this.logFilePath); - this.currentFileSize = stats.size; - } catch (error) { - this.currentFileSize = 0; - } + private getCurrentFileSize(): void { + try { + const stats = fs.statSync(this.logFilePath); + this.currentFileSize = stats.size; + } catch (error) { + logger.warn('获取日志文件大小失败,将从0开始计算:', error); + this.currentFileSize = 0; + } + } // 添加日志条目 addLog(source: string, message: string, level: LogLevel = 'info'): void { @@ -85,7 +88,7 @@ export class LogManager extends EventEmitter { // 使用fs.promises重写为异步/等待模式并添加重试逻辑 this.appendLogWithRetry(logLine, 3).catch(err => { - console.error('Failed to write log to file after retries:', err); + logger.error('Failed to write log to file after retries:', err); }); } @@ -105,19 +108,20 @@ export class LogManager extends EventEmitter { } //轮转日志文件 - private rotateLogFile(): void { + private async rotateLogFile(): Promise { try { // 如果日志文件存在,则重命名为带时间戳格式 - if (fs.existsSync(this.logFilePath)) { + if (await fs.promises.access(this.logFilePath).then(() => true).catch(() => false)) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const rotatedPath = `${this.logFilePath}.${timestamp}`; - fs.renameSync(this.logFilePath, rotatedPath); + await fs.promises.rename(this.logFilePath, rotatedPath); this.currentFileSize = 0; // 可以添加旧日志文件的压缩或清理逻辑 } } catch (error) { - console.error('Failed to rotate log file:', error); + logger.error('Failed to rotate log file:', error); } + } // 获取特定源的日志 getLogs(source: string, limit: number = 100): LogEntry[] { @@ -142,14 +146,15 @@ export class LogManager extends EventEmitter { } // 清除所有日志 - clearAllLogs(): void { + async clearAllLogs(): Promise { this.logs.clear(); // 也可以选择清除日志文件 - fs.writeFile(this.logFilePath, '', (err) => { - if (err) { - console.error('Failed to clear log file:', err); - } - }); + try { + await fs.promises.writeFile(this.logFilePath, ''); + this.currentFileSize = 0; + } catch (err) { + logger.error('Failed to clear log file:', err); + } } } 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/main/src/utils/config.ts b/packages/main/src/utils/config.ts deleted file mode 100644 index 0021e562..00000000 --- a/packages/main/src/utils/config.ts +++ /dev/null @@ -1,4 +0,0 @@ -export default { - port: 12590, - applications: [] -} \ No newline at end of file diff --git a/packages/main/src/utils/logger.ts b/packages/main/src/utils/logger.ts new file mode 100644 index 00000000..e6777af7 --- /dev/null +++ b/packages/main/src/utils/logger.ts @@ -0,0 +1,86 @@ +import { createWriteStream, existsSync, mkdirSync } from 'fs'; +import { join } from 'path'; +import { app } from 'electron'; +import { format } from 'util'; + +// 确保日志目录存在 +const logDir = join(app.getPath('userData'), 'logs'); +if (!existsSync(logDir)) { + mkdirSync(logDir, { recursive: true }); +} + +// 创建日志写入流并添加错误处理 +const createLogStream = (filename: string) => { + const stream = createWriteStream(join(logDir, filename), { flags: 'a' }); + stream.on('error', (err) => { + console.error('日志流错误:', err); + }); + return stream; +}; + +// 创建日志写入流 +const errorLogStream = createLogStream('error.log'); +const combinedLogStream = createLogStream('combined.log'); + +// 在应用退出时关闭日志流 +app.on('will-quit', () => { + errorLogStream.end(); + combinedLogStream.end(); +}); + +/** + * 安全序列化元数据 + */ +const safeStringify = (data: unknown): string => { + try { + return JSON.stringify(data); + } catch (e) { + return format('%o', data); // 使用util.format作为后备 + } +}; + +/** + * 日志工具类 + */ +export const logger = { + /** + * 普通信息日志 + */ + info: (message: string, ...meta: unknown[]): void => { + const logMessage = `[${new Date().toISOString()}] [INFO] ${message} ${meta.length ? safeStringify(meta) : ''}\n`; + console.log(logMessage); + combinedLogStream.write(logMessage); + }, + + /** + * 错误日志 + */ + error: (message: string, ...meta: unknown[]): void => { + const logMessage = `[${new Date().toISOString()}] [ERROR] ${message} ${meta.length ? safeStringify(meta) : ''}\n`; + console.error(logMessage); + errorLogStream.write(logMessage); + combinedLogStream.write(logMessage); + }, + + /** + * 警告日志 + */ + warn: (message: string, ...meta: unknown[]): void => { + const logMessage = `[${new Date().toISOString()}] [WARN] ${message} ${meta.length ? safeStringify(meta) : ''}\n`; + console.warn(logMessage); + combinedLogStream.write(logMessage); + }, + + /** + * 调试日志 + */ + debug: (message: string, ...meta: unknown[]): void => { + const logMessage = `[${new Date().toISOString()}] [DEBUG] ${message} ${meta.length ? safeStringify(meta) : ''}\n`; + if (process.env.NODE_ENV === 'development') { + console.debug(logMessage); + combinedLogStream.write(logMessage); + } + } +}; + +export default logger; \ No newline at end of file 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 @@