import { logger } from "./chunk-47DJ6YUB.js"; import { NOTEBOOK_ROOT, asyncHandler, createApiModule, defineApiModule, defineEndpoints, successResponse } from "./chunk-74TMTGBG.js"; // shared/modules/time-tracking/api.ts var TIME_TRACKING_ENDPOINTS = defineEndpoints({ current: { path: "/current", method: "GET" }, event: { path: "/event", method: "POST" }, day: { path: "/day/:date", method: "GET" }, week: { path: "/week/:startDate", method: "GET" }, month: { path: "/month/:yearMonth", method: "GET" }, year: { path: "/year/:year", method: "GET" }, stats: { path: "/stats", method: "GET" } }); // shared/modules/time-tracking/index.ts var TIME_TRACKING_MODULE = defineApiModule({ id: "time-tracking", name: "\u65F6\u95F4\u7EDF\u8BA1", basePath: "/time", order: 20, version: "1.0.0", endpoints: TIME_TRACKING_ENDPOINTS }); // shared/utils/tabType.ts var KNOWN_MODULE_IDS = [ "home", "settings", "search", "weread", "recycle-bin", "todo", "time-tracking", "pydemos" ]; function getTabTypeFromPath(filePath) { if (!filePath) return "other"; if (filePath.startsWith("file-transfer-panel")) { return "file-transfer"; } if (filePath.startsWith("remote-git://")) { return "remote-git"; } if (filePath.startsWith("remote-desktop://")) { return "remote-desktop"; } if (filePath.startsWith("remote-") && filePath !== "remote-tab") { return "remote-desktop"; } if (filePath === "remote-tab" || filePath === "remote") { return "remote"; } for (const moduleId of KNOWN_MODULE_IDS) { if (filePath === `${moduleId}-tab` || filePath === moduleId) { if (moduleId === "home" || moduleId === "settings" || moduleId === "search" || moduleId === "weread") { return "other"; } return moduleId; } } if (filePath.endsWith(".md")) { return "markdown"; } return "other"; } function getFileNameFromPath(filePath) { if (!filePath) return "\u672A\u77E5"; if (filePath.startsWith("file-transfer-panel")) { const params = new URLSearchParams(filePath.split("?")[1] || ""); const deviceName = params.get("device") || ""; return deviceName ? `\u6587\u4EF6\u4F20\u8F93 - ${deviceName}` : "\u6587\u4EF6\u4F20\u8F93"; } for (const moduleId of KNOWN_MODULE_IDS) { if (filePath === `${moduleId}-tab` || filePath === moduleId) { const names = { "home": "\u9996\u9875", "settings": "\u8BBE\u7F6E", "search": "\u641C\u7D22", "weread": "\u5FAE\u4FE1\u8BFB\u4E66", "recycle-bin": "\u56DE\u6536\u7AD9", "todo": "TODO", "time-tracking": "\u65F6\u95F4\u7EDF\u8BA1", "pydemos": "Python Demo", "remote": "\u8FDC\u7A0B\u684C\u9762" }; return names[moduleId] ?? moduleId; } } const parts = filePath.split("/"); return parts[parts.length - 1] || filePath; } // api/modules/time-tracking/heartbeatService.ts var DEFAULT_HEARTBEAT_INTERVAL = 6e4; var HeartbeatService = class { interval = null; lastHeartbeat = /* @__PURE__ */ new Date(); intervalMs; callback = null; constructor(intervalMs = DEFAULT_HEARTBEAT_INTERVAL) { this.intervalMs = intervalMs; } setCallback(callback) { this.callback = callback; } start() { if (this.interval) { this.stop(); } this.interval = setInterval(async () => { if (this.callback) { try { this.lastHeartbeat = /* @__PURE__ */ new Date(); await this.callback(); } catch (err) { logger.error("Heartbeat callback failed:", err); } } }, this.intervalMs); this.lastHeartbeat = /* @__PURE__ */ new Date(); } stop() { if (this.interval) { clearInterval(this.interval); this.interval = null; } } isRunning() { return this.interval !== null; } getLastHeartbeat() { return this.lastHeartbeat; } updateHeartbeat() { this.lastHeartbeat = /* @__PURE__ */ new Date(); } getState() { return { lastHeartbeat: this.lastHeartbeat, isRunning: this.isRunning() }; } restoreState(state) { this.lastHeartbeat = new Date(state.lastHeartbeat); } }; var createHeartbeatService = (intervalMs) => { return new HeartbeatService(intervalMs); }; // api/modules/time-tracking/sessionPersistence.ts import fs from "fs/promises"; import path from "path"; var TIME_ROOT = path.join(NOTEBOOK_ROOT, "time"); var getDayFilePath = (year, month, day) => { const monthStr = month.toString().padStart(2, "0"); const dayStr = day.toString().padStart(2, "0"); return path.join(TIME_ROOT, year.toString(), `${year}${monthStr}${dayStr}.json`); }; var getMonthFilePath = (year, month) => { const monthStr = month.toString().padStart(2, "0"); return path.join(TIME_ROOT, year.toString(), `${year}${monthStr}.json`); }; var getYearFilePath = (year) => { return path.join(TIME_ROOT, "summary", `${year}.json`); }; var ensureDirExists = async (filePath) => { const dir = path.dirname(filePath); await fs.mkdir(dir, { recursive: true }); }; var createEmptyDayData = (year, month, day) => ({ date: `${year}-${month.toString().padStart(2, "0")}-${day.toString().padStart(2, "0")}`, totalDuration: 0, sessions: [], tabSummary: {}, lastUpdated: (/* @__PURE__ */ new Date()).toISOString() }); var createEmptyMonthData = (year, month) => ({ year, month, days: {}, monthlyTotal: 0, averageDaily: 0, activeDays: 0, lastUpdated: (/* @__PURE__ */ new Date()).toISOString() }); var createEmptyYearData = (year) => ({ year, months: {}, yearlyTotal: 0, averageMonthly: 0, averageDaily: 0, totalActiveDays: 0 }); var SessionPersistenceService = class { stateFilePath; constructor() { this.stateFilePath = path.join(TIME_ROOT, ".current-session.json"); } async loadCurrentState() { try { const content = await fs.readFile(this.stateFilePath, "utf-8"); const state = JSON.parse(content); return { session: state.session || null, currentTabRecord: state.currentTabRecord || null, isPaused: state.isPaused || false, lastHeartbeat: state.lastHeartbeat || (/* @__PURE__ */ new Date()).toISOString() }; } catch (err) { logger.debug("No existing session to load or session file corrupted"); return { session: null, currentTabRecord: null, isPaused: false, lastHeartbeat: (/* @__PURE__ */ new Date()).toISOString() }; } } async saveCurrentState(state) { await ensureDirExists(this.stateFilePath); await fs.writeFile(this.stateFilePath, JSON.stringify({ session: state.session, currentTabRecord: state.currentTabRecord, isPaused: state.isPaused, lastHeartbeat: state.lastHeartbeat }), "utf-8"); } async clearCurrentState() { try { await fs.unlink(this.stateFilePath); } catch (err) { logger.debug("Session state file already removed or does not exist"); } } async saveSessionToDay(session) { const startTime = new Date(session.startTime); const year = startTime.getFullYear(); const month = startTime.getMonth() + 1; const day = startTime.getDate(); const filePath = getDayFilePath(year, month, day); await ensureDirExists(filePath); let dayData = await this.getDayData(year, month, day); dayData.sessions.push(session); dayData.totalDuration += session.duration; for (const record of session.tabRecords) { const key = record.filePath || record.fileName; if (!dayData.tabSummary[key]) { dayData.tabSummary[key] = { fileName: record.fileName, tabType: record.tabType, totalDuration: 0, focusCount: 0 }; } dayData.tabSummary[key].totalDuration += record.duration; dayData.tabSummary[key].focusCount += record.focusedPeriods.length; } dayData.lastUpdated = (/* @__PURE__ */ new Date()).toISOString(); await fs.writeFile(filePath, JSON.stringify(dayData, null, 2), "utf-8"); await this.updateMonthSummary(year, month, day, session.duration); await this.updateYearSummary(year, month, session.duration); } async getDayData(year, month, day) { const filePath = getDayFilePath(year, month, day); try { const content = await fs.readFile(filePath, "utf-8"); return JSON.parse(content); } catch (err) { return createEmptyDayData(year, month, day); } } async getMonthData(year, month) { const filePath = getMonthFilePath(year, month); try { const content = await fs.readFile(filePath, "utf-8"); const data = JSON.parse(content); data.activeDays = Object.values(data.days).filter((d) => d.totalDuration > 0).length; return data; } catch (err) { return createEmptyMonthData(year, month); } } async getYearData(year) { const filePath = getYearFilePath(year); try { const content = await fs.readFile(filePath, "utf-8"); const data = JSON.parse(content); data.totalActiveDays = Object.values(data.months).filter((m) => m.totalDuration > 0).length; return data; } catch (err) { return createEmptyYearData(year); } } async updateDayDataRealtime(year, month, day, session, currentTabRecord) { const filePath = getDayFilePath(year, month, day); await ensureDirExists(filePath); let dayData = await this.getDayData(year, month, day); const currentSessionDuration = session.tabRecords.reduce((sum, r) => sum + r.duration, 0) + (currentTabRecord?.duration || 0); const existingSessionIndex = dayData.sessions.findIndex((s) => s.id === session.id); const realtimeSession = { ...session, duration: currentSessionDuration, tabRecords: currentTabRecord ? [...session.tabRecords, currentTabRecord] : session.tabRecords }; if (existingSessionIndex >= 0) { const oldDuration = dayData.sessions[existingSessionIndex].duration; dayData.sessions[existingSessionIndex] = realtimeSession; dayData.totalDuration = dayData.totalDuration - oldDuration + currentSessionDuration; } else { dayData.sessions.push(realtimeSession); dayData.totalDuration += currentSessionDuration; } dayData.tabSummary = {}; for (const s of dayData.sessions) { for (const record of s.tabRecords) { const key = record.filePath || record.fileName; if (!dayData.tabSummary[key]) { dayData.tabSummary[key] = { fileName: record.fileName, tabType: record.tabType, totalDuration: 0, focusCount: 0 }; } dayData.tabSummary[key].totalDuration += record.duration; dayData.tabSummary[key].focusCount += record.focusedPeriods.length; } } dayData.lastUpdated = (/* @__PURE__ */ new Date()).toISOString(); await fs.writeFile(filePath, JSON.stringify(dayData, null, 2), "utf-8"); return dayData; } async updateMonthSummary(year, month, day, duration) { const filePath = getMonthFilePath(year, month); await ensureDirExists(filePath); let monthData = await this.getMonthData(year, month); const dayStr = day.toString().padStart(2, "0"); if (!monthData.days[dayStr]) { monthData.days[dayStr] = { totalDuration: 0, sessions: 0, topTabs: [] }; } monthData.days[dayStr].totalDuration += duration; monthData.days[dayStr].sessions += 1; monthData.monthlyTotal += duration; monthData.activeDays = Object.values(monthData.days).filter((d) => d.totalDuration > 0).length; monthData.averageDaily = monthData.activeDays > 0 ? Math.floor(monthData.monthlyTotal / monthData.activeDays) : 0; monthData.lastUpdated = (/* @__PURE__ */ new Date()).toISOString(); await fs.writeFile(filePath, JSON.stringify(monthData, null, 2), "utf-8"); } async updateYearSummary(year, month, duration) { const filePath = getYearFilePath(year); await ensureDirExists(filePath); let yearData = await this.getYearData(year); const monthStr = month.toString().padStart(2, "0"); if (!yearData.months[monthStr]) { yearData.months[monthStr] = { totalDuration: 0, activeDays: 0 }; } yearData.months[monthStr].totalDuration += duration; yearData.yearlyTotal += duration; yearData.totalActiveDays = Object.values(yearData.months).reduce((sum, m) => { const hasActiveDays = m.totalDuration > 0 ? 1 : 0; return sum + hasActiveDays; }, 0); const activeMonthCount = Object.values(yearData.months).filter((m) => m.totalDuration > 0).length; yearData.averageMonthly = activeMonthCount > 0 ? Math.floor(yearData.yearlyTotal / activeMonthCount) : 0; yearData.averageDaily = yearData.totalActiveDays > 0 ? Math.floor(yearData.yearlyTotal / yearData.totalActiveDays) : 0; await fs.writeFile(filePath, JSON.stringify(yearData, null, 2), "utf-8"); } async recalculateMonthSummary(year, month, todayDuration) { const monthFilePath = getMonthFilePath(year, month); await ensureDirExists(monthFilePath); let monthData = await this.getMonthData(year, month); const dayStr = (/* @__PURE__ */ new Date()).getDate().toString().padStart(2, "0"); if (!monthData.days[dayStr]) { monthData.days[dayStr] = { totalDuration: 0, sessions: 0, topTabs: [] }; } const oldDayDuration = monthData.days[dayStr].totalDuration; monthData.days[dayStr].totalDuration = todayDuration; monthData.monthlyTotal = monthData.monthlyTotal - oldDayDuration + todayDuration; monthData.activeDays = Object.values(monthData.days).filter((d) => d.totalDuration > 0).length; monthData.averageDaily = monthData.activeDays > 0 ? Math.floor(monthData.monthlyTotal / monthData.activeDays) : 0; monthData.lastUpdated = (/* @__PURE__ */ new Date()).toISOString(); await fs.writeFile(monthFilePath, JSON.stringify(monthData, null, 2), "utf-8"); } async recalculateYearSummary(year) { const yearFilePath = getYearFilePath(year); await ensureDirExists(yearFilePath); let yearData = await this.getYearData(year); const monthStr = ((/* @__PURE__ */ new Date()).getMonth() + 1).toString().padStart(2, "0"); const monthFilePath = getMonthFilePath(year, (/* @__PURE__ */ new Date()).getMonth() + 1); try { const monthContent = await fs.readFile(monthFilePath, "utf-8"); const monthData = JSON.parse(monthContent); if (!yearData.months[monthStr]) { yearData.months[monthStr] = { totalDuration: 0, activeDays: 0 }; } const oldMonthTotal = yearData.months[monthStr].totalDuration; yearData.months[monthStr].totalDuration = monthData.monthlyTotal; yearData.months[monthStr].activeDays = monthData.activeDays; yearData.yearlyTotal = yearData.yearlyTotal - oldMonthTotal + monthData.monthlyTotal; yearData.totalActiveDays = Object.values(yearData.months).reduce((sum, m) => { const hasActiveDays = m.totalDuration > 0 ? 1 : 0; return sum + hasActiveDays; }, 0); const activeMonthCount = Object.values(yearData.months).filter((m) => m.totalDuration > 0).length; yearData.averageMonthly = activeMonthCount > 0 ? Math.floor(yearData.yearlyTotal / activeMonthCount) : 0; yearData.averageDaily = yearData.totalActiveDays > 0 ? Math.floor(yearData.yearlyTotal / yearData.totalActiveDays) : 0; } catch (err) { logger.debug("Month file not found for year summary calculation"); } await fs.writeFile(yearFilePath, JSON.stringify(yearData, null, 2), "utf-8"); } }; var createSessionPersistence = () => { return new SessionPersistenceService(); }; // api/modules/time-tracking/timeService.ts var generateId = () => { return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; }; var TimeTrackerService = class _TimeTrackerService { currentSession = null; currentTabRecord = null; isPaused = false; todayDuration = 0; _initialized = false; static _initializationPromise = null; heartbeatService; persistence; constructor(dependencies) { this.heartbeatService = dependencies.heartbeatService; this.persistence = dependencies.persistence; } static async create(config) { const heartbeatService = createHeartbeatService(config?.heartbeatIntervalMs); const persistence = createSessionPersistence(); const instance = new _TimeTrackerService({ heartbeatService, persistence }); await instance.initialize(); return instance; } static async createWithDependencies(dependencies) { const instance = new _TimeTrackerService(dependencies); await instance.initialize(); return instance; } async initialize() { if (this._initialized) { return; } if (_TimeTrackerService._initializationPromise) { await _TimeTrackerService._initializationPromise; return; } _TimeTrackerService._initializationPromise = this.loadCurrentState(); await _TimeTrackerService._initializationPromise; this._initialized = true; _TimeTrackerService._initializationPromise = null; this.heartbeatService.setCallback(async () => { if (this.currentSession && !this.isPaused) { try { this.heartbeatService.updateHeartbeat(); await this.updateCurrentTabDuration(); await this.saveCurrentState(); await this.updateTodayDataRealtime(); } catch (err) { logger.error("Heartbeat update failed:", err); } } }); } ensureInitialized() { if (!this._initialized) { throw new Error("TimeTrackerService \u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u4F7F\u7528 TimeTrackerService.create() \u521B\u5EFA\u5B9E\u4F8B"); } } async loadCurrentState() { const now = /* @__PURE__ */ new Date(); const todayData = await this.persistence.getDayData(now.getFullYear(), now.getMonth() + 1, now.getDate()); this.todayDuration = todayData.totalDuration; const state = await this.persistence.loadCurrentState(); if (state.session && state.session.status === "active") { const sessionStart = new Date(state.session.startTime); const now2 = /* @__PURE__ */ new Date(); if (now2.getTime() - sessionStart.getTime() < 24 * 60 * 60 * 1e3) { this.currentSession = state.session; this.isPaused = state.isPaused; if (state.currentTabRecord) { this.currentTabRecord = state.currentTabRecord; } this.heartbeatService.restoreState({ lastHeartbeat: state.lastHeartbeat }); } else { await this.endSession(); } } } async saveCurrentState() { await this.persistence.saveCurrentState({ session: this.currentSession, currentTabRecord: this.currentTabRecord, isPaused: this.isPaused, lastHeartbeat: this.heartbeatService.getLastHeartbeat().toISOString() }); } async startSession() { if (this.currentSession && this.currentSession.status === "active") { return this.currentSession; } const now = /* @__PURE__ */ new Date(); this.currentSession = { id: generateId(), startTime: now.toISOString(), duration: 0, status: "active", tabRecords: [] }; this.isPaused = false; this.heartbeatService.updateHeartbeat(); this.heartbeatService.start(); await this.saveCurrentState(); return this.currentSession; } async pauseSession() { if (!this.currentSession || this.isPaused) return; this.isPaused = true; await this.updateCurrentTabDuration(); await this.saveCurrentState(); } async resumeSession() { if (!this.currentSession || !this.isPaused) return; this.isPaused = false; this.heartbeatService.updateHeartbeat(); if (this.currentTabRecord) { const now = /* @__PURE__ */ new Date(); const timeStr = `${now.getHours().toString().padStart(2, "0")}:${now.getMinutes().toString().padStart(2, "0")}:${now.getSeconds().toString().padStart(2, "0")}`; this.currentTabRecord.focusedPeriods.push({ start: timeStr, end: timeStr }); } await this.saveCurrentState(); } async endSession() { if (!this.currentSession) return; this.heartbeatService.stop(); await this.updateCurrentTabDuration(); const now = /* @__PURE__ */ new Date(); this.currentSession.endTime = now.toISOString(); this.currentSession.status = "ended"; const startTime = new Date(this.currentSession.startTime); this.currentSession.duration = Math.floor((now.getTime() - startTime.getTime()) / 1e3); await this.persistence.saveSessionToDay(this.currentSession); this.todayDuration += this.currentSession.duration; this.currentSession = null; this.currentTabRecord = null; this.isPaused = false; await this.persistence.clearCurrentState(); } async updateCurrentTabDuration() { if (!this.currentSession || !this.currentTabRecord) return; const now = /* @__PURE__ */ new Date(); const periods = this.currentTabRecord.focusedPeriods; if (periods.length > 0) { const lastPeriod = periods[periods.length - 1]; const [h, m, s] = lastPeriod.start.split(":").map(Number); const startSeconds = h * 3600 + m * 60 + s; const currentSeconds = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds(); this.currentTabRecord.duration = currentSeconds - startSeconds; lastPeriod.end = `${now.getHours().toString().padStart(2, "0")}:${now.getMinutes().toString().padStart(2, "0")}:${now.getSeconds().toString().padStart(2, "0")}`; } } async updateTodayDataRealtime() { if (!this.currentSession) return; const now = /* @__PURE__ */ new Date(); const year = now.getFullYear(); const month = now.getMonth() + 1; const day = now.getDate(); const dayData = await this.persistence.updateDayDataRealtime( year, month, day, this.currentSession, this.currentTabRecord ); this.todayDuration = dayData.totalDuration; await this.persistence.recalculateMonthSummary(year, month, this.todayDuration); await this.persistence.recalculateYearSummary(year); } async handleTabSwitch(tabInfo) { if (!this.currentSession || this.isPaused) return; await this.updateCurrentTabDuration(); if (this.currentTabRecord && this.currentTabRecord.duration > 0) { this.currentSession.tabRecords.push({ ...this.currentTabRecord }); } const now = /* @__PURE__ */ new Date(); const timeStr = `${now.getHours().toString().padStart(2, "0")}:${now.getMinutes().toString().padStart(2, "0")}:${now.getSeconds().toString().padStart(2, "0")}`; this.currentTabRecord = { tabId: tabInfo.tabId, filePath: tabInfo.filePath, fileName: getFileNameFromPath(tabInfo.filePath), tabType: getTabTypeFromPath(tabInfo.filePath), duration: 0, focusedPeriods: [{ start: timeStr, end: timeStr }] }; await this.saveCurrentState(); } async handleEvent(event) { switch (event.type) { case "window-focus": if (!this.currentSession) { await this.startSession(); if (event.tabInfo) { await this.handleTabSwitch(event.tabInfo); } } else { await this.resumeSession(); await this.updateTodayDataRealtime(); } break; case "window-blur": await this.pauseSession(); await this.updateTodayDataRealtime(); break; case "app-quit": await this.endSession(); break; case "tab-switch": case "tab-open": if (!this.currentSession) { await this.startSession(); } if (event.tabInfo) { await this.handleTabSwitch(event.tabInfo); } await this.updateTodayDataRealtime(); break; case "tab-close": await this.updateCurrentTabDuration(); await this.updateTodayDataRealtime(); break; case "heartbeat": if (this.currentSession && !this.isPaused) { this.heartbeatService.updateHeartbeat(); await this.updateCurrentTabDuration(); await this.saveCurrentState(); await this.updateTodayDataRealtime(); } break; } } async getDayData(year, month, day) { return this.persistence.getDayData(year, month, day); } async getWeekData(startDate) { const result = []; for (let i = 0; i < 7; i++) { const date = new Date(startDate); date.setDate(date.getDate() + i); const data = await this.persistence.getDayData(date.getFullYear(), date.getMonth() + 1, date.getDate()); result.push(data); } return result; } async getMonthData(year, month) { return this.persistence.getMonthData(year, month); } async getYearData(year) { return this.persistence.getYearData(year); } getCurrentState() { return { isRunning: this.currentSession !== null, isPaused: this.isPaused, currentSession: this.currentSession, todayDuration: this.todayDuration, currentTabRecord: this.currentTabRecord }; } async getStats(year, month) { const now = /* @__PURE__ */ new Date(); const targetYear = year || now.getFullYear(); const targetMonth = month; let totalDuration = 0; let activeDays = 0; let longestDay = null; let longestSession = null; const tabDurations = {}; const tabTypeDurations = {}; if (targetMonth) { const monthData = await this.persistence.getMonthData(targetYear, targetMonth); totalDuration = monthData.monthlyTotal; activeDays = Object.values(monthData.days).filter((d) => d.totalDuration > 0).length; for (const [day, summary] of Object.entries(monthData.days)) { if (!longestDay || summary.totalDuration > longestDay.duration) { longestDay = { date: `${targetYear}-${targetMonth.toString().padStart(2, "0")}-${day}`, duration: summary.totalDuration }; } for (const tab of summary.topTabs || []) { tabDurations[tab.fileName] = (tabDurations[tab.fileName] || 0) + tab.duration; } } const dayData = await this.persistence.getDayData(targetYear, targetMonth, 1); for (const session of dayData.sessions) { for (const record of session.tabRecords) { const key = record.filePath || record.fileName; tabDurations[key] = (tabDurations[key] || 0) + record.duration; tabTypeDurations[record.tabType] = (tabTypeDurations[record.tabType] || 0) + record.duration; } } } else { const yearData = await this.persistence.getYearData(targetYear); totalDuration = yearData.yearlyTotal; activeDays = Object.values(yearData.months).reduce((sum, m) => sum + m.activeDays, 0); for (const [month2, summary] of Object.entries(yearData.months)) { if (!longestDay || summary.totalDuration > longestDay.duration) { longestDay = { date: `${targetYear}-${month2}`, duration: summary.totalDuration }; } } for (let m = 1; m <= 12; m++) { const monthStr = m.toString().padStart(2, "0"); const monthData = await this.persistence.getMonthData(targetYear, m); for (const dayData of Object.values(monthData.days)) { for (const tab of dayData.topTabs || []) { tabDurations[tab.fileName] = (tabDurations[tab.fileName] || 0) + tab.duration; } } } } return { totalDuration, activeDays, averageDaily: activeDays > 0 ? Math.floor(totalDuration / activeDays) : 0, longestDay, longestSession, topTabs: Object.entries(tabDurations).map(([fileName, duration]) => ({ fileName, duration, percentage: totalDuration > 0 ? Math.round(duration / totalDuration * 100) : 0 })).sort((a, b) => b.duration - a.duration).slice(0, 10), tabTypeDistribution: Object.entries(tabTypeDurations).map(([tabType, duration]) => ({ tabType, duration, percentage: totalDuration > 0 ? Math.round(duration / totalDuration * 100) : 0 })).sort((a, b) => b.duration - a.duration) }; } }; var _timeTrackerService = null; var getTimeTrackerService = () => { if (!_timeTrackerService) { throw new Error("TimeTrackerService \u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 initializeTimeTrackerService()"); } return _timeTrackerService; }; var initializeTimeTrackerService = async (config) => { if (_timeTrackerService) { return _timeTrackerService; } _timeTrackerService = await TimeTrackerService.create(config); return _timeTrackerService; }; var initializeTimeTrackerServiceWithDependencies = async (dependencies) => { if (_timeTrackerService) { return _timeTrackerService; } _timeTrackerService = await TimeTrackerService.createWithDependencies(dependencies); return _timeTrackerService; }; // api/modules/time-tracking/routes.ts import express from "express"; var createTimeTrackingRoutes = (deps) => { const router = express.Router(); const { timeTrackerService } = deps; router.get( "/current", asyncHandler(async (_req, res) => { const state = timeTrackerService.getCurrentState(); successResponse(res, { isRunning: state.isRunning, isPaused: state.isPaused, currentSession: state.currentSession ? { id: state.currentSession.id, startTime: state.currentSession.startTime, duration: state.currentSession.duration, currentTab: state.currentTabRecord ? { tabId: state.currentTabRecord.tabId, fileName: state.currentTabRecord.fileName, tabType: state.currentTabRecord.tabType } : null } : null, todayDuration: state.todayDuration }); }) ); router.post( "/event", asyncHandler(async (req, res) => { const event = req.body; await timeTrackerService.handleEvent(event); successResponse(res, null); }) ); router.get( "/day/:date", asyncHandler(async (req, res) => { const { date } = req.params; const [year, month, day] = date.split("-").map(Number); const data = await timeTrackerService.getDayData(year, month, day); const sessionsCount = data.sessions.length; const averageSessionDuration = sessionsCount > 0 ? Math.floor(data.totalDuration / sessionsCount) : 0; const longestSession = data.sessions.reduce((max, s) => s.duration > max ? s.duration : max, 0); const topTabs = Object.entries(data.tabSummary).map(([_, summary]) => ({ fileName: summary.fileName, duration: summary.totalDuration })).sort((a, b) => b.duration - a.duration).slice(0, 5); successResponse(res, { ...data, stats: { sessionsCount, averageSessionDuration, longestSession, topTabs } }); }) ); router.get( "/week/:startDate", asyncHandler(async (req, res) => { const { startDate } = req.params; const [year, month, day] = startDate.split("-").map(Number); const start = new Date(year, month - 1, day); const data = await timeTrackerService.getWeekData(start); const totalDuration = data.reduce((sum, d) => sum + d.totalDuration, 0); const activeDays = data.filter((d) => d.totalDuration > 0).length; successResponse(res, { days: data, totalDuration, activeDays, averageDaily: activeDays > 0 ? Math.floor(totalDuration / activeDays) : 0 }); }) ); router.get( "/month/:yearMonth", asyncHandler(async (req, res) => { const { yearMonth } = req.params; const [year, month] = yearMonth.split("-").map(Number); const data = await timeTrackerService.getMonthData(year, month); successResponse(res, data); }) ); router.get( "/year/:year", asyncHandler(async (req, res) => { const { year } = req.params; const data = await timeTrackerService.getYearData(parseInt(year)); successResponse(res, data); }) ); router.get( "/stats", asyncHandler(async (req, res) => { const year = req.query.year ? parseInt(req.query.year) : void 0; const month = req.query.month ? parseInt(req.query.month) : void 0; const stats = await timeTrackerService.getStats(year, month); successResponse(res, stats); }) ); return router; }; // api/modules/time-tracking/index.ts var createTimeTrackingModule = (moduleConfig = {}) => { let serviceInstance; return createApiModule(TIME_TRACKING_MODULE, { routes: (container) => { const timeTrackerService = container.getSync("timeTrackerService"); return createTimeTrackingRoutes({ timeTrackerService }); }, lifecycle: { onLoad: async (container) => { serviceInstance = await initializeTimeTrackerService(moduleConfig.config); container.register("timeTrackerService", () => serviceInstance); } } }); }; var time_tracking_default = createTimeTrackingModule; export { HeartbeatService, createHeartbeatService, SessionPersistenceService, createSessionPersistence, TimeTrackerService, getTimeTrackerService, initializeTimeTrackerService, initializeTimeTrackerServiceWithDependencies, createTimeTrackingRoutes, createTimeTrackingModule, time_tracking_default };