Files
XCDesktop/dist-api/chunk-QS2CMBFP.js

913 lines
33 KiB
JavaScript
Raw Normal View History

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
};