From 960fec1e2fc0b9228ab872959ec47d85f309700c Mon Sep 17 00:00:00 2001 From: qq617564112 <617564112@qq.com> Date: Wed, 27 Aug 2025 08:31:44 +0800 Subject: [PATCH 1/8] Add mini program, analytics, and management modules Introduces mini program management APIs and backend logic, including endpoints for listing, adding, updating, and removing mini programs. Adds modules for blacklist management, stream recording, and user authentication. Implements analytics endpoints and related methods for live statistics, audience analysis, and gift statistics. Updates AcfunDanmuModule with danmu and stream management methods. Adds new Vue pages for error handling, mini program management, record management, and stream monitoring. Updates styles and settings page to support new features. --- packages/main/src/apis/httpApi.ts | 263 ++++++- packages/main/src/managers/AppManager.ts | 155 ++++ packages/main/src/modules/AcfunDanmuModule.ts | 50 ++ packages/main/src/modules/BlacklistManager.ts | 187 +++++ .../main/src/modules/StreamRecordingModule.ts | 191 +++++ packages/main/src/modules/UserAuthModule.ts | 193 +++++ packages/main/src/utils/AppManager.ts | 223 ++++++ packages/renderer/src/pages/ErrorPage.vue | 79 ++ .../src/pages/MiniProgramManagement.vue | 259 +++++++ .../renderer/src/pages/RecordManagement.vue | 135 ++++ packages/renderer/src/pages/Settings.vue | 2 +- .../renderer/src/pages/StreamMonitoring.vue | 206 ++++++ packages/renderer/src/style.css | 16 +- ...00\346\261\202\346\226\207\346\241\243.md" | 675 ++++++++++++++++++ 14 files changed, 2629 insertions(+), 5 deletions(-) create mode 100644 packages/main/src/managers/AppManager.ts create mode 100644 packages/main/src/modules/BlacklistManager.ts create mode 100644 packages/main/src/modules/StreamRecordingModule.ts create mode 100644 packages/main/src/modules/UserAuthModule.ts create mode 100644 packages/renderer/src/pages/ErrorPage.vue create mode 100644 packages/renderer/src/pages/MiniProgramManagement.vue create mode 100644 packages/renderer/src/pages/RecordManagement.vue create mode 100644 packages/renderer/src/pages/StreamMonitoring.vue create mode 100644 "\351\234\200\346\261\202\346\226\207\346\241\243/\346\225\264\345\220\210\351\234\200\346\261\202\346\226\207\346\241\243.md" diff --git a/packages/main/src/apis/httpApi.ts b/packages/main/src/apis/httpApi.ts index f43aec86..13e88dcb 100644 --- a/packages/main/src/apis/httpApi.ts +++ b/packages/main/src/apis/httpApi.ts @@ -174,7 +174,268 @@ 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: {} }); + })); + + // 数据分析模块API +router.get('/analytics/live-stats', async (req, res) => { + try { + const stats = await appManager.getLiveStatistics(); + res.json({ success: true, data: stats }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.get('/analytics/audience', async (req, res) => { + try { + const audienceData = await appManager.getAudienceAnalysis(); + res.json({ success: true, data: audienceData }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.get('/analytics/gifts', async (req, res) => { +// 快捷键设置相关接口 +router.get('/settings/shortcuts', async (req, res) => { + try { + const shortcuts = await appManager.getShortcuts(); + res.json({ success: true, data: shortcuts }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.post('/settings/shortcuts', async (req, res) => { + try { + const { shortcuts } = req.body; + await appManager.setShortcuts(shortcuts); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.post('/settings/shortcuts/reset', async (req, res) => { + try { + await appManager.resetShortcuts(); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +// 小程序市场相关接口 +router.get('/mini-programs/marketplace', async (req, res) => { + try { + const marketplaceApps = await appManager.getMarketplaceApps(); + res.json({ success: true, data: marketplaceApps }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.post('/mini-programs/install', async (req, res) => { + try { + const { name, source } = req.body; + await appManager.installMiniProgram(name, source); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.post('/mini-programs/update', async (req, res) => { + try { + const { name } = req.body; + await appManager.updateMiniProgram(name); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +// 弹幕系统相关接口 +router.get('/danmu/send', async (req, res) => { + try { + const { roomId, content, userId, nickname } = req.query; + const result = await acfunDanmuModule.sendDanmu(Number(roomId), Number(userId), nickname as string, content as string); + res.json({ success: true, data: result }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.get('/danmu/history', async (req, res) => { + try { + const { roomId, page = 1, pageSize = 20 } = req.query; + const history = await acfunDanmuModule.getDanmuHistory(Number(roomId), Number(page), Number(pageSize)); + res.json({ success: true, data: history }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.post('/danmu/block', async (req, res) => { + try { + const { roomId, userId, duration = 3600 } = req.body; + await acfunDanmuModule.blockUser(Number(roomId), Number(userId), Number(duration)); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +// 房间管理相关接口 +router.get('/room/managers', async (req, res) => { + try { + const { uid, page = 1, pageSize = 20 } = req.query; + const managers = await acfunDanmuModule.getManagerList(Number(uid), Number(page), Number(pageSize)); + res.json({ success: true, data: managers }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.post('/room/managers/add', async (req, res) => { + try { + const { uid, targetId } = req.body; + await acfunDanmuModule.addManager(Number(uid), Number(targetId)); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.post('/room/managers/remove', async (req, res) => { + try { + const { uid, targetId } = req.body; + await acfunDanmuModule.removeManager(Number(uid), Number(targetId)); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.post('/room/kick', async (req, res) => { + try { + const { uid, targetId, reason, duration = 3600 } = req.body; + await acfunDanmuModule.managerKickUser(Number(uid), Number(targetId), reason, Number(duration)); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +// 推流管理相关接口 +router.post('/stream/start', async (req, res) => { + try { + const { roomId, streamKey, quality = 'medium' } = req.body; + const result = await acfunDanmuModule.startStream(Number(roomId), streamKey, quality); + res.json({ success: true, data: result }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.post('/stream/stop', async (req, res) => { + try { + const { roomId } = req.body; + await acfunDanmuModule.stopStream(Number(roomId)); + res.json({ success: true }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +router.get('/stream/status', async (req, res) => { + try { + const { roomId } = req.query; + const status = await acfunDanmuModule.getStreamStatus(Number(roomId)); + res.json({ success: true, data: status }); + } catch (error) { + res.status(500).json({ success: false, error: error.message }); + } +}); + +// 日志系统相关接口 +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 }); + } +}); + +// ====== 应用管理相关HTTP接口 ====== // 获取已安装应用 router.get('/app/getInstalledApps', errorHandler(async (req: Request, res: Response) => { const result = await globalThis.appManager.getInstalledApps(); diff --git a/packages/main/src/managers/AppManager.ts b/packages/main/src/managers/AppManager.ts new file mode 100644 index 00000000..770289ca --- /dev/null +++ b/packages/main/src/managers/AppManager.ts @@ -0,0 +1,155 @@ +import { singleton } from 'tsyringe'; +import { acfunLiveAPI } from 'acfundanmu'; +import { ConfigManager } from '../utils/ConfigManager'; +import logger from '../utils/logger'; + +@singleton() +export class AppManager { + private configManager: ConfigManager; + private lastGiftStatsUpdate: number = 0; + private giftStatsCache: any = null; + private lastAudienceUpdate: number = 0; + private audienceCache: any = null; + private readonly CACHE_DURATION = 5 * 60 * 1000; // 5分钟缓存 + + constructor() { + this.configManager = new ConfigManager(); + } + + /** + * 获取详细观众分析数据 + */ + async getAudienceAnalysis(roomId: number): Promise { + const now = Date.now(); + // 检查缓存 + if (this.audienceCache && now - this.lastAudienceUpdate < this.CACHE_DURATION) { + return this.audienceCache; + } + + try { + // 调用ACFUN API获取详细观众数据 + const audienceData = await acfunLiveAPI.getAudienceDetail(roomId); + // 处理并缓存数据 + this.audienceCache = this.processAudienceData(audienceData); + this.lastAudienceUpdate = now; + return this.audienceCache; + } catch (error) { + logger.error('获取观众分析数据失败:', error); + // 返回缓存数据或空对象 + return this.audienceCache || { regions: {}, devices: {}, activeTimes: {} }; + } + } + + /** + * 处理观众数据分析 + */ + private processAudienceData(rawData: any): any { + // 实现观众地域、设备、活跃度等维度分析 + const regions = this.aggregateByRegion(rawData.viewers); + const devices = this.aggregateByDevice(rawData.viewers); + const activeTimes = this.aggregateActiveTime(rawData.viewers); + + return { + totalViewers: rawData.totalViewers || 0, + newViewers: rawData.newViewers || 0, + returningViewers: rawData.returningViewers || 0, + regions, + devices, + activeTimes, + topViewers: rawData.topViewers?.slice(0, 10) || [] + }; + } + + /** + * 按地域聚合观众数据 + */ + private aggregateByRegion(viewers: any[]): Record { + const regions: Record = {}; + viewers.forEach(viewer => { + const region = viewer.region || '未知'; + regions[region] = (regions[region] || 0) + 1; + }); + // 排序并返回前10个地域 + return Object.entries(regions) + .sort(([, a], [, b]) => b - a) + .slice(0, 10) + .reduce((obj, [k, v]) => ({ ...obj, [k]: v }), {}); + } + + /** + * 按设备类型聚合观众数据 + */ + private aggregateByDevice(viewers: any[]): Record { + const devices: Record = {}; + viewers.forEach(viewer => { + const device = viewer.device || '未知设备'; + devices[device] = (devices[device] || 0) + 1; + }); + return devices; + } + + /** + * 分析观众活跃时间段 + */ + private aggregateActiveTime(viewers: any[]): Record { + const activeTimes: Record = {}; + viewers.forEach(viewer => { + if (viewer.activeTime) { + const hour = new Date(viewer.activeTime).getHours(); + const hourRange = `${hour}:00-${hour+1}:00`; + activeTimes[hourRange] = (activeTimes[hourRange] || 0) + 1; + } + }); + return activeTimes; + } + + /** + * 获取礼物统计数据 + */ + async getGiftStatistics(roomId: number, days: number = 7): Promise { + const now = Date.now(); + // 检查缓存 + if (this.giftStatsCache && now - this.lastGiftStatsUpdate < this.CACHE_DURATION) { + return this.giftStatsCache; + } + + try { + // 调用ACFUN API获取礼物数据 + const giftData = await acfunLiveAPI.getGiftStats(roomId, days); + // 处理并缓存数据 + this.giftStatsCache = this.processGiftData(giftData); + this.lastGiftStatsUpdate = now; + return this.giftStatsCache; + } catch (error) { + logger.error('获取礼物统计数据失败:', error); + // 返回缓存数据或空对象 + return this.giftStatsCache || { total: 0, topGifts: [], dailyTrend: [] }; + } + } + + /** + * 处理礼物数据 + */ + private processGiftData(rawData: any): any { + // 计算总收益 + const total = rawData.items?.reduce((sum: number, item: any) => sum + (item.amount || 0) * (item.price || 0), 0) || 0; + // 获取Top礼物 + const topGifts = (rawData.items || []) + .sort((a: any, b: any) => (b.amount || 0) - (a.amount || 0)) + .slice(0, 10); + // 生成每日趋势 + const dailyTrend = rawData.dailyData?.map((day: any) => ({ + date: day.date, + amount: day.totalAmount || 0, + count: day.giftCount || 0 + })) || []; + + return { + total, + topGifts, + dailyTrend, + totalGifts: rawData.totalGifts || 0, + uniqueUsers: rawData.uniqueUsers || 0 + }; + } +} \ No newline at end of file diff --git a/packages/main/src/modules/AcfunDanmuModule.ts b/packages/main/src/modules/AcfunDanmuModule.ts index ab12eb35..fe3068f0 100644 --- a/packages/main/src/modules/AcfunDanmuModule.ts +++ b/packages/main/src/modules/AcfunDanmuModule.ts @@ -196,6 +196,31 @@ export class AcfunDanmuModule implements AppModule { } } + // 弹幕相关方法 + async sendDanmu(roomId: number, userId: number, nickname: string, content: string): Promise { + return this.callAcfunDanmuApi( + `/danmu/send`, + 'POST', + { roomId, userId, nickname, content } + ); + } + + async getDanmuHistory(roomId: number, page: number = 1, pageSize: number = 20): Promise { + return this.callAcfunDanmuApi( + `/danmu/history`, + 'GET', + { roomId, page, pageSize } + ); + } + + async blockUser(roomId: number, userId: number, duration: number = 3600): Promise { + return this.callAcfunDanmuApi( + `/danmu/block`, + 'POST', + { roomId, userId, duration } + ); + } + // 房管相关方法 async getManagerList(uid: number, page: number = 1, pageSize: number = 20): Promise { return this.callAcfunDanmuApi( @@ -294,6 +319,31 @@ export class AcfunDanmuModule implements AppModule { ); } + // 推流管理相关方法 + 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( diff --git a/packages/main/src/modules/BlacklistManager.ts b/packages/main/src/modules/BlacklistManager.ts new file mode 100644 index 00000000..9729d779 --- /dev/null +++ b/packages/main/src/modules/BlacklistManager.ts @@ -0,0 +1,187 @@ +import { EventEmitter } from 'events'; +import { ConfigManager } from '../utils/ConfigManager'; +import { LogManager } from '../utils/LogManager'; + +// 黑名单用户接口 +export interface BlacklistUser { + userId: string; + username: string; + reason?: string; + addedAt: number; + expiresAt?: number; // 可选的过期时间,永久黑名单不设置此值 +} + +/** + * 黑名单管理模块 + * 负责处理用户黑名单的添加、移除、查询和持久化 + */ +export class BlacklistManager extends EventEmitter { + private blacklist: Map; + private configManager: ConfigManager; + private logManager: LogManager; + private static readonly CONFIG_KEY = 'blacklist'; + + constructor() { + super(); + this.configManager = globalThis.configManager; + this.logManager = globalThis.logManager; + this.blacklist = new Map(); + this.initialize(); + } + + /** + * 初始化黑名单模块 + * 从配置加载已保存的黑名单数据 + */ + private initialize(): void { + this.logManager.addLog('BlacklistManager', 'Initializing blacklist manager', 'info'); + this.loadBlacklist(); + this.cleanupExpiredEntries(); + } + + /** + * 从配置加载黑名单数据 + */ + private loadBlacklist(): void { + try { + const savedBlacklist = this.configManager.readConfig()[BlacklistManager.CONFIG_KEY] || []; + savedBlacklist.forEach((user: BlacklistUser) => { + this.blacklist.set(user.userId, user); + }); + this.logManager.addLog('BlacklistManager', `Loaded ${this.blacklist.size} blacklist entries`, 'info'); + } catch (error) { + this.logManager.addLog('BlacklistManager', `Failed to load blacklist: ${error.message}`, 'error'); + // 加载失败时初始化空黑名单 + this.blacklist = new Map(); + } + } + + /** + * 保存黑名单数据到配置 + */ + private saveBlacklist(): void { + try { + const blacklistArray = Array.from(this.blacklist.values()); + this.configManager.writeConfig({ + [BlacklistManager.CONFIG_KEY]: blacklistArray + }); + this.logManager.addLog('BlacklistManager', `Saved ${blacklistArray.length} blacklist entries`, 'info'); + } catch (error) { + this.logManager.addLog('BlacklistManager', `Failed to save blacklist: ${error.message}`, 'error'); + } + } + + /** + * 清理过期的黑名单条目 + */ + private cleanupExpiredEntries(): void { + const now = Date.now(); + let expiredCount = 0; + + this.blacklist.forEach((user, userId) => { + if (user.expiresAt && user.expiresAt < now) { + this.blacklist.delete(userId); + expiredCount++; + } + }); + + if (expiredCount > 0) { + this.logManager.addLog('BlacklistManager', `Removed ${expiredCount} expired blacklist entries`, 'info'); + this.saveBlacklist(); + this.emit('blacklistUpdated', this.getBlacklist()); + } + } + + /** + * 添加用户到黑名单 + * @param user - 要添加的黑名单用户信息 + * @returns 添加结果 + */ + public addToBlacklist(user: BlacklistUser): boolean { + if (this.blacklist.has(user.userId)) { + this.logManager.addLog('BlacklistManager', `User ${user.username} (${user.userId}) is already in blacklist`, 'warn'); + return false; + } + + // 设置添加时间(如果未提供) + const userToAdd = { + ...user, + addedAt: user.addedAt || Date.now() + }; + + this.blacklist.set(user.userId, userToAdd); + this.logManager.addLog('BlacklistManager', `Added user ${user.username} (${user.userId}) to blacklist. Reason: ${user.reason || 'Not specified'}`, 'info'); + this.saveBlacklist(); + this.emit('blacklistUpdated', this.getBlacklist()); + this.emit('userAdded', userToAdd); + return true; + } + + /** + * 从黑名单移除用户 + * @param userId - 要移除的用户ID + * @returns 移除结果 + */ + public removeFromBlacklist(userId: string): boolean { + const user = this.blacklist.get(userId); + if (!user) { + this.logManager.addLog('BlacklistManager', `User ${userId} is not in blacklist`, 'warn'); + return false; + } + + this.blacklist.delete(userId); + this.logManager.addLog('BlacklistManager', `Removed user ${user.username} (${userId}) from blacklist`, 'info'); + this.saveBlacklist(); + this.emit('blacklistUpdated', this.getBlacklist()); + this.emit('userRemoved', user); + return true; + } + + /** + * 检查用户是否在黑名单中 + * @param userId - 要检查的用户ID + * @returns 是否在黑名单中 + */ + public isBlacklisted(userId: string): boolean { + this.cleanupExpiredEntries(); // 检查前先清理过期条目 + return this.blacklist.has(userId); + } + + /** + * 获取黑名单中的用户信息 + * @param userId - 用户ID + * @returns 用户信息或undefined + */ + public getBlacklistUser(userId: string): BlacklistUser | undefined { + this.cleanupExpiredEntries(); + return this.blacklist.get(userId); + } + + /** + * 获取完整的黑名单列表 + * @returns 黑名单用户数组 + */ + public getBlacklist(): BlacklistUser[] { + this.cleanupExpiredEntries(); + return Array.from(this.blacklist.values()); + } + + /** + * 清空黑名单 + * @returns 操作结果 + */ + public clearBlacklist(): boolean { + if (this.blacklist.size === 0) { + this.logManager.addLog('BlacklistManager', 'Blacklist is already empty', 'warn'); + return false; + } + + const count = this.blacklist.size; + this.blacklist.clear(); + this.logManager.addLog('BlacklistManager', `Cleared ${count} entries from blacklist`, 'info'); + this.saveBlacklist(); + this.emit('blacklistUpdated', this.getBlacklist()); + this.emit('blacklistCleared'); + return true; + } +} \ No newline at end of file diff --git a/packages/main/src/modules/StreamRecordingModule.ts b/packages/main/src/modules/StreamRecordingModule.ts new file mode 100644 index 00000000..82966a57 --- /dev/null +++ b/packages/main/src/modules/StreamRecordingModule.ts @@ -0,0 +1,191 @@ +import { singleton } from 'tsyringe'; +import { spawn } from 'child_process'; +import { existsSync, mkdirSync, writeFileSync, readdirSync } from 'fs'; +import { join } from 'path'; +import logger from '../utils/logger'; +import { ConfigManager } from '../utils/ConfigManager'; + +@singleton() +export class StreamRecordingModule { + private recordingsPath: string; + private isRecording: boolean = false; + private currentProcess: any = null; + private configManager: ConfigManager; + private currentRecordingFile: string = ''; + + constructor() { + this.configManager = new ConfigManager(); + this.recordingsPath = join(this.configManager.getConfigPath(), 'recordings'); + // 确保录制目录存在 + if (!existsSync(this.recordingsPath)) { + mkdirSync(this.recordingsPath, { recursive: true }); + } + } + + /** + * 开始录制直播 + * @param roomId 直播间ID + * @param quality 录制质量 (high, medium, low) + * @returns 录制文件名 + */ + startRecording(roomId: number, quality: 'high' | 'medium' | 'low' = 'high'): string { + if (this.isRecording) { + throw new Error('已有录制任务正在进行中'); + } + + try { + // 生成录制文件名 + const timestamp = new Date().toISOString().replace(/:/g, '-'); + const filename = `acfun_${roomId}_${timestamp}.flv`; + this.currentRecordingFile = filename; + const outputPath = join(this.recordingsPath, filename); + + // 获取直播流地址 (实际应用中需要从ACFUN API获取真实流地址) + const streamUrl = this.getStreamUrl(roomId, quality); + + // 使用ffmpeg录制流 (需要确保系统已安装ffmpeg) + this.currentProcess = spawn('ffmpeg', [ + '-i', streamUrl, + '-c:v', 'copy', + '-c:a', 'copy', + '-y', // 覆盖已存在文件 + outputPath + ]); + + this.isRecording = true; + + // 记录日志 + this.currentProcess.stdout.on('data', (data: Buffer) => { + logger.info(`录制输出: ${data.toString().trim()}`); + }); + + this.currentProcess.stderr.on('data', (data: Buffer) => { + logger.error(`录制错误: ${data.toString().trim()}`); + }); + + this.currentProcess.on('close', (code: number) => { + this.isRecording = false; + this.currentProcess = null; + this.updateRecordingMetadata(filename, 'completed'); + logger.info(`录制进程已退出,代码: ${code}`); + }); + + // 记录录制元数据 + this.saveRecordingMetadata(filename, roomId, quality); + + return filename; + } catch (error) { + logger.error('开始录制失败:', error); + this.isRecording = false; + throw error; + } + } + + /** + * 停止录制 + */ + stopRecording(): boolean { + if (!this.isRecording || !this.currentProcess) { + return false; + } + + try { + // 优雅地结束ffmpeg进程 + this.currentProcess.kill('SIGINT'); + this.isRecording = false; + this.updateRecordingMetadata(this.currentRecordingFile, 'stopped'); + return true; + } catch (error) { + logger.error('停止录制失败:', error); + return false; + } + } + + /** + * 获取录制状态 + */ + getRecordingStatus(): { isRecording: boolean; currentFile?: string; recordingsPath: string } { + return { + isRecording: this.isRecording, + currentFile: this.currentRecordingFile || undefined, + recordingsPath: this.recordingsPath + }; + } + + /** + * 获取录制文件列表 + */ + getRecordingList(): { name: string; size: number; date: Date }[] { + try { + const files = readdirSync(this.recordingsPath, { withFileTypes: true }) + .filter(dirent => dirent.isFile() && dirent.name.endsWith('.flv')) + .map(file => { + const stats = file.statSync(); + return { + name: file.name, + size: stats.size, + date: new Date(stats.mtime) + }; + }); + + // 按日期降序排序 + return files.sort((a, b) => b.date.getTime() - a.date.getTime()); + } catch (error) { + logger.error('获取录制列表失败:', error); + return []; + } + } + + /** + * 获取直播流地址 (模拟实现) + * @param roomId 直播间ID + * @param quality 质量 + */ + private getStreamUrl(roomId: number, quality: string): string { + // 实际应用中需要从ACFUN API获取真实的流地址 + const qualityParams = { + high: '1080p', + medium: '720p', + low: '480p' + }; + // 这里使用模拟地址,实际项目中需替换为真实API调用 + return `https://live.acfun.cn/api/stream/${roomId}?quality=${qualityParams[quality]}`; + } + + /** + * 保存录制元数据 + */ + private saveRecordingMetadata(filename: string, roomId: number, quality: string): void { + const metadata = { + filename, + roomId, + quality, + startTime: new Date().toISOString(), + status: 'recording', + fileSize: 0 + }; + + const metadataPath = join(this.recordingsPath, `${filename}.json`); + writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), 'utf8'); + } + + /** + * 更新录制元数据 + */ + private updateRecordingMetadata(filename: string, status: 'completed' | 'stopped' | 'failed'): void { + try { + const metadataPath = join(this.recordingsPath, `${filename}.json`); + if (existsSync(metadataPath)) { + // 实际实现中应读取并更新现有元数据 + const metadata = { + ...require(metadataPath), + status, + endTime: new Date().toISOString() + }; + writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), 'utf8'); + } + } catch (error) { + logger.error('更新录制元数据失败:', error); + } + } +} \ No newline at end of file diff --git a/packages/main/src/modules/UserAuthModule.ts b/packages/main/src/modules/UserAuthModule.ts new file mode 100644 index 00000000..a23946d7 --- /dev/null +++ b/packages/main/src/modules/UserAuthModule.ts @@ -0,0 +1,193 @@ +import { EventEmitter } from 'events'; +import { app } from 'electron'; +import { ConfigManager } from '../utils/ConfigManager'; +import { LogManager } from '../utils/LogManager'; + +// 用户认证状态枚举 +export enum AuthStatus { + LOGGED_OUT = 'logged_out', + LOGGING_IN = 'logging_in', + LOGGED_IN = 'logged_in', + GUEST = 'guest' +} + +// 用户信息接口 +export interface UserInfo { + userId: string; + username: string; + avatar?: string; + token?: string; + permissions: string[]; +} + +/** + * 用户认证模块 + * 负责处理登录、登出、会话管理和权限控制 + */ +export class UserAuthModule extends EventEmitter { + private status: AuthStatus = AuthStatus.LOGGED_OUT; + private currentUser: UserInfo | null = null; + private configManager: ConfigManager; + private logManager: LogManager; + private autoLoginTimer?: NodeJS.Timeout; + + constructor() { + super(); + this.configManager = globalThis.configManager; + this.logManager = globalThis.logManager; + this.initialize(); + } + + /** + * 初始化认证模块 + */ + private async initialize(): Promise { + this.logManager.addLog('UserAuthModule', 'Initializing user authentication module', 'info'); + await this.checkAutoLogin(); + } + + /** + * 检查是否需要自动登录 + */ + private async checkAutoLogin(): Promise { + const authConfig = this.configManager.readConfig().auth || {}; + + if (authConfig.autoLogin && authConfig.lastLoginUser) { + this.logManager.addLog('UserAuthModule', 'Attempting auto-login for user', 'info'); + this.status = AuthStatus.LOGGING_IN; + this.emit('statusChanged', this.status); + + try { + // 模拟自动登录过程 + this.autoLoginTimer = setTimeout(async () => { + // 在实际实现中,这里应该调用ACFUN API验证token有效性 + this.currentUser = authConfig.lastLoginUser; + this.status = AuthStatus.LOGGED_IN; + this.logManager.addLog('UserAuthModule', `Auto-login successful for user: ${this.currentUser.username}`, 'info'); + this.emit('statusChanged', this.status); + this.emit('userChanged', this.currentUser); + }, 1500); + } catch (error) { + this.logManager.addLog('UserAuthModule', `Auto-login failed: ${error.message}`, 'error'); + this.status = AuthStatus.LOGGED_OUT; + this.emit('statusChanged', this.status); + } + } + } + + /** + * 以游客模式登录 + */ + public enterGuestMode(): void { + this.logManager.addLog('UserAuthModule', 'Entering guest mode', 'info'); + this.status = AuthStatus.GUEST; + this.currentUser = { + userId: 'guest', + username: '游客', + permissions: ['guest'] + }; + this.emit('statusChanged', this.status); + this.emit('userChanged', this.currentUser); + } + + /** + * 用户登录 + * @param token - 认证token + * @param userInfo - 用户信息 + */ + public async login(token: string, userInfo: UserInfo): Promise { + if (this.status === AuthStatus.LOGGING_IN) { + this.logManager.addLog('UserAuthModule', 'Login process already in progress', 'warn'); + return false; + } + + this.status = AuthStatus.LOGGING_IN; + this.emit('statusChanged', this.status); + + try { + // 在实际实现中,这里应该验证token并获取完整用户信息 + this.currentUser = { + ...userInfo, + token + }; + + // 保存登录状态用于自动登录 + const authConfig = { + autoLogin: true, + lastLoginUser: this.currentUser + }; + this.configManager.writeConfig({ auth: authConfig }); + + this.status = AuthStatus.LOGGED_IN; + this.logManager.addLog('UserAuthModule', `Login successful for user: ${this.currentUser.username}`, 'info'); + this.emit('statusChanged', this.status); + this.emit('userChanged', this.currentUser); + return true; + } catch (error) { + this.logManager.addLog('UserAuthModule', `Login failed: ${error.message}`, 'error'); + this.status = AuthStatus.LOGGED_OUT; + this.emit('statusChanged', this.status); + return false; + } + } + + /** + * 用户登出 + * @param keepAutoLogin - 是否保持自动登录状态 + */ + public logout(keepAutoLogin: boolean = false): void { + this.logManager.addLog('UserAuthModule', 'User logging out', 'info'); + + // 清除自动登录状态(如果需要) + if (!keepAutoLogin) { + const authConfig = this.configManager.readConfig().auth || {}; + authConfig.autoLogin = false; + this.configManager.writeConfig({ auth: authConfig }); + } + + // 清除当前用户信息 + this.currentUser = null; + this.status = AuthStatus.LOGGED_OUT; + this.emit('statusChanged', this.status); + this.emit('userChanged', null); + + // 清除可能的自动登录定时器 + if (this.autoLoginTimer) { + clearTimeout(this.autoLoginTimer); + this.autoLoginTimer = undefined; + } + } + + /** + * 获取当前认证状态 + */ + public getStatus(): AuthStatus { + return this.status; + } + + /** + * 获取当前用户信息 + */ + public getUserInfo(): UserInfo | null { + return this.currentUser; + } + + /** + * 检查用户是否有权限执行某个操作 + * @param permission - 权限名称 + */ + public hasPermission(permission: string): boolean { + if (!this.currentUser) return false; + return this.currentUser.permissions.includes(permission) || this.currentUser.permissions.includes('admin'); + } + + /** + * 销毁模块资源 + */ + public destroy(): void { + if (this.autoLoginTimer) { + clearTimeout(this.autoLoginTimer); + } + this.removeAllListeners(); + } +} \ No newline at end of file diff --git a/packages/main/src/utils/AppManager.ts b/packages/main/src/utils/AppManager.ts index e06753c6..d84bc491 100644 --- a/packages/main/src/utils/AppManager.ts +++ b/packages/main/src/utils/AppManager.ts @@ -116,6 +116,43 @@ export class AppManager extends EventEmitter { 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); } @@ -143,6 +180,192 @@ export class AppManager extends EventEmitter { 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) { + // 实现小程序安装逻辑 + // 1. 验证来源合法性 + // 2. 下载小程序资源 + // 3. 安装到本地目录 + // 4. 更新installedApps列表 + const newApp = { + name, + type: 'miniProgram', + path: path.join(this.appsDir, name), + isRunning: false, + config: {}, + lastUpdated: new Date().toISOString() + }; + + this.installedApps.push(newApp); + await this.saveInstalledApps(); + } + + 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(name: string) { + // 实现小程序更新逻辑 + const appIndex = this.installedApps.findIndex(app => app.name === name && app.type === 'miniProgram'); + if (appIndex === -1) { + throw new Error(`小程序 ${name} 未安装`); + } + + // 模拟更新检查 + this.installedApps[appIndex].lastUpdated = new Date().toISOString(); + await this.saveInstalledApps(); + } + + 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`; diff --git a/packages/renderer/src/pages/ErrorPage.vue b/packages/renderer/src/pages/ErrorPage.vue new file mode 100644 index 00000000..73ee2e85 --- /dev/null +++ b/packages/renderer/src/pages/ErrorPage.vue @@ -0,0 +1,79 @@ + + + + + \ No newline at end of file diff --git a/packages/renderer/src/pages/MiniProgramManagement.vue b/packages/renderer/src/pages/MiniProgramManagement.vue new file mode 100644 index 00000000..612185f6 --- /dev/null +++ b/packages/renderer/src/pages/MiniProgramManagement.vue @@ -0,0 +1,259 @@ + + + + + \ No newline at end of file diff --git a/packages/renderer/src/pages/RecordManagement.vue b/packages/renderer/src/pages/RecordManagement.vue new file mode 100644 index 00000000..7cd7eacd --- /dev/null +++ b/packages/renderer/src/pages/RecordManagement.vue @@ -0,0 +1,135 @@ + + + + + \ No newline at end of file diff --git a/packages/renderer/src/pages/Settings.vue b/packages/renderer/src/pages/Settings.vue index 13e491da..c12a99bb 100644 --- a/packages/renderer/src/pages/Settings.vue +++ b/packages/renderer/src/pages/Settings.vue @@ -204,7 +204,7 @@ const clearConfigCache = async () => {
配置缓存大小
{{ settings.cacheSize }} - +
diff --git a/packages/renderer/src/pages/StreamMonitoring.vue b/packages/renderer/src/pages/StreamMonitoring.vue new file mode 100644 index 00000000..8a43ad83 --- /dev/null +++ b/packages/renderer/src/pages/StreamMonitoring.vue @@ -0,0 +1,206 @@ + + + + + \ No newline at end of file diff --git a/packages/renderer/src/style.css b/packages/renderer/src/style.css index 0552a3c8..94569b4e 100644 --- a/packages/renderer/src/style.css +++ b/packages/renderer/src/style.css @@ -4,7 +4,7 @@ font-weight: 400; /* 基础主题变量 - 浅色模式默认值 */ - --td-brand-color: #3662E3; + --td-brand-color: #1890ff; --td-brand-color-hover: #406ffd; --td-brand-color-active: #2855d8; --td-brand-color-disabled: #8daaf2; @@ -52,7 +52,8 @@ /* 深色模式主题变量 */ :root[theme-mode="dark"] { /* 品牌色保持不变 */ - --td-brand-color: #3662E3; + --td-brand-color: #1890ff; + --td-brand-color: #1890ff; --td-brand-color-hover: #406ffd; --td-brand-color-active: #2855d8; --td-brand-color-disabled: #8daaf2; @@ -109,7 +110,7 @@ h1 { } button { - border-radius: 8px; + border-radius: 4px; border: 1px solid transparent; padding: 0.6em 1.2em; font-size: 1em; @@ -127,6 +128,15 @@ button:focus-visible { outline: 4px auto -webkit-focus-ring-color; } +h1, h2, h3 { + font-size: 18px; + font-weight: bold; +} + +p, div { + font-size: 14px; +} + .card { padding: 2em; } diff --git "a/\351\234\200\346\261\202\346\226\207\346\241\243/\346\225\264\345\220\210\351\234\200\346\261\202\346\226\207\346\241\243.md" "b/\351\234\200\346\261\202\346\226\207\346\241\243/\346\225\264\345\220\210\351\234\200\346\261\202\346\226\207\346\241\243.md" new file mode 100644 index 00000000..555a9451 --- /dev/null +++ "b/\351\234\200\346\261\202\346\226\207\346\241\243/\346\225\264\345\220\210\351\234\200\346\261\202\346\226\207\346\241\243.md" @@ -0,0 +1,675 @@ +# ACFUN直播工具箱整合需求文档(字节跳动标准) + +## 一、项目概述 +ACFUN直播工具箱是一款基于Electron开发的跨平台直播辅助工具,支持Windows、macOS和Linux系统。工具采用"应用主程序+小程序"架构,主程序提供稳定可靠的核心直播管理功能,小程序系统提供丰富多样的扩展能力,为ACFUN直播平台的主播提供高效、灵活的直播辅助解决方案。 + +### 1.1 产品定位 +- **目标用户**:ACFUN平台主播(个人主播、机构主播、签约主播) +- **核心价值**:简化直播流程、增强观众互动、提供数据洞察、支持功能扩展 +- **产品形态**:桌面应用程序(Electron)+小程序生态 +- **技术架构**:分层架构设计,主进程+渲染进程+小程序沙箱环境 + +### 1.2 核心优势 +- **开源免费**:降低主播使用门槛,支持社区贡献 +- **跨平台支持**:覆盖主流操作系统,满足不同主播的设备需求 +- **模块化设计**:核心功能稳定可靠,扩展功能灵活可选 +- **性能优化**:针对直播场景优化资源占用,确保直播流畅性 +- **安全可控**:完善的权限管理机制,保障直播内容安全 + +### 1.3 后端技术架构 + +#### 1.3.1 总体架构 +ACFUN直播工具箱后端采用分层架构设计,基于Electron框架实现跨平台支持,采用模块化设计理念,将核心功能与扩展能力分离。 + +- **核心层**:包含应用基础模块、配置管理、日志系统等基础设施 +- **服务层**:实现弹幕处理、推流管理、小程序系统等核心业务功能 +- **API层**:提供统一的接口供渲染进程和小程序调用 +- **通信层**:处理进程间通信、网络请求和实时数据流 + +#### 1.3.2 技术栈 +- 基础框架:Electron +- 语言:TypeScript +- 服务器:Express +- 进程通信:IPC、WebSocket、EventSource +- 数据存储:本地文件系统、electron-data + +### 1.3.3 核心模块设计 + +#### 应用生命周期管理 + +#####模块系统 +应用模块接口定义了标准化的模块启用方法,所有模块需实现enable接口以接收上下文信息并完成初始化。 + +- ModuleRunner负责初始化和管理所有应用模块 + +#####初始化流程 +应用初始化流程通过模块运行器依次加载核心功能模块,包括单实例控制、窗口生命周期管理、硬件加速配置、自动更新和弹幕系统等,确保各组件按序完成初始化。 + +#### 弹幕系统模块 +- 采用子进程模式运行,确保弹幕处理不影响主进程稳定性 +- 支持WebSocket和TCP两种连接模式,提高连接可靠性 +- 提供完整的配置管理和日志收集机制 +- 实现弹幕流接收、存储、分发。支持同时接收/分发多个直播间弹幕流。 + +核心实现代码示例: +弹幕系统模块包含存储和分发逻辑:存储逻辑将全量弹幕保存到sqlite数据库中,支持增量更新。分发逻辑根据收到的直播间ID将弹幕分发给对应的小程序。设置超时机制,如果弹幕对应的直播间ID不属于登陆者本人,则在一段时间没有小程序请求该直播间弹幕之后关闭对应直播间ID弹幕流 + +**核心API接口:** +- `GET /api/danmu/connect`: 建立弹幕连接 +- `GET /api/danmu/disconnect`: 断开弹幕连接 +- `GET /api/events/danmu`: 获取实时弹幕流(SSE) +- `GET /api/danmu/history`: 获取历史弹幕 +- `GET /api/danmu/config`: 获取弹幕配置 +- `POST /api/danmu/config`: 更新弹幕配置 + +**对应需求:** +- 性能需求: 弹幕处理能力≥1000条/秒 +- 功能验收: 弹幕显示准确率≥99%,过滤规则生效时间≤1秒 + +#### 窗口管理 +- 基于Electron BrowserWindow实现 +- 用于管理支持窗口打开的小程序窗口 +- 支持多窗口管理和窗口间通信 +- 延迟初始化机制确保IPC事件处理器已注册 +- 窗口创建和配置分离,通过WindowOptions接口实现灵活配置 +- 支持窗口状态持久化(位置、大小、最大化状态) + +#### 小程序管理 +- 实现应用生命周期管理 +- 支持模块化加载和卸载 +- 提供小程序配置持久化存储 + +小程序管理器采用事件驱动架构,通过扫描指定目录加载小程序配置,维护小程序注册表和窗口映射关系,提供小程序生命周期管理和跨进程通信能力。 + +### 1.3.4 通信机制 + +#### 进程间通信 +- 使用Electron的ipcMain和ipcRenderer实现主进程与渲染进程通信 +- 定义标准化的API接口,支持异步调用和错误处理 + +进程间通信API采用Electron的ipcMain机制实现,提供弹幕模块启停等核心功能的跨进程调用接口,包含错误处理和状态返回机制。 + +#### 实时数据流 +- 使用EventSource实现单向实时数据流(如弹幕流) +- 支持心跳机制和断线重连策略 +- 基于HTTP长连接,减少资源消耗 + +HTTP服务器模块实现了基于EventSource的服务器推送功能,通过建立持久连接向客户端推送实时事件数据流,包含连接管理、事件发布和资源清理机制。 + +### 1.3.5 存储系统设计 + +#### 配置管理 +- 基于electron-data实现跨平台配置存储 +- 支持配置文件备份和恢复 +- 配置变更实时生效机制 +配置管理器基于electron-data实现跨平台配置存储,支持自定义路径、自动保存和格式化存储,提供配置目录创建、初始化和错误处理功能。 + +####数据管理 +- 单例模式实现的数据管理器 +- 支持应用数据的CRUD操作 +- 数据变更通知机制 + +数据管理器采用单例模式设计,提供键值对数据存储和变更通知机制,支持数据监听和事件驱动的数据更新,确保应用状态一致性。 + +#### 日志系统 +- 基于EventEmitter实现的日志管理器 +- 支持多来源日志分类管理 +- 文件日志(按日期轮转)和内存日志(限制1000条)双存储机制 +- 日志级别控制(info/debug/error/warn) + +日志管理器基于事件驱动架构,实现多来源日志分类管理,采用文件日志(按日期轮转)和内存日志(限制1000条)双存储机制,支持日志级别控制和日志添加事件通知。 + +### 1.3.6 HTTP服务器与API设计 + +#### 服务器架构 +- 基于Express实现的轻量级HTTP服务器 +- 支持静态资源托管和API路由 +- 动态路由注册机制,支持模块化扩展 + +HTTP服务器管理模块基于Express框架构建轻量级服务器,支持静态资源托管和API路由,实现动态路由注册机制和模块化扩展,提供服务器初始化和端口配置功能。 + +#### API设计原则 +- RESTful API设计风格 +-统一的响应格式({success: boolean, data?: any, error?: string}) +-完善的错误处理 +-权限验证机制 + +### 1.3.7 性能优化策略 + +#### 资源管理 +- 子进程隔离CPU密集型任务(弹幕处理≥1000条/秒) +- 延迟加载非核心模块(首屏加载≤3个核心模块) +- 资源使用限制(单小程序内存≤100MB,CPU占用≤5%) +- 实现代码: +性能优化采用子进程隔离机制处理CPU密集型任务,如弹幕处理(≥1000条/秒),通过设置5MB缓冲区限制和独立进程资源,避免影响主进程稳定性。 + +#### 网络优化 +- 请求合并减少网络往返(批量API请求合并≤100ms延迟) +- 数据缓存策略(内存缓存TTL=5分钟,磁盘缓存TTL=24小时) +- 长连接复用(WebSocket连接池大小=5,EventSource复用率≥90%) + +#### 渲染优化 +- 虚拟列表处理大数据渲染(弹幕列表可视区域渲染≤50条) +- 图片懒加载和压缩(WebP格式,质量70%,预加载视口内图片) +- 避免不必要的重绘(使用CSS containment,减少DOM操作) +- 避免不必要的UI更新 + +### 1.3.8 安全性设计 + +- 主进程与渲染进程严格隔离 +- 小程序运行在独立沙箱中 +- 敏感数据加密存储 +- 直播码定期刷新 +- IPC通信权限验证 + +### 1.3.9 小程序设计 + +#### 1.3.9.1 模块系统 +- 标准化的AppModule接口 +- 动态模块加载机制 +- 模块依赖管理 + +#### 1.3.9.2 小程序架构 +- 小程序运行时环境 +- 标准化通信接口 +- 资源限制和安全沙箱 + +#### 1.3.9.3 小程序系统 +- **插件注册机制**: 基于manifest.json声明式注册,支持静态和动态注册 +小程序系统采用声明式注册机制,通过manifest.json定义插件元数据和钩子函数,支持静态和动态注册,实现小程序启动/关闭、直播开始/结束等关键节点的功能扩展。 +- **钩子函数扩展点**: 主程序启动/关闭、直播开始/结束、弹幕接收等关键节点 +- **插件生命周期管理**: 加载→初始化→激活→停用以→卸载,支持热重载 + +### 1.3.10 部署与构建 + +#### 1.3.10.1 构建流程 +- TypeScript编译 +- 资源打包 +- 代码压缩和优化 + +#### 1.3.10.2 自动更新 +- 增量更新机制 +- 更新检测和通知 +- 回滚策略 + +#### 1.3.10.3 版本管理 +- 语义化版本控制 +- 版本兼容性保证 +- 版本发布流程 + + + +### 1.2 前端技术架构 + +#### 1.2.1 总体架构 +ACFUN直播工具箱前端采用组件化、模块化架构设计,基于Vue+TypeScript+TDesign实现,遵循MVVM模式 + +#### 1.2.2 组件设计 +- 基础组件:按钮输入框、选择器等高复用组件 +- 功能组件:弹幕组件、直播数据卡片等特殊业务组件 +- 页面组件:登录页、仪表盘等完整页面 + +### 1.2.3 功能模块实现方案 + +#### 1.2.3.1 主客户端核心模块 + +##### 仪表盘模块 +-数据获取:通过userProfile接口获取用户信息 +-状态管理:使用状态管理工存储用户信和核心数据 +- 动态内容渲染:根据数据类型动态渲染不同内容块 + +##### 用户认证模块 +-登录流程:生成登录二维码、监听扫描状态、验证登录信息、存储用户会话 +-状态管理:使用状态管理工具管理登录状态、浏览器存储持久化 +-权限控制:路由守卫、组件级别权限控制 + +##### 直播管理模块 +房间管理:房间信CRUD操作、封面上传裁剪分区选择 +-推流管理:RTMP地址直播码管理OB连接状态监控 +-信息展:实时观众数、点赞数、香蕉数等统计数据 + +#### 1.2.3.2 小程序系统模块 + +##### 1.2.3.2.1 小程序开发者支持 +- 提供完整的API文档和开发指南 +- 开发调试工具集成 +- 小程序打包和发布流程支持 +- 版本管理和兼容性测试工具 + +##### 1.2.3.2.2 小程序管理 +-小程序加载机制:动态加载远程组件、权限验证、生命周期管理 +-小程序状态监控:实时监控运行状态、资源使用情况、性能指标 +-小程序权限管理:细粒度权限控制、API访问限制、数据隔离 + +##### 1.2.3.2.3 小程序市场 +小程序商店:分类展示、搜索功能、评分系统 +安装更新:一键安装、自动更新、版本回退 +- 安全性检测:代码扫描、权限审计、运行时监控 + +### 1.2.4 核心功能实现细节 + +#### 1.2.4.1 状态管理设计 +使用状态管理工具Pinia进行全局状态管理 +模块化设计分离不同功能域的状态 +派生计算状态 +处理异步操作 +修改状态的规范方法 + +#### 1.2.4.2 网络请求设计 +封装HTTP请求 +实现EventSource连接管理用于处理单向数据流 +请求拦截器添加认证信息 +响应拦截器处理错误 +请求重试机制 + +#### 1.2.4.3 组件通信机制 +父子组件通过props事件通信 +跨组件通信使用事件总线 +全局状态通过状态管理工具共享 +小程序与主应用通过定制事件通信 + +#### 1.2.4.4 性能优化策略 +组件懒加载 +图片懒加载和压缩 +虚拟列表处理大数据渲染 +防抖节流处理频繁操作 +缓存策略减少重复请求 + +### 1.3 部署与构建 + +#### 1.3.1 构建流程 +-TypeScript编译 +-资源打包 +-代码压缩和优化 + +#### 1.3.2 自动更新 +-增量更新机制 +-更新检测和通知 +-回滚策略 + +#### 1.3.3 版本管理 +-语义化版本控制 +-版本兼容性保证 +-版本发布流程 + + +## 二、用户核心诉求分析 + +- **新手主播**:需要简单易用的开播流程,基础弹幕管理功能 +- **成长型主播**: 需要数据分析功能,观众互动工具,直播效果优化建议 +- **资深主播**: 需要高度自定义能力,多平台整合,专业级数据洞察 + +### 2.2 用户痛点与需求 +- **开播准备复杂**: 希望简化开播流程,一键完成房间设置和推流配置 +- **互动管理困难**: 需要高效弹幕过滤工具,自定义互动规则 +- **功能扩展受限**: 需要灵活添加新功能,无需等待整体版本更新 +- **数据决策缺失**: 需要直观的直播数据展示,观众行为分析 +- **多平台管理繁琐**: 希望一站式管理多个直播平台和账号 +## 三、UI设计需求 +### 3.1 UI整体架构 +ACFUN直播工具箱采用分层架构设计,整体风格为深色主题,符合直播工具的专业感和夜间使用场景需求。UI设计遵循"主客户端聚焦核心,小程序扩展功能"的架构理念,通过清晰的视觉层次和交互设计区分核心功能与扩展功能。 + +#### 3.1.1 布局层次 +- **应用层**:基于跨平台应用框架,包含主客户端和小程序容器 +- **框架层**:自定义的布局组件(内容框架、行框架等)和小程序运行时环境 +- **组件层**:核心功能组件、UI组件和小程序公共组件 +- **页面层**:核心功能页面和小程序页面 + +#### 3.1.2 技术栈 +- 前端框架:vue + tdesign +- 小程序容器技术:wujie 微前端框架 + +### 3.2 主要页面布局 +#### 3.2.1 登录页面 +- **布局结构**:顶部导航栏(含应用logo)、中部登录区域(二维码登录模块和欢迎界面)、底部区域(版本号和版权信息)、右下角背景图 +- **设计特点**:简洁登录流程、响应式布局、实时登录状态反馈 +- **核心控件**:二维码登录按钮、同意条款复选框、免责声明弹窗 + +#### 3.2.2 仪表盘页面 +- **布局结构**:全屏内容框架(列布局)、欢迎区域(顶部行框架)、动态内容块(多行框架)、左侧底部半透明背景动画图 +- **设计特点**:信息展示为主、背景图片增强视觉效果、内容层次清晰 + +#### 3.2.3 房间管理页面 +- **布局结构**:封面设置区域、房间信息区域、弹幕流链接区域、OBS同步设置区域、RTMP设置区域、推流状态区域、下播按钮区域 +- **核心控件**:图片上传控件(支持裁剪)、房间标题输入框、直播分区级联选择器、OBS同步开关、推流状态指示器 + +#### 3.2.4 小程序管理中心 +- **布局结构**:顶部导航栏(搜索框和操作按钮)、分类标签栏、卡片式小程序列表、底部状态栏 +- **设计特点**:卡片式设计(含图标/名称/描述/评分/状态)、实时状态展示、快速操作按钮、拖拽排序功能 + +#### 3.2.5 其他关键页面 +- **错误页面**:全屏居中容器、错误图标(282x218px)、友好错误提示文本、返回按钮 +- **通用设置页面**:模块化设置区域(使用说明/系统/推流工具路径/端口设置/配置文件/缓存/账号缓存) +- **推流监控页面**:左侧房间信息区域(封面/标题/分类/时长)、右侧数据统计区域(观众数/点赞数/香蕉数/AC币) + +### 3.3 UI设计风格与特点 +#### 3.3.1 颜色方案 +- 主色调:蓝色系(#1890ff) +- 背景色:深色背景(黑色或深灰色) +- 文本色:白色和浅灰色(提高可读性) +- 状态色:绿色(在线/成功)、红色(离线/失败) + +#### 3.3.2 排版规范 +- 字体:系统默认字体,标题加粗 +- 字体大小层次: + - 标题:较大字号(18px+) + - 正文:中等字号(14px-16px) + - 辅助文字:较小字号(12px-13px) + +#### 3.3.5 组件尺寸规范 +- 按钮尺寸:默认40px高度,紧凑型32px,大型48px +- 输入框:统一高度40px,边框半径4px +- 卡片组件:默认阴影2px,悬停时阴影4px +- 图标尺寸:16x16px(紧凑)、24x24px(默认)、32x32px(强调) + +#### 3.3.3 组件风格 +- 圆角设计:轻微圆角(4px) +- 阴影效果:适度阴影增强层次感 +- 悬停效果:按钮和可交互元素有明显的悬停状态变化 +- 一致性:相同类型的组件保持统一的设计风格 + +#### 3.3.4 交互体验 +- 表单验证实时反馈 +- 操作成功/失败提示 +- 加载状态显示 +- 平滑过渡动画 + +### 3.4 响应式设计 +- 适配不同屏幕分辨率(1080p、720p、480p等) +- 使用弹性布局实现弹性空间分配 +- 部分页面元素根据屏幕尺寸自动调整大小或隐藏 + +### 3.5 组件库与自定义组件 +#### 3.5.1 第三方组件库使用 +主要使用第三方组件库,核心组件包括: +- 基础组件:按钮、输入框、开关、勾选框 +- 表单组件:表单、选择器、级联选择器、滑块 +- 导航组件:选项卡、菜单 +- 消息组件:消息提示、对话框、通知 +- 布局组件:容器、行、列 + +#### 3.5.2 自定义组件 +项目中开发了多个自定义组件以满足特定需求: +- 弹幕流组件 +- 房间列表组件 +- 超级聊组件 +- 规则引擎组件 +- 状态条组件 + +### 3.6 设计规范 +#### 3.6.1 布局规范 +- 统一使用内容框架和行框架构建页面结构 +- 保持一致的间距和边距(通常为8px、16px) +- 功能相关的元素放在同一区域 + +#### 3.6.2 颜色规范 +- 遵循品牌色体系 +- 确保文本与背景的对比度符合无障碍标准 +- 状态色使用统一(成功绿色、失败红色、警告黄色) + +#### 3.6.3 图标规范 +- 使用统一的图标库 +- 保持图标风格一致 +- 图标大小适中,确保可读性 + +#### 3.6.4 命名规范 +- 组件名称使用帕斯卡命名法 +- CSS类名使用短横线命名法 +- 文件名称与组件名称保持一致 + +### 3.7 性能优化 +- 组件懒加载 +- 图片优化(压缩、懒加载) +- 样式按需加载 +- 减少不必要的重绘和回流 + +### 3.8 未来优化方向 +1. 增加更多主题选项(明亮主题、深色主题切换) +2. 进一步优化响应式设计,适配更多设备 +3. 提升组件动画流畅度 +4. 增强无障碍设计 +5. 优化大流量场景下的UI表现(如高并发弹幕) + +## 4. 功能模块划分 +### 4.1 应用主程序核心模块 +#### 4.1.1 仪表盘模块 +**功能目标**:为主播提供直播全生命周期数据监控与高效操作入口 +**用户场景**:主播日常开播前准备、直播中监控、直播后复盘 +**核心功能点**: +- 个性化数据看板: + - 实时直播状态卡片: + - **状态定义**: + - 准备中(黄色):已配置房间信息但未开始推流,倒计时提示“距离计划开播时间还有XX分钟” + - 在线(绿色):推流正常,显示“直播中”标签及实时观看人数 + - 离线(灰色):未开播状态,显示上次直播时长及观众峰值 + - 异常(红色):推流中断/网络异常,显示错误码及“重新连接”按钮 + - **核心显示元素**: + - 状态指示灯(圆形,直径24px) + - 直播时长计时器(格式:HH:MM:SS) + - 实时观众数(含今日累计观看人次) + - 关键事件提示区(如“5分钟前达到今日观众峰值”) + - **交互规则**: + - 点击卡片展开详情面板(显示完整推流参数、网络状态) + - 双击卡片快速切换开播/关播状态 + - 悬停显示tooltip(当前码率、帧率、CPU占用) + + - 核心指标趋势图: + - **数据维度**: + - 观众指标:实时在线人数(折线)、累计观看人次(柱状)、平均观看时长(虚线) + - 互动指标:弹幕密度(条/分钟)、点赞率(互动人数/观看人数)、礼物触发率(礼物数/弹幕数) + - 收入指标:礼物总收入(折线)、付费转化率(付费用户数/观众数)、人均消费(总收入/付费用户数) + - **图表类型与布局**: + - 主图表区:7天趋势折线图(支持日/周/月切换),右侧Y轴双刻度(左侧人数/右侧金额) + - 数据对比区:今日vs昨日同期数据卡片(百分比变化带箭头指示) + - 异常标记:自动标记峰值/谷值点(如“20:00出现观众峰值,较前日增长35%”) + - **交互分析功能**: + - 时间粒度控制:支持15分钟/1小时/3小时数据聚合切换 + - 数据下钻:点击任意数据点显示该时段详细指标(如“19:00-20:00观众来源分布”) + - 对比分析:支持选择任意两天数据叠加显示,高亮差异区域 + - 导出功能:支持PNG图片导出和CSV数据下载 + + - 待办事项提醒(未配置推流参数、版本更新等) + - **优先级机制**: + - P0(阻断型):必须处理才能继续操作(如推流配置错误),需弹窗确认 + - P1(高优先级):影响直播体验但非阻断性(如网络带宽不足),顶部悬浮通知 + - P2(常规提醒):优化类建议(如功能使用技巧),折叠列表展示 + - **交互处理流程**: + - 右滑标记完成(带确认动画,自动同步至已完成列表) + - 长按唤起操作菜单(延期提醒/标记为已读/永久忽略) + - 点击直接跳转至对应处理页面(如点击“版本更新”打开更新中心) + - 批量处理功能(支持勾选多个事项一键标记完成) +- 快捷操作区: + **功能目标**:减少高频操作路径长度,支持个性化工作流定制 + **用户场景**: + - 新手主播:需要直观的核心功能入口,避免操作复杂度 + - 资深主播:需要高度定制化的快捷组合,提升操作效率 + - 多场景切换:根据直播状态(准备/直播中/复盘)自动调整可用功能 + **核心功能点**: + - 可配置快捷工具栏: + - **自定义布局**:支持拖拽调整按钮位置,最多显示8个常用功能 + - **分组管理**:可创建“开播准备组”、“直播控场组”等场景化按钮组 + - **大小切换**:提供紧凑模式(图标+文字)/极简模式(仅图标)切换 + - 智能场景化动作: + - **开播准备模式**:显示“房间设置→推流配置→封面上传→测试推流”流程化按钮 + - **直播中模式**:切换为“弹幕过滤→禁言管理→互动抽奖→紧急停播”控场工具 + - **复盘模式**:显示“数据报告→录屏回放→观众画像→下次预告”分析工具 + - 交互增强设计: + - **长按预览**:长按按钮显示功能说明及快捷键提示(如“房间设置 [F2]”) + - **操作反馈**:按钮点击带波纹动画,重要操作(如关播)有震动反馈 + - **快捷添加**:任意功能页面的“添加到快捷栏”选项,支持一键收藏 + +- 个性化配置: + - 支持自定义看板布局(拖拽调整组件位置) + - 可配置数据刷新频率(15s/30s/1min) + +#### 4.1.2 用户认证与授权模块 +- 多因子认证(密码+手机验证码+扫码登录) +- 账号安全保护(异地登录检测、登录异常提醒) + - 失败处理:5次密码错误后锁定30分钟,支持短信快速解锁 + - 会话管理:自动登出闲置2小时的会话,支持手动一键清除所有登录设备 + - 数据加密:传输层采用TLS 1.3加密,密码存储使用bcrypt算法加盐哈希 + - **异常处理机制**: + - 登录异常提醒:1分钟内连续失败3次,触发微信/短信双重告警 + - 可疑行为分析:检测到模拟器环境/代理IP登录时,强制启用最高级别验证 + - 应急登录通道:账号锁定后,可通过备用邮箱接收临时登录链接(有效期15分钟) + + +#### 4.1.3 直播管理模块 +**功能目标**:提供一站式直播全流程管理能力 +**用户场景**:开播前配置、直播中实时管理、紧急情况处理 +**核心功能点**: +- 房间配置中心: + - 基础信息设置(标题、封面、分区标签) + - 内容属性控制(直播类型标记、互动功能开关) + - 回放管理(自动开启回放、可见范围设置) + +- 推流控制: + - 基础协议支持(RTMP/HLS) + - 视频编码配置(H.264/AVC) + - 断线重连机制 + +- 观众互动管理: + - 弹幕过滤功能 + - 禁言管理工具 + - 礼物贡献榜展示 + - 申诉与审计: + - 违规记录存档:保留违规内容截图+处理时间+操作人,支持导出审计日志 + - 申诉通道:被处罚用户可提交申诉,主播收到申诉通知并处理 + + +#### 4.1.4 系统设置模块 +**功能目标**:提供灵活的系统级配置与个性化选项 +**用户场景**:首次使用初始化、性能优化、界面定制 +**核心功能点**: +- 界面个性化: + **功能目标**:提供高度可定制的界面体验,满足不同用户的视觉偏好和操作习惯 + **用户场景**: + - 新手用户:通过预设主题快速配置界面 + - 资深用户:精细化调整界面元素,创建个性化工作流 + - 特殊需求用户:通过辅助功能提升可访问性 + **核心功能点**: + - 主题系统: + - 预设主题库:提供商务蓝/活力橙/简约灰等5套主题,支持一键切换 + - 自定义主题编辑器:允许调整主色调/辅助色/中性色,实时预览效果 + - 主题同步:支持跨设备保存主题设置,与tdesign设计系统深度集成 + - 布局定制: + - 组件拖拽:支持核心面板(如直播控制台/数据看板)自由调整位置 + - 视图模式:提供紧凑/标准/宽松三种布局密度,适应不同屏幕尺寸 + - 自定义工作区:支持保存3套常用布局方案(如“直播中”/“数据分析”/“休息模式”) + - 排版设置: + - 字体控制:支持系统字体/自定义字体选择,提供微软雅黑/思源黑体等5种预设 + - 多级缩放:字号支持12px-20px无极调节(替代原5档固定缩放) + - 显示优化:可开启字体抗锯齿/字间距调整/行高设置(1.0-1.8倍) + - 快捷键体系: + - 全功能自定义:支持95%以上操作的快捷键重定义,含组合键设置 + - 场景化预设:提供“高效编辑”/“直播控场”等快捷键模板,一键应用 + - 冲突检测:自动识别快捷键冲突并提供替代方案建议 + - 辅助功能: + - 无障碍模式:支持高对比度显示(WCAG AA标准),屏幕阅读器兼容 + - 操作增强:可开启按键音效/震动反馈,提升操作确认感 + - 视觉提示:重要按钮高亮显示,危险操作二次确认弹窗 + +- 性能设置: + - 资源占用控制(小程序最大运行数量限制) + - 缓存管理(手动清理/自动清理策略) + - 启动项配置(开机自启/最小化到托盘) + +- 高级选项: + - 日志级别设置(调试/信息/警告/错误) + - API接口调试模式(供开发人员使用) + - 数据备份与恢复(配置信息导出/导入) + + +### 4.2 小程序系统模块 +#### 4.2.1 小程序管理中心 +- 小程序发现:分类展示、推荐列表、搜索功能 +- 生命周期管理:安装、更新、卸载、启用/禁用 +- 权限控制:小程序权限申请与管理 +- 性能监控:资源占用监控,异常行为检测 + +#### 4.2.2 小程序运行机制 +- 多显示支持:主窗口集成、独立窗口、OBS插件模式 +- 资源加载流程:目录扫描、入口文件解析、配置读取 +- 版本控制:本地缓存与远程版本比对 +- 通信机制:基于EventSource的标准化事件总线 + +## 5. 交互流程设计 +### 5.1 核心用户流程 +#### 5.1.1 开播流程 +1. 启动应用并完成用户认证 +2. 进入"直播管理"模块配置房间信息(封面/标题/分区) +3. 配置推流参数(OBS自动配置/手动输入RTMP信息) +4. 预览直播画面并点击"开始直播" +5. 直播中监控指标与互动情况 +6. 结束直播并生成数据报告 + +### 5.2 关键界面流转 +- **登录页**→**仪表盘**:完成认证后默认进入 +- **仪表盘**→**直播管理**:点击"开始直播"按钮 +- **直播管理**→**小程序中心**:直播中可随时添加功能 + +## 6. 非功能需求 +### 6.1 性能需求 +- 启动时间<10秒 +- 内存占用<200MB(空闲状态) +- CPU占用<10%(常规操作) +- 支持同时运行5个以上小程序 + +### 6.2 兼容性需求 +- 操作系统:Windows 10+/macOS 11+/Linux(Ubuntu 20.04+) +- 分辨率:最低支持1366×768 +- 推流软件:OBS 27.0+、Streamlabs OBS + +### 6.3 安全需求 +- **数据加密**: 所有本地存储的用户数据需采用AES-256加密 +- **权限控制**: 实现基于RBAC的权限管理系统 +- **安全审计**: 记录所有敏感操作日志,保存至少90天 +- **输入验证**: 对所有用户输入实施严格的验证和过滤 +- **第三方安全**: 第三方小程序运行在沙箱环境,限制系统资源访问 + +## 7. 验收标准 +### 7.1 功能验收标准 +- 核心功能覆盖率100% +- 小程序安装成功率>99% +- 推流连接稳定性>99.5% +- **测试方法**: + - 功能测试: 基于场景的黑盒测试,覆盖率≥95% + - 性能测试: 使用JMeter模拟1000条/秒弹幕负载 + - 安全测试: 进行OWASP Top 10漏洞扫描 + - 兼容性测试: 在Windows 10/11及macOS 12+环境验证 +- **通过标准**: 所有测试用例通过率≥98%,无严重缺陷 + +### 7.2 性能验收标准 +- 冷启动时间<10秒 +- 直播状态下CPU占用<15% +- 弹幕渲染延迟<300ms + +### 7.3 兼容性验收标准 +- 在3种主流操作系统上功能正常 +- 适配5种常见屏幕分辨率 + +## 8. 优先级与迭代计划 +### 8.1 MVP版本(V1.0) +- 核心功能模块开发完成 +- 支持基础弹幕管理 +- 实现小程序基础框架 + +### 8.2 迭代计划 +- V1.1: 增加数据分析功能 +- V1.2: 扩展小程序生态 +- V1.3: 性能优化与多平台适配 + +## 9. 风险评估 +### 9.1 风险评估 +- **技术风险**:Electron版本升级可能导致兼容性问题 + - 缓解策略: 建立版本兼容性测试矩阵,保留历史稳定版本回滚通道 +- **性能风险**:高并发弹幕处理可能导致界面卡顿 + - 缓解策略: 实现弹幕渲染优先级队列,非关键帧弹幕延迟渲染 +- **安全风险**:第三方小程序权限管理不当可能导致数据泄露 + - 缓解策略: 实施细粒度权限控制,敏感操作需用户二次确认 +- **依赖风险**:第三方API服务不稳定影响功能可用性 + - 缓解策略: 设计服务降级机制,关键数据本地缓存 +- **运营风险**:用户 adoption率低 + - 缓解策略: 提供交互式新手引导与详细功能教程 +- **生态风险**:小程序生态发展缓慢 + - 缓解策略: 推出开发者激励计划与完善的开发支持工具 \ No newline at end of file From 27a017f254b655f44073f544c7522ad44bcea023 Mon Sep 17 00:00:00 2001 From: FQZ <617564112@qq.com> Date: Wed, 27 Aug 2025 17:47:50 +0800 Subject: [PATCH 2/8] =?UTF-8?q?=E9=87=8D=E6=9E=84=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E5=88=92=E5=88=86=E4=B8=8E=E8=AF=A6=E7=BB=86?= =?UTF-8?q?=E4=BA=A4=E4=BA=92=E6=B5=81=E7=A8=8B=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对主客户端核心模块、用户认证、直播管理、数据分析、系统设置等章节进行了重写和细化,采用更结构化的操作逻辑、交互元素、界面效果和用户反馈描述。提升了文档的可读性和实现指导性,便于开发团队理解各模块的具体功能和交互流程。 --- ...00\346\261\202\346\226\207\346\241\243.md" | 551 ++++++++++-------- 1 file changed, 312 insertions(+), 239 deletions(-) diff --git "a/\351\234\200\346\261\202\346\226\207\346\241\243/\346\225\264\345\220\210\351\234\200\346\261\202\346\226\207\346\241\243.md" "b/\351\234\200\346\261\202\346\226\207\346\241\243/\346\225\264\345\220\210\351\234\200\346\261\202\346\226\207\346\241\243.md" index 555a9451..eb9283fd 100644 --- "a/\351\234\200\346\261\202\346\226\207\346\241\243/\346\225\264\345\220\210\351\234\200\346\261\202\346\226\207\346\241\243.md" +++ "b/\351\234\200\346\261\202\346\226\207\346\241\243/\346\225\264\345\220\210\351\234\200\346\261\202\346\226\207\346\241\243.md" @@ -431,245 +431,318 @@ ACFUN直播工具箱采用分层架构设计,整体风格为深色主题,符 5. 优化大流量场景下的UI表现(如高并发弹幕) ## 4. 功能模块划分 -### 4.1 应用主程序核心模块 +### 4.1 主客户端核心模块 #### 4.1.1 仪表盘模块 -**功能目标**:为主播提供直播全生命周期数据监控与高效操作入口 -**用户场景**:主播日常开播前准备、直播中监控、直播后复盘 -**核心功能点**: -- 个性化数据看板: - - 实时直播状态卡片: - - **状态定义**: - - 准备中(黄色):已配置房间信息但未开始推流,倒计时提示“距离计划开播时间还有XX分钟” - - 在线(绿色):推流正常,显示“直播中”标签及实时观看人数 - - 离线(灰色):未开播状态,显示上次直播时长及观众峰值 - - 异常(红色):推流中断/网络异常,显示错误码及“重新连接”按钮 - - **核心显示元素**: - - 状态指示灯(圆形,直径24px) - - 直播时长计时器(格式:HH:MM:SS) - - 实时观众数(含今日累计观看人次) - - 关键事件提示区(如“5分钟前达到今日观众峰值”) - - **交互规则**: - - 点击卡片展开详情面板(显示完整推流参数、网络状态) - - 双击卡片快速切换开播/关播状态 - - 悬停显示tooltip(当前码率、帧率、CPU占用) - - - 核心指标趋势图: - - **数据维度**: - - 观众指标:实时在线人数(折线)、累计观看人次(柱状)、平均观看时长(虚线) - - 互动指标:弹幕密度(条/分钟)、点赞率(互动人数/观看人数)、礼物触发率(礼物数/弹幕数) - - 收入指标:礼物总收入(折线)、付费转化率(付费用户数/观众数)、人均消费(总收入/付费用户数) - - **图表类型与布局**: - - 主图表区:7天趋势折线图(支持日/周/月切换),右侧Y轴双刻度(左侧人数/右侧金额) - - 数据对比区:今日vs昨日同期数据卡片(百分比变化带箭头指示) - - 异常标记:自动标记峰值/谷值点(如“20:00出现观众峰值,较前日增长35%”) - - **交互分析功能**: - - 时间粒度控制:支持15分钟/1小时/3小时数据聚合切换 - - 数据下钻:点击任意数据点显示该时段详细指标(如“19:00-20:00观众来源分布”) - - 对比分析:支持选择任意两天数据叠加显示,高亮差异区域 - - 导出功能:支持PNG图片导出和CSV数据下载 - - - 待办事项提醒(未配置推流参数、版本更新等) - - **优先级机制**: - - P0(阻断型):必须处理才能继续操作(如推流配置错误),需弹窗确认 - - P1(高优先级):影响直播体验但非阻断性(如网络带宽不足),顶部悬浮通知 - - P2(常规提醒):优化类建议(如功能使用技巧),折叠列表展示 - - **交互处理流程**: - - 右滑标记完成(带确认动画,自动同步至已完成列表) - - 长按唤起操作菜单(延期提醒/标记为已读/永久忽略) - - 点击直接跳转至对应处理页面(如点击“版本更新”打开更新中心) - - 批量处理功能(支持勾选多个事项一键标记完成) -- 快捷操作区: - **功能目标**:减少高频操作路径长度,支持个性化工作流定制 - **用户场景**: - - 新手主播:需要直观的核心功能入口,避免操作复杂度 - - 资深主播:需要高度定制化的快捷组合,提升操作效率 - - 多场景切换:根据直播状态(准备/直播中/复盘)自动调整可用功能 - **核心功能点**: - - 可配置快捷工具栏: - - **自定义布局**:支持拖拽调整按钮位置,最多显示8个常用功能 - - **分组管理**:可创建“开播准备组”、“直播控场组”等场景化按钮组 - - **大小切换**:提供紧凑模式(图标+文字)/极简模式(仅图标)切换 - - 智能场景化动作: - - **开播准备模式**:显示“房间设置→推流配置→封面上传→测试推流”流程化按钮 - - **直播中模式**:切换为“弹幕过滤→禁言管理→互动抽奖→紧急停播”控场工具 - - **复盘模式**:显示“数据报告→录屏回放→观众画像→下次预告”分析工具 - - 交互增强设计: - - **长按预览**:长按按钮显示功能说明及快捷键提示(如“房间设置 [F2]”) - - **操作反馈**:按钮点击带波纹动画,重要操作(如关播)有震动反馈 - - **快捷添加**:任意功能页面的“添加到快捷栏”选项,支持一键收藏 - -- 个性化配置: - - 支持自定义看板布局(拖拽调整组件位置) - - 可配置数据刷新频率(15s/30s/1min) - -#### 4.1.2 用户认证与授权模块 -- 多因子认证(密码+手机验证码+扫码登录) -- 账号安全保护(异地登录检测、登录异常提醒) - - 失败处理:5次密码错误后锁定30分钟,支持短信快速解锁 - - 会话管理:自动登出闲置2小时的会话,支持手动一键清除所有登录设备 - - 数据加密:传输层采用TLS 1.3加密,密码存储使用bcrypt算法加盐哈希 - - **异常处理机制**: - - 登录异常提醒:1分钟内连续失败3次,触发微信/短信双重告警 - - 可疑行为分析:检测到模拟器环境/代理IP登录时,强制启用最高级别验证 - - 应急登录通道:账号锁定后,可通过备用邮箱接收临时登录链接(有效期15分钟) - +- **欢迎信息展示** + - **操作逻辑流程**:用户登录成功后自动显示,根据当前时间和用户昵称动态生成个性化欢迎语 + - **交互元素定义**:页面顶部居中显示的文本区域,包含用户头像和昵称 + - **界面呈现效果**:大字号欢迎文本,配合用户头像,背景为半透明渐变效果 + - **用户操作反馈**:鼠标悬停时无特殊反馈,内容在用户重新登录或刷新页面时更新 + +- **核心数据概览** + - **操作逻辑流程**:页面加载时自动从后端获取最新直播数据,每5秒刷新一次 + - **交互元素定义**:数据卡片组件,包含观众数、礼物数、弹幕数等关键指标的文本和图标 + - **界面呈现效果**:多列网格布局的数据卡片,使用数字动画效果展示数据变化 + - **用户操作反馈**:数据更新时显示微妙的数字变化动画,点击卡片可跳转到对应数据分析详情页 + +- **背景视觉效果** + - **操作逻辑流程**:页面加载时自动启动,无需用户交互 + - **交互元素定义**:页面左侧底部固定区域的半透明动画图 + - **界面呈现效果**:低饱和度的动态背景图案,不影响前景内容的可读性 + - **用户操作反馈**:无直接用户操作,动画持续循环播放 + +#### 4.1.2 用户认证模块 +- **登录系统** + - **操作逻辑流程**: + 1. 应用启动时显示登录页面 + 2. 默认展示二维码登录方式 + 3. 用户可切换至账号密码登录或游客模式 + 4. 二维码登录:显示二维码 → 用户扫描 → 手机确认 → 登录成功 + 5. 账号密码登录:输入用户名密码 → 点击登录 → 验证成功 → 登录成功 + - **交互元素定义**: + - 二维码显示区域:居中显示动态刷新的登录二维码 + - 账号输入框:支持文本输入,带清空按钮 + - 密码输入框:支持密码输入,带显示/隐藏切换功能 + - 登录按钮:主操作按钮,点击后触发登录请求 + - 模式切换选项卡:用于在不同登录方式间切换 + - 游客模式入口:独立的链接或按钮 + - **界面呈现效果**:简洁的单页布局,中央聚焦登录功能,背景为品牌色渐变 + - **用户操作反馈**: + - 二维码有效期倒计时显示 + - 输入框焦点状态提示 + - 登录过程中显示加载动画 + - 登录失败时显示明确的错误提示 + +- **状态管理** + - **操作逻辑流程**: + 1. 登录成功后生成并存储会话令牌 + 2. 应用启动时自动检测会话有效性 + 3. 支持用户选择自动登录选项 + 4. 登出时清除会话数据 + - **交互元素定义**: + - 自动登录复选框:位于登录表单下方 + - 登出按钮:位于用户头像下拉菜单中 + - **界面呈现效果**:无直接界面呈现,为后台功能 + - **用户操作反馈**: + - 自动登录成功时直接进入主界面 + - 会话过期时自动跳转至登录页面并显示提示 + +- **权限控制** + - **操作逻辑流程**: + 1. 基于用户角色分配不同权限 + 2. 所有接口请求自动附加权限验证 + 3. 功能访问前进行权限检查 + - **交互元素定义**:无直接用户可见的交互元素 + - **界面呈现效果**:无权限功能在界面上隐藏或置灰 + - **用户操作反馈**:尝试访问无权限功能时显示权限不足提示 #### 4.1.3 直播管理模块 -**功能目标**:提供一站式直播全流程管理能力 -**用户场景**:开播前配置、直播中实时管理、紧急情况处理 -**核心功能点**: -- 房间配置中心: - - 基础信息设置(标题、封面、分区标签) - - 内容属性控制(直播类型标记、互动功能开关) - - 回放管理(自动开启回放、可见范围设置) - -- 推流控制: - - 基础协议支持(RTMP/HLS) - - 视频编码配置(H.264/AVC) - - 断线重连机制 - -- 观众互动管理: - - 弹幕过滤功能 - - 禁言管理工具 - - 礼物贡献榜展示 - - 申诉与审计: - - 违规记录存档:保留违规内容截图+处理时间+操作人,支持导出审计日志 - - 申诉通道:被处罚用户可提交申诉,主播收到申诉通知并处理 - - -#### 4.1.4 系统设置模块 -**功能目标**:提供灵活的系统级配置与个性化选项 -**用户场景**:首次使用初始化、性能优化、界面定制 -**核心功能点**: -- 界面个性化: - **功能目标**:提供高度可定制的界面体验,满足不同用户的视觉偏好和操作习惯 - **用户场景**: - - 新手用户:通过预设主题快速配置界面 - - 资深用户:精细化调整界面元素,创建个性化工作流 - - 特殊需求用户:通过辅助功能提升可访问性 - **核心功能点**: - - 主题系统: - - 预设主题库:提供商务蓝/活力橙/简约灰等5套主题,支持一键切换 - - 自定义主题编辑器:允许调整主色调/辅助色/中性色,实时预览效果 - - 主题同步:支持跨设备保存主题设置,与tdesign设计系统深度集成 - - 布局定制: - - 组件拖拽:支持核心面板(如直播控制台/数据看板)自由调整位置 - - 视图模式:提供紧凑/标准/宽松三种布局密度,适应不同屏幕尺寸 - - 自定义工作区:支持保存3套常用布局方案(如“直播中”/“数据分析”/“休息模式”) - - 排版设置: - - 字体控制:支持系统字体/自定义字体选择,提供微软雅黑/思源黑体等5种预设 - - 多级缩放:字号支持12px-20px无极调节(替代原5档固定缩放) - - 显示优化:可开启字体抗锯齿/字间距调整/行高设置(1.0-1.8倍) - - 快捷键体系: - - 全功能自定义:支持95%以上操作的快捷键重定义,含组合键设置 - - 场景化预设:提供“高效编辑”/“直播控场”等快捷键模板,一键应用 - - 冲突检测:自动识别快捷键冲突并提供替代方案建议 - - 辅助功能: - - 无障碍模式:支持高对比度显示(WCAG AA标准),屏幕阅读器兼容 - - 操作增强:可开启按键音效/震动反馈,提升操作确认感 - - 视觉提示:重要按钮高亮显示,危险操作二次确认弹窗 - -- 性能设置: - - 资源占用控制(小程序最大运行数量限制) - - 缓存管理(手动清理/自动清理策略) - - 启动项配置(开机自启/最小化到托盘) - -- 高级选项: - - 日志级别设置(调试/信息/警告/错误) - - API接口调试模式(供开发人员使用) - - 数据备份与恢复(配置信息导出/导入) - - -### 4.2 小程序系统模块 -#### 4.2.1 小程序管理中心 -- 小程序发现:分类展示、推荐列表、搜索功能 -- 生命周期管理:安装、更新、卸载、启用/禁用 -- 权限控制:小程序权限申请与管理 -- 性能监控:资源占用监控,异常行为检测 - -#### 4.2.2 小程序运行机制 -- 多显示支持:主窗口集成、独立窗口、OBS插件模式 -- 资源加载流程:目录扫描、入口文件解析、配置读取 -- 版本控制:本地缓存与远程版本比对 -- 通信机制:基于EventSource的标准化事件总线 - -## 5. 交互流程设计 -### 5.1 核心用户流程 -#### 5.1.1 开播流程 -1. 启动应用并完成用户认证 -2. 进入"直播管理"模块配置房间信息(封面/标题/分区) -3. 配置推流参数(OBS自动配置/手动输入RTMP信息) -4. 预览直播画面并点击"开始直播" -5. 直播中监控指标与互动情况 -6. 结束直播并生成数据报告 - -### 5.2 关键界面流转 -- **登录页**→**仪表盘**:完成认证后默认进入 -- **仪表盘**→**直播管理**:点击"开始直播"按钮 -- **直播管理**→**小程序中心**:直播中可随时添加功能 - -## 6. 非功能需求 -### 6.1 性能需求 -- 启动时间<10秒 -- 内存占用<200MB(空闲状态) -- CPU占用<10%(常规操作) -- 支持同时运行5个以上小程序 - -### 6.2 兼容性需求 -- 操作系统:Windows 10+/macOS 11+/Linux(Ubuntu 20.04+) -- 分辨率:最低支持1366×768 -- 推流软件:OBS 27.0+、Streamlabs OBS - -### 6.3 安全需求 -- **数据加密**: 所有本地存储的用户数据需采用AES-256加密 -- **权限控制**: 实现基于RBAC的权限管理系统 -- **安全审计**: 记录所有敏感操作日志,保存至少90天 -- **输入验证**: 对所有用户输入实施严格的验证和过滤 -- **第三方安全**: 第三方小程序运行在沙箱环境,限制系统资源访问 - -## 7. 验收标准 -### 7.1 功能验收标准 -- 核心功能覆盖率100% -- 小程序安装成功率>99% -- 推流连接稳定性>99.5% -- **测试方法**: - - 功能测试: 基于场景的黑盒测试,覆盖率≥95% - - 性能测试: 使用JMeter模拟1000条/秒弹幕负载 - - 安全测试: 进行OWASP Top 10漏洞扫描 - - 兼容性测试: 在Windows 10/11及macOS 12+环境验证 -- **通过标准**: 所有测试用例通过率≥98%,无严重缺陷 - -### 7.2 性能验收标准 -- 冷启动时间<10秒 -- 直播状态下CPU占用<15% -- 弹幕渲染延迟<300ms - -### 7.3 兼容性验收标准 -- 在3种主流操作系统上功能正常 -- 适配5种常见屏幕分辨率 - -## 8. 优先级与迭代计划 -### 8.1 MVP版本(V1.0) -- 核心功能模块开发完成 -- 支持基础弹幕管理 -- 实现小程序基础框架 - -### 8.2 迭代计划 -- V1.1: 增加数据分析功能 -- V1.2: 扩展小程序生态 -- V1.3: 性能优化与多平台适配 - -## 9. 风险评估 -### 9.1 风险评估 -- **技术风险**:Electron版本升级可能导致兼容性问题 - - 缓解策略: 建立版本兼容性测试矩阵,保留历史稳定版本回滚通道 -- **性能风险**:高并发弹幕处理可能导致界面卡顿 - - 缓解策略: 实现弹幕渲染优先级队列,非关键帧弹幕延迟渲染 -- **安全风险**:第三方小程序权限管理不当可能导致数据泄露 - - 缓解策略: 实施细粒度权限控制,敏感操作需用户二次确认 -- **依赖风险**:第三方API服务不稳定影响功能可用性 - - 缓解策略: 设计服务降级机制,关键数据本地缓存 -- **运营风险**:用户 adoption率低 - - 缓解策略: 提供交互式新手引导与详细功能教程 -- **生态风险**:小程序生态发展缓慢 - - 缓解策略: 推出开发者激励计划与完善的开发支持工具 \ No newline at end of file +- **房间管理** + - **操作逻辑流程**: + 1. 进入房间管理页面 + 2. 配置房间封面(上传图片)、标题(文本输入)、分区(下拉选择) + 3. 设置观众剪辑权限(开关) + 4. 管理管理员列表(添加/删除) + 5. 管理观众列表(禁言/踢出) + 6. 点击保存按钮提交修改 + - **交互元素定义**: + - 封面图片上传区域:支持拖拽上传和点击选择文件 + - 标题输入框:多行文本输入,带字数限制提示 + - 分区选择下拉框:支持搜索的分类选择组件 + - 权限开关:布尔值切换组件 + - 用户列表:表格形式展示,带操作按钮列 + - 添加/删除/保存按钮:标准操作按钮 + - **界面呈现效果**:表单布局,左侧为基础信息设置,右侧为权限管理 + - **用户操作反馈**: + - 保存成功/失败提示 + - 图片上传进度条 + - 操作按钮点击反馈 + +- **推流管理** + - **操作逻辑流程**: + 1. 进入推流管理页面 + 2. 系统自动生成RTMP服务器信息和直播码 + 3. 用户可选择OBS同步推流(自动配置OBS)或手动配置 + 4. 点击"开始推流"按钮启动直播 + 5. 推流过程中实时显示推流状态和数据 + 6. 点击"结束直播"按钮停止直播 + - **交互元素定义**: + - RTMP信息展示区:文本信息,带复制按钮 + - OBS同步开关:布尔值切换组件 + - 开始/结束推流按钮:状态切换按钮 + - 推流状态指示器:颜色变化的状态灯 + - 推流数据仪表盘:显示码率、延迟等关键指标 + - **界面呈现效果**:顶部为操作按钮区,中间为状态和信息展示区,底部为数据图表 + - **用户操作反馈**: + - 按钮状态变化(可用/禁用) + - 推流状态切换动画 + - 数据实时更新动画 + - 异常情况告警提示 + +- **直播信息展示** + - **操作逻辑流程**:直播开始后自动加载并实时更新 + - **交互元素定义**: + - 欢迎面板:位于直播界面顶部的动态文本 + - 直播状态统计:实时更新的数字和图表 + - **界面呈现效果**:覆盖在直播画面上方的半透明信息层,不影响直播观看 + - **用户操作反馈**:数据更新时的动画效果 + +#### 4.1.4 数据分析模块 +- **实时统计数据** + - **操作逻辑流程**:直播开始后自动收集数据,每3秒刷新一次 + - **交互元素定义**: + - 观看人数计数器:大字号数字显示 + - 礼物收入指示器:动态更新的金额显示 + - 弹幕数量统计:滚动更新的数字 + - **界面呈现效果**:位于主界面顶部的关键数据条,使用对比鲜明的颜色和字体 + - **用户操作反馈**:数据增长时的动画效果 + +- **观众行为分析** + - **操作逻辑流程**: + 1. 进入数据分析详情页 + 2. 选择时间范围和分析维度 + 3. 系统生成观众来源、观看时长、互动行为等分析图表 + - **交互元素定义**: + - 时间范围选择器:日期区间选择组件 + - 分析维度切换选项卡:用于在不同分析维度间切换 + - 数据图表:柱状图、折线图、饼图等可视化组件 + - **界面呈现效果**:多区域图表布局,支持数据筛选和排序 + - **用户操作反馈**: + - 图表加载动画 + - 数据筛选时的实时更新 + +- **礼物数据统计** + - **操作逻辑流程**:同观众行为分析 + - **交互元素定义**: + - 收入统计图表:显示礼物总收入趋势 + - 用户排行榜:表格形式展示贡献榜 + - 礼物类型分布图:饼图展示不同礼物类型占比 + - **界面呈现效果**:多图表组合布局,突出显示高价值数据 + - **用户操作反馈**:同观众行为分析 + +- **数据报表** + - **操作逻辑流程**: + 1. 直播结束后自动生成完播复盘报告 + 2. 用户可查看历史数据分析和趋势对比 + 3. 支持数据导出功能 + - **交互元素定义**: + - 报告列表:表格展示历史报告 + - 导出按钮:标准操作按钮 + - 详情查看链接:点击进入报告详情 + - **界面呈现效果**:简洁的报告列表页,点击后进入详细报告视图 + - **用户操作反馈**: + - 报告生成成功提示 + - 导出进度和完成提示 + +#### 4.1.5 小本本功能 + - **操作逻辑流程**: + 1. 进入小本本功能页面 + 2. 可添加、编辑、删除房管和黑名单用户 + 3. 支持搜索和筛选功能 + - **交互元素定义**: + - 用户列表:表格形式展示房管和黑名单用户 + - 添加/编辑/删除按钮:标准操作按钮 + - 搜索框:支持用户名搜索 + - **界面呈现效果**:简洁的表格布局,支持分页和排序 + - **用户操作反馈**: + - 操作成功/失败提示 + - 列表更新动画 + +#### 4.1.6 系统设置模块 +- **通用设置** + - **操作逻辑流程**: + 1. 进入系统设置页面 + 2. 在通用设置区域修改各项配置 + 3. 点击保存按钮应用更改 + - **交互元素定义**: + - 界面主题选择器:下拉菜单或单选按钮组 + - 语言选择器:下拉菜单 + - 路径输入框:支持手动输入和文件夹选择 + - 配置备份/恢复按钮:标准操作按钮 + - 通知设置开关:布尔值切换组件 + - **界面呈现效果**:分类清晰的表单布局,每类设置有明确的标题 + - **用户操作反馈**: + - 保存成功/失败提示 + - 配置更改后的即时预览(如主题切换) + +- **网络设置** + - **操作逻辑流程**: + 1. 在系统设置页面的网络设置区域修改配置 + 2. 可配置后端端口,查看网络状态和日志 + 3. 系统自动检测端口冲突 + - **交互元素定义**: + - 端口输入框:数字输入,带范围限制 + - 状态显示区:文本显示当前网络状态 + - 日志查看按钮:点击打开日志查看器 + - **界面呈现效果**:简洁的表单布局,突出显示当前状态信息 + - **用户操作反馈**: + - 端口冲突时的警告提示 + - 网络状态变化的实时更新 + +- **快捷键设置** + - **操作逻辑流程**: + 1. 在系统设置页面的快捷键设置区域配置 + 2. 点击输入框后按下想要设置的快捷键组合 + 3. 支持恢复默认快捷键 + - **交互元素定义**: + - 快捷键输入框:特殊输入组件,支持键盘事件捕获 + - 恢复默认按钮:标准操作按钮 + - **界面呈现效果**:列表形式展示各项功能及其快捷键 + - **用户操作反馈**: + - 快捷键冲突时的提示 + - 设置成功的确认信息 + +- **服务器配置** + - **操作逻辑流程**:系统根据当前环境自动选择服务器配置,一般无需用户手动修改 + - **交互元素定义**: + - 开发环境服务器地址:只读文本显示 + - 生产环境服务器地址:只读文本显示 + - **界面呈现效果**:简洁的信息展示,突出显示当前使用的服务器 + - **用户操作反馈**:无特殊用户操作反馈 + +- **网络检测** + - **操作逻辑流程**: + 1. 用户点击网络检测按钮 + 2. 系统执行网络连接测试 + 3. 显示检测结果 + - **交互元素定义**: + - 网络检测按钮:标准操作按钮 + - 检测结果显示区:文本和图标展示连接状态 + - **界面呈现效果**:位于网络设置区域的独立功能块 + - **用户操作反馈**: + - 检测过程中的加载动画 + - 检测结果的颜色编码提示(成功/失败) + +- **界面主题** + - **操作逻辑流程**: + 1. 在系统设置的通用设置中选择主题 + 2. 点击切换按钮或选择器应用新主题 + - **交互元素定义**: + - 主题选择器:下拉菜单或卡片选择组件 + - 明暗主题切换开关:布尔值切换组件 + - **界面呈现效果**:主题选择预览区域,实时展示主题效果 + - **用户操作反馈**:主题切换时的平滑过渡动画 + +- **语言设置** + - **操作逻辑流程**: + 1. 在系统设置的通用设置中选择语言 + 2. 点击应用按钮切换语言 + 3. 可能需要重启应用才能完全生效 + - **交互元素定义**: + - 语言选择器:下拉菜单 + - 应用按钮:标准操作按钮 + - **界面呈现效果**:语言列表,显示当前选中语言 + - **用户操作反馈**: + - 语言切换成功提示 + - 重启应用提示(如需) + +- **字体设置** + - **操作逻辑流程**: + 1. 在系统设置的通用设置中调整字体 + 2. 选择字体类型和大小 + 3. 点击应用按钮保存更改 + - **交互元素定义**: + - 字体类型选择器:下拉菜单 + - 字体大小调整滑块:范围选择组件 + - 应用按钮:标准操作按钮 + - **界面呈现效果**:字体预览区域,实时展示字体变化 + - **用户操作反馈**:字体调整时的实时预览更新 + +- **通知设置** + - **操作逻辑流程**: + 1. 在系统设置的通用设置中配置通知 + 2. 开启/关闭各类通知 + 3. 调整通知频率 + - **交互元素定义**: + - 通知类型开关:布尔值切换组件组 + - 频率调整滑块:范围选择组件 + - **界面呈现效果**:分类清晰的通知设置列表 + - **用户操作反馈**:设置更改后的即时保存提示 + +#### 4.1.7 错误处理模块 +- **错误页面展示** + - **操作逻辑流程**:系统遇到错误时自动跳转至错误页面 + - **交互元素定义**: + - 错误图标:直观的错误类型图标 + - 错误标题:简洁的错误类型描述 + - 错误详情:可选展开/收起的详细错误信息 + - **界面呈现效果**:居中对齐的错误信息展示,背景为轻微的错误主题色 + - **用户操作反馈**:无特殊用户操作反馈 + +- **错误信息反馈** + - **操作逻辑流程**:错误发生时自动收集和展示错误信息 + - **交互元素定义**: + - 错误描述文本:清晰的错误原因说明 + - 导航建议:提供可能的解决方案或下一步操作建议 + - **界面呈现效果**:错误页面的核心内容区,使用易读的字体和颜色 + - **用户操作反馈**:无特殊用户操作反馈 + +- **返回机制** + - **操作逻辑流程**:用户点击返回按钮回到上一页或主界面 + - **交互元素定义**: + - 返回按钮:标准的返回图标和文本按钮 + - 主页按钮:可选的主页快捷入口 + - **界面呈现效果**:位于错误页面底部或顶部的操作按钮区 + - **用户操作反馈**: + - 按钮点击反馈 + - 页面跳转过渡动画 \ No newline at end of file From 01a51714d03222cc7d45f026dba18d570f05940e Mon Sep 17 00:00:00 2001 From: qq617564112 <617564112@qq.com> Date: Thu, 28 Aug 2025 14:53:45 +0800 Subject: [PATCH 3/8] Refactor app structure and add new features Reorganized modules by moving AppManager to modules, added DashboardModule and live.js for dashboard and live APIs, and removed the old AppManager. Enhanced httpApi with user authentication, RTMP config, OBS status, and mini-program management endpoints. Updated AcfunDanmuModule and LiveManagementModule for RTMP/OBS integration and improved stream key handling. Added QuickToolbar and theme management to the renderer, improved Dashboard and Login pages with new data visualizations and authentication logic. --- packages/main/src/apis/httpApi.ts | 120 +++++-- packages/main/src/apis/live.js | 36 +++ packages/main/src/managers/AppManager.ts | 155 --------- packages/main/src/modules/AcfunDanmuModule.ts | 109 +++++++ packages/main/src/modules/AppManager.ts | 109 +++++++ packages/main/src/modules/DashboardModule.js | 217 +++++++++++++ .../main/src/modules/LiveManagementModule.ts | 77 ++++- packages/renderer/src/App.vue | 2 + .../renderer/src/components/QuickToolbar.vue | 124 ++++++++ packages/renderer/src/components/TitleBar.vue | 9 +- packages/renderer/src/composables/useTheme.ts | 50 +++ packages/renderer/src/pages/Dashboard.vue | 89 +++++- .../renderer/src/pages/LiveManagement.vue | 18 +- packages/renderer/src/pages/Login.vue | 299 ++++++++++++++++-- .../renderer/src/pages/MiniProgramCenter.vue | 179 +++++++++++ .../src/pages/MiniProgramManagement.vue | 146 +++++++-- packages/renderer/src/pages/Settings.vue | 79 ++++- packages/renderer/src/style.css | 13 +- 18 files changed, 1571 insertions(+), 260 deletions(-) create mode 100644 packages/main/src/apis/live.js delete mode 100644 packages/main/src/managers/AppManager.ts create mode 100644 packages/main/src/modules/AppManager.ts create mode 100644 packages/main/src/modules/DashboardModule.js create mode 100644 packages/renderer/src/components/QuickToolbar.vue create mode 100644 packages/renderer/src/composables/useTheme.ts create mode 100644 packages/renderer/src/pages/MiniProgramCenter.vue diff --git a/packages/main/src/apis/httpApi.ts b/packages/main/src/apis/httpApi.ts index 13e88dcb..1aded833 100644 --- a/packages/main/src/apis/httpApi.ts +++ b/packages/main/src/apis/httpApi.ts @@ -57,12 +57,44 @@ export function initializeHttpApi() { // 健康检查接口 - router.get('/health', errorHandler(async (req: Request, res: Response) => { - res.json>({ - success: true, - data: { status: 'ok', timestamp: new Date().toISOString() } - }); - })); +router.get('/health', errorHandler(async (req: Request, res: Response) => { + res.json>({ + success: true, + data: { status: 'ok', timestamp: new Date().toISOString() } + }); +})); + +// 用户认证相关接口 +const validateLogin = (req: Request): ValidationError[] => { + const errors: ValidationError[] = []; + if (!req.body.username) errors.push({ field: 'username', message: '用户名必填' }); + if (!req.body.password) errors.push({ field: 'password', message: '密码必填' }); + return errors; +}; + +// 用户登录 +router.post('/auth/login', validateRequest([validateLogin]), errorHandler(async (req: Request, res: Response) => { + const { username, password } = req.body; + const token = await acfunDanmuModule.login(username, password); + res.json>({ success: true, data: { token } }); +})); + +// 用户注销 +router.post('/auth/logout', errorHandler(async (req: Request, res: Response) => { + const { token } = req.body; + await acfunDanmuModule.logout(token); + res.json>({ success: true, data: {} }); +})); + +// 验证用户令牌 +router.get('/auth/verify', errorHandler(async (req: Request, res: Response) => { + const token = req.headers.authorization?.split(' ')[1]; + if (!token) { + return res.status(401).json({ success: false, error: '未提供令牌' }); + } + const isValid = await acfunDanmuModule.verifyToken(token); + res.json>({ success: true, data: { valid: isValid } }); +})); // 窗口关闭验证函数 const validateWindowClose = (req: Request): ValidationError[] => { @@ -209,27 +241,7 @@ export function initializeHttpApi() { res.json>({ success: true, data: {} }); })); - // 数据分析模块API -router.get('/analytics/live-stats', async (req, res) => { - try { - const stats = await appManager.getLiveStatistics(); - res.json({ success: true, data: stats }); - } catch (error) { - res.status(500).json({ success: false, error: error.message }); - } -}); - -router.get('/analytics/audience', async (req, res) => { - try { - const audienceData = await appManager.getAudienceAnalysis(); - res.json({ success: true, data: audienceData }); - } catch (error) { - res.status(500).json({ success: false, error: error.message }); - } -}); - -router.get('/analytics/gifts', async (req, res) => { -// 快捷键设置相关接口 + // 快捷键设置相关接口 router.get('/settings/shortcuts', async (req, res) => { try { const shortcuts = await appManager.getShortcuts(); @@ -391,6 +403,38 @@ router.get('/stream/status', async (req, res) => { } }); +// 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 { @@ -435,6 +479,28 @@ router.get('/mini-programs/status', async (req, res) => { } }); +// 小程序安装接口 +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) => { diff --git a/packages/main/src/apis/live.js b/packages/main/src/apis/live.js new file mode 100644 index 00000000..502df607 --- /dev/null +++ b/packages/main/src/apis/live.js @@ -0,0 +1,36 @@ +const { ipcMain } = require('electron'); +const ac = require('acfundanmu'); + +/** + * 封面上传API实现 + * @param {Object} params - 请求参数 + * @param {Buffer} params.cover - 封面图片Buffer数据 + * @returns {Promise} 上传结果 + */ +async function uploadCover(params) { + try { + // 调用acfundanmu SDK的图片上传方法 + const result = await ac.uploadImage(params.cover); + return { + success: true, + data: { + coverUrl: result.url + } + }; + } catch (error) { + console.error('封面上传失败:', error); + return { + success: false, + error: error.message + }; + } +} + +// 注册IPC处理函数 +ipcMain.handle('live:uploadCover', async (event, params) => { + return uploadCover(params); +}); + +module.exports = { + uploadCover +}; \ No newline at end of file diff --git a/packages/main/src/managers/AppManager.ts b/packages/main/src/managers/AppManager.ts deleted file mode 100644 index 770289ca..00000000 --- a/packages/main/src/managers/AppManager.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { singleton } from 'tsyringe'; -import { acfunLiveAPI } from 'acfundanmu'; -import { ConfigManager } from '../utils/ConfigManager'; -import logger from '../utils/logger'; - -@singleton() -export class AppManager { - private configManager: ConfigManager; - private lastGiftStatsUpdate: number = 0; - private giftStatsCache: any = null; - private lastAudienceUpdate: number = 0; - private audienceCache: any = null; - private readonly CACHE_DURATION = 5 * 60 * 1000; // 5分钟缓存 - - constructor() { - this.configManager = new ConfigManager(); - } - - /** - * 获取详细观众分析数据 - */ - async getAudienceAnalysis(roomId: number): Promise { - const now = Date.now(); - // 检查缓存 - if (this.audienceCache && now - this.lastAudienceUpdate < this.CACHE_DURATION) { - return this.audienceCache; - } - - try { - // 调用ACFUN API获取详细观众数据 - const audienceData = await acfunLiveAPI.getAudienceDetail(roomId); - // 处理并缓存数据 - this.audienceCache = this.processAudienceData(audienceData); - this.lastAudienceUpdate = now; - return this.audienceCache; - } catch (error) { - logger.error('获取观众分析数据失败:', error); - // 返回缓存数据或空对象 - return this.audienceCache || { regions: {}, devices: {}, activeTimes: {} }; - } - } - - /** - * 处理观众数据分析 - */ - private processAudienceData(rawData: any): any { - // 实现观众地域、设备、活跃度等维度分析 - const regions = this.aggregateByRegion(rawData.viewers); - const devices = this.aggregateByDevice(rawData.viewers); - const activeTimes = this.aggregateActiveTime(rawData.viewers); - - return { - totalViewers: rawData.totalViewers || 0, - newViewers: rawData.newViewers || 0, - returningViewers: rawData.returningViewers || 0, - regions, - devices, - activeTimes, - topViewers: rawData.topViewers?.slice(0, 10) || [] - }; - } - - /** - * 按地域聚合观众数据 - */ - private aggregateByRegion(viewers: any[]): Record { - const regions: Record = {}; - viewers.forEach(viewer => { - const region = viewer.region || '未知'; - regions[region] = (regions[region] || 0) + 1; - }); - // 排序并返回前10个地域 - return Object.entries(regions) - .sort(([, a], [, b]) => b - a) - .slice(0, 10) - .reduce((obj, [k, v]) => ({ ...obj, [k]: v }), {}); - } - - /** - * 按设备类型聚合观众数据 - */ - private aggregateByDevice(viewers: any[]): Record { - const devices: Record = {}; - viewers.forEach(viewer => { - const device = viewer.device || '未知设备'; - devices[device] = (devices[device] || 0) + 1; - }); - return devices; - } - - /** - * 分析观众活跃时间段 - */ - private aggregateActiveTime(viewers: any[]): Record { - const activeTimes: Record = {}; - viewers.forEach(viewer => { - if (viewer.activeTime) { - const hour = new Date(viewer.activeTime).getHours(); - const hourRange = `${hour}:00-${hour+1}:00`; - activeTimes[hourRange] = (activeTimes[hourRange] || 0) + 1; - } - }); - return activeTimes; - } - - /** - * 获取礼物统计数据 - */ - async getGiftStatistics(roomId: number, days: number = 7): Promise { - const now = Date.now(); - // 检查缓存 - if (this.giftStatsCache && now - this.lastGiftStatsUpdate < this.CACHE_DURATION) { - return this.giftStatsCache; - } - - try { - // 调用ACFUN API获取礼物数据 - const giftData = await acfunLiveAPI.getGiftStats(roomId, days); - // 处理并缓存数据 - this.giftStatsCache = this.processGiftData(giftData); - this.lastGiftStatsUpdate = now; - return this.giftStatsCache; - } catch (error) { - logger.error('获取礼物统计数据失败:', error); - // 返回缓存数据或空对象 - return this.giftStatsCache || { total: 0, topGifts: [], dailyTrend: [] }; - } - } - - /** - * 处理礼物数据 - */ - private processGiftData(rawData: any): any { - // 计算总收益 - const total = rawData.items?.reduce((sum: number, item: any) => sum + (item.amount || 0) * (item.price || 0), 0) || 0; - // 获取Top礼物 - const topGifts = (rawData.items || []) - .sort((a: any, b: any) => (b.amount || 0) - (a.amount || 0)) - .slice(0, 10); - // 生成每日趋势 - const dailyTrend = rawData.dailyData?.map((day: any) => ({ - date: day.date, - amount: day.totalAmount || 0, - count: day.giftCount || 0 - })) || []; - - return { - total, - topGifts, - dailyTrend, - totalGifts: rawData.totalGifts || 0, - uniqueUsers: rawData.uniqueUsers || 0 - }; - } -} \ No newline at end of file diff --git a/packages/main/src/modules/AcfunDanmuModule.ts b/packages/main/src/modules/AcfunDanmuModule.ts index fe3068f0..5552de8d 100644 --- a/packages/main/src/modules/AcfunDanmuModule.ts +++ b/packages/main/src/modules/AcfunDanmuModule.ts @@ -5,6 +5,7 @@ import path from 'path'; import { app } from 'electron'; import { getPackageJson } from '../utils/Devars.js'; import { getLogManager } from '../utils/LogManager.js'; +import WebSocket from 'ws'; // 定义配置接口 interface AcfunDanmuConfig { @@ -24,6 +25,8 @@ const DEFAULT_CONFIG: AcfunDanmuConfig = { export class AcfunDanmuModule implements AppModule { private process: ChildProcess | null = null; + private obsWebSocket: WebSocket | null = null; + private obsConnectionStatus: 'disconnected' | 'connecting' | 'connected' = 'disconnected'; private config: AcfunDanmuConfig; private logCallback: ((message: string, type: 'info' | 'error') => void) | null = null; private logManager: ReturnType; @@ -319,6 +322,112 @@ export class AcfunDanmuModule implements AppModule { ); } + // RTMP地址管理 + async saveRtmpConfig(roomId: number, rtmpUrl: string, streamKey: string): Promise { + return this.callAcfunDanmuApi( + `/stream/saveRtmpConfig`, + 'POST', + { roomId, rtmpUrl, streamKey } + ); + } + + async getRtmpConfig(roomId: number): Promise { + return this.callAcfunDanmuApi( + `/stream/getRtmpConfig`, + 'GET', + { roomId } + ); + } + + // OBS连接状态监控 + async getObsConnectionStatus(roomId: number): Promise { + return this.callAcfunDanmuApi( + `/stream/obsStatus`, + 'GET', + { roomId } + ); + } + + // RTMP配置管理 + async saveRtmpConfig(roomId: number, rtmpUrl: string, streamKey: string): Promise { + try { + const rtmpConfigs = this.configManager.readConfig().rtmpConfigs || {}; + rtmpConfigs[roomId] = { rtmpUrl, streamKey, updatedAt: new Date().toISOString() }; + this.configManager.writeConfig({ rtmpConfigs }); + return true; + } catch (error) { + this.logManager.addLog('AcfunDanmuModule', `Failed to save RTMP config: ${error.message}`, 'error'); + return false; + } + } + + async getRtmpConfig(roomId: number): Promise<{rtmpUrl: string, streamKey: string} | null> { + try { + const rtmpConfigs = this.configManager.readConfig().rtmpConfigs || {}; + return rtmpConfigs[roomId] || null; + } catch (error) { + this.logManager.addLog('AcfunDanmuModule', `Failed to get RTMP config: ${error.message}`, 'error'); + return null; + } + } + + // OBS连接管理 + private setupOBSWebSocket(obsHost: string, obsPort: number, password: string): void { + const wsUrl = `ws://${obsHost}:${obsPort}/ws`; + this.obsWebSocket = new WebSocket(wsUrl); + this.obsConnectionStatus = 'connecting'; + + this.obsWebSocket.on('open', () => { + this.logManager.addLog('AcfunDanmuModule', 'OBS WebSocket connected', 'info'); + this.obsConnectionStatus = 'connected'; + // 发送认证请求 + this.obsWebSocket?.send(JSON.stringify({ + "op": 1, + "d": { + "rpcVersion": 1, + "authentication": password + } + })); + }); + + this.obsWebSocket.on('close', () => { + this.logManager.addLog('AcfunDanmuModule', 'OBS WebSocket disconnected', 'info'); + this.obsConnectionStatus = 'disconnected'; + // 自动重连逻辑 + setTimeout(() => this.setupOBSWebSocket(obsHost, obsPort, password), 5000); + }); + + this.obsWebSocket.on('error', (error) => { + this.logManager.addLog('AcfunDanmuModule', `OBS WebSocket error: ${error.message}`, 'error'); + this.obsConnectionStatus = 'disconnected'; + }); + + this.obsWebSocket.on('message', (data) => { + const message = JSON.parse(data.toString()); + // 处理OBS事件通知 + if (message.op === 5) { + this.logManager.addLog('AcfunDanmuModule', `OBS event: ${message.d.eventType}`, 'info'); + // 可扩展处理具体事件(如流状态变化) + } + }); + } + + connectToOBS(obsHost: string = 'localhost', obsPort: number = 4455, password: string = ''): void { + this.setupOBSWebSocket(obsHost, obsPort, password); + } + + disconnectFromOBS(): void { + if (this.obsWebSocket) { + this.obsWebSocket.close(); + this.obsWebSocket = null; + this.obsConnectionStatus = 'disconnected'; + } + } + + getOBSConnectionStatus(): 'disconnected' | 'connecting' | 'connected' { + return this.obsConnectionStatus; + } + // 推流管理相关方法 async startStream(roomId: number, streamKey: string, quality: string): Promise { return this.callAcfunDanmuApi( diff --git a/packages/main/src/modules/AppManager.ts b/packages/main/src/modules/AppManager.ts new file mode 100644 index 00000000..f2c9a55a --- /dev/null +++ b/packages/main/src/modules/AppManager.ts @@ -0,0 +1,109 @@ +import { AppModule } from '../AppModule.js'; +import { ModuleContext } from '../ModuleContext.js'; +import { ConfigManager } from '../utils/ConfigManager.js'; +import { LogManager } from '../utils/LogManager.js'; +import fs from 'fs'; +import path from 'path'; +import { app } from 'electron'; + +export class AppManager implements AppModule { + private configManager: ConfigManager; + private logManager: LogManager; + private miniPrograms: Map = new Map(); + + constructor() { + this.configManager = globalThis.configManager; + this.logManager = globalThis.logManager; + } + + // 小程序安装方法 + async installMiniProgram(packageUrl: string, version: string): Promise { + try { + this.logManager.addLog('AppManager', `Installing mini-program from ${packageUrl}`, 'info'); + const response = await fetch(packageUrl); + const manifest = await response.json(); + + const installPath = path.join(app.getPath('userData'), 'mini-programs', manifest.appId); + fs.mkdirSync(installPath, { recursive: true }); + + // 下载小程序包 + const packageResponse = await fetch(manifest.downloadUrl); + const buffer = await packageResponse.arrayBuffer(); + fs.writeFileSync(path.join(installPath, 'package.zip'), Buffer.from(buffer)); + + // 记录已安装小程序 + const installedPrograms = this.configManager.readConfig().miniPrograms || []; + installedPrograms.push({ + appId: manifest.appId, + name: manifest.name, + version, + path: installPath, + lastUpdated: new Date().toISOString() + }); + + this.configManager.writeConfig({ miniPrograms: installedPrograms }); + this.logManager.addLog('AppManager', `Successfully installed mini-program: ${manifest.name}`, 'info'); + return true; + } catch (error) { + this.logManager.addLog('AppManager', `Failed to install mini-program: ${error.message}`, 'error'); + return false; + } + } + + // 小程序更新方法 + async updateMiniProgram(appId: string): Promise { + try { + const installedPrograms = this.configManager.readConfig().miniPrograms || []; + const program = installedPrograms.find(p => p.appId === appId); + if (!program) throw new Error('小程序未安装'); + + // 获取最新版本信息 + const manifestUrl = `${program.path}/manifest.json`; + const response = await fetch(manifestUrl); + const manifest = await response.json(); + + // 比较版本号 + if (manifest.version === program.version) return true; + + // 执行更新 + return this.installMiniProgram(manifestUrl, manifest.version); + } catch (error) { + this.logManager.addLog('AppManager', `Failed to update mini-program: ${error.message}`, 'error'); + return false; + } + } + + // 获取小程序市场应用列表 + async getMarketplaceApps(): Promise { + try { + this.logManager.addLog('AppManager', 'Fetching marketplace apps', 'info'); + const response = await fetch('https://api.example.com/mini-programs/marketplace'); + if (!response.ok) { + throw new Error(`Marketplace API request failed: ${response.statusText}`); + } + return await response.json(); + } catch (error) { + this.logManager.addLog('AppManager', `Failed to fetch marketplace apps: ${error.message}`, 'error'); + return []; + } + } + + // 小程序状态管理 + async getMiniProgramStatus(appId: string): Promise { + return this.miniPrograms.has(appId) ? 'running' : 'not_installed'; + } + + async enable({ app }: ModuleContext): Promise { + this.logManager.addLog('AppManager', 'Enabling AppManager module', 'info'); + // 加载已安装的小程序 + const installedPrograms = this.configManager.readConfig().miniPrograms || []; + installedPrograms.forEach(program => { + this.miniPrograms.set(program.appId, program); + }); + } + + async disable(): Promise { + this.logManager.addLog('AppManager', 'Disabling AppManager module', 'info'); + this.miniPrograms.clear(); + } +} \ No newline at end of file diff --git a/packages/main/src/modules/DashboardModule.js b/packages/main/src/modules/DashboardModule.js new file mode 100644 index 00000000..2cd036d0 --- /dev/null +++ b/packages/main/src/modules/DashboardModule.js @@ -0,0 +1,217 @@ +const { acfunLive } = require('acfundanmu'); +const { dataManager } = require('../utils/DataManager'); + +class DashboardModule { + constructor() { + this.liveClient = acfunLive; + this.statsCache = null; + this.trendCache = null; + this.giftCache = null; + this.cacheDuration = 30000; // 30秒缓存 + } + + /** + * 获取直播统计数据 + */ + async getStats() { + // 检查缓存是否有效 + if (this.statsCache && Date.now() - this.statsCache.timestamp < this.cacheDuration) { + return this.statsCache.data; + } + + try { + // 从acfunLive API获取数据 + const liveInfo = await this.liveClient.getLiveInfo(); + const giftStats = await this.liveClient.getGiftStats(); + const viewerData = await this.liveClient.getViewerStats(); + + // 处理并整合数据 + const stats = { + viewerCount: viewerData.onlineUsers || 0, + likeCount: liveInfo.likeCount || 0, + bananaCount: giftStats.bananaCount || 0, + acCoinCount: giftStats.acCoinCount || 0, + peakViewerCount: viewerData.peakOnlineUsers || 0, + averageViewerCount: viewerData.averageOnlineUsers || 0, + newFollowerCount: liveInfo.newFollowerCount || 0 + }; + + // 更新缓存 + this.statsCache = { + data: stats, + timestamp: Date.now() + }; + + return stats; + } catch (error) { + console.error('获取统计数据失败:', error); + // 缓存为空时才返回默认值 + if (!this.statsCache) { + return { + viewerCount: 0, + likeCount: 0, + bananaCount: 0, + acCoinCount: 0, + peakViewerCount: 0, + averageViewerCount: 0, + newFollowerCount: 0 + }; + } + return this.statsCache.data; + } + } + + /** + * 获取观众趋势数据 + */ + async getAudienceTrend() { + if (this.trendCache && Date.now() - this.trendCache.timestamp < this.cacheDuration) { + return this.trendCache.data; + } + + try { + // 获取最近12个时间点的数据 + const trendData = await this.liveClient.getAudienceTrend({ + interval: '5m', + count: 12 + }); + + // 格式化数据为前端需要的格式 + const formattedData = trendData.map(item => ({ + time: new Date(item.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), + viewers: item.userCount + })); + + this.trendCache = { + data: formattedData, + timestamp: Date.now() + }; + + return formattedData; + } catch (error) { + console.error('获取观众趋势数据失败:', error); + // 生成模拟数据 + if (!this.trendCache) { + const mockData = []; + const now = new Date(); + for (let i = 11; i >= 0; i--) { + const time = new Date(now - i * 5 * 60 * 1000); + mockData.push({ + time: time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), + viewers: Math.floor(Math.random() * 100) + 10 + }); + } + this.trendCache = { + data: mockData, + timestamp: Date.now() + }; + } + return this.trendCache.data; + } + } + + /** + * 获取礼物统计数据 + */ + async getGiftTrend() { + // 模拟礼物趋势数据 + return this.generateTimeSeriesData(24, 100, 500); + } + + async getAudienceBehavior() { + // 模拟观众行为数据 + return Array.from({length: 12}, (_, i) => ({ + time: `${i+8}:00`, + viewers: Math.floor(Math.random() * 500) + 100, + interactions: Math.floor(Math.random() * 200) + 50, + conversion: (Math.random() * 10).toFixed(2) + })); + } + + async getGiftStats() { + if (this.giftCache && Date.now() - this.giftCache.timestamp < this.cacheDuration) { + return this.giftCache.data; + } + + try { + const giftData = await this.liveClient.getGiftRank(); + + // 取前5名礼物 + const topGifts = giftData.slice(0, 5).map(gift => ({ + name: gift.name, + value: gift.count + })); + + this.giftCache = { + data: topGifts, + timestamp: Date.now() + }; + + return topGifts; + } catch (error) { + console.error('获取礼物统计数据失败:', error); + // 生成模拟数据 + if (!this.giftCache) { + this.giftCache = { + data: [ + { name: '香蕉', value: 120 }, + { name: 'AC币', value: 85 }, + { name: '辣条', value: 60 }, + { name: '火箭', value: 15 }, + { name: '飞机', value: 8 } + ], + timestamp: Date.now() + }; + } + return this.giftCache.data; + } + } + + /** + * 获取动态内容块 + */ + async getDynamicBlocks() { + try { + // 从数据管理器获取配置的动态内容块 + const blocks = await dataManager.get('dashboard_blocks', [ + { + title: '直播公告', + type: 'string', + content: '欢迎使用ACFUN直播工具箱!这是您的直播数据仪表盘,实时展示直播关键指标。' + }, + { + title: '今日任务', + type: 'list', + content: [ + '完成直播时长2小时', + '获得100个点赞', + '新增5名粉丝' + ] + } + ]); + + return blocks; + } catch (error) { + console.error('获取动态内容块失败:', error); + // 返回默认内容块 + return [ + { + title: '直播公告', + type: 'string', + content: '欢迎使用ACFUN直播工具箱!这是您的直播数据仪表盘,实时展示直播关键指标。' + }, + { + title: '今日任务', + type: 'list', + content: [ + '完成直播时长2小时', + '获得100个点赞', + '新增5名粉丝' + ] + } + ]; + } + } +} + +module.exports = DashboardModule; \ No newline at end of file diff --git a/packages/main/src/modules/LiveManagementModule.ts b/packages/main/src/modules/LiveManagementModule.ts index 50e3f10d..95e4b588 100644 --- a/packages/main/src/modules/LiveManagementModule.ts +++ b/packages/main/src/modules/LiveManagementModule.ts @@ -3,6 +3,7 @@ import { getLogManager } from '../utils/LogManager.js'; import { AppModule } from '../AppModule.js'; import { ModuleContext } from '../ModuleContext.js'; import OBSWebSocket from 'obs-websocket-js'; +import fetch from 'node-fetch'; // 直播管理模块配置接口 export interface LiveManagementConfig { @@ -157,8 +158,8 @@ private obsStatus: OBSStatus = 'offline'; // 实际应用中,这里应该调用API获取真实推流码 // 如果没有推流码,则生成一个模拟的 if (!this.config.streamKey) { - this.config.streamKey = this.generateMockStreamKey(); - } + this.config.streamKey = await this.fetchStreamKeyFromAPI(); + } return { server: this.config.rtmpServer || 'rtmp://push.acfun.cn/live', key: this.config.streamKey @@ -176,7 +177,7 @@ private obsStatus: OBSStatus = 'offline'; async refreshStreamKey(): Promise<{ server: string; streamKey: string }> { try { // 实际应用中,这里应该调用API刷新推流码 - this.config.streamKey = this.generateMockStreamKey(); + this.config.streamKey = await this.fetchStreamKeyFromAPI(); this.logger.info('Stream key refreshed'); return { server: this.config.rtmpServer || 'rtmp://push.acfun.cn/live', @@ -192,6 +193,39 @@ private obsStatus: OBSStatus = 'offline'; * 连接OBS * @returns 是否成功 */ + private async fetchStreamKeyFromAPI(attempt = 0): Promise { + try { + if (!this.config.roomId) { + throw new Error('Room ID is not configured'); + } + + // 调用真实API获取推流码 + const response = await fetch(`https://api.acfun.cn/v2/rooms/${this.config.roomId}/stream-key`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${global.authToken}` + } + }); + + if (!response.ok) { + throw new Error(`Failed to fetch stream key: ${response.statusText}`); + } + + const data = await response.json(); + this.reconnectAttempts = 0; // 重置重试计数器 + return data.streamKey; + } catch (error) { + if (attempt < this.maxReconnectAttempts) { + const delay = Math.min(1000 * Math.pow(2, attempt), this.maxReconnectDelay); + this.logger.warn(`Stream key fetch failed, retrying in ${delay}ms (attempt ${attempt + 1})`); + await new Promise(resolve => setTimeout(resolve, delay)); + return this.fetchStreamKeyFromAPI(attempt + 1); + } + this.logger.error('Max retries reached for stream key fetch'); + throw error; + } + } + async connectOBS(): Promise { try { this.obsStatus = 'connecting'; @@ -221,6 +255,43 @@ 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 + } + }); + + // 开始推流 + 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状态 * @returns OBS状态对象 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 @@