Spaces:
Paused
Paused
| const express = require('express'); | |
| const router = express.Router(); | |
| const { SECRET, statsRecorder, statsQuery, aiSummary } = require('../index'); | |
| // ==================== 辅助函数 ==================== | |
| /** | |
| * 将环境变量字符串转换为布尔值 | |
| * @param {string|boolean|undefined} value - 环境变量值 | |
| * @param {boolean} defaultValue - 默认值 | |
| * @returns {boolean} 布尔值 | |
| */ | |
| function parseBoolean(value, defaultValue = false) { | |
| // 如果值为 undefined、null 或空字符串,返回默认值 | |
| if (value === undefined || value === null || value === '') { | |
| return defaultValue; | |
| } | |
| // 如果已经是布尔值,直接返回 | |
| if (typeof value === 'boolean') { | |
| return value; | |
| } | |
| // 如果是字符串,转换为小写后判断 | |
| if (typeof value === 'string') { | |
| const str = value.toLowerCase().trim(); | |
| return str === 'true' || str === '1' || str === 'yes' || str === 'on'; | |
| } | |
| // 如果是数字,非0为true | |
| if (typeof value === 'number') { | |
| return value !== 0; | |
| } | |
| // 其他情况返回默认值 | |
| return defaultValue; | |
| } | |
| /** | |
| * 检查是否允许访问汇总数据 | |
| * @param {string} deviceId - 设备ID | |
| * @returns {boolean} 是否允许访问 | |
| */ | |
| function isSummaryAllowed(deviceId) { | |
| if (deviceId !== 'summary') { | |
| return true; | |
| } | |
| return parseBoolean(process.env.WEB_SUMMARY); | |
| } | |
| /** | |
| * 获取客户端IP地址 | |
| */ | |
| function getClientIp(req) { | |
| // 优先从X-Forwarded-For获取(适用于反向代理场景) | |
| const forwarded = req.headers['x-forwarded-for']; | |
| if (forwarded) { | |
| return typeof forwarded === 'string' | |
| ? forwarded.split(',')[0].trim() | |
| : forwarded[0].trim(); | |
| } | |
| // 如果没有代理,直接使用connection的remoteAddress | |
| return req.connection?.remoteAddress || req.socket?.remoteAddress || req.ip; | |
| } | |
| // ==================== API路由 ==================== | |
| // 应用上报API | |
| router.post('/', async (req, res) => { | |
| const { secret, device, app_name, running, batteryLevel, isCharging } = req.body; | |
| if (secret !== SECRET) { | |
| return res.status(401).json({ error: 'Invalid secret' }); | |
| } | |
| if (!device) { | |
| return res.status(400).json({ error: 'Missing device' }); | |
| } | |
| try { | |
| // 1. 处理电池信息 | |
| if (batteryLevel !== undefined && batteryLevel > 0 && batteryLevel <= 100) { | |
| const chargingStatus = isCharging === true; | |
| statsRecorder.recordBattery(device, batteryLevel, chargingStatus); | |
| } | |
| // 2. 处理应用信息 | |
| if (app_name !== undefined || running !== undefined) { | |
| // 校验应用信息的完整性 | |
| if (running !== false && !app_name) { | |
| return res.status(400).json({ | |
| error: 'Missing app_name when running is true' | |
| }); | |
| } | |
| await statsRecorder.recordUsage(device, app_name, running); | |
| } | |
| // 返回成功响应 | |
| res.json({ | |
| success: true, | |
| batteryInfo: statsRecorder.getLatestBatteryInfo(device), | |
| timestamp: new Date() | |
| }); | |
| } catch (error) { | |
| console.error('Record error:', error); | |
| res.status(500).json({ | |
| error: 'Database error', | |
| details: error.message | |
| }); | |
| } | |
| }); | |
| // 获取设备列表 | |
| router.get('/devices', async (req, res) => { | |
| try { | |
| const devices = await statsQuery.getDevices(); | |
| res.json(devices); | |
| } catch (error) { | |
| res.status(500).json({ error: 'Database error' }); | |
| } | |
| }); | |
| // 获取所有设备的全部切换记录(测试用) | |
| router.get('/recentall', (req, res) => { | |
| try { | |
| // 将Map转换为数组形式 | |
| const allRecords = {}; | |
| statsRecorder.recentAppSwitches.forEach((switches, deviceId) => { | |
| allRecords[deviceId] = switches.map(entry => ({ | |
| appName: entry.appName, | |
| timestamp: entry.timestamp, | |
| running: entry.running !== false | |
| })); | |
| }); | |
| res.json({ | |
| success: true, | |
| data: allRecords, | |
| count: statsRecorder.recentAppSwitches.size | |
| }); | |
| } catch (error) { | |
| res.status(500).json({ | |
| error: 'Server error', | |
| details: error.message | |
| }); | |
| } | |
| }); | |
| // 获取特定设备应用切换记录 | |
| router.get('/recent/:deviceId', async (req, res) => { | |
| try { | |
| const deviceId = req.params.deviceId; | |
| let records = []; | |
| if (statsRecorder.recentAppSwitches.has(deviceId)) { | |
| const switchEntries = statsRecorder.recentAppSwitches.get(deviceId) | |
| // 转换为所需格式 | |
| records = switchEntries.map(entry => ({ | |
| appName: entry.appName, | |
| timestamp: entry.timestamp, | |
| running: entry.running !== false | |
| })); | |
| } | |
| res.json({ | |
| success: true, | |
| data: records, | |
| count: records.length | |
| }); | |
| } catch (error) { | |
| res.status(500).json({ | |
| success: false, | |
| error: 'Server error', | |
| details: error.message | |
| }); | |
| } | |
| }); | |
| // 获取当天统计数据(支持全部设备) | |
| router.get('/stats/:deviceId', async (req, res) => { | |
| try { | |
| const deviceId = req.params.deviceId; | |
| // 检查是否允许访问汇总数据 | |
| if (!isSummaryAllowed(deviceId)) { | |
| return res.status(403).json({ error: 'Summary is disabled' }); | |
| } | |
| // 解析日期参数(如果有的话) | |
| let date; | |
| if (req.query.date) { | |
| // 支持 YYYY-MM-DD 格式或完整日期字符串 | |
| date = new Date(req.query.date); | |
| if (isNaN(date.getTime())) { | |
| return res.status(400).json({ | |
| error: 'Invalid date format. Please use YYYY-MM-DD format.' | |
| }); | |
| } | |
| } else { | |
| date = new Date(); | |
| } | |
| let stats; | |
| if (deviceId === 'summary') { | |
| stats = await statsQuery.getDailyStatsForAllDevices(date); | |
| } else { | |
| stats = await statsQuery.getDailyStats(deviceId, date); | |
| } | |
| if (!stats || stats.totalUsage === 0) { | |
| return res.json({ | |
| totalUsage: 0, | |
| appStats: {}, | |
| hourlyStats: Array(24).fill(0), | |
| appHourlyStats: {} | |
| }); | |
| } | |
| res.json(stats); | |
| } catch (error) { | |
| console.error('Error in /api/stats/:deviceId:', error); | |
| res.status(500).json({ error: 'Database error', details: error.message }); | |
| } | |
| }); | |
| // 获取周统计数据(支持全部设备)- 后端处理时区 | |
| router.get('/weekly/:deviceId', async (req, res) => { | |
| try { | |
| const deviceId = req.params.deviceId; | |
| // 检查是否允许访问汇总数据 | |
| if (!isSummaryAllowed(deviceId)) { | |
| return res.status(403).json({ error: 'Summary is disabled' }); | |
| } | |
| const weekOffset = parseInt(req.query.weekOffset) || 0; | |
| const appName = req.query.appName || null; | |
| let stats; | |
| if (deviceId === 'summary') { | |
| stats = await statsQuery.getWeeklyAppStatsForAllDevices(appName, weekOffset); | |
| } else { | |
| stats = await statsQuery.getWeeklyAppStats(deviceId, appName, weekOffset); | |
| } | |
| res.json(stats); | |
| } catch (error) { | |
| console.error('Error in /api/weekly/:deviceId:', error); | |
| res.status(500).json({ error: 'Database error', details: error.message }); | |
| } | |
| }); | |
| // 获取月统计数据(支持全部设备)- 后端处理时区 | |
| router.get('/monthly/:deviceId', async (req, res) => { | |
| try { | |
| const deviceId = req.params.deviceId; | |
| // 检查是否允许访问汇总数据 | |
| if (!isSummaryAllowed(deviceId)) { | |
| return res.status(403).json({ error: 'Summary is disabled' }); | |
| } | |
| const monthOffset = parseInt(req.query.monthOffset) || 0; | |
| const appName = req.query.appName || null; | |
| let stats; | |
| if (deviceId === 'summary') { | |
| stats = await statsQuery.getMonthlyAppStatsForAllDevices(appName, monthOffset); | |
| } else { | |
| stats = await statsQuery.getMonthlyAppStats(deviceId, appName, monthOffset); | |
| } | |
| res.json(stats); | |
| } catch (error) { | |
| console.error('Error in /api/monthly/:deviceId:', error); | |
| res.status(500).json({ error: 'Database error', details: error.message }); | |
| } | |
| }); | |
| // ==================== AI总结相关API ==================== | |
| // 获取最近一次AI总结(无需验证,只读操作) | |
| router.get('/ai/summary/:deviceId', (req, res) => { | |
| try { | |
| const deviceId = req.params.deviceId; | |
| const summary = aiSummary.getRecentSummary(deviceId); | |
| if (!summary) { | |
| return res.status(404).json({ | |
| success: false, | |
| error: 'No recent summary found for this device', | |
| message: '该设备暂无AI总结记录' | |
| }); | |
| } | |
| res.json({ | |
| success: true, | |
| deviceId, | |
| ...summary | |
| }); | |
| } catch (error) { | |
| console.error('Error in /api/ai/summary/:deviceId:', error); | |
| res.status(500).json({ | |
| error: 'Failed to retrieve summary', | |
| details: error.message | |
| }); | |
| } | |
| }); | |
| // 获取所有设备的最近总结(无需验证,只读操作) | |
| router.get('/ai/summaries', (req, res) => { | |
| try { | |
| const summaries = aiSummary.getAllRecentSummaries(); | |
| res.json({ | |
| success: true, | |
| count: Object.keys(summaries).length, | |
| summaries | |
| }); | |
| } catch (error) { | |
| console.error('Error in /api/ai/summaries:', error); | |
| res.status(500).json({ | |
| error: 'Failed to retrieve summaries', | |
| details: error.message | |
| }); | |
| } | |
| }); | |
| // 手动触发AI总结生成 (GET方式,需要secret验证) | |
| router.get('/ai/trigger/:deviceId', async (req, res) => { | |
| try { | |
| // 验证secret | |
| const { secret, date } = req.query; | |
| if (!secret || secret !== SECRET) { | |
| return res.status(401).json({ | |
| success: false, | |
| error: 'Invalid or missing secret' | |
| }); | |
| } | |
| const deviceId = req.params.deviceId; | |
| const result = await aiSummary.triggerSummary(deviceId, { | |
| date: date || null | |
| }); | |
| res.json(result); | |
| } catch (error) { | |
| console.error('Error in /api/ai/trigger/:deviceId:', error); | |
| res.status(500).json({ | |
| error: 'AI summary generation failed', | |
| details: error.message | |
| }); | |
| } | |
| }); | |
| // 获取AI总结状态(无需验证,只读操作) | |
| router.get('/ai/status', (req, res) => { | |
| res.json({ | |
| enabled: aiSummary.enabled, | |
| aiConfigured: !!aiSummary.aiConfig.apiKey, | |
| publishEnabled: aiSummary.publishConfig.publishEnabled, | |
| cronJobsCount: aiSummary.cronJobs.length, | |
| schedules: aiSummary.getScheduleStrings(), | |
| model: aiSummary.aiConfig.model, | |
| defaultTimezone: `UTC${aiSummary.scheduleConfig.timezoneOffset >= 0 ? '+' : ''}${aiSummary.scheduleConfig.timezoneOffset}` | |
| }); | |
| }); | |
| // 预留:周总结API | |
| router.post('/ai/weekly/:deviceId', async (req, res) => { | |
| res.status(501).json({ | |
| error: 'Weekly summary not implemented yet', | |
| message: '周总结功能将在后续版本中实现' | |
| }); | |
| }); | |
| // 预留:月总结API | |
| router.post('/ai/monthly/:deviceId', async (req, res) => { | |
| res.status(501).json({ | |
| error: 'Monthly summary not implemented yet', | |
| message: '月总结功能将在后续版本中实现' | |
| }); | |
| }); | |
| // 获取客户端IP地址 | |
| router.get('/ip', (req, res) => { | |
| const clientIp = getClientIp(req); | |
| res.json({ ip: clientIp }); | |
| }); | |
| // ==================== 页面组件配置 API ==================== | |
| /** | |
| * 获取页面组件显示配置 | |
| */ | |
| router.get('/pageConfig', (req, res) => { | |
| try { | |
| const config = { | |
| WEB_DEVICE_COUNT: parseBoolean(process.env.WEB_DEVICE_COUNT, true), | |
| WEB_COMMENT: parseBoolean(process.env.WEB_COMMENT, true), | |
| WEB_AI_SUMMARY: parseBoolean(process.env.AI_SUMMARY_ENABLED, true), | |
| WEB_SUMMARY: parseBoolean(process.env.WEB_SUMMARY, true), | |
| GISCUS_REPO: process.env.GISCUS_REPO || '', | |
| GISCUS_REPOID: process.env.GISCUS_REPOID || '', | |
| GISCUS_CATEGORY: process.env.GISCUS_CATEGORY || '', | |
| GISCUS_CATEGORYID: process.env.GISCUS_CATEGORYID || '', | |
| GISCUS_MAPPING: process.env.GISCUS_MAPPING || 'pathname', | |
| GISCUS_REACTIONSENABLED: parseBoolean(process.env.GISCUS_REACTIONSENABLED, true), | |
| GISCUS_EMITMETADATA: parseBoolean(process.env.GISCUS_EMITMETADATA, false), | |
| GISCUS_INPUTPOSITION: process.env.GISCUS_INPUTPOSITION || 'bottom', | |
| GISCUS_THEME: process.env.GISCUS_THEME || 'light', | |
| GISCUS_LANG: process.env.GISCUS_LANG || 'zh-CN' | |
| }; | |
| const tzOffset = parseInt(process.env.DEFAULT_TIMEZONE_OFFSET ?? '8') | |
| res.json({ | |
| success: true, | |
| config, | |
| tzOffset | |
| }); | |
| } catch (error) { | |
| console.error('获取页面配置错误:', error); | |
| res.status(500).json({ | |
| success: false, | |
| message: '获取配置失败', | |
| details: error.message | |
| }); | |
| } | |
| }); | |
| module.exports = router; |